@openwop/spec-artifacts 2.42.5 → 2.42.7

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 (63) hide show
  1. package/CORPUS-STAMP.json +64 -63
  2. package/api/seams-v2.yaml +238 -316
  3. package/api/v2/asyncapi.yaml +83 -113
  4. package/api/v2/openapi.yaml +339 -724
  5. package/package.json +1 -1
  6. package/schemas/audit-verify-result.schema.json +2 -2
  7. package/schemas/v2/audit-verify-result.schema.json +2 -2
  8. package/schemas/v2/capabilities.schema.json +2 -5
  9. package/spec/v1/core-standard-manifest.json +2 -2
  10. package/spec/v1/gaps.json +171 -1
  11. package/spec/v2/README.md +34 -36
  12. package/spec/v2/core/capabilities.md +186 -119
  13. package/spec/v2/core/conformance.md +78 -27
  14. package/spec/v2/core/connection-packs.md +8 -3
  15. package/spec/v2/core/conversation.md +10 -4
  16. package/spec/v2/core/errors.md +30 -9
  17. package/spec/v2/core/events.md +116 -80
  18. package/spec/v2/core/form-content-packs.md +11 -13
  19. package/spec/v2/core/headers.md +22 -14
  20. package/spec/v2/core/host-services.md +22 -5
  21. package/spec/v2/core/idempotency.md +48 -36
  22. package/spec/v2/core/identity.md +110 -42
  23. package/spec/v2/core/interop.md +82 -21
  24. package/spec/v2/core/interrupt.md +79 -40
  25. package/spec/v2/core/node-pack-runtimes.md +18 -4
  26. package/spec/v2/core/oauth.md +40 -6
  27. package/spec/v2/core/overview.md +61 -33
  28. package/spec/v2/core/packs.md +53 -28
  29. package/spec/v2/core/persistence.md +61 -80
  30. package/spec/v2/core/replay.md +56 -42
  31. package/spec/v2/core/runs.md +198 -60
  32. package/spec/v2/core/security-defaults.md +57 -33
  33. package/spec/v2/core/tool-catalog.md +29 -6
  34. package/spec/v2/core/versioning.md +95 -49
  35. package/spec/v2/core/webhooks.md +59 -20
  36. package/spec/v2/core/workflow-chain-packs.md +16 -14
  37. package/spec/v2/corrections.json +17 -1
  38. package/spec/v2/corrections.schema.json +1 -1
  39. package/spec/v2/declaration.json +24 -7
  40. package/spec/v2/declaration.schema.json +10 -0
  41. package/spec/v2/errors.json +109 -2
  42. package/spec/v2/errors.schema.json +38 -0
  43. package/spec/v2/ext/README.md +36 -23
  44. package/spec/v2/ext/a2uiSurface/README.md +75 -89
  45. package/spec/v2/ext/brand/README.md +15 -15
  46. package/spec/v2/ext/canvas/README.md +15 -15
  47. package/spec/v2/ext/chat/README.md +15 -15
  48. package/spec/v2/ext/coordination/README.md +15 -15
  49. package/spec/v2/ext/dataIntegration/README.md +17 -20
  50. package/spec/v2/ext/entities/README.md +15 -15
  51. package/spec/v2/ext/grpc-transport/README.md +6 -8
  52. package/spec/v2/ext/kanban/README.md +15 -15
  53. package/spec/v2/ext/knowledge/README.md +15 -15
  54. package/spec/v2/ext/launchStudio/README.md +16 -16
  55. package/spec/v2/ext/messaging/README.md +15 -15
  56. package/spec/v2/ext/portability/README.md +3 -17
  57. package/spec/v2/ext/provider-idempotency/README.md +7 -10
  58. package/spec/v2/ext/restTransport/README.md +30 -19
  59. package/spec/v2/ext/sandbox-runtime-notes/README.md +7 -9
  60. package/spec/v2/ext/webResearch/README.md +16 -16
  61. package/spec/v2/profiles.json +6 -2
  62. package/spec/v2/release.json +2 -2
  63. package/spec/v2/surface-baseline.json +542 -2
@@ -1,15 +1,15 @@
1
1
  # Form Content Packs
2
2
 
