@openwop/spec-artifacts 2.35.1 → 2.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/CORPUS-STAMP.json +103 -89
  2. package/api/openapi.yaml +325 -4
  3. package/api/seams-v2.yaml +342 -1
  4. package/api/v2/asyncapi.yaml +1 -1
  5. package/api/v2/openapi.yaml +564 -8
  6. package/package.json +1 -1
  7. package/schemas/a2a-task-state.schema.json +3 -3
  8. package/schemas/artifact-type-pack-manifest.schema.json +3 -3
  9. package/schemas/capabilities.schema.json +34 -1
  10. package/schemas/connection-pack-manifest.schema.json +14 -0
  11. package/schemas/debug-bundle.schema.json +20 -0
  12. package/schemas/dispatch-config.schema.json +1 -1
  13. package/schemas/localized-content-language-settings.schema.json +3 -3
  14. package/schemas/localized-content-page.schema.json +2 -2
  15. package/schemas/localized-content-section.schema.json +3 -3
  16. package/schemas/run-event-payloads.schema.json +4 -2
  17. package/schemas/suspend-request.schema.json +61 -4
  18. package/schemas/v2/a2a-task-state.schema.json +10 -9
  19. package/schemas/v2/agent-deployment-transition.schema.json +4 -4
  20. package/schemas/v2/agent-deployment.schema.json +7 -7
  21. package/schemas/v2/agent-eval-suite.schema.json +10 -10
  22. package/schemas/v2/agent-inventory-response.schema.json +8 -2
  23. package/schemas/v2/artifact-type-pack-manifest.schema.json +3 -3
  24. package/schemas/v2/artifact.schema.json +84 -0
  25. package/schemas/v2/capabilities.schema.json +82 -8
  26. package/schemas/v2/certification-bundle.schema.json +1 -1
  27. package/schemas/v2/channel-presence-payload.schema.json +3 -3
  28. package/schemas/v2/connection-pack-manifest.schema.json +14 -0
  29. package/schemas/v2/conversation-event.schema.json +9 -2
  30. package/schemas/v2/conversation-turn.schema.json +19 -12
  31. package/schemas/v2/dispatch-config.schema.json +1 -1
  32. package/schemas/v2/envelopes/ui.a2ui-surface.schema.json +920 -22
  33. package/schemas/v2/error-envelope.schema.json +11 -1
  34. package/schemas/v2/eval-summary.schema.json +1 -2
  35. package/schemas/v2/goal.schema.json +4 -4
  36. package/schemas/v2/localized-content-language-settings.schema.json +3 -3
  37. package/schemas/v2/localized-content-page.schema.json +2 -2
  38. package/schemas/v2/localized-content-section.schema.json +3 -3
  39. package/schemas/v2/node-pack-manifest.schema.json +105 -0
  40. package/schemas/v2/part.schema.json +81 -0
  41. package/schemas/v2/proposal.schema.json +6 -6
  42. package/schemas/v2/run-event-payloads.schema.json +5 -3
  43. package/schemas/v2/self-hosted-runner-dispatch-frame.schema.json +2 -2
  44. package/schemas/v2/self-hosted-runner-result-frame.schema.json +1 -1
  45. package/schemas/v2/suspend-request.schema.json +251 -6
  46. package/schemas/v2/tool-descriptor.schema.json +246 -2
  47. package/schemas/v2/trigger-event.schema.json +1 -2
  48. package/schemas/v2/trigger-subscription.schema.json +1 -2
  49. package/schemas/v2/webhook-verification.schema.json +22 -0
  50. package/schemas/v2/workflow-definition.schema.json +25 -25
  51. package/spec/v1/alias-detectors.json +13 -1
  52. package/spec/v1/core-standard-manifest.json +2 -2
  53. package/spec/v1/deprecations.json +45 -1
  54. package/spec/v1/deprecations.schema.json +119 -4
  55. package/spec/v1/gaps.json +1925 -4
  56. package/spec/v2/README.md +5 -2
  57. package/spec/v2/core/capabilities.md +13 -26
  58. package/spec/v2/core/conformance.md +5 -8
  59. package/spec/v2/core/connection-packs.md +1 -1
  60. package/spec/v2/core/conversation.md +1 -1
  61. package/spec/v2/core/errors.md +8 -3
  62. package/spec/v2/core/events.md +8 -20
  63. package/spec/v2/core/form-content-packs.md +2 -2
  64. package/spec/v2/core/headers.md +5 -4
  65. package/spec/v2/core/host-services.md +6 -2
  66. package/spec/v2/core/idempotency.md +1 -1
  67. package/spec/v2/core/identity.md +23 -12
  68. package/spec/v2/core/interop.md +34 -4
  69. package/spec/v2/core/interrupt.md +6 -3
  70. package/spec/v2/core/node-pack-runtimes.md +20 -0
  71. package/spec/v2/core/oauth.md +32 -0
  72. package/spec/v2/core/overview.md +19 -6
  73. package/spec/v2/core/packs.md +4 -6
  74. package/spec/v2/core/persistence.md +20 -55
  75. package/spec/v2/core/replay.md +5 -11
  76. package/spec/v2/core/runs.md +9 -8
  77. package/spec/v2/core/security-defaults.md +18 -20
  78. package/spec/v2/core/tool-catalog.md +22 -0
  79. package/spec/v2/core/versioning.md +10 -28
  80. package/spec/v2/core/webhooks.md +16 -4
  81. package/spec/v2/core/workflow-chain-packs.md +1 -1
  82. package/spec/v2/corrections.json +32 -0
  83. package/spec/v2/corrections.schema.json +49 -0
  84. package/spec/v2/declaration.json +18 -13
  85. package/spec/v2/declaration.schema.json +3 -1
  86. package/spec/v2/errors.json +46 -1
  87. package/spec/v2/ext/a2uiSurface/README.md +131 -17
  88. package/spec/v2/ext/dataIntegration/README.md +5 -0
  89. package/spec/v2/facets/a2a.schema.json +4 -0
  90. package/spec/v2/facets/auth.schema.json +24 -0
  91. package/spec/v2/facets/mcp.schema.json +4 -0
  92. package/spec/v2/facets/oauth.schema.json +70 -0
  93. package/spec/v2/facets/webhooks.schema.json +23 -2
  94. package/spec/v2/id-field-bindings.json +2 -0
  95. package/spec/v2/interop-map.json +639 -0
  96. package/spec/v2/interop-map.schema.json +458 -0
  97. package/spec/v2/migrations.json +7 -0
  98. package/spec/v2/migrations.schema.json +58 -0
  99. package/spec/v2/path-manifest.json +6 -1
  100. package/spec/v2/release.json +3 -3
  101. package/spec/v2/surface-baseline.json +7981 -0
package/spec/v2/README.md CHANGED
@@ -42,6 +42,9 @@ and [`core/overview.md`](./core/overview.md).
42
42
  | [`profiles.json`](./profiles.json) | Generated profile predicates. |
43
43
  | [`peer-dependency-aliases.json`](./peer-dependency-aliases.json) | Generated v1 alias mapping. |
44
44
  | [`release.json`](./release.json) | Authoritative corpus release identity. |
45
+ | [`migrations.json`](./migrations.json) | RFC 0197 — the v2→v2 migration register: what a 2.x minor replaced and which deprecation row schedules the old surface out. Empty is a legal state. |
46
+ | [`corrections.json`](./corrections.json) | RFC 0197 §A.4 — W3C Class-3 corrections that changed a v2 shape, each with the bundle and registry census that showed nothing conforming stopped conforming. |
47
+ | [`surface-baseline.json`](./surface-baseline.json) | RFC 0197 §A.1 — GENERATED census of every v2 surface, enumerated by instance path; `check-v2-surface-monotone.mjs` diffs the tree against it. Regenerated by the release PR. |
45
48
 
46
49
  Generated files identify their generator in `$comment` or their header. Do not
47
50
  edit generated outputs directly. Machine artifacts are published in
@@ -52,7 +55,7 @@ exact-version peer dependency.
52
55
 
53
56
  | Area | Documents |
54
57
  | --- | --- |
