Documentation — Configuration reference
Reference · Configuration

Configuration reference

This page lists every configuration path agentd accepts: its type, its default, whether a reload applies it, and whether an instruction document may set it. It is for operators writing a config and for anyone reviewing one. For why configuration is layered and validated the way it is, read How configuration works; for the flags that are not settings and the exit codes, read CLI, environment and exit codes.

How to read this reference

Each section below is one top-level key of the document. Its table names each path in full, so a search for limits.run.steps lands on its row.

ColumnMeaning
PathThe dotted path. [] marks an element of a list, <name> a key you choose in a map.
Typestring, int, number, bool, duration, rate, size, secret, a list ([…]), a map, or an enum written as a | b.
DefaultWhat applies when the path is unset. unset means the feature it controls is off.
Classlive — a reload applies it; restart — a reload that changes it is refused. Then doc — an instruction document's :::!config fragment may set it too; or op — operator only.

The value syntax — durations, sizes, rates, secret references — is in Value syntax. agentd --config-schema prints the same paths as a JSON Schema (Draft 2020-12); when this page and that output disagree, the binary is right.

Note. Release binaries carry every build feature except exec and internal-mocks. A path that needs a feature says so; what each feature gates is in Build features and crates.

Paths, environment variables and flags

Every path is settable three ways, with names derived from the path. Nothing is plumbed per field, so a path added to the schema gets all three.

SourceSpelling for limits.run.steps
File (YAML or JSON)limits: {run: {steps: 40}}
EnvironmentAGENTD_LIMITS_RUN_STEPS=40
Flag--limits.run.steps 40 — ., _ and - are interchangeable, so --limits-run-steps 40 is the same flag; --flag=value works too

agentd --help prints the whole list under CONFIG PATHS. Precedence is built-in default, then files, then environment, then flags; each layer changes only the keys it sets.

Values from the environment and flags are typed by the schema:

Path typeAccepted spellingsExample
int, number, boolthe literalAGENTD_LIMITS_MAX_RUNS=4
enumone of its values--lifecycle.run-until drained
list[a, b] or a, bAGENTD_TOOLS_DISABLED=exec,code.run
object or mapa {k: v} flow literal, or JSON--mcp-servers '[{name: fs, endpoint: "https://mcp-fs.internal/mcp"}]'
anything elsethe verbatim stringAGENTD_AGENT_NAME=triage

These rules hold for every path:

  • A value that does not type is exit 2, naming its source: invalid AGENTD_LIMITS_RUN_STEPS: expected an integer, got "many".
  • Setting a list or map path replaces what the files declared. The named repeatable flags (--mcp, --a2a-peer, --workflow) add one element.
  • A dotted flag reaches into a free-form map and sets one entry: --intelligence.headers.x-team ops keeps the other headers.
  • List elements are not addressable: --mcp-servers.0.name is refused. Set the whole list, or use the named flag.
  • An unknown flag is unknown argument and an unknown file key is unknown field, both exit 2. An AGENTD_ variable that names no path is not read.

Short aliases

The aliases below are shorter spellings for the paths operators reach for most. The short environment name is read under the same AGENTD_ prefix.

FlagPathShort env var
--instructionagent.instructionAGENTD_INSTRUCTION
--instruction.text, .file, .dir, .url, .oci, .mcpagent.instruction.<source>—
--instruction.glob, --instruction.orderagent.instruction.dir.glob, .dir.order—
--instruction.refresh, --instruction.unavailableagent.instruction.refresh, .unavailable—
--promptagent.promptAGENTD_PROMPT
--prompt.text, .file, .dir, .url, .ociagent.prompt.<source>—
--prompt.glob, --prompt.orderagent.prompt.dir.glob, .dir.order—
--intelligenceintelligence.endpointsAGENTD_INTELLIGENCE
--intelligence-tokenintelligence.tokenAGENTD_INTELLIGENCE_TOKEN
--intelligence-token-fileintelligence.token_fileAGENTD_INTELLIGENCE_TOKEN_FILE
--modelintelligence.modelAGENTD_MODEL
--model-swapintelligence.swap_policyAGENTD_MODEL_SWAP
--budget-tokens-lifetimeintelligence.budget.lifetime_tokensAGENTD_BUDGET_TOKENS
--mcp name=<endpoint>mcp.servers (adds one)—
--mcp-tags name=tag,tagmcp.servers[].tags of a declared server—
--a2a-peer name=<endpoint>a2a.peers (adds one)—
--workflow <file>workflows (adds {name: <file stem>, file: <file>})—
--max-stepslimits.run.stepsAGENTD_MAX_STEPS
--max-tokenslimits.run.tokensAGENTD_MAX_TOKENS
--deadlinelimits.run.deadlineAGENTD_DEADLINE
--max-depthlimits.subagents.depth—
--run-idlifecycle.run_idAGENTD_RUN_ID
--drain-timeoutlifecycle.drain_timeoutAGENTD_DRAIN_TIMEOUT
--watch-configlifecycle.watch_configAGENTD_WATCH_CONFIG
--budget-exit-code <N>lifecycle.exit_code_map (sets {"3": N, "7": N})—
--listena2a.listenAGENTD_A2A_LISTEN (the derived name)
--log-levelobservability.log_levelAGENTD_LOG_LEVEL
--log-contentobservability.log_contentAGENTD_LOG_CONTENT
--metrics-addrobservability.metrics_addrAGENTD_METRICS_ADDR
--health-fileobservability.health_file—
--events-ringobservability.events_ring—
--traceparentobservability.traceparentAGENTD_TRACEPARENT
--allow-trifectasecurity.allow_trifectaAGENTD_ALLOW_TRIFECTA
--tls-casecurity.tls_caAGENTD_TLS_CA
--aauth-provider, --aauth-key-file, --aauth-enroll-token, --aauth-enroll-assertion-file, --aauth-person-serversecurity.aauth.*—
--cgroup, --cgroup-memory-max, --cgroup-pids-maxsecurity.cgroup.spec, .memory_max, .pids_max—

These environment variables are read but are not paths:

VariableMeaning
AGENTD_CONFIGConfig files to load, :-separated (a.yaml:b.yaml), before any --config.
AGENTD_INTELLIGENCE_TOKEN_<N>, AGENTD_INTELLIGENCE_TOKEN_<N>_FILEThe credential for endpoint N of the top-level connection (endpoint 1 uses the bare names above). Never applied to intelligence.providers.
AGENTD_STATE_DIRThe file store's root when store.file.path is unset.
AGENTD_POD_NAMEThe instance name when agent.name is unset (set it from the Kubernetes downward API).
OTEL_EXPORTER_OTLP_ENDPOINTThe OTLP endpoint when observability.otel.endpoint is unset.

The control flags (--validate-config, --effective-config, --config-schema, --login, --env, --fresh and the rest) and the tui/ui launchers are in CLI, environment and exit codes.

Files, discovery and merge

File formats

--config <path> (or -c, repeatable) loads YAML or JSON. The extension picks the syntax: .yaml and .yml are YAML, .json and .jsonc are JSON with // and /* */ comments. Any other extension is sniffed: a document starting with { or [ is JSON.

YAML is read by agentd's own subset reader: mappings, sequences, flow collections, quoted and plain scalars, | and > block scalars, comments, YAML 1.2 core typing (yes and on are strings). Anchors, aliases, tags, merge keys, multi-document streams, tab indentation and duplicate keys are refused with a line and column. A document carries no version field.

The discovery chain

When no config is named — no --config and no AGENTD_CONFIG — agentd loads every rung of this chain that has a file, in this order:

RungFiles looked for
user$XDG_CONFIG_HOME/agentd/config.yml (or .yaml), else ~/.config/agentd/config.yml (or .yaml)
project./agentd.yml, ./agentd.yaml, ./.agentd.yml, ./.agentd.yaml
local./agentd.local.yml, ./agentd.local.yaml
  • Naming a config by flag or environment skips the whole chain.
  • The project and local rungs are the working directory only; there is no walk up to parent directories and no /etc.
  • Two spellings of one rung (agentd.yml beside agentd.yaml) is exit 2.
  • A discovered file may not relax a security control: one that sets security.allow_trifecta is exit 2. Pass --config if you meant it.
  • A discovered file that sets intelligence.endpoints or adds an intelligence.providers entry loads with a warning naming the file.
  • --help, --version, --config-schema and --workflow-schema never discover a config.

Conventional folders

Four folders beside the config fill in a setting you did not write:

FolderFills inFiles
workflows/workflows*.yaml, *.yml, *.json, one workflow document each
skills/skills.dir<name>/SKILL.md, one Agent Skill each
subagents/subagents.templates*.yaml, *.yml, *.json, one template each, named by file stem
context/context.templates*.md, *.txt, *.hbs, the whole body as the template, named by stem

A folder applies only when its setting is absent (an explicit empty list counts as set) and only when it yields at least one file. agentd looks beside each loaded config file, the last one first, and uses the first directory that has the folder; with no config file, the working directory. Files load in filename order.

.agents/skills/ beside a loaded config file is read as well, as the Agent Skills project scope: its <name>/SKILL.md skills sit beside those of skills.dir, and win a name over them. It has no key, and is never read from the working directory alone. A skill in either folder that breaks an Agent Skills rule refuses the configuration (Agent Skills).

Several files

The files in play are every entry of AGENTD_CONFIG, then every --config in argument order. They merge into one document, later over earlier, with JSON Merge Patch rules (RFC 7396):

In the later fileEffect
an objectmerges key by key, recursively
a scalar or a listreplaces the earlier value
nullunsets the key, so the built-in default applies

Each file is type-checked on its own, so an unknown key names the file it is in. The merged document is the file layer; environment and flags still apply on top.

Editor schema

The schemas are published so an editor can complete keys and flag typos:

DocumentSchema URL
a config filehttps://agentd.dev/schema/config.json
a standalone workflow filehttps://agentd.dev/schema/workflow.json

Put a modeline on the first line of the file:

# yaml-language-server: $schema=https://agentd.dev/schema/config.json
agent:
  name: triage

Or map files to schemas once in VS Code's settings.json (the YAML extension; Neovim's yamlls takes the same map):

"yaml.schemas": {
  "https://agentd.dev/schema/config.json": ["agentd.yml", "agentd.yaml", "agentd.local.yml", "*.agentd.yaml"],
  "https://agentd.dev/schema/workflow.json": ["workflows/*.yaml", "workflows/*.yml"]
}

In JetBrains IDEs the mapping is under Settings → Languages & Frameworks → Schemas and DTDs → JSON Schema Mappings. The schema does not check rules that span fields, such as a model name that is not declared; --validate-config does. agentd --config-schema and agentd --workflow-schema print the same schemas the site publishes.

Value syntax

Durations

A duration is a number with an optional unit. A bare number is seconds.

UnitExampleMeaning
ms250msmilliseconds
s90sseconds
m5mminutes
h2hhours
d30ddays
w2wweeks
(none)30seconds

An empty value, an unparsable number or an unknown unit is exit 2 (unknown duration unit 'x' in 2x).

Rates

A rate is <burst>/<per>, where <per> is a duration: 60/1m is a burst of 60 refilling over a minute, 1/1d one per day.

Sizes

store.file.min_free and security.cgroup.memory_max take a size: plain bytes, or a number with a unit such as 256MB, 512M, 2G or 1.5GiB.

Secret references

A credential never appears in a config file as a literal. It is a reference, resolved at startup and never logged:

ReferenceResolves to
{{secret:NAME}}the environment variable NAME
{{secret-file:PATH}}the contents of the file at PATH

From the environment or a flag, a credential may be inline. The paths that must be references in a file are listed in Credential-bearing paths. An unset variable or a missing file is exit 2 before the first dial, every gap reported at once:

{"event":"config.invalid","msg":"{{secret:ANTHROPIC_API_KEY}} is not set in the environment (referenced at config.intelligence.token)"}

--validate-config reads the environment it runs in, so run it with the daemon's. --prompt-missing asks for each missing {{secret:NAME}} on the terminal instead (cli.md).

Environment substitution

${VAR} and ${VAR:-default} in any string value are replaced from the process environment when the document loads. Braces are required, so a bare $VAR stays as written, and $$ is a literal $. An unset variable with no default is exit 2. The instruction text is exempt: there ${name} is an instruction parameter. Use ${…} for plain values such as hostnames and {{secret:…}} for credentials.

Configuration variables

{{config.NAME}} names an entry under vars and is substituted at load, in the config and in every workflow definition.

agent

The agent's identity, its standing instruction, and how its conversation behaves.

PathTypeDefaultClassWhat it does
agent.namestring$AGENTD_POD_NAME, else $HOSTNAME, else agentdrestart · docThe instance identity. The durable store keys every record by it, so renaming the agent starts a different instance.
agent.descriptionstringa generic sentencelive · opThe public Agent Card's description, readable without signing in.
agent.instructionstring or objectunsetlive · opThe standing instruction, given to every turn. In a job with no workflow and no prompt it is also the task: agentd generates the workflow main (once → agent → finish) to run it. A daemon runs nothing for it at startup. See The instruction source.
agent.promptstring or objectunsetlive · opA task, delivered as a message into the root conversation at every start: a job's whole work, or what a daemon does as soon as it starts. Takes the text, file, dir, url and oci sources of the instruction, classified the same way; no mcp. A prompt also suppresses the generated main workflow a bare --instruction job gets.
agent.workspacepathunsetlive · opThe workspace the agent works in: its AGENTS.md files — the root's, then one in each directory down to agentd's working directory when that is inside it, the nearest last — reach every turn's system prompt as text, below the instruction (the template's agents_md). UTF-8, at most 32768 bytes together, a symlink followed only to a file inside the workspace; a file that breaks one refuses the load, naming it. Read again by a reload, and on a change with lifecycle.watch_config.
agent.preflightnever | auto | alwaysautolive · docTriage of an inbound message before its turn, on intelligence.roles.preflight. intelligence.routes needs auto or always.
agent.wake_onlist of a2a_message | human_reply | subagent_result | workflow_finished | workflow_failed | instruction_updated | budget_resumed[a2a_message, human_reply, workflow_failed]live · docThe events that wake the root conversation or leave it a note. subagent_result notes each child's result.
agent.on_workflow_finishedignore | note | thinkignorelive · docWhat a finished run leaves in the root transcript, for the runs wake_on names: nothing, a note, or a note that starts a turn. Withheld when the run may carry outside input and the root holds sensitive and egress.
agent.tools.internal, agent.tools.mcp, agent.tools.codeall | none or a list of namesalllive · opWhich internal, MCP and code-registered tools the root conversation is granted.
agent.max_parallel_turnsint4live · docConversation turns that may run at once.
agent.conversation_budgetbudgetunsetlive · docA token budget per conversation, in the budget shape.
agent.approvalask | auto | acceptasklive · docHow a gate is answered: by a person, by a judge on intelligence.roles.approve, or by taking the ask's recommendation (accept falls back to auto when the ask carries none). The operator can change it at runtime with admin.set.
agent.ask_human_fallbackfail | wait | autofaillive · docWhat an ask_human does when it does not open a gate: fail at once, park until its timeout, or let the judge answer. auto also answers an unaddressed gate that times out. No judge answers an ask whose run, subagent or peer conversation carries outside input; the ask fails (human.judge.withheld). pause and idle are accepted for wait, finish and stop for fail.
agent.ask_human_unownedgate | fallbackfallbacklive · docWhat an ask nobody owns does (one raised by a schedule, webhook, stream or subagent): open a gate task on the A2A listener (gate, needs a2a.listen), or apply ask_human_fallback.
agent.gate_channelsmap: workflow → step path → channel{}live · opThe channel a configured workflow's gate is announced on, where no instruction document declared one.
agent.document_capabilitieslist of material | knowledge | interface | identity | compute | infra | compose[]restart · opThe instruction-document families the instruction may use (the trust ladder). Empty grants the default rung only. A served document opens a webhook route only under interface. See Directive reference.

