diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..10d57df --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,37 @@ +# Changelog + +All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) once it reaches 1.0.0. Pre-1.0 releases MAY change the ACS-Core baseline in patch versions; the wire format version (`acs_version` in the handshake) evolves separately in the `specification/` directory. + +## [0.1.1] — 2026-07-09 + +### Changed — ACS-Core baseline (relaxations) + +- `MODIFY` is now SHOULD-support rather than MUST-support. Deployments whose framework cannot mutate a request (shell-hook integrations, IDE plugins without an argument-mutation surface) or that disable `MODIFY` for auditability reasons are conformant without it. See new [§6.5 MODIFY-incapable clients](docs/spec/instrument/specification.md#65-modify-incapable-clients-normative). +- `subagentStop` is now SHOULD-emit rather than MUST-emit; `subagentStart` remains MUST-emit for subagent-capable frameworks. +- `system/ping` is now SHOULD-implement rather than MUST-implement, provided the deployment declares an alternative liveness mechanism (transport-level keepalive on HTTP; process supervision or heartbeat on stdio; continuous observed hook traffic). Omitting `system/ping` without a declared alternative is non-conformant. +- Wrapped MCP (`protocols/MCP/*`) MUST be implemented by deployments whose sessions involve MCP at any point (tools, resources, prompts, notifications), and MAY be omitted by deployments whose sessions never involve MCP. Deployments that declared no MCP at handshake MUST NOT spawn or register MCP servers mid-session; renegotiation is undefined in v0.1. + +### Changed — ACS-Core baseline (additions) + +- `subagentStart` is now part of the MUST-emit minimum hook set for subagent-capable frameworks. A client whose framework has no sub-agent abstraction has nothing to emit; the Guardian determines client subagent-emission capability by deployment-defined means (mirroring the [§9.2 ASK precedent](docs/spec/instrument/specification.md#92-approver-incapable-clients-normative)). + +### Added + +- **§6.5 MODIFY-incapable clients (normative)** in `docs/spec/instrument/specification.md`. Defines Guardian-side substitution (`DENY` with `reason_codes: ["modify_unsupported"]` + audit event) and client-side fallback (a client receiving an unapplicable `MODIFY` MUST treat it as `DENY` + audit event). +- Audit-event mandate in **§9.2** (approver-incapable clients): the Guardian MUST record the ASK-substitution as an audit event, so the substitution rate is machine-detectable rather than only reachable by grepping log prose. + +### Changed — normative force harmonization + +- **§6.3** (malformed MODIFY): `SHOULD record an audit event` → `MUST record an audit event`. All fallback / substitution rules now use consistent MUST-audit language across §6.3, §6.4, §6.5, and §9.2. + +### Changed — schema + +- `specification/v0.1.0/hooks/subagent-stop.json`: `final_chain_hash` moved from `required` to optional. Frameworks that maintain no session-chain (shell-hook integrations without an internal audit chain) MAY omit the field rather than fabricate a value; fabrication would corrupt the exact artifact the field exists to produce. A conformant Guardian MUST treat omission as "chain not maintained by this framework" rather than as an integrity failure. + +### Related issues + +- Guardian-side hook-coverage unfalsifiability tracked at [#31](https://github.com/GenAI-Security-Project/agent-control-standard/issues/31); out of scope for this release, needed for the enforcement side of `subagentStart`'s security rationale to be checkable. + +## [0.1.0] — 2026-06-05 + +- Initial canonical v0.1.0 spec integrated (see [#2](https://github.com/GenAI-Security-Project/agent-control-standard/pull/2)). diff --git a/docs/acs.md b/docs/acs.md index 0baff63..badfa7e 100644 --- a/docs/acs.md +++ b/docs/acs.md @@ -8,7 +8,7 @@ ACS extends existing standards rather than reinventing them: JSON-RPC 2.0 for th ## What v0.1.0 ships -- **ACS-Core** (mandatory baseline) — capability-negotiation handshake, JSON-RPC envelope, 16 native lifecycle hooks (`sessionStart`/`End`, `agentTrigger`, `userMessage`, `agentResponse`, `turnStart`/`End`, `toolCallRequest`/`Result`, `knowledgeRetrieval`, `memoryContextRetrieval`, `memoryStore`, `preCompact`/`postCompact`, `subagentStart`/`Stop`), wrapped MCP, five dispositions (`allow`, `deny`, `modify`, `ask`, `defer`), SessionContext with rolling SHA-256 chain hash, optional Intent with immutability rule, replay protection, and `system/ping` liveness. +- **ACS-Core** (mandatory baseline) — capability-negotiation handshake, JSON-RPC envelope, minimum hook set (`sessionStart`, `userMessage` or `agentTrigger`, `toolCallRequest`/`Result`, `agentResponse`, `sessionEnd`, `subagentStart` for subagent-capable frameworks), four MUST-support dispositions (`allow`, `deny`, `ask`, `defer`) with `modify` SHOULD-support, SessionContext with rolling SHA-256 chain hash, optional Intent with immutability rule, replay protection, baseline HMAC-SHA256 signature, decision honoring, declared liveness mechanism (`system/ping` or a transport-level alternative), and Wrapped MCP when sessions involve MCP. Additional hooks (`turnStart`/`End`, `preCompact`/`postCompact`, `subagentStop`, `knowledgeRetrieval`, `memoryContextRetrieval`, `memoryStore`, `skillRegister`/`Load`/`Unload`) are normatively defined and SHOULD be implemented when the harness can observe the corresponding event. - **ACS-Trace** profile — OpenTelemetry semconv mapping + OCSF event-class mapping, with decisions emitted as span events on the parent step span. - **ACS-Inspect** / **ACS-Inspect-Dynamic** profiles — canonical AgBOM with `agbom/snapshot` and `agbom/changed`, deterministic CycloneDX / SPDX / SWID derivations. - **ACS-Provenance** profile — field-level `Provenance` objects with `origin`, `source_id`, `derived_from`, and an OPTIONAL wire-format `trust` enum that obeys the monotonicity rule. diff --git a/docs/spec/conformance.md b/docs/spec/conformance.md index 95ad9f7..bb83007 100644 --- a/docs/spec/conformance.md +++ b/docs/spec/conformance.md @@ -16,18 +16,18 @@ A v0.1.0-conformant deployment MUST implement ACS-Core. ACS-Core comprises: - **Handshake** — `handshake/hello` with ClientHello/ServerHello ([Specification §4](./instrument/specification.md#4-capability-negotiation-handshake)). - **Request/response envelope** — JSON-RPC 2.0 with ACS extensions ([§3](./instrument/specification.md#3-wire-format)). `request_id`, `timestamp`, `acs_version`, `metadata` required on every request. -- **Hook taxonomy** — At minimum: `sessionStart`, `userMessage` or `agentTrigger`, `toolCallRequest`, `toolCallResult`, `agentResponse`, `sessionEnd`. Additional hooks (`turnStart`/`turnEnd`, `preCompact`/`postCompact`, `subagentStart`/`subagentStop`, `knowledgeRetrieval`, `memoryContextRetrieval`, `memoryStore`, `skillRegister`/`skillLoad`/`skillUnload`) are normatively defined and SHOULD be implemented when the harness can observe the corresponding event; they are capability-negotiated via the handshake. A deployment whose AgBOM includes `skill` components SHOULD emit the skill lifecycle hooks, so a Guardian is not blind to a composition surface the inventory already exposes. -- **Dispositions** — All five (ALLOW, DENY, MODIFY, ASK, DEFER) with required fields per [§6](./instrument/specification.md#6-disposition-vocabulary). +- **Hook taxonomy** — At minimum: `sessionStart`, `userMessage` or `agentTrigger`, `toolCallRequest`, `toolCallResult`, `agentResponse`, `sessionEnd`, `subagentStart`. A subagent-capable client MUST emit `subagentStart` when it spawns a sub-agent; a client whose framework has no sub-agent abstraction has nothing to emit. The Guardian determines client subagent-emission capability by deployment-defined means (agent identity bound at handshake, policy keyed on `agent_id`, organizational configuration), mirroring the ASK precedent in [§9.2](./instrument/specification.md#92-approver-incapable-clients-normative). Without `subagentStart`, a Guardian is blind to cross-agent propagation (the confused-deputy attack class). `subagentStop` is SHOULD-emit (audit-only, not decision-eligible; its `final_chain_hash` field is optional for frameworks maintaining no session-chain — see [`subagent-stop.json`](https://github.com/afogel/ACS_official/blob/dev/specification/v0.1.0/hooks/subagent-stop.json)). Additional hooks (`turnStart`/`turnEnd`, `preCompact`/`postCompact`, `knowledgeRetrieval`, `memoryContextRetrieval`, `memoryStore`, `skillRegister`/`skillLoad`/`skillUnload`) are normatively defined and SHOULD be implemented when the harness can observe the corresponding event; they are capability-negotiated via the handshake. A deployment whose AgBOM includes `skill` components SHOULD emit the skill lifecycle hooks, so a Guardian is not blind to a composition surface the inventory already exposes. +- **Dispositions** — Four MUST-support: ALLOW, DENY, ASK, DEFER, with required fields per [§6](./instrument/specification.md#6-disposition-vocabulary). MODIFY is SHOULD-support: deployments that implement MODIFY MUST follow the MODIFY composition rules ([§6.3](./instrument/specification.md#63-modify-composition-normative)); deployments that do not implement MODIFY are handled per [§6.5](./instrument/specification.md#65-modify-incapable-clients-normative) (the Guardian substitutes `DENY` with `reason_codes: ["modify_unsupported"]` and an audit event recording the substitution, determined by deployment-defined means rather than a wire declaration, mirroring the §9.2 precedent for ASK). - **SessionContext and Intent**: `session_id`, `chain_hash` (rolling SHA-256), append-only ContextEntry chain, with the Guardian publishing the chain head (`chain_hash`) on responses for content-bearing steps ([§8](./instrument/specification.md#8-sessioncontext-and-intent)). Intent is optional but normative when IBAC is the enforcement paradigm. - **Replay protection** — `request_id` (UUID) and `timestamp` on every request; Guardians MUST reject replays per [§10.3](./instrument/specification.md#103-replay-protection). - **Baseline integrity**: every request and response carries a signature over the canonical envelope ([§10](./instrument/specification.md#10-cryptographic-signatures)). `HMAC-SHA256` with an HKDF-derived per-session key from deployment-provided key material is the baseline; asymmetric and post-quantum algorithms are the ACS-Crypto profile. - **Decision honoring**: the Observed Agent MUST wait for the Guardian's decision up to the negotiated timeout and apply it; on a decision failure (timeout, transport failure, or an error without a decision) it applies the `on_decision_failure` posture (default `proceed`, fail-open) and records every fail-open proceed as an audit event ([§6.4](./instrument/specification.md#64-honoring-decisions-normative)). Handshake failure follows the deployment's startup posture ([§4.1](./instrument/specification.md#41-handshake-failure-normative)). -- **Liveness** — `system/ping` ([§13](./instrument/specification.md#13-liveness-system-methods)). -- **Wrapped MCP** — `protocols/MCP/*` ([Hooks](./instrument/hooks.md#protocolsmcp)). +- **Liveness** — `system/ping` ([§13](./instrument/specification.md#13-liveness-system-methods)) SHOULD be implemented. A deployment MAY omit `system/ping` only if it has declared an alternative liveness mechanism in its configuration (for example: TCP keepalive or HTTP/2 PING on HTTP transports; process supervision or an out-of-band heartbeat on stdio; or continuous observed hook traffic where the session's own cadence acts as the probe). Omitting `system/ping` without a declared alternative is non-conformant: the chain of defaults (`startup_posture: proceed`, `on_decision_failure: proceed`, no liveness) produces silent fail-open on Guardian outage, which the Core baseline cannot allow. +- **Wrapped MCP** — `protocols/MCP/*` ([Hooks](./instrument/hooks.md#protocolsmcp)) MUST be implemented by deployments whose sessions involve MCP at any point (tools, resources, prompts, notifications), so that `protocols/MCP/resources/read` — the canonical prompt-injection ingress for attacker-controlled external content — is not bypassed. Support is declared in the handshake via `wrapped_protocols`. Deployments that declared no MCP at handshake MUST NOT spawn or register MCP servers mid-session; renegotiation is undefined in v0.1, so mid-session MCP addition is non-conformant until v0.2 ships it. Deployments whose sessions never involve MCP MAY omit the namespace. ACS-Core does NOT require: field-level Provenance objects, Trace event emission, AgBOM, asymmetric or post-quantum signatures, or `request_hash` on ContextEntry (`request_hash` remains SHOULD). It DOES require the baseline signature (§10) and decision honoring (§6.4). ACS-Core deployments validate hook payloads against the base schemas, where `provenance` is OPTIONAL; field-level Provenance is added by the ACS-Provenance profile. -What "ACS-Core conformant" guarantees: the channel is authenticated and the Observed Agent honors the Guardian's decisions. It does NOT assert that a deployment's policies are strict, nor that the audit chain is tamper-evident against a compromised Guardian (that is the ACS-Crypto and ACS-Audit profiles, since the HMAC baseline is symmetric). A permissive Guardian is a conformant but permissive deployment, not a violation. +What "ACS-Core conformant" guarantees: the channel is authenticated and the Observed Agent honors the Guardian's decisions on the four MUST-support dispositions (ALLOW, DENY, ASK, DEFER). Deployments that also implement MODIFY (SHOULD-support, [§6.5](./instrument/specification.md#65-modify-incapable-clients-normative)) honor it too; deployments that do not receive `DENY` substitutions with `reason_code: modify_unsupported`. A minimum-conformant deployment can therefore permit or refuse an action but cannot alter it — redaction and other content rewrites require MODIFY support. This clause does NOT assert that a deployment's policies are strict, nor that the audit chain is tamper-evident against a compromised Guardian (that is the ACS-Crypto and ACS-Audit profiles, since the HMAC baseline is symmetric). A permissive Guardian is a conformant but permissive deployment, not a violation. ## ACS-Trace @@ -80,7 +80,7 @@ Profiles compose. A deployment that wants full observability and supply-chain in | Profile | What it adds | When to claim | |---|---|---| -| `acs-core` | Handshake, envelope, hook taxonomy, dispositions, SessionContext + published chain head, replay protection, baseline signature (HMAC-SHA256), decision honoring, ping | Always (mandatory) | +| `acs-core` | Handshake, envelope, minimum hook set (incl. `subagentStart` for subagent-capable clients), four MUST-support dispositions (ALLOW/DENY/ASK/DEFER) with MODIFY SHOULD-support (§6.5), SessionContext + published chain head, replay protection, baseline signature (HMAC-SHA256), decision honoring, declared liveness mechanism, Wrapped MCP when sessions involve MCP | Always (mandatory) | | `acs-trace` | OTel + OCSF event emission per step | Cross-vendor observability or SIEM integration | | `acs-inspect` | `agbom/snapshot` + canonical AgBOM serialization | Policy depends on component inventory | | `acs-inspect-dynamic` | `agbom/changed` on mutation | Agent hot-swaps components mid-session | diff --git a/docs/spec/instrument/specification.md b/docs/spec/instrument/specification.md index 92f6ee3..535fd42 100644 --- a/docs/spec/instrument/specification.md +++ b/docs/spec/instrument/specification.md @@ -109,7 +109,7 @@ Inspect-pillar methods (`agbom/*`): `agbom/snapshot` and `agbom/changed` (see [I |---|---|---| | `ALLOW` | Proceed | none (`reasoning` RECOMMENDED when user-visible audit trails are expected) | | `DENY` | Block | `reasoning` | -| `MODIFY` | Proceed with changes (covers redaction via `modifications.redactions`; composition rules in [§6.3](#63-modify-composition-normative)) | `reasoning`, `modifications` | +| `MODIFY` | Proceed with changes (covers redaction via `modifications.redactions`; composition rules in [§6.3](#63-modify-composition-normative); substituted with `DENY` for MODIFY-incapable clients, see [§6.5](#65-modify-incapable-clients-normative)) | `reasoning`, `modifications` | | `ASK` | Pause and request approval (substituted with `DEFER` or `DENY` for approver-incapable clients; see [§9.2](#92-approver-incapable-clients-normative)) | `reasoning`, `ask_details` | | `DEFER` | Verdict not yet reachable | `reasoning`, `defer_details` | @@ -143,7 +143,7 @@ These fields support the v0.1 paradigm targets (FIDES, CaMeL, AARM-style cumulat - **Wholesale replacement.** `modified_content` replaces the entire payload. It is exclusive: a MODIFY that carries `modified_content` MUST NOT also carry `redactions` or `parameter_overrides`, because path-addressed edits have nothing to address inside an opaque replacement string. - **Structured edits.** `redactions` and `parameter_overrides` MAY appear together, but their targets MUST be disjoint: no `redactions` path may address the same field as a `parameter_overrides` key, nor an ancestor or descendant of it. Disjoint edits commute, so the effective payload is well-defined with no apply-order rule. -A Guardian MUST NOT emit a `modifications` object that violates either rule. An Observed Agent that receives one cannot determine the Guardian's intent and MUST fail closed, treating the decision as `DENY`, and SHOULD record an audit event. +A Guardian MUST NOT emit a `modifications` object that violates either rule. An Observed Agent that receives one cannot determine the Guardian's intent and MUST fail closed, treating the decision as `DENY`, and MUST record an audit event so the enforcement gap is visible. The disjoint-target rule is deliberately narrower than a precedence rule. A fixed apply-order would force every overlap to silently pick a winner, and either order has a failure mode: applying overrides last can re-expose a field a redaction just removed, and applying redactions last lets arbitrary replacement text overwrite a value an override deliberately sanitized. Requiring disjoint targets removes the conflict rather than resolving it by an order the Guardian cannot observe. @@ -157,6 +157,16 @@ A step suffers a **decision failure** when no usable decision arrives within the Every step that proceeds without a decision MUST be recorded as an audit event, so the bypass is visible rather than silent. When a decision does arrive within the timeout, the agent MUST honor it regardless of the posture. Fail-open trades enforcement for availability under disruption: an adversary who can disrupt the channel converts control into audit. Deployments for which that trade is unacceptable set `on_decision_failure: deny`. +### 6.5 MODIFY-incapable clients (normative) + +Some Observed Agents cannot apply a `MODIFY` disposition. This covers both framework constraints (shell-hook integrations whose hook API returns allow/deny only, IDE plugins without a mid-flight argument-mutation surface) and deployment preferences (organizations that disable MODIFY for auditability, so the executed action always equals the requested action). The Guardian determines client MODIFY-handling capability by deployment-defined means such as agent identity bound at handshake, policy keyed on `agent_id`, organizational configuration, or any other out-of-band signal the deployment trusts. ACS does not put this declaration on the wire in v0.1; it is part of the Guardian's policy bundle, mirroring the ASK precedent in [§9.2](#92-approver-incapable-clients-normative). + +When the Guardian determines that the client cannot apply `MODIFY`, the Guardian MUST NOT return `MODIFY`. The Guardian MUST instead substitute `DENY` with `reason_codes: ["modify_unsupported"]` and `reasoning` that names the intended modification, and MUST record the substitution as an audit event so the enforcement gap is visible. + +If a client receives a `MODIFY` it cannot apply — from a Guardian that misjudged capability, or a Guardian that did not consult the declaration — the client MUST treat the decision as `DENY` and MUST record an audit event with `reason_codes: ["modify_unsupported"]`. Proceeding with the original payload is non-conformant: a Guardian that intended to redact a secret out of a tool argument would otherwise get the unredacted secret shipped while the audit log recorded a modification that never happened. + +This rule preserves the security guarantee (actions that would have been rewritten by the Guardian are not silently allowed with the unmodified payload) while letting clients whose framework cannot mutate requests, or deployments that choose strict allow/deny for auditability, participate in ACS sessions as fully conformant ACS-Core deployments. + ## 7. Provenance The Provenance concept and its fields are defined in [Concepts › Provenance](../../concepts/provenance.md) (normative). This section specifies the wire shape and the v0.1 trust-classification stance. @@ -279,6 +289,8 @@ When the Guardian determines that the client cannot resolve `ASK`, the Guardian 1. `DEFER` with `timeout_decision: "deny"`: when the underlying issue might resolve through retry, an out-of-band escalation, or a later state change. The deferred verdict still counts toward cascading-deferral limits (§6). 2. `DENY` with `reason_codes: ["approver_unavailable"]` and `reasoning` that names the missing capability: when no recovery path exists. +In either case, the Guardian MUST record the substitution as an audit event so the enforcement gap is visible. Without a recorded substitution the substitution rate is only reachable by grepping log prose, which defeats the one metric that catches a client misdeclaring its capability. + The choice is policy-driven: deployments SHOULD prefer `DEFER` when the request is potentially recoverable through a different surface, and `DENY` when the action is unconditionally outside the client's reachable authority. This rule preserves the security guarantee (actions that would have been `ASK`'d in an approver-capable deployment are not silently allowed) while letting clients without approver UX participate in ACS sessions as fully conformant ACS-Core deployments. diff --git a/pyproject.toml b/pyproject.toml index 02eac05..36af930 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "acs" -version = "0.1.0" +version = "0.1.1" description = "ACS Documentation" requires-python = ">=3.8" dependencies = [ "mike>=1.2.0", "mkdocs-material>=9.6.14", "pymdown-extensions>=10.0.0",] diff --git a/specification/v0.1.0/hooks/subagent-stop.json b/specification/v0.1.0/hooks/subagent-stop.json index e4dc1b7..c7a4940 100644 --- a/specification/v0.1.0/hooks/subagent-stop.json +++ b/specification/v0.1.0/hooks/subagent-stop.json @@ -4,7 +4,7 @@ "title": "steps/subagentStop payload", "description": "Fires when an in-process subagent terminates. The envelope's metadata.session_id is the PARENT's session (the parent receives the subagent-stop notification on its own audit chain); the subagent's own session has by this point already emitted its sessionEnd with its own final_chain_hash. This hook lets the parent's audit chain reference the subagent's terminal state without merging the two chains. Not decision-eligible — the subagent has already terminated.", "type": "object", - "required": ["subagent_session_id", "outcome", "final_chain_hash"], + "required": ["subagent_session_id", "outcome"], "properties": { "subagent_session_id": { "type": "string", @@ -19,7 +19,7 @@ "final_chain_hash": { "type": "string", "pattern": "^[0-9a-f]{64}$", - "description": "The subagent's final SessionContext chain_hash at sessionEnd. Lets parent-side audit replay verify the subagent's chain integrity without the parent maintaining a copy of it." + "description": "OPTIONAL. The subagent's final SessionContext chain_hash at sessionEnd. When present, lets parent-side audit replay verify the subagent's chain integrity without the parent maintaining a copy of it. Frameworks that maintain no session-chain (e.g., shell-hook integrations without an internal audit chain) MAY omit this field rather than fabricate a value — fabrication would corrupt the exact artifact the field exists to produce. A conformant Guardian MUST treat omission as 'chain not maintained by this framework' rather than as an integrity failure." }, "summary": { "type": "object", diff --git a/version.txt b/version.txt index 6c6aa7c..6da28dd 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.1.0 \ No newline at end of file +0.1.1 \ No newline at end of file