3
- > **Status: Stable · RFC 0177, RFC 0137.**
3
+ > **Status: Stable.**
4
4
  > **Normative home:** `forms`.
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. 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](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". 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"](workflow-chain-packs.md). 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",
@@ -18,19 +18,15 @@ A field MAY carry `when: <EdgeCondition>`. The grammar is the `WorkflowEdge.cond
18
18
 
19
19
  ## Instantiation
20
20
 
21
- A form instantiated from a pack advertised through `forms.contentPacks` MUST be
22
- created through the same path a hand-authored form uses, and MUST remain
23
- editable afterwards: the pack is a starting point, not a managed object. A host
24
- MUST degrade an unrecognized field type to plain text rather than failing the
25
- instantiation, and MUST NOT execute anything carried by the pack. A pack-authored
26
- string, and any value collected through an instantiated template, is untrusted
27
- input: when one is interpolated into a prompt, the composed envelope MUST carry
28
- `meta.contentTrust: "untrusted"`, so a downstream reader can tell authored text
29
- from pack-supplied text.
21
+ A form instantiated from a pack advertised through `forms.contentPacks` MUST be created through the same path a hand-authored form uses, and MUST remain editable afterwards: the pack is a starting point, not a managed object.
22
+
23
+ - A host MUST degrade an unrecognized field type to plain text rather than failing the instantiation.
24
+ - A host MUST NOT execute anything carried by the pack.
25
+ - A pack-authored string, and any value collected through an instantiated template, is untrusted input. When one is interpolated into a prompt, the composed envelope MUST carry `meta.contentTrust: "untrusted"`, so a downstream reader can tell authored text from pack-supplied text.
30
26
 
31
27
  ## Localized strings
32
28
 
33
- `label`, `title`, and `description` are localized strings. A host MUST select the rendered language by the locale-selection and fallback rules of i18n.md, and MUST treat every rendered string as untrusted: escaped for the target surface, never interpreted as markup, script, or a template directive.
29
+ `label`, `title`, and `description` are localized strings. A host MUST select the rendered language by the locale-selection and fallback rules of `spec/v1/i18n.md`, the `i18n` family's normative text in `spec/v2/declaration.json`, and MUST treat every rendered string as untrusted: escaped for the target surface, never interpreted as markup, script, or a template directive.
34
30
 
35
31
  ## Validation
36
32
 
@@ -43,3 +39,5 @@ from pack-supplied text.
43
39
  | `pattern` | `text`, `longtext` | a host MUST reject a non-matching value |
44
40
 
45
41
  The five spec-reserved `format` values are the core set. A host that recognizes a format SHOULD apply it; one that does not MUST ignore it and accept plain text. A host MUST ignore `format` on any type other than `text` or `longtext`.
42
+
43
+ *Sources: RFC 0137, RFC 0177.*
@@ -1,41 +1,49 @@
1
1
  # Headers
2
2
 
3
- > **Status: Stable · RFC 0171 §C.1, RFC 0172 §A.3–§A.4.** GENERATED by `scripts/derive-v2-api.py` from `api/v2/openapi.yaml`; do not edit.
3
+ > **Status: Stable.** Generated from `api/v2/openapi.yaml` by `scripts/derive-v2-api.py`; do not edit.
4
4
 
5
5
  ## Why this exists
6
6
 
7
- RFC 0171 §C.1: every non-standard header is `OpenWOP-<Name>` and every header is declared in OpenAPI, so this table — generated from the same declaration — enumerates all of them. A header that is not in this table is not part of the protocol. Standard headers keep their standard names (`Idempotency-Key`, `ETag`, `If-None-Match`, `Last-Event-ID`, `Retry-After`, `Authorization`).
7
+ Every non-standard header is named `OpenWOP-<Name>`, and every header is declared in OpenAPI. These tables are generated from that declaration, so they list all of them: a header not listed here is not part of the protocol.
8
+
9
+ Standard headers keep their standard names: `Idempotency-Key`, `ETag`, `If-None-Match`, `Last-Event-ID`, `Retry-After`, `Authorization`.
8
10
 
9
11
  ## Request headers
10
12
 
11
13
  | Header | Operations | Meaning |
12
14
  | --- | --- | --- |
13
- | `Accept-Language` | 1 | BCP-47 preference list; authoritative for locale selection (i18n.md). A malformed value MUST NOT 400. |
14
- | `Idempotency-Key` | 15 | Per-mutation idempotency token (see `idempotency.md` Layer 1). |
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). |
15
+ | `Accept-Language` | 1 | BCP-47 preference list; authoritative for locale selection (i18n.md). A malformed value MUST NOT produce a 400. |
16
+ | `Idempotency-Key` | 15 | Per-mutation idempotency token (`idempotency.md` Layer 1). The server caches `(tenantId, endpoint, key)` → response for ≥24h. A duplicate request returns the cached response with `OpenWOP-Idempotent-Replay: true`. |
17
+ | `If-None-Match` | 2 | Conditional request on the discovery document (capabilities.md §1) and the run snapshot (runs.md §Snapshot). A value matching the `ETag` the host sent MUST yield `304 Not Modified` with no body. The 304 carries `OpenWOP-Version` like every response (versioning.md §1.4). |
16
18
  | `Last-Event-ID` | 1 | Resume from sequence after this ID. |
17
- | `OpenWOP-Dedup` | 1 | When set, server cross-host claim system rejects duplicate `(tenantId, scopeId)` pairs with `409 Conflict`. |
18
- | `OpenWOP-Force-Engine-Version` | 1 | **Test-keys-only.** When set, the server emits events for this run AS IF it |
19
- | `OpenWOP-Version` | 55 | RFC 0172 §A.3 — selects a listed major.minor; absent ⇒ the host's `preferredVersion`; unlisted ⇒ 406 protocol_version_unsupported. |
19
+ | `OpenWOP-Dedup` | 1 | When set, the host's cross-host claim system rejects a duplicate `(tenantId, scopeId)` pair with `409 Conflict`. |
20
+ | `OpenWOP-Force-Engine-Version` | 1 | Test keys only. The server emits this run's events as if it ran the given engine version, which must be within `Capabilities.testing.forceEngineVersionRange`. Servers MUST reject it on production API keys with `403 force_engine_version_forbidden`. |
21
+ | `OpenWOP-Version` | 55 | Selects one of the host's listed major.minor versions. Absent, the host uses its `preferredVersion`; an unlisted value is 406 protocol_version_unsupported. |
20
22
 
21
23
  ## Response headers
22
24
 
23
25
  | Header | Operations | Meaning |
24
26
  | --- | --- | --- |
25
- | `Cache-Control` | 2 | |
26
- | `Content-Encoding` | 1 | RFC 0115. Present only when the host negotiated compression |
27
+ | `Cache-Control` | 2 | Standard HTTP caching directive (RFC 9111), set per operation: public content-page delivery, and prompt templates (immutable semantics when the version was pinned); see each operation. |
28
+ | `Content-Encoding` | 1 | Present only when the host negotiated compression from `Accept-Encoding`; pairs with `Vary: Accept-Encoding`. The decoded body is byte-identical to the identity body. |
27
29
  | `Content-Language` | 1 | The BCP-47 locale actually used (equals the response `locale`). |
28
30
  | `ETag` | 3 | Standard HTTP validator (RFC 9110 §8.8.3). The obligation is per operation: MUST on the discovery document (capabilities.md §1), SHOULD on the run snapshot (runs.md §Snapshot), a content hash on a prompt template (getPromptTemplate); see each operation. |
29
31
  | `Location` | 1 | Canonical URI of the new template. |
30
32
  | `OpenWOP-Idempotent-Replay` | 1 | Set when the response was served from the idempotency cache. |
31
- | `OpenWOP-Version` | 55 | RFC 0172 §A.4 — the contract that produced this response; MUST equal the one used. |
33
+ | `OpenWOP-Version` | 55 | The contract that produced this response. It MUST equal the one used. |
32
34
  | `Retry-After` | 1 | Seconds until the active claim is stale-eligible. |
33
- | `WWW-Authenticate` | 48 | 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. |
35
+ | `WWW-Authenticate` | 48 | A `Bearer` challenge. On a host with an oauth2 or oidc lane it carries `resource_metadata` and `error="invalid_token"`. Never sent on a non-disclosure 404. |
34
36
 
35
37
  ## Webhook delivery headers
36
38
 
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).
39
+ Declared in `webhooks.md`, not in OpenAPI, because the host is the client:
40
+
41
+ - `OpenWOP-Webhook-Id`, `OpenWOP-Event-Type`, `OpenWOP-Timestamp`, `OpenWOP-Signature`, `OpenWOP-Signature-Algorithm`.
42
+ - On a subscription that opted into Standard Webhooks (webhooks.md): that standard's `webhook-id`, `webhook-timestamp` and `webhook-signature`, under their standard names.
43
+ - During the v1 overlap only: the `X-openwop-*` family, emitted beside them and removed at v1 end-of-support.
38
44
 
39
45
  ## Removed in v2
40
46
 
41
- `Capabilities-Etag` (the standard `ETag`/`If-None-Match` pair applies to the discovery document), `X-Dedup`, `X-Force-Engine-Version`, `X-Pack-Sha256`, `X-Pack-Signing-Method` (renamed under the one scheme), `X-openwop-*` (webhooks), `openwop-Webhook-Signature` (SDK-only). Each has a `spec/v1/deprecations.json` row with a removal trigger.
47
+ These header names are not part of v2: `Capabilities-Etag` (use the standard `ETag`/`If-None-Match` pair), `X-Dedup`, `X-Force-Engine-Version`, `X-Pack-Sha256`, `X-Pack-Signing-Method`, `X-openwop-*` (webhooks), `openwop-Webhook-Signature`.
48
+
49
+ *Sources: RFC 0165, RFC 0171, RFC 0172, RFC 0201.*
@@ -1,19 +1,27 @@
1
1
  # Host services
2
2
 
3
- > **Status: Stable · v2.0 · RFC 0144.** Normative contract for the advertised `host.*` service surfaces a node pack invokes through `ctx`.
3
+ > **Status: Stable.**
4
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 but states no RFC 2119 obligation; the contract a host takes on by advertising them is written here.
8
+ A node pack invokes advertised `host.*` services through `ctx`. This document states the contract a host takes on by advertising them.
9
9
 
10
10
  ## `aiEnvelope`
11
11
 
12
- A host advertising `aiEnvelope` MUST expose `ctx.aiEnvelope.generate` to pack code, and MUST refuse a node whose `typeId` requires the family when it does not advertise it. It MUST expose `ctx.aiEnvelope.await` when, and only when, it advertises `aiEnvelope.await`.
12
+ A host advertising `aiEnvelope`:
13
+
14
+ - MUST expose `ctx.aiEnvelope.generate` to pack code;
15
+ - MUST refuse a node whose `typeId` requires the family when it does not advertise it;
16
+ - MUST expose `ctx.aiEnvelope.await` when, and only when, it advertises `aiEnvelope.await`.
13
17
 
14
18
  ## `promptLibrary`
15
19
 
16
- A host advertising `promptLibrary` MUST expose `ctx.promptLibrary.get`, MUST return a pinned version verbatim so that a replayed run resolves the same template, and MUST fail the calling node rather than substitute an unpinned one.
20
+ A host advertising `promptLibrary`:
21
+
22
+ - MUST expose `ctx.promptLibrary.get`;
23
+ - MUST return a pinned version verbatim, so that a replayed run resolves the same template;
24
+ - MUST fail the calling node rather than substitute an unpinned one.
17
25
 
18
26
  ## `agentRuntime`
19
27
 
@@ -21,4 +29,13 @@ A host advertising `agentRuntime` MUST expose `spawn`, `delegate`, `consensus` a
21
29
 
22
30
  ## `mcp`
23
31
 
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.
32
+ 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.
33
+
34
+ Each rejects only for an unknown `serverId` (`not_found`), an MCP error response (carried unaltered), or a transport failure.
35
+
36
+ - `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.
37
+ - `listTools` MUST resolve to one `ListToolsResult` page unaltered, `outputSchema`, `annotations`, `nextCursor`, `ttlMs` and `cacheScope` included, and MUST forward a pack's `cursor`.
38
+ - `readResource` resolves to the `ReadResourceResult` unaltered.
39
+ - `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.
40
+
41
+ *Sources: RFC 0144.*
@@ -1,59 +1,71 @@
1
1
  # Idempotency
2
2
 
3
- > **Status: Stable · RFC 0170 §D.3, RFC 0171 §B.2, RFC 0173 §B.**
3
+ > **Status: Stable.**
4
4
  > **Normative home:** `idempotency`.
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; a host that advertises `idempotency` MUST implement Layer 2 for every node executor that performs an external side effect (security-defaults.md).
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](security-defaults.md)).
9
9
 
10
10
  ## Layer 1: `Idempotency-Key`
11
11
 
12
- The header keeps its standard name (RFC 0171 §C.1) and applies to every mutating operation in `api/v2/openapi.yaml`; `GET` operations MUST NOT honor it.
13
-
14
- | Rule | Requirement |
15
- | --- | --- |
16
- | Grammar | The value MUST match `^[A-Za-z0-9._~-]{22,128}$` and MUST carry at least 128 bits of entropy (a UUIDv4 in canonical or base64url form satisfies it). A host MUST reject a value outside the grammar with `400 idempotency_key_invalid`. |
17
- | Record key | A record MUST be keyed by `(authenticatedTenantId, canonicalEndpointId, callerIdempotencyKey)`; the tenant MUST come from the credential, never the body. |
18
- | Final outcomes | A host MUST cache `2xx` and non-retryable `4xx` responses (status, headers, body) and MUST return the cached response to a same-key duplicate. |
19
- | Retryable outcomes | `429` and `5xx` MUST NOT be replayed from cache; a same-key retry MUST re-execute, and a later final outcome replaces the record. |
20
- | Not cached | `400 idempotency_key_invalid`, `400 validation_error`, `401` and `403` MUST NOT be cached. |
21
- | Digest mismatch | A different request digest under the same record key MUST fail with `409 idempotency_key_mismatch` and MUST NOT return the cached body. This is the only mismatch code. |
22
- | Concurrency | Of two concurrent same-key requests a host MUST process exactly one to completion and MUST NOT process both. The other MAY wait, bounded by the host's request timeout, and receive the winner's response only if it is a final outcome (one this record caches), marked `OpenWOP-Idempotent-Replay: true`. Otherwise the host MUST answer `409 idempotency_in_flight` with no retry timing in `details`, and SHOULD set `Retry-After`. The code's registry `retriable: false` means not retryable without waiting (RFC 0213 §B). |
23
- | Replay marker | A response served from cache MUST carry `OpenWOP-Idempotent-Replay: true`. |
24
- | Retention | A record MUST be retained for at least 24 hours. |
25
- | Keyspace | Host-minted identifiers MUST NOT share the caller idempotency store. Logs and spans MUST NOT expose keys. |
12
+ The header keeps its standard name and applies to every mutating operation in `api/v2/openapi.yaml`; `GET` operations MUST NOT honor it.
13
+
14
+ ### Key and record
15
+
16
+ - **Grammar.** The value MUST match `^[A-Za-z0-9._~-]{22,128}$` and MUST carry at least 128 bits of entropy (a UUIDv4 in canonical or base64url form satisfies it). A host MUST reject a value outside the grammar with `400 idempotency_key_invalid`.
17
+ - **Record key.** A record MUST be keyed by `(authenticatedTenantId, canonicalEndpointId, callerIdempotencyKey)`; the tenant MUST come from the credential, never the body.
18
+ - **Retention.** A record MUST be retained for at least 24 hours.
19
+ - **Keyspace.** Host-minted identifiers MUST NOT share the caller idempotency store. Logs and spans MUST NOT expose keys.
20
+
21
+ ### Outcomes
22
+
23
+ - **Final.** A host MUST cache `2xx` and non-retryable `4xx` responses (status, headers, body) and MUST return the cached response to a same-key duplicate.
24
+ - **Retryable.** `429` and `5xx` MUST NOT be replayed from cache; a same-key retry MUST re-execute, and a later final outcome replaces the record.
25
+ - **Not cached.** `400 idempotency_key_invalid`, `400 validation_error`, `401` and `403` MUST NOT be cached.
26
+ - **Digest mismatch.** A different request digest under the same record key MUST fail with `409 idempotency_key_mismatch` and MUST NOT return the cached body. This is the only mismatch code.
27
+ - **Replay marker.** A response served from cache MUST carry `OpenWOP-Idempotent-Replay: true`.
28
+
29
+ ### Concurrency
30
+
31
+ Of two concurrent same-key requests a host MUST process exactly one to completion and MUST NOT process both.
32
+
33
+ - The other MAY wait, bounded by the host's request timeout, and receive the winner's response only if it is a final outcome (one this record caches), marked `OpenWOP-Idempotent-Replay: true`.
34
+ - Otherwise the host MUST answer `409 idempotency_in_flight` with no retry timing in `details`, and SHOULD set `Retry-After`.
35
+
36
+ The code's registry `retriable: false` means not retryable without waiting.
26
37
 
27
38
  ## Layer 2: effect identity
28
39
 
29
- Layer 2 is bound by advertising `idempotency` (security-defaults.md). Its unit is the **effect**, identified once and stable across every transport or provider retry.
40
+ Layer 2 is bound by advertising `idempotency` ([security-defaults.md](security-defaults.md)). Its unit is the **effect**, identified once and stable across every transport or provider retry.
30
41
 
31
- | Rule | Requirement |
32
- | --- | --- |
33
- | Keying | An effect MUST be keyed on its business identity (`keying: business-identity`): derived from the business operation, stable across every entry point, containing no `runId`, `nodeId` or ordinal. The activity recipe (`keying: activity-recipe`: tenant, run, node, ordinal, `providerKey`) is the fallback for a provider with no business key. |
34
- | Attempts | The retry counter MUST NOT participate in the identity. Two distinct logical invocations MUST receive different identities. |
35
- | Claim | The persist that guards the effect MUST be an atomic claim (compare-and-set or insert-if-absent) that at most one executor can win, so at most one concurrent duplicate performs the effect. |
36
- | Provider key | When the provider accepts an idempotency key, the host MUST inject the effect identity (or a documented deterministic derivative), stable across retries. A host that cannot use the provider's convention MUST still persist the outcome. |
37
- | Streaming | A streamed body MUST NOT be cached in the ledger; the host SHOULD record the request and its final outcome. |
38
- | Retention | An effect record MUST be retained for at least 14 days. |
42
+ - **Keying.** An effect MUST be keyed on its business identity (`keying: business-identity`): derived from the business operation, stable across every entry point, containing no `runId`, `nodeId` or ordinal. The activity recipe (`keying: activity-recipe`: tenant, run, node, ordinal, `providerKey`) is the fallback for a provider with no business key.
43
+ - **Attempts.** The retry counter MUST NOT participate in the identity. Two distinct logical invocations MUST receive different identities.
44
+ - **Claim.** The persist that guards the effect MUST be an atomic claim (compare-and-set or insert-if-absent) that at most one executor can win, so at most one concurrent duplicate performs the effect.
45
+ - **Provider key.** When the provider accepts an idempotency key, the host MUST inject the effect identity (or a documented deterministic derivative), stable across retries. A host that cannot use the provider's convention MUST still persist the outcome.
46
+ - **Streaming.** A streamed body MUST NOT be cached in the ledger; the host SHOULD record the request and its final outcome.
47
+ - **Retention.** An effect record MUST be retained for at least 14 days.
39
48
 
40
49
  ### Witness: `GET /runs/{runId}/effects`
41
50
 
42
- A host that advertises `idempotency` MUST serve `schemas/v2/effect-ledger-projection.schema.json` at `GET /runs/{runId}/effects` (`getRunEffects`): `{ runId, effects[] }`, each record carrying `effectId` (tenant-bound, `schemas/v2/ids.schema.json`), `nodeId`, `attempt`, `keying`, `state` (`claimed` | `completed` | `released` | `escaped`), `at`, optional `invocationId` and a redaction-safe `providerKey`. The projection MUST be content-free of provider payloads and credential material.
51
+ A host that advertises `idempotency` MUST serve `schemas/v2/effect-ledger-projection.schema.json` at `GET /runs/{runId}/effects` (`getRunEffects`): `{ runId, effects[] }`. Each record carries:
52
+
53
+ - `effectId` (tenant-bound, `schemas/v2/ids.schema.json`), `nodeId`, `attempt`, `keying`, `at`;
54
+ - `state`: `claimed` | `completed` | `released` | `escaped`;
55
+ - optional `invocationId` and a redaction-safe `providerKey`.
56
+
57
+ The projection MUST be content-free of provider payloads and credential material.
43
58
 
44
59
  ## Composition
45
60
 
46
- Layer 1 deduplicates the caller's request; Layer 2 deduplicates the run's effects. A retried provider call inside a run MUST resolve to the same effect record. Effects under replay and fork are in replay.md; identifier grammars are in identity.md.
61
+ Layer 1 deduplicates the caller's request; Layer 2 deduplicates the run's effects. A retried provider call inside a run MUST resolve to the same effect record. Effects under replay and fork are in [replay.md](replay.md); identifier grammars are in [identity.md](identity.md).
47
62
 
48
63
  ## Multi-region
49
64
 
50
- `multiRegion` and `crossRegion` are the two region facets of `idempotency`, and
51
- they claim different things. `crossRegion` names the host's deployment posture
52
- for this axis and MUST be held constant for the life of an advertisement.
53
- `multiRegion` is the behavioural claim: when a host advertises it, both layers
54
- above MUST hold across regions — an `Idempotency-Key` replayed into a second
55
- region MUST resolve to the first region's response rather than starting new
56
- work, and effect identity MUST collapse a duplicate effect wherever it is
57
- observed. A host that does not advertise `multiRegion` makes no cross-region
58
- promise, and a client MUST NOT infer one from `crossRegion` alone.
65
+ `multiRegion` and `crossRegion` are the two region facets of `idempotency`, and they claim different things.
66
+
67
+ - `crossRegion` names the host's deployment posture for this axis and MUST be held constant for the life of an advertisement.
68
+ - `multiRegion` is the behavioural claim. When a host advertises it, both layers MUST hold across regions: an `Idempotency-Key` replayed into a second region MUST resolve to the first region's response rather than starting new work, and effect identity MUST collapse a duplicate effect wherever it is observed.
69
+ - A host that does not advertise `multiRegion` makes no cross-region promise, and a client MUST NOT infer one from `crossRegion` alone.
59
70
 
71
+ *Sources: RFC 0170, RFC 0171, RFC 0173, RFC 0213.*
@@ -1,17 +1,17 @@
1
1
  # Identity
2
2
 
3
- > **Status: Stable · RFC 0170, 0165, 0176.**
3
+ > **Status: Stable.**
4
4
  > **Normative home:** `auth`, `authorization`.
5
5
 
6
6
  ## Why this exists
7
7
 
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`.
8
+ The Subject owns every run. Every lane binds to a trust root and a revocation rule, the link and every id have a grammar, and tokens carry a prefix so a host can rotate them. Idempotency-key grammar is in [idempotency.md](idempotency.md).
9
9
 