The instruction source

agent.instruction has a short form, a value agentd classifies, and a long form that names the source.

PathTypeDefaultClassWhat it does
agent.instruction.textstring—live · opThe instruction itself, never read as a path or URI.
agent.instruction.filepath—live · opA local file, read at load. Watched when lifecycle.watch_config is on.
agent.instruction.dirpath, or {path, glob, order}—live · opA folder of documents combined into one instruction.
agent.instruction.dir.pathpath—live · opThe folder.
agent.instruction.dir.globstring*.md,*.markdown,*.txt,*.instructionlive · opComma-separated globs relative to path; ** recurses.
agent.instruction.dir.ordername | datenamelive · opPath order, or modification time oldest first.
agent.instruction.urlhttps:// URL—live · opA document fetched at load.
agent.instruction.ocireference, or {ref, cosign_key}—live · opAn OCI artifact (ghcr.io/acme/agent:v3; @sha256:… pins it). cosign_key is a PEM public key file (P-256 or Ed25519) the artifact's cosign signature must verify against. Needs oci.
agent.instruction.mcpURI, or {server, resource, events}—live · opA resource a configured MCP server serves, read and subscribed. server picks the server to ask; events names MCP Events types whose occurrences re-read the instruction (needs mcp-events).
agent.instruction.refreshauto | off | durationautolive · opHow often to re-read the source.
agent.instruction.unavailableauto | keep | freeze | drain | exitautolive · opWhat to do when the source stops answering after startup.
agent.instruction.unenforceablewarn | refuse | ignorewarnlive · opWhat a trust pin that names no key file means at load (its author keys are instruction:// URIs, or none). refuse is exit 2. A key file that cannot be read is always exit 2.
agent.instruction.decrypt.keyslist of paths[]live · opRecipient key files for an encrypted envelope (age v1 or JWE): an AGE-SECRET-KEY-1… identity, 64 hex characters, or base64. Needs decrypt.
agent.instruction.decrypt.passphrasesecret—live · opThe passphrase for an age scrypt envelope.
agent.instruction.trust[]list of pins[]restart · opWho may sign the document. Needs sign.
agent.instruction.trust[].uristring—restart · opThe document the pin applies to (instruction://…).
agent.instruction.trust[].publisherstring—restart · opThe publisher the author signature must claim. With one pinned, an unsigned document is refused.
agent.instruction.trust[].author_keys, .delivery_keyslist[]restart · opEd25519 key files (raw, hex or base64url), or instruction://…keys.json JWKS URIs.
agent.instruction.trust[].readerstring—restart · opThis consumer's reader id; delivery verification runs only when set.
agent.instruction.trust[].max_capabilitieslist of families—restart · opThe ceiling on the families this source may attest.
agent.instruction.trust[].freshnessduration—restart · opThe revocation deadline: how long a signed document may go unconfirmed. When refresh is slower, the tighter one wins.

Naming two sources is exit 2, and so is a long form that names none. To run with no instruction, omit agent.instruction. glob and order belong inside dir; written beside it they are an unknown field.

The short form is classified like this:

The valueRead as
contains a newlinetext
file://…a file
oci://…an OCI artifact
any other scheme://…, https:// includeda resource a connected MCP server serves (mcp)
no whitespace and a document extension (.md, .markdown, .txt, .instruction)a file
no whitespace, path-shaped (/, ./, ../, ~/), ending in / or naming a folder that existsa folder
no whitespace and path-shaped otherwisea file
anything elsetext

Warning. agent.instruction: https://docs.example/agent.md names an MCP resource, not a web page. To fetch a document over HTTPS, write agent.instruction: {url: https://docs.example/agent.md} or pass --instruction.url.

A named file or folder that does not exist is exit 2. A file, dir, url or oci source that cannot be read at load is exit 2; an mcp source that no connected server serves at startup is exit 6. The two spellings compose: --instruction ./agent.md --instruction.refresh 30s keeps both.

What refresh: auto does for each source:

Sourceauto
a filean inotify watch when lifecycle.watch_config is on, otherwise no re-read
a folderwatches the folder and its documents when lifecycle.watch_config is on
urlno poll; fetched again on each reload
oci pinned by @sha256:never re-read
oci by tagpolls every 5 minutes
mcpthe server's resources/updated notification and any events it names, with a 5-minute poll as fallback
textnothing to re-read

A re-read updates the text the model reads. Machinery the document declares (:::!workflow, :::!mcp, …) folds into configuration at load, so it changes on a reload or a restart, not on a poll.

What unavailable does after a successful start:

ValueBehaviour
keepcarry on with the last good instruction
freezeserve live work, refuse new work
drainfinish live work, then exit (0 for a daemon)
exitstop at once, non-zero
autofreeze when the source is trust-pinned (a publisher under trust), otherwise keep

A re-read whose signature fails to verify counts as the source not answering. Each outcome logs one instruction.unavailable line. The how-to is Write and ship instruction documents.

intelligence

The model connections, the named models, the job each model does, and the token budget. The concepts are in Models, providers and roles; the task is Connect model providers.

Connections

The top-level intelligence.* keys describe the default connection. Each intelligence.providers.<name> is a further connection with the same keys and nothing more.

PathTypeDefaultClassWhat it does
intelligence.endpointslist, or one comma-separated string[]live · opOrdered endpoints for failover. Each is https://host[:port][/path], a loopback http://, or mock: (the built-in mock model: mock:final, mock:file:<playbook.json>; needs mock-models, which release binaries carry).
intelligence.dialectopenai | anthropic | bedrock | decisionsopenailive · docThe wire format. bedrock pairs with auth.kind: aws; decisions is for a provider only.
intelligence.tokensecret—live · opThe bearer for every endpoint of the connection.
intelligence.token_filepath—live · opA file holding the bearer, read at every dial. token wins when both are set.
intelligence.headersmap{}live · opExtra request headers. A credential-shaped header needs a reference.
intelligence.authobject—live · opA credential provider; see Credentials. Refused at load when the connection cannot send it as a bearer: a static credential under a header other than Authorization (put that in intelligence.headers), or a SPIFFE x509 SVID. Needs the oauth build feature, which release binaries carry.
intelligence.timeoutduration120slive · docThe ceiling on one call.
intelligence.structured_outputauto | promptautolive · docHow a call whose answer has a JSON Schema is asked: natively where the dialect and schema allow, or in the prompt alone for a server that refuses a response format. The answer is validated either way. prompt is refused on a decisions provider.
intelligence.providers.<name>object—live · opA further connection: endpoints, dialect, token, token_file, headers, auth, timeout, structured_output. Egress-checked like intelligence.endpoints.

Where a request goes:

Endpoint writtenPosted to
host only, dialect: openai<host>/v1/chat/completions
host only, dialect: anthropic<host>/v1/messages
dialect: bedrockthe Converse path for the model
host only, dialect: decisions<host>/v1/decisions
any URL with a path, bedrock asidethat path, exactly as written

So https://api.openai.com/v1/chat/completions and https://api.anthropic.com are the forms to write; https://api.openai.com/v1 posts to /v1 and fails.

Note. A connection with real endpoints and no model named for it is refused at load. mock: endpoints need no model.

Models and roles

PathTypeDefaultClassWhat it does
intelligence.modelstringunsetlive · docThe turn model: a declared model name, or a literal wire model on the top-level connection. The same setting as roles.turn; setting both is exit 2.
intelligence.swap_policyfinish-on-old | restart-turnfinish-on-oldlive · docWhen a reload moves the turn model: the in-flight turn finishes on the old model, or re-runs on the new one. Running subagents apply it at their next turn boundary.
intelligence.models.<name>.modelstringrequiredlive · docThe wire model name sent to the provider.
intelligence.models.<name>.kindchat | reasoning | decisionchatlive · docA chat model generates; a reasoning model thinks first, at an effort; a decision model answers typed questions natively and writes nothing.
intelligence.models.<name>.providerstringthe top-level connectionlive · opThe connection: a name under intelligence.providers. A model a document declares may not name one.
intelligence.models.<name>.windowintinferred from the model name, else 128000live · docThe context window in tokens.
intelligence.models.<name>.fallbackstring—live · docThe model asked when this one is unavailable (every endpoint failed or broken open). A cycle is exit 2.
intelligence.models.<name>.escalatestring—live · docA decision model's: the model an unsure answer is asked again on, once.
intelligence.models.<name>.reasoning.effortlow | medium | highmediumlive · docA reasoning model's default effort; a node's or think call's effort overrides it.
intelligence.models.<name>.reasoning.budgetintby effortlive · docThinking tokens, for the dialects that take a budget.
intelligence.roles.turnmodel, or {model}intelligence.modellive · docThe agent's turns; agent, extract and summarize nodes; subagents that name no model.
intelligence.roles.thinkmodel, or {model}roles.turnlive · docThe think tool and think nodes.
intelligence.roles.decidemodel, or {model, confidence}roles.turnlive · docdecide, classify, route and judge nodes, decision guards and the decide tool. Its confidence is the default threshold of every deciding node.
intelligence.roles.preflightmodel, or {model, confidence}roles.turnlive · docTriage of an inbound message.
intelligence.roles.compactionmodel, or {model}roles.turnlive · docSummarising the transcript when it outgrows the window.
intelligence.roles.goalmodel, or {model, confidence}roles.turnlive · docThe goal check.
intelligence.roles.approvemodel, or {model, confidence}roles.turnlive · opAsks answered on the operator's behalf (approval: auto). A decision model here must set confidence.
intelligence.routesmap: model → criterion{}live · opThe models a turn may run on instead of roles.turn, each with when to use it. Preflight picks one per message. Each is a declared chat or reasoning model; 32 at most; refused with agent.preflight: never.

A model reference is a declared name, or, when no model of that name exists, a literal wire model on the top-level connection. Every reference is checked at load. confidence is refused on turn, think and compaction. Choosing models per job is the guide Choose models for each job; the wire details are Intelligence wire reference.

Budgets

intelligence.budget is the instance's token budget. The same shape is agent.conversation_budget (per conversation), a2a.principals[].quotas.budget (per principal), subagents.templates.<name>.budget (an instance-tier child's own) and a workflow's limits.budget. Where a budget is declared decides what it bounds. All intelligence.budget paths are live · doc.

PathTypeDefaultWhat it does
intelligence.budget.windows[]list[]Fixed windows: {per, tokens, requests, reset}.
intelligence.budget.windows[].persecond | minute | hour | day | weekrequiredThe window length. day and week are calendar windows.
intelligence.budget.windows[].tokensint—Tokens allowed per window.
intelligence.budget.windows[].requestsint—Model calls allowed per window.
intelligence.budget.windows[].resetstring00:00ZWhen a calendar window resets (UTC).
intelligence.budget.on_exhaustedwait | slow | degrade | refuse | failwaitWhat an admission does when a window is spent.
intelligence.budget.slow.factornumber, 0–10.5For slow: pace admissions to this share of the window rate.
intelligence.budget.degrade.modelstring—For degrade: the model a squeezed turn moves to. Required with degrade.
intelligence.budget.lifetime_tokensint0 (unbounded)A ceiling across every run of the process.
intelligence.budget.lifetime_exhausteddrain | refuse | exitdrainWhat the process does once the ceiling is spent: finish live work and exit (0 for a daemon), stay up refusing new work, or exit 7 at once.

The how-to, with the exit codes and gauges each choice produces, is Control spend with budgets.

Credentials

The auth block is the same wherever it appears: intelligence.auth, intelligence.providers.<name>.auth, mcp.servers[].auth, services.<name>.auth and a2a.peers[].auth. One mechanism per endpoint, named by auth.kind.

MechanismRequired fieldsOptional fields
statictoken (a bearer), or header + value—
oauth2client_id, or security.client and issuer; token_url or issuer — on an MCP server or an MCP catalog entry neither, the server's RFC 9728 metadata naming it; for grant: device, device_authorization_url or issuer (or neither, on an MCP server); for grant: client_credentials with a client_id, client_secret or client_auth: {method: private_key_jwt}grant (device default, authorization_code, client_credentials), scopes, audience, resource (auto, omit or a URI; unset, auto on an MCP server or MCP catalog entry, omit elsewhere), authorization_url, client_auth (method: private_key_jwt default with key, kid, assertion_aud; client_secret_basic; client_secret_post; with no client_id, assertion_aud only)
awsregion, service (for example bedrock); for source: sso, sso_start_url, account_id, role_namesource (env default, static, imds, irsa, sso)
spiffefor svid: jwt (the default), jwt_svid_file; for svid: x509, svid_file + key_fileaudience

token, value, client_secret and client_auth.key must be references in a file. Interactive grants are completed with agentd --login <target>, where the target is intelligence, intelligence:<provider>, mcp:<name> or service:<name>. kind: oauth2, and any auth block on an MCP server, need the oauth feature. An oauth2 block with no client_id presents agentd's Client ID Metadata Document (security.client) where its issuer's metadata advertises one. The guide is Outbound credentials.

mcp

The MCP servers the agent's task tools come from. All mcp.* paths are live · doc, except where a row says op.

PathTypeDefaultWhat it does
mcp.default_timeoutduration60sPer-call timeout for a server that sets none.
mcp.servers[].namestringrequiredUnique; code is reserved for code-registered tools.
mcp.servers[].endpointURLrequired unless servicehttps://host[:port][/path], or a loopback http://. Reached over Streamable HTTP; agentd spawns no process.
mcp.servers[].servicestring—A services: entry to inherit endpoint, credential and tags from. endpoint, auth and headers beside it are refused.
mcp.servers[].headersmap{}Extra headers. A credential-shaped one needs a reference. Headers the MCP client sets itself (MCP-Protocol-Version, Mcp-Method, Mcp-Name, Mcp-Param-*, Mcp-Session-Id, Last-Event-ID) are refused.
mcp.servers[].tagsmap: glob → list of untrusted_input | sensitive | egress | trusted{}Capability tags for the Rule of Two. The union applies to every tool of the server. An untagged server counts as untrusted_input, in the check and at runtime: its results taint the work that reads them. trusted is no leg: it marks a server whose results are not outside text, and changes nothing beside untrusted_input. Refused on a delegated server (auth.trusted_output says it there) and in an instruction document, whose servers count as untrusted_input unless a services: entry with tags vouches for them.
mcp.servers[].nsstring—A namespace prefix: tools register as <ns>.<tool>.
mcp.servers[].allowlist of globsallAdmit only the advertised tools that match.
mcp.servers[].excludelist of globs[]Never admit these; beats allow.
mcp.servers[].timeoutdurationmcp.default_timeoutPer-call timeout for this server.
mcp.servers[].tasksauto | offautoWhether the server may answer a long tool call with an MCP task (SEP-2663). auto declares the extension on 2026-07-28 connections, on the calls that have a time limit (a turn's, a workflow step's, a store adapter's); off never does, so the server answers every call inline. See Long-running tool calls.
mcp.servers[].authobject—A credential provider; see Credentials. With grant: id_jag — or grant: token_exchange, for a server behind the person's own IdP — the server is reached with an enrolled person's grant or a caller's own token, by the supervisor alone; its fields are under Enterprise-managed authorization, restart · op, and a served document may set none of them. Needs xaa.
mcp.servers[].oauth{client_id, client_secret, token_url, scope}—An OAuth client-credentials shortcut. Needs oauth.
mcp.servers[].aauthboolfalseSign requests with the agent's AAuth identity. Needs aauth.
mcp.servers[].events[]list[]MCP Events bindings (op).
mcp.servers[].event_grants[]list[]Event types runtime subscriptions may be made for (op).

--mcp name=<endpoint> adds one server; a spec with no = is exit 2. A server down at startup is dialed again in the background and joins when it answers. The how-to is Connect MCP servers; protocol support is MCP support reference.

MCP Events

Bindings and grants need the mcp-events feature, which tracks the draft extension SEP-3415; a config that declares one says so at load. A served document or a subagent template that declares either is refused.

events[] subscribes one event type with fixed arguments:

FieldTypeDefaultWhat it does
namestringrequiredThe event type, as the server's events/list names it.
argumentsobject{}Subscription arguments, checked against the type's input schema.
deliverylist of poll | webhookrequiredAcceptable modes in preference order; the first the server offers is used.
into{stream, subject}requiredA stream declared under streams:, and the subject each occurrence lands under.
replayduration, or none1hHow far back to catch up after downtime.
raterate—An intake ceiling. Required on an untagged or untrusted_input server.
ttlduration1hWebhook only: the lease agentd asks for, refreshed at 80% of what the server grants.

event_grants[] takes name, delivery, into, replay, rate and ttl as above, plus:

FieldTypeDefaultWhat it does
fixedobject{}Arguments no runtime subscription may set.
maxint, 1–648Live runtime subscriptions under this grant.
lifetimeduration, up to 30d1dHow long a tool-made subscription lives.
toolsboolfalseOffer the events.* tools for this type.

webhook delivery also needs webhooks.listen, an https:// webhooks.url, no webhooks.tls.client_ca, and a credential on the server. The how-to is React to changes and events.

# fragment: under mcp.servers (with a stream named issues under streams:)
- name: tracker
  endpoint: https://mcp-tracker.internal/mcp
  tags: {"*": [untrusted_input]}
  events:
    - name: issue.opened
      arguments: {project: web}
      delivery: [poll]
      into: {stream: issues, subject: issue.opened}
      rate: "30/1m"

services

The service catalog: the external services this deployment may use, named once. All services paths are live · op.

PathTypeDefaultWhat it does
services.<name>.kindmcp | intelligence | peer | http | companymcpThe surface the entry serves. Matching is by kind. company needs the companies feature; see Companies.
services.<name>.endpointURLrequiredThe connection URL, and the match base (scheme, authority and path prefix on segment boundaries).
services.<name>.auth, .headers, .timeout——Connection settings consumers inherit.
services.<name>.tagsmap{}Authoritative tags, unioned into every server whose endpoint matches, referencing or not. trusted is refused on a kind: company entry.
services.<name>.allowlist of globsallThe tool ceiling; a consumer may only narrow it.
services.<name>.excludelist of globs[]Never admitted, for every consumer.
services.<name>.raterate—Pacing toward the service, shared by every consumer in each process.
services.<name>.methodslistanykind: http only: the methods http steps may use.
services.<name>.breaker{failures, cooldown}—kind: mcp only: the default breaker for mcp.tool steps against it.
services.<name>.hostslist of host globsthe endpoint's host, the document's organization.domain, the subdomains of both, the issuer's hostkind: company only: every URL the Company's documents name must be on one of these hosts (example.com, *.example.com), and its host a plain one — no userinfo, and no \, which a browser reads as /.
services.<name>.allow_privateboolfalsekind: company only: admit loopback, private and link-local addresses.
services.<name>.interfaceslist of mcp | agent | openapi[mcp]kind: company only: the Company's interfaces agentd uses — its MCP servers, its agent (conversations, and the stream company.<name>), its OpenAPI APIs. See A Company's agent and its APIs.
services.<name>.mcp_namesmap of an MCP API url to a server name—kind: company only: names the Company's MCP servers when it lists more than one. A company entry whose interfaces lack mcp refuses it, and rate, allow and exclude with it: they shape only the Company's MCP servers.
services.<name>.privacystrict | standardstrictkind: company only: under strict a Company sees none of agentd's correlators. Its calls carry their idempotency key keyed under the (User, Company) pair's secret and a trace of their own per unit of work and Company — no other agent/* _meta key, no tracestate — and the handshake and tool list carry no trace context and no clientInfo.title. standard sends agentd's usual correlators, the instance label and the process trace included, and is logged at boot (company.privacy.standard).
services.<name>.sign_inlist of device | direct[]kind: company only: the sign-in types agentd may start with the User, the first the Company offers taken. direct needs security.companies.client.serve. mediated is refused: the User's password would pass through a gate, whose answer agentd keeps in the task's history and hands to the model. Needs security.companies.seal_key and scopes.
services.<name>.scopeslist of scopes[]kind: company only: the most account scopes agentd asks the User for there. A sign-in asks for what is needed within these and what its type grants; a held Account Token is used for no more, and one that grants none of these — or any, once the entry lists no sign_in — is not used: requests there are signed out, and the token is kept until the User signs out.
services.<name>.api_allowlist of METHOD /path-templatenonekind: company only: the OpenAPI operations agentd may call there, each as the description's paths names it (GET /orders/{order_id}; a parameter matches another of any name). An operation outside it, or outside the description, is refused unsent. Each one the description declares is also a tool of its own, named from its entry (operation tools). An operation listed twice, or two entries that make one tool name, are refused. Needs openapi in interfaces.
services.<name>.api_namesmap of an OpenAPI API url to a server name—kind: company only: names the server a Company's operation tools are named under, by the URL its OpenAPI API is listed at. Without it the server is named after the entry, or <entry>_api when the entry's own MCP server has that name, which needs the Company to list one OpenAPI API. Needs openapi in interfaces and an operation in api_allow.
services.<name>.context{locale, time_zone, share_availability}inheritskind: company only: what this Company's conversations share of the User's situation, each field over security.companies.context. Needs agent in interfaces.

With security.egress: closed, every MCP dial, intelligence endpoint (top-level and providers; mock: aside), A2A peer, http step, HTTP store, workflow URL and A2A push target must match an entry of its kind. observability.otel.endpoint is the one destination it does not cover. --validate-config prints each consumer's effective endpoint, tool lists and tags.

services:
  billing:
    endpoint: https://billing.internal/mcp
    auth: {kind: static, token: "{{secret:BILLING_MCP_TOKEN}}"}
    tags: {"*": [sensitive]}
    allow: [charge_lookup, "invoice_*"]
    rate: "60/1m"
mcp:
  servers:
    - {name: money, service: billing, allow: [charge_lookup]}
security:
  egress: closed

Under closed, an intelligence endpoint needs a kind: intelligence entry of its own. The hardening guide is Harden a deployment.

Companies

A kind: company entry declares a Company agentd acts at for its one User, under the Personal Agent Protocol draft agentd is pinned to (RFC 0047; the companies feature, which release binaries carry). endpoint is https://<host>, the domain whose /.well-known/poppy.json is read; a path is refused. The supervisor discovers and verifies the Company after load (never under --validate-config, which dials nothing), holds a signed-out Session there, and connects to each MCP API the Company lists as an MCP server of its own: named after the entry when there is one, by mcp_names when there are several, with every tool registered as <server>.<tool>. A Company that does not answer, or does not verify, is tried again with the backoff of a server down at boot. The tools are the supervisor's alone: no turn worker or subagent is handed a Company server to dial, and every call to one comes back to the supervisor. They are offered only in work the configured User owns, and in work nobody owns when the User is operator or an operator principal. A workflow calls them with tool steps (shop.search), under the same rule; an mcp.tool or mcp.resource step, a resource wait or a subscribe start naming a Company's server is refused, and neither an instruction, a workflow definition nor the store is read from one.

A Company's answer is its content: the conversation, run or subagent it reaches carries the Company's origin from then on, and a call to another Company from it waits for the configured User's answer (Company gates). A call that fails after it was sent — no answer in the entry's timeout, a 5xx, a result agentd could not read — answers outcome unknown and is not sent again, by a step's retry: or by a restart that finds the step still running. A 429 or 503 with Retry-After holds every request to the Company until the wait is over. company.status (a tool, and an operator op) says where each Company stands; the operator op company.user_id.reset makes the User new at one, revoking every Account Token held there first.

The User signs in at a Company themselves, at a gate agentd opens for them (Company gates), when an entry lists sign_in and scopes: the model asks with the company.sign_in tool, a workflow with a tool step, the operator with agentd --login company:<name> (CLI). Device Sign-In shows the User a link and a code to check on the Company's page; Direct Sign-In a link to it, which returns to the callback the A2A listener serves. Neither ever opens a browser. The Account Token the sign-in gives agentd is kept only sealed with security.companies.seal_key, read by the supervisor alone for each request that uses it, and written back before use when the Company rotates it. Its account's Session is signed in with it — renewed, or started again when the Company ends one — until the Company answers invalid_grant for it: a disconnect, a revocation, an expiry. Then it is forgotten and work goes on signed out. company.sign_out and agentd --logout company:<name> revoke and forget it. The User may hold up to eight accounts at one Company, each named by a label a sign-in gives it (account; agentd --login company:<name>/<label>) and each with an Account Token and a Session of its own; the label never reaches the Company (Several accounts).

A Company's agent and its APIs

An entry whose interfaces list agent talks with the Company's agent, in the one conversation protocol the draft defines, poppy; one that lists openapi calls the operations of the OpenAPI APIs the Company lists. Both are reached through company.* tools — internal tools the supervisor serves, as it serves a Company's MCP tools, under the same rules: the User's work only, limits.in_flight, the Company's Retry-After, the cross-Company gate, and the Company's origin on whatever reads the answer. Every request carries the User's DPoP Session Token and a proof of its own, and no correlator of agentd's. A request made for a unit of work names a trace in traceparent: under privacy: strict the unit's own trace at this Company — the same for each of the unit's requests there, linked to the unit's trace only in agentd's telemetry — and under standard the unit's own. One that belongs to no unit of work — a reader's — names the process trace under standard, and none under strict. Such a Company is verified at boot too, and opens its Session at the first call.

  • company.converse {company, conversation?, account?, text?, data?, wait?} sends a message: with no conversation it starts one and answers its id. The message's id is msg_ and 128 random bits, kept for the call before it is sent, so every send of the call — the transport's retry, and a workflow step's retry: or its replay after a restart, the step's own key holding across its attempts — carries the same id, and the Company answers it as the message it has (other content under it is message_id_conflict). A model's call is its turn's: a step's next attempt is a new turn, whose calls are new. Its sender is always agent, and its context holds only what security.companies.context (or the entry's own context) shares. With wait, at most 30 s, the reply is read too.
  • company.read {company, conversation, account?, wait?} answers the events since the model's last read, waiting up to wait (default 30) while there are none; company.handoff asks for a person at the Company; company.close closes the conversation. A read asked to wait is waited for that long and 15 s more, whatever the entry's timeout.
  • The reader. Each open conversation — at most limits.conversations in the instance, a new one refused until one closes — has a reader on a thread of its own: it asks for a stream (text/event-stream, a long poll from a Company that does not stream), opens it again with Last-Event-ID when it ends, and appends the conversation's message, state, authorization, user_requested, direct_opened and direct_closed events to the stream company.<name>, carrying the Company's origin, so a workflow can start from them (stream) or wait for one (wait {on: event}). It saves its cursor once they are appended, and a restart reads on from it. It stops when the conversation closes and when the instance drains. Each read skips event types agentd does not recognise and events it handled before, never hands on a text-delta piece, and on cursor_expired reads again from the start, telling the model that part of the conversation is missing. agentd declares company.<name> itself; a streams: entry of that name sets its retention.
  • What the supervisor does with an event, once: an authorization event is answered with the User's Account Token when one is held whose scopes cover it — the Session is signed in, and the conversation goes on with the new token — and otherwise reaches the model as a note that company.sign_in asks the User; the Company's authorization event never opens a gate itself. A user_requested event asks the User whether to talk with the Company directly (Direct Conversations), when they can be asked — the A2A listener serves and agent.ask_human_unowned is gate — and is otherwise answered in the conversation: the User is not available for a direct conversation. An operation a Company message carries is kept as one the conversation proposed (A Company's operations).
  • company.api {company, api?, method, path, query?, json?, expect?, account?} calls one operation of an OpenAPI 3.0 or 3.1 description (JSON, at most 1 MiB; an operation's $refs are followed when it is called or described, within the description, at most 16 deep and to at most 20 000 values). Unless method and path name an operation of the description that api_allow lists, and the path parameters, query and body hold to its schemas, nothing is sent. Each path segment is read percent-decoded, and one that is . or .., or holds / or \ once decoded, is refused: a server may resolve it to another operation. What is sent is the operation's template, each parameter percent-encoded into it. A write carries an Idempotency-Key, a UUID kept for the call, and is sent again after a lost answer only when its operation declares that header; any other write that may have run is outcome unknown. It answers {status, json}; a status outside expect (default 2xx) is an error. A Company's WWW-Authenticate errors are handled as for its MCP servers. company.api.describe {company, api?, operation?} lists the operations, or gives one's parameters and schemas; each operation api_allow lists names its operation tool.
  • The http step with auth: {kind: company, company: <name>} is a company.api call made by the workflow: its url is the operation's (or its path under the API's server URL), with the step's method, query, json and expect, and auth.account the label of the User's account it acts with, when they hold several there. It takes no headers, body, sign, allow_private or idempotency: the credential, the body, the hosts and the key are agentd's.
  • The User's accounts. Each of these tools, the operation tools and company.operation.* take an optional account, the label of the User's account to act with (Several accounts); a call naming none while several are held is refused unsent, and a conversation or an operation goes on in the account it belongs to.
# fragment: a Company's agent and one of its APIs
security:
  companies:
    context: {locale: en-US, time_zone: America/Los_Angeles}
    limits: {conversations: 8}
services:
  shop:
    kind: company
    endpoint: https://example.com
    interfaces: [agent, openapi]
    api_allow: ["GET /orders/{order_id}", "POST /orders/{order_id}/exchanges"]
workflows:
  - name: exchange
    steps:
      s: {kind: once}
      ask: {kind: tool, name: company.converse, depends_on: [s],
            args: {company: shop, text: "The jacket from order 1042 is too thin. Can it be exchanged?", wait: 20}}
      order: {kind: http, depends_on: [s], url: "https://api.example.com/orders/1042",
              auth: {kind: company, company: shop}}
      f: {kind: finish, depends_on: [ask, order], status: completed}

A Company's operation tools

Each operation api_allow lists that the Company's description declares is a tool of its own (RFC 0047 §9, P5a), offered once the description is read — at boot, and for a Company down at boot once it comes up — as a Company's MCP tools are offered once its server joins.

  • Its name is the operator's: its api_allow entry in lower case, each run of characters other than a-z and 0-9 one _, none at either end, at most 64 characters — GET /orders/{order_id} is get_orders_order_id — under the server named after the entry (<entry>_api when the entry's MCP server has its name), or the one api_names gives the API: shop.get_orders_order_id. The operationId, summary and description go into the tool's description, never its name. So the names are known at load, and load refuses a tag key, a security.policies rule's match.tool or a workflow's tool step that names an operation tool the entry does not make, two api_allow entries that make one name, and a server name another server has.
  • Its input schema is the operation's: its path and query parameters as properties, each with its schema and required as declared — nested under path and query when one's name is another's, json, expect or account — json for a JSON body, required as declared, expect, and account, the User's account it acts with; nothing else is taken. Header and cookie parameters are not taken, and an operation that requires one other than Idempotency-Key has no tool. Neither has one whose schemas do not resolve within the bounds above, or whose input schema is over 16 KiB serialized.
  • Its description starts with the note a Company's MCP tools carry — whose words follow — then the operation as api_allow lists it, then the Company's operationId, summary and description, each quoted, its control characters removed and at most 2000 characters.
  • A call is the company.api call of the operation: its path parameters filled into the operation's template, its query and json as given, and everything after that company.api's — the checks, the DPoP-signed request, the Idempotency-Key, outcome unknown, Retry-After, the privacy profile, the cross-Company gate, an operation the Company proposes. A path the description matches to another operation — a path parameter's value that is a fixed segment of that one's path — is refused unsent.
  • What holds company.api holds it. A security.policies rule judges its call as its own and as company.api's, under that name with company.api's arguments ({company, api, method, path, query, json, expect, account}) as the call fills them and the tool's own tags; the first rule that matches either decides. tools.disabled naming company.api disables every operation tool, tools.narrow on company.api adds its tags and note to each, and agent.tools offers one to the conversation only where agent.tools.internal offers company.api and agent.tools.mcp offers the tool.
  • Its tags, when a key of the entry's tags other than * names it (its name, or a glob of it), are those of each key that matches its name, * included; otherwise every tag the entry lists — with no tags, untrusted_input and egress — as company.api carries them. The Rule of Two at load counts all of the entry's tags, as for its servers.
  • A fetch of the description that finds it changed lists the tools again: those that changed replace the old ones in the registry (company.api.changed), and a turn planned after sees them. An allowed operation with no tool is logged once (company.api.not_offered), and why none can be made at all (company.api.unavailable).

They reach turn workers and subagents as a Company's MCP tools do: routed to the supervisor, a subagent granted the server handed their definitions.

# fragment: a Company's MCP server, and the tools of its API's operations
services:
  shop:
    kind: company
    endpoint: https://example.com
    interfaces: [mcp, openapi]
    api_allow: ["GET /orders/{order_id}", "POST /orders/{order_id}/exchanges"]
    api_names: {"https://api.example.com/openapi.json": orders}
    tags: {"*": [untrusted_input, egress]}
workflows:
  - name: order
    steps:
      s: {kind: once}
      read: {kind: tool, name: orders.get_orders_order_id, depends_on: [s],
             args: {order_id: "1042"}}
      f: {kind: finish, depends_on: [read], status: completed}

A Company's operations

With security.companies.client.extensions: [operations] the client metadata document lists the draft's Operations extension, version 1, and a Company may propose an action — an exchange, a booking change, a cancellation — as an operation instead of performing it: in an MCP tool's structuredContent, an OpenAPI 202 {operation} or a conversation message. agentd keeps each one it sees in the User's ledger at the Company, with how it came and the digest of the request that proposed it, and does nothing with it until it is confirmed. Every company entry has the stream company.<name> then, where an operation's final state is appended.

  • company.operation.get {company, operation_id, account?} reads it at the Company's operations endpoint; company.operation.cancel cancels it.
  • company.operation.confirm {company, operation_id, account?} confirms it, and takes nothing else: the model passes no revision and no approval. agentd reads the operation again and confirms its current revision only with the User's approve at their gate (Operation approvals) or under a standing permission that covers it. The call waits while the User decides. terms_changed asks again; user_approval_required asks the User, never a permission; in_progress is read again after the Company's Retry-After, across a restart too, and its final state appended to the stream. A confirm that got no answer, or whose answer agentd could not keep, is outcome unknown, and the operation is read until it is final; a later confirm sends the one the User approved. A confirm whose run or task is cancelled on its way is not sent, or — once the Company took it — cancelled there.
  • Nothing proposes the same action twice while it is open. A request whose digest — the tool and its arguments, or the method, path, query and body — proposed an operation that is not final is refused unsent, and an operation with the same terms in the same Session as an older one that is open is not confirmed. The same action asked for with other arguments and other terms is not recognised: the draft gives an operation no kind of action.

security.companies.standing lists the operator's standing permissions: an operation one of them covers is confirmed with approved_by: standing_permission, and the User is not asked. Each covers one Company, one channel — via: {tool: <glob>}, a glob of a Company tool's registered name, or via: {api: "METHOD /path-template"}, an operation api_allow lists — and the term keys in terms. It applies only when the Company does not require the User's own approval, every channel the operation came through is via (one proposed in a conversation never matches), the revision has terms and every top-level key in them is listed, when — CEL over terms and summary, so it needs the cel feature — is true with every term it reads present, it has not passed expires, and a use is free. This is asked again at each confirm, a retry of one too: an earlier confirm by a permission is never reused. A limited use is reserved with the confirm's lock, so two calls racing for the last use confirm once; it is spent once the Company records a confirmation by the permission, and given back when the Company refuses the confirm, answers it without taking it, or the operation ends with none — a cancel after a confirm it took gives nothing back. A permission covers the User's accounts its accounts lists, by the labels sign-ins gave them, and without the list the account labelled default alone, and the signed-out Session: a second account is never covered unlisted. A sign-in that may change the account under a label — one with new_account, or one under a label whose account was signed out of or otherwise given up, which goes in a new Session — suspends the permissions covering that label until a restart. They are restart-only and operator-only, like every security.companies path.

# fragment: operations, and the exchanges agentd may confirm on its own
security:
  companies:
    client:
      id: https://agent.example/companies/client.json
      name: Dana's agent
      extensions: [operations]
    standing:
      - company: shop
        via: {tool: "shop.exchange_*"}
        terms: [order_id, item_id, replacement_sku, price_difference]
        when: "terms.price_difference.currency == 'USD' && double(terms.price_difference.amount) <= 20.0"
        expires: 2026-12-31T00:00:00Z
        uses: 3
services:
  shop:
    kind: company
    endpoint: https://example.com

A company entry refuses auth, headers, methods and breaker: the protocol defines how agentd authenticates there. Its tags default to {"*": [untrusted_input, egress]}. Adding, removing or changing a company entry is a restart (config.reload.restart_required). It needs security.companies.client, and a store that keeps the User ID: store.kind none and memory are refused, and a remote store (mcp, http) needs security.companies.seal_key. Once the store's User IDs are sealed, every Company is unavailable without the key they were sealed with — a removed key as much as a wrong one (company.seal_key.mismatch) — until the key is restored, or company.user_id.reset is run for that Company: it starts over there as a new User, the others staying unavailable until each is reset, and once every Company has, the store takes the key.

# fragment: a Company, and who agentd is to it
security:
  companies:
    client:
      id: https://agent.example/companies/client.json
      name: Dana's agent
services:
  shop:
    kind: company
    endpoint: https://example.com
    hosts: ["example.com", "*.example.com"]
    allow: ["search*", "order_*"]
# fragment: the User may sign in at the Company with a link and a code
security:
  companies:
    seal_key: "{{secret-file:/run/secrets/agentd-company-seal}}"
services:
  shop:
    kind: company
    endpoint: https://example.com
    sign_in: [device]
    scopes: ["poppy:read", "poppy:write"]

tools

PathTypeDefaultClassWhat it does
tools.disabledlist of tool names[]live · docTools never registered. company.api here disables a Company's operation tools too.
tools.narrow.<tool>.tagslist of tags—live · docTrifecta tags to add to a tool; never removes one. Narrowing company.api narrows a Company's operation tools too.
tools.narrow.<tool>.describestring—live · docA note appended beneath the tool's description.
tools.overrides.<tool>.serverstring—live · opRetarget a tool at a declared MCP server.
tools.overrides.<tool>.toolstring—live · opThe server's tool name.
tools.overrides.<tool>.argsstring—live · opA JSON template or CEL: … producing the arguments from args and ctx.
tools.overrides.<tool>.resultstring—live · opA JSON pointer, template or CEL: … mapping the result.

context

How the system prompt is built and when the transcript is compacted. All paths are live · doc.

PathTypeDefaultWhat it does
context.templatestringthe built-in templateThe system-prompt template. agentd --context-template prints the built-in one.
context.templates.<name>string—Named alternates a node selects with context: {template: <name>}.
context.compact_atnumber0.7Compact when the transcript reaches this share of the model window.
context.keep_lastint12Messages kept verbatim through a compaction.
context.model_windowintthe turn model's windowOverrides the window compaction measures against.
context.plan.max_itemsint32Items a plan.create may hold.
context.summarize.promptstringbuilt-in guidanceThe summarizer's guidance. The summary's JSON shape is fixed; the model is intelligence.roles.compaction.

The template language:

FormMeaning
{{ expr }}interpolate
{{#if expr}} … {{else}} … {{/if}}conditional; an empty value is false
{{#each expr}} … {{/each}}loop; this is the element, @index its position
{{! comment }}comment

An expression is a path first and CEL second. Bare paths ({{instance}}, {{#each services}}) work in any build; anything more needs the cel feature, with two helpers CEL lacks: take(list, n) and join(list, sep). The data names are instance, instruction, agents_md, workflows, services, streams, templates, skills, peers, signals, memory and tools.internal. agents_md is the list of agent.workspace's AGENTS.md files, root first, each {path, text}.

# fragment: under context:
template: |
  You are {{instance}}. {{#if egress_closed}}Egress is closed.{{/if}}
  ## Instruction
  {{instruction}}
  {{#if services}}
  ## Services
  {{#each take(services, 16)}}- {{this.name}}{{#if this.tags}} [{{join(this.tags, ", ")}}]{{/if}}
  {{/each}}{{/if}}
templates:
  minimal: "You are {{instance}}: {{instruction}}"

Order a template stable to volatile: providers cache on the request's literal prefix. A malformed block or an unknown name is exit 2; a template that never mentions {{instruction}} loads with a warning, and so does one that never mentions {{agents_md}} while agent.workspace is set. How a turn uses it is How a turn runs.

memory, knowledge, search and skills

PathTypeDefaultClassWhat it does
memory.max_value_bytesint65536live · docThe largest value a memory.set may store.
memory.list_default_limitint100live · docKeys a memory.list returns when it sets no limit.
knowledge.serverstring—live · docThe MCP server behind the knowledge.* tools.
knowledge.auto_context.onturn | neverneverlive · docSearch the knowledge server before each turn and add the hits.
knowledge.auto_context.top_kint5live · docHits added per turn.
knowledge.auto_context.max_bytesint16384live · docThe most bytes of hits added per turn.
search.serverstring—live · docThe MCP server behind the search.* tools.
skills.sources[].serverstringrequiredlive · opAn MCP server whose Skills extension (io.modelcontextprotocol/skills) skills are read from. Not local: that is the origin a reference names your own skills by.
skills.sources[].filterstring—live · opOne skill name, or a prefix ending in *: the server's skills to take.
skills.sources[].dynamicrefuse | allowrefuselive · opLoad a skill whose content is dynamic, which cannot be verified.
skills.dirpathskills/ beside the config, when it holds a skilllive · opA folder of <name>/SKILL.md skills, the user scope. A folder that cannot be read refuses the configuration.
skills.reference_prefixstring@skill:live · docHow a text references a skill.
skills.max_loadedint8live · docSkills loaded into a context at once.
skills.max_bytesint32768live · docThe most text a skill body or file puts into context; the rest is cut.

store

Where durable state lives. Leave the section out and the shape of the instance decides: a one-shot job gets none; a long-lived instance gets file.

PathTypeDefaultClassWhat it does
store.kindfile | mcp | http | memory | noneby shaperestart · docThe adapter. memory is lost on exit; none keeps no state and is exit 2 on a long-lived instance.
store.prefixstringagentdrestart · docThe first key segment: <prefix>/<instance>/<kind>/<id>.
store.timeoutduration2slive · docPer-call timeout to the store.
store.on_errorhalt | degradehaltlive · docWhat a failed durable write does: halt the daemon, or mark the store degraded, count the write as made and carry on.
store.checkpoint.debounce_msint250live · docCoalesce checkpoints written within this window.
store.durability.workdurable | ephemeraldurablelive · docThe default class for runs and subagent records. ephemeral keeps them in memory unless a workflow or spawn says durable: true.
store.durability.a2a, store.durability.stepsstrictstrictlive · docstrict is the only behaviour; eventual is exit 2.
store.max_value_bytesintunboundedrestart · docRefuse a durable write larger than this. Set it when the store's read limit is lower than its write limit.
store.retention.runs.keep_last, .ttlint, durationkeep alllive · opDrop terminal runs outside the newest N or older than the TTL. Nothing in flight is dropped.
store.retention.tasks.keep_last, .ttlint, durationkeep alllive · opThe same for finished A2A tasks. keep_last is one bound across every principal; prefer ttl on a shared listener. A settled task nobody has read back is kept at least 30 seconds; the bound is applied when a task finishes and on a sweep every second.
store.file.pathpathsee belowrestart · opThe file store's root directory.
store.file.min_freesize256MBrestart · opShed new work below this much free disk; warn at twice it. "0" disables.
store.mcp.serverstringrequired for mcprestart · opThe MCP server whose tools hold state.
store.mcp.put, .get, .list, .delete{tool, args, ok, conflict, value, keys}state.put, state.get, state.list, state.deleterestart · opThe tool and argument mapping for each operation.
store.http.base_urlURLrequired for httprestart · opThe key-value service.
store.http.headersmap{}restart · opRequest headers; credentials as references.
store.http.get, .put, .list, .delete{method, url, body, value, keys, conflict_status}method GET, conflict_status 409restart · opThe request for each operation. get and put are required.

The file store's root is the first of: store.file.path, $AGENTD_STATE_DIR, $XDG_STATE_HOME/agentd/state, $HOME/.local/state/agentd/state, the OS temp directory. One process may hold a root; a second fails at startup naming the first one's pid, so a fleet needs mcp or http. Files are 0600 in 0700 directories, unencrypted. Running a durable daemon is Deploy; scaling it is Scale out.

streams

PathTypeDefaultClassWhat it does
streams.<name>object—live · docDeclares a durable event stream. An emit or stream start naming an undeclared stream is exit 2.
streams.<name>.retention.max_eventsint10000live · docTrim from the head past this many events.
streams.<name>.retention.max_ageduration—live · docTrim events older than this.

A stream fed by a webhook or A2A into: carries outside input; a workflow that reads it into model-driven steps reaching sensitive and egress servers is exit 2 (Security model).

workflows

A workflows: entry is an inline definition or a reference. All workflows paths are live · doc. The fields of an inline definition are in the Workflow language reference.

EntryFieldsWhat it does
inlinename, steps, …The definition itself.
filename, fileA local YAML or JSON file.
MCP resourcename, uriA resource a connected server serves, read at startup.
URLname, url, headers, timeout (default 20s), allow_private (default false)Fetched at startup; an unreachable URL or a body that does not parse refuses startup. headers follow the credential rules.
folderdir: <path> or dir: {path, glob, order}Every matching file becomes a workflow named by its own name:. glob defaults to *.yaml,*.yml,*.json; order is name or date. A folder that matches nothing is exit 2.

A workflow name is one definition. Two definitions that resolve to one name, from any two sources (including the instruction document's), are exit 2 at startup and refuse a reload. --validate-config parses inline definitions and local files and folders; url and uri definitions are checked at startup. security.workflows.immutable: true makes the loaded set read-only to the agent's workflow.* tools. The how-to is Author workflows.

A definition's act_for: {principal, via} names the person its schedule, loop and once firings act for with nobody there (needs xaa; Work nobody is there for). It is the operator's alone: an instruction document, a subagent template's machinery and a definition workflow.create or workflow.update stored may not carry one. It is part of the definition its digest is over, so a reload that changes it, or anything else in the workflow, suspends its firings until the person signs in again.

# fragment: under workflows:
- name: inline-one
  steps:
    start: {kind: manual}
    done:  {kind: finish, depends_on: [start]}
- {name: triage, file: ./triage.yaml}
- name: from-service
  url: https://config.internal/workflows/sync.yaml
  headers: {authorization: "Bearer {{secret:WF_TOKEN}}"}
- dir: {path: ./scheduled, glob: "**/*.yaml", order: name}

webhooks

The inbound webhook listener, for webhook start nodes and wait {on: webhook}. Needs the a2a feature.

PathTypeDefaultClassWhat it does
webhooks.listenhttps://host:port, or loopback http://unsetrestart · opThe listener. Required when any webhook node is used.
webhooks.urlhttps://host[:port][/prefix]unset (built on listen)restart · opThe URL callers reach the listener at, behind a proxy. Callback URLs are built on it. Not a wildcard host; loopback http:// aside, https://.
webhooks.tls.cert, .key, .client_capaths—restart · opThe listener's TLS identity, and a client CA for mTLS.
webhooks.default_auth.hmac.secretsecret—live · opHMAC-SHA256 over the body.
webhooks.default_auth.hmac.headerstringX-Signaturelive · opThe header carrying the signature.
webhooks.default_auth.hmac.prefixstring—live · opStripped before the compare (sha256=).
webhooks.default_auth.hmac.algosha256sha256live · opThe algorithm.
webhooks.default_auth.bearersecret—live · opA static bearer.
webhooks.default_auth.header.name, .equalsstring, secret—live · opA named header that must equal a secret.
webhooks.default_auth.noneboolfalselive · opNo auth, loopback only.
webhooks.default_auth.jwt{issuer, audience, max_lifetime?}—live · opA person's access token from the a2a.inbound_tokens issuer, for this webhook's own audience (never one an A2A token is issued for), living at most max_lifetime (5m, at most 15m), its jti never taken twice: the run is that person's, as a2a.principals names them. Taken only by a node of the operator's own configuration that names no auth: never by a served document's workflow or one workflow.create/update stored. Needs a2a and oauth.

A non-loopback webhooks.listen needs auth on every route: a default_auth, or one on each webhook node. Paths under /hooks/_ are agentd's own. A route answers as follows:

StatusMeaning
202kept: its run is in the durable inbox, or its event appended
401authentication failed — under auth.jwt, a token presented before included
200 duplicatea replay of a kept delivery (same idempotency key) within seven days
413larger than store.max_value_bytes
422the start node's inputs mapping cannot render it
429refused for pressure, with Retry-After
502a respond: sync route whose run failed
503refused inside the loop (a freeze, a failed write, an into: append refused)

a2a

The A2A listener, the principals it admits, and the peers agentd dials. Needs the a2a feature: a build without it refuses a2a.listen, a2a.peers and webhooks.listen at load. The concepts are The A2A channel; the API is A2A API reference.

Listener

PathTypeDefaultClassWhat it does
a2a.listenhttps://host:port, loopback http://host:port, or unix:///pathunsetrestart · opThe listener. An https:// listen needs a2a.tls.cert and .key. A non-loopback bind needs a2a.bearer or a2a.tls.client_ca.
a2a.urloriginthe bind addressrestart · opThe origin callers reach the listener at, published as the card's interface URL and the OAuth issuer: scheme://host[:port], lowercase, no default port, no path, https unless loopback. Required for a wildcard bind (0.0.0.0, ::).
a2a.tls.cert, .keypaths—restart · opThe listener's certificate and key.
a2a.tls.client_capath—restart · opVerify client certificates: every caller then needs one.
a2a.bearersecret—restart · opA static operator bearer.
a2a.cors.originslist of origins[]live · opBrowser origins admitted, matched exactly. * and paths are refused. A loopback UI must be listed too.
a2a.events.enabledboolfalserestart · opDeclare the events extension and serve the observation feed the display clients render.
a2a.introspection.enabledboolfalselive · opServe the operator introspection ops (transcripts, step detail, the log ring). admin.set can change it at runtime.
a2a.push.enabledboolfalselive · opAccept push-notification configs and deliver task updates to them.
a2a.push.allow_privateboolfalselive · opPermit push targets on private or loopback addresses.
a2a.resourceURI—restart · opThe resource identifier enterprise access tokens are issued for (their aud), canonical, on the listener's origin; its RFC 9728 metadata is served under it. Needs a2a.inbound_tokens.
a2a.inbound_tokens[]list[]restart · opThe authorization servers whose JWT access tokens (RFC 9068) name a caller: {issuer, name?, profile?, audiences?, jwks_uri?, algs?, typ?, subject_claim?, tenant_claim?, clients, require_user_token?, max_token_age?, max_chain_depth?, flow?, scopes_to_skills?}. A TCP listener and a2a.resource only. Needs a2a and oauth (Enterprise access tokens; every field in the A2A API reference).

With no credential at all, a loopback listener treats a local caller that presents nothing as the operator and warns at startup. Any credential mechanism ends that, and a request carrying Origin never gets it.

Principals

a2a.principals[] maps a caller's evidence to a principal. All paths are live · op.

PathTypeDefaultWhat it does
a2a.principals[].match.sanglob—Matches a verified client certificate's SANs or subject.
a2a.principals[].match.subglob—Matches a verified client certificate's subject CN.
a2a.principals[].match.bearer_refsecret—Matches a presented bearer. May not equal a2a.bearer, another rule's secret, or start with agentd_at_.
a2a.principals[].match.anyboolfalseNames every caller left; cannot grant operator.
a2a.principals[].match.uidint—The unix listener's peer, by the uid the kernel names (SO_PEERCRED): the per-person operator operator:<id>. Alone in its match, with role: operator and an id, on a unix:// listener. Needs xaa.
a2a.principals[].match.spki64 lowercase hex—A verified client certificate's key, the SHA-256 of its SubjectPublicKeyInfo: the rule admits that key alone, beside any san or sub. Needs a2a.tls.client_ca. With an id and idp_subject, a certificate principal that delegates; an operator one is operator:<id>.
a2a.principals[].match.token_ississuer—An enterprise access token from this a2a.inbound_tokens issuer; with sub, a glob over its subject. role user or agent, no id or idp_subject: the token names the person, user:oidc:<iss>#<subject> (#<tenant>/<subject> under tenant_claim). Without a rule, a token's caller is a user. The same rules name the person a webhook's auth.jwt token is.
a2a.principals[].idp_subject{iss, sub}—The one IdP identity this principal is, exact: required to delegate. On a match.uid rule, or a match.spki one with an id; iss is an identity.providers issuer. Several rules may each carry one, one per person. Needs xaa.
a2a.principals[].roleoperator | user | agent | anonymousrequiredThe caller's role.
a2a.principals[].idstringderivedThe principal id (<role>:<id>), unique across rules; 1..128 of A-Z a-z 0-9 . _ @ : / + -. Required on bearer_ref and any rules; a certificate rule without one is <role>:cn=<CN> or <role>:san=<first SAN>.
a2a.principals[].grantslist of op globs—The operations the principal may call.
a2a.principals[].labelsmap{}Operator-declared attributes carried into the run, the MCP _meta and the audit line.
a2a.principals[].quotas.raterate—Arrival quota at the listener; operators are exempt.
a2a.principals[].quotas.budgetbudget—A token budget charged by the work done for this principal.

A rule matches only a caller that every field its match names admits: san and sub together admit a certificate both match, and bearer_ref beside either admits the bearer only with a certificate they match. any names every caller left, so it is a rule's only matcher. A glob has one * for any run of characters. A san or sub rule matches only a certificate the listener verified, so it needs a2a.tls.client_ca and is refused at load without it; an any rule needs no CA. A user id may not reuse a name ever approved through the device grant.

Device grant

PathTypeDefaultClassWhat it does
a2a.device_grant.enabledboolfalserestart · opServe the OAuth device authorization grant: a client shows a code, an operator approves it.
a2a.device_grant.scopeslist of user | operator[user]restart · opWhat a client may request.
a2a.device_grant.token_ttlduration, 5m–30d8hrestart · opSession lifetime.
a2a.device_grant.code_ttlduration, 1m–30m10mrestart · opHow long a code waits for approval.
a2a.device_grant.verification_uriURLthe listener's own pagerestart · opWhere the approving person is sent.
a2a.device_grant.raterateunlimitedrestart · opPer session principal.
a2a.login.idpidentity.providers entry—restart · opDisplay clients sign people in at this IdP, each session the person it names, user:oidc:<iss>#<sub>; where a template admits them, the sign-in is their enrolment. The provider's sign_in.grants must include authorization_code. A TCP listener only, never beside a2a.tls.client_ca. Needs xaa and a2a (The A2A channel).
a2a.login.token_ttlduration, 5m–30d8hrestart · opSession lifetime.

It needs an operator credential (a2a.bearer, or an operator rule with bearer_ref) and is refused beside a2a.tls.client_ca and on a unix:// listener.

Peers

PathTypeDefaultClassWhat it does
a2a.peers[].namestringrequiredlive · docThe peer's name, unique.
a2a.peers[].endpointhttps://, loopback http://, or unix:///pathrequired unless servicelive · docWhere the peer listens.
a2a.peers[].servicestring—live · docA kind: peer service entry to inherit from.
a2a.peers[].headersmap{}live · docExtra headers; credentials as references.
a2a.peers[].authobject—live · docA credential provider.
a2a.peers[].client_cert, .client_keypaths—live · docA client certificate for an mTLS peer.

The how-to is Connect agents over A2A.

# fragment: under a2a:
listen: https://0.0.0.0:8443
url: https://desk.example.com:8443
tls: {cert: /tls/cert.pem, key: /tls/key.pem}
bearer: "{{secret:A2A_OPERATOR_TOKEN}}"
cors: {origins: ["https://ui.example.com"]}
principals:
  - id: acme-portal
    match: {bearer_ref: "{{secret:ACME_PORTAL_TOKEN}}"}
    role: user
    labels: {tenant: acme}
    quotas: {rate: "30/60s", budget: {windows: [{per: day, tokens: 200000}]}}
device_grant: {enabled: true}
events: {enabled: true}

Asking a person

Who answers an ask is spread over three keys, gathered here.

PathDefaultClassWhat it does
agent.ask_human_unownedfallbacklive · docSee agent.
agent.ask_human_fallbackfaillive · docSee agent.
security.policies[].to{role: operator}live · opWhich operator may answer an action: ask gate: {role: operator} narrowed by labels, or the id operator.

An addressee must be able to see the task, and a task is visible only to its owner and to operators. A to: naming any other principal, or a channel, is exit 2. The how-to is Human in the loop.

subagents

Templates the model may instantiate, and the defaults for every spawn. All paths are live · op.

PathTypeDefaultWhat it does
subagents.allow_freeformbooltruefalse makes templates the only spawn path.
subagents.defaults.modelstringroles.turnThe model a spawn runs on.
subagents.defaults.modesync | async | detached | warmsyncHow the parent waits.
subagents.defaults.prioritylow | normal | highnormalContention weight.
subagents.defaults.durableboolstore.durability.workfalse keeps the spawn's record in memory.
subagents.defaults.limitsobject—Per-spawn limits (max_steps, max_tokens, deadline, memory, cpu).
subagents.templates.<name>.instructionstring, or {text | file | dir | url | oci}requiredThe child's instruction document. Machinery in it (:::!workflow, :::!mcp, :::!stream, :::!config, :::!tools) makes the child an instance-tier daemon; otherwise it is a flat worker.
subagents.templates.<name>.params.<param>{type, required, default, enum, description}—The only values the model may fill, checked at spawn. type is string (default), number, integer or boolean.
subagents.templates.<name>.servers, .toolsliststhe parent'sFlat tier: narrow the parent's grants.
subagents.templates.<name>.model, .mode, .priority, .skills, .context—the defaultsPer-template overrides. Instance tier supports mode: detached, and sync with result.
subagents.templates.<name>.limitsobject—Flat tier: the spawn limits. Instance tier: memory and cpu only.
subagents.templates.<name>.budgetbudget—Instance tier: the child's own budget.
subagents.templates.<name>.output_contract, .output_schemastring, object—What the child must return.
subagents.templates.<name>.ttlduration—Instance tier: retire gracefully after this long.
subagents.templates.<name>.untilstring—Instance tier: a signal name whose delivery in the child retires it.
subagents.templates.<name>.singletonboolfalseOne live child; its peer alias is the template name.
subagents.templates.<name>.durableboolstore.durability.workfalse keeps the record in memory. A parent with a memory or none store runs every instance child non-durable.
subagents.templates.<name>.result.workflowstring—Instance tier, mode: sync: resolve the spawn when the child's named workflow first completes. Needs a parent A2A listener.
subagents.templates.<name>.mirror_streamslist—Instance tier: child streams forwarded into the parent's same-named streams. Needs a parent A2A listener.

The how-to is Delegate to subagents.

spawn

What every child process — turn worker, subagent, think child, instance — is started with. A child's environment is an allow-list: the baseline (PATH, HOME, TMPDIR, TZ, LANG, AGENTD_CRED_DIR, RUST_LOG, NO_PROXY, XDG_*, LC_*, SSL_CERT_*, OTEL_*, *_PROXY), the variables its own secret references and routed aws credentials read, and this list (Security model).

PathTypeDefaultClassWhat it does
spawn.env_passthroughlist of names and PREFIX* patterns[]live · opVariables every child also gets, when agentd has them. * alone, and a name a supervisor-only secret is read from, are refused.
spawn.uid, spawn.gidints ≥ 1— (agentd's own)restart · opThe uid and gid every worker runs as — turn workers, subagents, think children, an instance child's own workers, exec commands — with no supplementary group, inside its Landlock domain as before. Both or neither. Every capability is dropped with the uid, and no_new_privs set, so a worker can never set its uid back. Needs root, or CAP_SETUID, CAP_SETGID and CAP_KILL (with an instance-tier template, root); refused at load where agentd cannot drop to them, and for agentd's own uid, root's, or an external signer's (Security model). With spawn.instance_uid, an instance child's own workers run under that uid instead.
spawn.instance_uid, spawn.instance_gidints ≥ 1— (agentd's own)restart · opThe uid and gid every instance-tier child daemon runs as, with no capability, inside its Landlock domain; its directory is made its own first, and its own workers run under it too. Both or neither. Needs agentd to run as root; refused for root's, agentd's own, an external signer's uid or spawn.uid (Subagents).

goal

A goal watchdog that checks progress and acts. All paths are live · doc. A goal makes the instance long-lived.

PathTypeDefaultWhat it does
goal.statementstring—The goal in natural language, for the judge.
goal.check.everyduration5mThe check cadence.
goal.check.conditionCEL—A cheap predicate over durable state, evaluated first. Needs cel.
goal.check.viaboth | condition | agentbothCEL then the judge, CEL only, or the judge only. The judge runs on intelligence.roles.goal.
goal.stuck_afterint3Consecutive checks without progress before acting.
goal.on_achievedfinish | idle | replan | escalate, or {workflow}finishWhat to do when achieved.
goal.on_stucksamereplanWhat to do when stuck.

limits

The hard bounds the supervisor enforces. All paths are live · doc.

PathTypeDefaultWhat it does
limits.max_runsint8Workflow runs live at once.
limits.run.stepsint500Steps per run.
limits.run.tokensint2000000Tokens per run.
limits.run.deadlineduration3600sWall-clock per run; it also bounds a model call in flight.
limits.step_timeoutduration600sA step's timeout when it sets none.
limits.inline_max_bytesint65536A step output larger than this is stored as an artifact and replaced by a reference.
limits.max_message_depthint8Chained message deliveries before one is refused.
limits.workflow.fan_outint8Concurrent lanes a foreach or batch body may use; a definition asking for more is exit 2.
limits.subagents.depthint3How deep delegation may nest.
limits.subagents.breadthint8Live children at once.
limits.subagents.totalint64Children over the process's life.
limits.subagents.raterate8/2sSpawn rate.
limits.subagents.instances.breadthint2Live instance-tier children.
limits.subagents.instances.totalint8Instance-tier children over the process's life.
limits.subagents.instances.raterate4/1hInstance-tier spawn rate.

A one-shot that exhausts its steps or tokens exits 7, and one that passes its deadline exits 124; cli.md has the full table.

lifecycle

PathTypeDefaultClassWhat it does
lifecycle.run_untilauto | idle | drainedautorestart · docidle: a job that exits once no work remains. drained: a daemon that exits only on a signal; refused when nothing can give it work (no a2a.listen, workflow or agent.prompt; a webhook listener or a goal only starts workflows). auto: a daemon when the instance has an A2A listener or a long-lived start node, otherwise a job.
lifecycle.idle_graceduration5slive · docHow long an idle job waits before exiting.
lifecycle.until_signalstring—live · docA signal whose delivery begins a graceful shutdown.
lifecycle.drain_timeoutduration25srestart · opThe graceful drain budget on SIGTERM: how long work already running may finish before the drain stops it and leaves it for the next start. Keep it below the pod's terminationGracePeriodSeconds.
lifecycle.run_idstringa fresh ULIDrestart · opSent as agent/run_id in every MCP call's _meta, beside agent/instance. Set a stable one per unit of work (a Job name, an input hash) so servers can dedupe retries; the default correlates logs but dedupes nothing.
lifecycle.exit_code_mapmap: "3" or "7" → 0–255{}restart · opRemap the policy exit codes 3 (partial) and 7 (budget). Any other key is exit 2.
lifecycle.watch_configboolfalserestart · opReload when a config file, the instruction file or folder, a workflow file or folder, a skill file, or an AGENTS.md of agent.workspace on the way down to the working directory changes. Needs a --config file and the config-watch feature.

The long-lived start kinds are every kind but once and manual; they are listed in Workflow node reference and explained in Jobs, daemons and triggers.

identity

Who autonomous work is done for. All paths are live · op.

PathTypeDefaultWhat it does
identity.autonomous_asstringsystemThe principal a schedule, webhook, stream or once firing runs as. It may not be the per-person operator of a delegation (operator:<id>, the xaa feature): autonomous work never acts with an enrolled person's grant.
identity.labelsmap{}Labels stamped on that work.
identity.providers.<name>object—An enterprise IdP agentd is a client of: issuer, client_id (required), profile (generic | okta), client_auth, client_secret, sign_in, subject, subjects, jwks_uri, equivalent_issuers (the a2a.inbound_tokens issuers whose subjects are this provider's), ciba. Restart. Needs xaa; see Enterprise-managed authorization.
identity.providers.<name>.ciba{enabled, request_expiry, per_day}{false, 300s, 10}OpenID CIBA in poll mode at this IdP: how a via: ciba firing asks its person on their own device. request_expiry (1s–1h) is sent as request_expiry under profile: okta, else requested_expiry; per_day caps the requests one person is sent a day. The IdP's metadata must name its backchannel_authentication_endpoint and list poll. Restart.
identity.delegation.seal_key, .seal_key_previoussecret reference—32 random bytes, hex or base64, from a mounted file or the startup prompt — never the environment. Every grant's refresh token is sealed with seal_key; seal_key_previous only opens. Required with a grant: id_jag or token_exchange server under key_custody: local; refused under external. Restart.
identity.delegation.key_custodylocal | externallocalWho holds the seal key and every private_key_jwt key under identity.providers and on a delegated server: agentd, from secret references, or the external signer. Under external each such key is named by client_auth.kid with no key. Changing it means enrolling again. Restart.
identity.delegation.signer{socket, uid, timeout, seal_key, seal_key_previous}—key_custody: external only (required there): the signer's unix socket (an absolute path), the uid its process runs as (the socket's peer must be it; agentd's own uid is a load warning, since the peer check cannot then tell the signer from agentd's workers), the bound on a whole request, the connect included (5s, at most 60s), and the ids of its key-encryption key and of the one before a rotation, which only unwraps (The external signer protocol). Restart.
identity.delegation.at_cache_capduration60mThe longest an access token is used, whatever expires_in says. Restart.
identity.delegation.liveness_intervalduration15mAn access token is used only within this of the grant's last proof that its refresh token is alive. Restart.
identity.delegation.consent_timeoutduration24hHow long a consent gate waits before it denies. Reload.
identity.delegation.consent_session_max_ageduration12hA display session answers a consent gate only while its own IdP sign-in is younger than this. Reload.
identity.delegation.consent_per_minuteint10How many consent gates may open for one person a minute; a call past that is refused rather than asked. At least 1. Reload.
identity.delegation.templates[]list[]Who may enrol, for what: {name, principals: {source, role?, labels?}, idp, servers: {<name>: {scopes, document_workflows?}}, ttl, budget: {per_day, per_month, per_run, per_minute}, subagents, approvers, agent_mode_servers}. source is operator (a per-person operator), oidc (an a2a.login session), x509 (a certificate principal its rule pins by key) or device (a device session a pin or the first binding makes eligible); subagents is none (default) or inherit (a subagent acts with the grant at the delegated servers it declared); approvers is [principal] (default) or [principal, operator]; agent_mode_servers are the agent-mode servers tagged sensitive or egress the person's work may still use (every other one is hidden from it); document_workflows ({name, digest}, the hash workflow.list shows) are workflows an instruction document defined whose calls to the server are not document_sourced; per_minute (default 4) paces mints per person and server; clients admits only the callers whose enterprise access token one of these front ends was issued (source: oidc); unattended (never, the default, ciba or standing, which allows ciba too) lets an act_for workflow's firings act with the grant with nobody there, standing at a profile: okta IdP only with allow_against_idp_guidance: true; unattended_tools are the delegated <server>.<tool>s tagged egress a firing may call (every other is refused). No template matching, no enrolment. Reload: a narrowed one holds its grants at the next call; one removed, or a server removed from one, revokes what it allowed.
identity.delegation.device_pins.<name>{iss, sub}—The one IdP identity a device name delegates as. Needs a2a.device_grant. Reload: a changed or removed pin revokes what it bound.
identity.delegation.device_first_bindingboolfalseAn unpinned device name binds to the identity of its first enrolment (a load warning: a forwarded sign-in link binds it to whoever completes it). Reload: turned off, it revokes what it bound.

The acting principal and its labels reach the run record (run.start carries acting_for), each MCP call's _meta (agent/acting_for, agent/labels) and the audit line. A call to a delegated server carries the ID-JAG's sub as agent/acting_for instead, and no labels. A run is charged to the principal it is for, so quotas.budget covers work a person started, not only their turns. Labels are an operator-declared, closed set. This is attribution and quota, not multi-tenancy: a caller that needs a different tool surface needs a different process.

observability

PathTypeDefaultClassWhat it does
observability.log_leveltrace | debug | info | warn | errorinfolive · docThe JSON-lines log level on stderr.
observability.log_contentboolfalselive · opLog tool arguments and results, not only their lengths.
observability.metrics_addrhost:port or :portunsetrestart · opServe /metrics, /healthz and /readyz. Needs metrics.
observability.health_filepathunsetrestart · opA liveness file rewritten every second; alive turns false when the supervisor has not ticked for 10 seconds.
observability.events_ringint1024restart · opLog lines held in memory for the introspection ops; allocated only when introspection is on.
observability.traceparentW3C traceparenta random tracerestart · opContinue an upstream trace: the process's, and the work it was started to do.
observability.otel.endpointURL$OTEL_EXPORTER_OTLP_ENDPOINTrestart · opThe OTLP/HTTP base URL, http:// or https://; anything else is refused at load. Each run's spans are sent to <endpoint>/v1/traces. Needs otel; https:// also needs tls.
observability.otel.logsboolfalserestart · opAlso send the log lines to <endpoint>/v1/logs.
observability.audit.sinklist of log | store | streamunset (no audit records)live · opWhere audit records go. store needs a store; stream needs audit.stream.
observability.audit.streamstring—live · opThe declared stream sink: [stream] appends to.
observability.audit.chain.pathabsolute path—restart · opThe hash-chained audit log: every audit record appended, each carrying the SHA-256 of the one before, verified at every start; a chain that does not hold refuses the start. Written whatever sink says. The tombstone register is anchored in it. Closed segments are kept beside it as <path>.<first seq>. Its head is kept in the durable store: with store.kind: none or memory the load warns that a cut tail is not caught.
observability.audit.chain.rotate_atsize64MiBrestart · opThe size the open segment is closed at; at least 64KiB.
observability.audit.chain.retentionduration400drestart · opHow long a closed segment is kept after its last record. Each deletion is a record naming the segment's last record; a segment removed by hand refuses the start. Each segment's header carries the register, so no revocation is forgotten.
observability.runtime_events.streamstringrequiredlive · docA declared stream that receives selected runtime events.
observability.runtime_events.includelist of families[]live · docEvent families taken in full (the segment before the first dot).
observability.runtime_events.sampledlist of families[]live · docFamilies taken one in sixteen.
observability.runtime_events.queueint512live · docEvents that may queue between ticks before the tap drops and counts.
observability.status_valueslist of memory keys[]live · opMemory keys the status op publishes. Every caller of status sees them.

The events and metrics are in Events and metrics reference; the guide is Monitor in production.

security

Every security path is restart · op but security.policies, which a reload swaps whole or not at all (Reload classes).

PathTypeDefaultWhat it does
security.allow_trifectaboolfalseDowngrade the lethal-trifecta refusals to an audited warning. Refused from a discovered config.
security.egressopen | closedopenclosed refuses any outbound destination with no matching services: entry.
security.tls_capaththe bundled roots onlyExtra PEM CA certificates trusted for every outbound https:// dial, added to the bundled roots. Needs tls.
security.workflows.immutableboolfalseRefuse workflow.create, update and delete at runtime.
security.policies[]list[]Ordered verdicts on a tool call; the first match wins, no match is allow.
security.policies[].match.toolglobevery toolTool name. A Company's operation tool's call also matches as company.api's (operation tools).
security.policies[].match.tagslist of tags—Every listed tag must be on the tool.
security.policies[].match.callerlist of root | workflow | subagent—Who is calling.
security.policies[].match.principalglob—The principal id, for calls that carry one.
security.policies[].match.argsCEL—A predicate over args, tool and caller. Needs cel.
security.policies[].match.delegatedbool—The call is made with an enrolled person's grant.
security.policies[].match.taintedbool—The work making the call has read outside text — a delegated server's result (unless trusted_output), an untrusted_input tool's or an untagged server's, a server's skill, a webhook's or a non-operator principal's message (a person's own, or a per-person operator's, only to work not acting with their grant), a peer's answer, an http step's response, knowledge context, a served instruction, a tainted memory entry (Security model).
security.policies[].match.holds_delegated_databool—A delegated server's result has reached the work making the call.
security.policies[].match.any_tagslist of tags—At least one listed tag is on the tool.
security.policies[].match.unattendedbool—Nobody is there to answer: a trigger started the work (a schedule, a webhook, a stream), not a principal's request.
security.policies[].match.document_sourcedbool—A workflow an instruction document defined makes the call — for a delegated call, one the person's template does not list for the server in document_workflows.
security.policies[].match.modeagent | delegated—Whose authority the work acts with: an enrolled person's grant, or agentd's own.
security.policies[].actionallow | deny | ask | shadowrequiredshadow refuses and says the call was held.
security.policies[].questionstring—For ask: the question; {{tool}} (quoted), {{caller}} and {{args}} (the call's arguments as printable-ASCII JSON, at most 1,500 characters) are substituted — the gate carries agentd's badge, so what the work chose is shown escaped.
security.policies[].timeoutduration—For ask: how long to wait.
security.policies[].on_timeoutallow | deny | ask | shadowdenyWhat an unanswered ask becomes.
security.policies[].to{role: operator, labels}, operator or principal{role: operator}Who may answer an ask. principal is the person whose grant the work acts with: the gate is their consent gate — agentd's own text (no question), never judged, denied unanswered — and needs a delegated server (Enterprise-managed authorization).
security.policies[].once_perrun_tool—For a to: principal rule: ask at the first use per run — or conversation, or subagent — and tool alone; the person's approval, kept with the run in the store, passes later calls of the tool of that shape there — after a restart too — while the rule, their grant and their template are unchanged, for at most a day.
security.experimental.id_jagboolfalseAllow grant: id_jag and grant: token_exchange servers until ID-JAG reaches IETF working-group last call. Needs xaa.
security.exec.enabledboolfalseRun the exec tool locally. Needs the exec feature, which no release binary carries. Beside a delegation (identity.providers, a seal key, a delegated server) only as spawn.uid inside Landlock, with every supervisor-only path closed to that uid; the load names each piece missing.
security.exec.allowlist[] (deny all)Allowed command names (argv[0]).
security.exec.workdirpath—The directory commands run in; a requested cwd must resolve inside it.
security.exec.timeoutduration30sWall-clock per command.
security.exec.max_outputint1048576Captured stdout and stderr bytes.
security.exec.envlist of names[]Environment variables passed through.
security.cgroup.specauto or a pathunsetA cgroup-v2 parent for children: auto derives <own cgroup>/agent. Linux only, best-effort.
security.cgroup.memory_maxmax or a size—Per-child memory.max.
security.cgroup.pids_maxmax or a count—Per-child pids.max; counts threads.
security.aauth.provider, .key_file, .enroll_token, .enroll_assertion_file, .person_server——The AAuth agent identity. Needs aauth; see AAuth agent identity.
security.client.idHTTPS URL—agentd's OAuth client where it has no registration: the client_id of its Client ID Metadata Document, and where the document is published. A path of its own, with no userinfo, query, fragment or ./.. segment. An oauth2 block with no client_id uses it. Needs oauth, like every security.client path.
security.client.name, .logo_uristring, HTTPS URLrequired, —The document's client_name and logo_uri.
security.client.jwks_uriHTTPS URLjwks.json beside the documentThe key set's URL.
security.client.key_filepath/var/lib/agentd/client-keyThe ES256 signing key (PKCS#8), created 0600 when absent. Read where a token request is made.
security.client.published_key_fileslist of paths[]Keys whose public halves the key set lists and that are never signed with: the next key before a rotation, the previous one after.
security.client.grantslist of device | client_credentials | authorization_code[device]The grants the document lists in grant_types, with refresh_token beside device and authorization_code. A block that uses the document with another grant is refused at load.
security.client.redirect_urislist of loopback URIs[]For authorization_code, and only with it: http://127.0.0.1[:port]/path or http://[::1][:port]/path. A login listens on the first one's port, or on a free port when it names none.
security.client.serveboolfalseServe the document and the key set on the A2A listener, at their URLs' paths. Needs a2a, and a2a.url the client_id's origin. Otherwise publish what agentd --print-client-metadata writes.
security.companies.useroperator or a principal idoperatorThe one User this instance acts for at every Company. operator is refused when a2a.principals holds more than one rule of role operator. Needs companies, like every security.companies path.
security.companies.client.idHTTPS URLrequired with a company entryThe client_id: the URL of the client metadata document.
security.companies.client.name, .logo_uristring, HTTPS URLrequired, —The document's client_name and logo_uri.
security.companies.client.jwks_uriHTTPS URL<client_id origin>/companies/jwks.jsonThe key set's URL, on the client_id's host.
security.companies.client.key_filepath/var/lib/agentd/company-client-keyThe ES256 signing key (PKCS#8), created 0600 when absent. Only the supervisor reads it.
security.companies.client.published_key_fileslist of paths[]Keys whose public halves the key set lists and that are never signed with: the next key before a rotation, the previous one after.
security.companies.client.redirect_uriHTTPS URL<client_id origin>/companies/callbackWhere Direct Sign-In returns the User, on the client_id's host, with no query. Listed in the document's redirect_uris when a Company lists direct, which is [] otherwise.
security.companies.client.serveboolfalseServe the document and the key set on the A2A listener, at their URLs' paths, and the Direct Sign-In callback at redirect_uri's when a Company lists direct. Needs a2a, and a2a.url the client_id's origin. Otherwise publish what agentd --print-company-client writes.
security.companies.client.extensionslist of operations[]The extensions the document lists. operations (version 1): a Company may propose an action as an operation, which agentd confirms only with the User's approval or a standing permission (A Company's operations).
security.companies.standing[]list[]Standing permissions: an operation one covers is confirmed with approved_by: standing_permission, the User not asked. Each needs company, via, terms, when and expires. Needs client.extensions: [operations] and the cel feature.
security.companies.standing[].companyentry namerequiredThe company entry it covers.
security.companies.standing[].via{tool: <glob>} or {api: "METHOD /path-template"}requiredThe channel that proposed the operation: a glob of a Company tool's registered name (shop.exchange_*), or an operation api_allow lists. An operation proposed in a conversation never matches.
security.companies.standing[].termslist of term keysrequiredEvery top-level term key it covers: a revision with another key, or with no terms, asks the User.
security.companies.standing[].whenCELrequiredA predicate over terms and summary; a term it reads that is missing is no match.
security.companies.standing[].expiresRFC 3339 timerequiredWhen it stops applying.
security.companies.standing[].usesint ≥ 1no limitHow many confirms it covers: each reserved with the confirm, spent once the Company records a confirmation by it, and given back when the Company does not take the confirm or the operation ends unconfirmed. Counted per permission as written: one changed starts its count again.
security.companies.standing[].accountslist of account labelsdefault aloneThe User's accounts at the Company it covers, by the labels sign-ins gave them ([a-z0-9_-]{1,32}; Several accounts): an operation proposed in another account's Session asks the User. Without it, the account labelled default and the signed-out Session alone, so signing in to a second account never widens it.
security.companies.seal_keysecret reference—32 random bytes, hex or base64. Seals the Account Tokens a sign-in gives agentd, and the User ID records, so the store cannot read them. Required with any sign_in, and with a remote store, under key_custody: local; refused under external.
security.companies.key_custodylocal | externallocalWho holds the seal key: agentd, from seal_key, or the external signer signer names, which wraps each record's data key and derives the record-id keys, so agentd holds neither (The external signer protocol). A store sealed under one custody does not open under the other. Restart.
security.companies.signer{socket, uid, timeout, seal_key, derive_key}—key_custody: external only (required there): the signer's unix socket (an absolute path), the uid its process runs as (agentd's own is a load warning), the bound on a request (5s, at most 60s), its key-encryption key's id and its derivation key's id. Restart.
security.companies.browserslist of server names[]Browser MCP servers to keep off every Company's hosts; each must name an mcp.servers entry. A call on one with any string argument naming a Company's host as a browser reads it — or a host that is not ASCII — is refused unsent (company.browser.refused); their tools are served by the supervisor.
security.companies.limits.in_flightint4Company calls in flight per Company; one more is refused until one ends.
security.companies.limits.conversationsint16Open conversations in the instance, each followed by a reader; a new one is refused until one closes.
security.companies.context.localelanguage tag—Shared in a conversation message's context as locale (RFC 5646, en-US). Needs an entry that uses agent.
security.companies.context.time_zoneIANA name—Shared as time_zone (America/Los_Angeles).
security.companies.context.share_availabilityboolfalseShare user_available: whether the User has a client attached to the observation feed now.
security.payments.approveroperator or a principal idoperatorWho approves a payment the policy asks about: any operator principal, or that one principal (user:alice). A glob or a role is refused. Needs x402, like every security.payments path (Payments).
security.payments.approval_timeoutduration, 1s–7d1hHow long the approver is waited for; unanswered, the payment is declined.
security.payments.ask_when_taintedbooltrueAsk the approver about every payment of work that has read outside text, or that a model asked for, whatever its amount.
security.payments.max_validityduration, 10s–1h5mThe longest an authorization agentd signs is valid; the server's maxTimeoutSeconds when shorter.
security.payments.key_custodylocal | externallocalWho holds the wallet keys: agentd, each key file sealed under seal_key, or the external signer signer names (The external signer protocol).
security.payments.seal_keysecret reference—32 random bytes, hex or base64: every wallet key file is sealed under it. A single {{secret-file:…}} reference, never inline and never the environment. Required under key_custody: local; refused under external.
security.payments.signer{socket, uid, timeout}—key_custody: external only (required there): the signer's unix socket (an absolute path), the uid its process runs as (agentd's own is a load warning) and the bound on a request (5s, at most 60s).
security.payments.wallets.<name>.chaineip155 | solanaeip155The chain family it pays on: a secp256k1 key on EVM chains, an Ed25519 key on Solana. A step's max may name only assets of its wallet's family.
security.payments.wallets.<name>.key_fileabsolute path—key_custody: local: the sealed key file, made 0600 by agentd --print-wallet-address <name> when absent. The start is refused when it is a symlink, another user owns it or can read it, or it does not open to its address.
security.payments.wallets.<name>.keystring—key_custody: external: the signer's key id.
security.payments.wallets.<name>.addressaddress—The wallet's address — EVM, or base58 on Solana; a key of another address refuses the start.
security.payments.assets.<name>.assetCAIP-19 idrequiredeip155:<chain id>/erc20:<contract>, or solana:<genesis hash prefix>/token:<mint>: the token, and the network it is on.
security.payments.assets.<name>.token_programspl-token | token-2022spl-tokenA Solana token's program, which decides its token accounts and moves it. Refused on an EVM asset.
security.payments.assets.<name>.decimalsint, 0–36requiredThe token's decimals: "1" is 10^decimals atomic units.
security.payments.assets.<name>.limits{per_payment, per_run, per_day, per_payee}per_run, per_day, per_payee requiredCaps, each a decimal in whole units: one payment, one run's payments, every payment in any 24 hours, every payment to one payee in any 24 hours.
security.payments.assets.<name>.approve_abovedecimal—Above this amount the approver is asked.
security.payments.payees.<name>.addressaddressrequiredThe address paid (payTo): an EVM address — a mixed-case one is held to its EIP-55 checksum — or a Solana address in base58.
security.payments.payees.<name>.assetslist of asset namesevery assetThe assets it may be paid in, each on the address's chain family.
security.payments.rpc.<network>URL—By Solana network (CAIP-2), the RPC endpoint asked for a recent blockhash (getLatestBlockhash) when a server's entry gives none: https://, or http:// to this host, its {{secret:NAME}} references resolved when it is dialled. A network no asset is on is refused.

The model these keys enforce is Security model; the checklist is Harden a deployment.

Payments

security.payments is the spend policy every x402 payment is held to (the x402 feature, which release binaries carry): the wallets agentd pays from and who holds their keys, the assets it may pay in with their caps, the payees it may pay, and the one person who approves a payment above an asset's approve_above. A payment outside it is refused before anything is signed. It needs observability.audit.chain — every payment is a record there, the one before it is sent flushed first — and a store that outlives the process (file, mcp or http), which keeps the ledger. Every path is restart · op: a served document cannot set it, and a reload does not change it under a question the approver is reading.

A step pays only with its own payment field — {wallet, max, approve?} on an http, mcp.tool, a2a.send or a2a.delegate step (node registry) — which is literal and names a wallet and assets of the policy; an instruction document's workflow with one is refused at load, and one workflow.create or workflow.update stored fails the step without paying. The guide is Pay for things with x402.

vars

vars holds named constants, any JSON value, nestable. Live · doc.

vars:
  region: eu-1
  api_base: "https://api.eu-1.internal"
  batch: {size: 20}

agent:
  instruction: You keep the item index in sync.

intelligence:
  endpoints: https://api.openai.com/v1/chat/completions
  model: gpt-5-mini
  token: "{{secret:OPENAI_API_KEY}}"

workflows:
  - name: sync
    steps:
      start: {kind: manual}
      pull:
        kind: http
        depends_on: [start]
        url: "{{config.api_base}}/items?region={{config.region}}"
      each:
        kind: batch
        depends_on: [pull]
        over: "{{steps.pull.output.json}}"
        size: "{{config.batch.size}}"      # one exact token keeps the number type
        body:
          steps:
            note: {kind: noop}
      done: {kind: finish, depends_on: [each]}

{{config.NAME}} is substituted at load, before validation and before a workflow's hash, so a definition fetched from a URL and one written inline resolve the same way. A value that is exactly one token keeps its type; embedded tokens become text. An undefined reference is exit 2, every miss reported together. The namespace is config. because vars. is the run namespace an assign step writes. Values are not secrets: credentials stay {{secret:…}}.

Reload classes

A running daemon reloads on SIGHUP (the hot-reload feature) or a watched file change. The reload validates the new document with the startup pipeline, stages every change, and applies it whole or not at all. A change to any of these paths, by the configuration or by the instruction's own :::!config, refuses the reload with config.reload.restart_required:

  • agent.name, agent.document_capabilities, agent.instruction.trust
  • store.kind, store.prefix, store.mcp, store.http, store.file, store.max_value_bytes
  • lifecycle.run_until, lifecycle.drain_timeout, lifecycle.run_id, lifecycle.exit_code_map, lifecycle.watch_config
  • a2a.listen, a2a.tls, a2a.bearer, a2a.url, a2a.device_grant, a2a.login, a2a.events, a2a.resource, a2a.inbound_tokens
  • spawn.uid, spawn.gid, spawn.instance_uid, spawn.instance_gid
  • webhooks.listen, webhooks.tls, webhooks.url
  • observability.otel, observability.metrics_addr, observability.health_file, observability.events_ring, observability.traceparent, observability.audit.chain
  • security (every path but security.policies)
  • identity.providers, and of identity.delegation its keys and caps: seal_key, seal_key_previous, key_custody, signer, at_cache_cap, liveness_interval
  • a per-person operator's a2a.principals rule, added, removed or changed, while a delegation is configured
  • a services entry of kind: company, added, removed or changed
  • an mcp.servers entry of grant: id_jag or token_exchange, added, removed or changed

Every other path is applied live, as follows:

AreaWhat a reload does
intelligenceEach call resolves against the live settings, and a reload that moves any connection rebuilds every one. An in-flight turn follows swap_policy; running subagents take the new model at their next turn boundary. A token that cannot be resolved refuses the reload.
intelligence.routesRead as each turn is launched; a turn routed to a model the reload removed runs on roles.turn.
agent.instructionRe-read, through the servers the reload would run on, and its machinery folded again. A resource that cannot be read refuses the reload.
agent.workspaceIts AGENTS.md files read again; the next turn's prompt carries them. A file the load cannot take refuses the reload.
mcp.serversAdded or changed servers are dialed beside the running set; unchanged ones keep their connection; removed ones disconnect when the reload applies. A server is unchanged only when everything its dial reads is (endpoint, headers, auth, oauth, aauth, service, the service's rate, timeout), so a rotated credential re-dials it and a tags-only change does not.
tools, knowledge, search, skillsThe tool registry is rebuilt; the skills sources are listed again and both skills folders read again. A context keeps the skill bodies it loaded of the skills the new catalogue holds; a server's skill whose source was removed, or no longer admits it, is unloaded (skills.unloaded).
workflowsDefinitions are read and parsed before anything is dialed, then reloaded and re-armed. Live runs stay pinned to the definition they started on; a changed definition whose schedule node is unchanged keeps its next deadline. A definition the agent stored that names something the reload removed is left out (workflow.stored.invalid) and kept in the store.
a2a.principalsRecompiled and swapped into the live listener, so a demotion or a rotated bearer_ref applies to the next request.
security.policiesSwapped whole at the next call (security.policies.reloaded); a list that does not load, or whose rules could hold a tool a running turn or subagent dials itself, refuses the reload and the old one stands. A gate already open keeps the rule that raised it.
identity.delegation templates, pins, first binding, consent timingTaken at once: a narrowed template holds every grant it allowed to the new ceilings at the next call; a removed template, a server removed from one, a changed or removed pin and first binding turned off revoke the grants they allowed — at the IdP, tombstoned, off the loop — and drop the IdP proofs of those device names' sessions (delegation.bindings.cleared). A template's unattended and unattended_tools hold every firing from its next call.
a workflow's act_for, and the definition it is inA firing acts only while its workflow's digest is the one the person consented to when they last signed in: a changed definition's firings start nothing (start.unattended.suspended) until they sign in again. A firing already running keeps the definition it started on.
a2a.cors.originsReplaced in the live listener; removing an origin revokes it.
webhooks.default_auth and the webhook routesThe route table is rebuilt on every reload, so a rotated {{secret-file:…}} is picked up; each route's in-flight count and rate bucket carry over.
store.retentionThe next sweep applies the new bound.

An applied reload logs config.reloaded with the changed groups and is audited as config.reload; a refused one logs config.reload.invalid or config.reload.restart_required and changes nothing. A reload does not watch TLS files: the A2A and webhook listeners re-read a changed cert, key or client_ca file at the next connection, with no reload, and keep the last good identity when the new files do not load. security.tls_ca takes a restart. How a reload stages and commits is How configuration works; the operator's view is Operate a running daemon.

What a served document may write

An instruction document's :::!config fragment merges under the operator's configuration. It may write only these paths; anything else is refused at load, naming the line:

  • workflows, streams, mcp (but not mcp.servers[].events or event_grants, nor a server of grant: id_jag or token_exchange or any of its fields — auth.idp, .subject, .audience, .resource, .client_auth, .tool_scopes, .scope_mode, .tools_from, .tools_file, .id_jag, .id_jag_reuse, .on_revoked, .allow_unbound_at, .attribution, .max_at_age, .trusted_output, .dpop — on any server, nor a reference to a secret under identity.* or on a delegated server; nor a workflow's act_for), vars, context, goal, knowledge, search, memory, limits
  • intelligence.model, intelligence.models (a model it declares may not name a provider), intelligence.roles.turn, .think, .decide, .preflight, .compaction, .goal, intelligence.dialect, intelligence.budget, intelligence.timeout, intelligence.structured_output, intelligence.swap_policy
  • agent.name, agent.approval, agent.ask_human_fallback, agent.ask_human_unowned, agent.conversation_budget, agent.max_parallel_turns, agent.on_workflow_finished, agent.preflight, agent.wake_on
  • a2a.peers, tools.narrow, tools.disabled
  • skills.max_bytes, skills.max_loaded, skills.reference_prefix
  • store.kind, store.durability, store.checkpoint, store.on_error, store.timeout, store.max_value_bytes, store.prefix
  • lifecycle.run_until, lifecycle.idle_grace, lifecycle.until_signal
  • observability.log_level, observability.runtime_events

An operator's turn model wins over a document's; the document's is dropped with a warning. The directive itself is in Directive reference.

Credential-bearing paths

In a config file these must be {{secret:…}} or {{secret-file:…}} references; a literal is exit 2. From the environment or a flag they may be inline.

PathMessage when inline in a file
intelligence.tokenconfig file: intelligence.token carries an inline credential; …
intelligence.providers.<name>.tokenconfig file: intelligence.providers.<name>.token carries an inline credential; …
a2a.bearerconfig file: a2a.bearer carries an inline credential; …
security.aauth.enroll_tokenconfig file: security.aauth.enroll_token carries an inline credential; …
security.companies.seal_keyconfig file: security.companies.seal_key carries an inline credential; …
security.payments.seal_keysecurity.payments.seal_key: a single {{secret-file:PATH}} reference — never an inline key, and never the environment (a {{secret:…}} reference too)
mcp.servers[].oauth.client_secretconfig file: mcp server '<name>' oauth.client_secret carries an inline credential; …
a2a.principals[].match.bearer_refconfig file: a2a.principals[0].match.bearer_ref carries an inline credential; …
a webhook verifier's bearer, header.equals and hmac.secret: webhooks.default_auth, and the auth of every webhook start and webhook waitconfig file: workflows[0].steps.hook.auth.hmac.secret carries an inline credential; …
every auth.token, auth.client_secret, auth.value, auth.client_auth.key<where>: auth.token carries an inline credential; use a {{secret:…}} reference
a credential-shaped header (Authorization and similar) in intelligence.headers, a provider's, an MCP server's, a peer's or store.http.headersintelligence.headers['authorization'] looks like a credential but has an inline value; …

Credential values never reach the logs; --effective-config shows them as "<redacted>" and a reference as written.

Validation diagnostics

Validation runs after every layer merges and before any side effect: no MCP dial, no model call, no socket bound. It collects every problem in one pass, prints one line per problem on stderr, and exits 2. --validate-config runs it alone and exits 0 or 2.

{"event":"config.invalid","msg":"intelligence.roles.decide: \"smal\" is not a declared model (known: fast, smart)"}
{"event":"config.invalid","msg":"intelligence.routes: agent.preflight is never, so nothing would choose a route — set preflight to auto or always, or remove the routes"}
AreaExample message (exit 2)
typinginvalid AGENTD_LIMITS_RUN_STEPS: expected an integer, got "many"
unknown keyconfig file c.yaml parse error: unknown field `model`, expected `prompt`
endpointsintelligence endpoint must be https://host[:port][/path] (got: ftp://nope) · plaintext http:// intelligence is allowed for loopback only (dev); use https://
modelsintelligence.roles.turn ("smart") and intelligence.model ("smart") both name the turn model; say it once · intelligence.models: fallback cycle fast -> smart -> fast
providersintelligence.providers.dec.structured_output: prompt — a decisions endpoint answers decisions natively and is sent no schema; …
dialectintelligence.dialect: bedrock requires intelligence.auth.kind = aws (SigV4)
authintelligence: auth.token carries an inline credential; use a {{secret:…}} reference · …: auth.kind oauth2 needs `client_id`
secrets{{secret:LLM_KEY}} is not set in the environment (referenced at config.intelligence.token)
MCP servers--mcp: want name=endpoint (got: nope) · mcp.servers[]: duplicate server name 'web' · mcp.servers[]: the server name 'code' is reserved for code-registered tools
referencesstore.mcp.server 'state' is not a declared MCP server
storestore.kind is none but the instance is long-lived (…) — configure a durable store (store.kind: file | mcp | http), or drop store.kind to get the local file store by default · store.durability.steps: `eventual` is not implemented — …
instructionagent.instruction names 2 sources (text, file) — name exactly one · agent.instruction names no source — …
workflowsworkflow "w" step "s": unknown field "every" for kind "loop" (allowed: …) · workflows['w'] must have exactly one of file | uri | url | steps (dir is a separate entry shape)
webhooksa `webhook` node (start or wait) is used but webhooks.listen is not set — configure webhooks.listen (https://host:port) · webhooks.listen on a non-loopback address needs auth: …
A2A listenera2a.listen is https:// but a2a.tls.cert / a2a.tls.key are not set · a2a.listen on a non-loopback address needs client auth: a2a.bearer or a2a.tls.client_ca (…) · a2a.listen binds a wildcard host (https://0.0.0.0:8443), which is no address a caller can reach: set a2a.url …
A2A origina2a.url: "https://Desk.example.com:443/" is not in canonical form; write "https://desk.example.com" … · a2a.cors.origins: `*` is not allowed; list each exact origin (a loopback UI origin must be listed too) · a2a.events.enabled requires a2a.listen (it is served on the A2A listener)
principalsa2a.principals[0]: match needs one of san | sub | bearer_ref | any · a2a.principals[0]: a certificate rule (san | sub) matches only a client certificate the listener verifies — set a2a.tls.client_ca, or match a bearer with bearer_ref
build featuresa2a.listen needs the a2a build feature, which this binary was built without — … · …: cron schedules require the 'cron' build feature
lifecyclelifecycle.exit_code_map: only the policy codes 3 and 7 are remappable (got key "5") · lifecycle.watch_config requires a config file (--config / AGENTD_CONFIG)
trifectalethal-trifecta refused: the root grant wires untrusted_input + sensitive + egress into one agent; narrow the tags or set security.allow_trifecta (audited)

Warnings use the same channel as {"event":"config.warning",…} and do not change the exit code. Common ones: a one-shot with no durable store, a memory store, a TCP listener with no credential, a discovered config that sets an intelligence endpoint, and the MCP Events draft notice. The full event list is in Events and metrics reference.

A complete example

A durable triage daemon: two model connections, an A2A listener with mTLS principals, a subscription-driven workflow and a remote store. It validates with the two API keys set.

# yaml-language-server: $schema=https://agentd.dev/schema/config.json
# /etc/agentd/agentd.yaml — a durable triage daemon

agent:
  name: triage
  description: Triages the support inbox and escalates risky items.
  instruction: |
    You triage incoming support items. Escalate anything that mentions
    a refund over 500 EUR or a security problem.
  preflight: auto

intelligence:
  # The top-level connection: Anthropic.
  endpoints: https://api.anthropic.com
  dialect: anthropic
  token: "{{secret:ANTHROPIC_API_KEY}}"
  providers:
    openrouter:
      endpoints: https://openrouter.ai/api/alpha/decisions
      dialect: decisions
      token: "{{secret:OPENROUTER_API_KEY}}"
  models:
    smart: {model: claude-sonnet-5, fallback: fast}
    fast:  {model: claude-haiku-4-5}
    deep:  {kind: reasoning, model: claude-opus-5-5, reasoning: {effort: high}}
    jev:   {kind: decision, model: typesafe/jev-1.13, provider: openrouter, escalate: smart}
  roles:
    turn: smart
    decide: jev
    preflight: {model: jev, confidence: 0.8}
    compaction: fast
  routes:
    fast: "a status question or one simple operation"
    deep: "multi-step analysis that needs careful reasoning"
  budget:
    windows: [{per: hour, tokens: 2000000}]

mcp:
  servers:
    - {name: tickets, endpoint: "https://mcp-tickets.internal/mcp", tags: {"*": [untrusted_input]}}
    - {name: state,   endpoint: "https://mcp-state.internal/mcp"}

store:                                   # a daemon is durable
  kind: mcp
  mcp: {server: state}
  retention:
    runs: {keep_last: 1000, ttl: 30d}

a2a:
  listen: https://0.0.0.0:8443
  url: https://triage.internal:8443      # a wildcard bind says where callers reach it
  tls: {cert: /tls/cert.pem, key: /tls/key.pem, client_ca: /tls/clients.pem}
  principals:
    - {match: {san: "spiffe://example.org/ops/*"},  role: operator}
    - {match: {san: "spiffe://example.org/team/*"}, role: user, grants: ["workflow.*"]}

workflows:
  - name: watch-inbox
    steps:
      wake:   {kind: subscribe, server: tickets, uri: "tickets://inbox"}
      triage: {kind: agent, depends_on: [wake], instruction: "Triage the new item."}
      done:   {kind: finish, depends_on: [triage]}

limits:
  max_runs: 8
  run: {steps: 80, tokens: 150000, deadline: 5m}
  subagents: {depth: 3}

lifecycle: {run_until: drained, drain_timeout: 20s}

observability:
  log_level: info
  health_file: /run/agentd/health
  metrics_addr: "127.0.0.1:9090"
  audit: {sink: [log, store]}

security:
  cgroup: {spec: auto, memory_max: 2G}

Check it before deploying:

agentd --validate-config --config /etc/agentd/agentd.yaml
{"event":"config.valid","files":["/etc/agentd/agentd.yaml"]}

Any path can be overridden at deploy time without editing the file:

AGENTD_LIMITS_RUN_STEPS=120 agentd --config /etc/agentd/agentd.yaml --log-level debug

To try a config shape with no key and no network, point it at the built-in mock model. mock:final answers every turn at once, and mock:file:<playbook.json> answers from a script. Release binaries and the image carry it:

agentd --prompt "Say hello." --intelligence mock:final

Next steps