Agent Package and repository reference¶
Agent notes
There is no local validator for an agent manifest; astro agent
register-repo is the check, and astro agent workloads ls --json shows
what registered. astro agent inspect <task-guid> --json shows a run's
package, brains and status. Parsers in astrolift-app:
backend/astrolift_manifest/parser.py (workloads, brief, skills),
backend/astrolift_agents/services/agent_package.py ([package],
[environment], [secrets], [workspace] delivery) and
backend/astrolift_agents/services/brief_assembler.py (config-repo
includes). Legacy: skill_slug; the aider and openhands runtimes are
retired. Frequent mistakes: a secret ref without the organization GUID;
[environment] setting a dispatcher-owned variable; a skill ref with two
@; [package] include paths that climb out of the manifest directory.
An Astrolift agent is exactly one Workload(kind = "agent") plus an immutable
Agent Package snapshot. The package composes a brief, ordered skills, resolved
tools, runtime image, environment references, execution policy, and a chrooted
source payload.
Repository layouts¶
Single agent:
Monorepo:
agents/triage/astrolift.toml
agents/triage/brief/README.md
agents/triage/skills/emr/SKILL.md
agents/reporter/astrolift.toml
workflows/triage.toml
astrolift.agents.toml
shared/schemas/finding.json
Without a federation file, discovery accepts a root astrolift.toml and exact
agents/<slug>/astrolift.toml paths. Deeper arbitrary manifest paths are not
auto-discovered.
Native agent manifest¶
astrolift_version = 1
name = "emr-triage"
brief = "brief/README.md"
skills = [
{ emr = "skills/emr" },
"pr-review@1.2.0",
"operations/runbooks/incident-triage@main",
]
[[workloads]]
name = "emr-triage"
kind = "agent"
run_family = "task"
max_retries = 3
tool_timeout_seconds = 1200
result_ttl_hours = 168
[[workloads.containers]]
name = "agent"
is_primary = true
image_ref = "docker.io/calliopeai/astrolift-agent-claude-code:latest"
[environment]
LOG_LEVEL = "info"
[secrets]
ANTHROPIC_API_KEY = "secret://agents/<organization-guid>/anthropic"
[package]
root = "."
include = ["astrolift.toml", "brief/**", "skills/**", "scripts/**"]
exclude = ["**/*.tmp", "fixtures/**"]
executables = ["scripts/*.sh"]
[[package.shared]]
source = "shared/schemas"
mount = "shared/schemas"
Replace <organization-guid> with the id from astro org current --json for
the owning organization. Agent secret references must stay under that GUID's
secret://agents/<organization-guid>/ namespace; an organization slug or an
unscoped reference is rejected. The declaration under [secrets] is a
reference, never plaintext: a bare string, or a table with exactly one of
secret_name or uri. The dispatcher owns callback, task, payload, and
workspace variables; a [environment] entry that names one is refused at
registration.
The runtime image is the primary container's image_ref, unless the
environment spec the agent is dispatched with pins an --image-tag or names
a --runtime from the catalog. The watchable -vnc variant is never named;
the dispatcher composes it when the spec enables VNC.
[workspace]¶
[[workspace.repos]]
url = "https://github.com/acme/orders-api.git"
ref = "main"
path = "orders-api"
depth = 1
token_env = "GITHUB_TOKEN"
[workspace.deps]
python = ["requirements.txt"]
node = ["orders-api/package.json"]
The runner reads [workspace] from the astrolift.toml inside the delivered
payload and sets the workspace up after extracting it and before the driver
starts: [[workspace.repos]] clones repositories (url, optional ref,
path, depth and token_env, the environment variable holding a private
repository's token), [workspace.deps] installs Python dependencies into a
virtualenv at <workspace>/.venv and Node dependencies into each package's
own node_modules. One invalid entry, failed clone or failed install stops
the boot with an error naming the entry. Declaring [workspace] requires a
payload, the same as [package]; an environment spec with --box-workspace
runs the same setup before an interactive box session.
Briefs and skills¶
brief points to a Markdown README. Relative links to sibling files inside the
brief directory become immutable context files. Links outside that directory
are ignored.
Skill references have three forms:
| Form | Source |
|---|---|
{ local_name = "skills/path" } or "./skills/path" |
This agent's package root |
"skill-name@version" |
Built-in catalogue |
"repo-alias/path/to/skill@ref" |
Registered organization skill repo |
Each local skill is an agentskills.io directory with SKILL.md. Its YAML
frontmatter requires name and description; the Markdown body supplies
instructions. scripts/, references/, and assets/ are packaged with it.
The resolved skill order is preserved. Duplicate tool slugs are de-duplicated. Harness runtimes receive the brief, skill instructions, and command-backed tool contract as one composed system prompt.
Package source boundary¶
Declaring [package] opts into immutable runtime files. Paths are relative to
the manifest directory, POSIX-only, and may not be absolute or contain ...
rootdefines the virtual chroot.includedefaults to**.excludedefaults to common VCS/cache/dependency paths.executablesadds executable bits to matched files.[[package.shared]]is the only way to mount a repo-root path across the package boundary;mountmust remain inside the resulting package.
The control plane fetches a byte-preserving source archive, filters it, hashes the result, stores it, and supplies a presigned payload URL. Symlinks, path escapes, duplicate destinations, oversized archives, and an empty slice fail registration instead of silently producing a partial agent.
Modular brief/config manifests¶
Environment specs may select a config repo and manifest_path. That format can
split large prompts and assets across files:
include = ["base/runtime.toml", "skills/emr.toml"]
[skills.triage]
system_prompt_file = "prompts/triage.md"
tools = ["jira-search", "jira-create"]
files = ["references/severity.md"]
scripts = ["scripts/collect.sh"]
binaries = ["bin/redactor"]
[assets]
files = ["schemas/finding.json"]
[environment]
tool_preset = "dev+cloud"
allow_install = false
LOG_LEVEL = "info"
[secrets]
JIRA_TOKEN = { secret_name = "secret://agents/<organization-guid>/jira" }
Includes are relative to the including file, loaded in order, and recursively deep-merged; later includes and then the main file win. Cycles, more than 16 levels, missing files, non-UTF-8 prompts, and path escapes fail assembly. Scripts and binaries are marked executable. If files are requested but no blob store can deliver the payload, dispatch fails rather than reporting success.
Every [skills.<slug>] table is composed. The stored skill_slug value is a
first-skill compatibility alias for older runtimes; do not author it as a
top-level selector. Workflow stages add skills with their skills = [...]
array, which becomes ordered skill_refs at runtime.
Explicit federated bundle¶
astrolift.agents.toml selects a controlled subset of a monorepo:
schema = "astrolift.agent.federation/v1"
name = "support-agents"
include = ["agents/*/astrolift.toml"]
exclude = ["agents/experimental-*/astrolift.toml"]
auto_register_new = false
[[agents]]
manifest = "agents/triage/astrolift.toml"
alias = "triage"
[[agents]]
manifest = "agents/reporter/astrolift.toml"
enabled = false
When present, the federation file replaces legacy discovery selection.
auto_register_new adds matching manifests not explicitly listed. Exclusions
still win. Every alias must be unique.
Register and run¶
The full walkthrough, including the secret value, is
Set up and dispatch an agent. owner/repo is the
pushed source repository; <guid> is the owning project's id from
astro project list --json; emr-triage is the manifest's name.
astro agent register-repo owner/repo --project-id <guid> --ref main \
--manifest-path agents/triage/astrolift.toml
astro agent env-spec upsert triage-prod \
--agent-type claude --runtime claude-code --vnc \
--config-repo owner/repo \
--manifest-path agents/triage/astrolift.toml
astro agent dispatch emr-triage --env-spec triage-prod --tail
astro agent dispatch emr-triage --env-spec triage-prod --brain handbook --input @payload.json
register-repo prints one Agent <slug> (workload <slug>, <state>) line per
manifest, with any notes beneath it; register failed: <reason> names the
manifest key or file at fault. env-spec upsert prints Created env-spec
triage-prod in org <org-slug> or Updated env-spec triage-prod; a secret ref
answered with is outside this organization's agent secret namespace needs the
organization GUID from astro org current --json in place of a slug. dispatch
prints AgentTask ID: and Status:, then with --wait or --tail each status
change and Final status: completed; failed, cancelled and timed_out are
the other terminal states, and exit code 1 means the dispatch or run failed.
Repo sync is idempotent by source repo and manifest path. Added and changed agents are reconciled. Removing a source manifest does not automatically delete the registered agent; teardown is explicit so audit history is retained.
--runtime names a catalog runtime, published as
docker.io/calliopeai/astrolift-agent-<name>:
| Runtime | Harness |
|---|---|
claude, claude-code |
Claude Code |
codex, codex-cli |
Codex |
opencode |
OpenCode |
goose |
Goose |
mistral, deepseek |
Vendor CLIs |
calliope-cli |
Calliope AI CLI |
base |
The shared base layer, for a bare environment |
An explicit --image-tag wins over --runtime; with neither, the workload's
primary container image is used. --vnc composes the -vnc variant of
whichever image was resolved. The aider and openhands runtimes are retired
and refused, by name or by image. --brain on dispatch adds a hosted brain the
run reads (at most 10), beyond the agent's own brains.
Runtime environment¶
The platform injects these into the agent container. They are reserved: a
native manifest [environment] block that sets one of them is refused at
registration, and a config-repo brief that sets one is dropped with a warning
at dispatch, because the dispatcher owns the value.
| Variable | Carries | Absent when |
|---|---|---|
ASTROLIFT_TRIGGER_PAYLOAD |
The per-dispatch input, JSON-encoded: astro agent dispatch --input '{...}', a workflow stage's payload, or a trigger-bound webhook's mapped body |
No input was supplied. An empty payload and no payload are the same thing, so nothing is set rather than the string null |
ASTROLIFT_TASK_ID |
The task's GUID | never |
ASTROLIFT_CONTROLLER_URL |
Base URL of the control plane | never |
AGENT_CALLBACK_URL + ASTROLIFT_CLUSTER_KEY |
Where a one-shot pod reports its terminal result, and the task-scoped credential to do it with. The key is minted per spawn, so a token from a retried render is dead | never |
ASTROLIFT_MESSAGES_URL, ASTROLIFT_RUN_MCP_URL |
The run's message endpoint (REST) and its MCP endpoint, authenticated with the same ASTROLIFT_CLUSTER_KEY |
never |
ASTROLIFT_BRIEF_ID, ASTROLIFT_BRIEF_HASH |
Identify the Brief the run was assembled from | The task has no Brief |
ASTROLIFT_PAYLOAD_URL, ASTROLIFT_PAYLOAD_HASH |
Where to fetch the agent package archive, and its sha256: digest |
The Brief stored no bundle (no [package] or [workspace]) |
ASTROLIFT_MANIFEST_PATH |
The manifest's path inside that archive | No archive |
AGENT_SYSTEM, AGENT_PROMPT |
The assembled system prompt and the kickoff turn | The runtime idles in listener mode instead of running once |
ASTROLIFT_WORKLOAD_KIND, ASTROLIFT_MAX_RETRIES, ASTROLIFT_TOOL_TIMEOUT |
agent, and the workload's max_retries and tool_timeout_seconds |
Not rendered from an agent workload |
ASTROLIFT_SNAPSHOT_URL, ASTROLIFT_SNAPSHOT_INTERVAL |
Where and how often a VNC session uploads its snapshot | Not a VNC task, or no blob store configured |
ASTROLIFT_WORKSPACE, ASTROLIFT_TMUX_SESSION |
The working directory and tmux session of an interactive agent box | Not an agent box |
Everything else in the container comes from your own manifest [environment]
block and the environment spec's secret refs.
Reading the trigger input¶
import json, os
payload = json.loads(os.environ.get("ASTROLIFT_TRIGGER_PAYLOAD") or "{}")
mode = payload.get("mode", "smoke")
A prompt-driven agent does not have to read the variable at all: the kickoff
turn in AGENT_PROMPT carries the same JSON inline, so --input reaches a
harness that only ever sees its prompt.
Import other formats¶
The Agent Package importer accepts agents_md, astrolift_package, langflow,
and flowise. Imports return semantic gaps. Langflow/Flowise graphs may be
flattened to one task or preserved as a federation requiring stage bindings.
Runtime image selection is required before a package is runnable; unresolved
external tool names remain warnings until matching ToolDefs are bound.
Receive the final result¶
Register an optional callback at dispatch, using an organization signing secret
reference. The completion callback guide
covers FULL and NOTIFY modes, signed final events, durable outage retries and
backoff for at least 24 hours, delivery-state queries, and manual replay.
The pod reporting URL AGENT_CALLBACK_URL is a separate platform-owned field.