Skip to content

Agent architecture

Astrolift's agent support is part of the main control plane. It is not a separate product or a set of optional Django applications. Agent definitions, skills, environment recipes, tasks, workflows, secrets, and dispatch records share the platform's organization, project, permission, and audit boundaries.

This page describes the code that runs today. The public CLI and API reference remain the source of truth for command syntax and wire contracts.

The objects that make up an agent

An agent is assembled from several independently managed objects:

Object Purpose
RegisteredApp + Workload(kind="agent") Registered agent identity, source repository, image, run policy, and deployment metadata
AgentEnvironmentSpec Reusable image/runtime recipe, tool preset, install policy, VNC choice, managed-model choice, environment variables, and secret references
Skill Versioned instructions and dependency metadata; global skills are readable by every organization and organization skills remain tenant-scoped
ToolDef Callable tool metadata for a skill: Python function, HTTP endpoint, or MCP server
Brief Immutable, content-hashed snapshot assembled for one definition or dispatch
AgentTask One dispatch and its lifecycle, runtime location, callback identity, result, failure, and telemetry pointers
AgentRun Fleet/run history associated with a workload; newer dispatches link it directly to their AgentTask
DispatcherInstance Registered execution endpoint and its cloud, region, backend, capabilities, and heartbeat
AgentBox One live instance of a run_mode = "persistent" agent: a warm pod a person attaches to (astro box)
AgentMessage, AgentMessageRecipient, AgentMessageReply What a run tells people and what they answer; the agent inbox
AgentTaskInputMessage A queued follow-up prompt the runner consumes at its next turn boundary, in order, at most once
AgentTaskArtifact Evidence attached to a result: JUnit, coverage, conformance JSON or another named text file
AgentTaskEvent, AgentInteraction Structured turn/terminal events and the append-only log of control-plane-visible interactions
AgentDispatchQuarantine A control-plane hold on one agent or environment spec that refuses new dispatches until cleared
AgentTaskCallbackPolicy, AgentTaskCompletionCallback Allowed callback hosts and the encrypted outbox for signed completion callbacks
AgentConversationTransfer A managed IDE conversation moving into a paused Astrolift session, bound to its uploaded envelope
WorkflowSchedule, WorkflowWebhook Cron and secret-authenticated inbound triggers for workflows and agents

This split is intentional. Updating a skill or environment recipe does not rewrite historical tasks: a task uses a frozen Brief and stores the environment recipe and source reference selected for that run.

Repository definitions

The ordinary source of an agent definition is astrolift.toml. A repository may contain one agent at its root or many independently registered agents below different paths. An environment spec's config_manifest_path selects either a particular manifest or a directory whose astrolift.toml is authoritative.

Registration turns an agent workload into database objects and resolves its skill and tool references. Repository-owned files may supply prompts, scripts, and other assets without pasting them into a single TOML string; path resolution is constrained to the selected manifest tree. Imported skill repositories are represented by OrgSkillRepo and use a source connection for private GitHub access.

The declarative manifest is the desired configuration. Secret values are not declarative repository content. Manifests and environment specs contain secret references; the dispatcher resolves values at launch.

Brief assembly

A Brief is the hand-off between definition and execution. Assembly combines:

  • the registered workload and source manifest snapshot;
  • resolved environment variables and dispatch input;
  • selected skills and their versions;
  • tool definitions and install metadata;
  • secret references, never plaintext secret values; and
  • organization, project, workflow, and source context.

The canonical JSON payload is SHA-256 addressed (content_hash) and stored under an object-store key (storage_key). A READY Brief can therefore be reused when its content is identical. A change creates a new Brief rather than mutating the package used by an existing task. Briefs can also expire or be revoked.

Dispatch lifecycle

The platform creates an AgentTask, prepares its Brief, and starts a durable Temporal dispatch workflow. Its states are:

