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_IDandASTROLIFT_CONTROLLER_URL;AGENT_CALLBACK_URLandASTROLIFT_CLUSTER_KEY, thealft_cb_callback key minted for this spawn;ASTROLIFT_MESSAGES_URLandASTROLIFT_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_URLandASTROLIFT_PAYLOAD_HASHfor the frozen Brief and dispatch payload;AGENT_SYSTEMandAGENT_PROMPT, which put the in-pod runner into one-shot mode; withoutAGENT_PROMPTthe runner idles as a listener;ASTROLIFT_SNAPSHOT_URL(VNC recipes),ASTROLIFT_MODEL_GATEWAY_KEY(gateway recipes) andASTROLIFT_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:
- an organization/app secret value in a configured secrets backend;
- a reference or bundle attached to an agent/environment; and
- 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:
agentselects the registered agent workload by local slug orguid:<uuid>(stored asagent_ref);environment_spec_slugoptionally overrides the agent's environment recipe;skillsadds local, catalogue, or organization-repository skill references (stored asskill_refs);promptadds instructions for this stage;output_keynames the stage result in the workflow's accumulated output map; andmax_attempts,back_edgeanditerationbound 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.