From 38b0746c767baa10f6dbd88ce4104ac994584a82 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 11 Sep 2026 15:30:17 +0000 Subject: [PATCH 1/2] docs(ws4): add Verifiable Intent to the interop map, plus two seam notes Mastercard's Verifiable Intent is a layered SD-JWT delegation credential and a layer-2 occupant, so it gets a row next to ODIS. Interface level only, per the map's own rules: what it emits, what it consumes, and where the OCSF boundary falls. The fact that matters here is that its spec rules a record or audit layer out of scope, so every verification produces a verdict and then nothing durable. That is added to the seams list. Two notes are written as questions for the owners of the representations involved rather than as findings about anyone's row, because the map is descriptive and each player corrects their own row. The first asks whether a representation carrying a credential holds the key that credential is bound to. A hop recording who delegated to whom, without the key, preserves the assertion and drops the proof. The second asks how an absolute expiry and a relative one reconcile. VI carries exp as an absolute timestamp; a representation carrying a duration needs a defined anchor, and the credential's issued-at and the carrier's observation time differ by the ingestion delay, so two identical grants can expire at different moments depending on when they were seen. That one generalises past any particular pair: any layer timestamping an artifact it received rather than the artifact's own claim inherits it. A third note records where the terms of a grant live, framed as a seam rather than one player's gap, since it is the field an ODIS passport, a CMF delegation chain and a VI mandate would all serialize into. Cites ocsf#1756. Added lines are plain ASCII per CLAUDE.md, which means the new row reads "2, Authority grant" where existing rows use an em dash. Cosmetic and open. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XyU3PRkgvMxgimkPBC5726 --- docs/strategy/cosai-ws4-interop-map.md | 41 ++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/docs/strategy/cosai-ws4-interop-map.md b/docs/strategy/cosai-ws4-interop-map.md index 1b10f972..d3835f7c 100644 --- a/docs/strategy/cosai-ws4-interop-map.md +++ b/docs/strategy/cosai-ws4-interop-map.md @@ -52,6 +52,7 @@ corrected by each owner.** |---|---|---|---|---|---| | **AI Identity** | 5 — Record/evidence | Signed OCSF events; DSSE envelopes; offline-verifiable; Evidence Anchor (inclusion proofs); `attestation` object + `record_integrity` profile, merged 2026-07-17, shipped in OCSF 1.9.0 (released 2026-08-03) | Signed, queryable OCSF event records | Identity, authority, attestation, policy signals from layers 1–4 | Maps other layers' outputs into the neutral OCSF evidence schema | | **ODIS** | 2 — Authority grant | Passport / Bridge / Router; "Delegation Chain Record" | Delegation grants / passports | Identity (layer 1) | Grant → recorded as an OCSF delegation event | +| **Verifiable Intent** (Mastercard) | 2, Authority grant | Layered SD-JWT credential chain: L1 binds the user's device key (RFC 7800 `cnf`), L2 delegates to the agent with constraints, L3 splits network- and merchant-facing. Builds only on SD-JWT / JWS / JWK / RFC 7800 | L1/L2/L3 credentials; a verification result (`satisfied`, `violations`, `checked`, `skipped`); a registered constraint vocabulary (`mandate.checkout.*`, `mandate.payment.*`) | User device key; user-authored constraints | Verification result becomes an OCSF event; constraint vocabulary becomes the terms of a delegation. Spec scope excludes any record or audit layer | | **TrustGraph** (Red Hat) | 1 + 2 — Identity + delegation graph; **also 5** — per the group's 2026-07-02 discussion, its audit-trail / traceability output is itself record-of-evidence, currently in OTel rather than OCSF form | KeyCloak SPI + SPIFFE + AuthBridge sidecar → OTel spans → delegation DAG | OTel spans; runtime delegation graph; audit trail | Workload identity, tokens | OTel ↔ OCSF mapping (spans ↔ event records) | | **EQTY Lab** | 4 — Environment attestation | TEE (AMD SEV/TDX, NVIDIA CC); DIDs; model signing; RFC 9421 gateway; offline-verifiable | Hardware attestation quotes; integrity graph; signed certs | Workloads, models | Hardware quote → OCSF workload-attestation object | | **Agent Manifest / TRACE** | 1 + 2 — deploy-time declaration of what the agent *is*; **also 5** via TRACE, its own attestation-record format and append-only registry | Hardware-anchored manifest over deploy-time artifacts (prompt, policy bundle, model identity, tool schemas, delegation chain, provenance); TRACE claims | Signed manifest; TRACE records; registry anchors | Deploy-time artifacts; TEE attestation | Manifest hash as an attested artifact in the runtime record; TRACE claims ↔ OCSF events | @@ -83,9 +84,49 @@ convergence point: - **EQTY hardware quote → OCSF** workload-attestation object - **CMF delegation.chain / security labels / tool context → OCSF** - **Agent Manifest hash → an attested artifact** in the runtime record; **TRACE claims ↔ OCSF** events +- **VI verification result, into an OCSF** event record. Every VI verification produces a verdict and then nothing durable: the spec rules a record layer out of scope, so the result has no interoperable shape to land in - All of the above compose on the **layer-5 record** — one verifiable account of who was authorized, what ran, in what environment, and what happened. +### A note on carrying one layer-2 credential through another (added 2026-09-11) + +Layer 2 now has several occupants, and a credential issued in one +representation will sometimes be carried in another: a VI L2 mandate recorded +as a CMF `delegation.chain` element, an ODIS passport recorded as an OCSF +`delegation`. Reading VI's credential format against a delegation-chain shape +surfaced two questions that are not specific to either pair and are worth +settling once rather than per-integration. Both are for the owners of the +representations involved to answer, not for this document to assert. + +- **Does the carrying representation hold the key the credential is bound to?** + VI's chain integrity rests on key confirmation at each layer. A hop that + records who delegated to whom, without the key, preserves the assertion and + drops the proof. Whether that matters depends on whether the consumer + re-verifies or trusts the carrier. +- **How do an absolute expiry and a relative one reconcile?** VI carries `exp` + as an absolute timestamp with a hard reject rule. A representation carrying + a duration instead needs a defined anchor: the credential's own issued-at, or + the moment the carrier observed it. Those differ by the ingestion delay, so + two identical grants can expire at different moments depending on when they + were seen. + +The second is the more general one. Any layer that timestamps an artifact it +received rather than the artifact's own claim inherits it. + +### Where the terms of a grant live (added 2026-09-11) + +Every layer-2 occupant here expresses bounds on the authority it grants, and +layer 5 currently has nowhere to put them. OCSF's `delegation` object carries +four attributes and all four are correlation identifiers, so a record can say a +grant existed, who issued it and its ancestry, but not what it permitted. That +is filed as [ocsf#1756](https://github.com/ocsf/ocsf-schema/issues/1756), +proposing a `constraint` object covering both a grant's terms and the +consumption against them, and citing VI's constraint vocabulary as prior art. + +Named here because it is a seam rather than one player's gap: it is the field +an ODIS passport, a CMF delegation chain and a VI mandate would all serialize +into. + ## How to use this - Each player **corrects their own row** (1–2 lines is fine). From dfbf1db584e306d2fc1d657dddd87d6bd67390ce Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 11 Sep 2026 15:37:25 +0000 Subject: [PATCH 2/2] docs: changelog entry for the interop map Verifiable Intent addition Records what shipped in the map, not the strategy behind it, per AGENTS.md: the VI row, the seam for a spec that excludes any record or audit layer, and the three notes. Follows the docs-only precedent already in Unreleased. The row stays plain ASCII, so it reads "2, Authority grant" where the surrounding rows use an em dash. Decided, not left open. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XyU3PRkgvMxgimkPBC5726 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bcd51ff9..2dc6646a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - **The OCSF audit plugin tracks PR #84 at `499ee91` and holds `base64` at 0.22 to match the PPE tree.** Teryl answered the two dependency findings on praxis-proxy/policy PR #84 on 2026-09-09: `sha2` is now a workspace dependency at `0.11` (`ef20d8f`), and `base64` stays at `0.22` across the tree because `jsonwebtoken` 11, which `identity-jwt` depends on, requires it, so `0.23` would put two majors in the build (verified against the PPE lock). The same head merges the #82 docs reorganisation, which moves the auditing guide to `docs/content/auditing.md`; nothing on the seam this crate consumes changed between `3e7734e` and `499ee91`. `integrations/cpex-ocsf-audit` moves its pin to that head and its `base64` requirement from `0.23` (a Renovate bump, #497) down to `0.22`, so the copy that lands under `reference/plugins/ocsf-audit` (`PRAXIS-INTREE-PLAN.md`) is a copy and the PPE build stays on one base64; a Renovate rule holds it there. Verified the way CI runs it, on toolchain 1.96.1 with a cpex sibling at `64c8eba`: warning-free `--locked` builds and 34 tests green on each host, `emit_sample` and `decision_sink_demo` byte-identical to the committed vectors, so the AID-EMIT-1 section 12 conformance vector is untouched. The in-tree plan drops `sha2` and `base64` from its list of workspace-table adds (only `p256` remains), records the `base64` answer as settled, and follows the auditing guide to its new path. He also repeated that the `plugin_settings:` load error predates #84 (praxis-proxy/policy #55), which `PRAXIS-PORT-RESULTS.md` observation 3 already records. ### Added +- **The WS4 interop map covers Verifiable Intent, and records three seams it had no entry for.** `docs/strategy/cosai-ws4-interop-map.md` described layer 2 through ODIS alone. Mastercard's Verifiable Intent is a layered SD-JWT delegation credential occupying the same layer: L1 binds the user's device key through the RFC 7800 `cnf` claim, L2 delegates to the agent with constraints attached, L3 splits the network-facing and merchant-facing views. It gets a row next to ODIS, at interface level only, per the map's own descriptive-not-evaluative rule. The seam entry records that the spec puts any record or audit layer out of scope, so a verification yields a verdict and nothing durable. Two notes are written as questions for the owners of the representations involved rather than as findings about any one row, since each player corrects their own: whether a representation carrying a credential also carries the key that credential is bound to, because a hop recording who delegated to whom without the key keeps the assertion and drops the proof; and how an absolute `exp` reconciles with a relative duration, where the anchor is undefined and the credential's issued-at differs from the carrier's observation time by the ingestion delay, so two identical grants expire at different moments depending on when each was seen. The second generalizes past any one pair: any layer that timestamps an artifact it received, rather than the artifact's own claim, inherits it. A third note records where the terms of a grant live and cites `ocsf#1756`, framed as a seam rather than one player's gap, since it is the field an ODIS passport, a CMF delegation chain and a VI mandate would all serialize into. Docs only, no product impact. - **Constraint types on the mandate document are namespaced, and the schema version bump that required is the interesting part.** A mandate could express exactly one bound, `spend_limit`, as a bare field with no type discriminator, so adding a second kind was never a one-line change: there was nothing to dispatch on and every consumer would have had to infer a bound's kind from which keys happened to be present. `MandateDocument.constraints` now carries `Constraint` entries with a dotted `type` of at least two segments (`mandate/app/constraints.py`), so `spend.ceiling` and a counterparty's `mastercard.payment.budget` coexist without either side winning a naming argument first. Unknown types are rejected at parse rather than skipped, the same rule already applied to unknown top-level fields and unevaluated `conditions`, and a registered type with no evaluator behind it denies at spend time under the new `mandate_constraint_unevaluable` reason: naming a type is a promise to evaluate it. The ceiling itself stays on `spend_limit`, registered in the vocabulary but refused inside `constraints[]`, because one bound with two encodings is how the two disagree later. **The risk was the signature, not the vocabulary.** `constraints` sits inside the signed payload, so adding it naively would have given every re-serialized 1.0 and 1.1 document an empty list it was never signed over, silently invalidating every mandate ever issued. `_POST_1_1_FIELDS` strips it below schema 1.2, extending the gate `_POST_1_0_FIELDS` already established for `spend_limit`; the default `schema_version` moves to 1.2. Two parametrized tests pin the canonical bytes for 1.0 and 1.1 and both go red against the naive implementation, verified by removing the gate and re-running. Thirteen new tests, including that a constraint added after signing fails verification (built with `model_construct`, since an attacker editing a stored document does not pass through the validator). Mandate suite 81 passed / 6 skipped, gateway mandate tests 21 passed, ruff clean. - **The gateway's enforcement path now evaluates a mandate's signed `conditions`, closing the gap `#531` documented but did not fix.** `conditions` was written at issue time and read by nothing: the verify endpoint reported `valid: true` while ignoring it (fixed in `#531`), and the gateway, which actually gates requests, never saw it, because it draws against `/mandates/{id}/spend` and that response carried no conditions. An issuer could sign `{"env": "prod"}` and a staging agent would spend against the grant unimpeded. The evaluation lands in the Mandate Service rather than the gateway, which looks backwards until the ordering is considered: the check must be atomic with the spend, and evaluating at the gateway means evaluating after the draw returns, by which point an accepted spend is already recorded against the budget of a request about to be denied. So the gateway sends the context it alone holds (agent metadata plus endpoint and method, with the observed request facts winning over stored attributes claiming otherwise) and the service that owns the grant applies its own terms before mutating state. Conditions reuse `common.policy.eval.evaluate_when`, the same function behind a policy's `when` clause, so a mandate condition and a policy condition mean the same thing and fail the same way: a missing key or an unknown operator is a failed match, never a pass. A caller that sends no context is therefore denied on a conditioned mandate, which is the intended rollout direction. Conditions are checked after status but before currency and limit, because a grant that does not cover the request is not a budget question; `deny_reason` gains `mandate_conditions_unmet`, and the per-condition results ride the audit row and the OCSF `unmapped.mandate` block as `conditions_checked` / `conditions_failed` so a denial says not just "denied" but which field failed. A condition-failing settlement is refused rather than recorded, unlike an over-limit settlement: incrementing `spent_cents` would assert this grant funded the spend, which is what the failed condition denies, and the audit row and event log still capture the attempt either way. Thirteen new tests; mandate suite 68 passed / 6 skipped, gateway suite 142 passed, ruff clean. - **The in-tree port plan now sizes the `base64` version conflict, and answers the #84 review's version-spec claim before it reaches our PR.** `PRAXIS-INTREE-PLAN.md` listed `p256`, `sha2` and `base64` as three equal adds to PPE's `[workspace.dependencies]`. Read against the manifests at `3e7734e`, only `p256` is one: `sha2 = "0.11"` is already declared directly by four crates (`ppe-core`, `ppe-apl-runtime`, `builtins/plugins/delegator-oauth`, `builtins/session/valkey`) at the version this crate uses, which is the inconsistency praxis-bot flagged on PR #84 on 2026-09-09, and `base64` is a conflict rather than an add, since `identity-jwt`, `delegator-oauth` and `elicitation-ciba` each declare `0.22` directly while this crate is on `0.23`, a semver-incompatible major. The plan's default is now to pin this crate down to `0.22`: base64 is reached only through `Engine::encode` and `Engine::decode` on `general_purpose::STANDARD` in `src/sign.rs` and `src/emitter.rs`, an API unchanged across both versions, so the expected cost is the manifest line rather than a base64 upgrade across three builtins the port PR has no other reason to touch. Carrying both majors is the third option and the worst one; `deny.toml` sets `multiple-versions = "warn"`, so `make audit` would not fail, but the tree has one major today. The choice goes to the PR thread as an open question. The plan also records that the other half of the #84 review, a patch component on the version spec (`sha2 = "0.11.0"`), is not this tree's convention: the workspace table uses `tokio = "1"`, `thiserror = "2"` and `hashbrown = "0.17"` throughout. Docs only, no product impact.