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 its provider sub-key demonstrably configures non-Anthropic endpoints. Do not read spec.claude.* as “the Claude harness only” — and do not confuse spec.claude.provider (which inference endpoint answers) with the top-level spec.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).

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_skills

Advertises 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 via spec.skills.required[] @-imports in the auto-generated CLAUDE.md

Loaded into the agent’s prompt by the Claude SDK

Also note A2A’s separate top-level capabilities field is for transport properties (streaming, pushNotifications, etc.) — not a synonym for “what the agent can do”. The “can do” surface is always skills[].

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

name

parent directory of spec.yaml

description

metadata.labels.description (else auto)

version

apiVersion

url

<base>/agents/<name>

provider.organization

metadata.labels.team

skills[0].id / name

metadata.labels.role

skills[0].description

metadata.labels.function

skills[0].tags

metadata.labels.capabilities ∪ metadata.labels.skills (both CSV)

x-scitex-agent-container.role_class

metadata.labels.role

x-scitex-agent-container.cardinality

metadata.labels.cardinality

x-scitex-agent-container.scheduling

derived from spec.host / spec.hosts

x-scitex-agent-container.runtime

spec.runtime

x-scitex-agent-container.model

spec.claude.model (v3) / spec.model (v2 back-compat)

x-scitex-agent-container.required_skills

metadata.labels.skills (CSV) ∪ legacy spec.skills.required

x-scitex-agent-container.multiplexer

spec.multiplexer

spec — top-level

Field

Type

Description

harness

anthropic (default) | openai | codex

Which agent program drives the turn — NOT spec.claude.provider, which points the SDK session at an Anthropic-compatible inference endpoint. The two axes compose, and codex is a legal value of BOTH: as a HARNESS the Codex agent program runs the loop, as a PROVIDER Claude Code still drives and Codex only answers. Launch caveat today — read before choosing: the registry has four entries (claude-code-tui, claude-agent-sdk, openai-agents, codex-sdk), but only anthropic can be STARTED. openai and codex load, validate and resolve to their registry entries, then every lifecycle launch path REFUSES them — loudly, rather than silently launching a Claude runner under a spec that asked for something else. The working path for the OpenAI SDK meanwhile is the openai_session A2A executor (spec.a2a.handler); there is no equivalent executor for codex yet.

provider

(deprecated alias of harness)

The spelling this field had before it was renamed. Still honoured: a spec carrying provider: loads unchanged; starting it logs a one-line deprecation. Both keys with the SAME value is fine; both with DIFFERENT values is a hard load error naming both.

runtime

tui (default) | claude-agent-sdk | apptainer

Launch mode within the Anthropic harness family. Empty/unset = tui (the interactive Claude Code TUI in a PTY); claude-agent-sdk = the headless SDK runner; apptainer is the back-compat spelling of the pre-2026-06-13 container-engine field and maps to claude-agent-sdk. docker/podman were dropped 2026-05-13.

workdir

path

Mounted rw at /work (default: ~/.scitex/agent-container/runtime/agents/<name>/)

to_home

path

Mirrored into the agent’s container $HOME (= runtime/<name>/home/) at start. Every path under to_home/ lands at the same relative path under $HOME. Default ./to_home — auto-discovers a sibling to_home/ next to spec.yaml.

python-venv

string | list

Pre-activated for startup_commands; auto probes ~/.venv-3.11, ~/.venv

env-file

string | list

dotenv paths sourced at start. (VERIFY: parsed by the loader but currently rejected by _validation._KNOWN_SPEC_KEYS — known parser/validator drift.)

user

"" | "host" | "<uid>:<gid>"

Container user override; empty = image default.

multiplexer

tmux | screen

Long-lived session host (default tmux). (VERIFY: parsed by the loader but currently rejected by _validation._KNOWN_SPEC_KEYS — known parser/validator drift.)

host / hosts

string / list of strings

Singleton on one peer / multi-instance one-per-peer (mutually exclusive). hosts: "all" = every fleet host.

session

string

