@openwop/spec-artifacts 2.0.3 → 2.0.5

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 -11
  2. package/api/seams-v2.yaml +1 -1
  3. package/api/v2/asyncapi.yaml +1 -1
  4. package/api/v2/openapi.yaml +1 -1
  5. package/package.json +1 -1
  6. package/schemas/v2/certification-bundle.schema.json +5 -0
  7. package/spec/v1/core-standard-manifest.json +2 -2
  8. package/spec/v2/README.md +15 -0
  9. package/spec/v2/core/capabilities.md +404 -0
  10. package/spec/v2/core/conformance.md +66 -0
  11. package/spec/v2/core/connection-packs.md +32 -0
  12. package/spec/v2/core/errors.md +131 -0
  13. package/spec/v2/core/events.md +109 -0
  14. package/spec/v2/core/form-content-packs.md +32 -0
  15. package/spec/v2/core/headers.md +40 -0
  16. package/spec/v2/core/idempotency.md +45 -0
  17. package/spec/v2/core/identity.md +141 -0
  18. package/spec/v2/core/interop.md +65 -0
  19. package/spec/v2/core/interrupt.md +83 -0
  20. package/spec/v2/core/overview.md +58 -0
  21. package/spec/v2/core/packs.md +72 -0
  22. package/spec/v2/core/persistence.md +168 -0
  23. package/spec/v2/core/replay.md +104 -0
  24. package/spec/v2/core/runs.md +109 -0
  25. package/spec/v2/core/security-defaults.md +108 -0
  26. package/spec/v2/core/versioning.md +116 -0
  27. package/spec/v2/core/webhooks.md +64 -0
  28. package/spec/v2/core/workflow-chain-packs.md +36 -0
  29. package/spec/v2/declaration.json +8 -0
  30. package/spec/v2/declaration.schema.json +34 -2
  31. package/spec/v2/ext/a2uiSurface/README.md +20 -0
  32. package/spec/v2/ext/brand/README.md +20 -0
  33. package/spec/v2/ext/canvas/README.md +20 -0
  34. package/spec/v2/ext/chat/README.md +20 -0
  35. package/spec/v2/ext/coordination/README.md +20 -0
  36. package/spec/v2/ext/dataIntegration/README.md +20 -0
  37. package/spec/v2/ext/entities/README.md +20 -0
  38. package/spec/v2/ext/grpc-transport/README.md +13 -0
  39. package/spec/v2/ext/kanban/README.md +20 -0
  40. package/spec/v2/ext/knowledge/README.md +20 -0
  41. package/spec/v2/ext/launchStudio/README.md +20 -0
  42. package/spec/v2/ext/messaging/README.md +20 -0
  43. package/spec/v2/ext/portability/README.md +11 -0
  44. package/spec/v2/ext/provider-idempotency/README.md +11 -0
  45. package/spec/v2/ext/restTransport/README.md +20 -0
  46. package/spec/v2/ext/sandbox-runtime-notes/README.md +11 -0
  47. package/spec/v2/ext/webResearch/README.md +20 -0
  48. package/spec/v2/release.json +2 -2
