@openwop/spec-artifacts 2.42.6 → 2.42.8
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.
- package/CORPUS-STAMP.json +70 -67
- package/api/seams-v2.yaml +238 -322
- package/api/v2/asyncapi.yaml +83 -113
- package/api/v2/openapi.yaml +408 -724
- package/package.json +1 -1
- package/schemas/audit-verify-result.schema.json +2 -2
- package/schemas/v2/audit-verify-result.schema.json +2 -2
- package/schemas/v2/capabilities.schema.json +4 -7
- package/schemas/v2/error-envelope.schema.json +144 -69
- package/spec/v1/alias-detectors.json +6 -0
- package/spec/v1/core-standard-manifest.json +2 -2
- package/spec/v1/deprecations.json +22 -0
- package/spec/v1/gaps.json +188 -1
- package/spec/v1/migrations.json +18 -0
- package/spec/v2/README.md +35 -37
- package/spec/v2/core/capabilities.md +186 -119
- package/spec/v2/core/conformance.md +78 -27
- package/spec/v2/core/connection-packs.md +8 -3
- package/spec/v2/core/conversation.md +10 -4
- package/spec/v2/core/errors.md +30 -9
- package/spec/v2/core/events.md +116 -80
- package/spec/v2/core/form-content-packs.md +11 -13
- package/spec/v2/core/headers.md +23 -14
- package/spec/v2/core/host-services.md +22 -5
- package/spec/v2/core/i18n.md +64 -0
- package/spec/v2/core/idempotency.md +48 -36
- package/spec/v2/core/identity.md +110 -42
- package/spec/v2/core/interop.md +82 -21
- package/spec/v2/core/interrupt.md +79 -40
- package/spec/v2/core/node-pack-runtimes.md +18 -4
- package/spec/v2/core/oauth.md +40 -6
- package/spec/v2/core/overview.md +61 -33
- package/spec/v2/core/packs.md +53 -28
- package/spec/v2/core/persistence.md +61 -80
- package/spec/v2/core/portability.md +44 -0
- package/spec/v2/core/replay.md +56 -42
- package/spec/v2/core/runs.md +198 -60
- package/spec/v2/core/security-defaults.md +57 -33
- package/spec/v2/core/tool-catalog.md +29 -6
- package/spec/v2/core/versioning.md +115 -50
- package/spec/v2/core/webhooks.md +61 -20
- package/spec/v2/core/workflow-chain-packs.md +16 -14
- package/spec/v2/corrections.json +17 -1
- package/spec/v2/corrections.schema.json +1 -1
- package/spec/v2/declaration.json +26 -9
- package/spec/v2/declaration.schema.json +10 -0
- package/spec/v2/errors.json +121 -4
- package/spec/v2/errors.schema.json +38 -0
- package/spec/v2/ext/README.md +36 -23
- package/spec/v2/ext/a2uiSurface/README.md +75 -89
- package/spec/v2/ext/brand/README.md +15 -15
- package/spec/v2/ext/canvas/README.md +15 -15
- package/spec/v2/ext/chat/README.md +15 -15
- package/spec/v2/ext/coordination/README.md +15 -15
- package/spec/v2/ext/dataIntegration/README.md +17 -20
- package/spec/v2/ext/entities/README.md +15 -15
- package/spec/v2/ext/grpc-transport/README.md +6 -8
- package/spec/v2/ext/kanban/README.md +15 -15
- package/spec/v2/ext/knowledge/README.md +15 -15
- package/spec/v2/ext/launchStudio/README.md +16 -16
- package/spec/v2/ext/messaging/README.md +15 -15
- package/spec/v2/ext/portability/README.md +3 -17
- package/spec/v2/ext/provider-idempotency/README.md +7 -10
- package/spec/v2/ext/restTransport/README.md +30 -19
- package/spec/v2/ext/sandbox-runtime-notes/README.md +7 -9
- package/spec/v2/ext/webResearch/README.md +16 -16
- package/spec/v2/profiles.json +6 -2
- package/spec/v2/release.json +2 -2
- package/spec/v2/surface-baseline.json +544 -2
package/spec/v2/core/identity.md
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# Identity
|
|
2
2
|
|
|
3
|
-
> **Status: Stable
|
|
3
|
+
> **Status: Stable.**
|
|
4
4
|
> **Normative home:** `auth`, `authorization`.
|
|
5
5
|
|
|
6
6
|
## Why this exists
|
|
7
7
|
|
|
8
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
31
|
+
### 1.2 The legacy subject rule
|
|
29
32
|
|
|
30
|
-
|
|
33
|
+
A run created before the host began emitting subjects has a legacy subject:
|
|
31
34
|
|
|
32
|
-
|
|
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
|
-
|
|
39
|
+
### 1.3 Fork
|
|
35
40
|
|
|
36
|
-
|
|
41
|
+
On fork the host MUST copy `owner` verbatim onto the child: `tenant`, `workspace` and `subject`.
|
|
37
42
|
|
|
38
|
-
|
|
43
|
+
### 1.4 A2A anonymous end users
|
|
39
44
|
|
|
40
|
-
|
|
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
|
-
|
|
47
|
+
## 2. One binding pipeline, every lane
|
|
43
48
|
|
|
44
|
-
|
|
49
|
+
### 2.1 The pipeline
|
|
45
50
|
|
|
46
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
114
|
+
### 2.5 Protected-resource metadata and challenges
|
|
80
115
|
|
|
81
|
-
|
|
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
|
-
|
|
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
|
-
|
|
123
|
+
Challenges on such a host:
|
|
86
124
|
|
|
87
|
-
|
|
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
|
-
|
|
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
|
-
|
|
130
|
+
## 3. The link is a record
|
|
92
131
|
|
|
93
|
-
|
|
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
|
|
140
|
+
The record and both `SubjectRef`s are closed; `a`, `b`, `keyClass`, `issuer`, `tenant`, `formedAt` are REQUIRED. A link:
|
|
102
141
|
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
`
|
|
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 ([portability.md](portability.md)).
|
|
127
183
|
|
|
128
|
-
|
|
184
|
+
### Wire form
|
|
129
185
|
|
|
130
|
-
|
|
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
|
-
|
|
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
|
|
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`
|
|
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
|
-
|
|
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
|
-
|
|
225
|
+
*Sources: RFC 0132, RFC 0165, RFC 0170, RFC 0176, RFC 0184, RFC 0200, RFC 0210.*
|
package/spec/v2/core/interop.md
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
# Interop
|
|
2
2
|
|
|
3
|
-
> **Status: Stable
|
|
3
|
+
> **Status: Stable.**
|
|
4
4
|
> **Normative home:** `a2a`, `mcp`.
|
|
5
5
|
|
|
6
6
|
## Why this exists
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
This document covers the two embedded protocols, A2A and MCP: how a host advertises them, how a version is negotiated, and what every negotiation leaves behind. Capability shapes are in [capabilities.md](capabilities.md); the peer identity is the Subject of [identity.md](identity.md).
|
|
9
9
|
|
|
10
10
|
## REST is the wire
|
|
11
11
|
|
|
12
|
-
REST and SSE are the wire.
|
|
12
|
+
REST and SSE are the wire. A2A and MCP are compositions over it, advertised by their own facets and nothing else.
|
|
13
|
+
|
|
14
|
+
- A host MUST NOT advertise a transport list.
|
|
15
|
+
- A discovery document carrying `supportedTransports` MUST fail validation against `schemas/v2/capabilities.schema.json`.
|
|
13
16
|
|
|
14
17
|
## The facets
|
|
15
18
|
|
|
@@ -21,14 +24,16 @@ A host that speaks either protocol MUST advertise the corresponding facet — `a
|
|
|
21
24
|
| Default | `preferredVersion` | `preferredVersion` | REQUIRED; served when the peer names none |
|
|
22
25
|
| Floor | `minimumVersion` | `minimumRevision` | REQUIRED; below it negotiation fails closed |
|
|
23
26
|
| Freshness | `refreshedAt` | `refreshedAt` | REQUIRED; see the refresh SLA |
|
|
24
|
-
| Profiles | `profiles[]` `a2a-<major.minor>` | `profiles[]` `mcp-<date>` | no `-legacy`
|
|
27
|
+
| Profiles | `profiles[]` `a2a-<major.minor>` | `profiles[]` `mcp-<date>` | no `-legacy` suffix |
|
|
25
28
|
| Protocol-specific | `agentCardUrl`, `streaming`, `pushNotifications`, `durableTasks` | `features[]`, `serverUrls[]`, `serverMount.transports[]` (`stdio` \| `streamable-http`), `mrtr.maxRounds` | optional |
|
|
26
29
|
|
|
27
30
|
`mcp.serverMount.transports[]` is the MCP server's own transport enum; it is not a host transport advertisement.
|
|
28
31
|
|
|
29
32
|
## Legacy profiles are absent
|
|
30
33
|
|
|
31
|
-
The
|
|
34
|
+
The `profiles[]` item patterns admit no `-legacy` suffix, so `a2a-0.3-legacy` and `mcp-2025-06-18-legacy` are not valid profile ids. A host that still speaks a legacy version does so as a private, non-advertised behavior.
|
|
35
|
+
|
|
36
|
+
When no `A2A-Version` header is present, a host MUST serve the agent card of `preferredVersion`.
|
|
32
37
|
|
|
33
38
|
## Negotiation is a protocol
|
|
34
39
|
|
|
@@ -43,23 +48,45 @@ The profile ids `a2a-0.3-legacy` and `mcp-2025-06-18-legacy` do not exist in v2.
|
|
|
43
48
|
"negotiated": "…" | null, "outcome": "accepted" | "downgraded" | "refused", "reason": "…" }
|
|
44
49
|
```
|
|
45
50
|
|
|
46
|
-
The event is content-free: `peer` MUST be a digest of the peer origin, never the origin in clear. The event
|
|
51
|
+
The event is content-free: `peer` MUST be a digest of the peer origin, never the origin in clear. The event is the normative witness of the invariants `a2a-version-no-silent-downgrade` and `mcp-version-no-silent-downgrade`; the seams profile ([conformance.md](conformance.md)) drives the exchange and captures the wire leg.
|
|
47
52
|
|
|
48
53
|
**The refresh SLA.** A host MUST re-evaluate its advertised `versions[]` / `revisions[]` against the upstream registry within the window its `refreshedAt` declares, and that window MUST NOT exceed 90 days. An advertisement older than its window is non-conformant.
|
|
49
54
|
|
|
50
55
|
**Downgrade above the floor.** A host MAY accept an authenticated request for a version between the floor and `preferredVersion`; the event then reports `outcome: downgraded`.
|
|
51
56
|
|
|
52
|
-
## The operation mappings
|
|
57
|
+
## The operation mappings
|
|
58
|
+
|
|
59
|
+
`spec/v2/interop-map.json` (schema `interop-map.schema.json`) maps each profile's upstream operations, states, fields and errors to the v2 wire, pinned to an upstream release. A host advertising a profile:
|
|
60
|
+
|
|
61
|
+
- MUST serve every row it implements as the row states, under the caller's Subject with the authorization, tenant scoping and state of the v2 operation the row names;
|
|
62
|
+
- MUST refuse a row whose `requires` facet it does not advertise with the row's error;
|
|
63
|
+
- MUST list every feature the map requires for that profile.
|
|
64
|
+
|
|
65
|
+
What the map does not name is opaque: it MUST round-trip where upstream requires it and MUST NOT become authority, a prompt segment, a tool call or a workflow variable. A patch release that re-maps a row is a map edit; patch numbers are never negotiated.
|
|
53
66
|
|
|
54
|
-
|
|
67
|
+
**Isolation.** On either interface:
|
|
55
68
|
|
|
56
|
-
|
|
69
|
+
- A task the caller could not read through `getRun` MUST be answered exactly as a nonexistent one, including a tenant mismatch REST refuses `403`.
|
|
70
|
+
- `ListTasks` MUST return only runs `listRuns` would return to the same Subject, whether or not `runList` is advertised.
|
|
71
|
+
- `contextId`, `tenant` and `_meta` never select a tenant, workspace or principal.
|
|
57
72
|
|
|
58
|
-
**A2A multi-turn (A2A §3.4.3).**
|
|
73
|
+
**A2A multi-turn (A2A §3.4.3).**
|
|
59
74
|
|
|
60
|
-
|
|
75
|
+
- A message carrying `taskId` without `contextId` MUST be answered with the task's `contextId`.
|
|
76
|
+
- A message whose `contextId` is not its task's MUST be refused with its binding's invalid-parameters error and MUST NOT change the run.
|
|
77
|
+
- A message to a retained terminal task MUST be refused `UnsupportedOperationError`; `TaskNotFoundError` is for unknown, purged and unreadable tasks.
|
|
61
78
|
|
|
62
|
-
|
|
79
|
+
**A2A error details.** A host:
|
|
80
|
+
|
|
81
|
+
- on a 1.0 `JSONRPC` interface, MUST make an A2A error's `error.data` an array of objects each carrying `@type`, including exactly one `type.googleapis.com/google.rpc.ErrorInfo` whose `reason` is the map row's `reason` and whose `domain` is `a2a-protocol.org` (a tightening of A2A §9.5's SHOULD);
|
|
82
|
+
- on an `HTTP+JSON` interface, MUST answer A2A §11.6's `google.rpc.Status`;
|
|
83
|
+
- MUST NOT answer with the OpenWOP error envelope on an interface URL the card lists, including a refusal before dispatch;
|
|
84
|
+
- MUST NOT vary `TaskNotFoundError` details between an unknown task and an unreadable one, apart from an echo of the requested id;
|
|
85
|
+
- on `VersionNotSupportedError`, SHOULD set `metadata.supportedVersions` to a comma-joined list; a client falls back to the card's `supportedInterfaces[].protocolVersion`.
|
|
86
|
+
|
|
87
|
+
A client MUST identify an error by code or by the ErrorInfo `reason`, MUST accept `data` as an array, and SHOULD accept a `data` object carrying `reason` through 2.x.
|
|
88
|
+
|
|
89
|
+
## MCP tasks and cancellation
|
|
63
90
|
|
|
64
91
|
A host MAY serve the MCP Tasks extension `io.modelcontextprotocol/tasks` (revision `2026-07-28`) on its server mount. It advertises it in its `server/discover` `capabilities.extensions` and by listing `extensions` in `mcp.features[]`, and nowhere else. A host that advertises it MUST implement the extension as published and the map's `mcp.tasks` rows, and:
|
|
65
92
|
|
|
@@ -67,7 +94,11 @@ A host MAY serve the MCP Tasks extension `io.modelcontextprotocol/tasks` (revisi
|
|
|
67
94
|
- MUST use the run's projected `runId` (identity.md §5) as `taskId`, with an opaque segment of at least 128 bits of entropy. A `taskId` is never a credential;
|
|
68
95
|
- MUST NOT append to a run's log to answer `tasks/get`.
|
|
69
96
|
|
|
70
|
-
**Cancellation.** Until the host has sent its whole response to a request that starts or continues a run, the run belongs to that request
|
|
97
|
+
**Cancellation.** Until the host has sent its whole response to a request that starts or continues a run, the run belongs to that request.
|
|
98
|
+
|
|
99
|
+
- Before the response is sent, a client disconnect on streamable HTTP, or a stdio `notifications/cancelled` naming the request, MUST cancel the run as `cancelRun` would, with `run.cancelled.reason` `mcp-request-cancelled`.
|
|
100
|
+
- Once the response is sent, a disconnect MUST NOT affect the run; a task ends through `tasks/cancel`, `cancelRun`, or its own terminal state.
|
|
101
|
+
- A host MUST NOT send `notifications/cancelled` except to end a `subscriptions/listen` stream.
|
|
71
102
|
|
|
72
103
|
## The MCP round ceiling
|
|
73
104
|
|
|
@@ -77,28 +108,58 @@ A host MAY serve the MCP Tasks extension `io.modelcontextprotocol/tasks` (revisi
|
|
|
77
108
|
|
|
78
109
|
`auth-required` remains a member of the persisted A2A task state enum (`schemas/v2/a2a-task-state.schema.json`) for the reverse direction (consuming an external A2A agent). The forward projection MUST emit it, with `interruptKind: credential` and a status message carrying `connectUrl`, for a run suspended on a `credential` interrupt (interrupt.md), and MUST NOT emit it otherwise.
|
|
79
110
|
|
|
80
|
-
## A2A push delivery
|
|
111
|
+
## A2A push delivery
|
|
81
112
|
|
|
82
|
-
A host advertising `a2a.pushNotifications` treats each push as a webhook egress: webhooks.md §Egress binds at delivery time as well as registration, a `3xx` is a failed delivery, and the push credential is bound as security-defaults.md §"Onward hops" states.
|
|
113
|
+
A host advertising `a2a.pushNotifications` treats each push as a webhook egress: webhooks.md §Egress binds at delivery time as well as registration, a `3xx` is a failed delivery, and the push credential is bound as security-defaults.md §"Onward hops" states.
|
|
83
114
|
|
|
84
|
-
A
|
|
115
|
+
- A host MUST attempt each push at least once; a host that retries follows `webhooks.md` `retryPolicy` semantics.
|
|
116
|
+
- The body is an A2A 1.0 `StreamResponse` sent as `application/a2a+json`, carrying `Authorization: {scheme} {credentials}` from the config. When only `token` is set, a host SHOULD send it as `Authorization: Bearer <token>` (A2A v1.0.1 leaves its carriage undefined).
|
|
117
|
+
- A host MUST NOT add an OpenWOP signature.
|
|
118
|
+
- Push dead-letters are not visible to A2A clients; a client recovers with `GetTask`.
|
|
119
|
+
- A `replay` fork MUST NOT push re-emitted history, and no fork inherits a source run's push configs.
|
|
120
|
+
- A push-config read or delete on a task the caller cannot read, or naming a `configId` that is not that task's, MUST answer exactly as for an unknown id, apart from the JSON-RPC `id`.
|
|
121
|
+
- A `configId` MUST NOT encode a tenant, workspace or principal. Delete is idempotent.
|
|
85
122
|
|
|
86
123
|
## Per-agent cards
|
|
87
124
|
|
|
88
125
|
A host advertising `a2a.agentCards` MUST also offer the `a2a-1.0` profile and `agents.manifestRuntime`, and MUST declare `capabilities.extendedAgentCard: true` on its public card. It publishes each entry of a caller's agent inventory (`GET /agents`) as an A2A `AgentCard`, reached through the entry's `a2aTenant`: an opaque routing value `R` the host mints, stable for the agent and host version, that MUST NOT encode a tenant, workspace, or principal.
|
|
89
126
|
|
|
90
|
-
`GetExtendedAgentCard` with `tenant: R` MUST return that agent's card:
|
|
127
|
+
`GetExtendedAgentCard` with `tenant: R` MUST return that agent's card:
|
|
91
128
|
|
|
92
|
-
|
|
129
|
+
- `name` is the entry's `persona`, `version` its `packVersion`, `description` its `description` or else `label`;
|
|
130
|
+
- `supportedInterfaces[]` are the host card's interfaces, each carrying `tenant: R`;
|
|
131
|
+
- `capabilities` and `securitySchemes` equal the host card's;
|
|
132
|
+
- `skills[]` holds one skill per workflow the host routes to the agent for this caller.
|
|
133
|
+
|
|
134
|
+
The card MUST NOT carry anything the inventory entry may not, and does not replace it: `degraded[]` and `memoryDegraded` stay on the entry.
|
|
135
|
+
|
|
136
|
+
**Non-disclosure.**
|
|
137
|
+
|
|
138
|
+
- A request carrying `R` MUST be authenticated and authorized as `GET /agents/{agentId}` is, before `R` is resolved.
|
|
139
|
+
- For an `R` naming an agent outside the caller's inventory, every A2A operation MUST return what it returns for an `R` the host never minted, apart from the JSON-RPC `id`.
|
|
140
|
+
- The public card at `agentCardUrl` MUST NOT list any `R`.
|
|
141
|
+
- `R` is a `tenant` value under §"The operation mappings" **Isolation**.
|
|
93
142
|
|
|
94
143
|
## gRPC
|
|
95
144
|
|
|
96
|
-
gRPC is not part of the core wire. Its document lives at `spec/v2/ext/grpc-transport/` with `witness: unwitnessable` and `adoption: none`; its requirements are SHOULDs of that extension.
|
|
145
|
+
gRPC is not part of the core wire. Its document lives at `spec/v2/ext/grpc-transport/` with `witness: unwitnessable` and `adoption: none`; its requirements are SHOULDs of that extension.
|
|
146
|
+
|
|
147
|
+
- A host MUST NOT advertise a `grpc` capability block — an unwitnessable family is not advertisable.
|
|
148
|
+
- `api/v2/openapi.yaml` and the AsyncAPI document are the only canonical API descriptions.
|
|
97
149
|
|
|
98
|
-
|
|
150
|
+
The extension re-enters core only by an additive RFC that generates the proto from `spec/v2/declaration.json` and lands a suite client.
|
|
99
151
|
|
|
100
|
-
|
|
152
|
+
## Trace context
|
|
153
|
+
|
|
154
|
+
A host that propagates W3C Trace Context:
|
|
155
|
+
|
|
156
|
+
- into an MCP request MUST carry it in that request's `params._meta` (unprefixed `traceparent`, and `tracestate` when present; MCP 2026-07-28 `_meta`, SEP-414) or in the HTTP `traceparent` header, and SHOULD use `_meta`, the only carrier on stdio;
|
|
157
|
+
- into an A2A message MUST carry it in `Message.metadata.openwop.traceparent` and `.tracestate` or in the HTTP header, and SHOULD use the metadata.
|
|
158
|
+
|
|
159
|
+
A receiver prefers the in-message value, ignores a malformed one, and MUST NOT derive tenant, principal or scope from either.
|
|
101
160
|
|
|
102
161
|
## Threat model
|
|
103
162
|
|
|
104
163
|
`SECURITY/threat-model-interop.md` is the threat model for this document; its invariants are rows of `SECURITY/invariants.yaml`. Peer identity and authorization at the boundary are governed by security-defaults.md; a peer MUST NOT gain authority the caller's Subject does not hold.
|
|
164
|
+
|
|
165
|
+
*Sources: RFC 0175, RFC 0198, RFC 0207, RFC 0208, RFC 0211, RFC 0214.*
|