10
- ## 1. The Subject is the owner (RFC 0170 §A)
10
+ ## 1. The Subject is the owner
11
11
 
12
12
  ### 1.1 Shape (`schemas/v2/subject.schema.json`)
13
13
 
14
- `RunSnapshot.owner` is `{ tenant, workspace?, subject }` with `subject` REQUIRED; `principal` and `principalKind` are removed (`subject.subjectId` and `subject.kind` carry them). `run.started` MUST echo the same block (`runs.md`, `events.md`). The Subject is closed (`additionalProperties: false`):
14
+ `RunSnapshot.owner` is `{ tenant, workspace?, subject }` with `subject` REQUIRED. `run.started` MUST echo the same block ([runs.md](runs.md), [events.md](events.md)). The Subject is closed (`additionalProperties: false`):
15
15
 
16
16
  | Field | Rule |
17
17
  | --- | --- |
@@ -23,27 +23,37 @@ v2 makes the Subject the owner of every run, binds every lane to a trust root an
23
23
  | `keyClass` | `opaque-idp \| configured-immutable`; MUST be present iff `lane ∈ {saml, scim}` |
24
24
  | `actor` | OPTIONAL; a nested Subject that acts on this subject's behalf; depth bounded at four |
25
25
 
26
- `kind: anonymous` REQUIRES `lane: anonymous` and `lane: anonymous` REQUIRES `kind: anonymous`. The `actor` depth bound (4) is a four-level `$ref` chain (`actor1`…`actor4`) rather than a recursive `$ref`; a fifth level MUST fail validation. `session` is a host-native credential the host itself issued (a durable login session, a local password); `anonymous` is an RFC 0132 public surface. The lane enum grows only under `overview.md` §0.
26
+ - `kind: anonymous` REQUIRES `lane: anonymous`, and `lane: anonymous` REQUIRES `kind: anonymous`.
27
+ - The `actor` depth bound (4) is a four-level `$ref` chain (`actor1`…`actor4`), not a recursive `$ref`. A fifth level MUST fail validation.
28
+ - `session` is a host-native credential the host itself issued (a durable login session, a local password). `anonymous` is a public surface.
29
+ - The lane enum grows only under [overview.md](overview.md) §0.
27
30
 