55
- | Foundation | [`overview`](./core/overview.md), [`versioning`](./core/versioning.md), [`headers`](./core/headers.md), [`identity`](./core/identity.md), [`capabilities`](./core/capabilities.md) |
58
+ | Foundation | [`overview`](./core/overview.md), [`versioning`](./core/versioning.md), [`headers`](./core/headers.md), [`identity`](./core/identity.md), [`oauth`](./core/oauth.md), [`capabilities`](./core/capabilities.md) |
56
59
  | Execution | [`runs`](./core/runs.md), [`events`](./core/events.md), [`interrupt`](./core/interrupt.md), [`persistence`](./core/persistence.md), [`idempotency`](./core/idempotency.md), [`replay`](./core/replay.md) |
57
- | Integration | [`webhooks`](./core/webhooks.md), [`interop`](./core/interop.md), [`packs`](./core/packs.md), [`connection packs`](./core/connection-packs.md), [`form-content packs`](./core/form-content-packs.md), [`workflow-chain packs`](./core/workflow-chain-packs.md) |
60
+ | Integration | [`webhooks`](./core/webhooks.md), [`interop`](./core/interop.md), [`packs`](./core/packs.md), [`node-pack runtimes`](./core/node-pack-runtimes.md), [`connection packs`](./core/connection-packs.md), [`form-content packs`](./core/form-content-packs.md), [`workflow-chain packs`](./core/workflow-chain-packs.md) |
58
61
  | Reliability | [`errors`](./core/errors.md), [`security defaults`](./core/security-defaults.md), [`conformance`](./core/conformance.md) |
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## Why this exists
6
6
 
7
- v1 discovery had 91 root properties on an open root, `supported` in four shapes, and four machine registries with no schema of their own. v2 has one capability record type on a closed root, generated from one declaration file that also mints pack peer-dependency identifiers and the `§` anchors below. Profiles are derived predicates, never a wire field.
7
+ v2 discovery has one capability record type on a closed root, generated from one declaration file that also mints pack peer-dependency identifiers and the `§` anchors below. Profiles are derived predicates, never a wire field.
8
8
 
9
9
  ## 1. One well-known resource (RFC 0176 §C.1)
10
10
 
@@ -47,19 +47,19 @@ The root of `schemas/v2/capabilities.schema.json` is `additionalProperties: fals
47
47
 
48
48
  ### 3.1 Metadata keys (17)
49
49
 
50
- `protocolVersion`, `protocolVersions`, `preferredVersion`, `extensions`, `implementation`, `engineVersion`, `eventLogSchemaVersion`, `configurable`, `observability`, `minClientVersion`, `runtimeCapabilities`, `testing`, `conformance`, `fixtures`, `compliance`, `discovery`, and `supportedTransports`. Each is declared as metadata with its own schema in `spec/v2/declaration.json`; a metadata key is not a record and carries no `status` or `witness`. `supportedTransports` is declared only to record its deletion (§4). Version-axis metadata are specified in `versioning.md`; `configurable` in `runs.md`.
50
+ `protocolVersion`, `protocolVersions`, `preferredVersion`, `extensions`, `implementation`, `engineVersion`, `eventLogSchemaVersion`, `configurable`, `observability`, `minClientVersion`, `runtimeCapabilities`, `testing`, `conformance`, `fixtures`, `compliance`, `discovery`, and `supportedTransports`. Each is declared as metadata with its own schema in `spec/v2/declaration.json`; a metadata key is not a record and carries no `status` or `witness`. `supportedTransports` is declared only to record its deletion (§4). Version-axis metadata are specified in `versioning.md`; `configurable` in `runs.md`. `implementation` (`name`, `version`, `vendor`, `url`) is self-reported: a client SHOULD NOT change behaviour because of it or authorize from it.
51
51
 
52
52
  ### 3.2 `extensions.<org>.<name>`
53
53
 
54
- Vendor and host extensions live under one key, `extensions`, whose members MUST match `^[a-z][a-z0-9]*(-[a-z0-9]+)*\.[a-z][a-z0-9]*(-[a-z0-9]+)*$` (short-form `<org>.<name>`). The orgs `openwop` and `vendor` are reserved: a host MUST NOT use either. An extension record's shape is the org's own (`additionalProperties: true` inside the record). The 11 v1 extension-class `host.*` families (§6) are advertised as `extensions.openwop-app.*` by the host that serves them.
54
+ Vendor and host extensions live under one key, `extensions`, whose members MUST match `^[a-z][a-z0-9]*(-[a-z0-9]+)*\.[a-z][a-z0-9]*(-[a-z0-9]+)*$` (short-form `<org>.<name>`). The orgs in `spec/v2/declaration.json` `reservedOrgs` — `openwop`, `vendor`, `effect-seams` and `events` — are reserved: a host MUST NOT use any of them. An extension record's shape is the org's own (`additionalProperties: true` inside the record). The 11 v1 extension-class `host.*` families (§6) are advertised as `extensions.openwop-app.*` by the host that serves them. A change to an extension that would break an existing reader MUST ship under a new key; a host MUST NOT read one key's record as another's (A2A §4.6.3).
55
55
 
56
56
  ## 4. Deleted keys (RFC 0169 §A.5, §B.3; RFC 0175)
57
57
 
58
58
  | Key | Why |
59
59
  | --- | --- |
60
60
  | `contractProvenance` | an advisory self-declaration the wire cannot falsify (RFC 0169 §A.5) |
61
- | `supportedTransports` | REST is the wire; there is no transport advertisement — A2A and MCP are compositions advertised by their own facets (RFC 0175 §B.1) |
62
- | `grpc` | `witness: unwitnessable` (the suite ships no client); an unwitnessable family is not advertisable and its text moves to `spec/v2/ext/grpc-transport/` (RFC 0175 §A.1) |
61
+ | `supportedTransports` | REST is the wire (interop.md; RFC 0175 §B.1) |
62
+ | `grpc` | unwitnessable, so not advertisable (interop.md §gRPC; RFC 0175 §A.1) |
63
63
  | `Capabilities-Etag`, `auth.subjectLinking`, `replay.fork`, bare `a2a.supported` / `mcp.supported`, the `openwop-core` alias | absent from the v2 root; rows `C2.1`–`C2.10` in `spec/v1/migrations.json` carry each with its codemod |
64
64
  | `host.media`, `host.collaboration` | reserved slots are unrepresentable under a closed root; `host.workspace` is the declared family `workspace` |
65
65
 
@@ -75,11 +75,11 @@ Each heading is a `spec/v2/declaration.json` row with `anchor: core`; `scripts/c
75
75
 
76
76
  ### § supportedEnvelopes
77
77
 
78
- Witness `witnessable-gated`; owner no owning RFC (declaration row).
78
+ Witness `witnessable-gated`; owner RFC 0193; facets `kinds`.
79
79
 
80
80
  ### § schemaVersions
81
81
 
82
- Witness `witnessable-gated`; owner no owning RFC (declaration row).
82
+ Witness `witnessable-gated`; owner RFC 0193; facets `kinds`.
83
83
 
84
84
  ### § limits
85
85
 
@@ -87,7 +87,7 @@ Witness `witnessable-gated`; owner no owning RFC (declaration row).
87
87
 
88
88
  ### § envelopeStrictness
89
89
 
90
- Witness `claims-check`; owner no owning RFC (declaration row).
90
+ Witness `claims-check`; owner RFC 0193; facets `mode`.
91
91
 
92
92
  ### § envelopeContracts
93
93
 
@@ -255,7 +255,7 @@ Witness `witnessable-gated`; owner RFC 0053.
255
255
 
256
256
  ### § webhooks
257
257
 
258
- Witness `witnessable-gated`; owner RFC 0188; facets `deadLetter`, `retryPolicy`, `signatureAlgorithms`.
258
+ Witness `witnessable-gated`; owner RFC 0188; facets `deadLetter`, `retryPolicy`, `secretRotation`, `signatureAlgorithms`.
259
259
 
260
260
  ### § triggerBridge
261
261
 
@@ -309,11 +309,11 @@ Witness `witnessable-gated`; owner RFC 0019.
309
309
 
310
310
  ### § workflowChainPacks
311
311
 
312
- Witness `witnessable-gated`; owner RFC 0013.
312
+ Witness `witnessable-gated`; owner RFC 0013; facets `subChains`.
313
313
 
314
314
  ### § packs
315
315
 
