@openwop/spec-artifacts 2.44.4 → 2.44.6

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 (48) hide show
  1. package/CORPUS-STAMP.json +49 -47
  2. package/api/seams-v2.yaml +1 -1
  3. package/api/v2/asyncapi.yaml +1 -1
  4. package/api/v2/openapi.yaml +31 -122
  5. package/package.json +1 -1
  6. package/schemas/v2/agent-ref.schema.json +2 -3
  7. package/schemas/v2/budget-policy.schema.json +2 -3
  8. package/schemas/v2/capabilities.schema.json +42 -38
  9. package/schemas/v2/debug-bundle.schema.json +2 -2
  10. package/schemas/v2/error-envelope.schema.json +282 -4
  11. package/schemas/v2/node-pack-manifest.schema.json +2 -2
  12. package/schemas/v2/prompt-ref.schema.json +2 -3
  13. package/schemas/v2/prompt-template.schema.json +3 -3
  14. package/schemas/v2/run-event-payloads.schema.json +5 -5
  15. package/spec/v1/alias-detectors.json +25 -1
  16. package/spec/v1/core-standard-manifest.json +2 -2
  17. package/spec/v1/deprecations.json +89 -1
  18. package/spec/v1/gaps.json +42 -8
  19. package/spec/v1/migrations.json +57 -1
  20. package/spec/v2/core/artifact-type-packs.md +5 -1
  21. package/spec/v2/core/capabilities.md +20 -22
  22. package/spec/v2/core/conformance.md +24 -7
  23. package/spec/v2/core/errors.md +17 -119
  24. package/spec/v2/core/events.md +7 -7
  25. package/spec/v2/core/execution.md +56 -16
  26. package/spec/v2/core/host-services.md +57 -58
  27. package/spec/v2/core/i18n.md +10 -10
  28. package/spec/v2/core/idempotency.md +3 -3
  29. package/spec/v2/core/identity.md +11 -9
  30. package/spec/v2/core/interop.md +6 -8
  31. package/spec/v2/core/interrupt.md +8 -8
  32. package/spec/v2/core/oauth.md +4 -4
  33. package/spec/v2/core/packs.md +11 -5
  34. package/spec/v2/core/persistence.md +11 -13
  35. package/spec/v2/core/portability.md +4 -4
  36. package/spec/v2/core/replay.md +11 -13
  37. package/spec/v2/core/runs.md +20 -7
  38. package/spec/v2/core/security-defaults.md +0 -2
  39. package/spec/v2/core/storage.md +18 -20
  40. package/spec/v2/core/versioning.md +4 -9
  41. package/spec/v2/core/webhooks.md +13 -8
  42. package/spec/v2/declaration.json +3 -3
  43. package/spec/v2/errors.json +206 -2
  44. package/spec/v2/facets/agents.schema.json +367 -0
  45. package/spec/v2/facets/aiProviders.schema.json +2 -2
  46. package/spec/v2/generated/error-codes.md +130 -0
  47. package/spec/v2/release.json +2 -2
  48. package/spec/v2/surface-baseline.json +38 -2
@@ -5,7 +5,7 @@
5
5
 
6
6
  ## Why this exists
7
7
 
8
- `interrupt` is how a run waits for something outside itself: a decision, an answer, an event, a conversation turn. Every kind shares one payload shape, event pair, resolve contract and token scheme, so a client that resolves an approval can resolve anything.
8
+ `interrupt` is how a run waits for something outside itself: a decision, an answer, an event, a conversation turn. Every kind shares one payload shape, event pair, resolve contract and token scheme.
9
9
 
10
10
  ## Payload
11
11
 
@@ -35,7 +35,7 @@ Per-kind rules:
35
35
 
36
36
  ### Re-entry and resume values
37
37
 
38
- `key` is the deterministic re-entry key of one invocation. A host MUST derive it from at least the run, the node and the node's visit index: the number of that node's interrupts in this run whose resolution was consumed before this execution began. Its spelling is host-defined.
38
+ `key` is the deterministic re-entry key of one invocation. A host MUST derive it from at least the run, the node and the node's visit index: how many of that node's interrupts in this run had their resolution consumed before this execution began. Its spelling is host-defined.
39
39
 
40
40
  - A replay or recovery of the same execution MUST re-derive the same key.
41
41
  - A later execution of the node, reached over an edge, MUST derive a different key, MUST raise a new `interrupt.requested`, and MUST NOT return an earlier visit's `resumeValue`.
@@ -45,7 +45,7 @@ Per-kind rules:
45
45
  - An in-memory cache MAY serve in-process replays but MUST NOT replace the event log for cross-process replays.
46
46
  - A host MUST validate the resume value against `resumeSchema` when one is declared, and MUST refuse a failing value with `400 validation_error`.
47
47
 
48
- `timeoutMs`, when set, is the interrupt's own deadline. What an approval gate does when it elapses is §Rejection.
48
+ `timeoutMs`, when set, is the interrupt's own deadline; for an approval gate see §Rejection.
49
49
 
50
50
  ## Events
51
51
 
@@ -54,7 +54,7 @@ Every kind uses two registered types ([events.md](events.md)):
54
54
  - `interrupt.requested` — the payload is the `InterruptPayload` verbatim.
55
55
  - `interrupt.resolved` — the closed payload is `interruptResolved`. Resolving an approval-kind interrupt MUST record the applied `action` there, with the field §Approval requires.
56
56
 
57
- The kind-specific `approval.*` and `clarification.*` types remain registered. Their payload definitions in `schemas/v2/run-event-payloads.schema.json` are `$ref` aliases of `interruptRequested` and `interruptResolved`. A host emitting `interrupt.requested` MAY also emit the kind-specific type.
57
+ The kind-specific `approval.*` and `clarification.*` types remain registered; their payloads in `schemas/v2/run-event-payloads.schema.json` are `$ref` aliases of `interruptRequested` and `interruptResolved`. A host emitting `interrupt.requested` MAY also emit the kind-specific type.
58
58
 
59
59
  Both events are durable and appear in the `updates` and `debug` stream modes. While suspended, `RunSnapshot.currentNodeId` names the node and `status` is `waiting-approval`, `waiting-input` or `waiting-external`.
60
60
 
@@ -72,7 +72,7 @@ Both events are durable and appear in the `updates` and `debug` stream modes. Wh
72
72
 
73
73
  ### Callback delivery
74
74
 
75
- `createRun.callbackUrl` names where a host that advertises `interrupt.callbackDelivery: true` delivers notice of an interrupt, so its holder can resolve it through the token surface. The payload, timing and signing are host-defined.
75
+ `createRun.callbackUrl` names where a host advertising `interrupt.callbackDelivery: true` delivers notice of an interrupt, for resolution through the token surface. Payload, timing and signing are host-defined.
76
76
 
77
77
  A host advertising the facet:
78
78
 