28
- ### 1.2 The legacy subject rule (RFC 0170 §A.3)
31
+ ### 1.2 The legacy subject rule
29
32
 
30
- On every read of a run created before the host began emitting subjects, the host MUST stamp `issuer: "urn:openwop:legacy"`, `lane` as attested else `api-key`, `kind` as recorded else `user`. A host MUST stamp the legacy subject at first read and MUST NOT rewrite it later. A legacy subject MUST NOT participate in a link (§3), an actor chain, or a delegation decision.
33
+ A run created before the host began emitting subjects has a legacy subject:
31
34
 
32
- ### 1.3 Fork (RFC 0170 §A.4)
35
+ - On every read, the host MUST stamp `issuer: "urn:openwop:legacy"`, `lane` as attested else `api-key`, and `kind` as recorded else `user`.
36
+ - A host MUST stamp the legacy subject at first read and MUST NOT rewrite it later.
37
+ - A legacy subject MUST NOT participate in a link (§3), an actor chain, or a delegation decision.
33
38
 
34
- On fork the host MUST copy `owner` verbatim onto the child: `tenant`, `workspace`, and `subject`. There is no `principal` asymmetry.
39
+ ### 1.3 Fork
35
40
 
36
- ### 1.4 A2A anonymous end users (RFC 0170 §A.5)
41
+ On fork the host MUST copy `owner` verbatim onto the child: `tenant`, `workspace` and `subject`.
37
42
 
