@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.
Files changed (69) hide show
  1. package/CORPUS-STAMP.json +70 -67
  2. package/api/seams-v2.yaml +238 -322
  3. package/api/v2/asyncapi.yaml +83 -113
  4. package/api/v2/openapi.yaml +408 -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 +4 -7
  9. package/schemas/v2/error-envelope.schema.json +144 -69
  10. package/spec/v1/alias-detectors.json +6 -0
  11. package/spec/v1/core-standard-manifest.json +2 -2
  12. package/spec/v1/deprecations.json +22 -0
  13. package/spec/v1/gaps.json +188 -1
  14. package/spec/v1/migrations.json +18 -0
  15. package/spec/v2/README.md +35 -37
  16. package/spec/v2/core/capabilities.md +186 -119
  17. package/spec/v2/core/conformance.md +78 -27
  18. package/spec/v2/core/connection-packs.md +8 -3
  19. package/spec/v2/core/conversation.md +10 -4
  20. package/spec/v2/core/errors.md +30 -9
  21. package/spec/v2/core/events.md +116 -80
  22. package/spec/v2/core/form-content-packs.md +11 -13
  23. package/spec/v2/core/headers.md +23 -14
  24. package/spec/v2/core/host-services.md +22 -5
  25. package/spec/v2/core/i18n.md +64 -0
  26. package/spec/v2/core/idempotency.md +48 -36
  27. package/spec/v2/core/identity.md +110 -42
  28. package/spec/v2/core/interop.md +82 -21
  29. package/spec/v2/core/interrupt.md +79 -40
  30. package/spec/v2/core/node-pack-runtimes.md +18 -4
  31. package/spec/v2/core/oauth.md +40 -6
  32. package/spec/v2/core/overview.md +61 -33
  33. package/spec/v2/core/packs.md +53 -28
  34. package/spec/v2/core/persistence.md +61 -80
  35. package/spec/v2/core/portability.md +44 -0
  36. package/spec/v2/core/replay.md +56 -42
  37. package/spec/v2/core/runs.md +198 -60
  38. package/spec/v2/core/security-defaults.md +57 -33
  39. package/spec/v2/core/tool-catalog.md +29 -6
  40. package/spec/v2/core/versioning.md +115 -50
  41. package/spec/v2/core/webhooks.md +61 -20
  42. package/spec/v2/core/workflow-chain-packs.md +16 -14
  43. package/spec/v2/corrections.json +17 -1
  44. package/spec/v2/corrections.schema.json +1 -1
  45. package/spec/v2/declaration.json +26 -9
  46. package/spec/v2/declaration.schema.json +10 -0
  47. package/spec/v2/errors.json +121 -4
  48. package/spec/v2/errors.schema.json +38 -0
  49. package/spec/v2/ext/README.md +36 -23
  50. package/spec/v2/ext/a2uiSurface/README.md +75 -89
  51. package/spec/v2/ext/brand/README.md +15 -15
  52. package/spec/v2/ext/canvas/README.md +15 -15
  53. package/spec/v2/ext/chat/README.md +15 -15
  54. package/spec/v2/ext/coordination/README.md +15 -15
  55. package/spec/v2/ext/dataIntegration/README.md +17 -20
  56. package/spec/v2/ext/entities/README.md +15 -15
  57. package/spec/v2/ext/grpc-transport/README.md +6 -8
  58. package/spec/v2/ext/kanban/README.md +15 -15
  59. package/spec/v2/ext/knowledge/README.md +15 -15
  60. package/spec/v2/ext/launchStudio/README.md +16 -16
  61. package/spec/v2/ext/messaging/README.md +15 -15
  62. package/spec/v2/ext/portability/README.md +3 -17
  63. package/spec/v2/ext/provider-idempotency/README.md +7 -10
  64. package/spec/v2/ext/restTransport/README.md +30 -19
  65. package/spec/v2/ext/sandbox-runtime-notes/README.md +7 -9
  66. package/spec/v2/ext/webResearch/README.md +16 -16
  67. package/spec/v2/profiles.json +6 -2
  68. package/spec/v2/release.json +2 -2
  69. package/spec/v2/surface-baseline.json +544 -2
@@ -1,129 +1,267 @@
1
1
  # Runs
2
2
 
3
- > **Status: Stable · RFC 0170 §A, §D.1; RFC 0171 §D; RFC 0176 §B.1.**
3
+ > **Status: Stable.**
4
4
  > **Normative home:** `runList`, `limits`, `conversationPrimitive`, `dataResidency`.
5
5
 
6
6
  ## Why this exists
7
7
 
8
- A run is the unit of execution, ownership and observation. This document is the run surface of `api/v2/openapi.yaml`: how a run is created, read, streamed, cancelled, paused, forked and diffed, and the one snapshot shape every host projects from the same event log (events.md).
8
+ A run is the unit of execution, ownership and observation. This document covers the run surface of `api/v2/openapi.yaml`: how a run is created, read, streamed, cancelled, paused, forked and diffed. Every host projects the same snapshot shape from the same event log ([events.md](events.md)).
9
9
 
10
10
  ## Identity
11
11
 
12
- Every id `$ref`s `schemas/v2/ids.schema.json` (identity.md). A `runId` is tenant-bound, `<tenantId>/<opaque>`, host-minted; a host MUST reject a `runId` whose tenant segment is not the caller's with `403 id_tenant_mismatch` and MUST NOT disclose whether the run exists. A caller MUST treat every id as opaque.
12
+ Every id `$ref`s `schemas/v2/ids.schema.json` ([identity.md](identity.md)). A `runId` is host-minted and tenant-bound: `<tenantId>/<opaque>`.
13
+
14
+ - A caller MUST treat every id as opaque.
15
+ - A host MUST reject a `runId` whose tenant segment is not the caller's with `403 id_tenant_mismatch`, and MUST NOT disclose whether the run exists.
13
16
 
14
17
  ## Surface
15
18
 
16
- Every operation accepts `OpenWOP-Version` (overview.md); every mutating operation accepts `Idempotency-Key` (idempotency.md); every response carries `OpenWOP-Version`. Scopes are the `auth.md` vocabulary.
19
+ Every operation accepts `OpenWOP-Version` ([overview.md](overview.md)) and every response carries it. Every mutating operation accepts `Idempotency-Key` ([idempotency.md](idempotency.md)). Scopes are the vocabulary listed in `api/v2/openapi.yaml`'s security schemes, matched as [identity.md](identity.md) §2.1 describes.
17
20
 
18
21
  | Operation | Method and path | Scope | Gate |
19
22
  | --- | --- | --- | --- |
20
23
  | `createRun` | `POST /runs` | `runs:create` | — |
21
24
  | `getRun` | `GET /runs/{runId}` | `runs:read` | — |
22
- | `streamRunEvents` | `GET /runs/{runId}/events` | `runs:read` | events.md |
23
- | `pollRunEvents` | `GET /runs/{runId}/events/poll` | `runs:read` | events.md |
25
+ | `listRuns` | `GET /runs` | `runs:read` | `runList` |
26
+ | `streamRunEvents` | `GET /runs/{runId}/events` | `runs:read` | [events.md](events.md) |
27
+ | `pollRunEvents` | `GET /runs/{runId}/events/poll` | `runs:read` | [events.md](events.md) |
24
28
  | `cancelRun` | `POST /runs/{runId}/cancel` | `runs:cancel` | — |
25
29
  | `bulkCancelRuns` | `POST /runs:bulk-cancel` | `runs:cancel` | — |
26
30
  | `pauseRun` | `POST /runs/{runId}:pause` | `runs:cancel` | — |
27
31
  | `resumeRun` | `POST /runs/{runId}:resume` | `runs:cancel` | — |