@@ -0,0 +1,65 @@
1
+ # Interop
2
+
3
+ > **Status: Draft · v2.0.0-rc (2026-09-03) · RFC 0175.**
4
+
5
+ ## Why this exists
6
+
7
+ v1 advertised a `supportedTransports` list that could only honestly say `rest`, carried two legacy embedded-protocol profiles with dated sunsets, and let an unauthenticated peer steer version negotiation with no floor, no refresh obligation, and no audit record. This document is the v2 contract for the two embedded protocols (A2A and MCP): how a host advertises them, how a version is negotiated, and what every negotiation leaves behind. Capability shapes are in capabilities.md; the peer identity is the Subject of identity.md.
8
+
9
+ ## REST is the wire
10
+
11
+ REST and SSE are the wire. A host MUST NOT advertise a transport list; `supportedTransports` does not exist in `schemas/v2/capabilities.schema.json`, and a discovery document carrying it MUST fail validation. A2A and MCP are **compositions** over the wire, advertised by their own facets and nothing else.
12
+
13
+ ## The facets
14
+
15
+ A host that speaks either protocol MUST advertise the corresponding facet with every required field (`spec/v2/facets/a2a.schema.json`, `spec/v2/facets/mcp.schema.json`).
16
+
17
+ | Facet field | A2A (`a2a`) | MCP (`mcp`) | Rule |
18
+ | --- | --- | --- | --- |
19
+ | Offered versions | `versions[]` (`major.minor`) | `revisions[]` (dates) | REQUIRED, at least one entry |
20
+ | Default | `preferredVersion` | `preferredVersion` | REQUIRED; served when the peer names none |
21
+ | Floor | `minimumVersion` | `minimumRevision` | REQUIRED; below it negotiation fails closed |
22
+ | Freshness | `refreshedAt` | `refreshedAt` | REQUIRED; see the refresh SLA |
23
+ | Profiles | `profiles[]` `a2a-<major.minor>` | `profiles[]` `mcp-<date>` | no `-legacy` alternative exists |
24
+ | Protocol-specific | `agentCardUrl`, `streaming`, `pushNotifications`, `durableTasks` | `features[]`, `serverUrls[]`, `serverMount.transports[]` (`stdio` \| `streamable-http`), `mrtr.maxRounds` | optional |
25
+
26
+ `mcp.serverMount.transports[]` is the MCP server's own transport enum; it is not a host transport advertisement.
27
+
28
+ ## Legacy profiles are absent
29
+
30
+ The profile ids `a2a-0.3-legacy` and `mcp-2025-06-18-legacy` do not exist in v2. The `profiles[]` item patterns admit no `-legacy` suffix, and the legacy code paths (the A2A 0.3 mapping and the MCP live-callback bridges) are not part of this corpus. A host that still speaks a legacy version does so as a private, non-advertised behavior. When no `A2A-Version` header is present, a host MUST serve the agent card of `preferredVersion`.
31
+
32
+ ## Negotiation is a protocol
33
+
34
+ **Authentication.** A version-negotiation exchange on either protocol MUST be authenticated: the peer identity is the caller's Subject (identity.md) or the host's own outbound identity. An unauthenticated exchange MUST NOT lower the negotiated version below `preferredVersion`.
35
+
36
+ **The floor.** A negotiation that would land below `minimumVersion` / `minimumRevision` MUST fail closed with `interop_version_unsupported` (`spec/v2/errors.json`), whether or not host policy permits an explicit downgrade above the floor.
37
+
38
+ **The audit event.** Every negotiation outcome, including the refused one, MUST emit a `negotiation.decided` event on the host's own event log:
39
+
40
+ ```jsonc
41
+ { "protocol": "a2a" | "mcp", "peer": "<origin digest>", "requested": "…",
42
+ "negotiated": "…" | null, "outcome": "accepted" | "downgraded" | "refused", "reason": "…" }
43
+ ```
44
+
45
+ The event is content-free: `peer` MUST be a digest of the peer origin, never the origin in clear. The event on the host's own log is the normative witness of the two silent-downgrade invariants (`a2a-version-no-silent-downgrade`, `mcp-version-no-silent-downgrade`); the conformance seams profile (conformance.md) drives the exchange and captures the wire leg.
46
+
47
+ **The refresh SLA.** A host MUST re-evaluate its advertised `versions[]` / `revisions[]` against the upstream registry within the window its `refreshedAt` declares, and that window MUST NOT exceed 90 days. An advertisement older than its window is non-conformant.
48
+
49
+ **Downgrade above the floor.** A host MAY accept an authenticated request for a version between the floor and `preferredVersion`; the event then reports `outcome: downgraded`.
50
+
51
+ ## The MCP round ceiling
52
+
53
+ `mcp.mrtr.maxRounds` (integer, 1–16) is the advertised ceiling on multi-round tool-result rounds. A host MUST refuse an `input_required` round beyond `maxRounds` with `mcp_mrtr_rounds_exceeded` (`spec/v2/errors.json`). The v1 `requestState` requirements carry over unchanged.
54
+
55
+ ## The durable-task projection
56
+
57
+ `auth-required` remains a member of the persisted A2A task state enum (`schemas/v2/a2a-task-state.schema.json`) for the reverse direction (consuming an external A2A agent). The forward projection MUST NOT emit it: v2 has no `auth` interrupt kind. Adding one is an additive v2.x RFC, not a host extension.
58
+
59
+ ## gRPC
60
+
61
+ gRPC is not part of the core wire. Its document lives at `spec/v2/ext/grpc-transport/` with `witness: unwitnessable` and `adoption: none`; its requirements are SHOULDs of that extension. A host MUST NOT advertise a `grpc` capability block — an unwitnessable family is not advertisable — and `api/v2/openapi.yaml` and the AsyncAPI document are the only canonical API descriptions. The extension re-enters core only by a v2.x additive RFC that generates the proto from `spec/v2/declaration.json` and lands a suite client.
62
+
63
+ ## Threat model
64
+
65
+ `SECURITY/threat-model-interop.md` is the threat model for this document: downgrade, card/runtime drift, cross-tenant lookup through a peer, artifact leakage across the boundary, the anonymous end-user actor, and negotiation replay. Its invariants are the two silent-downgrade rows plus `interop-negotiation-authenticated`, `interop-minimum-version-enforced`, and `interop-peer-no-authority-escalation` in `SECURITY/invariants.yaml`. Peer identity and authorization at the boundary are governed by security-defaults.md; a peer MUST NOT gain authority the caller's Subject does not hold.
@@ -0,0 +1,83 @@
1
+ # Interrupt
2
+
3
+ > **Status: Draft · v2.0.0-rc (2026-09-03) · RFC 0170 §E.1, RFC 0171 §A.4, RFC 0173 §B.**
4
+
5
+ ## Why this exists
6
+
7
+ `interrupt` is the one primitive by which a run waits for something outside itself: a human decision, an answer, an external event, a conversation turn. Every kind shares one payload shape, one pair of events, one resolve contract and one token scheme, so a client that can resolve an approval can resolve anything.
8
+
9
+ ## Payload
10
+
11
+ `schemas/v2/suspend-request.schema.json` (`InterruptPayload`) is closed and discriminated by `kind`; `kind`, `key` and `data` are REQUIRED.
12
+
13
+ | Kind | `data` (required fields) | Notes |
14
+ | --- | --- | --- |
15
+ | `approval` | `artifactId`, `artifactType`, `title`, `actions` | 5-action vocabulary below; quorum and eligibility fields |
16
+ | `clarification` | `questions[]` (`id`, `question`, optional `schema`) | Snapshot status `waiting-input` |
17
+ | `external-event` | `eventType`, `correlation` | Snapshot status MUST be `waiting-external` |
18
+ | `custom` | `customKind`, optional `payload` | A host MUST accept and persist it; rendering is best-effort |
19
+ | `conversation.start` | `conversationId` | Gated on the `conversation` capability (capabilities.md); `conversationId` MUST be tenant-unique and MUST NOT be assumed resolvable on another host |
20
+ | `conversation.exchange` | `conversationId`, `prompt` | The resume value MUST validate against `outcomeSchema` when supplied |
21
+ | `conversation.close` | `conversationId` | Gated as above |
22
+ | `low-confidence` | `agentId`, `threshold`, `observed` | An `agent.decided` with `confidence` below the threshold MUST be followed by `node.suspended { reason: 'low-confidence' }`; the per-run threshold is `configurable.run.escalationThreshold` (runs.md) |
23
+
24
+ `key` is the deterministic re-entry key: a host MUST invoke an interrupt with key `K` at most once for the lifetime of the run. On recovery the engine MUST consult the event log, find the prior `interrupt.resolved`, and return the persisted `resumeValue` without emitting a second `interrupt.requested`; an in-memory cache MAY serve in-process replays but MUST NOT replace the event log for cross-process replays. A host MUST validate the resume value against `resumeSchema` when one is declared and MUST refuse a failing value with `400 validation_error`. `timeoutMs`, when set, is the interrupt's own deadline.
25
+
26
+ ## Events
27
+
28
+ Every kind is recorded by two registered types (events.md): `interrupt.requested`, whose payload is the `InterruptPayload` verbatim, and `interrupt.resolved`, whose payload is `{ nodeId, interruptId, kind?, resumeValue? }` (closed). The legacy `approval.*` and `clarification.*` types remain registered; their payload definitions in `schemas/v2/run-event-payloads.schema.json` are `$ref` aliases of `interruptRequested` and `interruptResolved` (RFC 0171 §A.4 E4), so there is one shape per direction. A host emitting `interrupt.requested` SHOULD also emit the legacy kind-specific type until its consumers migrate. 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`.
29
+
30
+ ## Resolve surfaces
31
+
32
+ | Operation | Path | Auth | Body |
33
+ | --- | --- | --- | --- |
34
+ | `resolveInterruptByRun` | `POST /runs/{runId}/interrupts/{nodeId}` | `approvals:respond` | `{ resumeValue }` (closed) |
35
+ | `inspectInterruptByToken` | `GET /interrupts/{token}` | the token | — (returns the `InterruptPayload`) |
36
+ | `resolveInterruptByToken` | `POST /interrupts/{token}` | the token | `{ resumeValue }` (closed) |
37
+
38
+ A host MUST expose the run-scoped surface and SHOULD expose the signed-token surface for callers not authenticated to the protocol (a payment webhook, a mail link). Every resolve MUST honor `Idempotency-Key` (idempotency.md). Exactly one of two concurrent resolves MUST succeed; the other MUST receive `409 interrupt_already_resolved`.
39
+
40
+ | Status | Code | Condition |
41
+ | --- | --- | --- |
42
+ | `400` | `validation_error` | `resumeValue` fails `resumeSchema` or the approval action is not in `actions` |
43
+ | `401` | `interrupt_token_invalid` | MAC, `alg` or `kid` not accepted |
44
+ | `404` | `not_found` | No such run or node |
45
+ | `409` | `interrupt_already_resolved` | Already resolved; or a token invalidated by resolution, cancellation or completion |
46
+ | `410` | `interrupt_expired` | Token past `expiresAt` (token surface only) |
47
+
48
+ ## Tokens
49
+
50
+ The token grammar is `ow2.<alg>.<kid>.<payload>.<mac>`, defined in identity.md; `alg` MUST be one the host advertises in `interrupt.tokenAlgs[]` (`hs256` at the cut) and `kid` MUST select a secret the host holds, otherwise `401 interrupt_token_invalid`. A v1 two-segment token remains resolvable under `kid: legacy` until its `expiresAt`.
51
+
52
+ | Rule | Requirement |
53
+ | --- | --- |
54
+ | Expiry | Every token MUST carry `expiresAt`. The default SHOULD be 30 minutes; 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`. |
55
+ | 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`. |
56
+ | Verification | MAC comparison MUST be constant-time. `kid` selects the verification secret so secrets rotate without orphaning outstanding tokens. |
57
+ | Intent | A token minted with `intent: resolve` authorizes both operations; a host MAY mint `intent: inspect` tokens, and a resolve with one MUST be refused with `403`. |
58
+
59
+ ## Approval
60
+
61
+ `actions` is a non-empty subset of `accept`, `reject`, `refine`, `edit-accept`, `ask`; a host MUST enforce it on resolve. `ask` does not exit the suspend.
62
+
63
+ | `action` | Required field |
64
+ | --- | --- |
65
+ | `accept` | — (`feedback?`) |
66
+ | `reject` | — (`feedback?`) |
67
+ | `refine` | `refineFeedback { scope: whole \| section \| items, sectionPath?, itemIds?, tags?, text? }` |
68
+ | `edit-accept` | `editedArtifactData` |
69
+
70
+ Every resume carries `decidedAt`; `decidedBy` MAY be omitted by an authenticated caller, and every consumer MUST treat it as an opaque string. `requiredApprovals` sets the quorum (default 1) and `rejectionPolicy` is `single-veto` (default) or `majority`; when `overrideBypassesQuorum` is `true` a configured override principal MAY release the gate alone, otherwise its vote counts once.
71
+
72
+ ## Approver enforcement
73
+
74
+ Enforcement is an obligation of the fields, not a discovery flag (RFC 0173 §B). The facet `spec/v2/facets/interrupt.schema.json` carries `tokenAlgs[]` (REQUIRED) and `refKinds[]` ⊆ `principal`, `group`, `role`.
75
+
76
+ | Field | Binds |
77
+ | --- | --- |
78
+ | `approversList` (explicit principals) | Everywhere: a host advertising `interrupt` MUST refuse a resolver not in the list |
79
+ | `approverGroupRefs` | Only where `refKinds` includes `group`: the host MUST surface the field unchanged and MUST resolve and enforce its members as eligible approvers |
80
+ | `approverRoleRefs` | Only where `refKinds` includes `role`: as for groups, with holders |
81
+ | `audience` | A notification hint, never eligibility; omitted ⇒ the host SHOULD notify the union of the eligibility refs |
82
+
83
+ Refs are opaque to the engine; the host resolves them against its own identity model. 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). A host that does not advertise a ref kind MUST ignore that field. A relaxation of any obligation here is an operator setting recorded in the certification bundle, never a discovery field (security-defaults.md).
@@ -0,0 +1,58 @@
1
+ # OpenWOP v2 Core — Overview
2
+
3
+ > **Status: Draft · v2.0.0-rc (2026-09-03) · RFC 0167, 0168, 0169, 0171, 0174.**
4
+
5
+ ## Why this exists
6
+
7
+ `spec/v2/core/` is the front door a host implements to pass the 2.0.0 floor. This document fixes the reading order, restates the six axioms, and states once the rules other documents only reference.
8
+
9
+ ## Reading order
10
+
11
+ 1. `overview.md` — axioms, §0, claim vocabulary, `ext/` rule
12
+ 2. `versioning.md` — major negotiation, `OpenWOP-Version`, the 18 axes, release identity
13
+ 3. `capabilities.md` — one well-known resource, record type, closed root, derived profiles
14
+ 4. `identity.md` — Subject, lanes, `SubjectLink`, id grammars, resume tokens
15
+ 5. `runs.md` — create / get / cancel / fork, `configurable`, snapshot, owner
16
+ 6. `events.md`, `errors.md`, `headers.md` — event `oneOf`, payload and error registries, `OpenWOP-*`
17
+ 7. `streams.md`, `interrupt.md`, `idempotency.md`, `replay.md` — run-side surfaces
18
+ 8. `persistence.md` — era key, v1 reader rule, pinned runs
19
+ 9. `security-defaults.md`, `webhooks.md`, `interop.md` — obligations of a surface, signatures, A2A / MCP
20
+ 10. `packs.md`, `connection-packs.md`, `form-content-packs.md`, `workflow-chain-packs.md` — pack identity, engines ceiling
21
+ 11. `conformance.md` — requirement ids, witness classes, bundle v3, seams profile
22
+
23
+ ## Axioms in force (RFC 0167 §A)
24
+
25
+ 1. A MUST without a witness class is not a requirement.
26
+ 2. One name per thing; every alias has a removal date in `spec/v1/deprecations.json` and a codemod id.
27
+ 3. Closed by default: discovery root, event envelope, payload and error registries, bundle, and `configurable` are `additionalProperties: false`; vendor extension is one positive pattern in one namespace.
28
+ 4. Registers are data: gaps, risks, deprecations, migrations, witness classes, and dispositions are files with schemas and gates; prose is checked against them, never the reverse.
29
+ 5. Security defaults are obligations of the surface: a protecting behavior binds when the surface is advertised, never when a flag is set.
30
+ 6. Nothing persisted under v1 is orphaned: every v1 artifact has a disposition in the migration register.
31
+
32
+ ## §0 Closed-enum growth rule (RFC 0171 §A.5)
33
+
34
+ A registry-backed enum (event types, error codes, envelope kinds, reason vocabularies, lanes) grows by adding a row to its registry and regenerating. Consumers MUST accept an unknown member of a registry-backed enum and MUST NOT act on it. Producers MUST NOT emit an unregistered member. Adding a member is additive in v2.x; removing or renaming one is a major.
35
+
36
+ ## v1 end-of-support (RFC 0174 §B.4)
37
+
38
+ v1 support ends at the later of (a) every INTEROP-MATRIX host's non-vacuous v2 bundle plus 90 days and (b) 18 months from the v2 release, where (b) applies if and only if an independent host is in the matrix at release. Phase 5 computes the date from the matrix; nothing else MAY set it. The hosts counted under (a) are those with a row in the INTEROP-MATRIX v2 table; a reference host that stays on the 1.x line through the overlap (the matrix says which) is not a v2 host and does not count. A host's bundle is "non-vacuous" when at least one claimed profile carries `witnessCount ≥ 1`. The anchor for a host is the date its signed bundle was committed to `evidence/v2-host-bundles/` in the spec repository, read from the public history (`git log --diff-filter=A`), never from `generatedAt` inside the bundle, which nothing signs; a later re-certification replaces the file and does not move the anchor. `evidence/v1-end-of-support.json` is the computed date and is GENERATED (`scripts/generate-v1-eos-clock.mjs`); `check-removal-dates.mjs` reads it and fails the v1-tree sources of every `v1-end-of-support` row on or after it.
39
+
40
+ **Old-major retention floors.** Every published old-major artifact — the npm packages `@openwop/openwop` (1.x) and `@openwop/openwop-conformance` (1.x), the PyPI package `openwop-client` (1.x) and the Go module `github.com/openwop/openwop-sdks/go` (v1.x) — MUST remain installable at its last 1.x version for 12 months from the 2.0.0 publish (the `v2.0.0` tag's commit date), independent of v1 end-of-support, which may come first; a consumer pinned to the old major MUST be able to rebuild through that window. The identities and the last 1.x versions are `spec/v2/retention-floors.json`; `scripts/check-retention-floors.mjs` prints the floor state and, with `--network`, probes each registry for the pinned version. Unpublishing, deprecating-with-removal, or retracting a listed version inside the window is a Phase 5 exit failure.
41
+
42
+ ## Profile claim vocabulary (RFC 0169 §C.3; RFC 0155 §A unchanged)
43
+
44
+ Normative for any public conformance statement:
45
+
46
+ - An unqualified "OpenWOP conformant" or "OpenWOP compatible" statement MUST mean `openwop-core-standard`, the executable floor, never the discovery predicate.
47
+ - A discovery-only claim MUST say `openwop-discovery-core` and MUST NOT use the same badge as `openwop-core-standard`.
48
+ - Every claim MUST state every additional profile it relies on; an omitted profile is an unclaimed one.
49
+ - A certification bundle MUST name canonical profile ids; `openwop-core` is deleted (see `capabilities.md`).
50
+ - A vendor extension MUST NOT use an `openwop-*` id without an accepted RFC.
51
+
52
+ ## What is `ext/` (RFC 0174 §E.2; RFC 0169 §B.3)
53
+
54
+ `spec/v2/core/` is under 25,000 words (`scripts/check-core-budget.mjs`). Every `spec/v2/ext/<key>/` document MUST declare `witness` and both maturity axes (`technical`, `adoption`) in its header. A MUST with `witness: unwitnessable` MUST NOT appear in `core/`; a document whose only witness is "deferred to Active → Accepted" enters `ext/` or is deleted. An `ext/` family is advertised only under a wire-legal witness class (see `capabilities.md`).
55
+
56
+ ## What a MUST means (RFC 0168 §B.1; Axiom 1)
57
+
58
+ Every MUST, SHOULD, and MAY in `core/` is a requirement with an id in `requirements.json` and a `witness` from `witnessable-unaided | witnessable-gated | seam-gated | claims-check | negative-existence`. A seam-gated MUST MUST mint a normative observation path before the cut or is demoted to SHOULD (RFC 0168 §B.3; see `conformance.md`).
@@ -0,0 +1,72 @@
1
+ # Packs
2
+
3
+ > **Status: Draft · v2.0.0-rc (2026-09-03) · RFC 0177.**
4
+
5
+ ## Why this exists
6
+
7
+ Every one of the 282 pack versions published under v1 either pins `<2.0.0` or declares no ceiling at all, four peer-dependency grammars were signed into the registry, and two signing conventions shared one word while signing different bytes. This document is the v2 contract for pack manifests, the registry tree, peer-dependency identifiers, and signing. The per-kind rules live in connection-packs.md, form-content-packs.md, and workflow-chain-packs.md; the capability vocabulary a pack requires is capabilities.md.
8
+
9
+ ## The engine range
10
+
11
+ A manifest's `engines.openwop` MUST match the grammar in `schemas/v2/node-pack-manifest.schema.json`: a `>=` lower bound and an explicit `<` major ceiling (`^>=\d+(\.\d+){0,2} <\d+\.0\.0$`). A v2 host MUST treat a range with no upper bound as bounded by `<2.0.0`. 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`); `pack_runtime_requirement_unmet` remains a runtime-requirement code and MUST NOT be used for the protocol major. The check 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.
12
+
13
+ ## The registry tree
14
+
15
+ The registry is versioned by tree, not by 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; the v1 tree is served read-only through the overlap. A signed compatibility overlay MUST be rejected: signatures authorize by namespace, and a mirror re-derives the signer at ingest.
16
+
17
+ `.well-known/openwop-registry.json` `endpoints` is the negotiation: it names both trees, and a client MUST resolve every registry path through `endpoints` rather than by constructing one. `publicKey` is unversioned; keys are not protocol-versioned.
18
+
19
+ ## Peer-dependency identifiers
20
+
21
+ A `peerDependencies` key MUST be a root key of `spec/v2/declaration.json`; the declaration key, the peer-dependency identifier, and the capabilities.md section anchor are one identifier. A host MUST refuse a key the declaration file does not name with `pack_peer_dependency_undefined`. Facet paths are not identifiers: a pack requires a family by its key and names facets in `peerDependenciesMeta.<family>.facets[]`.
22
+
23
+ ```jsonc
24
+ "peerDependencies": { "aiProviders": "required" },
25
+ "peerDependenciesMeta": { "aiProviders": { "facets": ["imageGeneration"] } }
26
+ ```
27
+
28
+ ## The alias table
29
+
30
+ `spec/v2/peer-dependency-aliases.json` is generated from the declaration file and the published-manifest inventory, never hand-kept; each of its 23 rows is `{ alias, family, facets?, publishedUses, removalTrigger }` and covers a v1 grammar found in the wild (`host.*` dotted twins, `openwop.agents.memoryBackends`, facet paths such as `aiProviders.imageGeneration`). A v2 host MAY resolve an alias through the table during the overlap and MUST NOT resolve one after v1 end-of-support (`removalTrigger: v1-end-of-support`). A row the declaration file cannot explain fails the corpus gate.
31
+
32
+ ## The manifest schema family
33
+
34
+ The 13 manifest schemas carry `$id` under `https://openwop.dev/spec/v2/`; the v1 `$id`s are immutable and served read-only.
35
+
36
+ | Schema (`schemas/v2/…`) | Author | Vendor hatch |
37
+ | --- | --- | --- |
38
+ | `node-pack-manifest`, `prompt-pack-manifest`, `workflow-chain-pack-manifest`, `artifact-type-pack-manifest`, `chat-card-pack-manifest`, `connection-pack-manifest`, `form-content-pack-manifest`, `frontend-plugin-manifest`, `registry-version-manifest` | pack | REQUIRED |
39
+ | `agent-manifest`, `prompt-template` | pack (nested under a pack root) | REQUIRED |
40
+ | `pack-lockfile` | host | closed |
41
+ | `security-advisory` | registry | closed |
42
+ | `prompt-ref` | leaf | none |
43
+
44
+ Every pack-authored document MUST admit `patternProperties` `^(openwop-|x-|vendor\.)`. The `openwop-` prefix is the v2 spelling of the v1 `x-openwop-*` annotation keys, renamed so annotation keys and wire headers stop sharing a token shape. A consumer that does not recognize a hatch property MUST ignore it and MUST NOT reject the document; the value is pack-authored and therefore untrusted (security-defaults.md).
45
+
46
+ ## Signing
47
+
48
+ There is one signing scheme. `signing` on a version manifest (`schemas/v2/registry-version-manifest.schema.json`) is `{ keyId, scheme }`, both REQUIRED:
49
+
50
+ | Field | Rule |
51
+ | --- | --- |
52
+ | `scheme` | MUST be `ed25519-canonical-json`: a detached 64-byte Ed25519 signature over the canonical-JSON `pack.json` inside a deterministic tarball |
53
+ | `keyId` | the signing key id; `publicKeyRef` does not exist |
54
+ | `method` | does not exist; a manifest carrying it fails validation |
55
+
56
+ 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`. A signature over tarball bytes is not a v2 signature; such a pack MUST be re-signed, not relabeled.
57
+
58
+ ## Version manifests
59
+
60
+ `kind` is REQUIRED on every version manifest and every bare manifest; the v1 "absent means `node`" reading does not exist. A deprecated version is flagged `versionDeprecated: true`; the registry continues to serve it and a consumer MAY refuse to install it.
61
+
62
+ ## The registry's own schemas
63
+
64
+ 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.
65
+
66
+ ## Errors
67
+
68
+ | Code | Raised when |
69
+ | --- | --- |
70
+ | `pack_engine_unsupported` | the range does not admit the host's protocol major (install, every path) |
71
+ | `pack_peer_dependency_undefined` | a peer-dependency key is not a declaration-file key or an overlap alias |
72
+ | `pack_signature_invalid` | the signature, key, or namespace check fails |
@@ -0,0 +1,168 @@
1
+ # Persistence and Coexistence
2
+
3
+ > **Status: Draft · v2.0.0-rc (2026-09-03) · RFC 0176 (§A–§B, §D–§E), 0171 §A, 0170 §A.3.**
4
+
5
+ ## Why this exists
6
+
7
+ The v2 cut renames event types that are persisted, indexed, and unique-keyed in production stores, and fork and replay read those rows verbatim. This document states how a v2 host reads what a v1 host wrote, what happens to a run in flight at the cut, and what each persisted store becomes — so two hosts read one log one way.
8
+
9
+ ## The codemap is data
10
+
11
+ `spec/v2/event-codemap.json`, shipped in `@openwop/spec-artifacts`, is the only authority for the v1→v2 event-type mapping; every row is `decided` (RFC 0171 §A). A host MUST NOT carry a private mapping. A vendor-prefixed v1 type the codemap does not name MUST be read under its own name unchanged, where "vendor-prefixed" means the first segment is an org registered in the `extensions` object of `spec/v2/declaration.json` (events.md §Rules; RFC 0171 §A.1 — `openwop.` is the only reserved prefix). An unregistered first segment is not a vendor prefix and falls to the refusal below.
12
+
13
+ ## The era key
14
+
15
+ `eventLogSchemaVersion` is the era key and is required on every run snapshot (`schemas/v2/run-snapshot.schema.json`).
16
+
17
+ | Value | Meaning |
18
+ | --- | --- |
19
+ | absent | On a store a v1 host has ever written, the run MUST read as `2` (v1 era). |
20
+ | `2` | v1 era; every reader translates through the codemap. |
21
+ | `3` | v2 era; a v2 host MUST stamp `3` on every run it creates. |
22
+ | `< 2` | The v1 rule is unchanged: snapshot fallback, no projection write-through. |
23
+
24
+ Discovery MUST advertise the value the host writes for new runs and nothing else; a host MUST hold one constant for this axis.
25
+
26
+ **Absent stays era `2` forever; it is never backfilled.** A host MUST NOT rewrite
27
+ historical rows to add an explicit `2`, and a reader MUST NOT require one. The
28
+ trichotomy is sound only because a v2 host stamps `3` on *every* run it creates:
29
+ if any creation path is left unstamped after the cut, the runs it makes are
30
+ indistinguishable from pre-cut runs and every reader will translate them as era
31
+ `2` — a silent wrong read, not an error. So a host with more than one creation
32
+ path MUST begin stamping `3` on **all** of them in the same change; staging that
33
+ across deploys is the failure this rule exists to prevent.
34
+
35
+ **Collapsing to one constant is a precondition for advertising, not a
36
+ consequence.** A host whose creation paths disagree — one writing `2`, another
37
+ writing nothing — has no single value to advertise, and whatever it publishes is
38
+ false for some of its own runs. Unify the writers first, then advertise. This is
39
+ the same class of constraint as the writer rule below and is ordered the same
40
+ way: the store is made coherent before the wire describes it.
41
+
42
+ **The snapshot field is required on the wire, and MAY be synthesized.**
43
+ `schemas/v2/run-snapshot.schema.json` requires `eventLogSchemaVersion`, but an
44
+ era-`2` run predates the key and has nothing stored. The snapshot is a read
45
+ projection, so the host MUST supply `2` from the absent-⇒-`2` rule rather than
46
+ fail the read; a missing *stored* era is not a read error. The consequence is
47
+ worth stating plainly: on the wire this field is never absent, so it cannot
48
+ falsify a host's era handling on its own. What falsifies that is the vocabulary
49
+ of the events themselves, which is why the reader and writer rules below carry
50
+ the obligation and this field only reports it.
51
+
52
+ ## The reader rule
53
+
54
+ A v2 host reading a run in era `2` MUST translate every event through the codemap at the storage boundary:
55
+
56
+ - `type` is mapped; the payload is projected per RFC 0171 §B.
57
+ - `sequence` MUST be preserved verbatim, including `0`.
58
+ - `eventId`, `timestamp`, `causationId`, and vendor fields pass through.
59
+ - A type the codemap does not name and that carries no reserved vendor prefix MUST fail the read with `event_type_unmapped` (`spec/v2/errors.json`, `500`). A malformed row MUST fail the read rather than default any field.
60
+
61
+ The rule binds every reader: poll, SSE, fork, replay divergence, debug bundle, summary memory. The translation is a read projection. A host MUST NOT rewrite era-`2` rows in place; a background backfill that stamps `3` and rewrites `type` under the same `(runId, sequence)` key 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, RFC 0041 §C).
62
+
63
+ ### The writer rule
64
+
65
+ The era key is fixed when the run is created and fixes the log's vocabulary for
66
+ the run's lifetime. An append to a run in era `2` MUST use v1 vocabulary — the
67
+ name the codemap maps *from*, not the v2 name it maps to. A host that upgrades
68
+ mid-flight MUST NOT begin writing v2 names into a log the reader translates as
69
+ era `2`: the reader would map an already-mapped name a second time, or fail the
70
+ read with `event_type_unmapped` on a name the codemap does not carry on its v1
71
+ side. A run created after the upgrade is era `3` and is written in v2
72
+ vocabulary, untranslated.
73
+
74
+ This binds every writer for as long as an era-`2` run stays open, which on a
75
+ host with human-approval interrupts can be days. Draining era-`2` runs before
76
+ serving v2 is not the path — see §"Runs pinned to v1" — so the writer rule is
77
+ what makes an in-flight run safe across the cut. Its witness is
78
+ `v2-era-2-append-vocabulary`.
79
+
80
+ ### The v1 wire of an era-`3` log
81
+
82
+ The reader rule above is written for a v2 reader of an era-`2` log. Through the
83
+ overlap a host serves BOTH majors (`versioning.md` §5) and v1 operations keep
84
+ their `/v1/…` path keys unchanged (§1.2), so the mirror case is forced and the
85
+ corpus owed it a rule: a run created today is era `3` and its log is stored in
86
+ v2 vocabulary, yet the same log must still be readable on `/v1/…` exactly as it
87
+ was before the cut.
88
+
89
+ A host serving both majors MUST therefore map an era-`3` log's `type` back to
90
+ its v1 spelling on the v1 read path, through the **same codemap row, inverted**.
91
+ This is well defined and not a private mapping: `spec/v2/event-codemap.json` is
92
+ a bijection — 118 rows, 118 distinct `v1` names, 118 distinct `v2` names, no
93
+ many-to-one fold — so the inverse of a row is exact. A host MUST verify that
94
+ property at load rather than assume it; if a future row folds two v1 names onto
95
+ one v2 name, the inverse stops being a function and the host MUST refuse to
96
+ serve the v1 representation rather than guess which spelling to emit.
97
+
98
+ Two alternatives are rejected, and naming them is the point of this section.
99
+ Storing v1 spellings under an era-`3` stamp makes the stamp a lie, and the
100
+ closed-enum scenario would pass it by luck on any run whose types happen to be
101
+ identity rows. Serving v2 names on `/v1/…` breaks the v1 wire, which the
102
+ overlap exists to preserve. Neither is a smaller change than the inverse map;
103
+ they are the same change with the honesty removed.
104
+
105
+ ### The seat
106
+
107
+ 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. A host leg MUST name its seat in its ADR; the `v1-events-translated` scenario reads through poll, SSE, and a fork so a wrapper-only adapter is caught (conformance.md).
108
+
109
+ ### Forking a v1 run
110
+
111
+ A fork of an era-`2` run MUST produce a prefix byte-equivalent to the translated parent, and its `run.started` MUST carry the legacy Subject where the parent had none (RFC 0176 §A.5; replay.md, identity.md).
112
+
113
+ ## Runs pinned to v1
114
+
115
+ A non-terminal run a v2 host inherits carries `version.pinned` events naming change ids. The host MUST continue it or cancel it, never follow a pin silently.
116
+
117
+ | Condition | Requirement |
118
+ | --- | --- |
119
+ | Every pinned change id is still implemented | The run MUST continue under the reader rule; the pin is honored verbatim and `version.pinned` is never rewritten. |
120
+ | Any pinned change id is no longer implemented | The host MUST cancel the run with `run.cancelled` reason `v1_pin_unsupported` and `cancelledBy: "v2-cutover"`; the certification bundle reports the count. |
121
+ | Suspended on an interrupt at the cut | The run continues under the row above; its outstanding token is resolvable under `kid: legacy` until `expiresAt` (RFC 0170 §E.1), and the run reads through the adapter. |
122
+
123
+ "Drain" is retired as the only path. Multi-region skew is read-side only: after the cut a v2 region MUST NOT accept an era-`2` write for a run it has already stamped `3` (RFC 0176 §B.3). Discovery's `minClientVersion` rule is RFC 0172 row `C5.8`.
124
+
125
+ ## Everything else a v1 host persisted
126
+
127
+ | Artifact | Requirement |
128
+ | --- | --- |
129
+ | 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). |
130
+ | Webhook deliveries | A host advertising both majors MUST dual-emit the `X-openwop-*` and `OpenWOP-*` header families through the overlap; a v2 receiver MUST accept a v1-signed delivery (`X-openwop-*`, scheme `v1`) verifying the same bytes; per-subscription secrets are unchanged; deliveries queued before the cut are drained under their own retry policy with the payload they were serialized with (webhooks.md). |
131
+ | 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). The rule is written over the prefix and never over a segment count: the v1 `{token}` path parameter carries no `pattern`, so a conforming v1 token may be a single opaque row key, and a count-based rule leaves such a host with no drain rule at all. |
132
+ | 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). |
133
+ | Owner stamps | A run without a Subject MUST be legacy-stamped at first v2 read and MUST NOT be rewritten later (RFC 0170 §A.3). A host's stored owner fields are projected to the Subject; the projection is the host's to name. |
134
+ | Audit log | Never upgraded (RFC 0170). |
135
+
136
+ ## Per-store disposition
137
+
138
+ A host's ADR MUST name every store it persists and give each one disposition from the closed set below.
139
+
140
+ | Disposition | Meaning |
141
+ | --- | --- |
142
+ | `unchanged` | Rows keep their shape and keys; a v2 reader consumes them as they are. |
143
+ | `translated` | Rows are read through the codemap at the storage boundary; never rewritten in place except the atomic per-run backfill. |
144
+ | `drained` | Rows complete under their own v1 contract until exhausted or expired; no new rows of the v1 shape are written. |
145
+ | `legacy-stamped` | A missing v2 field is given its legacy value at first v2 read and never rewritten. |
146
+ | `never-upgraded` | Rows remain v1 evidence only; v2 evidence is produced fresh. |
147
+ | `not-persisted` | Nothing to migrate. |
148
+
149
+ Template — one row per store:
150
+
151
+ | Store | v1 artifact | Disposition |
152
+ | --- | --- | --- |
153
+ | events | v1 vocabulary; `UNIQUE (runId, sequence)` | `translated` |
154
+ | runs | no `eventLogSchemaVersion`; owner fields | `legacy-stamped` |
155
+ | interrupts | two-segment tokens | `drained` |
156
+ | webhook subscriptions and queued deliveries | subscriptions; serialized deliveries | `unchanged`; `drained` |
157
+ | idempotency, invocation, outbox, correlation tables | keyed records | `unchanged` |
158
+ | audit log | audit facts | `never-upgraded` |
159
+ | certification bundles | v1 bundles | `never-upgraded` |
160
+ | host-internal tables | outside the wire | `unchanged` |
161
+
162
+ The reference hosts' dispositions are recorded in RFC 0176; a host MUST NOT decide a store's disposition during the migration.
163
+
164
+ ## The corpus-tag pin
165
+
166
+ A consumer that vendors any file from `schemas/`, `api/`, or `spec/` MUST pin to a published `openwop-conformance/vX.Y.Z` tag, MUST record the tag, and MUST refuse a sync from any other ref. A v1.x consumer MUST NOT vendor `schemas/v2/` (RFC 0176 §E.1). The `corpus-tag-pinned` check verifies that each consumer's recorded tag resolves (conformance.md).
167
+
168
+ See also: overview.md, events.md, replay.md, identity.md, webhooks.md.
@@ -0,0 +1,104 @@
1
+ # Replay and Fork
2
+
3
+ > **Status: Draft · v2.0.0-rc (2026-09-03) · RFC 0140, 0041, 0173 §C, 0176 §A.5.**
4
+
5
+ ## Why this exists
6
+
7
+ The event log makes any past state of a run reconstructible by folding events up to a sequence. `POST /runs/{runId}:fork` turns that into a wire surface: 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
+
9
+ ## The surface
10
+
11
+ A host that advertises `replay` (capabilities.md) serves `forkRun` (`api/v2/openapi.yaml`, `POST /runs/{runId}:fork`) and `getEffectSeamManifest` (`GET /host/effect-seams`). The `replay` facet (`spec/v2/facets/replay.schema.json`) is `{ modes[], retention?, effectSeamsManifest }`; `modes` enumerates `fork | branch | rerun`; `effectSeamsManifest` is the constant `/host/effect-seams`. There is no `sideEffectSuppression` field: suppression is the only conforming replay behavior (RFC 0173 §B, row `C6.2`) and `none` is not a value.
12
+
13
+ The request body is `{ mode, fromSeq?, runOptionsOverlay? }`, `mode ∈ replay | branch`. Events with `sequence < fromSeq` are fixed history; events `>= fromSeq` are re-executed.
14
+
15
+ | Rule | Requirement |
16
+ | --- | --- |
17
+ | `fromSeq` for `replay` | MAY be omitted; omission means `0` (full re-execution). |
18
+ | `fromSeq` for `branch` | MUST be supplied. |
19
+ | `runOptionsOverlay` | MUST be omitted or empty for `replay`; MAY be supplied for `branch`. |
20
+ | `fromSeq` out of range | `400`; a sequence absent from the source log is `422`. |
21
+ | Source run not visible to the caller | `404`. |
22
+ | Response | `201` `{ runId, sourceRunId, fromSeq, mode, status, eventsUrl }`; the fork is a new run with its own log. |
23
+
24
+ The fork's `owner.tenant` and `owner.subject` MUST be copied verbatim from the source run (RFC 0165 §B.4; identity.md).
25
+
26
+ ## Modes
27
+
28
+ **`replay`** re-executes the workflow against current code from `fromSeq`, consuming the source run's events as fixed history.
29
+
30
+ **`branch`** starts from the projected state at `fromSeq` with caller-supplied `runOptionsOverlay`. A branch is an independent run and is NOT deterministic by design; determinism and suppression apply only to the inherited prefix.
31
+
32
+ ## Byte-equivalence of the prefix
33
+
34
+ The replay contract is observable-output-sequence determinism, not bit-equivalent execution (RFC 0041 §C):
35
+
36
+ 1. The events at indices `[0, fromSeq)` MUST be byte-equivalent between source and replay, modulo per-region clock fields (RFC 0036 §E) and ULID time-component entropy when ULIDs are minted fresh. The range is half-open, matching §The surface: `sequence < fromSeq` is fixed history, and the event AT `fromSeq` is re-executed — so it is governed by §Divergence, not by this clause.
37
+ 2. `variables`, `channels`, and `status` of the run snapshot at each index in that range MUST be byte-equivalent.
38
+ 3. The bytes on the wire of underlying tool and LLM calls MAY differ, provided the observable state at each index is byte-equivalent.
39
+
40
+ A host MUST cache the observable result (return value, workflow-state effects, emitted events), not merely the tool-call boundary. The cache key for LLM-calling nodes is the RFC 0041 content-addressed key; for other tool-calling nodes it MUST be content-addressable, never a host-internal sequence number or timestamp.
41
+
42
+ ## Determinism caveats (`replay` mode)
43
+
44
+ 1. A side-effecting node MUST NOT call the external system twice; see §Suppression.
45
+ 2. `ctx.interrupt(K)` MUST short-circuit to the persisted `interrupt.resolved` value.
46
+ 3. `ctx.getVersion` pins from the source run are fixed history; the replay MUST take the recorded branch.
47
+ 4. Nodes MUST consume time via `ctx.now()` where available; direct clock reads make replay non-deterministic.
48
+ 5. Recorded-fact events such as `memory.written` (RFC 0057) 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.
49
+ 6. Approver eligibility recorded on a resume event is fixed history; a host MUST NOT re-resolve membership during replay (RFC 0104).
50
+
51
+ ## Divergence
52
+
53
+ When a replayed node produces an event different from the source at the same sequence, the host MUST continue, MUST emit `replay.diverged` `{ originalEventId, replayEventId, divergencePoint }`, and MUST surface it in `debug` stream mode and as OTel attribute `openwop.replay.diverged: true`.
54
+
55
+ | Code (`spec/v2/errors.json`) | Where | Condition |
56
+ | --- | --- | --- |
57
+ | `replay_diverged_at_refusal` | fork fails, `409` | The source obtained a valid envelope and the replay a refusal, or the reverse. The host MUST NOT substitute silently; it MUST emit `replay.diverged-at-refusal` naming the node and both envelope kinds and fail the replay with this code (RFC 0041 §B). |
58
+ | `replay_source_missing` | `node.failed` payload; the fork request still returns `201` | A side-effecting node reached with no recorded source outcome for `(nodeId, attempt)` (§Suppression). |
59
+ | `replay_memory_snapshot_unavailable` | fork refused, `409` | The host cannot serve memory state as-of `fromSeq`. It MUST refuse rather than substitute current memory; `details.fromSeq` SHOULD name the index (RFC 0039 §B). |
60
+
61
+ ## Suppression
62
+
63
+ Suppression is an obligation of the `replay` surface (RFC 0173 §A.1, §B): advertising `replay` binds it, and a host that cannot suppress MUST NOT advertise `replay`. A relaxation is an operator setting recorded in the certification bundle (security-defaults.md).
64
+
65
+ For a fork with `mode: replay`:
66
+
67
+ 1. A node that performs an external side effect — any operation observable outside the run's own event log — MUST NOT perform it.
68
+ 2. The host MUST resolve the node's outcome from the source run's recorded terminal outcome for the same `(nodeId, attempt)`, keyed on `(sourceRunId, nodeId, attempt)` and never on the fork's own `runId` (the Layer-2 key includes `runId`, so it cannot span a fork).
69
+ 3. Absent a recorded outcome, the host MUST fail the node closed with `replay_source_missing`, MUST NOT perform the effect, and MUST NOT substitute a synthesized or empty success.
70
+ 4. A node whose pack manifest declares `role: "side-effect"` MUST be treated as side-effecting; a host classifier MAY add nodes and MUST NOT remove any. A throwing seam satisfies rule 1 only.
71
+ 5. The guarantee is whole-run and requires both classification before execution and a default-deny guard at every effect seam.
72
+ 6. A dispatch to a peer host is an outbound call under rule 2; the peer is never contacted.
73
+
74
+ Pure nodes and LLM calls served from the invocation log MUST re-execute live; otherwise divergence detection is vacuous.
75
+
76
+ **Fan-out.** A host that projects its log outward — webhook delivery, outbound streams, analytics or audit sinks — MUST NOT deliver events a replay re-emits as fixed history. 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).
77
+
78
+ **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.
79
+
80
+ ### The effect-seam manifest
81
+
82
+ A host advertising `replay` MUST publish `schemas/v2/effect-seam-manifest.schema.json`-shaped data at `GET /host/effect-seams` (RFC 0173 §C.1): `{ manifestVersion: "1", host: { name, build }, seams[] }`, one row per outbound effect path its node runtime can reach, `{ seam, kind, guarded: true, guardedBy, branchReFires?, note? }`. The host owns the list. The suite drives one seam of each kind it can reach and observes no re-fire (`effect-seam-manifest`, conformance.md).
83
+
84
+ `kind` names the outbound **wire mechanism** the seam leaves the host by — not the suite's driving mechanism, and not the business purpose. It MUST be one of `http`, `smtp`, `queue`, `storage`, `provider-sdk`, `webhook-fanout`, `other`. Two seams that 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. `other` is the escape for a mechanism this list does not name — raw TCP, gRPC, a filesystem write, a device SDK — and a row using it MUST carry `note` naming that mechanism, so an unnameable seam is still auditable and none can hide behind a vague label.
85
+
86
+ **Completeness outranks driveability.** Every outbound effect path the node runtime can reach MUST be listed, including one the suite has no way to drive: the suite's receiver speaks HTTP, so an `smtp` or `other` row is typically unreachable by it. Such a row is still MUST-list, and the scenario records it `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: an undriveable row that is listed is a known gap in the evidence, while a mislabelled or missing one is a false statement about the host. A seam omitted from the manifest is invisible to the suite: the manifest is a self-declaration whose false negatives are found by audit — negative-existence, not a witness.
87
+
88
+ ## Replay-from-event-log internals
89
+
90
+ 1. Load the source run's events with `sequence < fromSeq` through the storage boundary, where an era-`2` log is translated (persistence.md).
91
+ 2. Fold them to a projected state.
92
+ 3. Initialize the new run with that state, copy-on-write into its own log.
93
+ 4. For `replay`, resolve side-effecting nodes from the source run's recorded outcomes keyed on `(sourceRunId, nodeId, attempt)`; LLM invocations additionally consult the invocation log via the RFC 0041 content-addressed key.
94
+ 5. For `branch`, executor invocations create new invocation-log entries keyed on the new `runId`.
95
+
96
+ ## Forking a v1 run
97
+
98
+ A v2 host MUST fork a run created before the cut (era `2`, persistence.md). The fork's prefix MUST be byte-equivalent to the *translated* parent — the parent as read through the codemap, not its stored bytes — and `run.started` on the fork MUST carry the legacy Subject (`issuer: urn:openwop:legacy`, identity.md) where the parent had none (RFC 0176 §A.5, scenario `fork-a-v1-run`). A backfill of an era-`2` log is permitted only atomically per run with the original preserved, so this obligation stays checkable.
99
+
100
+ ## Retention
101
+
102
+ 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.
103
+
104
+ See also: events.md, runs.md, persistence.md, security-defaults.md.