38
- An end user reaching the host through an A2A peer is `kind: anonymous`, `lane: anonymous`, with the forwarding peer's subject as `actor`; such a subject MUST NOT be linked.
43
+ ### 1.4 A2A anonymous end users
39
44
 
40
- ## 2. One binding pipeline, every lane (RFC 0170 §B)
45
+ An end user reaching the host through an A2A peer is `kind: anonymous`, `lane: anonymous`, with the forwarding peer's subject as `actor`. Such a subject MUST NOT be linked.
41
46
 
42
- ### 2.1 The pipeline (§B.1)
47
+ ## 2. One binding pipeline, every lane
43
48
 
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`):
49
+ ### 2.1 The pipeline
45
50
 
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.
51
+ 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.
52
+
53
+ - 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`.
54
+ - The closed reason vocabulary is the family-wide error set in §6.
55
+
56
+ Every lane is advertised as one member of the `auth.lanes[]` facet (`spec/v2/facets/auth.schema.json`):
47
57
 
48
58
  ```json
49
59
  { "lane": "oidc", "issuers": ["https://idp.example"], "revocation": "exp-and-recheck",
@@ -51,9 +61,11 @@ Every lane MUST: verify the credential against the lane's trust root; bind the v
51
61
  "delegationProofs": ["dpop"] }
52
62
  ```