Top-level shortcut overriding spec.claude.session; legacy aliases accepted (continue-or-new, new).

screen.name

string

Legacy metadata (agent display name in sac fleet). Default = agent name. Does NOT drive a multiplexer.

startup_commands[]

list of {delay, command}

Run before the harness process starts. Each item is a dict with optional delay (int seconds, default 0) and required command (string); bare strings are not accepted.

startup_prompts[]

list of strings

Required explicitly; [] means no startup turn; no implicit prompt fallback

spec.apptainer — engine knobs

Field

Type

Description

image

path to .sif

sac-scitex.sif (full stack) or sac-base.sif (minimal). Optional; empty falls back to the sac default SIF at dispatch.

overlay

path

Writable rw layer above the SIF

overlay_size

size string (e.g. "5G", "500M")

When set together with overlay, sac auto-creates the overlay image at that path with the given size if it doesn’t exist (declarative — no manual apptainer overlay create step). Units: M/MB/G/GB only (K/KB rejected). Empty = no auto-create (missing overlay raises a clear FileNotFoundError at launch).

overlay_create_if_missing

bool (default true)

Gate for the auto-create behaviour above. When false AND the overlay is missing, sac raises FileNotFoundError without attempting creation (operator must pre-create with apptainer overlay create).

binds[]

host:container[:ro|rw] (or legacy {src,dst,mode} dict)

Bind mounts. Source side supports ~ / $VAR (sac expands before calling apptainer). Destination MUST be absolute (apptainer rejects relative / ~ / $VAR); conventional roots are /home/agent/... (D5 canonical HOME), /srv/, /work/, /opt/, /data/. The legacy {src, dst, mode} dict form is still accepted by the parser and normalized to the string form.

env

key-value dict

Env vars exported into the container

container_workdir

path (default /work)

Working directory inside the container.

nv / rocm

bool

Forward host NVIDIA / AMD ROCm libs. (DESIGN — mutual exclusion not currently enforced by the parser.)

raw_args[]

list of strings

Escape hatch — appended verbatim to apptainer exec

post / environment / def_file

string / KV dict / path

Apptainer %post shell snippet, %environment KV map, and override .def path for apptainer build. Empty / missing → no build extension.

relaxed

bool (default false)

(DESIGN — not yet implemented in the parser.) Intent: opt OUT of hardened-by-default isolation. When false (default), sac auto-prepends --containall / --cleanenv / --writable-tmpfs / --home /home/agent. Set true to disable; see docs/isolation.md + docs/adr/0001-isolation-hardening.md. TODO: wire into ApptainerSpec.

fakeroot

bool (default false)

(DESIGN — not yet implemented in the parser.) Intent: apptainer --fakeroot — uid 0 inside via user-namespace remap; host uid unchanged. D5 preflight detects userns-fakeroot via /proc/self/uid_map and accepts uid 0 only when remapped. TODO: wire into ApptainerSpec.

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

model

alias or full ID (default sonnet)

The model this agent’s turns run on. Without a provider override it is a Claude model — see Available models below. WITH one it is the override endpoint’s own id (e.g. deepseek-chat, gpt-5.6-sol) and the claude-* alias check relaxes. The sonnet default is applied to every agent regardless of harness — an Anthropic-shaped default the harness/runtime/inference layering work has not yet unwound.

account

string

Pin this agent to a stored OAuth account (sac accounts store-name). Mutually exclusive with provider.

provider

{ base_url, auth_token_env }

Point the SDK session at any Anthropic-compatible endpoint (e.g. DeepSeek). base_url is the endpoint; auth_token_env is the NAME of the host env var holding the key (never the key). Mutually exclusive with account; relaxes the claude-* model-alias check. See ADR-0011.

session

continue | new-session | resume

Session strategy (default continue — safe fallback). Legacy aliases continue-or-new, new accepted

resume_id

string

Explicit session UUID for session: resume

continue_max_age_minutes

int

Only resume if the stored session is newer than N minutes. Hermes defaults to and caps this at 4320 (3 days).