DRAFT -> QUEUED -> PROVISIONING -> RUNNING
  |        |            |            |
  +--------+------------+------------+-> CANCELLED
           +------------+------------+-> FAILED
                        +------------+-> TIMED_OUT
                                     +-> COMPLETED

The managed Kubernetes path creates a per-dispatch Job in an organization namespace. It resolves the target tenant cluster, materializes launch secrets in a Kubernetes Secret, injects task and callback metadata, and polls the Job until a terminal state. The terminal path cleans up the per-task secret. A configured timeout becomes the Job's activeDeadlineSeconds as well as a control-plane timeout.

Cancellation is an execution operation, not merely a database update. For a provisioning or running Kubernetes task, Astrolift deletes the Job and its pod before marking the task cancelled. Operators can use astro agent cancel for runaway tasks; see the operator runbooks.

A quarantine is the other hold. AgentDispatchQuarantine targets one agent or one environment spec; while it is live, task creation refuses with the recorded reason, policy ID and evidence URL. Quarantines are written by signed Zentinelle enforcement actions (AgentEnforcementAction, verified against a single-use nonce), and clearAgentQuarantine lifts one under agent.dispatch. Enforcement can also stop a run, revoke its model-gateway key or hold a pending gate, and the affected owner is told through the inbox.

K8sJobSpawner (backend k8s_job) is the managed production spawner. LocalDockerSpawner (local_docker) exists for development. DispatcherInstance and the /api/dispatch/v1/ controller endpoints support registered external dispatch processes, but the managed Temporal activity currently selects the Kubernetes Job spawner for cluster-backed tasks. Do not infer that every backend value on DispatcherInstance is implemented by that managed activity.

Container contract and callbacks

The injector supplies the task's runtime contract, including task/organization identity, input, prompt and Brief context. A valid externally reachable PLATFORM_API_URL is required to construct the URLs below; the spawn refuses otherwise. The pod receives:

  • ASTROLIFT_TASK_ID and ASTROLIFT_CONTROLLER_URL;
  • AGENT_CALLBACK_URL and ASTROLIFT_CLUSTER_KEY, the alft_cb_ callback key minted for this spawn;
  • ASTROLIFT_MESSAGES_URL and ASTROLIFT_RUN_MCP_URL, the run's message endpoints (REST and its own MCP server) authorized by the same key;
  • ASTROLIFT_BRIEF_ID, ASTROLIFT_BRIEF_HASH, ASTROLIFT_PAYLOAD_URL and ASTROLIFT_PAYLOAD_HASH for the frozen Brief and dispatch payload;
  • AGENT_SYSTEM and AGENT_PROMPT, which put the in-pod runner into one-shot mode; without AGENT_PROMPT the runner idles as a listener;
  • ASTROLIFT_SNAPSHOT_URL (VNC recipes), ASTROLIFT_MODEL_GATEWAY_KEY (gateway recipes) and ASTROLIFT_CONVERSATION_TRANSFER_URL (transfers) when those features apply.

The callback key is rotated on every spawn and task-scoped; only its SHA-256 hash is stored on the AgentTask. A terminal transition clears that hash. The agent callback endpoint persists the result, artifacts and interactions and then advances the task. Tooling must not report a callback as successful if no callback URL was injected; that is a configuration failure, not a successful no-op.

The controller REST surface is mounted by the main Django application at /api/dispatch/v1/: register/, heartbeat/, fleet/, runtimes/ and tasks/ for registered dispatchers, and per task agents/<task-id>/checkin/, callback/, messages/, mcp/, brains/ (fresh 15-minute read tokens for the semantic layers the run reads), transfer/, plus tasks/<task-id>/logs/, meter/ and status/. It may be used by a separately deployed dispatcher, but it does not require a separate control-plane service in the standard deployment.

Signed completion callbacks to an external HTTPS receiver are a separate, outbound contract (agent_task.finished, v1= HMAC, 24-hour retry window); see completion callbacks.

Logs, terminal, and VNC

Kubernetes pod stdout/stderr is the source used by astro agent logs. A terminal or noVNC session is a separate stream. An image that directs only its Xvnc supervisor to stdout can therefore show Xvnc messages in pod logs while an attached terminal displays application output. Agent images should send the actual runtime's stdout and stderr to the container streams as well as any interactive terminal.

When an environment recipe enables VNC, the task records a relay URL after it enters RUNNING. The ASGI application relays raw RFB traffic at /app/vnc/<task-guid> and can expose the latest uploaded JPEG snapshot: the -vnc image uploads a frame to the presigned ASTROLIFT_SNAPSHOT_URL under snapshots/<task-guid>/latest.jpg in the install's blob store, and the gallery reads it back with a 15-minute presigned URL. The old HTTP/WSGI VNC proxy no longer exists.

Two more WebSocket routes share the same ASGI handshake authentication: /app/exec/ for container exec and /app/ahp for the agent host protocol, a JSON-RPC transport the IDE uses to attach to running tasks and boxes, open terminals and receive catalog notifications (256 KiB frames).

Environments, tools, and managed models

AgentEnvironmentSpec.image_tag takes precedence over its runtime catalogue name. When no explicit image is supplied, the runtime catalogue resolves a canonical Astrolift agent image; an empty runtime falls back to the workload's image.

The catalogue names are claude, codex, opencode, mistral, deepseek, claude-code, codex-cli, goose, calliope-cli and base, each published as docker.io/calliopeai/astrolift-agent-<name> with a -vnc variant composed at spawn. ASTROLIFT_AGENT_REGISTRY and ASTROLIFT_AGENT_RUNTIME_TAG point the resolver at a mirror or a pinned tag. The aider and openhands runtimes are retired because they depended on LiteLLM; selecting them by name or by image is refused at spec save, import, registration and dispatch.

The recipe also controls agent_type (claude or codex), tool_preset, allow_install, VNC, managed_model, model_gateway, run_as_non_root, box_workspace, GPU requests (gpu, gpu_type, mig_profile), env_vars, secret_refs and the config repository, branch and manifest path. These fields are launch inputs, not documentation-only metadata. Tool definitions can bind to specific agent runtimes, declare command and package requirements, and indicate whether they are already in the image. Install-at-run behavior still depends on the selected image and policy; allowing installation does not grant arbitrary platform permissions.

With managed_model=true, the cluster driver supplies cloud-native model identity and provider environment instead of requiring a long-lived model API key. AWS uses Bedrock and GCP uses Vertex. Model identifiers should remain operator-configurable because cloud providers can retire models independently of IAM policy changes.

With model_gateway=true, the pod's model traffic goes through the Zentinelle gateway running in the pod's own cluster. Every run gets its own gateway key (ASTROLIFT_MODEL_GATEWAY_KEY), minted with the organization's Zentinelle connection for that run alone and revoked when it ends; the gateway swaps in the provider key, which never enters the pod. Opting in makes the gateway required: a missing install flag, connection, cluster gateway or an incapable runtime refuses the run, and nothing falls back to provider keys. Provider credential and base-URL environment names are stripped from the spec, bindings, bundles and Brief, and other names that would point a runtime at a provider refuse the run.

Agent pods carry a sandbox baseline: no default ServiceAccount token mount and bounded resources, with an optional non-root mode (run_as_non_root) that runs as the images' agent user with every capability dropped and therefore cannot install packages at boot. An operator can also enable the agent network fence (AGENT_NETWORK_FENCE): one NetworkPolicy per agent namespace that blocks inbound pod traffic and private or link-local egress (including cloud metadata) while allowing cluster DNS and the public internet, with AGENT_EGRESS_ALLOW_CIDRS for private git servers or databases. Gateway-mode pods get a stricter policy whatever the fence setting: cluster DNS, the gateway and AGENT_GATEWAY_EGRESS_ALLOW_CIDRS only.