53
63
 
54
- `lane`, `issuers[]` (min 1), `revocation`, and `minimumAssurance` are REQUIRED on each member. `auth.lanes[].issuers[]` is the realm; the v1 `auth.profiles` facet is replaced by it (`capabilities.md`).
64
+ `lane`, `issuers[]` (min 1), `revocation` and `minimumAssurance` are REQUIRED on each member. `auth.lanes[].issuers[]` is the realm ([capabilities.md](capabilities.md)).
65
+
66
+ `authorization.failClosed` advertises the fail-closed 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.
55
67
 
56
- ### 2.2 Trust roots and revocation (§B.2, §B.3)
68
+ ### 2.2 Trust roots and revocation
57
69
 
58
70
  Every lane MUST name its trust root as `subject.issuer` and MUST advertise it in `issuers[]`. Revocation exists for every lane; the `revocation` value names the rule.
59
71
 
@@ -70,27 +82,54 @@ Every lane MUST name its trust root as `subject.issuer` and MUST advertise it in
70
82
  | `session` | `urn:<host>:session` | refuse a revoked session on the next request (`credential_revoked`) | `next-request` |
71
83
  | `anonymous` | `urn:<host>:anon-surface` | — | — |
72
84
 
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.
85
+ - `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. A host MUST NOT advertise a window it does not enforce.
86
+ - A host MUST NOT advertise a `revocation` value the row above for its lane does not list.
87
+ - 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](overview.md) §0).
88
+
89
+ #### `exp-only`
90
+
91
+ `exp-only` names a host that honors `exp` and never re-checks revocation: it consults no introspection endpoint, no userinfo endpoint, no revocation list and no host-side epoch or `validAfter` record. A credential revoked at the trust root is accepted until its own `exp`, so the window is the only bound, and it is enforced.
92
+
93
+ - A host advertising `exp-only` on a lane MUST refuse a credential presented on that lane with `401 credential_lifetime_exceeded` when **either** `exp − iat` (total lifetime) **or** `exp − now` (remaining lifetime) exceeds the advertised `revocationWindowSeconds`. Both bounds matter: `exp − iat` alone admits a ten-year token minted ten years ago; `exp − now` alone admits a fresh ten-year token in its ninth year.
94
+ - 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).
95
+ - A host that cannot enforce both bounds MUST NOT advertise `exp-only`.
96
+ - `exp-only` SHOULD be advertised with a window of one hour or less. No maximum is set.
97
+ - `exp-only` MUST NOT be advertised on the `api-key` or `session` lane, where the host issued the credential and controls revocation itself. `auth.schema.json` refuses that pairing (invariant `lane-exp-only-lifetime-bounded`).
98
+
99
+ ### 2.3 Minimum assurance
100
+
101
+ - Each lane MUST advertise `minimumAssurance: bearer | sender-constrained | key-bound`.
102
+ - A request below the lane's floor MUST be refused with `sender_constraint_missing`.
103
+ - An audit fact MUST record the assurance actually used.
104
+ - A bearer fallback MUST NOT inherit a sender-constrained label (invariant `sender-constraint-no-bearer-downgrade`, `SECURITY/invariants.yaml`).
105
+
106
+ ### 2.4 Delegation proofs
74
107
 
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).
108
+ The proof format is lane-scoped: mTLS key binding or DPoP for the two JWT lanes (`oauth2`, `oidc`), SVID chains for `workload`.
76
109
 
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`).
110
+ - A host MUST advertise the proofs it accepts under `auth.lanes[].delegationProofs[]` (`mtls-key-binding | dpop | svid-chain`).
111
+ - A chain with no acceptable proof MUST be refused as `identity_unverified`.
112
+ - 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`).
78
113
 
79
- ### 2.3 Minimum assurance (§B.4)
114
+ ### 2.5 Protected-resource metadata and challenges
80
115
 
81
- Each lane MUST advertise `minimumAssurance: bearer | sender-constrained | key-bound`. A request below the lane's floor MUST be refused with `sender_constraint_missing`. An audit fact MUST record the assurance actually used. A bearer fallback MUST NOT inherit a sender-constrained label (invariant `sender-constraint-no-bearer-downgrade`, `SECURITY/invariants.yaml`).
116
+ A host advertising an `oauth2` or `oidc` lane MUST serve RFC 9728 metadata, unauthenticated, 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`.
82
117
 