flags[]

list of strings

Extra flags appended to claude invocation

auto_accept

bool (default True)

Auto-confirm permission prompts in the TUI

raw_options

dict

Escape hatch — splatted into ClaudeAgentOptions(**raw_options)

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

opus

Claude Opus 4.7

Most capable; heaviest / slowest

sonnet

Claude Sonnet 4.6 — default

Balanced capability and speed

haiku

Claude Haiku 4.5

Fastest, cheapest; light tasks

inherit / default

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 provider override (e.g. DeepSeek) the claude-* alias check is relaxed — use the provider’s own model names instead.

spec.health / spec.restart / spec.watchdog / spec.autonomous

Field

Description

health.enabled

bool — enable periodic liveness probe

health.interval

seconds between probes

health.timeout

per-probe timeout

health.method

sdk-alive (only value accepted by the validator). NOTE: the parser default is the legacy string multiplexer-alive; with the validator pin in place, any explicit value other than sdk-alive is rejected at load time.

autonomous.idle_kick_after_s

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.

restart.policy

never | on-failure | always

restart.max_retries

int

restart.backoff.initial

seconds before first retry

restart.backoff.max

cap on backoff

restart.backoff.multiplier

exponential factor

watchdog.enabled

parsed for back-compat; lifecycle managed via hooks

autonomous.enabled

drive turns until drive_until token or max_turns

autonomous.drive_until

string token the agent prints when done (default DONE)

autonomous.max_turns

int

autonomous.kick_text

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

a2a.host

Bind interface for the per-agent A2A sidecar (default 127.0.0.1).

a2a.port

auto (default) — sac claims a free port from ~/.scitex/agent-container/config.yaml’s a2a.port_range (default 19000-19999), persists it in the shared PostgreSQL store, and surfaces it via sac agents list. Set an explicit int (e.g. 7901) to pin for a stable external URL. Set null to disable the sidecar entirely. Most operators never touch this — auto is the right default.

listen[]

LIST of side-port DECLARATIONS (NOT a single port override). Each item: {port, proto, path, name, owner}. proto: tcp (default) / udp / unix. Entries that fail validation (tcp/udp needs port>0; unix needs path) are silently dropped. The container does NOT bind these — declarations only, surfaced on the AgentCard for peers. The host-level sac listen server port (default 7878) is configured in ~/.scitex/agent-container/config.yaml under listen.port, NOT here.

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

telegram.bot_token_env

Env var name holding the bot token (default SCITEX_AGENT_CONTAINER_TELEGRAM_BOT_TOKEN)

telegram.allowed_users

List of Telegram user IDs (strings) allowed to talk to this bridge

telegram.auto_connect

bool (default true) — auto-attach the bridge at agent start

telegram.greeting

Optional greeting string posted on connect

hooks.pre_start[]

Shell commands before apptainer exec (a mkdir -p <workdir>/.claude is auto-prepended)

hooks.post_start[]

Shell commands after the runner reports ready

hooks.pre_stop[]

Shell commands before SIGTERM

hooks.post_stop[]

Shell commands after the runner exits

extensions

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 of kind: Agent).

  • spec.proxy is REQUIRED.

  • spec.claude, spec.startup_prompts, spec.startup_commands are rejected at validation time (no SDK to configure / prompt).

  • spec.a2a.port works the same — that’s the port operators POST to.

spec.proxy reference

Field

Type

Default

Notes

upstream

string (REQUIRED)

—

Full URL to the upstream A2A endpoint (must start with http:// or https://).

trust

enum

untrusted

untrusted / local-mesh / trusted. Advisory — surfaced on our AgentCard.

redact

list[str]

[]

Substring tokens; any inbound text containing one is refused HTTP 400 before forward.

timeout_s

float > 0

30.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 trusted level 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 --net policy.

  • Runs in sac-proxy.sif — see containers/sac-proxy.def.

See examples/agents/proxy-agent/spec.yaml for a complete minimal example.

Examples

Copy from examples/agents/: