@openwop/openwop-conformance 2.45.2 → 2.45.4

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 (56) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/README.md +2 -2
  3. package/dist/cli.js +1 -1
  4. package/dist/lib/certification-bundle-verify.js +22 -1
  5. package/dist/lib/profiles.js +17 -2
  6. package/dist/lib/requirement-registry.js +15 -0
  7. package/dist/lib/scenario-disposition.js +60 -9
  8. package/dist/spec-artifacts.lock.json +2 -2
  9. package/fixtures/conformance-budget-tool-calls.json +81 -0
  10. package/fixtures/conformance-fs-probe.json +25 -0
  11. package/fixtures/conformance-queue-consume.json +27 -0
  12. package/fixtures/conformance-queue-publish.json +29 -0
  13. package/fixtures/conformance-safefetch-probe.json +23 -0
  14. package/fixtures/conformance-secret-resolve-then-fail.json +33 -0
  15. package/fixtures/conformance-storage-probe.json +27 -0
  16. package/fixtures/conformance-tool-scope-probe.json +27 -0
  17. package/fixtures/openwop-secrets-run-witness.json +29 -0
  18. package/fixtures.md +113 -1
  19. package/package.json +2 -2
  20. package/requirement-aliases.json +72 -71
  21. package/requirements.json +1081 -43
  22. package/scenario-majors.json +45 -3
  23. package/schemas/CORPUS-STAMP.json +22 -22
  24. package/src/cli.ts +1 -1
  25. package/src/lib/backpressure-witness.ts +163 -0
  26. package/src/lib/budget-witness.ts +165 -0
  27. package/src/lib/certification-bundle-verify.ts +23 -2
  28. package/src/lib/driver.ts +61 -2
  29. package/src/lib/major-profile.ts +93 -0
  30. package/src/lib/memoryAttribution.ts +41 -6
  31. package/src/lib/polling.ts +2 -18
  32. package/src/lib/profiles.ts +26 -2
  33. package/src/lib/requirement-registry.ts +12 -0
  34. package/src/lib/run-secrets-witness.ts +240 -0
  35. package/src/lib/scenario-disposition.ts +54 -7
  36. package/src/lib/scratch-host.ts +130 -0
  37. package/src/lib/secret-scan.ts +141 -0
  38. package/src/lib/timeout-scale.ts +25 -0
  39. package/src/lib/triggerBridge.ts +69 -1
  40. package/src/scenarios/byok-roundtrip.test.ts +20 -4
  41. package/src/scenarios/runner-ledger.test.ts +2 -0
  42. package/src/scenarios/secrets-run-witness.test.ts +154 -0
  43. package/src/scenarios/trigger-bridge-delivery.test.ts +183 -126
  44. package/src/scenarios/trigger-refused-event-keeps-subscription.test.ts +141 -0
  45. package/src/scenarios/v2-budget-enforcement.test.ts +94 -0
  46. package/src/scenarios/v2-eval-mode-unadvertised-refused.test.ts +55 -0
  47. package/src/scenarios/v2-fs-sandbox-escape-refused.test.ts +161 -0
  48. package/src/scenarios/v2-memory-cross-tenant-isolation.test.ts +113 -0
  49. package/src/scenarios/v2-production-backpressure.test.ts +74 -0
  50. package/src/scenarios/v2-queue-cross-tenant-isolation.test.ts +118 -0
  51. package/src/scenarios/v2-safefetch-ssrf-refused.test.ts +198 -0
  52. package/src/scenarios/v2-secret-canary-absent.test.ts +211 -0
  53. package/src/scenarios/v2-secrets-run-witness.test.ts +167 -0
  54. package/src/scenarios/v2-storage-cross-tenant-isolation.test.ts +213 -0
  55. package/src/scenarios/v2-tool-authorization-fail-closed.test.ts +197 -0
  56. package/src/scenarios/v2-workspace-scope-from-identity.test.ts +146 -0
@@ -0,0 +1,154 @@
1
+ /**
2
+ * RFC 0229 — a production host can witness secret resolution without an oracle
3
+ * (`spec/v1/capabilities.md` §"Run-supplied secrets"; suite 2.45.3, target
4
+ * major 1; gated on `capabilities.secrets.runSecrets` and the
5
+ * `openwop-secrets-run-witness` fixture). The v2 twin is
6
+ * `v2-secrets-run-witness`, and the four rows are the same requirement ids.
7
+ *
8
+ * This file is the SECOND floor path of the v1 `openwop-secrets` profile
9
+ * (`profiles.md` §`openwop-secrets`): the floor is an any-of group, satisfied
10
+ * by a witnessed pass of either `byok-roundtrip.test.ts` or this file. A
11
+ * production host can take this path: the node reads only a `run:` ref, which
12
+ * resolves only to a value supplied with the same run, and outputs only
13
+ * `{ matched }`, so it is no oracle over a stored secret.
14
+ *
15
+ * Rows: `run-witness-resolves`, `run-witness-redacted`,
16
+ * `run-witness-scope-bound`, `run-secrets-outside-request-digest` — see the v2
17
+ * twin for what each observes. Dispositions (RFC 0229 §F): facet absent ⇒
18
+ * `inapplicable`; facet without the fixture ⇒ `blocked`; a `createRun` that
19
+ * refuses a well-formed `runSecrets` ⇒ `executed-fail`.
20
+ *
21
+ * @see spec/v1/capabilities.md §"Run-supplied secrets"
22
+ * @see spec/v1/profiles.md §openwop-secrets
23
+ * @see RFCS/0229-production-safe-secrets-witness.md
24
+ */
25
+
26
+ import { describe, it, expect } from 'vitest';
27
+ import { randomUUID } from 'node:crypto';
28
+ import { req } from '../lib/requirement-ids.js';
29
+ import { readErrorCode } from '../lib/error-envelope.js';
30
+ import { blockedDespiteAssertions, softSkip } from '../lib/soft-skip.js';
31
+ import { CANARY_NAME, WITNESS_REF, WitnessHost, freshName, freshValue, hits, sha256Hex } from '../lib/run-secrets-witness.js';
32
+
33
+ const DOC = 'capabilities.md §"Run-supplied secrets" (RFC 0229)';
34
+ const host = new WitnessHost(1);
35
+
36
+ interface Witnessed { runId: string; createText: string; status: string; matched: boolean | undefined; value: string; digestSupplied: boolean }
37
+ type Outcome = { runs: [Witnessed, Witnessed] } | { skip: 'inapplicable' | 'blocked'; reason: string } | { refused: { status: number; detail: string } };
38
+
39
+ /** Run A (`C`, `sha256(C)`) and run B (`D`, the digest of a different value), once per file. */
40
+ let pair: Promise<Outcome> | undefined;
41
+ function witnessedPair(): Promise<Outcome> {
42
+ pair ??= (async (): Promise<Outcome> => {
43
+ const g = await host.gate();
44
+ if (!g.ok) return { skip: g.kind, reason: g.reason };
45
+ const out: Witnessed[] = [];
46
+ for (const same of [true, false]) {
47
+ const value = freshValue();
48
+ const expected = same ? sha256Hex(value) : sha256Hex(freshValue());
49
+ const res = await host.create({ ref: WITNESS_REF, expectedSha256: expected }, [{ ref: WITNESS_REF, value }]);
50
+ if (res === null) return { skip: 'blocked', reason: 'POST /v1/runs unreachable (fetch failed)' };
51
+ const runId = (res.json as { runId?: unknown } | null)?.runId;
52
+ if (res.status !== 201 || typeof runId !== 'string') return { refused: { status: res.status, detail: `POST /v1/runs with a well-formed runSecrets answered ${res.status} ${readErrorCode(res.json) ?? ''}`.trim() } };
53
+ const snap = await host.waitTerminal(runId);
54
+ out.push({ runId, createText: res.text, status: String(snap?.['status']), matched: await host.matched(runId, snap), value, digestSupplied: same });
55
+ }
56
+ return { runs: [out[0]!, out[1]!] };
57
+ })();
58
+ return pair;
59
+ }
60
+
61
+ /** A run that must end in `witness`'s `node.failed`; returns the code, or a blocked reason. */
62
+ async function failedCode(inputs: { ref: string; expectedSha256: string }, runSecrets?: ReadonlyArray<{ ref: string; value: string }>): Promise<{ code: string | null; status: string } | { reason: string }> {
63
+ const res = await host.create(inputs, runSecrets);
64
+ if (res === null) return { reason: 'POST /v1/runs unreachable (fetch failed)' };
65
+ const runId = (res.json as { runId?: unknown } | null)?.runId;
66
+ if (res.status !== 201 || typeof runId !== 'string') return { reason: `POST /v1/runs answered ${res.status} ${readErrorCode(res.json) ?? ''}`.trim() };
67
+ const snap = await host.waitTerminal(runId);
68
+ return { code: await host.nodeFailedCode(runId), status: String(snap?.['status']) };
69
+ }
70
+
71
+ describe('RFC 0229 — secrets-run-witness (gated on secrets.runSecrets + openwop-secrets-run-witness)', () => {
72
+ it('a run-supplied secret resolves intact: matched true for its digest, false for another', async () => {
73
+ const o = await witnessedPair();
74
+ if ('skip' in o) return softSkip(o.skip, o.reason);
75
+ const id = 'openwop.requirement.secrets.run-witness-resolves';
76
+ if ('refused' in o) {
77
+ expect(o.refused.status, req(id, DOC, `a host advertising runSecrets MUST accept a well-formed runSecrets on createRun (${o.refused.detail})`)).toBe(201);
78
+ return blockedDespiteAssertions(`${o.refused.detail} with no runId — no run to witness`);
79
+ }
80
+ const [a, b] = o.runs;
81
+ expect(a.status, req(id, DOC, 'the witness run with a matching digest MUST complete')).toBe('completed');
82
+ expect(b.status, req(id, DOC, 'the witness run with a different digest MUST complete (a mismatch is an output, not a failure)')).toBe('completed');
83
+ if (a.matched === undefined || b.matched === undefined) return blockedDespiteAssertions('the runs completed but witness\'s `matched` output was on no readable surface (node.completed outputs or the snapshot)');
84
+ expect(a.matched, req(id, DOC, 'witness MUST output matched: true when expectedSha256 is the digest of the supplied value')).toBe(true);
85
+ expect(b.matched, req(id, DOC, 'witness MUST output matched: false when expectedSha256 is the digest of a different value')).toBe(false);
86
+ }, 90_000);
87
+
88
+ it('a run-supplied secret appears on no readable surface, in any encoding or as its digest', async () => {
89
+ const o = await witnessedPair();
90
+ if ('skip' in o) return softSkip(o.skip, o.reason);
91
+ if ('refused' in o) return softSkip('blocked', `${o.refused.detail} — no run to scan (row run-witness-resolves convicts the refusal)`);
92
+ const id = 'openwop.requirement.secrets.run-witness-redacted';
93
+ for (const r of o.runs) {
94
+ const s = await host.surfaces(r.runId, [r.createText]);
95
+ expect(hits(s.text, r.value, r.digestSupplied), req(id, DOC, `the supplied value MUST NOT appear on any surface (read: ${s.read.join(', ')}), raw, base64, hex, percent- or JSON-encoded, or as its SHA-256 where the suite did not send that digest itself`)).toEqual([]);
96
+ }
97
+ }, 90_000);
98
+
99
+ it('a run: ref resolves only within the run that supplied it, and the witness refuses any other ref', async () => {
100
+ const g = await host.gate();
101
+ if (!g.ok) return softSkip(g.kind, g.reason);
102
+ const id = 'openwop.requirement.secrets.run-witness-scope-bound';
103
+ const v = freshValue();
104
+ const cases: Array<{ what: string; inputs: { ref: string; expectedSha256: string }; runSecrets?: Array<{ ref: string; value: string }>; code: string }> = [
105
+ { what: `the canary name ${CANARY_NAME}`, inputs: { ref: CANARY_NAME, expectedSha256: sha256Hex(v) }, runSecrets: [{ ref: WITNESS_REF, value: v }], code: 'credential_forbidden' },
106
+ { what: 'a fresh name without run:', inputs: { ref: freshName(), expectedSha256: sha256Hex(v) }, runSecrets: [{ ref: WITNESS_REF, value: v }], code: 'credential_forbidden' },
107
+ { what: `${WITNESS_REF} with no runSecrets`, inputs: { ref: WITNESS_REF, expectedSha256: sha256Hex(v) }, code: 'credential_not_found' },
108
+ ];
109
+ const earlier = freshValue();
110
+ const first = await host.create({ ref: WITNESS_REF, expectedSha256: sha256Hex(earlier) }, [{ ref: WITNESS_REF, value: earlier }]);
111
+ const firstId = (first?.json as { runId?: unknown } | null)?.runId;
112
+ if (first === null || first.status !== 201 || typeof firstId !== 'string') return softSkip('blocked', `the earlier run's POST /v1/runs answered ${first?.status ?? 'nothing'} ${readErrorCode(first?.json) ?? ''}`.trim());
113
+ await host.waitTerminal(firstId);
114
+ cases.push({ what: `an earlier run's ref (${WITNESS_REF}) while this run supplies only run:openwop-other`, inputs: { ref: WITNESS_REF, expectedSha256: sha256Hex(earlier) }, runSecrets: [{ ref: 'run:openwop-other', value: freshValue() }], code: 'credential_not_found' });
115
+ for (const c of cases) {
116
+ const r = await failedCode(c.inputs, c.runSecrets);
117
+ if ('reason' in r) return blockedDespiteAssertions(`${c.what}: ${r.reason}`);
118
+ expect(r.code, req(id, DOC, `${c.what}: witness MUST end in node.failed with ${c.code} (run status ${r.status})`)).toBe(c.code);
119
+ }
120
+ // §A.4: gated on replay advertising `branch` (RFC 0229 §F row 3).
121
+ if (g.branchFork) {
122
+ const fork = await host.fork(firstId);
123
+ const forkId = (fork?.json as { runId?: unknown } | null)?.runId;
124
+ // partial-witness-ok: the four scope legs above observed the requirement; this fork leg is conditional on a fork the host serves (the fork contract is not this row's)
125
+ if (fork === null || fork.status !== 201 || typeof forkId !== 'string') return softSkip('inapplicable', `replay advertises branch, but POST /v1/v1/runs/{runId}:fork answered ${fork?.status ?? 'nothing'} ${readErrorCode(fork?.json) ?? ''} — the fork leg did not run (rest-endpoints.md owns the fork contract)`.trim());
126
+ await host.waitTerminal(forkId);
127
+ expect(await host.nodeFailedCode(forkId), req(id, DOC, 'a branch fork taken before witness MUST NOT inherit runSecrets: witness fails credential_not_found')).toBe('credential_not_found');
128
+ }
129
+ }, 180_000);
130
+
131
+ it('the idempotency request digest excludes runSecrets: a retry differing only there replays, and its value is discarded', async () => {
132
+ const g = await host.gate();
133
+ if (!g.ok) return softSkip(g.kind, g.reason);
134
+ const id = 'openwop.requirement.secrets.run-secrets-outside-request-digest';
135
+ const key = `openwop-witness-${randomUUID()}`;
136
+ const c = freshValue();
137
+ const c2 = freshValue();
138
+ const inputs = { ref: WITNESS_REF, expectedSha256: sha256Hex(c) };
139
+ const first = await host.create(inputs, [{ ref: WITNESS_REF, value: c }], key);
140
+ const runId = (first?.json as { runId?: unknown } | null)?.runId;
141
+ if (first === null) return softSkip('blocked', 'POST /v1/runs unreachable (fetch failed)');
142
+ expect(first.status, req(id, DOC, 'a host advertising runSecrets MUST accept a well-formed runSecrets on createRun')).toBe(201);
143
+ if (typeof runId !== 'string') return blockedDespiteAssertions('the 201 carried no runId');
144
+ const retry = await host.create(inputs, [{ ref: WITNESS_REF, value: c2 }], key);
145
+ if (retry === null) return blockedDespiteAssertions('the same-key retry was unreachable (fetch failed)');
146
+ expect(readErrorCode(retry.json), req(id, `${DOC}; idempotency.md §Outcomes`, 'a same-key retry differing only in runSecrets MUST NOT be 409 idempotency_key_mismatch — the request digest MUST NOT cover runSecrets')).not.toBe('idempotency_key_mismatch');
147
+ expect(retry.status, req(id, DOC, 'the retry MUST be answered from cache with the original 201')).toBe(201);
148
+ expect((retry.json as { runId?: unknown } | null)?.runId, req(id, DOC, 'the retry MUST name the original runId')).toBe(runId);
149
+ expect(retry.headers.get('openwop-idempotent-replay'), req(id, 'idempotency.md §Outcomes', 'a response served from cache MUST carry OpenWOP-Idempotent-Replay: true')).toBe('true');
150
+ await host.waitTerminal(runId);
151
+ const s = await host.surfaces(runId, [first.text, retry.text]);
152
+ expect(hits(s.text, c2), req(id, DOC, `the retried value MUST be discarded unused: it appears on no surface of the run (read: ${s.read.join(', ')})`)).toEqual([]);
153
+ }, 90_000);
154
+ });
@@ -2,29 +2,35 @@
2
2
  * Durable trigger bridge — delivery model (RFC 0083 §C) — behavioral.
3
3
  *
4
4
  * Profile-gated on `openwop-trigger-bridge` (derived from the live discovery
5
- * doc per RFC 0083 §D: the bridge advertised + a dead-letter sink + a durable
6
- * source). Soft-skips when the profile isn't derived (default) / hard-fails
7
- * under `OPENWOP_REQUIRE_BEHAVIOR=true`. The always-on wire-shape coverage
8
- * lives in `trigger-bridge-shape.test.ts`; this asserts host BEHAVIOR via the
9
- * `POST /v1/host/sample/trigger-bridge/deliver` seam + the test event-log seam:
5
+ * doc per RFC 0083 §D). Soft-skips when the profile isn't derived / hard-fails
6
+ * under `OPENWOP_REQUIRE_BEHAVIOR=true`. The always-on wire-shape coverage lives
7
+ * in `trigger-bridge-shape.test.ts`.
10
8
  *
11
- * 1. DEDUP (§C-1) — the same `dedupKey` delivered twice is effectively-once:
12
- * exactly one `trigger.delivery.attempted { outcome:"delivered" }` for that
13
- * key (at-least-once collapses to once within the retention window).
14
- * 2. RETRY → DEAD-LETTER (§C-2 + RFC 0053) — an exhausted retry policy lands a
15
- * terminal `trigger.delivery.attempted { outcome:"dead-lettered" }` and a
16
- * `trigger.subscription.state.changed { toState:"dead-lettered" }`; both
17
- * content-free (SR-1: ids/states/counters only).
18
- * 3. CAUSATION (§C / RFC 0040) — a successful delivery's resulting run carries
19
- * `run.started.causationId` == the delivery id (trigger → run is resolvable
20
- * via `/ancestry`).
9
+ * Four legs, each its own requirement (RFC 0230 split the one combined `it`):
21
10
  *