28
- | `forkRun` | `POST /runs/{runId}:fork` | `runs:create` + `runs:read` | `replay` (replay.md) |
29
- | `diffRun` | `GET /runs/{runId}:diff?against=` | `runs:read` on both | OPTIONAL; `404` when absent |
30
- | `getRunAncestry` | `GET /runs/{runId}/ancestry` | `runs:read` | `multiAgent.executionModel.crossHostCausation.ancestryEndpointSupported`; `404` when unadvertised |
31
- | `createAnnotation` / `listAnnotations` | `POST` / `GET /runs/{runId}/annotations` | `runs:annotate` / `runs:read` | `feedback`; `404 not_found` when unadvertised (as `getEvalSummary` and `getRunAncestry`; the registry has no 501 for an unadvertised surface) |
32
+ | `forkRun` | `POST /runs/{runId}:fork` | `runs:create` + `runs:read` | `replay` |
33
+ | `diffRun` | `GET /runs/{runId}:diff?against=` | `runs:read` on both | OPTIONAL |
34
+ | `getRunAncestry` | `GET /runs/{runId}/ancestry` | `runs:read` | `multiAgent.executionModel.crossHostCausation.ancestryEndpointSupported` |
35
+ | `createAnnotation` / `listAnnotations` | `POST` / `GET /runs/{runId}/annotations` | `runs:annotate` / `runs:read` | `feedback` |
32
36
  | `getArtifact` | `GET /runs/{runId}/artifacts/{artifactId}` | `artifacts:read` | — |
33
- | `getEvalSummary` | `GET /runs/{runId}/eval-summary` | `runs:read` | `agents.evalSuite`; `404` when unadvertised |
34
- | `getRunCompensation` | `GET /runs/{runId}/compensation` | `runs:read` | `compensation` (security-defaults.md) |
35
- | `getRunEffects` | `GET /runs/{runId}/effects` | `runs:read` | `idempotency` (idempotency.md) |
36
- | `listRuns` | `GET /runs` | `runs:read` | `runList` (RFC 0182); `404` when unadvertised |
37
+ | `getEvalSummary` | `GET /runs/{runId}/eval-summary` | `runs:read` | `agents.evalSuite` |
38
+ | `getRunCompensation` | `GET /runs/{runId}/compensation` | `runs:read` | `compensation` |
39
+ | `getRunEffects` | `GET /runs/{runId}/effects` | `runs:read` | `idempotency` |
40
+
41
+ A gated operation the host does not advertise (or an absent `diffRun`) answers `404`; for annotations it is `404 not_found`.
37
42
 
38
43
  ## Create
39
44
 
40
- The `createRun` body is closed at the composition (`unevaluatedProperties: false`): `workflowId` (REQUIRED unless `mode: eval`), `inputs`, `residency`, `tenantId`, `scopeId`, `callbackUrl` (interrupt.md §Callback delivery; a refusal is `400 validation_error`, `details.field: "callbackUrl"`), `mode`, `evalSuiteRef`, `agentId`, and the `RunOptions` fields `configurable`, `tags`, `metadata`. A body without `RunOptions` MUST be accepted as if it were `{}`.
45
+ The `createRun` body is closed (`unevaluatedProperties: false`). Its fields:
46
+
47
+ - `workflowId` — REQUIRED unless `mode: eval`.
48
+ - `inputs`, `residency`, `tenantId`, `scopeId`.
49
+ - `callbackUrl` — see [interrupt.md](interrupt.md) §Callback delivery. A refused value is `400 validation_error` with `details.field: "callbackUrl"`.
50
+ - `mode`, `evalSuiteRef`, `agentId`.
51
+ - The `RunOptions` fields `configurable`, `tags`, `metadata`. A body without `RunOptions` MUST be accepted as if it were `{}`.
52
+
53
+ ### Request headers
41
54
 
42
55
  | Header | Rule |
43
56
  | --- | --- |
44
- | `Idempotency-Key` | RECOMMENDED; a replayed create MUST NOT create a second run and carries `OpenWOP-Idempotent-Replay: true`. |
57
+ | `Idempotency-Key` | RECOMMENDED. A replayed create MUST NOT create a second run, and carries `OpenWOP-Idempotent-Replay: true`. |
45
58
  | `OpenWOP-Dedup: enforce` | The host MUST reject a duplicate `(tenantId, scopeId)` with `409 run_already_active` and `Retry-After`. |
46
- | `OpenWOP-Force-Engine-Version` | Test keys only; the seams profile. A host MUST reject it on a production credential with `403`. |
59
+ | `OpenWOP-Force-Engine-Version` | Test keys only (the seams profile). A host MUST reject it on a production credential with `403`. |
60
+
61
+ ### Response
62
+
63
+ The `201` response is `{ runId, status, eventsUrl, statusUrl? }`. `status` is one of `pending`, `running`, `waiting-approval`, `waiting-input`, `waiting-external`.
47
64
 
48
- The `201` response is `{ runId, status, eventsUrl, statusUrl? }`, `status` one of `pending`, `running`, `waiting-approval`, `waiting-input`, `waiting-external`. `eventsUrl` and `statusUrl` MUST resolve under the origin the request was made to — a relative path, or an absolute URL on the same origin — and MUST NOT downgrade the scheme; a link that names a different host (a backing service behind the origin) or `http://` on an `https://` origin is non-conformant. The base that minted `runId` MUST resolve it: `GET /runs/{runId}` and `GET /runs/{runId}/events/poll` at that base MUST answer `200` for the id the `201` returned, percent-encoded per identity.md §5 (a front door that decodes `%2F` before routing makes every tenant-bound id unreachable). `mode: eval` (with `evalSuiteRef` and `agentId` REQUIRED) starts an eval-suite projection that emits the content-free `eval.*` family and terminates with an `EvalSummary`; a host that does not advertise `agents.evalSuite` MUST reject it. A host advertising `dataResidency` MUST reject a `residency.region` outside `dataResidency.regions` with `422 residency_unavailable` and create no run (§"Conversation and residency capabilities"). A workflow that references a capability-gated reserved node type on a host that does not advertise the capability MUST be rejected with `422 capability_required`.
65
+ - `eventsUrl` and `statusUrl` MUST resolve under the origin the request was made to — a relative path, or an absolute URL on the same origin — and MUST NOT downgrade the scheme. A link naming a different host, or `http://` on an `https://` origin, is non-conformant.
66
+ - The base that minted `runId` MUST resolve it: `GET /runs/{runId}` and `GET /runs/{runId}/events/poll` at that base MUST answer `200` for the returned id, percent-encoded per [identity.md](identity.md) §5. (A front door that decodes `%2F` before routing makes every tenant-bound id unreachable.)
49
67
 
50
- `run.started` (events.md) MUST echo the run's `owner` block exactly as `RunSnapshot.owner` carries it (RFC 0170 §A.1); `transport` records `rest`, `mcp`, `a2a` or `ui`.
68
+ ### Refusals
69
+
70
+ - `mode: eval` makes `evalSuiteRef` and `agentId` REQUIRED. It starts an eval-suite projection that emits the content-free `eval.*` family and terminates with an `EvalSummary`. A host that does not advertise `agents.evalSuite` MUST reject it.
71
+ - A host advertising `dataResidency` MUST reject a `residency.region` outside `dataResidency.regions` with `422 residency_unavailable` and create no run (see §"Conversation and residency capabilities").
72
+ - A workflow that references a capability-gated reserved node type on a host that does not advertise the capability MUST be rejected with `422 capability_required`.
73
+
74
+ ### The start event
75
+
76
+ `run.started` ([events.md](events.md)) MUST echo the run's `owner` block exactly as `RunSnapshot.owner` carries it. Its `transport` records `rest`, `mcp`, `a2a` or `ui`.
51
77
 
52
78
  ## Run options
53
79
 
54
80
  `schemas/v2/run-options.schema.json` is `{ configurable?, tags?, metadata? }`.
55
81
 
56
- `configurable` is `schemas/v2/configurable.schema.json`: closed, nested and versioned (RFC 0171 §D.1). The request body `$ref`s it directly; there is no `allOf`-merge of an open map. `version` is REQUIRED and is `1`.
82
+ `configurable` is `schemas/v2/configurable.schema.json`: closed, nested and versioned. The request body `$ref`s it directly; there is no `allOf`-merge of an open map. `version` is REQUIRED and is `1`. It has five sections:
83
+
84
+ | Section | Keys |
85
+ | --- | --- |
86
+ | `run` | `recursionLimit`, `runTimeoutMs`, `maxLoopIterations`, `escalationThreshold` |
87
+ | `ai` | `provider`, `model`, `temperature` (0..2), `maxTokens`, `credentialRef`, `promptOverrides`, `mockProvider`, `reasoningVerbosity` (`none` \| `summary` \| `full`), `maxRefusals` |
88
+ | `distillation` | `tokenBudget` |
89
+ | `budget` | `schemas/v2/budget-policy.schema.json` — the run's budget policy |
90
+ | `extensions` | `<org>: {…}` — a vendor key lives under its registered org and nowhere else |
91
+
92
+ ### `run` section
57
93
 
