agentd — configuration reference

Complete TOML + CLI + environment-variable reference. This doc is the authoritative shape of the workflow config file and every operator-facing override.

Paired with capabilities.md (what each capability does) and architecture.md (how it's wired together).


1. File layout

A workflow file is a TOML document. Top-level sections (every one optional except name):

name        = "..."             # required
description = "..."             # optional, human-readable

[[start_nodes]]                 # 1 or more entry points
[[triggers]]                    # 0+ event bindings
[[http_routes]]                 # 0+ HTTP endpoints
[[nodes]]                       # the DAG
[[edges]]

[policy.*]                      # optional; empty = AllowAll
[auth.*]                        # optional; HTTP auth bindings
[server.tls]                    # optional; HTTPS / mTLS
[logging]                       # optional; base logging config

An alternative wrapped form is also accepted:

[[workflows]]
name = "..."
# ... same body ...

The wrapped form allows one workflow per file. The WorkflowDoc::from_toml parser accepts both.

Every section uses #[serde(deny_unknown_fields)] — a typo fails the parse with a clear line + column from the toml crate.


2. Top-level fields

name (required, string)

Workflow identifier. Surfaces in tracing (workflow_id), metrics spans, and the /healthz response.

description (optional, string)

Human-readable blurb. Not used by the runtime.


3. [[start_nodes]]

[[start_nodes]]
name       = "on_http_request"        # required; unique per workflow
source     = "http"                   # required; manual | http | event
entry_node = "load_resource"          # optional; must reference a nodes[].id

Multiple start nodes may share an entry_node — the graph body is reached from any entry point.

If entry_node is omitted, the engine picks the unique node with zero incoming edges at run time. Ambiguity (multiple root nodes and no explicit entry_node) fails with Error::Workflow::ambiguous_start_entry.


4. [[triggers]]

Typed external-event bindings.

mcp.resource.updated / mcp.resource.created

[[triggers]]
type     = "mcp.resource.updated"     # or mcp.resource.created
server   = "docs"                     # logical server name (informational for now)
resource = "docs://pages/*"           # URI pattern
start_node = "on_resource_update"     # must match a [[start_nodes]].name

internal.event

[[triggers]]
type = "internal.event"
name = "retry-requested"
start_node = "on_retry"

Status: Triggers are cross-referenced at validation time (start-node must exist), but the listener side for MCP subscriptions is not wired yet. Declared triggers serve as forward-compat documentation; the live surface is HTTP routes + manual invocation.


5. [[http_routes]]

Each route maps an HTTP verb + path to a named start node.

[[http_routes]]
method       = "POST"                       # required; case-insensitive at runtime
path         = "/webhooks/github"           # required; exact-match routing
start_node   = "on_push"                    # required; must exist in start_nodes
input_schema = "schemas/gh-push.json"       # optional; NOT enforced yet (future)
auth         = "hmac:github"                # optional; see §8 for grammar

[http_routes.rate_limit]                    # optional
capacity   = 10                             # required if block is present; > 0
per_second = 1.0                            # required if block is present; > 0 and finite

Routes are validated at HttpServer::spawn:


6. [[nodes]] + [[edges]]

Node shape

[[nodes]]
id   = "analyze"                            # required; unique per workflow
type = "llm_infer"                          # required; variant discriminator
# ...variant-specific fields...

# Optional retry policy:
[nodes.retry]
max_attempts = 3                            # ≥ 1
backoff_ms   = 500                          # default 100
on           = "transient"                  # any | transient; default any

The discriminator type is one of:

read_file, read_env, read_mcp_resource, parse_json,
template_render, json_select, diff_compute,
llm_infer, agent_loop,
write_file, create_dir, http_request, call_mcp_tool, shell_run,
call, parallel, map,
condition, switch, merge, fail, pause_for_approval, respond, terminate

See capabilities.md §1 for the fields each variant requires.

Edge shape

[[edges]]
from = "analyze"                            # required; must reference a node id
to   = "decision"                           # required; must reference a node id
when = "comment"                            # optional; matches the source node's branch label

The validator catches:

The engine catches at runtime:


7. [policy]

Fail-closed allowlist. Absent block → AllowAll (Phase-3 back-compat).

[policy.fs]
read   = ["/workspace/docs/**"]
write  = ["/tmp/agent-out/**"]
delete = []
list   = []                            # falls back to `read` when empty

[policy.env]
read_keys = ["DOCS_ROOT", "AGENTD_*"]

[policy.http]
urls    = ["http://api.internal.example/*"]
methods = ["GET", "POST"]              # optional; empty list = any method

[policy.shell]
commands = ["/usr/bin/git", "/usr/local/bin/mytool"]

[policy.mcp]
servers   = ["docs"]                   # informational for now
tools     = ["comment_on_page"]
resources = ["docs://pages/*"]

Matcher grammar

PatternMatches
"*"anything
"prefix/**"the exact path prefix OR anything starting with prefix/
"prefix/*"same as prefix/** (both are accepted for ergonomics)
"prefix*"any string that begins with prefix
literal ("/usr/bin/git")exact equality

Empty-section semantics

Every sub-section defaults to []. An empty list means deny everything in this category. Declaring [policy] with only [policy.fs] populated means fs reads/writes within allowlist, but env / http / shell / MCP are fully denied.

To allow-all a category, set "*":

[policy.http]
urls = ["*"]

Command canonicalisation (shell)

shell_run runs std::fs::canonicalize on the command path before passing it to Policy::check_shell_run. A symlink at /bin/foo → /usr/local/bin/forbidden matches against /usr/local/bin/forbidden — symlink escape is caught.


8. [auth]

HTTP-route auth bindings. Each binding has an operator-facing name referenced from [[http_routes]].auth.

Bearer

[auth.bearer.ops]
tokens_env = "OPS_TOKENS"               # newline-separated in the env var
# tokens = ["literal-token"]             # tests only; discouraged in prose

Both tokens_env and tokens flatten into the same token set at verification time — both sources contribute.

HMAC

[auth.hmac.github]
secret_env = "GITHUB_WEBHOOK_SECRET"
# secret = "literal"                     # tests only
header = "X-Hub-Signature-256"          # default "X-Agent-Signature"
prefix = "sha256="                      # default "sha256="

Route reference grammar

[[http_routes]]
auth = "none"             # or omit entirely
auth = "bearer"           # → bearer:default
auth = "bearer:ops"
auth = "basic:twilio"     # RFC 7617; [auth.basic.<name>] credentials_env = newline-separated user:pass
auth = "hmac"             # → hmac:default
auth = "hmac:github"
auth = "mtls"             # requires [server.tls.client_auth]

Startup validation

Every [[http_routes]].auth ref is parsed + looked up in [auth.*] at HttpServer::spawn. A missing binding fails the bind with a message like:

agent: workflow `foo`: auth ref `bearer:missing` is not defined in [auth.bearer]

8.5 [[secrets]] — pluggable secret sources

Every secret-consuming field (api_key_env, tokens_env, credentials_env, secret_env, MCP child env values, {{secret:NAME}} header placeholders) names a secret. By default the name is read from the process environment; a [[secrets]] entry gives the name a different source — and because resolution goes through one front door, declaring a source upgrades every consumer at once.

# Alias another env var.
[[secrets]]
name = "API_TOKEN"
source = "env"
var = "LEGACY_TOKEN_VAR"

# Live file read per use — rotation = replace the file. Covers k8s
# Secret mounts, Vault Agent sidecars, SOPS-decrypted files.
[[secrets]]
name = "DB_PASSWORD"
source = "file"
path = "/run/secrets/db_password"
trim = true                        # default: trim trailing whitespace

# Argv-declared command; stdout is the value. Cached until SIGHUP.
# Feature `secrets-exec`. Covers `op read`, `vault kv get`, `pass`.
[[secrets]]
name = "OP_TOKEN"
source = "command"
argv = ["op", "read", "op://vault/item/credential"]

# OAuth2 client-credentials grant; token cached until expires_in − skew.
# Feature `secrets-oauth2`. The client credentials THEMSELVES resolve
# through this registry (env / file / command compose).
[[secrets]]
name = "SALESFORCE_TOKEN"
source = "oauth2"
token_url = "https://login.salesforce.com/services/oauth2/token"
client_id_env = "SF_CLIENT_ID"
client_secret_env = "SF_CLIENT_SECRET"
scopes = ["api"]
extra_params = { audience = "https://api.example.com" }   # Auth0-style
auth_style = "body"                # or "basic" (RFC 6749 §2.3.1)
skew_secs = 60                     # refresh this early; default 60

Semantics:

9. [server.tls]

Behind the server-tls Cargo feature.

[server.tls]
cert_file = "/etc/ssl/server.pem"       # PEM, leaf-first cert chain
key_file  = "/etc/ssl/server.key"       # PEM PKCS8 / RSA / EC

[server.tls.client_auth]                # omit for HTTPS-only
mode    = "required"                    # only `required` wired today
ca_file = "/etc/ssl/client-ca.pem"     # trust root for client certs

Build without server-tls + a [server.tls] block in config → bind fails:

agent: workflow `foo`: workflow declares [server.tls] but this build
lacks the `server-tls` Cargo feature; rebuild with --features server-tls

mode = "optional" is parsed and deserialises successfully, but is rejected at config-build time with:

tls.client_auth.mode: only `required` is supported in this build

Optional mode is future work.

Artefact requirements


10. [logging]

[logging]
level   = "info"                    # EnvFilter directive (see below)
format  = "text"                    # text | json
target  = "stderr"                  # stderr | stdout | file:/var/log/agent.log
enabled = true                      # default true; --quiet forces false

level — EnvFilter directives

Accepts any tracing_subscriber::EnvFilter directive:

An invalid string silently falls through to "info" (behaviour of EnvFilter::try_new(...).unwrap_or_else(|_| EnvFilter::new("info"))).

target parse grammar

StringTarget
"stderr" (case-insensitive)LogTarget::Stderr
"stdout"LogTarget::Stdout
"file:<path>"LogTarget::File(<path>); parent dirs auto-created

File writes are synchronous under a Mutex<File>. For high-throughput workloads log to stderr and pipe into vector / filebeat.

Precedence chain

CLI flag  >  AGENTD_LOG_* env  >  [logging] block  >  built-in default (warn / text / stderr / enabled)

--quiet and AGENTD_QUIET=1 short-circuit: both force enabled = false regardless of config.


11. CLI flags + environment variables

Global flags (consumed before subcommand dispatch)

FlagEnv twinDefaultPurpose
--log-level LEVELAGENTD_LOGwarnEnvFilter directive
--log-format text|jsonAGENTD_LOG_FORMATtextTracing output format
--log-target TARGETAGENTD_LOG_TARGETstderrstderr / stdout / file:PATH
--quietAGENTD_QUIET=1offDisable tracing entirely

Run flags

FlagEnv twinDefaultPurpose
--config FILE / -c FILEAGENTD_CONFIGWorkflow file (required unless embedded)
--input FILE / -i FILEAGENTD_INPUTOne-shot trigger payload JSON
--start NAME / -s NAMEAGENTD_STARTautoStart-node name
--mode once|serveAGENTD_MODEinferredOverride auto mode-selection
--bind HOST:PORT / -b HOST:PORTAGENTD_HTTP_BIND127.0.0.1:8080Serve-mode bind
--timeout-secs NAGENTD_TIMEOUT_SECS120Per-run engine deadline
--drain-timeout-secs NAGENTD_DRAIN_TIMEOUT_SECS30Graceful shutdown grace
--intel-unix PATHAGENTD_INTEL_UNIXIntelligence backend Unix socket
--mcp-stdio "CMD ARGS"AGENTD_MCP_STDIOMCP server to spawn as a stdio child
--dry-runAGENTD_DRY_RUN=1offSkip every side effect
--validate-onlyAGENTD_VALIDATE_ONLY=1offRun validator and exit
--version / -VPrint version; exit 0
--help / -hPrint usage; exit 0

No CLI override

These deliberately have no CLI flag:


12. Build modes (Cargo features)

tools-fs                fs handlers (read_file, write_file, create_dir)     [default]
tools-env               read_env                                             [default]
tools-data              parse_json, json_select, template_render             [default]
tools-http              http_request (outbound plain HTTP/1.1 client)
tools-http-tls          https:// in http_request + the agent_loop http tool
                        (ureq, rustls — same stack as intel-remote; implies
                        tools-http; redirects never followed, so the policy
                        allowlist decision stays exact)
tools-shell             shell_run
secrets-exec            [[secrets]] source = "command" (argv-declared exec)
secrets-oauth2          [[secrets]] source = "oauth2" (client-credentials
                        grant via ureq/rustls; cached + self-refreshing)
tools-mcp               (pre-declared; MCP is currently always compiled when used)

trigger-http            HTTP listener (agent serve HTTP)                    [default]
trigger-mcp             (declared; subscription listener not wired)

intel-unix              (declared; Unix intelligence client always compiled)
intel-http              (declared; HTTP intel client not wired)

auth                    bearer + HMAC HTTP auth (pulls sha2 + hmac crates)  [default]
server-tls              in-process TLS termination + mTLS (rustls)          [implies auth]

legacy-plan-act         (removed in the R1 cleanup pass; no longer exists)

Compile-time artefact patterns

# Default — batteries-included dev build
cargo build -p agentd

# Sealed, minimal surface — read/compute only
cargo build --release -p agentd \
    --no-default-features \
    --features "tools-fs tools-data"

# Production HTTPS service (in-process TLS + HTTPS outbound)
cargo build --release -p agentd \
    --features "auth server-tls tools-http-tls tools-shell"

# Baked config (Mode B — RFC §11.2)
AGENTD_EMBED_CONFIG=./my-workflow.toml cargo build --release -p agentd

build.rs validates the embedded config at compile time. Typos, dangling edges, duplicate IDs, and unknown-binding auth refs fail the build with a clear error — they never land in the binary.


13. Canonical example — a hardened HTTPS webhook handler

name        = "notify_router"
description = "Routes webhook events to downstream MCP tools"

# --- Logging -------------------------------------------------------

[logging]
level   = "agent=info,agentd::audit=warn,hyper=off"
format  = "json"
target  = "file:/var/log/agent.log"

# --- TLS + mTLS -----------------------------------------------------

[server.tls]
cert_file = "/etc/ssl/notify-router.pem"
key_file  = "/etc/ssl/notify-router.key"

[server.tls.client_auth]
mode    = "required"
ca_file = "/etc/ssl/internal-ca.pem"

# --- Auth bindings --------------------------------------------------

[auth.bearer.ops]
tokens_env = "OPS_TOKENS"

[auth.hmac.github]
secret_env = "GITHUB_WEBHOOK_SECRET"
header     = "X-Hub-Signature-256"

# --- Policy ---------------------------------------------------------

[policy.http]
urls    = ["http://api.internal.example/*"]
methods = ["POST"]

[policy.shell]
commands = ["/usr/bin/git"]

[policy.mcp]
tools     = ["page_oncall", "post_to_slack", "open_jira"]
resources = ["internal://events/*"]

# --- Entry points ---------------------------------------------------

[[start_nodes]]
name       = "on_github"
source     = "http"
entry_node = "classify"

[[start_nodes]]
name       = "on_ops"
source     = "http"
entry_node = "classify"

# --- HTTP routes ----------------------------------------------------

[[http_routes]]
method     = "POST"
path       = "/webhooks/github"
start_node = "on_github"
auth       = "hmac:github"
[http_routes.rate_limit]
capacity   = 30
per_second = 5.0

[[http_routes]]
method     = "POST"
path       = "/ops/page"
start_node = "on_ops"
auth       = "bearer:ops"
[http_routes.rate_limit]
capacity   = 5
per_second = 0.5

# --- DAG ------------------------------------------------------------

[[nodes]]
id = "classify"
type = "llm_infer"
backend = "default"
prompt  = "Classify this event. JSON: {\"route\":\"pager\"|\"chat\"|\"ticket\"}.\n\n{{payload}}"
input_from    = "trigger"
output_schema = "schemas/route.json"
[nodes.retry]
max_attempts = 3
backoff_ms   = 300
on           = "transient"

[[nodes]]
id = "dispatch"
type = "switch"
expr = "classify.parsed.route"

[[nodes]]
id = "pager"
type = "call_mcp_tool"
tool = "page_oncall"
args_from = "trigger"

[[nodes]]
id = "chat"
type = "call_mcp_tool"
tool = "post_to_slack"
args_from = "trigger"

[[nodes]]
id = "ticket"
type = "call_mcp_tool"
tool = "open_jira"
args_from = "trigger"

[[nodes]]
id = "done"
type = "terminate"

# --- Edges ----------------------------------------------------------

[[edges]]
from = "classify" to = "dispatch"
[[edges]]
from = "dispatch" to = "pager"  when = "pager"
[[edges]]
from = "dispatch" to = "chat"   when = "chat"
[[edges]]
from = "dispatch" to = "ticket" when = "ticket"
[[edges]]
from = "pager"  to = "done"
[[edges]]
from = "chat"   to = "done"
[[edges]]
from = "ticket" to = "done"

Invocation:

export OPS_TOKENS="$(cat /etc/agentd/ops.tokens)"
export GITHUB_WEBHOOK_SECRET="$(cat /etc/agentd/github.secret)"

agentd --config /etc/agentd/notify-router.toml \
      --bind 0.0.0.0:8443 \
      --intel-unix /run/intelligence.sock \
      --mcp-stdio "/usr/local/bin/mcp-ops --root /var/ops" \
      --drain-timeout-secs 60

Shutdown: systemctl stop agent (or kill -TERM $(pidof agent)). Any in-flight requests get up to 60 seconds to complete; the process exits 0 on clean drain, 5 on forced.


14. Validation modes

Three points where the harness validates your config:

Build time (build.rs, only with AGENTD_EMBED_CONFIG)

Runs a strict-subset validator. Catches structural errors that typos produce:

Workflow load (every start)

Runs the full workflow::validate. Adds to the build-time checks:

Server spawn (serve mode)

Adds:

Any failure at any layer fails fast with a structured JSON report (build + load) or a printable error (server spawn). The binary never runs with a config the runtime hasn't fully vetted.


15. Quick reference: every knob in one table

ScopeKnobDefaultNotes
WorkflownameRequired
WorkflowdescriptionInformational
Start nodesourceevent / http / manual
Start nodeentry_nodeautoFalls back to sole in-degree-0 node
HTTP routemethodCase-insensitive
HTTP routeauth"none"See §8
HTTP routeinput_schemaNot enforced yet
HTTP routerate_limit.capacity> 0
HTTP routerate_limit.per_second> 0, finite
Noderetry.max_attempts1≥ 1
Noderetry.backoff_ms100Linear ramp
Noderetry.onanyany / transient
Policy fsread/write/delete/list[] (deny)See §7
Policy envread_keys[] (deny)
Policy httpurls[] (deny)
Policy httpmethods[] (any)Empty = any
Policy shellcommands[] (deny)Canonicalised paths
Policy mcptools[] (deny)
Policy mcpresources[] (deny)
Auth bearertokens_envNewline-separated
Auth hmacsecret_env
Auth hmacheaderX-Agent-SignatureCase-insensitive match
Auth hmacprefixsha256=Empty string honoured
Server TLScert_filePEM leaf-first chain
Server TLSkey_filePEM PKCS8/RSA/EC
Client authmodeOnly required wired
Client authca_fileTrust root
LogginglevelwarnEnvFilter string
Loggingformattexttext / json
Loggingtargetstderrstderr / stdout / file:PATH
Loggingenabledtrue
CLI / envtimeout-secs120Per-run engine deadline
CLI / envdrain-timeout-secs30Server-mode SIGTERM grace
CLI only--modeinferredOverride once / serve