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.
On the name
spec.claude. It is a legacy key name, not a scope claim: the block carries the SDK-harness session/model knobs generally, and itsprovidersub-key demonstrably configures non-Anthropic endpoints. Do not readspec.claude.*as “the Claude harness only” — and do not confusespec.claude.provider(which inference endpoint answers) with the top-levelspec.harness(which agent program drives the turn). The two axes compose.
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
available_harnesses:
hermes:
session: { mode: continue, max_age_minutes: 4320 }
background_review: false
# run_budget_seconds: 900 # optional; one-shot/eval jobs only
compression:
threshold: 0.80
threshold_tokens: null # optional absolute trigger cap
target_ratio: 0.20
tail_mode: lean
in_place: true
comms:
channels: [server:sac, server:scitex-cards]
outbound: { siblings: allow, parent: allow }
inbound: { siblings: allow, parent: allow }
a2a: { listen: true }
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 the selected harness 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 |
|---|---|---|
|
|
Which agent program drives the turn — NOT |
|
(deprecated alias of |
The spelling this field had before it was renamed. Still honoured: a spec carrying |
|
|
Launch mode within the Anthropic harness family. Empty/unset = |
|
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 the harness process starts. Each item is a dict with optional |
|
list of strings |
Required explicitly; |
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.available_harnesses.claude-code.account
account pins a Claude Code harness to one saved Anthropic OAuth account by
its sac account list slug. It belongs to the harness that consumes the
credential, not to the legacy spec.claude compatibility block:
spec:
harness: claude-code
available_harnesses:
claude-code:
account: scitex-01-scitex-ai
session: {mode: continue, max_age_minutes: null}
approval_policy: never
watchdog:
enabled: false
interval: 1.5
responses: {y_n: "1", y_y_n: "2", waiting: /speak-and-call}
The slug must be a non-empty string with no surrounding whitespace. At the
parser boundary SAC folds it into the typed Claude runtime configuration, so
the existing start preflight checks that exact stored-account preference and
the auth bind resolves its snapshot. account is rejected on other harness
entries because Codex and Hermes do not consume Claude Code OAuth snapshots.
spec.available_harnesses.<key>.compression — Hermes only
Hermes context compaction is configured beside the Hermes harness that owns
the behavior. The block is optional; omitting it preserves SAC’s current
Hermes defaults: threshold: 0.80, threshold_tokens: null,
target_ratio: 0.20, tail_mode: lean, and in_place: true.
threshold_tokens, when set, is a positive integer absolute cap: Hermes
compacts at the lower of the ratio-derived trigger and this value while the
model’s truthful context_length remains unchanged. The ratios must satisfy
0 < target_ratio < threshold < 1; tail_mode is lean or legacy, and
in_place is a boolean.
For agents sharing one engine, derive the absolute trigger from fresh engine
KV capacity rather than adding their independent model windows. SAC’s
fail-closed helper reserves 25%, divides the remainder by the sharing agents,
and rounds down to a binary token boundary. Six agents sharing an observed
max_total_num_tokens: 2180096 therefore use threshold_tokens: 262144:
their six triggers total 1,572,864 tokens and leave 607,232 tokens of reserve.
A missing or stale capacity observation is an error. A 524,288-token trigger
is suitable only for a controlled single-agent canary; six such triggers total
3,145,728 and are not a safe fleet steady state.
The derivation helper is intentionally pure and does not rewrite running
agents. An operator or deployment planner must put its threshold_tokens
result in each selected Hermes harness block; normal spec loading then carries
that value into the compiled Hermes profile on the next authorized restart.
The same block under Claude Code, Codex, or another harness is rejected rather
than silently ignored. The former top-level spec.context_management example
was removed because that tolerated legacy key has no runtime consumer.
spec.available_harnesses.hermes.background_review
This boolean controls Hermes’ automatic post-turn review. It defaults to
false, so SAC-managed Hermes agents do not silently issue a second request
over the full conversation after completing foreground work. Set it to true
only when that extra review traffic is intentional. SAC compiles the value to
Hermes’ internal auxiliary.background_review.enabled setting.
On 2026-09-12, aligned Hermes and inference-server logs showed an automatic review replaying about 691,000 tokens while the next foreground turn began on the same conversation. After the gateway restarted and cancelled the review, the foreground turn performed a cold prefill and took about 276 seconds. This field makes that high-cost behavior declared and testable rather than implicit.
spec.available_harnesses.hermes.run_budget_seconds
This optional positive integer gives one Hermes run a wall-clock checkpoint for
one-shot/eval jobs that already have an external deadline. It is unset by
default for agentic SAC sessions, so Hermes runs to completion and does not
receive the 80%-elapsed wrap-up instruction. When explicitly set, SAC compiles
the value to Hermes’ internal agent.run_budget_seconds setting. This is not a
hard kill and does not trigger context compression; it injects a one-time
wrap-up notice and tightens implicit stale-call timeouts.
SAC separately emits Hermes’ agent.max_turns: none sentinel for agentic
sessions. This keeps Hermes’ own tool loop unbounded while leaving
spec.autonomous.max_turns as the independent safety cap for SAC’s outer
drive-until loop. One-shot/eval callers can still use Hermes CLI overrides.
Telegram is an edge transport, not a per-agent dependency. Select the CCT
Telegram channel only on the human-facing gateway agent (normally
scitex-lead); application agents communicate through Cards, SAC, or A2A.
For each spec that explicitly selects CCT, SAC owns one poller using that
spec’s declared token. SAC neither creates nor requires a separate BotFather
token for every agent. Selecting the same Telegram bot in several agent specs
would create competing pollers and is not a supported topology.
A CCT turn is acknowledged only after Hermes proves the exact delivery marker
visible in its native session. The receipt identifies agent, delivery_id,
and exchange_id, so a gateway or web client can report and retry the turn
without an operator watching or attaching to the TUI.
spec.claude — SDK knobs
Field |
Type |
Description |
|---|---|---|
|
alias or full ID (default |
The model this agent’s turns run on. Without a |
|
string |
Pin this agent to a stored OAuth account ( |
|
|
Point the SDK session at any Anthropic-compatible endpoint (e.g. DeepSeek). |
|
|
Session strategy (default |
|
string |
Explicit session UUID for |
|
int |
Only resume if the stored session is newer than N minutes. Hermes defaults to and caps this at 4320 (3 days). |
|
list of strings |
Extra flags appended to |
|
bool (default |
Auto-confirm permission prompts in the TUI |
|
dict |
Escape hatch — splatted into |
spec.comms — harness-neutral communication
spec.comms.channels declares inbound communication adapters once for the
agent. The selected harness does not own this list. SAC adapts the same
declaration to Claude Code, Hermes, or Codex while preserving the same durable
SAC/Cards identity and exchange ledger.
spec:
comms:
channels:
- server:sac
- server:scitex-cards
outbound: { siblings: allow, parent: allow }
inbound: { siblings: allow, parent: allow }
a2a: { listen: true }
With available_harnesses, a channels key inside a harness entry is rejected
with a relocation hint. Older direct spec.claude.channels specs remain
readable only for the fleet migration window; new specs must use
spec.comms.channels.
Available models (spec.claude.model)
sac validates model at YAML-load time, then hands it to the Claude Code
SDK (claude --model), which resolves the value to a concrete model. Two
shapes are accepted:
1. Bare alias — recommended; auto-tracks the latest version of that family:
Alias |
Resolves to (current, 2026-05) |
When to use |
|---|---|---|
|
Claude Opus 4.7 |
Most capable; heaviest / slowest |
|
Claude Sonnet 4.6 — default |
Balanced capability and speed |
|
Claude Haiku 4.5 |
Fastest, cheapest; light tasks |
|
SDK / host default |
Don’t pin a family explicitly |
Append [1m] for the 1M-token context window where the model offers it —
e.g. opus[1m], sonnet[1m].
2. Full versioned ID — pins one exact model, no auto-tracking:
claude-<family>-N-M[-<date>][[ctx]], e.g. claude-opus-4-7,
claude-opus-4-7[1m], claude-sonnet-4-6, claude-haiku-4-5-20251001.
Abbreviated forms missing the version digits (e.g. claude-opus[1m]) are
rejected at validate-time — they pass the YAML loader but the SDK
silently returns zero tokens (no API call), so the agent looks hung. The
pinned regex catches this early.
Under a
provideroverride (e.g. DeepSeek) theclaude-*alias check is relaxed — use the provider’s own model names instead.
spec.health / spec.restart / spec.watchdog / spec.autonomous
Field |
Description |
|---|---|
|
bool — enable periodic liveness probe |
|
seconds between probes |
|
per-probe timeout |
|
|
|
int seconds — minimum nudge cadence when no tool activity (default 120). A Hermes TUI applies its 60-second anti-busy-loop floor, then adds a deterministic per-agent 0–59 second cadence offset. |
|
|
|
int |
|
seconds before first retry |
|
cap on backoff |
|
exponential factor |
|
parsed for back-compat; lifecycle managed via hooks |
|
drive turns until |
|
string token the agent prints when done (default |
|
int |
|
nudge sent when the agent pauses |
For an owning Hermes TUI, enabling this block arms Hermes’ native session
heartbeat at launch. The heartbeat fires only while the run is idle and the
native input queue is empty, so queued human guidance/steering takes priority.
Each wake asks the agent to re-read Cards, verify assignment and ownership,
and reject overlap before editing. SAC does not poll Cards or issue model
turns on a timer itself. Hermes cron remains the right mechanism for a
wall-clock job; the session heartbeat is the right mechanism for an idle
worker that should look for another durable Card.
When an external CI/review status is merely pending, the wake contract records
that evidence and returns idle for a later heartbeat rather than holding an
active turn open with polling sleeps. A real running test, build, or useful
process is not a polling sleep and may still be monitored normally.
The exact Hermes stagger interprets the first eight SHA-256 digest bytes of the
agent name as an unsigned big-endian integer, takes
integer mod min(60, configured_interval) seconds, and adds that to the
configured/floored interval. It is stable across restarts and hosts, and the
configured value remains the minimum idle backoff.
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 an agent session 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