83
- ### 2.4 Delegation proofs (§B.5)
118
+ - `resource` MUST equal that identifier.
119
+ - `authorization_servers` MUST list exactly the URL-form issuers of those lanes.
120
+ - `scopes_supported` MUST list the scopes the host enforces.
121
+ - `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).
84
122
 
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`).
123
+ Challenges on such a host:
86
124
 
87
- ### 2.5 Protected-resource metadata and challenges (RFC 0200)
125
+ - 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.
126
+ - 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`.
88
127
 
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).
128
+ 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`).
90
129
 
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`).
130
+ ## 3. The link is a record
92
131
 
93
- ## 3. The link is a record (RFC 0170 §C; `schemas/v2/subject-link.schema.json`)
132
+ A subject link is a `schemas/v2/subject-link.schema.json` record:
94
133
 
95
134
  ```json
96
135
  { "a": { "issuer": "…", "subjectId": "…" }, "b": { "issuer": "…", "subjectId": "…" },
@@ -98,19 +137,31 @@ On such a host a `401` MUST carry `WWW-Authenticate: Bearer resource_metadata="<
98
137
  "tenant": "<tenantId>", "formedAt": "<date-time>", "deniedAt"?: "<date-time>" }
99
138
  ```
100
139
 
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.
140
+ The record and both `SubjectRef`s are closed; `a`, `b`, `keyClass`, `issuer`, `tenant`, `formedAt` are REQUIRED. A link:
102
141
 
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.
142
+ - MUST be tenant-scoped;
143
+ - MUST join exactly two subjects whose `issuer` values are bound to one IdP entityID (`issuer` on the record);
144
+ - MUST NOT include a legacy (`urn:openwop:legacy` is schema-rejected) or anonymous subject.
104
145
 
105
- ## 4. Resume tokens (RFC 0170 §E.1; RFC 0176 §B.2)
146
+ 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.
106
147
 
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`).
148
+ Advertising both `saml` and `scim` lanes implies the link contract; there is no `auth.subjectLinking` flag. Lanes stay separate facets, with no single "enterprise identity" profile. The `auth.subjectLinkKey` facet (`opaque-idp | configured-immutable`) names the key class the host forms links under.
108
149
 
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`.
150
+ ## 4. Resume tokens
151
+
152
+ An interrupt resume token is `ow2.<alg>.<kid>.<payload>.<mac>`:
153
+
154
+ - `alg ∈ {hs256}`, advertised in `interrupt.tokenAlgs[]`;
155
+ - `kid` (`keyId` grammar) selects the verification secret;
156
+ - `payload` and `mac` are as in v1.
157
+
158
+ 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`). Interrupt semantics are in [interrupt.md](interrupt.md).
159
+
160
+ A token not `ow2.`-prefixed was issued under v1; the rule is the prefix, never a segment count. Such a token MUST remain resolvable under `kid: legacy` until its `expiresAt`. A run suspended on an interrupt at the cut continues under [persistence.md](persistence.md), and its outstanding token resolves the same way.
110
161
 
111
162
  ## 5. Identifier grammars (`schemas/v2/ids.schema.json`)
112
163
 
113
- Every id field in every v2 schema and every `api/v2/openapi.yaml` parameter and response body MUST `$ref` its kind. `x-openwop-minted` records who mints the id: `host` (opaque and checkable), `author` (chosen in a workflow or pack; the v1 grammar stands), or `registry`.
164
+ Every id field in every v2 schema and every `api/v2/openapi.yaml` parameter and response body MUST `$ref` its kind. `x-openwop-minted` records who mints the id: `host` (opaque and checkable), `author` (chosen in a workflow or pack), or `registry`.
114
165
 
115
166
  | Kinds | Grammar | Minted |
116
167
  | --- | --- | --- |
@@ -123,17 +174,32 @@ Every id field in every v2 schema and every `api/v2/openapi.yaml` parameter and
123
174
  | `nodeId`, `workflowId`, `agentId`, `chainId`, `pluginId`, `templateId`, `libraryId` | `^[A-Za-z0-9._~:-]{1,128}$` | author |
124
175
  | `typeId` | `^[a-z][a-z0-9_-]*(\.[a-z][a-zA-Z0-9_-]*)+$`, maxLength 256 | author |
125
176
 
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.
177
+ `spec/v2/id-field-bindings.json` 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).
178
+
179
+ - A host MUST reject a tenant-bound id whose tenant segment is not the caller's with `403` `id_tenant_mismatch`.
180
+ - A host-minted opaque segment MUST match `^[A-Za-z0-9._~-]{16,128}$`.
181
+ - Ids in documents and bodies are bound, always. A client MAY bind at its request seam.
182
+ - 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 ([capabilities.md § portability](capabilities.md)).
127
183
 
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}$`.
184
+ ### Wire form
129
185
 
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.
186
+ On the wire a tenant-bound id is one path segment, projected: every UTF-8 byte outside `[A-Za-z0-9._-]` becomes `~` plus two uppercase hex digits, so `acme/r-9f3c…` travels as `acme~2Fr-9f3c…`.
131
187
 
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/`).
188
+ - A host MUST emit the projected form in every link and MUST accept it on every tenant-bound parameter.
189
+ - It MUST still accept `tenant%2Fopaque`, and MUST decode either form before matching the grammar.
190
+ - A host MUST NOT mint a tenant-bound id containing `~`; ids already minted MUST still resolve.
191
+ - A host MUST project exactly once, where an id leaves it, and MUST NOT re-encode its own output.
192
+
193
+ ### Bare ids during the v1 overlap
194
+
195
+ Through the overlap the bare form is admitted on a major-2 path parameter.
196
+
197
+ - 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](versioning.md) §5). The credential supplies the segment the `403` check would read.
198
+ - Once a host advertises no `1.x` member it MUST refuse the bare form `400 validation_error` (not `id_tenant_mismatch`, not `not_found`).
133
199
 
134
200
  ## 6. Identity error codes (`spec/v2/errors.json`)
135
201
 
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).
202
+ Every code below is a row with `retriable: false` and no `details` contract; the envelope is in [errors.md](errors.md).
137
203
 
138
204
  | Code | HTTP | Raised when |
139
205
  | --- | --- | --- |
@@ -150,8 +216,10 @@ Every code below is a row with `retriable: false` and no `details` contract; the
150
216
  | `id_tenant_mismatch` | 403 | a tenant-bound id's tenant segment is not the caller's (§5) |
151
217
  | `interrupt_token_invalid` | 401 | an unadvertised `alg` or an unheld `kid` (§4) |
152
218
 
153
- `unauthenticated` and `run_forbidden` keep their v1 rows. Every code is a registry member under `overview.md` §0.
219
+ `unauthenticated` and `run_forbidden` also apply. Every code is a registry member under [overview.md](overview.md) §0.
220
+
221
+ ## 7. Invariants
154
222
 
155
- ## 7. Invariants (RFC 0170 §E.2)
223
+ The identity invariants are registered in `SECURITY/invariants.yaml` with their scenarios. An invariant without a witness is demoted from `protocol` tier.
156
224
 
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.
225
+ *Sources: RFC 0132, RFC 0165, RFC 0170, RFC 0176, RFC 0184, RFC 0200, RFC 0210.*