@@ -98,7 +98,7 @@ The token grammar and the `interrupt.tokenAlgs[]` / `kid` check are [identity.md
98
98
 
99
99
  - **Expiry.** Every token MUST carry `expiresAt`. The default SHOULD be 30 minutes, and a host MUST cap the lifetime at the interrupt's `timeoutMs` when one exists. A token MUST NOT outlive the interrupt it resolves; past `expiresAt` the host MUST answer `410 interrupt_expired`.
100
100
  - **Invalidation.** A token MUST be invalidated when its interrupt is resolved or its run is cancelled or completed. Later use MUST answer `409 interrupt_already_resolved`.
101
- - **Verification.** MAC comparison MUST be constant-time. `kid` selects the verification secret, so secrets rotate without orphaning outstanding tokens.
101
+ - **Verification.** MAC comparison MUST be constant-time. `kid` selects the verification secret, so secrets rotate without orphaning tokens.
102
102
  - **Intent.** A token minted with `intent: resolve` authorizes both operations. A host MAY mint `intent: inspect` tokens; a resolve with one MUST be refused with `403`.
103
103
 
104
104
  ## Approval
@@ -141,9 +141,9 @@ The facet `spec/v2/facets/interrupt.schema.json` carries `tokenAlgs[]` (REQUIRED
141
141
  - **`audience`** is a notification hint, never eligibility. When omitted, the host SHOULD notify the union of the eligibility refs.
142
142
  - A host that does not advertise a ref kind MUST ignore that field.
143
143
 
144
- Eligibility binds every writer of the suspension record, not every route. A host whose durable store is writable by a principal other than the engine MUST enforce the same eligibility at the store, or MUST NOT expose the record to that principal for write.
144
+ Eligibility binds every writer of the suspension record. A host whose durable store is writable by a principal other than the engine MUST enforce the same eligibility at the store, or MUST NOT expose the record to that principal for write.
145
145
 
146
- Refs are opaque to the engine; the host resolves them. Membership MUST be resolved at decision time and MUST NOT be re-resolved during replay or `forkRun`: the recorded eligibility decision is fixed history ([replay.md](replay.md)).
146
+ Refs are opaque to the engine. Membership MUST be resolved at decision time and MUST NOT be re-resolved during replay or `forkRun`: the recorded eligibility decision is fixed history ([replay.md](replay.md)).
147
147
 
148
148
  ## During the v1 overlap
149
149
 
@@ -5,11 +5,11 @@
5
5
 
6
6
  ## Why this exists
7
7
 
8
- A connector node needs a token a user granted to a third party. The host obtains, stores and refreshes it and hands it to the node's sandbox, so a pack names a provider and scopes and never touches the grant. Here the host is an OAuth client; [identity.md](identity.md) covers the host as a protected resource.
8
+ A connector node needs a token a user granted to a third party. The host obtains, stores and refreshes it and hands it to the node's sandbox; a pack names a provider and scopes and never touches the grant. Here the host is an OAuth client; [identity.md](identity.md) covers it as a protected resource.
9
9
 
10
10
  ## Credentials
11
11
 
12
- A host advertising `credentials` MUST resolve a `{ ref, scope }` reference (`schemas/v2/credential-reference.schema.json`) at node execution and inject the material into the node sandbox only.
12
+ A host advertising `credentials` MUST resolve a `{ ref, scope }` reference (`schemas/v2/credential-reference.schema.json`) at node execution and inject the material only into the node sandbox.
13
13
 
14
14
  - The material MUST NOT appear in inputs, variables, events, the debug bundle or replay state (invariant `credential-payload-redaction`).
15
15
  - A failed resolution is `credential_not_found`, `credential_forbidden` (outside the caller's scope; fail closed) or `credential_scope_unsupported` (a scope not in `credentials.scopes`).
@@ -46,7 +46,7 @@ Where the provider is reached as an MCP server:
46
46
  - It fetches the server's Protected Resource Metadata (RFC 9728) only from URLs derived from the manifest's server URL.
47
47
  - Discovery verifies and never selects. A discovered issuer or endpoint that differs from the manifest's, or from the tuple pinned at registration, MUST be refused `connection_auth_metadata_mismatch`. A grant for such a provider whose manifest declares no `issuer` MUST be refused the same way.
48
48
 
49
- This binds a provider the host configures itself as well as one a connection pack defines: the configured server URL and issuer stand in for the manifest's, and the tuple is pinned when the host loads that configuration.
49
+ This binds a host-configured provider as well as a connection-pack one: the configured server URL and issuer stand in for the manifest's, pinned when the host loads that configuration.
50
50
 
51
51
  ## The credential interrupt
52
52
 
@@ -61,6 +61,6 @@ Then:
61
61
  - `connectUrl` MUST be host-owned, MUST NOT be pre-authenticated, and MUST complete only for the initiating Subject.
62
62
  - The host resolves the interrupt when the grant completes. A resolve of `authorized` MUST be refused `400 validation_error` unless a credential now resolves.
63
63
  - `declined` fails the node with `connector_auth_declined`.
64
- - A host that binds a node to one credential reference (for example, a connection) reads "resolves" as that reference resolving with the node's scopes, carries it as `credentialRef`, and still completes `connectUrl` only for the initiating Subject.
64
+ - A host that binds a node to one credential reference (such as a connection) reads "resolves" as that reference resolving with the node's scopes, carries it as `credentialRef`, and still completes `connectUrl` only for the initiating Subject.
65
65
 
66
66
  *Sources: RFCs 0046, 0047, 0199.*
@@ -14,7 +14,7 @@ A manifest's `engines.openwop` MUST match the grammar in `schemas/v2/node-pack-m
14
14
  - A v2 host MUST treat a range with no upper bound as bounded by `<2.0.0`.
15
15
  - A host MUST refuse to install a version whose range does not admit the host's protocol major with `pack_engine_unsupported` (`spec/v2/errors.json`).
16
16
  - `pack_runtime_requirement_unmet` remains a runtime-requirement code and MUST NOT be used for the protocol major.
17
- - Both checks MUST run at install on every publication path — the canonical registry, a vendor registry's write API, and a mirror ingest — so no registry-side artifact can bypass it.
17
+ - Both checks MUST run at install on every publication path — the canonical registry, a vendor registry's write API, and a mirror ingest.
18
18
 
19
19
  The range is a claim about the pack's own surface, not about run semantics. Admitting major M asserts that:
20
20
 
@@ -37,7 +37,7 @@ A host advertises `packs` when it serves the registry surface above:
37
37
 
38
38
  The registry is versioned by tree, not header. It publishes `registry/v2/packs/<name>/-/<version>.{json,sbom.json,sig,tgz}` as a parallel tree of re-signed manifests with regenerated SBOMs and index.
39
39
 
40
- - A signed compatibility overlay MUST be rejected: signatures authorize by namespace, and a mirror re-derives the signer at ingest.
40
+ - A signed compatibility overlay MUST be rejected; a mirror re-derives the signer at ingest.
41
41
  - `.well-known/openwop-registry.json` `endpoints` is the negotiation: it names both trees, and a client MUST resolve every registry path through it rather than construct one.
42
42
  - `publicKey` is unversioned: keys are not protocol-versioned.
43
43
 
@@ -85,7 +85,7 @@ There is one signing scheme. `signing` on a version manifest (`schemas/v2/regist
85
85
  - **`keyId`** is the signing key id.
86
86
  - A block carrying `method`, `publicKeyRef` or `signatureRef` fails validation.
87
87
 
88
- The same block applies to a **bare manifest** — the `pack.json` inside the tarball, which is the document the signature covers. `signing` is OPTIONAL there, because an authoring-time `pack.json` exists before it is signed. When present it MUST be the identical closed `{ keyId, scheme }` object, on every bare manifest kind.
88
+ The same block applies to a **bare manifest** — the `pack.json` inside the tarball, which is the document the signature covers. `signing` is OPTIONAL there; when present it MUST be the identical closed `{ keyId, scheme }` object, on every bare manifest kind.
89
89
 
90
90
  - A verifier MUST verify the signature against the issuing registry's key for `keyId`, and MUST check the pack name against that key's `permittedNamespaces`.
91
91
  - A signature over tarball bytes is not a v2 signature; such a pack MUST be re-signed, not relabeled.
@@ -103,7 +103,7 @@ A registry MUST refuse a submission that breaks these rules or republishes a ver
103
103
 
104
104
  ## The registry's own schemas
105
105
 
106
- A registry MUST validate submissions against vendored copies of these schemas pinned to a corpus tag, and MUST re-sync them from that tag before any v2 publication. An unpinned or drifted vendored schema is a registry defect: it rejects documents the protocol requires the registry to accept.
106
+ A registry MUST validate submissions against vendored copies of these schemas pinned to a corpus tag, and MUST re-sync them from that tag before any v2 publication. An unpinned or drifted vendored schema is a registry defect.
107
107
 
108
108
  ## Errors
109
109
 
@@ -126,7 +126,13 @@ A `frontend-plugin` pack (`schemas/v2/frontend-plugin-manifest.schema.json`) shi
126
126
 
127
127
  A host without `uiPlugins` MUST reject the pack and render its own way.
128
128
 
129
- A breaking method change is `ui-plugin/2`. `host.announce` MUST be length-capped (SHOULD ≤ 400 characters) and SHOULD be rate-limited; `host.documentChanged` SHOULD be debounced, and a plugin MUST treat the latest as authoritative and tolerate unknown `host.selectionChanged` kinds. `maxEntryBytes` caps an entry bundle.
129
+ On the message protocol:
130
+
131
+ - A breaking method change is `ui-plugin/2`.
132
+ - `host.announce` MUST be length-capped (SHOULD ≤ 400 characters) and SHOULD be rate-limited.
133
+ - `host.documentChanged` SHOULD be debounced. A plugin MUST treat the latest one as authoritative and tolerate unknown `host.selectionChanged` kinds.
134
+
135
+ `maxEntryBytes` caps an entry bundle.
130
136
 
131
137
  ## During the v1 overlap
132
138
 
@@ -14,7 +14,7 @@ How a v2 host reads what a v1 host wrote, what happens to a run in flight at the
14
14
  - A host MUST NOT carry a private mapping.
15
15
  - A vendor-prefixed v1 type the codemap does not name MUST be read under its own name, unchanged.
16
16
  - "Vendor-prefixed" means the first segment is an org registered in the `extensions` object of `spec/v2/declaration.json` ([events.md §Rules](events.md)). `openwop.` is the only reserved prefix. An unregistered first segment is not a vendor prefix and falls to the refusal in §"The reader rule".
17
- - An org is registered by pull request against the corpus and takes effect on the `@openwop/spec-artifacts` release that carries it. A shipped entry is append-only: deregistering an org would turn every log already written under it from pass-through into refusal.
17
+ - An org is registered by pull request against the corpus and takes effect on the `@openwop/spec-artifacts` release that carries it. A shipped entry is append-only.
18
18
 
19
19
  ## The era key
20
20
 
@@ -32,9 +32,9 @@ Discovery MUST advertise the value the host writes for new runs and nothing else
32
32
  Absent stays era `2` forever; it is never backfilled.
33
33
 
34
34
  - A host MUST NOT rewrite historical rows to add an explicit `2`, and a reader MUST NOT require one.
35
- - A host with more than one creation path MUST begin stamping `3` on all of them in the same change. An unstamped path's runs read as era `2` — a silent wrong read.
35
+ - A host with more than one creation path MUST begin stamping `3` on all of them in the same change. An unstamped path's runs read as era `2`.
36
36
 
37
- The snapshot field is required on the wire, and MAY be synthesized: for an era-`2` run with nothing stored, the host MUST supply `2` from the absent-⇒-`2` rule rather than fail the read. The field alone therefore cannot falsify era handling; the reader and writer rules carry the obligation.
37
+ The snapshot field is required on the wire, and MAY be synthesized: for an era-`2` run with nothing stored, the host MUST supply `2` from the absent-⇒-`2` rule rather than fail the read.
38
38
 
39
39
  ## The `eventLog` family
40
40
 
@@ -44,7 +44,7 @@ The snapshot field is required on the wire, and MAY be synthesized: for an era-`
44
44
  - MUST serve the cursor contract of [events.md §"Poll"](events.md) over that log;
45
45
  - MUST NOT emit an event `type` the codemap does not name.
46
46
 
47
- The record carries no separate storage claim: it asserts that the era key, the codemap and the poll cursor are implemented as written here. Its `crossEngineOrdering` facet is a replay property, specified in [replay.md §"Cross-engine ordering"](replay.md).
47
+ The record makes no separate storage claim. Its `crossEngineOrdering` facet is a replay property, specified in [replay.md §"Cross-engine ordering"](replay.md).
48
48
 
49
49
  ## The reader rule
50
50
 
@@ -58,16 +58,16 @@ A v2 host reading a run in era `2` MUST translate every event through the codema
58
58
 
59
59
  The rule binds every reader: poll, SSE, fork, replay divergence, debug bundle, summary memory.
60
60
 
61
- The translation is a read projection. A host MUST NOT rewrite era-`2` rows in place. The one exception is a background backfill that stamps `3` and rewrites `type` under the same `(runId, sequence)` key: it is permitted only as an atomic per-run operation with the original preserved, because the fork prefix must stay byte-equivalent to the translated parent ([replay.md](replay.md)).
61
+ The translation is a read projection. A host MUST NOT rewrite era-`2` rows in place. The one exception is a background backfill that stamps `3` and rewrites `type` under the same `(runId, sequence)` key: it is permitted only as an atomic per-run operation with the original preserved, so a fork prefix stays byte-equivalent ([replay.md](replay.md)).
62
62
 
63
63
  ### The writer rule
64
64
 
65
65
  The era key is fixed when the run is created and fixes the log's vocabulary for the run's lifetime.
66
66
 
67
67
  - An append to a run in era `2` MUST use v1 vocabulary — the name the codemap maps *from*, not the v2 name it maps to.
68
- - A host that upgrades mid-flight MUST NOT begin writing v2 names into a log the reader translates as era `2`. The reader would map an already-mapped name a second time, or fail with `event_type_unmapped` on a name absent from the codemap's v1 side.
68
+ - A host that upgrades mid-flight MUST NOT begin writing v2 names into a log the reader translates as era `2`.
69
69
  - A run created after the upgrade is era `3` and is written in v2 vocabulary, untranslated.
70
- - A writer that emits a property a closed def cannot seat MUST mark the row with what it could not seat, so the refusal names the writer instead of surfacing as an unexplained read failure.
70
+ - A writer that emits a property a closed def cannot seat MUST mark the row with what it could not seat, so the refusal names the writer.
71
71
 
72
72
  This binds every writer for as long as an era-`2` run stays open (§"Runs pinned to v1"). Its witness is `v2-era-2-append-vocabulary`.
73
73
 
@@ -76,7 +76,7 @@ This binds every writer for as long as an era-`2` run stays open (§"Runs pinned
76
76
  Through the overlap ([versioning.md §5](versioning.md)), an era-`3` log, stored in v2 vocabulary, must still read on `/v1/…` exactly as before the cut. A host serving both majors:
77
77
 
78
78
  - MUST map an era-`3` log's `type` back to its v1 spelling on the v1 read path, through the same codemap row, inverted.
79
- - MUST verify at load that `spec/v2/event-codemap.json` is a bijection, since only then is the inverse exact. If a row folds two v1 names onto one v2 name, the host MUST refuse to serve the v1 representation rather than guess a spelling.
79
+ - MUST verify at load that `spec/v2/event-codemap.json` is a bijection. If a row folds two v1 names onto one v2 name, the host MUST refuse to serve the v1 representation rather than guess a spelling.
80
80
  - MUST emit a type with no codemap row (v2-only vocabulary, which has no v1 spelling) unchanged on the v1 read path, MUST NOT drop the row, and MUST NOT refuse the read for it.
81
81
 
82
82
  ### The seat
@@ -84,7 +84,7 @@ Through the overlap ([versioning.md §5](versioning.md)), an era-`3` log, stored
84
84
  - The adapter MUST sit at the storage boundary every reader passes through — the storage interface's event-list method, not a wrapper some call sites bypass.
85
85
  - A host leg MUST name its seat in its ADR.
86
86
 
87
- The seat is a claims-check ([conformance.md §Witness class](conformance.md)): it is discharged by that disclosure and by audit, never by the wire. It binds every reader, including ones the suite has no name for; `run-event.schema.json` records why passing legs do not discharge it.
87
+ The seat is a claims-check ([conformance.md §Witness class](conformance.md)): it is discharged by that disclosure and by audit, never by the wire. It binds every reader, including ones the suite has no name for.
88
88
 
89
89
  ## Runs pinned to v1
90
90
 
@@ -100,9 +100,9 @@ Multi-region skew is read-side only: after the cut, a v2 region MUST NOT accept
100
100
 
101
101
  - **Certification bundles** — never upgraded. A v1 bundle substantiates no new certification after 2026-11-10; every host produces a fresh v2-rc bundle before the cut ([conformance.md](conformance.md)).
102
102
  - **Webhook deliveries** — dual-emitted, and queued deliveries drained, per [webhooks.md §"Dual emission through the overlap"](webhooks.md).
103
- - **Interrupt resume tokens** — drained. A token that is not `ow2.`-prefixed is a v1 token and resolves under `kid: legacy` until `expiresAt`; new tokens carry the `ow2.` prefix ([identity.md](identity.md)). The rule is written over the prefix, never a segment count.
103
+ - **Interrupt resume tokens** — drained, per [identity.md](identity.md) §4.
104
104
  - **Layer-1 and Layer-2 records** (idempotency, idempotent responses, invocation claims and logs, effect-escape ledger, dispatch outbox, envelope correlations) — unchanged; keyed on ids the cut does not rename. `GET /runs/{runId}/effects` and `GET /runs/{runId}/compensation` are new reads over them ([security-defaults.md](security-defaults.md)).
105
- - **Owner stamps** — a run without a Subject MUST be legacy-stamped at first v2 read and MUST NOT be rewritten later. A host's stored owner fields are projected to the Subject; the projection is the host's to name.
105
+ - **Owner stamps** — a run without a Subject is legacy-stamped per [identity.md](identity.md) §1.2. A host's stored owner fields are projected to the Subject; the projection is the host's to name.
106
106
  - **Audit log** — never upgraded.
107
107
 
108
108
  ## Per-store disposition
@@ -141,6 +141,4 @@ Template — one row per store:
141
141
 
142
142
  A host MAY claim a qualification rung (`durable-single-instance`, `durable-multi-instance`, `multi-region-qualified`; cumulative) only with the evidence [RFC 0158 §D](https://github.com/openwop/openwop/blob/main/RFCS/0158-durable-execution-and-disaster-recovery-qualification.md) names for it, and MUST NOT claim one from tests in which no process was terminated. A rung is evidence, not a capability: discovery carries none, and the certification bundle publishes the rung and its bounds ([conformance.md §Bundle v3](conformance.md)).
143
143
 
144
- See also: [overview.md](overview.md), [events.md](events.md), [replay.md](replay.md), [identity.md](identity.md), [webhooks.md](webhooks.md).
145
-
146
144
  *Sources: RFCs 0041, 0158, 0170, 0171, 0172, 0176, 0180, 0185, 0186, 0187.*
@@ -5,7 +5,7 @@
5
5
 
6
6
  ## Why this exists
7
7
 
8
- A tenant's reusable estate moves between hosts as one export bundle, and lands on the destination under the importing caller's identity. This document states the bundle and the rules an importer follows. An adapter that turns another platform's export into a bundle is host tooling.
8
+ A tenant's reusable estate moves between hosts as one export bundle, and lands on the destination under the importing caller's identity. An adapter that turns another platform's export into a bundle is host tooling.
9
9
 
10
10
  ## The export bundle
11
11
 
@@ -24,8 +24,8 @@ An `ExportBundle` (`schemas/v2/export-bundle.schema.json`) carries `bundleVersio
24
24
 
25
25
  ## Import rules
26
26
 
27
- - **No credential material.** A bundle MUST NOT contain credential values: a `connection-ref` item carries only references and provider ids. The importer MUST report unbound references in `secretsToRebind` and MUST NOT invent or transfer secret material. A host MUST reject with `422` an imported bundle whose payload carries a literal credential value.
28
- - **Dry run.** When `import` is advertised, import MUST offer a dry run. A dry run MUST NOT write, and MUST return the plan it would execute: creates, updates, skips, conflicts and unbound credential references.
27
+ - **No credential material.** A bundle MUST NOT contain credential values: a `connection-ref` item carries only references and provider ids. A host MUST reject with `422` an imported bundle whose payload carries a literal credential value. The importer MUST report unbound references in `secretsToRebind` and MUST NOT invent or transfer secret material.
28
+ - **Dry run.** When `import` is advertised, import MUST offer a dry run, which MUST NOT write and MUST return the plan it would execute: creates, updates, skips, conflicts and unbound credential references.
29
29
  - **Idempotent.** Import MUST be idempotent: re-applying a bundle resolves each item to `skipped` or `updated`, never to a duplicate create.
30
30
  - **Ordered.** Items MUST be applied in `dependsOn` topological order. A cycle is a `422`.
31
31
  - **Re-owned.** Every imported entity MUST be re-owned to the caller's Subject at the destination ([identity.md](identity.md) §1). `source.originPrincipal` is informational and MUST NOT grant any access. Host-scoped handles are re-minted as [identity.md](identity.md) §5 requires.
@@ -39,6 +39,6 @@ An applied import emits `import.applied` (`schemas/v2/run-event-payloads.schema.
39
39
 
40
40
  ## Routes
41
41
 
42
- No protocol path is defined for export or import: a host serves them on routes of its own. A host-private migration from an anonymous sandbox into a signed-in tenant fits this contract, because its response is a subset of the import outcome.
42
+ No protocol path is defined for export or import: a host serves them on routes of its own. A host-private migration from an anonymous sandbox into a signed-in tenant fits this contract.
43
43
 
44
44
  *Sources: RFCs 0043, 0096, 0098.*
@@ -5,11 +5,11 @@
5
5
 
6
6
  ## Why this exists
7
7
 
8
- `POST /runs/{runId}:fork` makes any past state of a run re-executable: a replay proves that current code reproduces recorded history; a branch explores an alternative from a recorded point. This document states what a fork MUST reproduce, what it MUST NOT re-fire, and how a host proves the second.
8
+ `POST /runs/{runId}:fork` makes any past run state re-executable: a replay proves current code reproduces recorded history; a branch explores an alternative from a recorded point. This document states what a fork MUST reproduce, what it MUST NOT re-fire, and how a host proves the second.
9
9
 
10
10
  ## The surface
11
11
 
12
- A host that advertises `replay` ([capabilities.md](capabilities.md)) serves `forkRun` (`api/v2/openapi.yaml`, `POST /runs/{runId}:fork`) and `getEffectSeamManifest` (`GET /host/effect-seams`).
12
+ A host advertising `replay` ([capabilities.md](capabilities.md)) serves `forkRun` (`api/v2/openapi.yaml`, `POST /runs/{runId}:fork`) and `getEffectSeamManifest` (`GET /host/effect-seams`).
13
13
 
14
14
  The `replay` facet (`spec/v2/facets/replay.schema.json`) is `{ modes[], retention?, effectSeamsManifest }`:
15
15
 
@@ -46,9 +46,9 @@ A host MUST cache the observable result (return value, workflow-state effects, e
46
46
  ## Determinism caveats (`replay` mode)
47
47
 
48
48
  1. A side-effecting node MUST NOT call the external system twice; see §Suppression.
49
- 2. `ctx.interrupt(K)` MUST short-circuit to the persisted `interrupt.resolved` value.
49
+ 2. `ctx.interrupt(K)` MUST short-circuit to the persisted `interrupt.resolved`, raising no new `interrupt.requested`.
50
50
  3. `ctx.getVersion` pins from the source run are fixed history; the replay MUST take the recorded branch.
51
- 4. Nodes MUST consume time via `ctx.now()` where available; direct clock reads make replay non-deterministic.
51
+ 4. Nodes MUST consume time via `ctx.now()` where available; direct clock reads are non-deterministic.
52
52
  5. Recorded-fact events such as `memory.written` are fixed history. A replay MUST re-emit them verbatim from the log and MUST NOT regenerate their identifiers or timestamps — never a new `memoryId`. A `branch` MAY perform its own memory writes with fresh identifiers.
53
53
  6. Approver eligibility recorded on a resume event is fixed history; a host MUST NOT re-resolve membership during replay.
54
54
 
@@ -80,11 +80,11 @@ For a fork with `mode: replay`:
80
80
  5. The guarantee is whole-run and requires both classification before execution and a default-deny guard at every effect seam.
81
81
  6. A dispatch to a peer host is an outbound call under rule 2; the peer is never contacted.
82
82
 
83
- Pure nodes and LLM calls served from the invocation log MUST re-execute live; otherwise divergence detection is vacuous.
83
+ Pure nodes and LLM calls served from the invocation log MUST re-execute live.
84
84
 
85
85
  **Fan-out.** A host that projects its log outward — webhook delivery, A2A push, outbound streams, analytics or audit sinks — MUST NOT deliver events a replay re-emits as fixed history, and a fork of either mode MUST NOT inherit its source's A2A push configs. Replay-ness MUST be read from the run, never from the event type; the fork's own log MUST still carry the re-emitted events ([webhooks.md](webhooks.md)).
86
86
 
87
- **Branch.** A branch re-fires effects for sequences `>= fromSeq`; those are effects the operator asked for. A host MAY suppress branch effects and MUST NOT report that as replay suppression. A host SHOULD surface the re-fire in operator-facing fork UI.
87
+ **Branch.** A branch re-fires effects for sequences `>= fromSeq`. A host MAY suppress branch effects and MUST NOT report that as replay suppression. A host SHOULD surface the re-fire in operator-facing fork UI.
88
88
 
89
89
  ### The effect-seam manifest
90
90
 
@@ -95,7 +95,7 @@ A host advertising `replay` MUST publish `schemas/v2/effect-seam-manifest.schema
95
95
  - Two seams a host guards through one code path but that leave by different mechanisms are different `kind`s; two that leave by the same mechanism for different business reasons are one.
96
96
  - `other` is the escape for a mechanism this list does not name — raw TCP, gRPC, a filesystem write, a device SDK. A row using it MUST carry `note` naming that mechanism.
97
97
 
98
- **Completeness outranks driveability.** Every outbound effect path the node runtime can reach MUST be listed, including one the suite cannot drive (typically `smtp` or `other`); the scenario records that one `inapplicable`, naming the mechanism. A host MUST NOT omit a seam because the suite cannot drive it, and MUST NOT relabel it as a `kind` the suite can drive. The manifest is a self-declaration whose false negatives are found by audit, not a witness.
98
+ **Completeness outranks driveability.** Every outbound effect path the node runtime can reach MUST be listed, including one the suite cannot drive (typically `smtp` or `other`); the scenario records that one `inapplicable`, naming the mechanism. A host MUST NOT omit a seam because the suite cannot drive it, and MUST NOT relabel it as a `kind` the suite can drive.
99
99
 
100
100
  ## Replay-from-event-log internals
101
101
 
@@ -111,7 +111,7 @@ A v2 host MUST fork a run created before the cut (era `2`, [persistence.md](pers
111
111
 
112
112
  - The fork's prefix MUST be byte-equivalent to the *translated* parent — the parent as read through the codemap, not its stored bytes.
113
113
  - `run.started` on the fork MUST carry the legacy Subject (`issuer: urn:openwop:legacy`, [identity.md](identity.md)) where the parent had none.
114
- - A backfill of an era-`2` log is permitted only atomically per run with the original preserved, so this obligation stays checkable.
114
+ - A backfill of an era-`2` log is permitted only atomically per run with the original preserved.
115
115
 
116
116
  ## Cross-engine ordering
117
117
 
@@ -120,16 +120,14 @@ When a host advertises both `idempotency.multiRegion` and `eventLog.crossEngineO
120
120
  - `status`, `variables`, and the projected event log up to `fromSeq` MUST be byte-equivalent across regions.
121
121
  - Per-region wall-clock and entropy fields in events after the boundary MAY differ.
122
122
 
123
- A host that advertises only one of the two keeps the single-region contract above; a host that advertises neither is single-region and the cross-region claim does not apply.
123
+ A host advertising only one of the two, or neither, keeps the single-region contract.
124
124
 
125
125
  ## Retention
126
126
 
127
- A host advertising `replay` MUST document retention for source snapshots, source logs, the invocation records replay depends on, and forked runs; `retention.days` MAY advertise the window. When the range `fromSeq` needs has expired, the host MUST reject the fork with `410` or `422`; `details` SHOULD carry `sourceRunId`, `fromSeq`, and the boundary.
127
+ A host advertising `replay` MUST document retention for source snapshots, source logs, the invocation records replay depends on, and forked runs; `retention.days` MAY advertise the window. When the range `fromSeq` needs has expired, the host MUST reject the fork with `410 run_expired` or `422`; `details` SHOULD carry `sourceRunId`, `fromSeq`, and the boundary.
128
128
 
129
129
  ## Declared nondeterminism
130
130
 
131
131
  `nondeterminismPolicy` is the host's statement of which nondeterministic sources it declares rather than suppresses. A host advertising `nondeterminismPolicy.declared` MUST record every declared source in the run's event log at the point it is read, so a fork replays the recorded value rather than re-drawing it. A source the host neither declares nor suppresses is a replay defect, not a policy choice.
132
132
 
133
- See also: events.md, runs.md, persistence.md, security-defaults.md.
134
-
135
- *Sources: RFCs 0036, 0039, 0041, 0057, 0104, 0111, 0140, 0173, 0176, 0194.*
133
+ *Sources: RFCs 0036, 0039, 0041, 0057, 0104, 0111, 0140, 0173, 0176, 0194, 0228.*
@@ -1,7 +1,7 @@
1
1
  # Runs
2
2
 
3
3
  > **Status: Stable.**
4
- > **Normative home:** `runList`, `limits`, `conversationPrimitive`, `dataResidency`, `deadLetter`.
4
+ > **Normative home:** `runList`, `limits`, `conversationPrimitive`, `dataResidency`, `deadLetter`, `budget`.
5
5
 
6
6
  ## Why this exists
7
7
 
@@ -38,7 +38,7 @@ Every operation accepts `OpenWOP-Version` ([overview.md](overview.md)) and every
38
38
  | `getRunCompensation` | `GET /runs/{runId}/compensation` | `runs:read` | `compensation` |
39
39
  | `getRunEffects` | `GET /runs/{runId}/effects` | `runs:read` | `idempotency` |
40
40
 
41
- A gated operation the host does not advertise (or an absent `diffRun`) answers `404`; for annotations it is `404 not_found`.
41
+ A gated operation the host does not advertise (or an absent `diffRun`) answers `404 not_found` ([errors.md](errors.md)).
42
42
 
43
43
  ## Create
44
44
 
@@ -67,7 +67,7 @@ The `201` response is `{ runId, status, eventsUrl, statusUrl? }`. `status` is on
67
67
 
68
68
  ### Refusals
69
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.
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 `422 capability_not_provided`.
71
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
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
73
 
@@ -99,9 +99,12 @@ The `201` response is `{ runId, status, eventsUrl, statusUrl? }`. `status` is on
99
99
 
100
100
  ### Limits
101
101
 
102
- `limits` always carries `clarificationRounds` (per task), `schemaRounds` (per envelope) and `envelopesPerTurn` (per chat turn). A host advertising `maxNodeExecutions`, `maxRunDurationMs` or `maxLoopIterations` MUST enforce it. On any breach the host MUST emit `cap.breached`, with `nodeId` for `clarification` and `schema`, and fail the node or run. Its `observed` exceeds `limit` and MUST be reused on replay and fork, never recomputed.
102
+ `limits` always carries `clarificationRounds` (per task), `schemaRounds` (per envelope) and `envelopesPerTurn` (per chat turn).
103
103
 
104
- `budget.maxTokens` and `budget.maxCostUsd` clamp to `maxBudgetTokens` and `maxBudgetCostUsd`. `maxRequestBodyBytes` is the largest REST request body accepted.
104
+ - A host advertising `maxNodeExecutions`, `maxRunDurationMs` or `maxLoopIterations` MUST enforce it.
105
+ - On any breach the host MUST emit `cap.breached` and fail the node or run. The event carries `nodeId` for a `clarification` or `schema` breach. Its `observed` exceeds `limit` and MUST be reused on replay and fork, never recomputed.
106
+ - `budget.maxTokens` and `budget.maxCostUsd` clamp to `maxBudgetTokens` and `maxBudgetCostUsd`.
107
+ - `maxRequestBodyBytes` is the largest REST request body accepted.
105
108
 
106
109
  ### `ai` section
107
110
 
@@ -114,6 +117,16 @@ The `201` response is `{ runId, status, eventsUrl, statusUrl? }`. `status` is on
114
117
 
115
118
  `tokenBudget` resolves to `min(tokenBudget, memory.distillation.maxTokenBudget)`. A run that cannot distill within it MUST fail atomically with `token_budget_exceeded`.
116
119
 
120
+ ### `budget` section
121
+
122
+ `budget` caps a run's spend. The effective budget is the minimum across the `scopes` that apply (run, workflow, agent, project), clamped per §Limits; only the run scope has a wire surface. Whichever of `budget` and the `run` section binds first fires its own `cap.breached` kind.
123
+
124
+ - `budget.reserved` records the effective budget, and consumption is derived from `provider.usage`, `agent.toolCalled` and `node.retried`, never measured twice. A replay reuses both.
125
+ - Under either `enforce` mode a host MUST emit `budget.reserved`, `budget.threshold-crossed` and `budget.exhausted`, and MAY coalesce `budget.consumed`.
126
+ - `hard` exhaustion under `onExhaustion: fail` emits `cap.breached` (`kind: budget-*`) and fails the run `budget_exhausted`. Under `interrupt` it raises an approval whose `resumeValue` adds budget, recorded by a second `budget.reserved`. An `advisory` host MUST NOT stop the run.
127
+ - A resolved model outside `modelAllow`, or in `modelDeny` (which wins), is refused `budget_model_denied` before the call.
128
+ - `dimensions` lists only what the host enforces and MAY omit `cost`. The `budget.*` events and `cap.breached` MUST NOT carry rate cards, unit prices, cost breakdowns, credentials or model prose (`budget-no-pricing-leak`); the aggregate cost is allowed.
129
+
117
130
  ### Validation and persistence
118
131
 
119
132
  - 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`.
@@ -277,10 +290,10 @@ A host advertising `deadLetter` MUST, when a run or node exhausts its retry poli
277
290
  - keep the failed run fork-eligible for `retentionDays`, and not purge it sooner;
278
291
  - purge it after `retentionDays`, after which a fork fails as for an unknown run.
279
292
 
280
- This sink holds runs. Queue messages ([host-services.md](host-services.md) §`queueBus`) and webhook deliveries ([webhooks.md](webhooks.md) §Dead letters) have their own.
293
+ Queue messages ([host-services.md](host-services.md) §`queueBus`) and webhook deliveries ([webhooks.md](webhooks.md) §Dead letters) have sinks of their own.
281
294
 
282
295
  ## During the v1 overlap
283
296
 
284
297
  A non-terminal run inherited from v1 continues, or is cancelled `v1_pin_unsupported`, per [persistence.md](persistence.md) §"Runs pinned to v1".
285
298
 
286
- *Sources: RFCs 0053, 0058, 0084, 0170, 0171, 0176, 0182.*
299
+ *Sources: RFCs 0053, 0058, 0084, 0170, 0171, 0176, 0182, 0228.*
@@ -140,6 +140,4 @@ A threat model missing a sibling section fails the template gate.
140
140
 
141
141
  Rows `C6.1`–`C6.9` are `spec/v1/migrations.json` entries.
142
142
 
143
- See also: identity.md, replay.md, webhooks.md, capabilities.md, conformance.md.
144
-
145
143
  *Sources: RFCs 0150, 0163, 0164, 0170, 0173, 0214, 0218, 0224.*
@@ -5,37 +5,35 @@
5
5
 
6
6
  ## Why this exists
7
7
 
8
- A node pack keeps files, records and indexes in storage services the host provides, reached through `ctx` ([host-services.md](host-services.md)). This document states what a host takes on by advertising one.
8
+ A node pack keeps files, records and indexes in storage services the host provides, reached through `ctx` ([host-services.md](host-services.md)). Each family below is a contract a host takes on by advertising it.
9
9
 
10
10
  ## Operations
11
11
 
12
- A host advertising a family MUST expose each of its operations to pack code, returning at least the fields shown; it MAY return more. An operation marked † is required only when the named facet is `true`.
12
+ A host advertising a family MUST expose each of its operations to pack code, returning at least the fields shown; it MAY return more. An operation marked † is required only when its facet is `true`: `kvStorage.atomicIncrement`, `kvStorage.compareAndSwap`, `sql.transactions` or `blobStorage.presignSupported`.
13
13
 
14
- | Family | Operations |
15
- | --- | --- |
16
- | `fs` (`ctx.fs`) | `read(path) → bytes, contentType?` · `write(path, bytes, contentType?) → path, sizeBytes` · `delete(path) → deleted` · `stat(path) → sizeBytes, modifiedAt, contentType?` · `list(prefix?, cursor?, limit?) → entries[path, sizeBytes], nextCursor?` |
17
- | `kvStorage` (`ctx.storage.kv`) | `get(key) → value?, expiresAt?` · `put(key, value, ttlSeconds?) → ok` · `delete(key) → deleted` · `list(prefix?, cursor?, limit?) → entries[key], nextCursor?` · † `atomicIncrement(key, delta?) → value` · † `compareAndSwap(key, expectedValue, newValue) → swapped` |
18
- | `tableStorage` (`ctx.storage.table`) | `createTable(name, schema) → ok` · `insert(table, row) → rowId` · `get(table, rowId) → row?` · `query(table, filter?, cursor?, limit?) → rows, nextCursor?` · `update(table, rowId, patch) → ok` · `delete(table, rowId) → deleted` |
19
- | `sql` (`ctx.db.sql`) | `query(datasourceId, sql, params) → rows, rowCount` · `execute(datasourceId, sql, params) → rowsAffected` · † `transaction(datasourceId, operations[sql, params]) → committed` |
20
- | `nosql` (`ctx.db.nosql`) | `insert(datasourceId, collection, doc) → id` · `get(…, id) → doc?` · `query(…, filter, cursor?, limit?) → docs, nextCursor?` · `update(…, id, patch) → ok` · `delete(…, id) → deleted` |
21
- | `vectorStore` (`ctx.db.vector`) | `upsert(collection, vectors[id, embedding, metadata?]) → upserted` · `query(collection, embedding, k, filter?) → matches[id, score, metadata?]` · `delete(collection, ids) → deleted` |
22
- | `searchIndex` (`ctx.db.search`) | `index(index, docs[id, fields]) → indexed` · `query(index, q, k?, filter?) → hits[id, score, fields?]` · `delete(index, ids) → deleted` |
23
- | `blobStorage` (`ctx.storage.blob`) | `put(bucket, key, bytes, contentType?) → url, sizeBytes` · `get(bucket, key) → bytes, contentType?` · `delete(bucket, key) → deleted` · `list(bucket, prefix?, cursor?) → entries[key, sizeBytes], nextCursor?` · † `presign(bucket, key, expiresInSeconds, method) → url, expiresAt` |
24
- | `cache` (`ctx.storage.cache`) | `get(key) → value?, expiresAt?` · `put(key, value, ttlSeconds) → ok` · `delete(key) → deleted` |
14
+ - **`fs`** (`ctx.fs`): `read(path) → bytes, contentType?` · `write(path, bytes, contentType?) → path, sizeBytes` · `delete(path) → deleted` · `stat(path) → sizeBytes, modifiedAt, contentType?` · `list(prefix?, cursor?, limit?) → entries[path, sizeBytes], nextCursor?`
15
+ - **`kvStorage`** (`ctx.storage.kv`): `get(key) → value?, expiresAt?` · `put(key, value, ttlSeconds?) → ok` · `delete(key) → deleted` · `list(prefix?, cursor?, limit?) → entries[key], nextCursor?` · † `atomicIncrement(key, delta?) → value` · † `compareAndSwap(key, expectedValue, newValue) → swapped`
16
+ - **`tableStorage`** (`ctx.storage.table`): `createTable(name, schema) → ok` · `insert(table, row) → rowId` · `get(table, rowId) → row?` · `query(table, filter?, cursor?, limit?) → rows, nextCursor?` · `update(table, rowId, patch) → ok` · `delete(table, rowId) → deleted`
17
+ - **`sql`** (`ctx.db.sql`): `query(datasourceId, sql, params) → rows, rowCount` · `execute(datasourceId, sql, params) → rowsAffected` · † `transaction(datasourceId, operations[sql, params]) → committed`
18
+ - **`nosql`** (`ctx.db.nosql`): `insert(datasourceId, collection, doc) → id` · `get(…, id) → doc?` · `query(…, filter, cursor?, limit?) → docs, nextCursor?` · `update(…, id, patch) → ok` · `delete(…, id) → deleted`
19
+ - **`vectorStore`** (`ctx.db.vector`): `upsert(collection, vectors[id, embedding, metadata?]) → upserted` · `query(collection, embedding, k, filter?) → matches[id, score, metadata?]` · `delete(collection, ids) → deleted`
20
+ - **`searchIndex`** (`ctx.db.search`): `index(index, docs[id, fields]) → indexed` · `query(index, q, k?, filter?) → hits[id, score, fields?]` · `delete(index, ids) → deleted`
21
+ - **`blobStorage`** (`ctx.storage.blob`): `put(bucket, key, bytes, contentType?) → url, sizeBytes` · `get(bucket, key) → bytes, contentType?` · `delete(bucket, key) → deleted` · `list(bucket, prefix?, cursor?) → entries[key, sizeBytes], nextCursor?` · † `presign(bucket, key, expiresInSeconds, method) → url, expiresAt`
22
+ - **`cache`** (`ctx.storage.cache`): `get(key) → value?, expiresAt?` · `put(key, value, ttlSeconds) → ok` · `delete(key) → deleted`
25
23
 
26
- The facets are `kvStorage.atomicIncrement` and `kvStorage.compareAndSwap`, `sql.transactions`, and `blobStorage.presignSupported`. `presign` takes `method` `GET` or `PUT`, and `…` in a `nosql` row repeats `datasourceId, collection`.
24
+ `presign` takes `method` `GET` or `PUT`, and `…` in a `nosql` operation repeats `datasourceId, collection`.
27
25
 
28
26
  ## Shared rules
29
27
 
30
28
  - **Tenant isolation.** A read for one tenant MUST NOT return data another tenant wrote, even under an identical key or name: `kvStorage` `get` and `list`, `tableStorage` `get` and `query`, `vectorStore` and `searchIndex` `query`, `blobStorage` and `cache` `get`.
31
- - **Datasources.** `sql` and `nosql` datasources are scoped per tenant, and access to another tenant's datasource MUST be refused.
29
+ - **Datasources.** `sql` and `nosql` datasources are scoped per tenant; access to another tenant's datasource MUST be refused.
32
30
  - **Backend-invariant.** The operation shapes of `sql` and `nosql` MUST NOT vary with the advertised `drivers`, nor those of `vectorStore` and `searchIndex` with the advertised `backends`. A host MAY back `sql` with any driver it advertises.
33
31
  - **Size limits.** A key over `kvStorage.maxKeyBytes`, and a write over `fs.maxFileSizeBytes`, `kvStorage.maxValueBytes`, `blobStorage.maxObjectBytes` or `cache.maxValueBytes`, MUST be refused. A `tableStorage` insert MUST be refused once `maxRowsPerTable` is reached.
34
32
  - **Expiry.** `kvStorage` and `cache` MUST honour an entry's expiry, as a read sees it, with at most one second of drift. `maxTtlSeconds` caps `ttlSeconds` for each.
35
33
 
36
- What a family advertises also names its targets: `sql.datasources`, `nosql.datasources`, `vectorStore.collections`, `searchIndex.indexes` and `blobStorage.buckets`. `tableStorage` advertises `maxColumnsPerRow`, `indexable` and `fullTextSearch` as limits and features, with no further rule.
34
+ A family's advertisement also names its targets: `sql.datasources`, `nosql.datasources`, `vectorStore.collections`, `searchIndex.indexes` and `blobStorage.buckets`. `tableStorage` advertises `maxColumnsPerRow`, `indexable` and `fullTextSearch` as limits and features, with no further rule.
37
35
 
38
- No error code specific to these families is registered; [errors.md](errors.md) governs the code a refused call carries.
36
+ A refused call carries `not_found`, `forbidden`, `validation_error`, or `storage_limit_exceeded` for a limit or quota ([errors.md](errors.md) §Host-service refusals). A sandbox escape is `forbidden` with `details.reason: path-outside-sandbox`.
39
37
 
40
38
  ## `fs`
41
39
 
@@ -57,8 +55,8 @@ No error code specific to these families is registered; [errors.md](errors.md) g
57
55
 
58
56
  ## `sql` and `nosql`
59
57
 
60
- - `sql` MUST be treated as a parametric template: bound values MUST flow through `params`, never through string interpolation. A host SHOULD verify parameter binding before execution, and a pack MUST NOT concatenate user input into `sql`.
61
- - When `sql.transactions` is `true`, a partial failure inside `transaction` MUST roll back the whole batch.
58
+ - `sql` MUST be treated as a parametric template: bound values MUST flow through `params`, never through string interpolation. A pack MUST NOT concatenate user input into `sql`, and a host SHOULD verify parameter binding before execution.
59
+ - When `sql.transactions` is `true`, a partial failure inside `transaction` MUST roll back the whole batch, which resolves `committed: false`.
62
60
  - `nosql` filter operators MUST NOT permit injection. Server-side script evaluation, such as MongoDB `$where`, MUST be refused unless an explicit allowlist is configured.
63
61
 
64
62
  ## `vectorStore` and `searchIndex`
@@ -70,4 +68,4 @@ No error code specific to these families is registered; [errors.md](errors.md) g
70
68
 
71
69
  Presigned URLs MUST expire at the advertised TTL. A presigned request after expiry MUST fail at the storage layer, not after an authorization skip.
72
70
 
73
- *Sources: RFCs 0014, 0015, 0016, 0018, 0019.*
71
+ *Sources: RFCs 0014, 0015, 0016, 0018, 0019, 0228.*
@@ -13,11 +13,11 @@ How a v2 host selects a major, what each version axis means, and what a release
13
13
  A v2 host MUST advertise two root fields, both REQUIRED in `schemas/v2/capabilities.schema.json` ([capabilities.md](capabilities.md)):
14
14
 
15
15
  - `protocolVersions[]` — every `<major>.<minor>` it serves, each matching `^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$`. Through the overlap that is `["1.<n>", "2.<m>"]`; after v1 end-of-support, `["2.<m>"]`.
16
- - `preferredVersion` — MUST be a member of `protocolVersions[]`. Through the overlap it MUST name a 1.x member, because a header-less request is a v1 client's ([capabilities.md](capabilities.md) §1; §1.3). On a host serving a single major, it MUST equal `protocolVersion`.
16
+ - `preferredVersion` — MUST be a member of `protocolVersions[]`. Through the overlap it MUST name a 1.x member ([capabilities.md](capabilities.md) §1; §1.3). On a host serving a single major, it MUST equal `protocolVersion`.
17
17
 
18
18
  A host that drops v1 advertises a `2.x` `preferredVersion`; its header-less representation becomes the closed v2 root.
19
19
 
20
- A v2 consumer reads `preferredVersion` as the header-less default. When it is absent on a v1 document, the default is `max(protocolVersions[])`, else `protocolVersion`. The suite's `--target-major` defaults from it.
20
+ A v2 consumer reads `preferredVersion` as the header-less default. When it is absent on a v1 document, the default is `max(protocolVersions[])`, else `protocolVersion`.
21
21
 
22
22
  ### 1.2 Paths
23
23
 
@@ -47,8 +47,6 @@ A request on an unversioned path MAY carry `OpenWOP-Version: <major>` or `OpenWO
47
47
  | Header absent on any other unversioned path | MUST serve major 2: the path is the v2 surface (§1.2) |
48
48
  | `/v1/…` path with `OpenWOP-Version` other than `1` | MUST answer `400` `protocol_version_mismatch` |
49
49
 
50
- `/.well-known/openwop` is the one resource both majors serve at the same unversioned path, so it is the only place a header-less request can come from a client of the earlier major.
51
-
52
50
  A request on a `/v1/…` path key MUST NOT carry `OpenWOP-Version` with a value other than `1`. `protocol_version_unsupported`, `protocol_version_mismatch` and `client_version_unsupported` (§1.5) are rows in `spec/v2/errors.json` ([errors.md](errors.md)).
53
51
 
54
52
  ### 1.4 The response header
@@ -131,7 +129,6 @@ Dispositions:
131
129
 
132
130
  - One grammar covers protocol, envelope-kind and pack axes wherever a version is `<major>.<minor>` (#1, #2, #11, #15).
133
131
  - `typeId@<semver>` is a pack axis (`packs.md`); the `2` in `typeId@2.0.0` never means `OpenWOP-Version: 2`.
134
- - `docs/PROTOCOL-STATUS.md` carries one row per axis.
135
132
 
136
133
  ### 2.1 `engineVersion` (axis 3)
137
134
 
@@ -141,7 +138,7 @@ Dispositions:
141
138
 
142
139
  ### 2.2 `eventLogSchemaVersion` (axis 4)
143
140
 
144
- `eventLogSchemaVersion` is the era key; the schema floor is `minimum 2`. Its stamping, absent-⇒-`2` and discovery rules are `persistence.md` §"The era key"; the reader contract is `persistence.md`.
141
+ `eventLogSchemaVersion` is the era key; the schema floor is `minimum 2`. Its stamping, absent-⇒-`2`, discovery and reader rules are in [persistence.md](persistence.md) §"The era key".
145
142
 
146
143
  ## 3. Where v2 lives
147
144
 
@@ -168,15 +165,13 @@ Through the overlap a host:
168
165
  - MUST emit `OpenWOP-Version` on every response (§1.4);
169
166
  - MUST serve `/.well-known/openwop` as one resource whose representation the request header selects ([capabilities.md](capabilities.md)).
170
167
 
171
- The dual-stack scenario creates one run through `/v1/runs` with no header and reads it through `/runs` with `OpenWOP-Version: 2`; the response headers name the contract used.
172
-
173
168
  A run minted under major 1 and read under major 2 MUST use the tenant-bound projection `<tenantId>/<v1-id>` ([identity.md](identity.md) §5). A host MUST NOT return a bare v1 id in a major-2 response.
174
169
 
175
170
  ### Retirement
176
171
 
177
172
  The overlap ends at v1 end-of-support ([overview.md](overview.md)): `protocolVersions[]` drops the `1.<n>` member, and every alias carrying the `v1-end-of-support` trigger is removed.
178
173
 
179
- - **Retirement is atomic.** §1.1 admits no state in which both majors are advertised and `2.x` is preferred, so dropping v1 retires the whole `/v1` path space at once.
174
+ - **Retirement is atomic.** Dropping v1 retires the whole `/v1` path space at once (§1.1).
180
175
  - **Retirement changes every header-less request's default contract**, from major 1 to major 2. Before retirement, a host MUST check for collisions between manifest top-level path segments and non-protocol unversioned routes, and MUST move each colliding route or apply §1.4 content negotiation.
181
176
 
182
177
  ### Host-proprietary paths: `/host/<org>/…`