22
- * Each leg soft-skips independently (seam absent / event-log seam absent).
11
+ * 1. DEDUP (§C-1) — a repeated delivery is effectively-once.
12
+ * 2. DEAD-LETTER (§C-2 + RFC 0053) — an exhausted/refused delivery starts no run.
13
+ * 3. CAUSATION (§C / RFC 0040) — a delivered run carries the delivery as
14
+ * `run.started.causationId`.
15
+ * 4. RUN-LESS CONTENT-FREENESS (SR-1) — the run-less `trigger.*` events
16
+ * (a dead-lettered attempt, `trigger.subscription.state.changed`) carry no
17
+ * inbound content.
18
+ *
19
+ * TWO WITNESS PATHS. The SEAM path (primary, whenever the host serves them)
20
+ * drives `POST /v1/host/sample/trigger-bridge/deliver` and reads the test
21
+ * event-log seam. When the seams are absent — as they are on every production
22
+ * host — legs 1–3 run the NORMATIVE-SURFACE path of RFC 0230 if the host
23
+ * advertises `triggerBridge.ingestion.inboundSigning: ["standard-webhooks-1"]`:
24
+ * register a webhook subscription and POST Standard-Webhooks-signed bodies to its
25
+ * `ingestUrl` with no OpenWOP credential. Each row notes which path ran
26
+ * (`observed:`). Leg 4 has no normative-surface path — the run-less events are
27
+ * on no run's log — so it stays seam-witnessed and records seam-absent
28
+ * otherwise; it remains in the floor (the floor never narrows).
23
29
  *
24
30
  * Spec references:
25
- * - https://github.com/openwop/openwop/blob/main/spec/v1/trigger-bridge.md (§C)
26
- * - https://github.com/openwop/openwop/blob/main/RFCS/0083-durable-trigger-and-channel-bridge-profile.md
27
- * - https://github.com/openwop/openwop/blob/main/spec/v1/profiles.md (§openwop-trigger-bridge)
31
+ * - spec/v1/trigger-bridge.md (§C, §F.2, §F.6)
32
+ * - RFCS/0083-durable-trigger-and-channel-bridge-profile.md, RFCS/0230-inbound-webhook-ingest-contract.md
33
+ * - spec/v1/profiles.md (§openwop-trigger-bridge)
28
34
  */
29
35
 
30
36
  import { describe, it, expect } from 'vitest';
@@ -35,144 +41,195 @@ import {
35
41
  driveDelivery,
36
42
  DELIVERY_OUTCOMES,
37
43
  SUBSCRIPTION_STATES,
44
+ freshDedupKey,
45
+ inboundSigningAdvertised,
46
+ registerSignedWebhook,
47
+ signedIngest,
48
+ freshWebhookId,
38
49
  } from '../lib/triggerBridge.js';
39
50
  import { queryTestEvents, requireEvents, isEventLogSeamAvailable, resetTestSeam } from '../lib/event-log-query.js';
40
51
  import { req } from '../lib/requirement-ids.js';
41
- import { freshDedupKey } from '../lib/triggerBridge.js';
52
+ import { noteObservation } from '../lib/row-observation.js';
53
+ import { driver } from '../lib/driver.js';
54
+
55
+ const R_DEDUP = 'openwop.requirement.0083.trigger-delivery.dedup';
56
+ const R_DEAD_LETTER = 'openwop.requirement.0083.trigger-delivery.dead-letter';
57
+ const R_CAUSATION = 'openwop.requirement.0083.trigger-delivery.causation';
58
+ const R_RUNLESS = 'openwop.requirement.0083.trigger-delivery.runless-content-free';
42
59
 
43
60
  const CONTENT_FREE_FORBIDDEN = ['body', 'headers', 'payload', 'secret', 'credentials', 'token', 'apiKey'];
44
61
 
