From b53c07dd35b48f6a70cb40710b6549586a85cc73 Mon Sep 17 00:00:00 2001 From: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com> Date: Fri, 7 Aug 2026 04:53:34 +0000 Subject: [PATCH 1/2] docs(spec): admit incremental mediation as a 12.1 exception Section 12.1 mandated assemble-before-post_model_call with no exception, while ACS section 18.1 (a host contract this spec already names in its conformance claims table) mediates streams incrementally. A host adopting that profile therefore read as non-conformant at 12.1 even though 12.1a already reserves the buffered_output: false capability for exactly this posture. The streaming comparison study (agent-control-spec analysis/streaming-acs-vs-maf.md, R6) flagged the tension. 12.1 now carries an exception: a host declaring buffered_output: false MAY evaluate the stream incrementally, emitting post_model_call per evaluated segment, provided it satisfies a bounded-exposure accounting discipline - verdict-covered release within a declared exposure bound, a deny that terminates the stream and withholds the unreleased remainder, fail-closed residue at end of stream with host_error:streaming_unsupported, and durability gated per 6.1. ACS 18.1 is referenced as one implementation of the discipline. The capability stays declaration-only and the exception is capability-shaped: 12.1a and 13.3 now require the declaration and claim to state the exposure bound, and conformance vectors exercising the accounting discipline against a mocked stream are left as future work. CLAIMS/HARNESS/THREAT-MODEL/PRODUCTION updated to match. Additive; no version bump. Signed-off-by: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com> --- CHANGELOG.md | 15 +++++++++++++++ conformance/CLAIMS.md | 7 +++++-- conformance/HARNESS.md | 10 +++++++--- docs/PRODUCTION.md | 2 +- docs/THREAT-MODEL.md | 2 +- spec/AGENT-HOOKS-0.1.md | 36 +++++++++++++++++++++++++++++++++++- 6 files changed, 64 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a55a82..5e74502 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,21 @@ User-visible changes to the spec and SDKs. Versioning rules: [VERSIONING.md](VERSIONING.md). +## Unreleased + +- **§12.1 admits incremental mediation.** Previously a host had to + assemble the complete response before `post_model_call` with no + exception, so a host mediating a stream incrementally (e.g. per ACS + §18.1) could not make a coherent conformance claim. §12.1 now + carries an exception: a host declaring `buffered_output: false` MAY + evaluate incrementally under a bounded-exposure accounting + discipline (verdict-covered release, terminating deny that withholds + the unreleased remainder, fail-closed residue at end of stream, + §6.1-gated durability). The capability stays declaration-only: the + §12.1a declaration and §13.3 claim MUST state the exposure bound, + and conformance vectors for the accounting discipline are future + work. Additive; no version-surface change. + ## 0.1.0-alpha.4 — tag `v0.1.0-alpha.4` - **.NET: the NuGet package ships the native library.** The nupkg now diff --git a/conformance/CLAIMS.md b/conformance/CLAIMS.md index 3866b97..768af08 100644 --- a/conformance/CLAIMS.md +++ b/conformance/CLAIMS.md @@ -23,7 +23,9 @@ provider MUST disclose whether the provider is **content-derived** approval binding and record correlation are only as strong as that property (§10.1). A claim with `buffered_output: false` MUST state that a `deny` at `output` cannot -retract already-streamed content (§12.1a). +retract already-streamed content (§12.1a); one whose host mediates +incrementally under the §12.1 exception MUST also state the exposure +bound its accounting discipline enforces. | Framework | Adapter version | Spec | Capabilities | Profiles | Identity provider | SDK | Report | Notes | | --- | --- | --- | --- | --- | --- | --- | --- | --- | @@ -47,7 +49,8 @@ artefacts, in the PR: the claim states records/approvals are identity-unbound; custom provider → content-derived or not; `buffered_output: false` → the claim states a deny at `output` cannot retract streamed - content. + content, plus the exposure bound when the host mediates + incrementally under the §12.1 exception. Acceptance is by CODEOWNERS review (`conformance/` owner). The reviewer checks: the report matches the declared surface tuple; the diff --git a/conformance/HARNESS.md b/conformance/HARNESS.md index de520f4..151dc80 100644 --- a/conformance/HARNESS.md +++ b/conformance/HARNESS.md @@ -106,9 +106,13 @@ coercion class the core's raw-text scan (§10.2) exists to reject; see `true` (the host buffers caller-bound output until the `output` combined verdict permits), and a host that streams to its caller without buffering declares `buffered_output: false` in its surface and -claim. The CTK drives hosts with mocked I/O and cannot exercise -streaming egress, so no vector carries this capability — the -declaration exists to make the retraction limitation visible (§13.3). +claim. A declaring host may also mediate `post_model_call` +incrementally under the §12.1 exception; its claim then states the +exposure bound (§12.1a). The CTK drives hosts with mocked I/O and +cannot exercise streaming egress, so no vector carries this capability +— the declaration exists to make the retraction limitation visible +(§13.3). Vectors exercising the §12.1 accounting discipline against a +mocked stream are future work. Non-finite floats (NaN/Infinity) and lone surrogates cannot be expressed in a JSON vector at all — those §4.4 marshalling guards are diff --git a/docs/PRODUCTION.md b/docs/PRODUCTION.md index b8df0f5..20183a9 100644 --- a/docs/PRODUCTION.md +++ b/docs/PRODUCTION.md @@ -16,7 +16,7 @@ the [operations runbook](OPERATIONS.md) covers day-2 concerns | 5 | **Record persistence** | `InterceptionRecord`s are the audit trail and are payload-free by construction (§10.3). Configure a record sink; the in-memory buffer is drop-oldest with a `records_dropped` counter — alert on it (see OPERATIONS). Persist `result_labels` with produced data and resurface them per §5.4. | | 6 | **Approval-channel redaction** | The `ApprovalRequest` carries the context as presented to the resolver and MAY be redacted (§9). Use the emitter's approval-redactor seam; the request identity is computed over the *redacted* context, so the binding covers exactly what the approver saw. Document redaction in `extensions..redacted`. | | 7 | **Payload bounds** | Contexts are canonicalized, hashed, and deep-copied per interceptor per emission. Apply the RECOMMENDED bounds (5 MiB serialized, depth 128, §12.3); whatever limit you enforce, breach MUST yield `deny host_error:context_invalid`, never a crash or truncation. | -| 8 | **Output streaming** | If you stream output to the caller, buffer until the `output` combined verdict permits (§12.1a) — or declare `buffered_output: false` and state in your claim that a deny at `output` cannot retract streamed content. | +| 8 | **Output streaming** | If you stream output to the caller, buffer until the `output` combined verdict permits (§12.1a) — or declare `buffered_output: false` and state in your claim that a deny at `output` cannot retract streamed content. A declaring host may also evaluate `post_model_call` incrementally under the §12.1 bounded-exposure exception; state the exposure bound in the claim. | | 9 | **Value domain** | String-encode 64-bit identifiers at the adapter boundary (§4.4). JavaScript hosts cannot observe big integers at all (`JSON.parse` rounds first); Go hosts must decode with `json.Number`. | | 10 | **Zero-interceptor state** | An `enforce`-mode emission with nothing registered fails closed (`host_error:no_interceptor`, §7). A deliberate passthrough is an explicit allow-all interceptor — deploy one consciously or not at all. | diff --git a/docs/THREAT-MODEL.md b/docs/THREAT-MODEL.md index 27325ef..ad4d3ce 100644 --- a/docs/THREAT-MODEL.md +++ b/docs/THREAT-MODEL.md @@ -53,7 +53,7 @@ SDKs; **GAP** = no automated verification exists. | TM-08 | Identity collision via canonicalization divergence: two SDKs (or two values) canonicalize differently, breaking approval binding and audit correlation | S, R | In | §10.2 RFC 8785 via single Rust core; closed required+conditional preimage; all bindings delegate. Non-I-JSON values fail closed: in-memory check + raw-text scan for integer literals serde-class parsers coerce (beyond u64/i64) | `golden` (11 fixtures asserted in Rust/Python/TS/.NET/Go); `sdk/rust/core/src/canonical.rs` JCS + scan tests; AH-CTK-090 (beyond 2⁵³), AH-CTK-091 (beyond u64, `bigint_json` harnesses) | | TM-09 | Audit-record payload leakage: records or failure messages exfiltrate context data into audit storage | I | In | §10.3 payload-free verdict projection (`transform.value` dropped, messages truncated); failure verdicts carry exception *type* only (including Go panic recovery); host-synthesized remediation details are value-free by rule (§6.3/§14) — the I-JSON and envelope rejections name the path and constraint, never the value | Record shape: `spec/schema/interception-record.schema.json`; projection tests per SDK; `canonical.rs` value-free-detail tests (`envelope_details_are_value_free`, raw-scan not-contains assertions); Go panic tests. Residual: an interceptor can still deliberately place payload in its own bounded `reason` | | TM-10 | Exfiltration/SSRF via `evidence.verification_pointers`: attacker-supplied URIs dereferenced by host or audit tooling | I | In (host obligation) | §5.3/§14: host MUST NOT dereference; propagate opaque | **GAP** — prose only; no test, no scheme allow-list guidance | -| TM-11 | Streaming egress before interception: partial model output reaches the caller before `output` (or `post_model_call`) is evaluated | E, I | In | §12.1 covers model→host streaming (assemble before `post_model_call`, else fail closed `host_error:streaming_unsupported`). §12.1a covers host→caller egress: buffer until the `output` combined verdict permits (MUST), or declare `buffered_output: false` — conformant, but the claim MUST state that a deny at `output` cannot retract streamed content (§13.3) | §12.1 negative path: **GAP** (no vector). §12.1a is declaration-only by construction — mocked CTK I/O cannot exercise egress; visibility is via the claim, not a vector | +| TM-11 | Streaming egress before interception: partial model output reaches the caller before `output` (or `post_model_call`) is evaluated | E, I | In | §12.1 covers model→host streaming (assemble before `post_model_call`, else fail closed `host_error:streaming_unsupported`; a `buffered_output: false` host MAY instead mediate incrementally under the §12.1 bounded-exposure exception, whose claim states the exposure bound). §12.1a covers host→caller egress: buffer until the `output` combined verdict permits (MUST), or declare `buffered_output: false` — conformant, but the claim MUST state that a deny at `output` cannot retract streamed content (§13.3) | §12.1 negative path: **GAP** (no vector). §12.1a is declaration-only by construction — mocked CTK I/O cannot exercise egress; visibility is via the claim, not a vector | | TM-12 | Resource exhaustion: unbounded `target`/`messages` canonicalized, hashed, and deep-copied per interceptor per emission — multiplied by parallel profiles' per-interceptor snapshots | D | In | §12.3 RECOMMENDED bounds (5 MiB / depth 128) with a normative failure mode: breach of whatever limit the host or core enforces MUST yield `deny host_error:context_invalid` and MUST NOT crash or truncate; the identity path enforces the depth default fail-closed. The record itself is bounded: §10.3 payload-free projection (transform.value dropped, messages truncated) plus the §5.3 10240-byte evidence cap | Depth: `sdk/rust/core/src/canonical.rs` depth tests. Evidence cap: AH-CTK-092 + per-SDK gate tests. Projection: AH-CTK-093. Serialized-size default: still host-side (RECOMMENDED, not emitter-enforced) | | TM-13 | Label-flow loss: `result_labels` from non-winning permit verdicts discarded, or §5.4 persistence/resurfacing not honoured | I | In | §7.3 unions: the combined verdict carries the first-seen-ordered label union across every permit verdict in the emission (all profiles, including approval substitutions) | Union half: AH-CTK-086 + per-SDK union tests. §5.4 persistence/`source_labels` resurfacing across emissions: **GAP** — no vector | | TM-14 | Supply-chain compromise of the artefacts: squatted names, mutable CI actions, unpinned deps | T, S | In | Distribution `agent-hooks-sdk` published on PyPI/crates.io (squatted `agent-hooks` avoided); GitHub Actions pinned by commit SHA; `Cargo.lock` committed; CodeQL enabled | Name claims live (registry state); pins in `.github/workflows/*.yml`. Lockfile enforcement (`--locked` CI builds), dependency automation (Dependabot across all ecosystems), and SBOM + provenance attestation in the release pipeline are in place. Published a1 alphas implement a superseded draft (see SECURITY.md) | diff --git a/spec/AGENT-HOOKS-0.1.md b/spec/AGENT-HOOKS-0.1.md index 50e0a78..33bf749 100644 --- a/spec/AGENT-HOOKS-0.1.md +++ b/spec/AGENT-HOOKS-0.1.md @@ -1135,6 +1135,32 @@ emitting `post_model_call`. A host that cannot assemble MUST emit `deny` self-verdict with `host_error:streaming_unsupported`, and MUST NOT incorporate the partial response. +**Exception — incremental mediation.** A host that declares +`buffered_output: false` (§12.1a, §13.1) MAY instead evaluate the +stream incrementally, emitting `post_model_call` more than once per +response, each emission over an assembled prefix or window of it, +provided it satisfies a bounded-exposure accounting discipline: + +1. no part of the response is released to the caller beyond the + declared exposure bound ahead of a verdict covering it; +2. a `deny` terminates the stream and withholds everything not yet + released, including content an earlier emission permitted; +3. content that no emission evaluated MUST fail closed at end of + stream with `host_error:streaming_unsupported` rather than settle + clean — the same failure mode as the non-assembling host above; +4. durable incorporation (§6.1) is gated by the same discipline as + release: denied or unevaluated content MUST NOT be persisted. + +Each such emission is an ordinary `post_model_call` under §4–§7; the +discipline governs what the host does with the verdicts, not the +emission contract. ACS §18.1 ("Incremental stream mediation", +[agent-control-spec](https://github.com/responsibleai/agent-control-spec)) +is one implementation of such a discipline. This exception is +capability-shaped, not vector-backed: conformance vectors exercising +the accounting discipline are future work, and until they exist the +surface is visible only through the declaration and claim (§12.1a, +§13.3). + ### 12.1a Streaming to the caller A host that streams output to its caller MUST buffer the stream and @@ -1149,6 +1175,12 @@ declaration makes that limitation visible in the conformance claim mocked I/O and therefore cannot exercise streaming egress; this capability is declaration-only. +A host that additionally mediates incrementally under the §12.1 +exception MUST state its exposure bound in the declaration: which +content can reach the caller ahead of the verdict covering it, and how +much (e.g. "none — release is watermark-gated" or "each chunk egresses +on arrival; evaluation runs behind the stream"). + ### 12.2 Parallel tool calls When a model response carries N tool calls, the host MAY invoke the @@ -1246,7 +1278,9 @@ plus the CTK report, recorded in `conformance/CLAIMS.md`. A claim with `identity_provider: null` MUST state that its approvals and records are identity-unbound. A claim with `buffered_output: false` MUST state that a `deny` at `output` cannot retract already-streamed content -(§12.1a). +(§12.1a), and, when the host mediates incrementally under the §12.1 +exception, MUST state the exposure bound its accounting discipline +enforces (§12.1a). --- From c7628a71d8dc1ad987936b4fd95d8352779e5a12 Mon Sep 17 00:00:00 2001 From: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com> Date: Fri, 7 Aug 2026 04:57:25 +0000 Subject: [PATCH 2/2] docs(spec): harden 12.1/12.1a per MAF cross-validation Four adjustments from validating the wording against the merged MAF implementation as the reference buffered_output: true host: - 12.1 distinguishes an errored model call from the stream_incomplete shape. Assembly that fails because the call itself errored is an errored action handled per 6.1 - nothing partial egresses or persists, and post_model_call is not the vehicle for reporting the provider's failure. stream_incomplete is for hosts that cannot buffer. - Exception item 4 covers withheld-but-permitted content: durability gating extends to content an earlier emission permitted but the host had not yet released at termination, not only denied or unevaluated content. - 12.1a pins released-content identity on the buffered path: the content released once the verdict permits is the verdicted (post-transform) content; a host must not rewrite content between the verdict and its release. - 12.1a defines the caller as any consumer outside the host's enforcement boundary, observers, callbacks, and preview channels included, matching the ACS 18.1 clause so the observer-channel protection no longer rests on an undefined term. Signed-off-by: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com> --- CHANGELOG.md | 17 +++++++++++++---- spec/AGENT-HOOKS-0.1.md | 20 +++++++++++++++++++- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e74502..0f3b61d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,10 +13,19 @@ User-visible changes to the spec and SDKs. Versioning rules: evaluate incrementally under a bounded-exposure accounting discipline (verdict-covered release, terminating deny that withholds the unreleased remainder, fail-closed residue at end of stream, - §6.1-gated durability). The capability stays declaration-only: the - §12.1a declaration and §13.3 claim MUST state the exposure bound, - and conformance vectors for the accounting discipline are future - work. Additive; no version-surface change. + §6.1-gated durability covering withheld-but-permitted content). The + capability stays declaration-only: the §12.1a declaration and §13.3 + claim MUST state the exposure bound, and conformance vectors for the + accounting discipline are future work. §12.1 also now distinguishes + an errored model call (handled per §6.1, no `post_model_call`) from + the `stream_incomplete` shape, which is for hosts that cannot + buffer. Additive; no version-surface change. +- **§12.1a defines the caller and pins released-content identity.** + The caller is any consumer outside the host's enforcement boundary, + observers, callbacks, and preview channels included, and the content + released once the verdict permits MUST be the verdicted + (post-transform) content — a host MUST NOT rewrite content between + the verdict and its release. ## 0.1.0-alpha.4 — tag `v0.1.0-alpha.4` diff --git a/spec/AGENT-HOOKS-0.1.md b/spec/AGENT-HOOKS-0.1.md index 33bf749..e47b602 100644 --- a/spec/AGENT-HOOKS-0.1.md +++ b/spec/AGENT-HOOKS-0.1.md @@ -1135,6 +1135,14 @@ emitting `post_model_call`. A host that cannot assemble MUST emit `deny` self-verdict with `host_error:streaming_unsupported`, and MUST NOT incorporate the partial response. +Assembly that fails because the model call itself errored is an +errored model call, not this shape: there is no complete response to +evaluate, the host handles the failure as it handles any errored +action — nothing partial egresses or persists (§6.1) — and +`post_model_call` is not the vehicle for reporting the provider's +error. The `stream_incomplete` shape above is for a host that cannot +buffer the stream it received. + **Exception — incremental mediation.** A host that declares `buffered_output: false` (§12.1a, §13.1) MAY instead evaluate the stream incrementally, emitting `post_model_call` more than once per @@ -1149,7 +1157,9 @@ provided it satisfies a bounded-exposure accounting discipline: stream with `host_error:streaming_unsupported` rather than settle clean — the same failure mode as the non-assembling host above; 4. durable incorporation (§6.1) is gated by the same discipline as - release: denied or unevaluated content MUST NOT be persisted. + release: content withheld at termination — including content an + earlier emission permitted but the host had not yet released — or + that no emission evaluated MUST NOT be persisted. Each such emission is an ordinary `post_model_call` under §4–§7; the discipline governs what the host does with the verdicts, not the @@ -1168,6 +1178,14 @@ MUST NOT release any part of it to the caller until the `output` emission's combined verdict permits, UNLESS the host declares the capability `buffered_output: false` in its conformance surface (§13.1). +For this section the *caller* is any consumer outside the host's +enforcement boundary — host-registered observers, callbacks, and +preview channels included — not only the far end of the connection. +The content released once the verdict permits MUST be the verdicted +content (the transformed value when the combined verdict is +`transform`); a host MUST NOT rewrite content between the verdict and +its release. + A host declaring `buffered_output: false` remains conformant, but a `deny` at `output` then cannot retract content already streamed; the declaration makes that limitation visible in the conformance claim