58
- | Section | Keys | Rule |
59
- | --- | --- | --- |
60
- | `run` | `recursionLimit`, `runTimeoutMs`, `maxLoopIterations`, `escalationThreshold` | `recursionLimit` is clamped to `limits.maxNodeExecutions`. `runTimeoutMs` resolves to `min(runTimeoutMs, limits.maxRunDurationMs)`; an out-of-range value MUST return `400 validation_error` at create, and a breach MUST emit `cap.breached { kind: 'run-duration' }` and terminate the run `failed` with `run_timeout`. `maxLoopIterations` resolves against `limits.maxLoopIterations`; a breach MUST emit `cap.breached { kind: 'loop-iterations' }` and fail with `loop_limit_exceeded`. `escalationThreshold` is the `low-confidence` threshold (interrupt.md). |
61
- | `ai` | `provider`, `model`, `temperature` (0..2), `maxTokens`, `credentialRef`, `promptOverrides`, `mockProvider`, `reasoningVerbosity` (`none` \| `summary` \| `full`), `maxRefusals` | `provider` MUST be in `aiProviders.providers`, else `400 validation_error`. `credentialRef` MUST reference a provider in `aiProviders.byok`, else `403 credential_forbidden`; it never carries key material. `mockProvider` is test-keys-only: a host MUST refuse it on a production credential with `403`. `maxRefusals` is the refusal ceiling (events.md E5). |
62
- | `distillation` | `tokenBudget` | Resolves to `min(tokenBudget, memory.distillation.maxTokenBudget)`; a run that cannot distill within it MUST fail atomically with `token_budget_exceeded`. |
63
- | `budget` | `schemas/v2/budget-policy.schema.json` | The run's budget policy. |
64
- | `extensions` | `<org>: {…}` | A vendor key lives under its registered org and nowhere else. |
94
+ - `recursionLimit` is clamped to `limits.maxNodeExecutions`.
95
+ - `runTimeoutMs` resolves to `min(runTimeoutMs, limits.maxRunDurationMs)`. An out-of-range value MUST return `400 validation_error` at create. A breach MUST emit `cap.breached { kind: 'run-duration' }` and terminate the run `failed` with `run_timeout`.
96
+ - `maxLoopIterations` resolves against `limits.maxLoopIterations`. A breach MUST emit `cap.breached { kind: 'loop-iterations' }` and fail with `loop_limit_exceeded`.
97
+ - `escalationThreshold` is the `low-confidence` threshold ([interrupt.md](interrupt.md)).
65
98
 
66
- An unknown root key, an unknown key inside a section, or a dotted key (`ai.provider` as a string key) MUST be rejected with `400 validation_error`. A host MUST persist `RunOptions` on the run at creation, MUST surface the same `configurable` to every attempt of a node, and MUST NOT allow `configurable` to change after creation. A workflow's `configurableSchema` MUST be validated against at create time and MUST be surfaced on `getWorkflow`.
99
+ ### `ai` section
67
100
 
68
- `tags` is an opaque string array (at most 100 entries, each at most 256 characters, valid UTF-8); a host MUST NOT reject a tag on format and MUST return `400 validation_error` over the limits. `metadata` is a free-form JSON object the engine MUST NOT consume for any execution decision; a host MUST persist it. Both surface unchanged on `RunSnapshot`.
101
+ - `provider` MUST be in `aiProviders.providers`, else `400 validation_error`.
102
+ - `credentialRef` MUST reference a provider in `aiProviders.byok`, else `403 credential_forbidden`. It never carries key material.
103
+ - `mockProvider` is test-keys-only: a host MUST refuse it on a production credential with `403`.
104
+ - `maxRefusals` is the refusal ceiling ([events.md](events.md) E5).
105
+
106
+ ### `distillation` section
107
+
108
+ `tokenBudget` resolves to `min(tokenBudget, memory.distillation.maxTokenBudget)`. A run that cannot distill within it MUST fail atomically with `token_budget_exceeded`.
109
+
110
+ ### Validation and persistence
111
+
112
+ - An unknown root key, an unknown key inside a section, or a dotted key (`ai.provider` as a string key) MUST be rejected with `400 validation_error`.
113
+ - A host MUST persist `RunOptions` on the run at creation, MUST surface the same `configurable` to every attempt of a node, and MUST NOT allow `configurable` to change after creation.
114
+ - A workflow's `configurableSchema` MUST be validated against at create time and MUST be surfaced on `getWorkflow`.
115
+
116
+ ### `tags` and `metadata`
117
+
118
+ - `tags` is an opaque string array: at most 100 entries, each at most 256 characters, valid UTF-8. A host MUST NOT reject a tag on format, and MUST return `400 validation_error` over the limits.
119
+ - `metadata` is a free-form JSON object. A host MUST persist it, and the engine MUST NOT consume it for any execution decision.
120
+ - Both surface unchanged on `RunSnapshot`.
69
121
 
70
122
  ## Snapshot
71
123
 
72
- `getRun` returns `schemas/v2/run-snapshot.schema.json`, the fold of the event log through the run projection. `runId`, `workflowId`, `status`, `owner` and `eventLogSchemaVersion` are REQUIRED; the object is closed.
124
+ `getRun` returns `schemas/v2/run-snapshot.schema.json`: the fold of the event log through the run projection. The object is closed. `runId`, `workflowId`, `status`, `owner` and `eventLogSchemaVersion` are REQUIRED.
73
125
 
74
- | Field | Rule |
126
+ | Field | Meaning |
75
127
  | --- | --- |