316
- Witness `claims-check`; owner RFC 0025.
316
+ Witness `claims-check`; owner RFC 0025; facets `testMode`.
317
317
 
318
318
  ### § mcp
319
319
 
@@ -385,7 +385,7 @@ The `openwop-core` alias is deleted (row `C2.3`); the canonical discovery-only i
385
385
 
386
386
  | Axis | Values | Source |
387
387
  | --- | --- | --- |
388
- | `technical` | `experimental \| stable \| deprecated` | the record's `status` |
388
+ | `technical` | `experimental \| stable \| deprecated` | the record's `status`, which MUST NOT exceed the declaration row (RFC 0197) |
389
389
  | `adoption` | `none \| single-witness \| multi-witness \| independent` | derived from INTEROP-MATRIX bundle evidence |
390
390
 
391
391
  `stable` does not require a tier-3 host; `independent` records whether one exists. The RFC 0155 numeric extension budget is repealed: a family MAY exist at any count if it declares its witness class. At the cut, `memory.injectionBudget`, `toolCatalog.compactView`, and `aiProviders.promptPrefixCache` keep their families with `adoption: single-witness` (RFC 0169 §C.5).
@@ -396,17 +396,4 @@ The `openwop-core` alias is deleted (row `C2.3`); the canonical discovery-only i
396
396
 
397
397
  ## 10. Migration rows (RFC 0169)
398
398
 
399
- | Row | v1 | v2 |
400
- | --- | --- | --- |
401
- | `C2.1` | `capabilities` wrapper | none (`openwop.codemod.capabilities-wrapper-removal`) |
402
- | `C2.2` | `host.<family>` dotted mirrors | none |
403
- | `C2.3` | `openwop-core` | `openwop-discovery-core` |
404
- | `C2.4` | `contractProvenance` | none |
405
- | `C2.5` | `auth.subjectLinking` | none; advertising `saml` and `scim` lanes implies the contract (`identity.md`) |
406
- | `C2.6` | bare `a2a.supported` / `mcp.supported` | `versions[]` / `revisions[]` facets (the codemod refuses when no array is present) |
407
- | `C2.7` | `replay.fork` boolean | `replay.modes[]` |
408
- | `C2.8` | the 11 extension-class `host.*` families | `extensions.<org>.<name>` |
409
- | `C2.9` | `Capabilities-Etag` | standard `ETag` / `If-None-Match` (dual emission through the overlap) |
410
- | `C2.10` | root `profiles[]` | none — schema-invalid |
411
-
412
- Rows `C2.2`–`C2.8` are transformed by `openwop.codemod.discovery-document-v2`; a family with `supported: false` is dropped, and a dotted-only declared family is promoted to its plain key. Certification bundles naming `openwop-core` are never-upgraded and remain valid v1 evidence at their version.
399
+ Rows `C2.1`–`C2.10` are `spec/v1/migrations.json` entries. `C2.2`–`C2.8` are transformed by `openwop.codemod.discovery-document-v2`; a family with `supported: false` is dropped, and a dotted-only declared family is promoted to its plain key. Certification bundles naming `openwop-core` are never-upgraded and remain valid v1 evidence at their version.
@@ -4,11 +4,11 @@
4
4
 
5
5
  ## Why this exists
6
6
 
7
- v1 could say a host passed and could not say what it witnessed: test ids were derived from titles, the witness class was recorded on extensions but not requirements, nine test-seam operations sat in the canonical API, and the certification bundle had an open root and no signature. This document is the v2 evidence contract: how a requirement is asserted, how every requirement declares what can witness it, how the seams are mounted, what the suite ships, and what a bundle proves. Profiles are in overview.md; the capability vocabulary the suite gates on is capabilities.md.
7
+ The v2 evidence contract: how a requirement is asserted, how every requirement declares what can witness it, how the seams are mounted, what the suite ships, and what a bundle proves. Profiles are in overview.md; the capability vocabulary the suite gates on is capabilities.md.
8
8
 
9
9
  ## Requirement ids
10
10
 
11
- `expect(x, req('openwop.<area>.<slug>', '<doc> §<section>', '<requirement>'))` is the only assertion form. A scenario assertion without a requirement id MUST fail the suite's lint. Ids are minted in `conformance/requirements.json`; every test declares its id explicitly. A title reword without a corresponding `requirement-aliases.json` row MUST fail CI, because published bundles cite ids and an orphaned id orphans every bundle that cited it.
11
+ `expect(x, req('openwop.<area>.<slug>', '<doc> §<section>', '<requirement>'))` is the only assertion form. A scenario assertion without a requirement id MUST fail the suite's lint. Ids are minted in `conformance/requirements.json`; every test declares its id explicitly. A title reword without a corresponding `requirement-aliases.json` row MUST fail CI.
12
12
 
13
13
  The ledger records per `it`, and a bundle's `results.requirements[]` is the per-assertion list. A post-assertion soft-skip MUST record `skipped` for every id not reached and MUST NOT record `pass`.
14
14
 
@@ -21,8 +21,7 @@ failure MUST be `blocked`, never `inapplicable`; a suite with a blocked row MUST
21
21
  NOT issue a certification (RFC 0168 §E.1).
22
22
 
23
23
  When more than one gate can skip a test, evaluate host predicates before suite
24
- predicates. This preserves the most specific truthful reason while ensuring a
25
- suite failure cannot be hidden as an inapplicable host requirement.
24
+ predicates.
26
25
 
27
26
  ## Witness class
28
27
 
@@ -39,8 +38,6 @@ Every family in `spec/v2/declaration.json`, every requirement in `conformance/re
39
38
 
40
39
  A protocol-tier invariant marked `unwitnessable` MUST fail the corpus gate. `tests: []` is expressible only as `unwitnessable`. A bundle disposition `blocked` does not exist as a witness class: what v1 called blocked is `seam-gated` or `unwitnessable`.
41
40
 
42
- The six v1 certification admissions map to one class each: the shape-versus-behavior dual grade is `witnessable-gated` on the behavioral leg, never two grades; "blocked as unobservable" is `seam-gated` or `unwitnessable`; install-time-only extension opacity is `claims-check`; corpus-structural legs run in the spec repo's CI and have no host class; gRPC end-to-end is `unwitnessable` (interop.md); a negative-existence claim is `negative-existence`.
43
-
44
41
  A MUST whose only witness is `seam-gated` MUST either mint a normative observation path before the cut or be demoted to SHOULD. The seam count in `docs/witness-baseline.json` is a ratchet and MUST NOT rise.
45
42
 
46
43
  ## The seams profile
@@ -73,11 +70,11 @@ A certification bundle validates against `schemas/v2/certification-bundle.schema
73
70
 
74
71
  `signature` is an Ed25519 attestation over the canonical JSON of `{ witnessSha256, host.build, suite.version, discovery.sha256 }`; `over` MUST list exactly those four members. A host that signs bundles MUST publish the corresponding public keys as `signingKeys[]` in its discovery document, and `signature.keyId` MUST name one of them. A verifier MUST resolve `keyId` there — in the discovery document of the host the bundle is *about* — and MUST verify the attestation under the published key.
75
72
 
76
- A signature that cannot be resolved to a published key attests **integrity only**: it proves the bundle was not altered after signing, and proves nothing about who signed it, because a signer can mint a keypair and a key id at will. Such a bundle MUST NOT be read as attributable evidence, and a gate MUST distinguish three outcomes that a presence check collapses into one — *no discovery document was read*, *read and the key is not published*, and *the attestation does not verify*. A retired key MUST stay listed, because removing it silently invalidates every bundle it already signed. `evidenceTier: independent` MUST carry a `verifierKeyId` distinct from the host's signing key; the verifier MUST refuse, not warn, on a missing or self-signed independent claim. A bundle with `totals.blocked > 0` does not certify. At major 2 a requirement a test did not observe records `blocked` even when the test asserted setup facts first; an `executed-pass` carrying a `partial-witness:` detail is reserved for a leg that observed its requirement and skipped an optional extra. `host.relaxations[]` lies outside the attestation, so `witnessSha256` covers it whenever it is non-empty; a verifier recomputing the digest therefore detects a relaxation removed after signing. A verifier MUST derive the operator's opt-outs from the signed `skipped` rows and MUST reject a bundle whose captured discovery document advertises one of them (`opted-out-but-advertised`). A profile that carries an operator relaxation (`host.relaxations[]`) cannot certify. v1 and v2 bundles are never upgraded to v3; a bundle is evidence at its own version.
73
+ A signature that cannot be resolved to a published key attests **integrity only**. Such a bundle MUST NOT be read as attributable evidence, and a gate MUST distinguish three outcomes that a presence check collapses into one — *no discovery document was read*, *read and the key is not published*, and *the attestation does not verify*. A retired key MUST stay listed. `evidenceTier: independent` MUST carry a `verifierKeyId` distinct from the host's signing key; the verifier MUST refuse, not warn, on a missing or self-signed independent claim. A bundle with `totals.blocked > 0` does not certify. At major 2 a requirement a test did not observe records `blocked` even when the test asserted setup facts first; an `executed-pass` carrying a `partial-witness:` detail is reserved for a leg that observed its requirement and skipped an optional extra. A verifier MUST derive the operator's opt-outs from the signed `skipped` rows and MUST reject a bundle whose captured discovery document advertises one of them (`opted-out-but-advertised`). v1 and v2 bundles are never upgraded to v3; a bundle is evidence at its own version.
77
74
 
78
75
  ### Recovery evidence
79
76
 
80
- RFC 0158 §E publishes a rung and its recovery bounds here instead of in discovery. The attestation covers `witnessSha256`, and `witnessSha256` digests the rows, so the evidence rides on rows: a row's `evidence` enters the digest only when present, and a bundle without it digests as it always did.
77
+ Recovery evidence (RFC 0158 §E) rides on rows, which `witnessSha256` digests: a row's `evidence` enters the digest only when present, and a bundle without it digests as it always did.
81
78
 
82
79
  | Row | `evidence` member |
83
80
  | --- | --- |
@@ -5,7 +5,7 @@
5
5
 
6
6
  ## Why this exists
7
7
 
8
- A connection pack is a signed provider definition — the endpoints, scope catalog, and reach a connector's `auth.provider` string resolves against. v1 let two definitions claim one `provider.id` and picked between them silently by version. v2 makes the id unique per host and makes a collision fail closed. The manifest is `schemas/v2/connection-pack-manifest.schema.json`; installation and signing follow packs.md.
8
+ A connection pack is a signed provider definition — the endpoints, scope catalog, and reach a connector's `auth.provider` string resolves against. The manifest is `schemas/v2/connection-pack-manifest.schema.json`; installation and signing follow packs.md.
9
9
 
10
10
  ## Provider identity
11
11
 
@@ -5,7 +5,7 @@
5
5
 
6
6
  ## Why this exists
7
7
 
8
- RFC 0101 and RFC 0110 are `Accepted`, their wire shapes are frozen in `schemas/v2/`, and conformance covers them — but each deliberately mints no client route for opening a conversation, and so neither was ever given an operative home. The obligation an advertising host takes on is real regardless of who routes the call; it is stated here.
8
+ RFC 0101 and RFC 0110 mint no client route for opening a conversation; the obligation an advertising host takes on is stated here.
9
9
 
10
10
  ## `multiPartyConversation`
11
11
 
@@ -8,9 +8,9 @@ Every error a v2 host returns is a row in one registry. A client routes on `erro
8
8
 
9
9
  ## The registry
10
10
 
11
- `spec/v2/errors.json` holds one row per code: `{ code, httpStatus, retriable, details, since, deprecated? }` plus the provenance fields `statusSource` and `source`. It registers **101** codes. `schemas/v2/error-envelope.schema.json` is GENERATED from it and MUST NOT be edited by hand.
11
+ `spec/v2/errors.json` holds one row per code: `{ code, httpStatus, retriable, details, since, deprecated? }` plus the provenance fields `statusSource` and `source`. It registers **106** codes. `schemas/v2/error-envelope.schema.json` is GENERATED from it and MUST NOT be edited by hand.
12
12
 
13
- A host MUST return a registered code, or a vendor code, in every error response. A vendor code MUST match `^(?!openwop\.)[a-z][a-z0-9]*(-[a-z0-9]+)*\.[a-z][a-z0-9_]*$` with its first segment an org registered in `spec/v2/declaration.json`; `openwop.` is reserved. The registry grows by the closed-enum rule in overview.md §0: a producer MUST NOT emit an unregistered member, and a consumer MUST accept an unknown registered member and MUST NOT act on it.
13
+ A host MUST return a registered code, or a vendor code, in every error response. A vendor code MUST match `^(?!openwop\.)[a-z][a-z0-9]*(-[a-z0-9]+)*\.[a-z][a-z0-9_]*$` with its first segment an org registered in `spec/v2/declaration.json`; `openwop.` is reserved. The registry grows by overview.md §0.
14
14
 
15
15
  ## The envelope
16
16
 
@@ -28,7 +28,7 @@ An interrupt has one code per state: a token or run-scoped resolve against an in
28
28
 
29
29
  ## Codes by HTTP status
30
30
 
31
- Generated from `spec/v2/errors.json` (101 codes; `retriable` and `statusSource` are in the registry).
31
+ Generated from `spec/v2/errors.json` (106 codes; `retriable` and `statusSource` are in the registry).
32
32
 
33
33
  Code | Status
34
34
  --- | ---
@@ -54,12 +54,16 @@ Code | Status
54
54
  `schedule_horizon_exceeded` | 400
55
55
  `sub_chain_cycle` | 400
56
56
  `sub_chain_depth_exceeded` | 400
57
+ `unsupported_runtime` | 400
57
58
  `unsupported_stream_mode` | 400
58
59
  `until_in_past` | 400
59
60
  `validation_error` | 400
61
+ `webhook_endpoint_unverified` | 400
60
62
  `webhook_url_rejected` | 400
61
63
  `audience_mismatch` | 401
64
+ `connector_auth_declined` | 401
62
65
  `connector_auth_expired` | 401
66
+ `credential_lifetime_exceeded` | 401
63
67
  `credential_revoked` | 401
64
68
  `delegation_expired` | 401
65
69
  `identity_unresolvable` | 401
@@ -108,6 +112,7 @@ Code | Status
108
112
  `unsupported_media_type` | 415
109
113
  `capability_not_provided` | 422
110
114
  `capability_required` | 422
115
+ `connection_auth_metadata_mismatch` | 422
111
116
  `credential_required` | 422
112
117
  `envelope_invalid` | 422
113
118
  `envelope_refusal` | 422
@@ -5,7 +5,7 @@
5
5
 
6
6
  ## Why this exists
7
7
 
8
- A run is its append-only event log; every snapshot, stream, poll, fork and diff is a projection of it. v2 has one event envelope, one closed type registry with one naming rule, one payload registry, one ordering field, one events channel and one poll cursor, so that a typo is a validation failure and not a silently ignored event.
8
+ A run is its append-only event log; every snapshot, stream, poll, fork and diff is a projection of it. v2 has one closed envelope, type registry, payload registry, ordering field, events channel and poll cursor.
9
9
 
10
10
  ## The envelope
11
11
 
@@ -24,7 +24,7 @@ A consumer MUST NOT throw on an event whose `type` it does not know; it folds wh
24
24
 
25
25
  ## Types
26
26
 
27
- `type` is `oneOf` a closed enum of registered protocol types and a vendor pattern. The enum is GENERATED from `spec/v2/event-codemap.json` (117 rows, every row `decided`) and MUST NOT be edited by hand. The vendor branch is exactly:
27
+ `type` is `oneOf` a closed enum of registered protocol types and a vendor pattern. The enum is GENERATED from `spec/v2/event-codemap.json` (118 rows, every row `decided`) and MUST NOT be edited by hand. The vendor branch is exactly:
28
28
 
29
29
  ```text
30
30
  ^(?!openwop\.)[a-z][a-z0-9]*(-[a-z0-9]+)*\.[a-z][a-z0-9]*(-[a-z0-9]+)*(\.[a-z][a-z0-9]*(-[a-z0-9]+)*)?$
@@ -43,8 +43,7 @@ A consumer MUST NOT throw on an event whose `type` it does not know; it folds wh
43
43
 
44
44
  The CloudEvents mapping and the webhook delivery envelope are GENERATED from the same definition (one source, three renderings): the event's `type`, `eventId`, `sequence` and `payload` are byte-identical across the run stream, a CloudEvents rendering and a webhook delivery.
45
45
 
46
- `run.started` carries `owner { tenant, workspace?, subject }`, the same closed
47
- block as `RunSnapshot.owner` with `subject` REQUIRED (runs.md, identity.md).
46
+ `run.started` carries the `owner` block (identity.md §1.1).
48
47
  `run.cancelled` carries `reason`, `cancelledBy`, `durationMs`, and `parentRunId`.
49
48
  `run.completed` MUST carry `outputs` as an object; an empty object is valid, but
50
49
  an absent key is not. This distinguishes “no outputs” from “outputs not
@@ -52,7 +51,7 @@ rendered” and is witnessed by `v2-run-completed-outputs`.
52
51
 
53
52
  ## AI envelopes: E1–E5
54
53
 
55
- `schemas/v2/ai-envelope.schema.json` is the shape an LLM emits; the engine records its acceptance as one or more `RunEventDoc`s. In v2 `correlationId` and `meta.source` are REQUIRED on every envelope, and an engine MUST reject an envelope that omits either; nothing is synthesized. An envelope kind MUST be namespaced under the same `<org>.` rule as events, universal kinds excepted.
54
+ `schemas/v2/ai-envelope.schema.json` is the shape an LLM emits; the engine records its acceptance as one or more `RunEventDoc`s. In v2 `correlationId` and `meta.source` are REQUIRED on every envelope, and an engine MUST reject an envelope that omits either; nothing is synthesized. An envelope kind MUST be namespaced under the same `<org>.` rule as events, universal kinds and the core content-primitive families `ui.*` and `media.*` excepted.
56
55
 
57
56
  | Gap | Contract |
58
57
  | --- | --- |
@@ -62,14 +61,6 @@ rendered” and is witnessed by `v2-run-completed-outputs`.
62
61
  | E4 sub-typing | `$ref` composition, as in the payload registry above. |
63
62
  | E5 refusal × retry | `configurable.ai.maxRefusals` (runs.md) is the ceiling on `envelope.refusal` events a run records. A host MUST NOT retry the emission that produced a refusal. |
64
63
 
65
- Worked example (E5), `maxRefusals: 2`:
66
-
67
- ```text
68
- seq 7 envelope.refusal nodeId n1 (refusal 1; the run's retry policy re-dispatches n1)
69
- seq 9 envelope.refusal nodeId n1 (refusal 2 = ceiling)
70
- seq 10 node.failed error.code envelope_refusal; n1 is not re-dispatched
71
- ```
72
-
73
64
  ## The events channel
74
65
 
75
66
  `api/v2/asyncapi.yaml` declares one channel, `runEvents`, at `/runs/{runId}/events`, and `api/v2/openapi.yaml` declares the same path (`streamRunEvents`); the two MUST resolve to the same absolute path (RFC 0172 §C.2). The `streamMode` query parameter is one pattern, not four enums:
@@ -116,26 +107,23 @@ A run's log MUST contain exactly one terminal run event — `run.completed`, `ru
116
107
 
117
108
  ## Era-2 logs
118
109
 
119
- An `eventLogSchemaVersion` of `2` means v1-written. Every reader (poll, stream, fork, diff, debug bundle) MUST translate each event through `spec/v2/event-codemap.json` at storage — `type` is mapped, the payload projected; `sequence` (including `0`), `eventId`, `timestamp`, `causationId` pass through. A type the codemap does not name, carrying no vendor org, MUST fail the read `500 event_type_unmapped`. A host MUST NOT carry a private mapping, nor rewrite era-2 rows in place. **A projection MUST NOT silently drop a property**: carry or fail `500 payload_unprojectable` (hatch `^(openwop-|x-|vendor\.)`; RFC 0185). Fork and replay over an era-2 parent: replay.md.
110
+ An `eventLogSchemaVersion` of `2` means v1-written; every reader, diff included, translates it per persistence.md §"The reader rule". **A projection MUST NOT silently drop a property**: carry or fail `500 payload_unprojectable` (hatch `^(openwop-|x-|vendor\.)`; RFC 0185). Fork and replay over an era-2 parent: replay.md.
120
111
 
121
112
  ## The envelope-kind catalog
122
113
 
123
114
  `supportedEnvelopes`, `schemaVersions` and `envelopeStrictness` are one flow, read in
124
- that order on every inbound envelope. v1 carried them as an array, a map and an enum;
125
- a v2 capability record is an object, so each holds its value in a named seat.
115
+ that order on every inbound envelope.
126
116
 
127
117
  `supportedEnvelopes.kinds` is the catalog. A host advertising it MUST refuse an
128
118
  emitted `type` that is neither universal nor a member, with `unknown_envelope_kind`.
129
119
  **An absent `kinds` is not an empty catalog and is not an unrestricted one**: a host
130
120
  advertising `supportedEnvelopes` without it has made no catalog claim, and an engine
131
- MUST refuse every non-universal kind rather than admit it unchecked. Absence that
132
- reads as "unrestricted" would let a record claiming `stable` admit any kind an
133
- attacker can name, which is precisely what `prompt-injection-envelope-typecheck`
134
- forbids.
121
+ MUST refuse every non-universal kind rather than admit it unchecked.
135
122
 
136
123
  `schemaVersions.kinds` maps a kind to its advertised floor; a kind absent from the map
137
124
  has a floor of `0`. An emitted `schemaVersion` ABOVE the floor MUST be refused with
138
125
  `unknown_schema_version` whatever the strictness.
126
+ `ui.a2ui-surface` at schema version 2 is specified by `ext/a2uiSurface/README.md`.
139
127
 
140
128
  `envelopeStrictness.mode` governs drift BELOW the floor only. Under `warn` — the value
141
129
  when the seat is absent — an engine MUST validate against the advertised version and
@@ -5,11 +5,11 @@
5
5
 
6
6
  ## Why this exists
7
7
 
8
- A form-content pack ships declarative form templates a host renders in its own chrome. v1 templates had no conditional visibility, no localization, and no validation beyond `required` and `format`. v2 adds all three by reusing constructs the corpus already defines. The manifest is `schemas/v2/form-content-pack-manifest.schema.json`; installation and signing follow packs.md.
8
+ A form-content pack ships declarative form templates a host renders in its own chrome. The manifest is `schemas/v2/form-content-pack-manifest.schema.json`; installation and signing follow packs.md.
9
9
 
10
10
  ## Conditional visibility
11
11
 
12
- A field MAY carry `when: <EdgeCondition>`. The grammar is the `WorkflowEdge.condition` object `{ type, left, right }` of `schemas/v2/workflow-definition.schema.json`, with the operator set of workflow-chain-packs.md §"Edge conditions": `type` is one of `expression`, `equals`, `notEquals`, `contains`, `regex`, `truthy`, `falsy`; `truthy` and `falsy` take `left` only. A host MUST evaluate `when` with its edge-condition semantics and MUST NOT accept any other expression language for visibility.
12
+ A field MAY carry `when: <EdgeCondition>`. The grammar is the `WorkflowEdge.condition` object `{ type, left, right }` of `schemas/v2/workflow-definition.schema.json`, with the operator set of workflow-chain-packs.md §"Edge conditions". A host MUST evaluate `when` with its edge-condition semantics and MUST NOT accept any other expression language for visibility.
13
13
 
14
14
  ```jsonc
15
15
  { "id": "region", "type": "select", "label": "Region",
@@ -11,12 +11,12 @@ RFC 0171 §C.1: every non-standard header is `OpenWOP-<Name>` and every header i
11
11
  | Header | Operations | Meaning |
12
12
  | --- | --- | --- |
13
13
  | `Accept-Language` | 1 | BCP-47 preference list; authoritative for locale selection (i18n.md). A malformed value MUST NOT 400. |
14
- | `Idempotency-Key` | 14 | Per-mutation idempotency token (see `idempotency.md` Layer 1). |
14
+ | `Idempotency-Key` | 15 | Per-mutation idempotency token (see `idempotency.md` Layer 1). |
15
15
  | `If-None-Match` | 2 | RFC 0165 §C.2; runs.md §Snapshot. Standard conditional request against any resource that carries an `ETag` — the discovery document (capabilities.md §1) and the run snapshot (runs.md §Snapshot): a matching value MUST yield `304 Not Modified` with no body, and the 304 carries `OpenWOP-Version` like every response (versioning.md §1.4). |
16
16
  | `Last-Event-ID` | 1 | Resume from sequence after this ID. |
17
17
  | `OpenWOP-Dedup` | 1 | When set, server cross-host claim system rejects duplicate `(tenantId, scopeId)` pairs with `409 Conflict`. |
18
18
  | `OpenWOP-Force-Engine-Version` | 1 | **Test-keys-only.** When set, the server emits events for this run AS IF it |
19
- | `OpenWOP-Version` | 53 | RFC 0172 §A.3 — selects a listed major.minor; absent ⇒ the host's `preferredVersion`; unlisted ⇒ 406 protocol_version_unsupported. |
19
+ | `OpenWOP-Version` | 54 | RFC 0172 §A.3 — selects a listed major.minor; absent ⇒ the host's `preferredVersion`; unlisted ⇒ 406 protocol_version_unsupported. |
20
20
 
21
21
  ## Response headers
22
22
 
@@ -28,12 +28,13 @@ RFC 0171 §C.1: every non-standard header is `OpenWOP-<Name>` and every header i
28
28
  | `ETag` | 3 | Optional probe handle for mid-session capability change detection. Deprecated toward v2 (RFC 0165 §C.2). |
29
29
  | `Location` | 1 | Canonical URI of the new template. |
30
30
  | `OpenWOP-Idempotent-Replay` | 1 | Set when the response was served from the idempotency cache. |
31
- | `OpenWOP-Version` | 53 | RFC 0172 §A.4 — the contract that produced this response; MUST equal the one used. |
31
+ | `OpenWOP-Version` | 54 | RFC 0172 §A.4 — the contract that produced this response; MUST equal the one used. |
32
32
  | `Retry-After` | 1 | Seconds until the active claim is stale-eligible. |
33
+ | `WWW-Authenticate` | 47 | RFC 0200 §B — `Bearer` challenge; `resource_metadata` and `error="invalid_token"` on a host with an oauth2/oidc lane. Never on a non-disclosure 404. |
33
34
 
34
35
  ## Webhook delivery headers
35
36
 
36
- Declared in `webhooks.md`, not in OpenAPI (the host is the client): `OpenWOP-Webhook-Id`, `OpenWOP-Event-Type`, `OpenWOP-Timestamp`, `OpenWOP-Signature`, `OpenWOP-Signature-Algorithm` (RFC 0165 §C.1). The `X-openwop-*` family is emitted beside them through the overlap and removed at v1 end-of-support (`spec/v1/deprecations.json` `webhook-x-header-family`).
37
+ Declared in `webhooks.md`, not in OpenAPI (the host is the client): `OpenWOP-Webhook-Id`, `OpenWOP-Event-Type`, `OpenWOP-Timestamp`, `OpenWOP-Signature`, `OpenWOP-Signature-Algorithm` (RFC 0165 §C.1). The `X-openwop-*` family is emitted beside them through the overlap and removed at v1 end-of-support (`spec/v1/deprecations.json` `webhook-x-header-family`). On a subscription that opted into Standard Webhooks (webhooks.md), that standard's `webhook-id`, `webhook-timestamp` and `webhook-signature`, which keep their standard names (RFC 0201 §F).
37
38
 
38
39
  ## Removed in v2
39
40
 
@@ -1,11 +1,11 @@
1
1
  # Host services
2
2
 
3
3
  > **Status: Stable · v2.0 · RFC 0144.** Normative contract for the advertised `host.*` service surfaces a node pack invokes through `ctx`.
4
- > **Normative home:** `aiEnvelope`, `promptLibrary`, `agentRuntime`.
4
+ > **Normative home:** `aiEnvelope`, `promptLibrary`, `agentRuntime`, `mcp`.
5
5
 
6
6
  ## Why this exists
7
7
 
8
- `spec/v1/host-capabilities.md` describes these three surfaces with a TypeScript sketch, a `**Required methods:**` label and a failure-mode list, and — unlike its sibling sections, which reach for `MUST` constantly — states no obligation. A lowercase section label is not an RFC 2119 keyword, so the contract a host takes on by advertising these families was never written down. v1 is frozen, so it is written here.
8
+ `spec/v1/host-capabilities.md` describes these three surfaces but states no RFC 2119 obligation; the contract a host takes on by advertising them is written here.
9
9
 
10
10
  ## `aiEnvelope`
11
11
 
@@ -18,3 +18,7 @@ A host advertising `promptLibrary` MUST expose `ctx.promptLibrary.get`, MUST ret
18
18
  ## `agentRuntime`
19
19
 
20
20
  A host advertising `agentRuntime` MUST expose `spawn`, `delegate`, `consensus` and `messageSend`, and MUST satisfy `agents.manifestRuntime`, which advertising it implies.
21
+
22
+ ## `mcp`
23
+
24
+ A host advertising `mcp.client` MUST expose to pack code `ctx.mcp.callTool`, `listTools`, `readResource` and `serverHealth`, each against a host-configured `serverId` at the revision `mcp` negotiates. Each rejects only for an unknown `serverId` (`not_found`), an MCP error response (carried unaltered), or a transport failure. `callTool` MUST resolve to the server's `CallToolResult` unaltered (`content[]`, `structuredContent`, `isError`, `_meta`), including when `isError` is true; the host handles an `InputRequiredResult` itself and never returns one. `listTools` MUST resolve to one `ListToolsResult` page unaltered, `outputSchema`, `annotations`, `nextCursor`, `ttlMs` and `cacheScope` included, and MUST forward a pack's `cursor`; `readResource` resolves to the `ReadResourceResult` unaltered. `serverHealth` MUST report `reachable`, `unreachable` or `incompatible` from a `server/discover` probe no older than its `ttlMs`, with the `DiscoverResult` when one was received; it MUST NOT report a connection or session state, which MCP 2026-07-28 does not have.
@@ -5,7 +5,7 @@
5
5
 
6
6
  ## Why this exists
7
7
 
8
- A retried request MUST NOT create a second run, and a retried node MUST NOT issue a second external effect. A host MUST implement Layer 1 for every mutating endpoint and Layer 2 for every node executor that performs an external side effect.
8
+ A retried request MUST NOT create a second run, and a retried node MUST NOT issue a second external effect. A host MUST implement Layer 1 for every mutating endpoint; a host that advertises `idempotency` MUST implement Layer 2 for every node executor that performs an external side effect (security-defaults.md).
9
9
 
10
10
  ## Layer 1: `Idempotency-Key`
11
11
 
@@ -5,7 +5,7 @@
5
5
 
6
6
  ## Why this exists
7
7
 
8
- v1 carried a `principal` beside an optional Subject, a legacy rule that was advisory, a `SubjectLink` with no schema, and resume tokens with no scheme. v2 makes the Subject the owner of every run, binds every lane to a trust root and a revocation rule, gives the link and every id a grammar, and prefixes tokens so a host can rotate them. Idempotency-key grammar is `idempotency.md`.
8
+ v2 makes the Subject the owner of every run, binds every lane to a trust root and a revocation rule, gives the link and every id a grammar, and prefixes tokens so a host can rotate them. Idempotency-key grammar is `idempotency.md`.
9
9
 
10
10
  ## 1. The Subject is the owner (RFC 0170 §A)
11
11
 
@@ -41,7 +41,7 @@ An end user reaching the host through an A2A peer is `kind: anonymous`, `lane: a
41
41
 
42
42
  ### 2.1 The pipeline (§B.1)
43
43
 
44
- Every lane MUST: verify the credential against the lane's trust root; bind the verified identity to the request, never to an asserted header; check audience; resolve to a Subject before any authorization decision; and fail closed. The closed reason vocabulary is the family-wide error set in §6. Every lane is advertised as one member of the `auth.lanes[]` facet (`spec/v2/facets/auth.schema.json`):
44
+ Every lane MUST: verify the credential against the lane's trust root; bind the verified identity to the request, never to an asserted header; check audience; resolve to a Subject before any authorization decision; and fail closed. On the `oidc` lane an ID token MAY be a bearer only when its `aud` equals the host's configured audience; any other `aud` is `audience_mismatch`. The closed reason vocabulary is the family-wide error set in §6. Every lane is advertised as one member of the `auth.lanes[]` facet (`spec/v2/facets/auth.schema.json`):
45
45
 
46
46
  `authorization.failClosed` advertises that rule and MUST be `true` when present; it does not gate it (invariant `authorization-fail-closed`). `authorization.roles` is the host role catalog: a request is authorized when any role-derived scope matches the required scope, under the same scope-match semantics this document applies to a credential.
47
47
 
@@ -60,8 +60,8 @@ Every lane MUST name its trust root as `subject.issuer` and MUST advertise it in
60
60
  | Lane | `subject.issuer` (trust root) | Revocation MUST | `revocation` |
61
61
  | --- | --- | --- | --- |
62
62
  | `api-key` | the key realm (`urn:<host>:api-key` or a host-chosen URI) | refuse a revoked key on the next request (`credential_revoked`) | `next-request` |
63
- | `oauth2` | the token issuer | honor `exp`; re-check the issuer within the advertised `revocationWindowSeconds` | `exp-and-recheck` |
64
- | `oidc` | `iss` | as `oauth2` | `exp-and-recheck` |
63
+ | `oauth2` | the token issuer | honor `exp` and re-check the issuer within the advertised `revocationWindowSeconds`; or honor `exp` alone under an enforced lifetime bound (`exp-only`, below) | `exp-and-recheck \| exp-only` |
64
+ | `oidc` | `iss` | as `oauth2` | `exp-and-recheck \| exp-only` |
65
65
  | `mtls` | the CA subject | check CRL or OCSP, or issue certificates whose lifetime is at most the advertised window | `crl \| ocsp \| short-lived` |
66
66
  | `saml` | the IdP entityID (`<saml:Issuer>`) | honor `NotOnOrAfter`; consult the SCIM link deny-set when both lanes are advertised (§3) | `not-on-or-after` |
67
67
  | `scim` | the SCIM connection id bound at configuration to one IdP entityID | bind each client credential to one IdP entityID; refuse an unbound request | `bound-connection` |
@@ -70,7 +70,11 @@ Every lane MUST name its trust root as `subject.issuer` and MUST advertise it in
70
70
  | `session` | `urn:<host>:session` | refuse a revoked session on the next request (`credential_revoked`) | `next-request` |
71
71
  | `anonymous` | `urn:<host>:anon-surface` | — | — |
72
72
 
73
- `revocationWindowSeconds` (integer ≥ 1) MUST be advertised wherever the rule names a window (`exp-and-recheck`, `short-lived`, `rebind`).
73
+ `revocationWindowSeconds` (integer ≥ 1) MUST be advertised wherever the rule names a window — `exp-and-recheck`, `exp-only`, `short-lived`, `rebind`. On every lane it is **an upper bound on the interval between a revocation at the trust root and the host's first refusal**, and a host MUST NOT advertise a window it does not enforce.
74
+
75
+ A host MUST NOT advertise a `revocation` value the row above for its lane does not list. A consumer meeting an unrecognized value MUST NOT act on it: it MUST read the lane as stating no revocation latency, never as `next-request` or any other member (`overview.md` §0).
76
+
77
+ **`exp-only` (RFC 0210).** `exp-only` names a host that honors `exp` and re-checks revocation never: it consults no introspection endpoint, no userinfo endpoint, no revocation list and no host-side epoch or `validAfter` record, so a credential revoked at the trust root is accepted until its own `exp`. Its window is therefore the only bound there is, and it is enforced rather than described. A host advertising `exp-only` on a lane MUST refuse a credential presented on that lane when **either** `exp − iat` (total lifetime) **or** `exp − now` (remaining lifetime) exceeds the advertised `revocationWindowSeconds`, with `401 credential_lifetime_exceeded`; a credential carrying no `iat` MUST be refused with the same code, because the first bound cannot be evaluated without it (§2.1 fail-closed). Both bounds are load-bearing: `exp − iat` alone admits a ten-year token minted ten years ago, `exp − now` alone admits a freshly minted ten-year token in its ninth year. A host that cannot enforce both MUST NOT advertise `exp-only`. `exp-only` SHOULD be advertised with a window of one hour or less; the corpus states no maximum, because no upstream specification does. `exp-only` MUST NOT be advertised on the `api-key` or `session` lane — there the host issued the credential itself, so revocation is in its own hands — and `auth.schema.json` refuses that pairing (invariant `lane-exp-only-lifetime-bounded`).
74
78
 
75
79
  ### 2.3 Minimum assurance (§B.4)
76
80
 
@@ -80,6 +84,12 @@ Each lane MUST advertise `minimumAssurance: bearer | sender-constrained | key-bo
80
84
 
81
85
  The proof format is lane-scoped: mTLS key binding or DPoP for the two JWT lanes (`oauth2`, `oidc`), SVID chains for `workload`. A host MUST advertise the proofs it accepts under `auth.lanes[].delegationProofs[]` (`mtls-key-binding | dpop | svid-chain`). A chain with no acceptable proof MUST be refused as `identity_unverified`. The chain rules keep their codes: a chain longer than the bound is `delegation_chain_too_long`, a cyclic chain is `delegation_chain_cyclic`, and a link that widens scope is `delegation_scope_amplified` (invariants `delegation-chain-bounded-acyclic`, `delegation-no-scope-amplification`, `delegation-provenance-not-authorization`).
82
86
 
87
+ ### 2.5 Protected-resource metadata and challenges (RFC 0200)
88
+
89
+ A host advertising an `oauth2` or `oidc` lane MUST serve RFC 9728 metadata at the well-known URI formed from its resource identifier, the base URL it serves this API under, with `/.well-known/oauth-protected-resource` inserted before any path (`https://h.example/api` → `https://h.example/.well-known/oauth-protected-resource/api`), unauthenticated. `resource` MUST equal that identifier; `authorization_servers` MUST list exactly the URL-form issuers of those lanes; `scopes_supported` MUST list the scopes the host enforces; `dpop_bound_access_tokens_required` or `tls_client_certificate_bound_access_tokens` MAY be `true` only where every such lane's `minimumAssurance` requires that binding (§2.3).
90
+
91
+ On such a host a `401` MUST carry `WWW-Authenticate: Bearer resource_metadata="<url>"`, adding `error="invalid_token"` when a credential was presented and no error code when none was, and a `403` for insufficient scope MUST carry `error="insufficient_scope"` with `scope` listing every scope the operation requires; a `403` for resource binding carries no `insufficient_scope`. Other hosts SHOULD send `WWW-Authenticate: Bearer` on a `401`. A challenge attaches only to a response already `401` or `403` and MUST NOT change a status: where a rule requires `404` for an unknown or unauthorized resource, the `404` stands and carries none (invariant `auth-challenge-no-oracle`).
92
+
83
93
  ## 3. The link is a record (RFC 0170 §C; `schemas/v2/subject-link.schema.json`)
84
94
 
