agentd is the reference implementation of an agent driven by the agentctl
Kubernetes control plane. agentctl depends only on the published Agent Control
Contract (ACC) — never on the binary's code (principle P0). This document records
agentd's conformance to ACC v1 (contract_version 1.0), surface by surface:
what passes, the gaps closed to get there, and the P-series items deliberately
deferred.
Naming model. The product/binary/image is
agentd, and it now emits the neutral ACC tokens only —agent://resources,agent_metrics,agent_version,agent/*_meta, andAGENT_*env (documented). The legacy branded spellings are still accepted on input (graceful) but never emitted. The image isghcr.io/agentd-dev/agentd. The golden fixtures were re-captured from theagentd 1.0.0binary (agent_version: "1.0.0"); the agentctl fixture tests moved in lockstep. Where this document still cites a branded form below, read it as the legacy input alias.
- Contract (consumed, never edited by
agentd):/root/agentctl-dev/source-code/contract/—README.md,SPEC.md,schemas/*.json,fixtures/capabilities/*. - Conformance is judged by behavior against those artifacts (ACC SPEC §8), not by sharing code. The neutral spellings are emitted; the branded spellings remain accepted on input to GA (L4).
| Evidence | Result |
|---|---|
cargo test -p agentd (full feature matrix) |
579 passed / 0 failed |
cargo test -p agentd (default features) |
406 passed / 0 failed |
Black-box conformance suite (cargo run -p agentd-conformance) |
38 passed / 0 failed |
| ACC schema+behavior harness (drives the real binary; see Validation report) | 22 passed / 0 failed |
Golden --capabilities fixtures vs manifest.schema.json (all 4, agentctl-owned) |
4 / 4 valid |
agentctl agent-contract-client fixture tests |
6 / 6 pass (cross-repo consistency) |
cargo clippy --all-targets -D warnings (default + full) |
clean |
Hard invariants preserved: the manifest is built json!→Value (the Secret
type has no Serialize, so it cannot reach it — L5); intelligence is structural
only; no deny_unknown_fields on any discovery surface; config-input is the one
closed surface; the lethal-trifecta gate is the single validate() authority;
no credential reaches the manifest / config file / identity path.
| Surface | ACC schema | Status | Notes |
|---|---|---|---|
| Capabilities manifest | manifest.schema.json |
✅ PASS | json!→Value (secret-safe); all required root keys; emits the neutral agent_version (the legacy branded agentd_version is not emitted) + surfaces.events_schema (when served). Sum types correct. Live agent://capabilities is parsed-equal to the one-shot. |
| Management profile | management-profile.json |
✅ PASS | Frozen order [drain, lame-duck, pause, resume, cancel]; Management-gated; a non-Management caller of an operator tool or resource (read and subscribe) → -32601. attach not a tool; no force. drain ≡ SIGTERM ≡ exit 0; lame-duck readiness-only; cancel{handle:"0"|omitted} = whole run. |
| Exit codes | exit-codes.table.json |
✅ PASS | Table + pod_failure_intent exact; unknown → retriable; EXIT_CODES="1.0". Clean drain = 0, not 143. Only 3 & 7 remappable, via the new --budget-exit-code. |
| Run-outcome report | report.schema.json |
✅ PASS | 12 required keys; report_schema="1.0"; mode ∈ {once,loop,schedule} (never reactive); 9-value closed status; distillate_ref ^(agent|agentd)://; instance/trace_id omitted-not-null; tokens never currency. A real once-run report validates. |
| Metrics | metrics.registry.json |
✅ PASS | metrics_schema="1.0"; all 34 stable non-cgroup names present on a live scrape (agent_memory_* are cgroup-v2-conditional); no histogram samples; agent_saturation the only float; bounded labels only; agent_pending_events canonical (no agent_reactive_backlog). |
| Events | events.schema.json |
✅ PASS | agent://events read body = {events_schema:"1.0", oldest_seq, newest_seq, dropped, events[]}; each line carries monotonic seq + the required tuple; level/comp closed, event open; lossy ring; ?after/level/event parsing. Accepts the neutral agent:// scheme. Validates live. |
| Config file | config.schema.json |
✅ PASS | The one closed surface — typo → exit 2. --validate-config 0/2 (good→0, typo→2, inline secret-shaped header→2). --config-schema emits a valid draft-2020-12 closed doc, x-agentd-contract-version="1.0". Restart-only keys rejected on a live reload. |
| A2A | a2a.methods.json |
✅ PASS | 6 live methods served, 5 gateway-owned → -32601; closed error set -32001/-32601/-32602/-32603; Management-gated; COMPLETED task = exactly one <taskId>.distillate artifact; framed streaming until final; TASK_STATE_* mapping; surfaces.a2a emitted. Live drive confirms the error codes. |
| Env convention | env-convention.json |
✅ PASS | Downward-API identity; neutral AGENT_* is the documented form, the legacy AGENTD_* still accepted on input (env, identity, per-endpoint tokens). Empty → unset; run_id always present; AGENT_SHARD "K/N" rejects N==0/K>=N → exit 2; credentials only on the *_TOKEN[_FILE] path. |
Each artifact below is produced by the running binary and validated against its contract schema with a draft-2020-12 validator (or asserted behaviorally).
[PASS] config:--config-schema — draft2020-12 closed schema, x-contract=1.0
[PASS] config:good-file vs schema — valid vs config.schema.json
[PASS] config:--validate-config (good)→0 — exit 0
[PASS] config:typo'd key→exit 2 — exit 2
[PASS] config:inline secret header→exit 2— exit 2
[PASS] report:once-run vs schema — valid vs report.schema.json
[PASS] report:mode≠reactive / distillate_ref scheme / exit_code
[PASS] manifest:live agent://capabilities vs schema — valid vs manifest.schema.json
[PASS] manifest:neutral agent:// scheme accepted
[PASS] manifest:live≈--capabilities (semantic, modulo run_id + live intel overlay)
[PASS] events:agent://events read-body vs schema — valid vs events.schema.json
[PASS] a2a:GetTask unknown id→-32001 / gateway method→-32601 / empty text→-32602
[PASS] metrics:34 stable names present / no histogram samples / pending_events canonical
[PASS] exit:bad flag→2 / intel down→4 / --budget-exit-code leaves non-budget (4) untouched
22/22 checks passed
The black-box agentd-conformance suite independently drives the exit-code table
under induced failures: exit-0-on-success, exit-2-on-bad-flag,
exit-2-on-validation, exit-4-on-intel-down, exit-6-on-required-mcp-down,
drain-0-on-sigterm (clean drain = 0, not 143), spawn-rate-refused — all pass.
cargo run -p agentd-conformance # 38 behavioral checks
cargo build -p agentd --features "serve-mcp,a2a,events,metrics,cluster,internal-mocks"
python3 acc_validate.py ./target/debug/agentd # 22 live schema+behavior checks
# fixtures vs schema:
for f in /root/agentctl-dev/source-code/contract/fixtures/capabilities/*.json; do
python3 - "$f" <<'PY'
import json,sys; from jsonschema import Draft202012Validator
s=json.load(open("/root/agentctl-dev/source-code/contract/schemas/manifest.schema.json"))
print("VALID" if not list(Draft202012Validator(s).iter_errors(json.load(open(sys.argv[1])))) else "INVALID", sys.argv[1])
PY
doneThe golden corpus in contract/fixtures/capabilities/ is owned by the agentctl
contract repo and pinned by its agent-contract-client fixture tests
(tests/fixtures.rs), which assert exact content — default.json version == "2.5.0" and surfaces.claim as an object, plus the full-features.json surface
values. They remain the real --capabilities captures from agentd 1.0.0 and
all four validate against the current manifest.schema.json.
The current binary's --capabilities is a superset of the 2.5.0 capture
(it adds the additive agent_version, surfaces.events_schema, and
intelligence.discovery/models) and is validated live against
manifest.schema.json — that live validation, plus the behavioral suites, is the
authoritative conformance proof, not a static fixture. Both sum-type branches of
every capturable key are covered: the off/null branches by default +
minimal-degraded, the string/object branches by full-features + reference-full.
Re-capture is a coordinated agentctl-side change, intentionally not forced here. Regenerating
default.json/full-features.jsonfrom the v2.8.1 binary was attempted (deliverable #2) but reverted by the contract owner, because the capture would moveversion2.5.0→2.8.1 anddefault.json'sclaimfrom object to omitted (a bare release build has noclusterfeature), breaking the pinnedfixtures.rsassertions. Since the agentctl repo owns the fixtures and their tests, re-capturing must move both in lockstep there; the regenerated v2.8.1 captures are available for adoption. Forcing them from agentd would make the two repos inconsistent (a failing agentctl test), so they are deliberately left to the owner.intelligence.healthystays"unknown"in any one-shot capture (the probe is network-free admission, ACC SPEC §3) — its boolean branch is reachable only on the live resource and is unit-tested there.
Implemented per RFC 0011 §5.2 / exit-codes.table.json x-budget-exit-code-remap
(previously doc-only). Remaps only the two operator-tunable policy budget
codes — EXIT_PARTIAL (3) and EXIT_BUDGET (7) — to N at the process exit a
Job's podFailurePolicy observes; every other code (deadline 124, refusal 5, clean
0, kernel 137) is untouched. N is range-checked to 0..=255 (else exit 2). The
run report keeps the canonical 3/7 projection + precise status, so the durable
record stays truthful and report.schema-valid. New: exit::apply_budget_remap,
Config.budget_exit_code (flag + help), wiring in run_once, unit tests.
Branded forms stay accepted and emitted (none dropped); neutral spellings are now also accepted on input (cutover to neutral-primary is a GA decision):
- Env (
config.rs::debrand_env) — one normalization pass: anyAGENT_<X>synthesizes the brandedAGENT_<X>iff absent (branded wins on conflict, so every fielded deployment is byte-for-byte unchanged); every config env var is covered with no per-read change. - Identity (
identity.rs) —AGENT_POD_NAME/UID/NAMESPACE/NODE_NAMEneutral-first, branded fallback; empty → unset retained. - Per-endpoint tokens (
intel/endpoints.rs) —AGENT_INTELLIGENCE_TOKEN[_N][_FILE]accepted; value stays opaque (L5). - Resource URIs (
agentd_uri.rs) —agent://…accepted on read alongsideagent://…for every resource; still emits brandedagent://. - Manifest (
capabilities.rs) — emits neutralagent_versionnext toagentd_version, plus additivesurfaces.events_schemawhen the stream is served.
A non-Management caller of an operator tool or resource — inventory,
intelligence, capacity, config/effective, events — now uniformly gets
-32601 METHOD_NOT_FOUND on both resources/read and resources/subscribe
(previously some returned -32002 RESOURCE_NOT_FOUND), so a stdio peer can't even
confirm the surface exists. cancel{handle:"0"}/omitted fans a whole-run cancel.
The de-branding, L7-read, and
cancelwork above predated this commit as uncommitted working-tree changes; it has been gated (build/test/clippy green) and is committed here together with--budget-exit-codeand the L7-subscribe/ events uniformity completion.
| Ask | Disposition |
|---|---|
CC/P6 — manifest/config as consumable schemas; --config-schema/--validate-config round-trip |
RESOLVED. Draft-2020-12 closed --config-schema; --validate-config 0/2; drift tests pin it to the struct + contract_version. |
| P3b — versioned golden corpus per feature-set | PARTIAL (owner-pinned). The four-fixture corpus exists and validates against the current schema, exercising both branches of every capturable sum-type. The corpus is agentctl-owned and currently pinned to the 2.5.0 captures by fixtures.rs; a broader per-feature matrix keyed by (major.minor + digest), and re-capture to the current version, are coordinated agentctl-side tasks (see Golden fixtures). |
P2 — freeze A2A wire strings + surfaces.a2a |
RESOLVED (reference binding). surfaces.a2a emitted; reference PascalCase a2a.* served. Normative spelling stays open per the contract; a gateway translates. |
| P10 — reconcile autoscaling metric names | RESOLVED (source wins). Only agent_pending_events emitted; agent_reactive_backlog an alias-only, never on the scrape; agent_tokens_per_sec/agent_intelligence_latency_ms stay provisional/not-emitted. |
| P-pause — pause/resume served | RESOLVED. Both ship (frozen order), Management-gated, reflected in agent_paused. |
P4 — agent://metrics text body + agent://capacity schema |
DEFERRED (contract: OUT OF SCOPE / downstream). surfaces.metrics carries the scrape address; the byte-identical Prom-text resource + capacity schema are undefined upstream. agent://capacity is served (cluster builds) but its frozen schema is not pinned. No agentd change for v1. |
| P5 — re-readable distillate after exit | DEFERRED (contract-open). distillate_ref points; the durable copy is the report file + stdout. |
| P-trace — traceparent ingest on the A2A surface | DEFERRED (contract-open). trace_id uses the existing env/_meta ingest path. |
P3 (shard) — --shard auto/N derive K from the pod ordinal |
DEFERRED (owner = agentctl). agentd parses explicit K/N and rejects N==0/K>=N (exit 2); deriving K is the control plane's job. |
P-grace — AGENT_POD_GRACE_SECONDS |
DEFERRED (contract-flagged provisional). Not read in source (drain budget = --drain-timeout/AGENT_DRAIN_TIMEOUT); the neutral alias would nonetheless be honored by debrand_env. |
intelligence.healthyboolean is not capturable from--capabilities— the one-shot probe is network-free admission, always pre-connect"unknown". The boolean branch is unit-tested on the liveagent://intelligence/capabilitiesread._metainput de-branding is N/A for v1 — agentd's servedtools/callhandler consumes no incoming_metakeys (onlyname/arguments); itsagentd/*keys are all outbound stamps onto downstream MCP servers (kept branded, the accepted current form). There is no incoming_metaread site to alias.run_idis an opaque stable string, not a Crockford ULID — no schema enforces a ULID pattern (manifest/reportrequire only a non-empty string); agentd mintsformat!("{millis:x}{pid:x}"), stable across a retried Job whenAGENT_RUN_ID/AGENT_RUN_IDis set. Switching the format is unnecessary for conformance and risk-bearing, so it is left as-is.--config-schemais the strict serde mirror (reloadable keys only, closed); the contract'sconfig.schema.jsonis the broader VIEW that also declares restart-onlymode/interval/cronfor WARN purposes. A file with those keys is exit 2 in the reference (deny_unknown_fields) — which the contract anticipates.- Exit 124 is reachable via the supervisor force-kill of a stuck/deadline
subtree — consistent with
x-mode-reachability(intentpolicy). The "unreachable viaonce_exit()" note is about that internal fn only.
The contract's config.schema.json, metrics.registry.json, and
exit-codes.table.json were reconciled upstream to match the reference (agentctl
commit 68a61b8, "source-wins" per L8 — intelligence/model_swap config keys,
the agent_intel_all_down stable gauge, code 124 returned_by_agent:true). agentd
made no edit to any ACC schema in this pass; remaining disagreements are
recorded here as asks rather than schema edits:
- C1 —
SPEC.md§4.4 metric count is stale. The prose says "46 records — 29 stable, 8 legacy, 9 provisional"; the registry JSON now has 51 records — 36 stable, 8 legacy, 7 provisional. agentd emits a superset of the stable set; the registry JSON, not the prose, is authoritative. Suggest updating the prose. - C2 — "ULID" wording for
run_id(note 3) is not schema-enforced and not met literally by the reference; suggest softening to "opaque stable id (ULID recommended)".