Messages, steering input, and boxes

A run talks to people through its own endpoints: POST to ASTROLIFT_MESSAGES_URL or the astrolift_message_user tool on its MCP server sends an update, question, approval, result or alert to the people who can read the agent (by default the person who launched it); the platform also posts messages about a run (finished, failed, a waiting gate, a policy alert). Replies return to the run through its input channel, its gate or a queued wake. People read and answer in the dashboard, with astro inbox, or through the GraphQL fields listed in the inbox guide.

Steering input (AgentTaskInputMessage) is a queued, in-order, at-most-once channel: an operator enqueues a follow-up prompt and the runner consumes up to 20 messages at its next turn boundary. There is no live stream into the pod. A terminal callback may attach AgentTaskArtifact rows (JUnit, coverage, conformance JSON, other text) that are validated as a set before any is written.

An agent box (AgentBox) is the persistent counterpart of a task: a warm pod holding a tmux session that a person attaches to and that is reaped when idle. astro box ensure is idempotent per (organization, owner, environment spec), so a second request attaches to the running box rather than starting a rival.

Secrets

There are three related layers:

  1. an organization/app secret value in a configured secrets backend;
  2. a reference or bundle attached to an agent/environment; and
  3. the short-lived Kubernetes Secret materialized for a task.

Secret access is permission-checked and API-token scopes are an additional ceiling over a user's RBAC grants. Being an organization administrator does not make a read-only device token capable of secret.write or agent.dispatch. Use a token carrying the required narrow scope, and remember that revealing a value also depends on the backing provider's reveal capability.

Agent workflows

WorkflowDefinition is the reusable workflow template and WorkflowStage is its ordered execution plan. Agent pipelines normally leave model_label blank: that field is a legacy Django content-object target, not an LLM model or an agent selector.

Stage kinds are agent_dispatch, human_gate, checkpoint, aggregation, workflow (a nested definition), collection (serial fan-out over items), format_record and github_checks; on_failure is fail, retry, skip or escalate. For an agent stage:

  • agent selects the registered agent workload by local slug or guid:<uuid> (stored as agent_ref);
  • environment_spec_slug optionally overrides the agent's environment recipe;
  • skills adds local, catalogue, or organization-repository skill references (stored as skill_refs);
  • prompt adds instructions for this stage;
  • output_key names the stage result in the workflow's accumulated output map; and
  • max_attempts, back_edge and iteration bound review loops and serial collections; see bounded workflows.

Ordered stages are the executable pipeline. The legacy states and transitions JSON fields still serve content-object state machines; they do not need to be invented for a declarative agent pipeline. A chained workflow passes each stage's result into the next stage and also stores it under the unique output_key. Human-gate stages pause the Temporal workflow until approval or rejection.

Workflow manifests use a [workflow] table followed by ordered [[stage]] tables. They can be bundled in Astrolift's catalogue or reconciled from a connected repository. See the generated CLI/config reference for the exact schema accepted by the installed release.

Tenant and trust boundaries

  • Definitions, environment recipes, tasks, dispatchers, secret bindings, and organization skills are organization-scoped.
  • Platform-global skills and workflow templates are readable by tenants but writable only by platform operators.
  • Dispatch lookup verifies workload kind, run family, organization, and permissions before preparing a task, and refuses a quarantined agent or environment spec.
  • Retired runtimes are refused wherever a spec or image is accepted, not only at spawn.
  • Repository sync is bound to the connected source identity and configured path.
  • Callback, deploy, API, and MCP tokens each have explicit scopes. A bearer token never inherits permissions outside its own scope ceiling.
  • Values stay in a secrets backend until launch and are not persisted in a Brief or repository manifest.

These boundaries must hold on every entry point: dashboard, CLI, GraphQL, REST, MCP, webhooks, schedulers, and workflow activities.