Agent Control Standard (ACS)
Signed, fail-closed control of Scrydon agent lifecycles.
Scrydon implements the Agent Control Standard (ACS) v0.1.0 wire contract. ACS
separates an Observed Agent, which reports a step before acting, from a
Guardian Agent, which returns allow, deny, modify, ask, or defer.
In the default deployment, Agentic is the Observed Agent and API Platform is the Guardian. Their traffic is organization-scoped, signed, replay-protected, and persisted through Dapr so a workflow can resume on another replica without starting a second control session.
Active conformance profiles
Profiles are negotiated for each session. Scrydon only declares a profile when the running composition has the required methods, algorithms, and sinks.
| Profile | Agentic Observed Agent | API Platform Guardian |
|---|---|---|
acs-core | Not claimed by the lifecycle-only session | Active |
acs-audit | Active | Active |
acs-inspect | Not emitted | Not enabled by default; requires a composed, actively maintained AgBOM inventory surface (the canonical projection, not a CycloneDX export — none exists) |
acs-inspect-dynamic | Not emitted | Not enabled by default; also requires the live-change surface |
acs-provenance | Not declared | Active by default; accepted only from deterministic producers |
acs-trace | Not enabled by default | Declared only when an OTel/OCSF sink is installed; there is no acs-trace Dapr component |
acs-crypto | Not declared; HMAC-SHA256 baseline is used | Not declared; HMAC-SHA256 baseline is used |
The ACS package contains Trace, canonical AgBOM, and provenance primitives for
compositions that install those capabilities. It ships one signature
implementation, HMAC-SHA256, so acs-crypto is refused at policy construction
rather than offered. Package support alone is not a deployment conformance
claim.
Governed Agentic lifecycle
One Agent block execution maps to one durable ACS session. Agentic currently emits these signed methods:
| Method | Control point |
|---|---|
handshake/hello | Negotiates the exact methods, profiles, transport, and failure posture |
steps/sessionStart | Opens the execution-scoped session |
steps/agentTrigger | Records how the agent was activated |
steps/turnStart | Gates each provider/tool-loop turn before it runs |
steps/agentResponse | Gates completed turn output before the workflow receives it |
steps/turnEnd | Records the terminal turn outcome |
steps/sessionEnd | Closes the session and commits the final chain head |
The lifecycle session deliberately does not advertise acs-core: Core requires
tool request and result hooks, while Scrydon governs those operations in API
Platform's separate execution session. Its methods_implemented list therefore
contains only methods this session can actually emit. The Guardian still
requires acs-audit, so the lifecycle remains signed, chained, and fail-closed.
Platform model, retrieval, memory, and tool operations are also governed at API Platform's extension-runtime chokepoint. That path uses the same ordered organization guardrail snapshot and ACS step vocabulary without adding a second policy pass: the in-process ACS hook records the verdict the kernel guardrail pipeline already reached onto the durable session chain — it does not evaluate a second time. Guardian ingress for agents Scrydon does not host (below) resolves that same guardrail snapshot but evaluates it through its own deterministic policy layer, not the kernel's runner.
Failure and replay posture
Scrydon negotiates on_decision_failure: deny, and this posture is a floor
the counterparty cannot lower: the Guardian always returns its own
configured posture in the negotiated ServerHello, regardless of what a
client requests, so a peer cannot ask its way into proceed. A timeout,
invalid signature, malformed response, missing Dapr sidecar, or unconfirmed
state write therefore stops the action. An unguarded outcome is recorded
distinctly and is never treated as permission.
Every request has a unique identity, a timestamp, and a nonce. The Guardian rejects duplicate request IDs, duplicate nonces, requests outside the negotiated skew window, and client chain heads that disagree with its durable session state.
Checkpoints use strong reads and optimistic concurrency, but Agentic and the
Guardian do not share one keyspace. Agentic's own lifecycle state stays in the
exec-security-state Dapr store. The Guardian's session state — the
negotiated handshake, the replay guard, and the audit chain its verdicts are
computed from — lives in its own component, acs-guardian-state, scoped to
API Platform alone and sealed at rest with AES-256-GCM under a key only API
Platform holds: the workload a Guardian judges must not be able to forge the
rows deciding its own verdicts. In the shipped chart that store still lives in
the Agentic database; a dedicated database role for it is tracked as a
follow-up, and until then a session deleted at the SQL level fails closed
rather than being replayed. If delivery succeeds but the client cannot confirm its
checkpoint, the session fails closed instead of replaying a request whose
outcome is uncertain.
Signatures
The default deployment derives a separate HMAC-SHA256 session key from the
shared ACS_GUARDIAN_HMAC_SECRET, organization, workflow, and session IDs.
Requests and responses are signed over the canonical envelope; the root secret
never travels on the wire.
Because every caller authenticates with that same deployment-wide secret, this control boundary works for Scrydon-hosted agents only — an agent your organization operates outside Scrydon's own deployment cannot safely hold the same shared root key. Extending signed control to an independently hosted, customer-operated agent needs asymmetric signing, which this deployment does not yet offer.
Configure one key (ACS_GUARDIAN_HMAC_SECRET, at least 32 characters) for
both API Platform and Agentic. The Helm chart generates and preserves a key on
first installation. Under global.requireExplicitSecrets: true — the mode
for GitOps/lookup-free rendering (helm template, ArgoCD, offline air-gap
manifests), where a generate-on-render fallback would silently rotate the
credential on every sync — the chart instead fails the render and names the
value the operator must set explicitly:
auth.secrets.ACS_GUARDIAN_HMAC_SECRET. The same applies to the Guardian
store's own sealing key, auth.secrets.ACS_GUARDIAN_STATE_ENCRYPTION_KEY.
Environment variables
| Variable | Applies to | Meaning |
|---|---|---|
ACS_REQUIRED | API Platform, Agentic | Whether a process that cannot reach the Guardian may boot at all. The Helm chart renders true in every environment (auth.acs.required); a raw process with no flag defaults to true only when NODE_ENV=production. With it off, a missing input disables ACS with a warning naming it, and the gauges scrydon_acs_guardian_enabled / scrydon_acs_observed_agent_enabled read 0. |
ACS_GUARDIAN_HMAC_SECRET | API Platform, Agentic | Root HMAC key. Required while ACS_REQUIRED is on — startup throws and the process does not boot without it, a reachable Dapr sidecar, and (API Platform only) the sealer key below. |
ACS_GUARDIAN_MAX_CONCURRENT | API Platform | Global cap on signed ACS ingress evaluations this pod runs at once, across all tenants. Default 64. The per-organization cap (16) is not an env knob — it exists specifically so one tenant cannot consume the whole budget. |
ACS_VERDICT_AUDIT | API Platform | Which ACS verdicts become Audit Log rows: all (default), denials, or off. See Audit and observability. |
ACS_GUARDIAN_STATE_ENCRYPTION_KEY | API Platform | AES-256-GCM key that seals the Guardian's acs-guardian-state values at rest. Held by API Platform only — deliberately not the EXEC_SECURITY_STATE_ENCRYPTION_KEY Agentic also holds, since Agentic runs the agents those rows judge. Required while ACS_REQUIRED is on; there is no "run unsealed" fallback. An unpadded 32-byte base64url value (43 characters); the chart generates and preserves one. |
Audit and observability
Content-bearing ACS requests advance a tamper-evident per-session chain. The
chain commits to canonical request parameters, including the previous digest
as raw bytes as required by ACS. turnEnd and sessionEnd are audit facts and
cannot retroactively veto completed work.
The extension runtime exposes one effects plane with separate durability contracts:
- audit and billing projections remain blocking;
- ACS Trace is best-effort and cannot change or delay a verdict;
- decision observations contain correlation and policy metadata, not modified content or policy-private payloads.
ACS_VERDICT_AUDIT (default all) selects which verdicts become Audit Log
rows:
- a governance row (
acs.decision.denied), written immediately for every denying verdict (deny, ask, defer), carrying the OCSF Detection Finding; - one telemetry row per execution (
acs.decision.recorded), written when the execution ends, containing that execution's whole decision list — allowed steps included. A streamed answer produces a verdict per governed window and per governed chunk; recording each one as its own row would have multiplied a governed call's audit volume by the length of its answer, so this row aggregates them instead. denialswrites only the governance row;offwrites neither — spans still trace.
There is no separate OCSF-to-SIEM forwarder: the governance row rides the
Audit Log's existing HMAC-signed SIEM delivery, so a Log Receiver already
configured for the Audit Log picks up ACS denials with no additional
configuration. There is no acs-trace Dapr component.
When a Trace sink is installed, each governed step uses the ACS OpenTelemetry
span name, its verdict is attached as an acs.decision span event, and the
same step and decision are emitted as idempotent OCSF events. Without an active
sink, the handshake does not claim acs-trace.
Streaming
An enforce-mode interceptor that declares a governable window
(streamWindow) evaluates and releases a streamed answer in windows, so it
reaches the caller as it is produced. That only holds when every
enforcing interceptor governing the stream declares a window: if even one
does not, the whole stream is held back and evaluated as a single buffer
before the first byte is released — this is the guardrail pipeline's
windowed-streaming design, not an outage. Audit-only interceptors and
windowed interceptors observe every governed chunk and window as it happens.
Whichever mode applies, the ACS session chain itself is not multiplied by stream length: the chain records one aggregate evidence step for the governed operation, built from the same windows and chunks the guardrail pipeline observed, rather than one chain entry per token.
Scrydon as a Guardian
Authenticated, organization-scoped workload identities can submit official
signed ACS JSON-RPC envelopes through the Platform operation
agent.control.evaluate. Its scope, agent-control:write, is the one
Platform scope a user session or API key never satisfies: only a service
workload the internal roster grants it (Agentic today) can reach the Guardian
or occupy its admission budget.
API Platform resolves the organization's current DLP, moderation, and
classification policy for each request; it does not cache one tenant's policy
for another tenant. Signed ACS ingress is bounded per organization (16
concurrent requests, 64 globally, a 2-second maximum wait); a saturated
organization is answered HTTP 429 with reason: rate_limited rather than
starving every other tenant's agent traffic.
Authentication and attribution answer two different questions. The Guardian
authenticates the calling workload through the Platform gateway's own
transport identity — never a value read out of the request body — and
attributes the step to an agent from the signed envelope's
metadata.agent_id. A well-formed envelope names the actual observed
workflow agent; a malformed one falls back to the authenticated workload
identity, so the Guardian returns the protocol's own INVALID_REQUEST
instead of trusting an unverified claim.
The default Guardian supports deterministic provenance validation, all five
dispositions, replay protection, and the request-content audit chain. The ACS
kernel can validate canonical AgBOM snapshots and changes, but the Guardian
only advertises Inspect after those pieces are composed into an active
inventory surface — the shipped deployment does not compose it today.
acs-crypto is not offered by the ACS envelope layer either: this package
ships one working signature implementation, HMAC-SHA256. Scrydon separately
signs its Better Auth audit-log checkpoints with ML-DSA-65, but that is a
different system with a different audit chain, and it does not make this
envelope layer's acs-crypto claim true. Malformed or unavailable policy
state fails closed.
Admin visibility
Organization admins reach this control at Settings → Governance → Agent
Control. The panel exposes the one ACS limit that is actually admin-
configurable — maxAgentTurnsPerExecution, the Guardian's per-execution turn
ceiling — plus read-only prose describing what the control plane already
enforces automatically. It is not a live handshake or posture probe: the rest
of ACS (audit, DLP, replay protection, Trace) is either enforced without a
toggle or not yet composed in this deployment, and the panel says so rather
than rendering a status badge with nothing behind it.
Current boundaries
The default deployment does not send Scrydon Agentic hooks to a
customer-operated external Guardian, does not emit Agentic compaction,
subagent, skill, or AgBOM lifecycle hooks, and does not claim acs-inspect,
acs-inspect-dynamic, or acs-crypto. Those capabilities must be composed and
negotiated before they can be advertised.