DEV Community

Cover image for Spacecraft Don't Trust Their Own Code. Neither Should Your AI Agent.
klement Gunndu
klement Gunndu

Posted on

Spacecraft Don't Trust Their Own Code. Neither Should Your AI Agent.

A satellite 400 million kilometers from Earth makes a decision that kills the mission. Nobody on the ground can stop it. The signal takes 22 minutes to arrive — and 22 minutes for the correction to travel back.

This is not hypothetical. This is why every spacecraft autonomous system uses a strict hierarchy of authorization levels, where no subsystem acts beyond its granted authority.

Your AI agent has the same problem. It calls tools, accesses databases, sends emails, and modifies files. Most developers give it full access on day one and hope for the best. Space engineers would never do that.

Here are 4 authorization patterns from spacecraft autonomy systems — with working Python code you can use today.

Why Space Systems Don't Trust Themselves

The European Cooperation for Space Standardization (ECSS) defines four autonomy levels in ECSS-E-ST-70-11C, the standard for space segment operability. As of the October 2025 revision, these levels are:

  • E1 — Ground control: Every action requires a command from Earth.
  • E2 — Scheduled operations: The spacecraft executes pre-loaded command sequences on a timer.
  • E3 — Event-based autonomy: Onboard procedures trigger based on sensor data, without waiting for ground.
  • E4 — Goal-based autonomy: The spacecraft plans and replans its own actions to achieve high-level objectives.

Each level grants more authority. And each level requires more safeguards.

NASA's Multi-level Autonomous Piloting Systems (MAPS) project at Armstrong Flight Research Center takes this further: deterministic systems sit at a higher authority layer and can override machine learning subsystems at any time. The ML layer operates within bounds set by the deterministic layer — it never gets to override its supervisor. This architecture allows lower certification requirements for the ML components because a trusted system always has the final say.

The same principle applies to AI agents. Here is how to implement it.

Pattern 1: Tiered Authority With IntEnum

The foundation of spacecraft authorization is a strict ordering of trust levels. Python's IntEnum from the standard library gives you the same thing — comparable, ordered authority levels.

from enum import IntEnum
from dataclasses import dataclass, field


class AuthorityLevel(IntEnum):
    """Spacecraft-inspired authority levels for AI agents.

    E1-E4 map to ECSS-E-ST-70-11C autonomy levels.
    """
    E1_GROUND_CONTROL = 1   # Every action needs approval
    E2_SCHEDULED = 2        # Pre-approved action sequences
    E3_EVENT_BASED = 3      # React to events within bounds
    E4_GOAL_BASED = 4       # Plan and act autonomously


@dataclass
class AgentPermission:
    """A specific permission granted at a specific authority level."""
    name: str
    min_level: AuthorityLevel
    description: str


@dataclass
class AgentAuthority:
    """The authority configuration for an agent."""
    level: AuthorityLevel
    permissions: list[AgentPermission] = field(default_factory=list)
    max_actions_per_minute: int = 10
    requires_human_approval: bool = True

    def can_perform(self, permission: AgentPermission) -> bool:
        """Check if this authority level allows the given permission."""
        return self.level >= permission.min_level
Enter fullscreen mode Exit fullscreen mode

IntEnum is a subclass of both int and Enum in the Python standard library. Members can be compared with standard integer operators (>=, <), which makes authority level checks a single comparison instead of a lookup table.

Notice requires_human_approval defaults to True. In spacecraft terms, you start at E1 and earn your way up. Agents start supervised.

Pattern 2: Permission Decorators That Enforce Before Execution

Spacecraft fault protection does not ask the autonomous system if it is allowed to do something. A separate, deterministic layer checks authority before the command reaches execution. In Python, this is a decorator.

import functools
import logging
from typing import Callable, Any

logger = logging.getLogger(__name__)


def requires_authority(permission: AgentPermission) -> Callable:
    """Decorator that enforces authority checks before execution.

    Mirrors spacecraft fault protection: a deterministic gate
    checks authority BEFORE the action runs. The action itself
    never decides whether it is allowed.
    """
    def decorator(func: Callable) -> Callable:
        @functools.wraps(func)
        def wrapper(self, *args: Any, **kwargs: Any) -> Any:
            authority: AgentAuthority = getattr(self, "authority", None)
            if authority is None:
                raise PermissionError(
                    f"No authority configured for {self.__class__.__name__}"
                )

            if not authority.can_perform(permission):
                logger.warning(
                    "BLOCKED: %s requires %s, agent has %s",
                    permission.name,
                    permission.min_level.name,
                    authority.level.name,
                )
                raise PermissionError(
                    f"Action '{permission.name}' requires "
                    f"{permission.min_level.name}, "
                    f"agent has {authority.level.name}"
                )

            logger.info(
                "AUTHORIZED: %s (level %s >= %s)",
                permission.name,
                authority.level.name,
                permission.min_level.name,
            )
            return func(self, *args, **kwargs)
        return wrapper
    return decorator
