@openwop/spec-artifacts 2.42.6 → 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.
- package/CORPUS-STAMP.json +64 -63
- package/api/seams-v2.yaml +187 -322
- package/api/v2/asyncapi.yaml +83 -113
- package/api/v2/openapi.yaml +339 -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 +2 -5
- package/spec/v1/core-standard-manifest.json +2 -2
- package/spec/v1/gaps.json +171 -1
- package/spec/v2/README.md +34 -36
- 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 +22 -14
- package/spec/v2/core/host-services.md +22 -5
- 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/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 +95 -49
- package/spec/v2/core/webhooks.md +59 -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 +24 -7
- package/spec/v2/declaration.schema.json +10 -0
- package/spec/v2/errors.json +109 -2
- 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 +542 -2
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# Form Content Packs
|
|
2
2
|
|
|
3
|
-
> **Status: Stable
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
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.*
|
package/spec/v2/core/headers.md
CHANGED
|
@@ -1,41 +1,49 @@
|
|
|
1
1
|
# Headers
|
|
2
2
|
|
|
3
|
-
> **Status: Stable
|
|
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
|
-
|
|
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 (
|
|
15
|
-
| `If-None-Match` | 2 |
|
|
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,
|
|
18
|
-
| `OpenWOP-Force-Engine-Version` | 1 |
|
|
19
|
-
| `OpenWOP-Version` | 55 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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
|
|
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
|
|
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
|
|
3
|
+
> **Status: Stable.**
|
|
4
4
|
> **Normative home:** `aiEnvelope`, `promptLibrary`, `agentRuntime`, `mcp`.
|
|
5
5
|
|
|
6
6
|
## Why this exists
|
|
7
7
|
|
|
8
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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[] }
|
|
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
|
-
|
|
52
|
-
for this axis and MUST be held constant for the life of an advertisement.
|
|
53
|
-
`multiRegion` is the behavioural claim
|
|
54
|
-
|
|
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.*
|
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 ([capabilities.md § portability](capabilities.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.*
|