76
- | `owner` | `{ tenant, workspace?, subject }`, closed, `subject` REQUIRED (`schemas/v2/subject.schema.json`). `principal` and `principalKind` do not exist. A run created before the host emitted subjects reads with the legacy subject rule (identity.md), stamped at first read and never rewritten. |
77
- | `status` | `pending`, `running`, `paused`, `waiting-approval`, `waiting-input`, `waiting-external`, `completed`, `failed`, `cancelling`, `cancelled`. `waiting-external` MUST be used when the suspended interrupt's `kind` is `external-event`. `cancelling` is the state between an accepted cancel and the terminal `cancelled`. The vocabulary grows by overview.md §0. |
78
- | `eventLogSchemaVersion` | The era key, integer ≥ 2 (persistence.md §"The era key"). |
79
- | `engineVersion` | Integer. |
80
- | `compensationStatus` | `none`, `pending`, `running`, `completed`, `partial`, `failed`, `manual`. A host that does not advertise `compensation` MUST omit it; a host that does MUST include it on every snapshot, `none` when never requested. |
81
- | `currentNodeId` | Set while suspended; names the node holding the interrupt. |
82
- | `error` | `{ code, message, details? }` on terminal `failed`. |
83
- | `configurable`, `tags`, `metadata` | The persisted `RunOptions`. |
84
- | `agent`, `runOrchestrator` | `schemas/v2/agent-ref.schema.json`; `runOrchestrator` MUST NOT change for the run's lifetime. |
85
- | `metrics.openwopCost` | `{ usd, tokens { input, output }, model, provider, duration_ms }`; absence is not zero. |
86
-
87
- The `200` SHOULD carry a strong `ETag` derived from the latest persisted `sequence`; when present it MUST change on every observable transition and be stable otherwise. A request whose `If-None-Match` matches MUST receive `304` with no body. A host MAY compress (`gzip` baseline; `br`, `zstd` only where advertised under `restTransport.contentEncodings`) and MUST then set `Content-Encoding` and `Vary: Accept-Encoding`; the decoded body is byte-identical.
128
+ | `owner` | `{ tenant, workspace?, subject }`, closed; `subject` REQUIRED (`schemas/v2/subject.schema.json`) |
129
+ | `status` | Run state (below) |
130
+ | `eventLogSchemaVersion` | The era key, integer ≥ 2 ([persistence.md](persistence.md) §"The era key") |
131
+ | `engineVersion` | Integer |
132
+ | `compensationStatus` | `none`, `pending`, `running`, `completed`, `partial`, `failed`, `manual` |
133
+ | `currentNodeId` | Set while suspended; names the node holding the interrupt |
134
+ | `error` | `{ code, message, details? }` on terminal `failed` |
135
+ | `configurable`, `tags`, `metadata` | The persisted `RunOptions` |
136
+ | `agent`, `runOrchestrator` | `schemas/v2/agent-ref.schema.json` |
137
+ | `metrics.openwopCost` | `{ usd, tokens { input, output }, model, provider, duration_ms }`; absence is not zero |
138
+
139
+ Field rules:
140
+
141
+ - `status` is one of `pending`, `running`, `paused`, `waiting-approval`, `waiting-input`, `waiting-external`, `completed`, `failed`, `cancelling`, `cancelled`. `waiting-external` MUST be used when the suspended interrupt's `kind` is `external-event`. `cancelling` is the state between an accepted cancel and the terminal `cancelled`. The vocabulary grows by [overview.md](overview.md) §0.
142
+ - `owner`: a run created before the host emitted subjects reads with the subject rule in [identity.md](identity.md), stamped at first read and never rewritten.
143
+ - `compensationStatus`: a host that does not advertise `compensation` MUST omit it. A host that does MUST include it on every snapshot, `none` when never requested.
144
+ - `runOrchestrator` MUST NOT change for the run's lifetime.
145
+
146
+ ### Caching and encoding
147
+
148
+ - The `200` SHOULD carry a strong `ETag` derived from the latest persisted `sequence`. When present it MUST change on every observable transition and be stable otherwise.
149
+ - When the host sends an `ETag`, a request whose `If-None-Match` matches it MUST receive `304` with no body.
150
+ - A host MAY compress (`gzip` baseline; `br` and `zstd` only where advertised under `extensions["<org>.rest-transport"].contentEncodings`, [ext/restTransport](../ext/restTransport/README.md)). It MUST then set `Content-Encoding` and `Vary: Accept-Encoding`. The decoded body is byte-identical.
88
151
 
89
152
  ## List
90
153
 
91
- `GET /runs` (RFC 0182; gated on `runList`) returns `{ runs: RunSnapshot[], nextCursor? }`, the caller's runs newest first: only runs whose tenant segment is the caller's, every `runId` bound (identity.md §5), and a run the caller created MUST appear. A page MUST NOT exceed `runList.maxPageSize`; `cursor` is opaque and one the host did not mint MUST be refused `400 validation_error`; `workflowId` and `status` are exact-match filters when `runList.filters` names them (an unadvertised filter is ignored).
154
+ `GET /runs` (gated on `runList`) returns `{ runs: RunSnapshot[], nextCursor? }`: the caller's runs, newest first.
155
+
156
+ - Only runs whose tenant segment is the caller's appear, and every `runId` is bound ([identity.md](identity.md) §5). A run the caller created MUST appear.
157
+ - A page MUST NOT exceed `runList.maxPageSize`.
158
+ - `cursor` is opaque. A cursor the host did not mint MUST be refused with `400 validation_error`.
159
+ - `workflowId` and `status` are exact-match filters when `runList.filters` names them. An unadvertised filter is ignored.
92
160
 
93
161
  ## Cancel
94
162
 
95
- `cancelRun` accepts `{ reason? }` and answers `200 { runId, status }` with `status` `cancelling` or `cancelled`; the cascade MAY be asynchronous and the run emits `run.cancelled` when it completes. A cancel on a run that is already terminal (`completed`, `failed`, `cancelled`) MUST be refused `409 run_terminal`; a `200` whose `status` echoes the terminal state is outside this grammar. Cancelling a parent MUST NOT silently abandon an active compensation (security-defaults.md). `run.cancelled.parentRunId` with `reason: parent-cancelled` records a cascade from a parent.
163
+ `cancelRun` accepts `{ reason? }` and answers `200 { runId, status }`, where `status` is `cancelling` or `cancelled`. The cascade MAY be asynchronous; the run emits `run.cancelled` when it completes.
164
+
165
+ - A cancel on a terminal run (`completed`, `failed`, `cancelled`) MUST be refused with `409 run_terminal`. A `200` echoing the terminal state is non-conformant.
166
+ - Cancelling a parent MUST NOT silently abandon an active compensation ([security-defaults.md](security-defaults.md)).
167
+ - `run.cancelled.parentRunId` with `reason: parent-cancelled` records a cascade from a parent.
168
+
169
+ ### Bulk cancel
170
+
171
+ `bulkCancelRuns` accepts `{ runIds[1..100], reason? }`.
172
+
173
+ - Over the host's cap (RECOMMENDED 100) it MUST return `400 validation_error` with `details.maxRunIds`.
174
+ - The host MUST process each id independently, and MUST return `200 { results[] }` in request order, even when every id failed.
175
+ - The host MUST enforce authorization per id. A run the caller cannot see yields `ok: false` with an error envelope in that entry, never a top-level `403`.
176
+
177
+ Per-entry errors ([errors.md](errors.md)):
96
178
 
97
- A non-terminal run inherited from v1 continues, or is cancelled `v1_pin_unsupported`, per persistence.md §"Runs pinned to v1" (RFC 0176 §B.1).
179
+ | Condition | Code |
180
+ | --- | --- |
181
+ | The id's tenant segment is not the caller's ([identity.md](identity.md) §5 applies inside an entry as on a path) | `id_tenant_mismatch`, or `not_found` where existence is not leaked |
182
+ | A run in the caller's tenant the caller may not cancel | `run_forbidden` |
183
+ | A run already terminal | `run_terminal` |
98
184
 
99
- `bulkCancelRuns` accepts `{ runIds[1..100], reason? }`; over the host's cap (RECOMMENDED 100) it MUST return `400 validation_error` with `details.maxRunIds`. The host MUST process each id independently, MUST return `200 { results[] }` in request order even when every id failed, and MUST enforce authorization per id: a run the caller cannot see yields `ok: false` with an error envelope in that entry, never a top-level `403` — `id_tenant_mismatch` (or `not_found` where existence is not leaked) when the id's tenant segment is not the caller's (identity.md §5 applies inside an entry exactly as on a path), `run_forbidden` for a run in the caller's tenant the caller may not cancel, `run_terminal` for a run already terminal. `ok: true` carries `status` `cancelling` or `cancelled`; `ok: false` carries the error envelope (errors.md).
185
+ `ok: true` carries `status` `cancelling` or `cancelled`; `ok: false` carries the error envelope.
100
186
 
101
187
  ## Pause and resume
102
188
 