Enter fullscreen mode Exit fullscreen mode

The key insight: functools.wraps preserves the original function's __name__, __doc__, and __module__ attributes on the wrapper — critical for debugging and introspection in production (Python functools docs).

The agent never checks its own permissions. The decorator does it. The agent's code does not even run if the authority check fails. This is the same separation of concerns that MAPS uses: the deterministic safety layer (decorator) has higher authority than the ML layer (agent logic).

Pattern 3: Graduated Authority Escalation

No spacecraft starts at E4. A new satellite begins at E1 (full ground control) and earns autonomy as mission controllers verify each subsystem. The same principle applies to AI agents — start restricted, widen authority as you verify behavior.

# Define the permission registry
READ_DATA = AgentPermission(
    name="read_data",
    min_level=AuthorityLevel.E1_GROUND_CONTROL,
    description="Read from approved data sources",
)

CALL_TOOL = AgentPermission(
    name="call_tool",
    min_level=AuthorityLevel.E2_SCHEDULED,
    description="Execute pre-approved tool calls",
)

REACT_TO_EVENT = AgentPermission(
    name="react_to_event",
    min_level=AuthorityLevel.E3_EVENT_BASED,
    description="Take action based on observed events",
)

MODIFY_PLAN = AgentPermission(
    name="modify_plan",
    min_level=AuthorityLevel.E4_GOAL_BASED,
    description="Replan and modify execution strategy",
)


class ResearchAgent:
    """An agent with spacecraft-grade authorization."""

    def __init__(self, authority: AgentAuthority) -> None:
        self.authority = authority
        self.action_log: list[dict] = []

    @requires_authority(READ_DATA)
    def fetch_documents(self, query: str) -> list[str]:
        """E1: Any agent can read approved sources."""
        self.action_log.append({"action": "fetch", "query": query})
        return [f"Result for: {query}"]

    @requires_authority(CALL_TOOL)
    def run_analysis(self, data: list[str]) -> dict:
        """E2: Only scheduled-authority agents can call tools."""
        self.action_log.append({"action": "analyze", "items": len(data)})
        return {"summary": f"Analyzed {len(data)} documents"}

    @requires_authority(REACT_TO_EVENT)
    def handle_new_data(self, event: dict) -> str:
        """E3: React to events without waiting for approval."""
        self.action_log.append({"action": "react", "event": event})
        return f"Handled event: {event.get('type', 'unknown')}"

    @requires_authority(MODIFY_PLAN)
    def replan_strategy(self, goal: str) -> dict:
        """E4: Full autonomy to replan. Highest trust required."""
        self.action_log.append({"action": "replan", "goal": goal})
        return {"new_plan": f"Revised strategy for: {goal}"}
Enter fullscreen mode Exit fullscreen mode

Here is what graduated escalation looks like in practice:

# Start at E1 — read-only, fully supervised
junior_agent = ResearchAgent(
    authority=AgentAuthority(
        level=AuthorityLevel.E1_GROUND_CONTROL,
        max_actions_per_minute=5,
        requires_human_approval=True,
    )
)

# This works — E1 agents can read
results = junior_agent.fetch_documents("spacecraft autonomy")

# This raises PermissionError — E1 agents cannot call tools
try:
    junior_agent.run_analysis(results)
except PermissionError as e:
    print(f"Blocked: {e}")
    # Blocked: Action 'call_tool' requires E2_SCHEDULED,
    #          agent has E1_GROUND_CONTROL


# After verification, promote to E3
verified_agent = ResearchAgent(
    authority=AgentAuthority(
        level=AuthorityLevel.E3_EVENT_BASED,
        max_actions_per_minute=20,
        requires_human_approval=False,
    )
)

# Now it can read, call tools, AND react to events
results = verified_agent.fetch_documents("fault tolerance")
analysis = verified_agent.run_analysis(results)
response = verified_agent.handle_new_data({"type": "new_paper"})

# But it still cannot replan — that requires E4
try:
    verified_agent.replan_strategy("find better sources")
except PermissionError as e:
    print(f"Still blocked: {e}")
    # Still blocked: Action 'modify_plan' requires E4_GOAL_BASED,
    #                agent has E3_EVENT_BASED
Enter fullscreen mode Exit fullscreen mode

Every action is logged in action_log. In spacecraft operations, telemetry is non-negotiable — you log every command, every response, every state change. The same discipline applies here.

Pattern 4: The Deterministic Override Layer

The most critical pattern from NASA's MAPS architecture: a deterministic system with higher authority can always override the autonomous system. The ML model never overrides the safety layer. Never.