45
- function expectContentFree(payload: Record<string, unknown>, where: string): void {
62
+ function expectContentFree(id: string, payload: Record<string, unknown>, where: string): void {
46
63
  for (const f of CONTENT_FREE_FORBIDDEN) {
47
- expect(
48
- !(f in payload),
49
- req('openwop.it.trigger-bridge-delivery.de-dups-by-dedupkey-retries-to-dead-letter-and-links-delivery-run-causation', 'RFC 0083 §C (SR-1)', `${where} MUST be content-free (no ${f})`),
50
- ).toBe(true);
64
+ expect(!(f in payload), req(id, 'RFC 0083 §C (SR-1)', `${where} MUST be content-free (no ${f})`)).toBe(true);
51
65
  }
52
66
  }
53
67
 
54
- describe('trigger-bridge-delivery (RFC 0083 §C)', () => {
55
- it('de-dups by dedupKey, retries to dead-letter, and links delivery→run causation', async () => {
56
- if (!behaviorGate('openwop-trigger-bridge', await isTriggerBridgeProfileAdvertised())) return;
57
- if (!(await isEventLogSeamAvailable())) return seamAbsent('host advertises openwop-trigger-bridge but the event-log seam is absent');
58
-
59
- // ---- Leg 1: dedup → effectively-once (§C-1) ---------------------------
60
- //
61
- // The dedupKey is MINTED PER EXERCISE, and that is load-bearing (2.37.0).
62
- // It used to be the literal `'conformance-dedup-key'`, and §C-1's dedup
63
- // window is a ≥24h FLOOR — so the second run of this file against the same
64
- // host, any time that day, hands the bridge a key it has already delivered.
65
- // A CONFORMANT host then collapses the whole exercise into the first run's
66
- // outcome and emits ZERO `delivered` attempts under this run's id, and the
67
- // `=== 1` below convicts it. Cold host passes, warm host fails, nothing
68
- // about the host having changed — the same shape as the RFC 0158 row that
69
- // shared one effect identity between two files (`lib/effect-receiver.ts`).
70
- // The REPETITION that §C-1 is about happens INSIDE `driveDelivery`'s
71
- // `scenario: 'dedup'`, so a fresh key per exercise removes the cross-run
72
- // collision without weakening the clause.
68
+ type Path = 'seam' | 'normative' | null;
69
+
70
+ /** Which witness path this host supports: the seams when present, else RFC 0230's. */
71
+ async function witnessPath(): Promise<Path> {
72
+ if (await isEventLogSeamAvailable()) return 'seam';
73
+ if (await inboundSigningAdvertised()) return 'normative';
74
+ return null;
75
+ }
76
+
77
+ const NO_PATH = 'host advertises openwop-trigger-bridge but neither the event-log seam nor an RFC 0230 inboundSigning ingest is available';
78
+
79
+ /** The bare opaque id of a (possibly tenant-bound) runId — the v1 read path. */
80
+ function bareRunId(runId: string): string {
81
+ return runId.includes('/') ? runId.slice(runId.lastIndexOf('/') + 1) : runId;
82
+ }
83
+
84
+ /** The seam witness path (primary when the host serves the seams). */
85
+ async function dedupViaSeam(): Promise<undefined> {
86
+ // The dedupKey is MINTED PER EXERCISE, and that is load-bearing (2.37.0):
87
+ // §C-1's dedup window is a ≥24h floor, so a literal key collides with an
88
+ // earlier run the same day and convicts a conformant host. The repetition
89
+ // §C-1 is about happens INSIDE `driveDelivery`'s `scenario: 'dedup'`.
73
90
  const dedupKey = freshDedupKey('queue');
74
91
  const dedup = await driveDelivery({ scenario: 'dedup', dedupKey, source: 'queue' });
75
92
  if (dedup === null) return seamAbsent('host advertises openwop-trigger-bridge but the delivery seam is unwired');
76
-
77
- // The profile is derived AND the seam is wired — missing evidence is a
78
- // FAILURE, not a soft-skip. A repeated dedupKey MUST be effectively-once:
79
- // EXACTLY one delivered attempt for the key (zero would mean no delivery at all).
80
- const dedupQueryId = dedup.runId ?? '__dedup__';
81
93
  const dedupEvents = requireEvents(
82
- await queryTestEvents(dedupQueryId, { type: 'trigger.delivery.attempted' }),
94
+ await queryTestEvents(dedup.runId ?? '__dedup__', { type: 'trigger.delivery.attempted' }),
83
95
  'trigger.delivery.attempted (dedup)',
84
96
  );
85
- const deliveredForKey = dedupEvents.filter(
86
- (e) => e.payload.dedupKey === dedupKey && e.payload.outcome === 'delivered',
87
- );
88
- expect(
89
- deliveredForKey.length === 1,
90
- req('openwop.it.trigger-bridge-delivery.de-dups-by-dedupkey-retries-to-dead-letter-and-links-delivery-run-causation', 'trigger-bridge.md §C-1', 'a repeated dedupKey MUST be effectively-once — EXACTLY one delivered attempt (not zero, not two)'),
91
- ).toBe(true);
97
+ const deliveredForKey = dedupEvents.filter((e) => e.payload.dedupKey === dedupKey && e.payload.outcome === 'delivered');
98
+ expect(deliveredForKey.length === 1, req(R_DEDUP, 'trigger-bridge.md §C-1', 'a repeated dedupKey MUST be effectively-once — EXACTLY one delivered attempt (not zero, not two)')).toBe(true);
92
99
  for (const e of dedupEvents) {
93
- expect(
94
- typeof e.payload.outcome === 'string' && DELIVERY_OUTCOMES.includes(e.payload.outcome as string),
95
- req('openwop.it.trigger-bridge-delivery.de-dups-by-dedupkey-retries-to-dead-letter-and-links-delivery-run-causation', 'run-event-payloads.schema.json#triggerDeliveryAttempted', 'outcome MUST be delivered|retrying|dead-lettered'),
96
- ).toBe(true);
97
- expectContentFree(e.payload, 'trigger.delivery.attempted');
100
+ expect(typeof e.payload.outcome === 'string' && DELIVERY_OUTCOMES.includes(e.payload.outcome as string), req(R_DEDUP, 'run-event-payloads.schema.json#triggerDeliveryAttempted', 'outcome MUST be delivered|retrying|dead-lettered')).toBe(true);
101
+ expectContentFree(R_DEDUP, e.payload, 'trigger.delivery.attempted');
98
102
  }
103
+ noteObservation('seam path');
104
+ await resetTestSeam();
105
+ return undefined;
106
+ }
99
107
 
100
- // ---- Leg 2: retry → dead-letter (§C-2 + RFC 0053) --------------------
108
+ /** The seam witness path (primary when the host serves the seams). */
109
+ async function deadLetterViaSeam(): Promise<undefined> {
101
110
  const exhaust = await driveDelivery({ scenario: 'exhaust', source: 'webhook' });
102
- expect(
103
- exhaust !== null,
104
- req('openwop.it.trigger-bridge-delivery.de-dups-by-dedupkey-retries-to-dead-letter-and-links-delivery-run-causation', 'trigger-bridge.md §C-2', 'the exhaust scenario MUST be wired when the delivery seam is'),
105
- ).toBe(true);
106
- const exKey = exhaust!.runId ?? '__exhaust__';
107
- const exhaustEvents = requireEvents(
108
- await queryTestEvents(exKey, { type: 'trigger.delivery.attempted' }),
109
- 'trigger.delivery.attempted (exhaust)',
110
- );
111
- expect(
112
- exhaustEvents.length >= 1,
113
- req('openwop.it.trigger-bridge-delivery.de-dups-by-dedupkey-retries-to-dead-letter-and-links-delivery-run-causation', 'trigger-bridge.md §C-2', 'an exhausted delivery MUST emit ≥1 trigger.delivery.attempted'),
114
- ).toBe(true);
111
+ if (exhaust === null) return seamAbsent('host advertises openwop-trigger-bridge but the delivery seam is unwired');
112
+ const exKey = exhaust.runId ?? '__exhaust__';
113
+ const exhaustEvents = requireEvents(await queryTestEvents(exKey, { type: 'trigger.delivery.attempted' }), 'trigger.delivery.attempted (exhaust)');
114
+ expect(exhaustEvents.length >= 1, req(R_DEAD_LETTER, 'trigger-bridge.md §C-2', 'an exhausted delivery MUST emit ≥1 trigger.delivery.attempted')).toBe(true);
115
115
  const terminal = exhaustEvents.sort((a, b) => a.sequence - b.sequence)[exhaustEvents.length - 1]!;
116
- expect(
117
- terminal.payload.outcome === 'dead-lettered',
118
- req('openwop.it.trigger-bridge-delivery.de-dups-by-dedupkey-retries-to-dead-letter-and-links-delivery-run-causation', 'trigger-bridge.md §C-2', 'an exhausted retry policy MUST terminate in a dead-lettered delivery'),
119
- ).toBe(true);
120
- const stateEvents = requireEvents(
121
- await queryTestEvents(exKey, { type: 'trigger.subscription.state.changed' }),
122
- 'trigger.subscription.state.changed (exhaust)',
123
- );
124
- expect(
125
- stateEvents.length >= 1,
126
- req('openwop.it.trigger-bridge-delivery.de-dups-by-dedupkey-retries-to-dead-letter-and-links-delivery-run-causation', 'trigger-bridge.md §B', 'exhaustion MUST emit ≥1 trigger.subscription.state.changed'),
127
- ).toBe(true);
128
- expect(
129
- stateEvents.some((e) => e.payload.toState === 'dead-lettered'),
130
- req('openwop.it.trigger-bridge-delivery.de-dups-by-dedupkey-retries-to-dead-letter-and-links-delivery-run-causation', 'trigger-bridge.md §B', 'the subscription MUST transition to dead-lettered on exhaustion'),
131
- ).toBe(true);
116
+ expect(terminal.payload.outcome === 'dead-lettered', req(R_DEAD_LETTER, 'trigger-bridge.md §C-2', 'an exhausted retry policy MUST terminate in a dead-lettered delivery')).toBe(true);
117
+ const stateEvents = requireEvents(await queryTestEvents(exKey, { type: 'trigger.subscription.state.changed' }), 'trigger.subscription.state.changed (exhaust)');
118
+ expect(stateEvents.length >= 1, req(R_DEAD_LETTER, 'trigger-bridge.md §B', 'exhaustion MUST emit ≥1 trigger.subscription.state.changed')).toBe(true);
119
+ expect(stateEvents.some((e) => e.payload.toState === 'dead-lettered'), req(R_DEAD_LETTER, 'trigger-bridge.md §B', 'the subscription MUST transition to dead-lettered on exhaustion')).toBe(true);
132
120
  for (const e of stateEvents) {
133
- expect(
134
- typeof e.payload.toState === 'string' && SUBSCRIPTION_STATES.includes(e.payload.toState as string),
135
- req('openwop.it.trigger-bridge-delivery.de-dups-by-dedupkey-retries-to-dead-letter-and-links-delivery-run-causation', 'trigger-bridge.md §B', 'toState MUST be in the four-state vocabulary'),
136
- ).toBe(true);
137
- expectContentFree(e.payload, 'trigger.subscription.state.changed');
121
+ expect(typeof e.payload.toState === 'string' && SUBSCRIPTION_STATES.includes(e.payload.toState as string), req(R_DEAD_LETTER, 'trigger-bridge.md §B', 'toState MUST be in the four-state vocabulary')).toBe(true);
138
122
  }
123
+ noteObservation('seam path');
124
+ await resetTestSeam();
125
+ return undefined;
126
+ }
139
127
 
140
- // ---- Leg 3: delivery → run causation (§C / RFC 0040) -----------------
141
- // §C: "the run started by a successful delivery MUST carry the delivery's
142
- // id as causationId on its run.started." The delivery's id is the
143
- // trigger.delivery.attempted{delivered} event's id, so we assert EQUALITY
144
- // (not merely "a causation id exists") — the trigger→run link MUST resolve.
128
+ /** The seam witness path (primary when the host serves the seams). */
129
+ async function causationViaSeam(): Promise<undefined> {
145
130
  const delivered = await driveDelivery({ scenario: 'deliver', source: 'schedule' });
146
- expect(
147
- delivered !== null && typeof delivered.runId === 'string' && (delivered.runId as string).length > 0,
148
- req('openwop.it.trigger-bridge-delivery.de-dups-by-dedupkey-retries-to-dead-letter-and-links-delivery-run-causation', 'trigger-bridge.md §C', 'a successful delivery MUST create a run'),
149
- ).toBe(true);
131
+ expect(delivered !== null && typeof delivered.runId === 'string' && (delivered.runId as string).length > 0, req(R_CAUSATION, 'trigger-bridge.md §C', 'a successful delivery MUST create a run')).toBe(true);
150
132
  const deliveredRunId = delivered!.runId as string;
151
- const attemptEvents = requireEvents(
152
- await queryTestEvents(deliveredRunId, { type: 'trigger.delivery.attempted' }),
153
- 'trigger.delivery.attempted (deliver)',
154
- );
133
+ const attemptEvents = requireEvents(await queryTestEvents(deliveredRunId, { type: 'trigger.delivery.attempted' }), 'trigger.delivery.attempted (deliver)');
155
134
  const deliveredEvent = attemptEvents.find((e) => e.payload.outcome === 'delivered');
156
- expect(
157
- deliveredEvent !== undefined,
158
- req('openwop.it.trigger-bridge-delivery.de-dups-by-dedupkey-retries-to-dead-letter-and-links-delivery-run-causation', 'trigger-bridge.md §C-1', 'a successful delivery MUST emit a trigger.delivery.attempted{outcome:delivered}'),
159
- ).toBe(true);
160
- const runStartedEvents = requireEvents(
161
- await queryTestEvents(deliveredRunId, { type: 'run.started' }),
162
- 'run.started (deliver)',
163
- );
164
- expect(
165
- runStartedEvents.length >= 1,
166
- req('openwop.it.trigger-bridge-delivery.de-dups-by-dedupkey-retries-to-dead-letter-and-links-delivery-run-causation', 'trigger-bridge.md §C', 'a delivered run MUST emit run.started'),
167
- ).toBe(true);
135
+ expect(deliveredEvent !== undefined, req(R_CAUSATION, 'trigger-bridge.md §C-1', 'a successful delivery MUST emit a trigger.delivery.attempted{outcome:delivered}')).toBe(true);
136
+ const runStartedEvents = requireEvents(await queryTestEvents(deliveredRunId, { type: 'run.started' }), 'run.started (deliver)');
137
+ expect(runStartedEvents.length >= 1, req(R_CAUSATION, 'trigger-bridge.md §C', 'a delivered run MUST emit run.started')).toBe(true);
168
138
  const runStarted = runStartedEvents.sort((a, b) => a.sequence - b.sequence)[0]!;
169
139
  expect(
170
- typeof runStarted.causationId === 'string' &&
171
- (runStarted.causationId as string).length > 0 &&
172
- runStarted.causationId === deliveredEvent!.eventId,
173
- req('openwop.it.trigger-bridge-delivery.de-dups-by-dedupkey-retries-to-dead-letter-and-links-delivery-run-causation', 'trigger-bridge.md §C / RFC 0040', 'run.started.causationId MUST EQUAL the delivery id (the trigger.delivery.attempted{delivered} eventId) — resolvable via /ancestry'),
140
+ typeof runStarted.causationId === 'string' && (runStarted.causationId as string).length > 0 && runStarted.causationId === deliveredEvent!.eventId,
141
+ req(R_CAUSATION, 'trigger-bridge.md §C / RFC 0040', 'run.started.causationId MUST EQUAL the delivery id (the trigger.delivery.attempted{delivered} eventId) — resolvable via /ancestry'),
174
142
  ).toBe(true);
143
+ noteObservation('seam path');
144
+ await resetTestSeam();
145
+ return undefined;
146
+ }
147
+
148
+ describe('trigger-bridge-delivery (RFC 0083 §C)', () => {
149
+ it('de-dups by dedupKey: a repeated delivery is effectively-once', async () => {
150
+ if (!behaviorGate('openwop-trigger-bridge', await isTriggerBridgeProfileAdvertised())) return;
151
+ const path = await witnessPath();
152
+ if (path === null) return seamAbsent(NO_PATH);
175
153
 
154
+ if (path === 'seam') return dedupViaSeam();
155
+
156
+ // Normative-surface path (RFC 0230 §C Identity + §D).
157
+ const sub = await registerSignedWebhook('none');
158
+ if ('reason' in sub) throw new Error(req(R_DEDUP, 'trigger-bridge.md §F.6', `an inboundSigning host MUST return the §B binding on webhook registration: ${sub.reason}`));
159
+ const body = JSON.stringify({ conformance: 'dedup' });
160
+ const webhookId = freshWebhookId();
161
+ const first = await signedIngest(sub, body, { webhookId });
162
+ expect(first.status, req(R_DEDUP, 'trigger-bridge.md §F.6', `a signed delivery MUST answer 202 (got ${first.status} ${JSON.stringify(first.json)})`)).toBe(202);
163
+ const runId = first.json?.runId;
164
+ expect(typeof runId === 'string' && runId.length > 0, req(R_DEDUP, 'trigger-bridge.md §F.6', 'a 202 MUST carry the started runId')).toBe(true);
165
+ const again = await signedIngest(sub, body, { webhookId });
166
+ expect(again.status, req(R_DEDUP, 'trigger-bridge.md §F.6 / §C-1', `a repeated webhook-id MUST answer 200 duplicate, starting no new run (got ${again.status})`)).toBe(200);
167
+ expect(again.json?.outcome, req(R_DEDUP, 'trigger-bridge.md §F.6', 'the dedup no-op MUST say outcome "duplicate"')).toBe('duplicate');
168
+ expect(again.json?.runId, req(R_DEDUP, 'trigger-bridge.md §F.4', 'a dedup no-op MUST return the PRIOR runId')).toBe(runId);
169
+ noteObservation('normative-surface path (RFC 0230)');
170
+ });
171
+
172
+ it('dead-letters an exhausted or refused delivery without starting a run', async () => {
173
+ if (!behaviorGate('openwop-trigger-bridge', await isTriggerBridgeProfileAdvertised())) return;
174
+ const path = await witnessPath();
175
+ if (path === null) return seamAbsent(NO_PATH);
176
+
177
+ if (path === 'seam') return deadLetterViaSeam();
178
+
179
+ // Normative-surface path (RFC 0230 §C Verification + §C.1).
180
+ const sub = await registerSignedWebhook('required');
181
+ if ('reason' in sub) throw new Error(req(R_DEAD_LETTER, 'trigger-bridge.md §F.6', `an inboundSigning host MUST return the §B binding on webhook registration: ${sub.reason}`));
182
+ const body = JSON.stringify({ conformance: 'dead-letter' });
183
+ const bad = await signedIngest(sub, body, { signature: 'v1,AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=' });
184
+ expect(bad.status, req(R_DEAD_LETTER, 'trigger-bridge.md §F.6', `a bad signature under required verification MUST answer 401 (got ${bad.status})`)).toBe(401);
185
+ expect(bad.json?.runId, req(R_DEAD_LETTER, 'trigger-bridge.md §F.2', 'a required-verification failure MUST NOT start a run')).toBeUndefined();
186
+ const read = await driver.get(`/v1/trigger-subscriptions/${encodeURIComponent(sub.subscriptionId)}`);
187
+ const state = (read.json as { subscription?: { state?: unknown } } | undefined)?.subscription?.state;
188
+ expect(state, req(R_DEAD_LETTER, 'trigger-bridge.md §F.6 (RFC 0230 §C.1)', `a refused post MUST NOT change the subscription's state (got ${String(state)})`)).toBe('active');
189
+ const ok = await signedIngest(sub, body);
190
+ expect(ok.status, req(R_DEAD_LETTER, 'trigger-bridge.md §F.6 (RFC 0230 §C.1)', `after a refused post the subscription MUST still deliver a correctly signed one (got ${ok.status})`)).toBe(202);
191
+ noteObservation('normative-surface path (RFC 0230)');
192
+ });
193
+
194
+ it('links delivery→run causation on run.started', async () => {
195
+ if (!behaviorGate('openwop-trigger-bridge', await isTriggerBridgeProfileAdvertised())) return;
196
+ const path = await witnessPath();
197
+ if (path === null) return seamAbsent(NO_PATH);
198
+
199
+ if (path === 'seam') return causationViaSeam();
200
+
201
+ // Normative-surface path: a signed delivery, then the run's own event log.
202
+ const sub = await registerSignedWebhook('none');
203
+ if ('reason' in sub) throw new Error(req(R_CAUSATION, 'trigger-bridge.md §F.6', `an inboundSigning host MUST return the §B binding on webhook registration: ${sub.reason}`));
204
+ const res = await signedIngest(sub, JSON.stringify({ conformance: 'causation' }));
205
+ expect(res.status, req(R_CAUSATION, 'trigger-bridge.md §F.6', `a signed delivery MUST answer 202 (got ${res.status})`)).toBe(202);
206
+ const runId = res.json?.runId;
207
+ expect(typeof runId === 'string' && runId.length > 0, req(R_CAUSATION, 'trigger-bridge.md §F.6', 'a 202 MUST carry the started runId')).toBe(true);
208
+ const poll = await driver.get(`/v1/runs/${encodeURIComponent(bareRunId(String(runId)))}/events/poll`);
209
+ const events = ((poll.json as { events?: Array<Record<string, unknown>> } | undefined)?.events ?? []);
210
+ const started = events.find((e) => e.type === 'run.started');
211
+ expect(started !== undefined, req(R_CAUSATION, 'trigger-bridge.md §C', `a delivered run MUST emit run.started (poll answered ${poll.status}; saw ${events.map((e) => e.type).join(',') || 'none'})`)).toBe(true);
212
+ const causationId = started?.causationId;
213
+ expect(typeof causationId === 'string' && causationId.length > 0, req(R_CAUSATION, 'trigger-bridge.md §C / RFC 0040', 'run.started MUST carry the delivery id as causationId')).toBe(true);
214
+ const deliveredEvent = events.find((e) => e.type === 'trigger.delivery.attempted' && (e.payload as Record<string, unknown> | undefined)?.outcome === 'delivered');
215
+ if (deliveredEvent !== undefined) {
216
+ expect(causationId, req(R_CAUSATION, 'trigger-bridge.md §C / RFC 0040', 'run.started.causationId MUST EQUAL the delivered attempt event id')).toBe(deliveredEvent.eventId);
217
+ expectContentFree(R_CAUSATION, (deliveredEvent.payload ?? {}) as Record<string, unknown>, 'trigger.delivery.attempted');
218
+ }
219
+ noteObservation(`normative-surface path (RFC 0230)${deliveredEvent === undefined ? '; the delivered attempt event is not on the run log, so equality was not checked' : ''}`);
220
+ });
221
+
222
+ it('keeps the run-less trigger events content-free', async () => {
223
+ if (!behaviorGate('openwop-trigger-bridge', await isTriggerBridgeProfileAdvertised())) return;
224
+ // No normative-surface path: a dead-lettered attempt and a state change are
225
+ // on no run's log, so only the seam can read them (RFC 0230 §Conformance).
226
+ if (!(await isEventLogSeamAvailable())) return seamAbsent('the run-less trigger events are readable only through the event-log seam, which is absent');
227
+ const exhaust = await driveDelivery({ scenario: 'exhaust', source: 'webhook' });
228
+ if (exhaust === null) return seamAbsent('host advertises openwop-trigger-bridge but the delivery seam is unwired');
229
+ const exKey = exhaust.runId ?? '__exhaust__';
230
+ for (const type of ['trigger.delivery.attempted', 'trigger.subscription.state.changed'] as const) {
231
+ for (const e of requireEvents(await queryTestEvents(exKey, { type }), `${type} (exhaust)`)) expectContentFree(R_RUNLESS, e.payload, type);
232
+ }
176
233
  await resetTestSeam();
177
234
  });
178
235
  });