103
- `pauseRun` accepts `{ reason?, drainPolicy? }` with `drainPolicy` `immediate` (snapshot between events) or `drain-current-node` (default; the executing node reaches a terminal first) and answers `202 { runId, status: 'paused', pausedAt? }`; the transition emits `run.paused`, whose payload echoes the request's `drainPolicy` word. With `immediate`, the attempt that was executing is cut between events: it has no terminal node event, a host MUST NOT record `node.failed` (or any terminal node event) for it, and the resumed run's `node.started` begins a fresh attempt. The record of the interruption is `run.paused` itself; its payload MAY carry `interruptedNodeId` and `interruptedAttempt` so a `debug` consumer can see which attempt was cut. A run already paused, terminal, or otherwise unpausable MUST receive `409`: `run_terminal` when the run is terminal, else `run_state_conflict` with `details.runStatus` naming the status that refused it. `resumeRun` accepts `{ reason? }`, answers `202 { runId, status: 'running', resumedAt? }`, emits `run.resumed`, and MUST return `409` when the run is not paused — `run_terminal` or `run_state_conflict` by the same rule. Only `resumeRun` or a cancel exits `paused`. A replay MUST fold `run.paused` and `run.resumed` as no-ops for projected state.
189
+ `pauseRun` accepts `{ reason?, drainPolicy? }` and answers `202 { runId, status: 'paused', pausedAt? }`. The transition emits `run.paused`, whose payload echoes the request's `drainPolicy` word.
190
+
191
+ `drainPolicy` is one of:
192
+
193
+ - `drain-current-node` (default) — the executing node reaches a terminal first.
194
+ - `immediate` — the run is snapshotted between events. The executing attempt is cut: it has no terminal node event, a host MUST NOT record `node.failed` (or any terminal node event) for it, and the resumed run's `node.started` begins a fresh attempt. `run.paused` itself records the interruption; its payload MAY carry `interruptedNodeId` and `interruptedAttempt` so a `debug` consumer can see which attempt was cut.
195
+
196
+ `resumeRun` accepts `{ reason? }`, answers `202 { runId, status: 'running', resumedAt? }`, and emits `run.resumed`.
197
+
198
+ Rules:
199
+
200
+ - A pause on a run that is already paused, terminal, or otherwise unpausable MUST receive `409`.
201
+ - A resume on a run that is not paused MUST return `409`.
202
+ - In both cases the code is `run_terminal` when the run is terminal, else `run_state_conflict` with `details.runStatus` naming the refusing status.
203
+ - Only `resumeRun` or a cancel exits `paused`.
204
+ - A replay MUST fold `run.paused` and `run.resumed` as no-ops for projected state.
104
205
 
105
206
  ## Fork
106
207
 
107
- `forkRun` accepts `{ mode: replay | branch, fromSeq?, runOptionsOverlay? }`: events with `sequence < fromSeq` are fixed history and events `≥ fromSeq` re-execute. `fromSeq` is REQUIRED for `branch` and defaults to `0` for `replay`; `runOptionsOverlay` is `branch`-only, and a `replay` with a non-empty overlay MUST be rejected with `400`. A `fromSeq` not in the source log MUST be rejected with `422 fork_point_invalid`. The `201` response is `{ runId, sourceRunId, fromSeq?, mode, status, eventsUrl }`. The child's `owner` is copied verbatim from the parent (RFC 0170 §A.4). Determinism, side-effect suppression, and forking an era-2 parent are in replay.md.
208
+ `forkRun` accepts `{ mode: replay | branch, fromSeq?, runOptionsOverlay? }`. Events with `sequence < fromSeq` are fixed history; events `≥ fromSeq` re-execute.
209
+
210
+ - `fromSeq` is REQUIRED for `branch` and defaults to `0` for `replay`.
211
+ - `runOptionsOverlay` is `branch`-only. A `replay` with a non-empty overlay MUST be rejected with `400`.
212
+ - A `fromSeq` not in the source log MUST be rejected with `422 fork_point_invalid`.
213
+
214
+ The `201` response is `{ runId, sourceRunId, fromSeq?, mode, status, eventsUrl }`. The child's `owner` is copied verbatim from the parent. Determinism, side-effect suppression, and forking an era-2 parent are in [replay.md](replay.md).
108
215
 
109
216
  ## Diff and ancestry
110
217
 
111
- `diffRun` returns `schemas/v2/run-diff-response.schema.json`: `divergedAtSeq`, ordered `eventDiffs[]`, `stateDiff`, optional `truncated`. The diff MUST be a pure function of the two logs: identical logs MUST yield `divergedAtSeq: null` and empty `eventDiffs`; `eventId`, `runId`, `timestamp` and other run-scoped fields MUST be excluded from comparison. A host that diffs an in-flight prefix MUST set `truncated: true`. A caller lacking `runs:read` on either run MUST receive `403`. `getRunAncestry` returns `schemas/v2/run-ancestry-response.schema.json` (`runId`, `hostId`, `parent` or `null`); a client walks the chain one hop at a time via `parent.wellKnownUrl`.
218
+ `diffRun` returns `schemas/v2/run-diff-response.schema.json`: `divergedAtSeq`, ordered `eventDiffs[]`, `stateDiff`, optional `truncated`.
219
+
220
+ - The diff MUST be a pure function of the two logs. Identical logs MUST yield `divergedAtSeq: null` and empty `eventDiffs`.
221
+ - `eventId`, `runId`, `timestamp` and other run-scoped fields MUST be excluded from comparison.
222
+ - A host that diffs an in-flight prefix MUST set `truncated: true`.
223
+ - A caller lacking `runs:read` on either run MUST receive `403`.
224
+
225
+ `getRunAncestry` returns `schemas/v2/run-ancestry-response.schema.json` (`runId`, `hostId`, `parent` or `null`). A client walks the chain one hop at a time via `parent.wellKnownUrl`.
112
226
 
113
227
  ## Annotations, artifacts, eval summary
114
228
 
115
- `createAnnotation` accepts `schemas/v2/annotation-create.schema.json` and returns `201` with `schemas/v2/annotation.schema.json`; `listAnnotations` returns `{ annotations[] }`. An annotation is a live notification (`run.annotated`), never a run event: it MUST NOT enter the event log and MUST be excluded from fork, replay and diff. `getArtifact` answers `application/json` with an implementation-defined object or, when `Accept` prefers `application/a2a+json`, an A2A `Artifact` (`schemas/v2/artifact.schema.json`); a host SHOULD offer the latter. A body served as `application/a2a+json` MUST validate against that schema with `artifactId` equal to the path's, and a `url` Part in it MUST NOT resolve beyond the caller's `artifacts:read` authorization. `getEvalSummary` returns `schemas/v2/eval-summary.schema.json` for a terminal eval run, `409` while it is running, `404` when the run is not an eval run; the summary MUST be content-free of task output, rubric prose and credentials.
229
+ ### Annotations
230
+
231
+ `createAnnotation` accepts `schemas/v2/annotation-create.schema.json` and returns `201` with `schemas/v2/annotation.schema.json`. `listAnnotations` returns `{ annotations[] }`.
232
+
233
+ An annotation is a live notification (`run.annotated`), never a run event. It MUST NOT enter the event log and MUST be excluded from fork, replay and diff.
234
+
235
+ ### Artifacts
236
+
237
+ `getArtifact` answers `application/json` with an implementation-defined object or, when `Accept` prefers `application/a2a+json`, an A2A `Artifact` (`schemas/v2/artifact.schema.json`). A host SHOULD offer the latter.
238
+
239
+ - A body served as `application/a2a+json` MUST validate against that schema, with `artifactId` equal to the path's.
240
+ - A `url` Part in it MUST NOT resolve beyond the caller's `artifacts:read` authorization.
241
+
242
+ ### Eval summary
243
+
244
+ `getEvalSummary` returns `schemas/v2/eval-summary.schema.json` for a terminal eval run, `409` while it is running, and `404` when the run is not an eval run. The summary MUST be free of task output, rubric prose and credentials.
116
245
 
117
246
  ## Conversation and residency capabilities
118
247
 
119
- `conversationPrimitive` carries no payload: its presence is the claim (capabilities.md §2). A
120
- workflow whose `nodes[].typeId` references `core.conversationGate` MUST be refused by a host that
121
- does not advertise `conversationPrimitive`, at registration or at run creation, with `422
122
- capability_required` naming the family in `details.requiredCapability`.
248
+ ### `conversationPrimitive`
249
+
250
+ `conversationPrimitive` carries no payload: its presence is the claim ([capabilities.md](capabilities.md) §2). A host that does not advertise it MUST refuse a workflow whose `nodes[].typeId` references `core.conversationGate` — at registration or at run creation — with `422 capability_required`, naming the family in `details.requiredCapability`.
251
+
252
+ A conversation turn MAY carry `parts`: a non-empty array of A2A `Part` objects (`schemas/v2/part.schema.json`) that marks the turn A2A-shaped. A producer SHOULD emit it and keep `content` readable by consumers that predate it. A turn without `parts` stays valid on emission, replay and fork.
253
+
254
+ ### `dataResidency`
255
+
256
+ A host advertising `dataResidency` MUST honor-or-reject, and MUST NOT silently accept-and-ignore:
257
+
258
+ - accept a `residency` constraint naming a region in `dataResidency.regions`;
259
+ - refuse one it does not advertise with `residency_unavailable`.
260
+
261
+ A host that does not advertise `dataResidency` MAY ignore or reject a `residency` constraint, but MUST NOT claim to honor it.
262
+
263
+ ## During the v1 overlap
123
264
 
