YAML Spec Reference (v3)
Container + session knobs nest under the engine that interprets them
(spec.apptainer.*, spec.claude.*). Cross-cutting knobs (workdir,
a2a, health, restart, autonomous, listen, skills, telegram, hooks)
stay at the top level. Every curated block has a raw_* escape hatch
— the full underlying surface is always reachable.
The agent name is the parent directory of spec.yaml (dir-as-SSoT —
no metadata.name field).
Quick links
Annotated full example:
examples/agents/full-agent/spec.yaml— every supported field with inline commentsMinimal example:
examples/agents/minimal-agent/spec.yamlQuickstart with
startup_prompts:examples/agents/hello-agent/spec.yaml
Top-level shape
apiVersion: scitex-agent-container/v3 # REQUIRED — v1/v2 raise loud validation errors
kind: Agent # REQUIRED — Agent | AgentProxy
# (AgentProxy → HTTP forwarder, no SDK;
# see spec.proxy + examples/agents/proxy-agent)
metadata:
labels: # drives `sac fleet` filters AND the AgentCard
role: ecosystem-auditor
team: lab-a
description: ... # → AgentCard.description
function: audit, git status, ... # → AgentCard.skills[0].description
capabilities: audit,health-check # CSV → AgentCard.skills[0].tags
cardinality: singleton # → AgentCard.x-scitex-agent-container.cardinality
spec:
runtime: apptainer # optional; only `apptainer` accepted; empty defaults to `apptainer` (since 2026-05-13)
workdir: ~/proj # mounted rw at /work
to_home: ./to_home # mirrored into the agent $HOME at start (auto-discovers ./to_home; default "./to_home")
python-venv: auto # string or list — fallback chain
env-file: .env # string or list of dotenv paths (VERIFY: validator currently rejects — must be added to _KNOWN_SPEC_KEYS)
multiplexer: tmux # tmux | screen (VERIFY: validator currently rejects — must be added to _KNOWN_SPEC_KEYS)
apptainer: { ... }
claude: { ... }
mcp_servers: { ... }
health: { ... }
restart: { ... }
autonomous: { ... }
a2a: { host: 127.0.0.1, port: auto } # port: auto | <int> | null (disable)
proxy: { upstream: https://peer/, trust: untrusted } # kind: AgentProxy only
listen: # LIST of side-port DECLARATIONS (no binding):
- { port: 9000, proto: tcp, name: api, owner: app }
- { proto: unix, path: /tmp/x.sock, name: ipc }
# NOTE: the host-level `sac listen` server port lives in
# ~/.scitex/agent-container/config.yaml (listen.port, default 7878),
# NOT in agent spec.yaml.
startup: # (optional) ready-pattern gating block (todo#291)
commands: [...] # shadows top-level startup_commands when set
ready_patterns: [...] # regex strings (or { regex: "..." } dicts)
ready_idle_ticks: 3
ready_poll_interval_seconds: 0.5
ready_timeout_seconds: 60
on_timeout: capture_and_proceed # capture_and_proceed | capture_and_fail
context_management: # context auto-management (compact/restart/noop)
trigger_at_percent: 70
strategy: noop # compact | restart | noop
warn_before_n_checks: 0
check_interval_seconds: 300
telegram: { bot_token_env: ..., allowed_users: [...], auto_connect: true, greeting: ... }
hooks: { pre_start: [...], post_start: [...], pre_stop: [...], post_stop: [...] }
extensions: { ... } # opaque per-deployment dict
startup_commands: # SHELL before claude starts (list of {delay, command} dicts)
- { delay: 0, command: "echo hi" }
startup_prompts: [...] # TEXT fed to claude as first user msg
session: continue # top-level shortcut overriding spec.claude.session
host: gpu-box # mutually exclusive: singleton on one peer
hosts: [laptop, gpu-box, nas] # OR multi-instance, one per peer
Field reference
metadata.labels → AgentCard fields
Note on naming — two “skills” concepts. A2A’s AgentCard has a standard top-level
skills[]array used to advertise capabilities to peers (id / name / description / tags / examples). Anthropic’s Claude Code separately uses “skills” for prompt-fragment markdown files under<HOME>/.claude/skills/<name>/that the SDK loads into the agent’s own context. Both share the English word but live at orthogonal layers:
Layer
Drives
Effect
metadata.labels.skills(CSV)A2A
skills[0].tags+x-scitex-agent-container.required_skillsAdvertises capabilities on the card; no behaviour change inside the agent
spec.to_home/.claude/skills/<name>/SKILL.md(files)Materialised at
runtime/<name>/home/.claude/skills/(ADR-0006) and surfaced viaspec.skills.required[]@-imports in the auto-generated CLAUDE.mdLoaded into the agent’s prompt by the Claude SDK
Also note A2A’s separate top-level
capabilitiesfield is for transport properties (streaming,pushNotifications, etc.) — not a synonym for “what the agent can do”. The “can do” surface is alwaysskills[].
The AgentCard at GET /.well-known/agent-card.json (per-agent sidecar
when spec.a2a.port is set) and GET /agents/<name>/card
(host-level sac listen) is built entirely from spec.yaml:
AgentCard field |
spec.yaml source |
|---|---|
|
parent directory of |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
derived from |
|
|
|
|
|
|
|
|
spec — top-level
Field |
Type |
Description |
|---|---|---|
|
|
Empty/unset defaults to |
|
path |
Mounted rw at |
|
path |
Mirrored into the agent’s container |
|
string | list |
Pre-activated for startup_commands; |
|
string | list |
dotenv paths sourced at start. (VERIFY: parsed by the loader but currently rejected by |
|
|
Container user override; empty = image default. |
|
|
Long-lived session host (default |
|
string / list of strings |
Singleton on one peer / multi-instance one-per-peer (mutually exclusive). |
|
string |
Top-level shortcut overriding |
|
string |
Legacy metadata (agent display name in |
|
list of |
Run before Claude starts. Each item is a dict with optional |
|
list of strings |
Fed to Claude as first user message(s) |
spec.apptainer — engine knobs
Field |
Type |
Description |
|---|---|---|
|
path to |
|
|
path |
Writable rw layer above the SIF |
|
size string (e.g. |
When set together with |
|
bool (default |
Gate for the auto-create behaviour above. When |
|
|
Bind mounts. Source side supports |
|
key-value dict |
Env vars exported into the container |
|
path (default |
Working directory inside the container. |
|
bool |
Forward host NVIDIA / AMD ROCm libs. (DESIGN — mutual exclusion not currently enforced by the parser.) |
|
list of strings |
Escape hatch — appended verbatim to |
|
string / KV dict / path |
Apptainer |
|
bool (default |
(DESIGN — not yet implemented in the parser.) Intent: opt OUT of hardened-by-default isolation. When |
|
bool (default |
(DESIGN — not yet implemented in the parser.) Intent: apptainer |
spec.claude — SDK knobs
Field |
Type |
Description |
|---|---|---|
|
|
Claude model |
|
|
Session strategy (default |
|
string |
Explicit session UUID for |
|
int |
Only resume if session.jsonl is newer than N minutes |
|
list of strings |
Extra flags appended to |
|
|
MCP push channels (passed as |
|
bool (default |
Auto-confirm permission prompts in the TUI |
|
dict |
Escape hatch — splatted into |
spec.health / spec.restart / spec.watchdog / spec.autonomous
Field |
Description |
|---|---|
|
bool — enable periodic liveness probe |
|
seconds between probes |
|
per-probe timeout |
|
|
|
int seconds — nudge cadence when no tool activity (default 120) |
|
|
|
int |
|
seconds before first retry |
|
cap on backoff |
|
exponential factor |
|
parsed for back-compat; lifecycle managed via hooks |
|
drive turns until |
|
string token Claude prints when done (default |
|
int |
|
nudge sent when Claude pauses |
spec.a2a / spec.listen — network endpoints
Field |
Description |
|---|---|
|
Bind interface for the per-agent A2A sidecar (default |
|
|
|
LIST of side-port DECLARATIONS (NOT a single port override). Each item: |
The per-agent sidecar binds the same URL shape as sac listen
(/agents/<name>/{turn,send,card}, /v1/a2a/agents/<name>/...,
/.well-known/agent-card.json, /health), so the same client code
works against either transport. Per-agent ports are an internal IPC
mechanism between sac listen and the runner (different processes);
clients reach every agent through the one stable host port at
sac listen (default :7878).
The AgentCard’s url field advertises the sac listen URL
(http://127.0.0.1:7878/agents/<name>) regardless of which
endpoint served the card, so external A2A clients caching the card
get a URL that survives per-agent port churn.
~/.scitex/agent-container/config.yaml
Host-wide sac configuration. All keys optional; defaults shown.
listen:
host: 127.0.0.1 # bind interface for sac listen (loopback only)
port: 7878 # host control-plane port
a2a:
port_range: [19000, 19999] # range the auto-allocator picks from
Skills
spec.skills was removed in v3 — skills now live under
to_home/.claude/skills/ (a sibling directory next to spec.yaml,
materialized into the agent’s $HOME at start).
For AgentCard publication, declare the skill IDs via
metadata.labels.skills as a CSV (e.g. skills: "scitex-dev, gh-cli, git").
The list ends up in the card’s skills[0].tags (unioned with
metadata.labels.capabilities) and x-scitex-agent-container.required_skills.
spec.mcp_servers
A dict-of-dicts merged into <workdir>/.mcp.json at start. Mirrors
the .mcp.json shape directly. Use this OR drop a .mcp.json into
to_home/ (lands at $HOME/.mcp.json).
spec.telegram / spec.hooks / spec.extensions
Field |
Description |
|---|---|
|
Env var name holding the bot token (default |
|
List of Telegram user IDs (strings) allowed to talk to this bridge |
|
bool (default |
|
Optional greeting string posted on connect |
|
Shell commands before |
|
Shell commands after the runner reports ready |
|
Shell commands before SIGTERM |
|
Shell commands after the runner exits |
|
Opaque dict — read by downstream tooling (priority, owner, etc.) |
Lifetime / session selection
Default = long-lived + safe-fallback session continue. The sac agents start CLI overrides at start time:
sac agents start <name> --one-shot # exits after first startup_prompt
sac agents start <name> --session continue # default (try continue, fall back to fresh)
sac agents start <name> --session new-session # force fresh
sac agents start <name> --resume <sid> # implies --session resume
CLI flags ALWAYS override the YAML — one-direction precedence so a per-invocation tweak doesn’t mutate the persistent default.
kind: AgentProxy — HTTP forwarder agents
A proxy agent forwards POST /v1/turn to an external A2A
endpoint instead of running a Claude SDK conversation in-process.
There is no SDK in the container; the runner is a thin Starlette
forwarder (image: sac-proxy.sif, lighter than sac-scitex.sif —
no Python ML stack).
Authoring contract:
kind: AgentProxy(instead ofkind: Agent).spec.proxyis REQUIRED.spec.claude,spec.startup_prompts,spec.startup_commandsare rejected at validation time (no SDK to configure / prompt).spec.a2a.portworks the same — that’s the port operators POST to.
spec.proxy reference
Field |
Type |
Default |
Notes |
|---|---|---|---|
|
string (REQUIRED) |
— |
Full URL to the upstream A2A endpoint (must start with |
|
enum |
|
|
|
list[str] |
|
Substring tokens; any inbound |
|
float > 0 |
|
Per-turn upstream HTTP timeout. Longer forwards return HTTP 504 to the caller. |
Security notes
Proxy is HTTP-only — no mTLS in the MVP (the
trustedlevel is reserved for future work).Default trust is
untrusted; operators must opt in to anything more permissive.Egress lockdown is application-layer: a 3xx redirect from upstream to a different host is rejected with HTTP 502. The MVP does not enforce an apptainer
--netpolicy.Runs in
sac-proxy.sif— seecontainers/sac-proxy.def.
See examples/agents/proxy-agent/spec.yaml
for a complete minimal example.
Examples
Copy from examples/agents/:
full-agent/— annotated spec exercising every supported field (plusto_home/layout)minimal-agent/— bare minimum, noto_homehello-agent/— quickstart withstartup_promptsproxy-agent/—kind: AgentProxyforwarder example