85
95
  ```json
@@ -90,13 +100,13 @@ The proof format is lane-scoped: mTLS key binding or DPoP for the two JWT lanes
90
100
 
91
101
  The record and both `SubjectRef`s are closed; `a`, `b`, `keyClass`, `issuer`, `tenant`, `formedAt` are REQUIRED. A link MUST be tenant-scoped, MUST join exactly two subjects whose `issuer` values are bound to one IdP entityID (`issuer` on the record), and MUST NOT include a legacy (`urn:openwop:legacy` is schema-rejected) or anonymous subject. Deactivation sets `deniedAt`; the SAML decision path MUST consult it (the leaver contract). The link is a reference, not a merge: nothing rewrites a subject already stamped on a run.
92
102
 
93
- `auth.subjectLinking` is removed (`capabilities.md` row `C2.5`): advertising both `saml` and `scim` lanes implies the contract. Lanes stay separate facets; there is no single "enterprise identity" profile. The `auth.subjectLinkKey` facet (`opaque-idp | configured-immutable`) names the key class the host forms links under. Invariant `subject-link-record-shape` is registered with its scenario.
103
+ `auth.subjectLinking` is removed (migration row `C2.5`): advertising both `saml` and `scim` lanes implies the contract. Lanes stay separate facets; there is no single "enterprise identity" profile. The `auth.subjectLinkKey` facet (`opaque-idp | configured-immutable`) names the key class the host forms links under.
94
104
 
95
105
  ## 4. Resume tokens (RFC 0170 §E.1; RFC 0176 §B.2)
96
106
 
97
107
  An interrupt resume token is `ow2.<alg>.<kid>.<payload>.<mac>`: `alg ∈ {hs256}` at the cut (`interrupt.tokenAlgs[]` advertises it), `kid` (`keyId` grammar) selects the verification secret, `payload` and `mac` as in v1. A host MUST refuse a token whose `alg` it does not advertise or whose `kid` it does not hold with `401` `interrupt_token_invalid`. The `{token}` path parameter carries the grammar (`api/v2/openapi.yaml`).
98
108
 
99
- An issued v1 two-segment token MUST remain resolvable under `kid: legacy` until its `expiresAt`; a run suspended on an interrupt at the cut continues under `persistence.md` and its outstanding token resolves the same way. Interrupt semantics are `interrupt.md`.
109
+ A token the host issued under v1 (any token not `ow2.`-prefixed; the rule is the prefix, never a segment count, persistence.md) MUST remain resolvable under `kid: legacy` until its `expiresAt`; a run suspended on an interrupt at the cut continues under `persistence.md` and its outstanding token resolves the same way. Interrupt semantics are `interrupt.md`.
100
110
 
101
111
  ## 5. Identifier grammars (`schemas/v2/ids.schema.json`)
102
112
 
@@ -113,17 +123,17 @@ Every id field in every v2 schema and every `api/v2/openapi.yaml` parameter and
113
123
  | `nodeId`, `workflowId`, `agentId`, `chainId`, `pluginId`, `templateId`, `libraryId` | `^[A-Za-z0-9._~:-]{1,128}$` | author |
114
124
  | `typeId` | `^[a-z][a-z0-9_-]*(\.[a-z][a-zA-Z0-9_-]*)+$`, maxLength 256 | author |
115
125
 
116
- `scripts/check-id-kinds-bound.mjs` enforces this against `spec/v2/id-field-bindings.json`, which sorts every `*Id` property in a v2 schema into two sets: it **is** a kind above (and MUST `$ref` it), or nothing here governs it (reason recorded). Neither **fails**, so no id field lands without someone deciding which it is. A map rather than a name rule because only 20 of 88 `*Id` properties share a name with a kind: `childRunId` sat as `{type: string, minLength: 1}` in the file where `parentRunId` was bound, and a name-keyed check reports green over that. *Every id field*, not every matching name.
126
+ `scripts/check-id-kinds-bound.mjs` enforces this against `spec/v2/id-field-bindings.json`, which sorts every `*Id` property in a v2 schema into two sets: it **is** a kind above (and MUST `$ref` it), or nothing here governs it (reason recorded); a field in neither set fails. *Every id field*, not every matching name.
117
127
 
118
128
  A host MUST reject a tenant-bound id whose tenant segment is not the caller's with `403` `id_tenant_mismatch`. A host-minted opaque segment MUST match `^[A-Za-z0-9._~-]{16,128}$`.
119
129
 
120
- **On the wire a tenant-bound id is one path segment, projected** (RFC 0184): every UTF-8 byte outside `[A-Za-z0-9._-]` becomes `~` plus two uppercase hex digits, so `acme/r-9f3c…` travels as `acme~2Fr-9f3c…`. A host MUST emit it in every link and MUST accept it on every tenant-bound parameter; it MUST still accept `tenant%2Fopaque`, and MUST decode either before matching the grammar. A host MUST NOT mint a tenant-bound id containing `~`; ids already minted MUST still resolve. A host MUST project exactly once, where an id leaves it, and MUST NOT re-encode its own output. `~` is the escape, not `%`, because RFC 3986 §2.3 makes it unreserved — no intermediary may rewrite it (`ids.schema.json` records what `%2F` costs).
130
+ **On the wire a tenant-bound id is one path segment, projected** (RFC 0184): every UTF-8 byte outside `[A-Za-z0-9._-]` becomes `~` plus two uppercase hex digits, so `acme/r-9f3c…` travels as `acme~2Fr-9f3c…`. A host MUST emit it in every link and MUST accept it on every tenant-bound parameter; it MUST still accept `tenant%2Fopaque`, and MUST decode either before matching the grammar. A host MUST NOT mint a tenant-bound id containing `~`; ids already minted MUST still resolve. A host MUST project exactly once, where an id leaves it, and MUST NOT re-encode its own output.
121
131
 
122
- **Through the overlap the bare form is admitted on a major-2 path parameter** — an affordance with an expiry, not a branch of the grammar. A parameter carrying only the opaque segment (what a `/v1/` create hands out) MUST resolve under the caller's tenant and never another's, and the response MUST name the resource bound (`versioning.md` §5); the credential supplies the segment the `403` check would read. Once a host advertises no `1.x` member it MUST refuse the bare form `400 validation_error` (not `id_tenant_mismatch`, not `not_found`). Ids in documents and bodies are bound, always. A client MAY bind at its request seam. Handle grammars (`memoryRef`, workspace `path`/`etag`, the plugin version token) and their `resolvability` class are specified where each handle is used; an importer MUST re-mint every `host`-scoped handle (`spec/v2/ext/portability/`).
132
+ **Through the overlap the bare form is admitted on a major-2 path parameter**. A parameter carrying only the opaque segment (what a `/v1/` create hands out) MUST resolve under the caller's tenant and never another's, and the response MUST name the resource bound (`versioning.md` §5); the credential supplies the segment the `403` check would read. Once a host advertises no `1.x` member it MUST refuse the bare form `400 validation_error` (not `id_tenant_mismatch`, not `not_found`). Ids in documents and bodies are bound, always. A client MAY bind at its request seam. Handle grammars (`memoryRef`, workspace `path`/`etag`, the plugin version token) and their `resolvability` class are specified where each handle is used; an importer MUST re-mint every `host`-scoped handle (`spec/v2/ext/portability/`).
123
133
 
124
134
  ## 6. Identity error codes (`spec/v2/errors.json`)
125
135
 
126
- Every code below is a row with `since: "2.0"`, `retriable: false`, and no `details` contract; the envelope is `errors.md`.
136
+ Every code below is a row with `retriable: false` and no `details` contract; the envelope is `errors.md`. All are `since: "2.0"` except `credential_lifetime_exceeded` (`since: "2.36"`, RFC 0210).
127
137
 
128
138
  | Code | HTTP | Raised when |
129
139
  | --- | --- | --- |
@@ -131,6 +141,7 @@ Every code below is a row with `since: "2.0"`, `retriable: false`, and no `detai
131
141
  | `identity_unresolvable` | 401 | a verified identity resolves to no Subject |
132
142
  | `audience_mismatch` | 401 | the credential's audience is not this host |
133
143
  | `credential_revoked` | 401 | a revoked key or session is presented (§2.2) |
144
+ | `credential_lifetime_exceeded` | 401 | a credential on an `exp-only` lane exceeds the advertised lifetime bound, or carries no `iat` (§2.2) |
134
145
  | `delegation_expired` | 401 | a delegation or workload credential is past its lifetime |
135
146
  | `sender_constraint_missing` | 401 | the request is below the lane's `minimumAssurance` (§2.3) |
136
147
  | `delegation_chain_too_long` | 400 | the actor chain exceeds depth 4 |
@@ -143,4 +154,4 @@ Every code below is a row with `since: "2.0"`, `retriable: false`, and no `detai
143
154
 
144
155
  ## 7. Invariants (RFC 0170 §E.2)
145
156
 
146
- `workload-identity-cryptographically-bound`, `delegation-provenance-not-authorization`, `delegation-no-scope-amplification`, `delegation-chain-bounded-acyclic`, `sender-constraint-no-bearer-downgrade`, `provenance-attestation-digest-bound`, `subject-link-record-shape`, and `subject-required-on-owner` are registered in `SECURITY/invariants.yaml` with their scenarios; an invariant that reaches the cut without a witness is demoted from `protocol` tier and recorded.
157
+ The RFC 0170 §E.2 invariants are registered in `SECURITY/invariants.yaml` with their scenarios; an invariant that reaches the cut without a witness is demoted from `protocol` tier and recorded.