124
- A conversation turn MAY carry `parts`, a non-empty array of A2A `Part` objects (`schemas/v2/part.schema.json`); its presence marks the turn A2A-shaped. A producer SHOULD emit it and keep `content` readable by consumers that predate it. A turn without `parts` stays valid on emission, replay and fork.
265
+ A non-terminal run inherited from v1 continues, or is cancelled `v1_pin_unsupported`, per [persistence.md](persistence.md) §"Runs pinned to v1".
125
266
 
126
- A host advertising `dataResidency` MUST honor-or-reject: accept a `residency` constraint naming a
127
- region in `dataResidency.regions`, refuse one it does not advertise with `residency_unavailable`,
128
- and MUST NOT silently accept-and-ignore. A host that does not advertise `dataResidency` MAY ignore or
129
- reject a `residency` constraint but MUST NOT claim to honor it.
267
+ *Sources: RFC 0170, RFC 0171, RFC 0176, RFC 0182.*
@@ -1,74 +1,99 @@
1
1
  # Security Defaults
2
2
 
3
- > **Status: Stable · RFC 0173 (§A–§E), 0164 §22, 0170 §B.**
3
+ > **Status: Stable.**
4
4
  > **Normative home:** `sandbox`, `compensation`, `purposePropagation`.
5
5
 
6
6
  ## Why this exists
7
7
 
8
- RFC 0173 replaces v1's opt-in security flags (RFC 0164 §22). This document is the obligation table: which surface binds which behavior, the invariant, and the witness.
8
+ In v2 a security behavior is not an opt-in flag. This document is the obligation table: which surface binds which behavior, the invariant, and the witness.
9
9
 
10
10
  ## The rule
11
11
 
12
- A security-load-bearing behavior is an obligation of the surface that needs it (RFC 0173 §A.1). Advertising the surface binds the behavior; no discovery field gates it. Every obligation in `core/` MUST name a surface, an invariant, and a witness class other than `unwitnessable`; a row that cannot is not in `core/` (§D.1).
12
+ A security-load-bearing behavior is an obligation of the surface that needs it. Advertising the surface binds the behavior; no discovery field gates it.
13
13
 
14
- A host MUST NOT advertise a surface whose obligation it has relaxed (§A.2).
14
+ - Every obligation in `core/` MUST name a surface, an invariant, and a witness class other than `unwitnessable`. A row that cannot is not in `core/`.
15
+ - A host MUST NOT advertise a surface whose obligation it has relaxed.
15
16
 
16
17
  ## The obligation table
17
18
 
18
- | Surface advertised | Obligation (v1 flag it replaces) | Witness | Invariant |
19
+ | Surface advertised | Obligation | Witness | Invariant |
19
20
  | --- | --- | --- | --- |
20
- | any lane in `auth.lanes[]` | the lane obligations of identity.md: the verify → bind → audience → resolve → fail-closed pipeline (RFC 0170 §B.1); a named trust root as `subject.issuer` (§B.2); revocation for the lane (§B.3); the advertised `minimumAssurance` floor, `mtls.required` becoming `key-bound` (§B.4); lane-scoped delegation proof (§B.5). Replaces the fourteen `auth.*` gates. | unaided or seam-gated per lane | `sender-constraint-no-bearer-downgrade`; per-lane rows (RFC 0170 §E) |
21
- | both `saml` and `scim` lanes | the leaver contract (RFC 0164; already mandatory) | seam-gated (RFC 0163 seams) | `subject-link-leaver-deny`, `subject-link-mandatory-when-both-advertised` |
22
- | `replay` (any mode) | side-effect suppression with `recorded-outcome` semantics as the only conforming behavior; `none` is not a value (replay.md) | witnessable-gated via the effect-seam manifest | `replay-fanout-no-refire`; effect-seam rows registered at Accepted |
23
- | `webhooks` | durable delivery: retries per the advertised policy with backoff, dead-letter on exhaustion, at-least-once; best-effort is not a conforming delivery mode (replaces `webhooks.durable`) | witnessable-gated (`webhook-signed-delivery` + dead-letter leg) | registered at Accepted (RFC 0173) |
24
- | `interrupt` with `approversList` or `refKinds` | enforcement: a resolver not in the list, group, or role MUST be refused (replaces the `approverRouting` gate; `refKinds[]` stays a facet) | witnessable-gated | registered at Accepted (RFC 0173) |
25
- | `packs` (pack execution) | isolation: the eight `node-pack-sandbox-*` invariants bind for pack code; `sandbox.isolationModel` names the mechanism and never relaxes the property (replaces `sandbox.supported`) | witnessable-gated (eight `sandbox-*` scenarios) | `node-pack-sandbox-*` |
26
- | `compensation` | the plan, attempt, and inverse-action obligations with the read projection `GET /runs/{runId}/compensation` and the operator action family as canonical wire (replaces `compensation.supported` with seam-only evidence) | witnessable-gated (reads) + seam-gated (operator actions) | `compensation-replay-no-refire`, `compensation-effect-id-retry-stable` |
27
- | `idempotency` | Layer-2 effect identity keyed on business identity; the activity recipe is the fallback; `GET /runs/{runId}/effects` is the read | witnessable-gated (fixture provider) | `logical-effect-id-retry-stable` |
28
- | an `oauth2` or `oidc` lane | protected-resource metadata and challenges (identity.md §2.5) | witnessable-gated | `auth-challenge-no-oracle` |
21
+ | any lane in `auth.lanes[]` | the lane obligations (§Auth lanes) | unaided or seam-gated per lane | `sender-constraint-no-bearer-downgrade`; per-lane rows |
22
+ | both `saml` and `scim` lanes | the leaver contract (mandatory) | seam-gated | `subject-link-leaver-deny`, `subject-link-mandatory-when-both-advertised` |
23
+ | `replay` (any mode) | side-effect suppression (§Replay suppression) | witnessable-gated via the effect-seam manifest | `replay-fanout-no-refire`; effect-seam rows registered at Accepted |
24
+ | `webhooks` | durable delivery (§Webhook durability) | witnessable-gated (`webhook-signed-delivery` + dead-letter leg) | registered at Accepted |
25
+ | `interrupt` with `approversList` or `refKinds` | approver enforcement (§Approver enforcement) | witnessable-gated | registered at Accepted |
26
+ | `packs` (pack execution) | isolation (§Sandbox isolation) | witnessable-gated (eight `sandbox-*` scenarios) | `node-pack-sandbox-*` |
27
+ | `compensation` | plan, attempt and inverse-action obligations (§Compensation) | witnessable-gated (reads) + seam-gated (operator actions) | `compensation-replay-no-refire`, `compensation-effect-id-retry-stable` |
28
+ | `idempotency` | Layer-2 effect identity (§Layer-2 effect identity) | witnessable-gated (fixture provider) | `logical-effect-id-retry-stable` |
29
+ | an `oauth2` or `oidc` lane | protected-resource metadata and challenges ([identity.md §2.5](identity.md)) | witnessable-gated | `auth-challenge-no-oracle` |
29
30
  | any outbound request | no inbound credential on an onward hop (§Onward hops) | seam-gated | `inbound-credential-no-passthrough` |
30
31
 
31
32
  ### Auth lanes
32
33
 
33
- The obligations are identity.md §2 and bind on advertisement.
34
+ The obligations are [identity.md §2](identity.md) and bind on advertisement:
35
+
36
+ - the verify → bind → audience → resolve → fail-closed pipeline;
37
+ - a named trust root as `subject.issuer`;
38
+ - revocation for the lane;
39
+ - the advertised `minimumAssurance` floor, with `mtls.required` becoming `key-bound`;
40
+ - lane-scoped delegation proof.
34
41
 
35
42
  ### Replay suppression
36
43
 
37
- Stated in replay.md §Suppression and §"The effect-seam manifest"; the `replay-side-effect-suppression` scenario witnesses it.
44
+ Side-effect suppression with `recorded-outcome` semantics is the only conforming behavior; `none` is not a value. Stated in replay.md §Suppression and §"The effect-seam manifest"; the `replay-side-effect-suppression` scenario witnesses it.
38
45
 