from dataclasses import dataclass, field
from typing import Any


@dataclass
class SafetyBound:
    """A hard limit that no agent can exceed, regardless of authority."""
    name: str
    check: Callable[..., bool]
    message: str


@dataclass
class DeterministicSafetyLayer:
    """The spacecraft's fault protection equivalent.

    This layer sits ABOVE the agent. It checks every action
    against hard safety bounds before the agent executes.
    No agent — not even E4 — can bypass this layer.
    """
    bounds: list[SafetyBound] = field(default_factory=list)
    violations: list[dict] = field(default_factory=list)

    def check_action(self, action_name: str, **context: Any) -> bool:
        """Check all safety bounds. Any failure blocks the action."""
        for bound in self.bounds:
            if not bound.check(**context):
                violation = {
                    "action": action_name,
                    "bound": bound.name,
                    "message": bound.message,
                    "context": context,
                }
                self.violations.append(violation)
                logger.error(
                    "SAFETY VIOLATION: %s — %s",
                    bound.name,
                    bound.message,
                )
                return False
        return True


# Define hard safety bounds
MAX_COST_PER_CALL = 0.50  # dollars
MAX_TOKENS_PER_REQUEST = 8000

safety = DeterministicSafetyLayer(bounds=[
    SafetyBound(
        name="cost_limit",
        check=lambda cost=0, **_: cost <= MAX_COST_PER_CALL,
        message=f"Cost exceeds ${MAX_COST_PER_CALL} per call",
    ),
    SafetyBound(
        name="token_limit",
        check=lambda tokens=0, **_: tokens <= MAX_TOKENS_PER_REQUEST,
        message=f"Request exceeds {MAX_TOKENS_PER_REQUEST} tokens",
    ),
    SafetyBound(
        name="no_external_writes",
        check=lambda target="", **_: not target.startswith("http"),
        message="External write operations are blocked",
    ),
])


# Usage: check BEFORE every agent action
if safety.check_action("run_analysis", cost=0.03, tokens=2000):
    result = verified_agent.run_analysis(results)
else:
    print("Action blocked by safety layer")
    print(f"Violations: {safety.violations}")
Enter fullscreen mode Exit fullscreen mode

The safety layer and the authority layer are separate systems. An agent with E4 authority can replan its strategy — but the safety layer still blocks it from exceeding the cost limit or writing to external systems. Authority governs what the agent is allowed to do. Safety bounds govern what no system is allowed to do.

This is exactly how MAPS works: the deterministic layer has absolute authority over the ML layer. The ML model's "autonomy" only applies within the bounds the deterministic layer sets.

Putting It Together

The full architecture has three layers, matching spacecraft design:

┌─────────────────────────────────────┐
│  DETERMINISTIC SAFETY LAYER         │
│  Hard bounds. No exceptions.        │
│  (Cost, tokens, external access)    │
├─────────────────────────────────────┤
│  AUTHORITY LAYER                    │
│  Permission checks via decorators.  │
│  (E1 → E2 → E3 → E4 escalation)   │
├─────────────────────────────────────┤
│  AGENT LOGIC                        │
│  The actual ML/LLM work.           │
│  Only runs if both layers pass.     │
└─────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Every action passes through two gates before execution:

  1. Safety bounds — Is this action within hard limits? (Deterministic, cannot be overridden)
  2. Authority check — Does this agent have permission? (Based on earned trust level)

If either gate fails, the action does not execute. The agent's code never runs. This is the same architecture that keeps a spacecraft from executing a thruster burn that would send it into the Sun.

What This Changes

Most AI agent frameworks give the agent a list of tools and let it call whatever it wants. That is E4 authority with no safety layer — the most dangerous configuration in spacecraft terms.

Apply the space systems approach:

  1. Start at E1. New agents read data and nothing else.
  2. Earn E2. After testing, allow pre-approved tool calls.
  3. Earn E3. After production verification, allow event-based reactions.
  4. E4 is rare. Full autonomy requires extensive verification and a deterministic override layer that cannot be bypassed.
  5. Safety bounds apply at every level. Even E4 agents operate within hard limits.

The spacecraft that made it to Mars did not get there by trusting its own code. It got there by having a system that verified every action before execution.

Your AI agent deserves the same discipline.


Follow @klement_gunndu for more AI architecture content. We're building in public.

Top comments (2)

Collapse
 
fabrizio_corpora_2409aa12 profile image
Fabrizio Corpora

I completely agree. It is crucial to have control over the agents, especially considering they often have issues with hallucinations.

Collapse
 
klement_gunndu profile image
klement Gunndu

Hallucination control is exactly why output validators matter more than input guardrails — by the time you catch a hallucinated API call downstream, the damage is already done. Treating every agent output as untrusted until verified, the same way spacecraft treat sensor readings, is the most reliable pattern.