39
46
  ### Webhook durability
40
47
 
41
- Stated in webhooks.md §Durability; the `webhook-durable-delivery` scenario witnesses it.
48
+ Durable delivery means retries per the advertised policy with backoff, dead-letter on exhaustion, and at-least-once delivery; best-effort is not a conforming delivery mode. Stated in webhooks.md §Durability; the `webhook-durable-delivery` scenario witnesses it.
42
49
 
43
50
  ### Approver enforcement
44
51
 
45
- Stated in interrupt.md §"Approver enforcement"; the `approver-enforced` scenario witnesses it.
52
+ A resolver not in the list, group, or role MUST be refused; `refKinds[]` stays a facet. Stated in interrupt.md §"Approver enforcement"; the `approver-enforced` scenario witnesses it.
46
53
 
47
54
  ### Sandbox isolation
48
55
 
49
- A host that executes third-party packs MUST enforce the eight `node-pack-sandbox-*` invariants of `SECURITY/invariants.yaml` (`no-process`, `network-gated`, `fs-gated`, `no-env`, `timeout`, `memory-cap`, `isolated-context`, `no-eval`) and MUST advertise `sandbox.isolationModel ∈ wasm | process | container | vm` (`spec/v2/facets/sandbox.schema.json`). `node:vm` is not a value. A host that cannot isolate MUST NOT execute third-party packs; it MAY register and validate them. The `no-eval` row stays reference-impl in `ext/sandbox-runtime-notes` (§D.1). The `pack-isolation` scenario drives the eight legs.
56
+ A host that executes third-party packs:
57
+
58
+ - MUST enforce the eight `node-pack-sandbox-*` invariants of `SECURITY/invariants.yaml`: `no-process`, `network-gated`, `fs-gated`, `no-env`, `timeout`, `memory-cap`, `isolated-context`, `no-eval`;
59
+ - MUST advertise `sandbox.isolationModel ∈ wasm | process | container | vm` (`spec/v2/facets/sandbox.schema.json`). `node:vm` is not a value. `sandbox.isolationModel` names the mechanism and never relaxes the property.
60
+
61
+ A host that cannot isolate MUST NOT execute third-party packs; it MAY register and validate them. The `no-eval` row stays reference-impl in `ext/sandbox-runtime-notes`. The `pack-isolation` scenario drives the eight legs.
50
62
 
51
63
  The remaining `sandbox` facets name the bound each invariant already carries: `allowedHostCalls` is the host-call allowlist `network-gated` and `fs-gated` enforce, `memoryLimitBytes` is the ceiling of `memory-cap`, and `wallClockLimitMs` is the ceiling of `timeout`. An advertised bound MUST be enforced; none of the three gates the invariant, which binds whether or not the facet is advertised.
52
64
 
53
65
  ### Compensation
54
66
 
55
- A host that advertises `compensation` MUST serve `GET /runs/{runId}/compensation` (`schemas/v2/compensation-projection.schema.json`), keyed on the node and attempt the operator family uses; a host that does not advertise `compensation` has no obligation.
67
+ A host that advertises `compensation` MUST serve `GET /runs/{runId}/compensation` (`schemas/v2/compensation-projection.schema.json`), keyed on the node and attempt the operator family uses. The read projection and the operator action family are the canonical wire. A host that does not advertise `compensation` has no obligation.
56
68
 
57
- The facets bind the policy shape (`schemas/v2/compensation-policy.schema.json`): `compensation.orderingModels` MUST list `reverse-completion` and MAY add `dependency-graph`, and a policy naming a model outside it MUST be refused at registration; `compensation.profileVersion` participates in the inverse-action identity, so a policy naming a different one MUST be refused; `compensation.manualIntervention` is the `manual` status above — a host advertising it records the unwind rather than abandoning it.
69
+ The facets bind the policy shape (`schemas/v2/compensation-policy.schema.json`):
70
+
71
+ - `compensation.orderingModels` MUST list `reverse-completion` and MAY add `dependency-graph`; a policy naming a model outside it MUST be refused at registration.
72
+ - `compensation.profileVersion` participates in the inverse-action identity, so a policy naming a different one MUST be refused.
73
+ - `compensation.manualIntervention` is the `manual` status — a host advertising it records the unwind rather than abandoning it.
58
74
 
59
75
  ### Layer-2 effect identity
60
76
 
61
- A host that advertises `idempotency` MUST serve `GET /runs/{runId}/effects`; the keying, provider-key, retention and projection rules are idempotency.md §"Layer 2: effect identity" (RFC 0150 §B).
77
+ Layer-2 effect identity is keyed on business identity; the activity recipe is the fallback. A host that advertises `idempotency` MUST serve `GET /runs/{runId}/effects`; the keying, provider-key, retention and projection rules are idempotency.md §"Layer 2: effect identity".
62
78
 
63
79
  ### Onward hops
64
80
 
65
- A host MUST NOT attach a credential it received inbound (an `Authorization`, `Cookie` or `Proxy-Authorization` value, a DPoP proof, an interrupt token, a peer's bearer, or credential material carried in a body) to any outbound request: A2A, MCP, webhook, callback, `httpClient` or connector. Outbound authentication uses only credentials the host holds for that destination (oauth.md). A verified delegation chain is not a passthrough: it carries provenance, never the inbound credential (identity.md §2.4). One credential is not inbound in this sense: an A2A push-config credential (`authentication.credentials`, `token`) is one the client gave the host for its registered push URL. A host MAY attach it only to a push delivery to that URL's origin, MUST NOT attach it after a redirect or to any other request, MUST discard it when the config is deleted or the task's final push is attempted, and MUST hold it by reference outside the event log, run state, debug bundle and every response (interop.md §"A2A push delivery", RFC 0214).
81
+ A host MUST NOT attach a credential it received inbound to any outbound request: A2A, MCP, webhook, callback, `httpClient` or connector. Inbound credentials are an `Authorization`, `Cookie` or `Proxy-Authorization` value, a DPoP proof, an interrupt token, a peer's bearer, or credential material carried in a body.
82
+
83
+ Outbound authentication uses only credentials the host holds for that destination (oauth.md). A verified delegation chain is not a passthrough: it carries provenance, never the inbound credential ([identity.md §2.4](identity.md)).
66
84
 
67
- A host advertising `purposePropagation` MUST re-emit a `permittedPurposes` label it received (A2A `metadata.openwop.permittedPurposes`, `TriggerEvent.permittedPurposes`) on every onward hop of the same data, narrowing and never widening, and MUST treat `[]` as no onward use; `purposePropagation.propagatesOnward` is `false` only on a host with no onward hop. The family advertises propagation, not enforcement.
85
+ One credential is not inbound in this sense: an A2A push-config credential (`authentication.credentials`, `token`) is one the client gave the host for its registered push URL ([interop.md §"A2A push delivery"](interop.md)). A host:
86
+
87
+ - MAY attach it only to a push delivery to that URL's origin;
88
+ - MUST NOT attach it after a redirect or to any other request;
89
+ - MUST discard it when the config is deleted or the task's final push is attempted;
90
+ - MUST hold it by reference outside the event log, run state, debug bundle and every response.
91
+
92
+ A host advertising `purposePropagation` MUST re-emit a `permittedPurposes` label it received (A2A `metadata.openwop.permittedPurposes`, `TriggerEvent.permittedPurposes`) on every onward hop of the same data, narrowing and never widening, and MUST treat `[]` as no onward use. `purposePropagation.propagatesOnward` is `false` only on a host with no onward hop. The family advertises propagation, not enforcement.
68
93
 
69
94
  ## Relaxations
70
95
 
71
- A relaxation, where one is legitimate — a development deployment, a single-tenant appliance — is an operator setting, never a discovery field (RFC 0173 §A.2). Every relaxation a host runs under MUST be recorded in its certification bundle as `host.relaxations[]` (`schemas/v2/certification-bundle.schema.json`): `{ obligation, durability, reason }`, `durability ∈ session | deployment | persisted`.
96
+ A relaxation, where one is legitimate — a development deployment, a single-tenant appliance — is an operator setting, never a discovery field. Every relaxation a host runs under MUST be recorded in its certification bundle as `host.relaxations[]` (`schemas/v2/certification-bundle.schema.json`): `{ obligation, durability, reason }`, `durability ∈ session | deployment | persisted`.
72
97
 
73
98
  | Durability | Meaning |
74
99
  | --- | --- |
@@ -80,7 +105,7 @@ A bundle that records a relaxation MUST NOT certify the profile the relaxed obli
80
105
 
81
106
  ## Three dispositions
82
107
 
83
- Every security obligation in `core/` is exactly one of (RFC 0173 §D.1):
108
+ Every security obligation in `core/` is exactly one of:
84
109
 
85
110
  | Disposition | Where | Requirement |
86
111
  | --- | --- | --- |
@@ -88,23 +113,22 @@ Every security obligation in `core/` is exactly one of (RFC 0173 §D.1):
88
113
  | extension | `spec/v2/ext/` | MUST declare a witness class and both maturity axes. |
89
114
  | removed | — | No text survives. |
90
115
 
91
- Operation ids in the declaration file are canonical, and aliases are migration
92
- register rows. The provider semantic-option registry is
93
- `spec/v2/ext/provider-idempotency/registry.json`; provider qualification uses a
94
- fixture that rejects a changed idempotency key (§D.2).
116
+ Operation ids in the declaration file are canonical, and aliases are migration register rows. The provider semantic-option registry is `spec/v2/ext/provider-idempotency/registry.json`; provider qualification uses a fixture that rejects a changed idempotency key.
95
117
 
96
118
  ## Threat models
97
119
 
98
- The following threat-model artifacts are required:
99
-
100
120
  | Artifact | Requirement |
101
121
  | --- | --- |
102
122
  | `SECURITY/threat-model-replay.md` §6 Residual risks | MUST record branch re-fires, seams outside the manifest, and the manifest as a self-declaration. |
103
- | `SECURITY/threat-model-replay.md` §7 Verification, §8 References | MUST name the manifest scenario and `fork-a-v1-run`; a threat model missing a sibling section fails the template gate. |
123
+ | `SECURITY/threat-model-replay.md` §7 Verification, §8 References | MUST name the manifest scenario and `fork-a-v1-run`. |
104
124
  | `SECURITY/threat-model-interop.md` | MUST cover downgrade, identity, and cross-tenant risks in protocol composition. |
105
125
 
126
+ A threat model missing a sibling section fails the template gate.
127
+
106
128
  ## Migration
107
129
 
108
130
  Rows `C6.1`–`C6.9` are `spec/v1/migrations.json` entries.
109
131
 
110
132
  See also: identity.md, replay.md, webhooks.md, capabilities.md, conformance.md.
133
+
134
+ *Sources: RFC 0150, RFC 0163, RFC 0164, RFC 0170, RFC 0173, RFC 0214.*
@@ -1,22 +1,45 @@
1
1
  # Tool catalog
2
2
 
3
- > **Status: Stable · RFC 0078, RFC 0112.**
3
+ > **Status: Stable.**
4
4
  > **Normative home:** `toolCatalog`.
5
5
 
6
6
  ## Why this exists
7
7
 
8
- v2 carried the catalog only by pointing at `spec/v1/tool-catalog.md`. This is its v2 contract, plus a projection onto MCP `ToolAnnotations` (RFC 0204). The shapes are `schemas/v2/tool-descriptor.schema.json` and `schemas/v2/compact-tool-descriptor.schema.json`.
8
+ A host's tool catalog lists the tools a caller may invoke, with a projection onto MCP `ToolAnnotations`. The shapes are `schemas/v2/tool-descriptor.schema.json` and `schemas/v2/compact-tool-descriptor.schema.json`.
9
9
 
10
10
  ## The catalog
11
11
 
12
- A host advertising `toolCatalog` MUST serve `GET /tools` (a `ToolDescriptor[]`) and `GET /tools/{toolId}`, both read-only. The list MUST hold only tools the caller may invoke in its tenant, and an unknown or unauthorized `toolId` MUST return `404`. `toolCatalog.sources` names the sources projected; a consumer MUST tolerate any subset. A host SHOULD return tools sorted by `toolId`, so an unchanged catalog reads identically.
12
+ A host advertising `toolCatalog` MUST serve `GET /tools` (a `ToolDescriptor[]`) and `GET /tools/{toolId}`, both read-only.
13
+
14
+ - The list MUST hold only tools the caller may invoke in its tenant.
15
+ - An unknown or unauthorized `toolId` MUST return `404`.
16
+ - `toolCatalog.sources` names the sources projected; a consumer MUST tolerate any subset.
17
+ - A host SHOULD return tools sorted by `toolId`, so an unchanged catalog reads identically.
13
18
 
14
19
  ## The descriptor
15
20
 
16
- `toolId` MUST be unique in the catalog and stable for a host version. `safetyTier: "exec"` MUST carry `source: "host-extension"`. A descriptor MUST NOT carry credential material. The host MUST assign `safetyTier`, `replayPolicy` and `egress` itself and MUST NOT copy them from an MCP server's `annotations`, which are untrusted; a `source: "mcp"` tool it has not classified MUST be `safetyTier: "write"`.
21
+ - `toolId` MUST be unique in the catalog and stable for a host version.
22
+ - `safetyTier: "exec"` MUST carry `source: "host-extension"`.
23
+ - A descriptor MUST NOT carry credential material.
24
+ - The host MUST assign `safetyTier`, `replayPolicy` and `egress` itself, and MUST NOT copy them from an MCP server's `annotations`, which are untrusted.
25
+ - A `source: "mcp"` tool the host has not classified MUST be `safetyTier: "write"`.
26
+
27
+ `annotations`, when present, MUST carry all four MCP hints, derived from those fields and not from MCP defaults (upstream, `destructiveHint` and `openWorldHint` default to `true`):
17
28
 
18
- `annotations`, when present, MUST carry all four MCP hints, derived from those fields and not from MCP defaults (`destructiveHint` and `openWorldHint` default to `true` upstream): `readOnlyHint` is true iff `safetyTier` is `pure` or `read`, `destructiveHint` iff it is `write` or `exec`, `idempotentHint` iff `replayPolicy` is `deterministic` or `idempotent`, and `openWorldHint` unless `egress` is `none`.
29
+ | Hint | True iff |
30
+ | --- | --- |
31
+ | `readOnlyHint` | `safetyTier` is `pure` or `read` |
32
+ | `destructiveHint` | `safetyTier` is `write` or `exec` |
33
+ | `idempotentHint` | `replayPolicy` is `deterministic` or `idempotent` |
34
+ | `openWorldHint` | `egress` is not `none` |
19
35
 
20
36
  ## Views and sessions
21
37
 
22
- A host advertising `toolCatalog.compactView` MUST answer `?view=compact` on both endpoints with `CompactToolDescriptor`s (the list as `{ tools: [...] }`) carrying the standard view's `toolId` set; a compact `inputSchema` MUST NOT use `$ref`, `oneOf`, `allOf`, `anyOf`, `not`, `patternProperties` or `dependentSchemas` at any depth. Any other `view`, or a host not advertising it, yields the standard view. A host advertising `toolCatalog.sessionLifecycle` MAY bracket calls with content-free `tool.session.opened` and `tool.session.closed`; a consumer MUST tolerate their absence.
38
+ A host advertising `toolCatalog.compactView` MUST answer `?view=compact` on both endpoints with `CompactToolDescriptor`s (the list as `{ tools: [...] }`) carrying the standard view's `toolId` set.
39
+
40
+ - A compact `inputSchema` MUST NOT use `$ref`, `oneOf`, `allOf`, `anyOf`, `not`, `patternProperties` or `dependentSchemas` at any depth.
41
+ - Any other `view`, or a host not advertising it, yields the standard view.
42
+
43
+ A host advertising `toolCatalog.sessionLifecycle` MAY bracket calls with content-free `tool.session.opened` and `tool.session.closed`; a consumer MUST tolerate their absence.
44
+
45
+ *Sources: RFC 0078, RFC 0112, RFC 0204.*