@openwop/openwop-conformance 2.42.2 → 2.42.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 (66) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +1 -1
  3. package/dist/spec-artifacts.lock.json +2 -2
  4. package/package.json +2 -2
  5. package/requirements.json +353 -133
  6. package/schemas/CORPUS-STAMP.json +10 -10
  7. package/src/lib/agentOrgChart.ts +21 -4
  8. package/src/lib/effect-receiver.ts +23 -2
  9. package/src/lib/toolCatalog.ts +29 -5
  10. package/src/scenarios/agent-manifest-runtime.test.ts +17 -4
  11. package/src/scenarios/agent-org-chart-scoping.test.ts +27 -4
  12. package/src/scenarios/agent-roster-attribution.test.ts +28 -4
  13. package/src/scenarios/agentPackExport.test.ts +33 -0
  14. package/src/scenarios/agentPackInstall.test.ts +32 -0
  15. package/src/scenarios/agentReasoningEvents.test.ts +22 -13
  16. package/src/scenarios/agents-run-tool-allowlist.test.ts +6 -2
  17. package/src/scenarios/ai-envelope-shape.test.ts +9 -8
  18. package/src/scenarios/anonymous-actor-write-gated.test.ts +12 -6
  19. package/src/scenarios/audit-log-integrity.test.ts +37 -0
  20. package/src/scenarios/context-budget-transcript-bound.test.ts +14 -4
  21. package/src/scenarios/context-summarization-replay.test.ts +17 -8
  22. package/src/scenarios/conversationLifecycle.test.ts +12 -1
  23. package/src/scenarios/conversationReplayDeterminism.test.ts +19 -3
  24. package/src/scenarios/cost-attribution.test.ts +12 -0
  25. package/src/scenarios/debug-bundle-truncation.test.ts +16 -3
  26. package/src/scenarios/distillation-shape.test.ts +30 -0
  27. package/src/scenarios/envelope-truncated.test.ts +6 -2
  28. package/src/scenarios/feedback-fork-not-copied.test.ts +28 -7
  29. package/src/scenarios/idempotency-concurrent-claim.test.ts +27 -9
  30. package/src/scenarios/idempotency-key-determinism.test.ts +6 -2
  31. package/src/scenarios/inbound-credential-no-passthrough.test.ts +13 -2
  32. package/src/scenarios/mcp-server-untrusted-args.test.ts +20 -5
  33. package/src/scenarios/mcp-tool-roundtrip.test.ts +7 -21
  34. package/src/scenarios/memory-compaction-provenance-tag.test.ts +11 -3
  35. package/src/scenarios/orchestratorDispatch.test.ts +18 -2
  36. package/src/scenarios/pack-registry.test.ts +8 -1
  37. package/src/scenarios/production-backpressure.test.ts +17 -11
  38. package/src/scenarios/redaction.test.ts +8 -2
  39. package/src/scenarios/replay-fanout-suppression.test.ts +18 -2
  40. package/src/scenarios/runner-ledger.test.ts +7 -2
  41. package/src/scenarios/stream-modes.test.ts +27 -0
  42. package/src/scenarios/tool-catalog-projection.test.ts +35 -12
  43. package/src/scenarios/v2-advertised-fixtures-exist.test.ts +17 -3
  44. package/src/scenarios/v2-artifact-a2a-shape.test.ts +8 -2
  45. package/src/scenarios/v2-bound-id-kinds.test.ts +13 -4
  46. package/src/scenarios/v2-effect-identity-business-key.test.ts +71 -13
  47. package/src/scenarios/v2-effect-seam-manifest.test.ts +3 -9
  48. package/src/scenarios/v2-era-2-append-vocabulary.test.ts +52 -9
  49. package/src/scenarios/v2-idempotency-in-flight.test.ts +16 -0
  50. package/src/scenarios/v2-interop-trace-context.test.ts +23 -0
  51. package/src/scenarios/v2-mcp-tasks.test.ts +33 -6
  52. package/src/scenarios/v2-min-client-version.test.ts +11 -1
  53. package/src/scenarios/v2-mrtr-rounds-ceiling.test.ts +11 -0
  54. package/src/scenarios/v2-poll-cursor-v2.test.ts +28 -5
  55. package/src/scenarios/v2-profiles-derived-only.test.ts +9 -2
  56. package/src/scenarios/v2-run-diff-identical.test.ts +13 -5
  57. package/src/scenarios/v2-run-pause-resume.test.ts +4 -1
  58. package/src/scenarios/v2-sse-last-event-id.test.ts +7 -0
  59. package/src/scenarios/v2-version-header-honored.test.ts +22 -3
  60. package/src/scenarios/v2-webhook-delivery-isolation.test.ts +28 -8
  61. package/src/scenarios/v2-webhook-durable-delivery.test.ts +24 -0
  62. package/src/scenarios/v2-webhook-endpoint-verification.test.ts +25 -6
  63. package/src/scenarios/v2-webhook-message-id-stable.test.ts +10 -2
  64. package/src/scenarios/v2-webhook-unregister-stops-delivery.test.ts +6 -1
  65. package/src/scenarios/version-negotiation.test.ts +12 -1
  66. package/src/scenarios/webhook-signed-delivery.test.ts +18 -3
@@ -69,6 +69,13 @@ describe('v2 sse-last-event-id (events.md §SSE frames)', () => {
69
69
  expect(ids.every((n) => n !== null), req(ID, DOC, `every frame MUST carry id: = its sequence (frames without a parseable id: ${ids.filter((n) => n === null).length} of ${ids.length})`)).toBe(true);
70
70
  const seqs = ids as number[];
71
71
  expect([...seqs].sort((a, b) => a - b), req(ID, DOC, 'frames MUST arrive in log order')).toEqual(seqs);
72
+ // unfailable-leg audit wave 2, 2026-09-27: with a single frame, `first` ===
73
+ // `last` and the mid-stream resume leg compared two empty sets — a host
74
+ // that ignored Last-Event-ID on a resume with backlog could not fail it. A
75
+ // completed run's log holds at least `run.started` (events.md: it carries
76
+ // the owner block) and its terminal event (§The terminal event), and debug
77
+ // mode streams every event, so ≥2 frames is required, not assumed.
78
+ expect(seqs.length, req(ID, DOC, `a completed run's debug stream MUST carry at least run.started and its terminal event (got ${seqs.length} frame(s))`)).toBeGreaterThanOrEqual(2);
72
79
  const first = seqs[0]!; const last = seqs[seqs.length - 1]!;
73
80
 
74
81
  const resumed = await subscribe(path, { timeoutMs: 8_000, extraHeaders: V2, lastEventId: String(first) });
@@ -145,10 +145,29 @@ describe('v2-version-header-honored (RFC 0172 §A.3)', () => {
145
145
  // A refusal is a correct answer and ends the check: the host has told the
146
146
  // truth about not serving major 2.
147
147
  if (asked.status === 406) {
148
+ // unfailable-leg audit wave 2, 2026-09-27: this branch asserted only the
149
+ // status it had just branched on (`406 === 406`), so ANY 406 passed — a
150
+ // host that lists `2.x` in protocolVersions[] and still refuses it
151
+ // (§1.3 row 1: MUST serve a listed major), or one answering 406 with a
152
+ // bare `{}` body. The refusal is now held to §1.3 row 2 exactly as the
153
+ // retired-host branch above is: code `protocol_version_unsupported`,
154
+ // `details.protocolVersions[]` echoing the list, and major 2 NOT listed.
155
+ let refusal: unknown;
156
+ try {
157
+ refusal = JSON.parse(asked.body);
158
+ } catch {
159
+ refusal = undefined;
160
+ }
161
+ const echoed406 = (refusal as { details?: { protocolVersions?: unknown } } | undefined)?.details?.protocolVersions;
148
162
  expect(
149
- asked.status,
150
- req(ID, DOC, 'a host that does not serve major 2 MUST refuse with 406 protocol_version_unsupported, which it did'),
151
- ).toBe(406);
163
+ {
164
+ status: asked.status,
165
+ code: readErrorCode(refusal),
166
+ echoed: Array.isArray(echoed406) ? [...echoed406].map(String).sort() : echoed406,
167
+ listsMajor2: majors.has('2'),
168
+ },
169
+ req(ID, DOC, `a host that does not serve major 2 MUST refuse OpenWOP-Version: 2.0 with 406 protocol_version_unsupported and details.protocolVersions[] echoing [${advertised.join(', ')}]; a host that LISTS a 2.x member MUST serve it (§1.3 row 1), not refuse it`),
170
+ ).toEqual({ status: 406, code: 'protocol_version_unsupported', echoed: [...advertised].sort(), listsMajor2: false });
152
171
  return softSkip('inapplicable', 'the host refused major 2 with the specified 406 — a correct answer, and there is no second representation to compare bytes against');
153
172
  }
154
173
  if (asked.status !== 200) {
@@ -26,11 +26,13 @@
26
26
  * host released one to make room), or never within the window, AND the held
27
27
  * attempts had stayed open until the run was terminal: the contention the
28
28
  * floor names existed when the healthy delivery fell due, and it waited.
29
- * - partial-witness — the host's own delivery timeout closed held attempts
30
- * before the run was terminal. Contention was not sustained when the
31
- * healthy delivery fell due, so this run of the leg cannot judge §A. The
32
- * timeout is the host's to choose; a host with a timeout shorter than
33
- * DELAY_MS is simply not measured by this instrument.
29
+ * - blocked (unjudged) — the host had all 8 held attempts open at once, but
30
+ * its own delivery timeout closed them before the run was terminal.
31
+ * Contention was not sustained when the healthy delivery fell due, so this
32
+ * run of the leg cannot judge §A. The timeout is the host's to choose; a
33
+ * host with a timeout shorter than DELAY_MS is simply not measured by this
34
+ * instrument. If fewer than 8 were EVER open at once (a serial or small
35
+ * pool), it fails instead (2026-09-26, unfailable-leg audit).
34
36
  * - blocked — fewer than 8 held attempts ever arrived AND the healthy one
35
37
  * never did either: nothing reached the suite, which says nothing about
36
38
  * isolation (`noDeliveryCause`).
@@ -121,6 +123,10 @@ describe('RFC 0215 §A — one subscription\'s receiver does not hold another\'s
121
123
  let healthyId: string | null = null;
122
124
  let healthyAt: number | null = null;
123
125
  let openAtHealthy = -1;
126
+ // The most held attempts ever open at once. Sampled at each held arrival:
127
+ // the open count only rises there (closes only lower it), so the max over
128
+ // arrivals is the true peak.
129
+ let peakOpen = 0;
124
130
  const openCount = (): number => [...held.values()].filter((h) => h.closedAt === null).length;
125
131
 
126
132
  const rx = await startScopedReceiver((hit, res) => {
@@ -137,6 +143,7 @@ describe('RFC 0215 §A — one subscription\'s receiver does not hold another\'s
137
143
  }
138
144
  const h: Held = { webhookId, res, openedAt: Date.now(), closedAt: null };
139
145
  held.set(webhookId, h);
146
+ peakOpen = Math.max(peakOpen, openCount());
140
147
  // The HOST closing the socket before we answer is the host abandoning the attempt.
141
148
  res.on('close', () => { if (h.closedAt === null) h.closedAt = Date.now(); });
142
149
  });
@@ -175,7 +182,7 @@ describe('RFC 0215 §A — one subscription\'s receiver does not hold another\'s
175
182
  const everOpened = held.size;
176
183
  await waitFor(() => healthyAt !== null, WINDOW_MS);
177
184
  const earliestClose = Math.min(...[...held.values()].map((h) => h.closedAt ?? Infinity));
178
- const detail = `${everOpened} held attempt(s) arrived; ${openAtTerminal} were still open when the run was seen terminal; `
185
+ const detail = `${everOpened} held attempt(s) arrived (at most ${peakOpen} open at once); ${openAtTerminal} were still open when the run was seen terminal; `
179
186
  + (healthyAt === null ? `the healthy attempt did not arrive within ${WINDOW_MS}ms of that` : `the healthy attempt arrived ${healthyAt - terminalAt}ms after it, with ${openAtHealthy} held open`);
180
187
 
181
188
  if (healthyAt === null && everOpened === 0) {
@@ -193,8 +200,21 @@ describe('RFC 0215 §A — one subscription\'s receiver does not hold another\'s
193
200
  // delivery timeout released the held attempts early (its choice), or it
194
201
  // never opened FLOOR of them at once. The second is the defect, the first
195
202
  // is not, and they differ in WHEN the first held attempt closed.
196
- expect(everOpened, req(ID, DOC, 'the held subscriptions\' attempts MUST have been dispatched')).toBeGreaterThan(0);
197
- return softSkip('skipped', `partial-witness: the host closed held attempts ${Number.isFinite(earliestClose) ? `${terminalAt - earliestClose}ms ` : ''}before the run was terminal (its delivery timeout is shorter than this leg's ${DELAY_MS}ms delay), so ${FLOOR} attempts were not outstanding when the healthy delivery fell due — ${detail}`);
203
+ //
204
+ // Unfailable-leg audit (2026-09-26): this branch asserted everOpened > 0
205
+ // and then `softSkip('skipped')`, which records `executed-pass` with a
206
+ // `partial-witness:` detail. A serial dispatcher (pool of 1) with a
207
+ // sub-2s delivery timeout landed here and passed — every held attempt
208
+ // arrived eventually (everOpened reached FLOOR), one at a time, while the
209
+ // healthy attempt queued behind them: head-of-line blocking. peakOpen
210
+ // separates the two cases the comment above names: below FLOOR the host
211
+ // never had FLOOR attempts outstanding at once, which is the defect; at
212
+ // FLOOR the host's timeout released them early, which is unjudged.
213
+ expect(
214
+ peakOpen,
215
+ req(ID, DOC, `a host MUST sustain ${FLOOR} subscriptions' attempts outstanding at once — at most ${peakOpen} were ever open together, so the held attempts were dispatched below the floor — ${detail}`),
216
+ ).toBeGreaterThanOrEqual(FLOOR);
217
+ return blockedDespiteAssertions(`unjudged: ${FLOOR} held attempts were open at once, but the host closed held attempts ${Number.isFinite(earliestClose) ? `${terminalAt - earliestClose}ms ` : ''}before the run was terminal (its delivery timeout is shorter than this leg's ${DELAY_MS}ms delay), so ${FLOOR} attempts were not outstanding when the healthy delivery fell due — ${detail}`);
198
218
  } else {
199
219
  expect.fail(req(ID, DOC, `the host never had ${FLOOR} subscriptions' attempts outstanding at once, and the healthy attempt waited — a bounded dispatcher below the floor — ${detail}`));
200
220
  }
@@ -348,6 +348,22 @@ describe('RFC 0173 §B — webhook-durable-delivery (gated on webhooks)', () =>
348
348
  req('openwop.requirement.0173.webhook-durable-delivery', 'webhooks.md §Durability', `retry attempts for ${key} MUST be spaced by the advertised ${policy.backoff} backoff (attempt ${i + 1} followed attempt ${i} after ${times[i]! - times[i - 1]!}ms)`),
349
349
  ).toBeGreaterThan(0);
350
350
  }
351
+ // Unfailable-leg audit (2026-09-26): `> 0 ms` alone let a host advertising
352
+ // `backoff: exponential` that retried in a tight loop (any constant
353
+ // spacing, even a few ms) pass. An exponential backoff MUST widen, so with
354
+ // three or more attempts the second gap is at least 1.5x the first (not
355
+ // 2x: jitter tolerance). Only the first two gaps are compared, so a host
356
+ // whose schedule hits its cap later is not convicted. `fixed` gets no
357
+ // equivalent: its base interval is not on the wire (see the comment above
358
+ // the `!retried` branch), so there is no floor to measure against.
359
+ if (policy.backoff === 'exponential' && times.length >= 3) {
360
+ const gap1 = times[1]! - times[0]!;
361
+ const gap2 = times[2]! - times[1]!;
362
+ expect(
363
+ gap2,
364
+ req('openwop.requirement.0173.webhook-durable-delivery', 'webhooks.md §Durability', `retry attempts for ${key} MUST follow the advertised exponential backoff — the second gap (${gap2}ms) MUST be at least 1.5x the first (${gap1}ms); a constant spacing is not exponential`),
365
+ ).toBeGreaterThanOrEqual(1.5 * gap1);
366
+ }
351
367
  }
352
368
  } else {
353
369
  softSkip('inapplicable', 'no advertised retryPolicy.backoff other than none — the spacing leg is not asserted');
@@ -443,6 +459,14 @@ describe('RFC 0173 §B — webhook-durable-delivery (gated on webhooks)', () =>
443
459
  ).toBeLessThanOrEqual(policy.maxAttempts);
444
460
  }
445
461
  if (inSink === null) {
462
+ // partial-witness-ok: the retry half of §Durability is observed above
463
+ // (retried, and capped at maxAttempts). The sink half has no read on a
464
+ // host without the webhooks.deadLetter facet (RFC 0188 §A.5 makes the read
465
+ // a 404, not an obligation), so it is unwitnessable here BY DESIGN. It is
466
+ // not a harness gap that `blocked` would honestly name: a bundle-fatal
467
+ // `blocked` would deny certification to every conforming host that
468
+ // declines the optional read. Reviewed in the unfailable-leg audit wave 2
469
+ // (2026-09-27); the acceptance predicate still refuses this row.
446
470
  return softSkip('inapplicable', 'host does not advertise the webhooks.deadLetter facet — RFC 0188 §A.5 makes the read a 404 rather than an obligation, so the sink half of §Durability is unwitnessable here (the retry half above passed)');
447
471
  }
448
472
  expect(
@@ -38,9 +38,10 @@
38
38
  import { afterEach, describe, it, expect } from 'vitest';
39
39
  import { req } from '../lib/requirement-ids.js';
40
40
  import { readErrorCode } from '../lib/error-envelope.js';
41
- import { softSkip } from '../lib/soft-skip.js';
41
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
42
42
  import { v2Validator } from '../lib/v2.js';
43
- import { hitHeader, mintWhsec, startModalReceiver, verifyStandardWebhooks } from '../lib/webhook-receiver.js';
43
+ import { scaledTimeoutMs } from '../lib/polling.js';
44
+ import { hitHeader, mintWhsec, startModalReceiver, verifyStandardWebhooks, type ModalHit } from '../lib/webhook-receiver.js';
44
45
  import {
45
46
  STANDARD_WEBHOOKS_ALG,
46
47
  deliveriesFor,
@@ -53,6 +54,9 @@ import {
53
54
  } from '../lib/standard-webhooks.js';
54
55
 
55
56
  const REFUSED = ['no-echo', 'wrong-echo', 'redirect'] as const;
57
+ /** A second echo subscription, registered AFTER the refused modes (routed as `echo`, told apart by this trailing segment). */
58
+ const LATE_CONTROL = 'late-control';
59
+ const isLateControl = (h: ModalHit): boolean => h.path.split('?')[0]!.endsWith(`/echo/${LATE_CONTROL}`);
56
60
 
57
61
  let closeReceiver: (() => Promise<void>) | null = null;
58
62
  afterEach(async () => {
@@ -80,10 +84,18 @@ describe('RFC 0201 §D — endpoint verification refuses without consent (opted-
80
84
  const res = await registerSw(body(mode));
81
85
  refused.set(mode, { status: res.status, code: readErrorCode(res.json), id: (res.json as { webhookId?: unknown } | null)?.webhookId });
82
86
  }
87
+ // Unfailable-leg audit, 2026-09-26: the persisted-nothing check synced only
88
+ // on the FIRST echo subscription plus a 1 s grace, so a host that answered
89
+ // 400 but kept the rows, delivering serially in registration order, could
90
+ // land those deliveries after the window. A second echo control registered
91
+ // AFTER the refused modes sits behind them in that order.
92
+ const late = await registerSw({ ...body('echo'), url: `${rx.urlFor('echo').url}/${LATE_CONTROL}` });
83
93
 
84
94
  const ID = 'openwop.requirement.0201.endpoint-verification';
85
95
  expect(ok.status, req(ID, 'RFC 0201 §D.14', `an endpoint that echoes the challenge MUST be registered 201 (got ${ok.status} ${readErrorCode(ok.json) ?? ''})`)).toBe(201);
86
96
  const echoId = (ok.json as { webhookId: string }).webhookId;
97
+ expect(late.status, req(ID, 'RFC 0201 §D.14', `a second echoing endpoint (the late control) MUST be registered 201 (got ${late.status} ${readErrorCode(late.json) ?? ''})`)).toBe(201);
98
+ const lateId = (late.json as { webhookId: string }).webhookId;
87
99
  for (const [mode, r] of refused) {
88
100
  expect(r.status, req(ID, 'RFC 0201 §D.14', `a ${mode} endpoint MUST be refused 400 before any 201 (got ${r.status})`)).toBe(400);
89
101
  expect(r.code, req(ID, 'RFC 0201 §D.14', `a ${mode} refusal MUST carry webhook_endpoint_unverified`)).toBe('webhook_endpoint_unverified');
@@ -91,7 +103,7 @@ describe('RFC 0201 §D — endpoint verification refuses without consent (opted-
91
103
 
92
104
  const validate = v2Validator('webhook-verification');
93
105
  for (const mode of ['echo', ...REFUSED]) {
94
- const vr = rx.hits.filter((h) => h.mode === mode && h.verification);
106
+ const vr = rx.hits.filter((h) => h.mode === mode && h.verification && !isLateControl(h));
95
107
  expect(vr.length, req(ID, 'RFC 0201 §D.13–§D.14', `the host MUST send exactly one verification request to the ${mode} endpoint and MUST NOT retry it (saw ${vr.length})`)).toBe(1);
96
108
  const h = vr[0]!;
97
109
  expect(h.method, req(ID, 'RFC 0201 §D.13', 'the verification request MUST be a POST')).toBe('POST');
@@ -113,12 +125,19 @@ describe('RFC 0201 §D — endpoint verification refuses without consent (opted-
113
125
  // delivery; by then any persisted refused subscription would have one too.
114
126
  const run = await driveRun();
115
127
  expect(run.status, req(ID, 'runs.md §Create', 'POST /runs MUST answer 201 for the noop fixture')).toBe(201);
116
- await waitFor(() => deliveriesFor(rx.hits, echoId, run.runId).length > 0, 15_000);
128
+ await waitFor(() => deliveriesFor(rx.hits, echoId, run.runId).length > 0 && deliveriesFor(rx.hits, lateId, run.runId).length > 0, 15_000);
117
129
  expect(deliveriesFor(rx.hits, echoId, run.runId).length, req(ID, 'webhooks.md §Durability', 'the verified subscription MUST receive run.completed — the sync point for the persisted-nothing check')).toBeGreaterThan(0);
118
- await new Promise((r) => setTimeout(r, 1_000));
130
+ // The late control is a SYNC POINT, not a requirement: a public receiver
131
+ // front that drops its extra path segment would starve it through no fault
132
+ // of the host. Unobserved ⇒ the persisted-nothing check is unjudged, never
133
+ // failed and never passed.
134
+ if (deliveriesFor(rx.hits, lateId, run.runId).length === 0) {
135
+ return blockedDespiteAssertions(`the late-control subscription (${lateId}) received no run.completed within the wait — the second sync point for the persisted-nothing check is missing, so that check is unjudged`);
136
+ }
137
+ await new Promise((r) => setTimeout(r, scaledTimeoutMs(3_000)));
119
138
  for (const mode of REFUSED) {
120
139
  const late = rx.hits.filter((h) => h.mode === mode && !h.verification);
121
140
  expect(late.length, req(ID, 'RFC 0201 §D.14', `a refused (${mode}) registration MUST persist no subscription — it received ${late.length} later request(s)`)).toBe(0);
122
141
  }
123
- }, 60_000);
142
+ }, 75_000);
124
143
  });
@@ -31,7 +31,7 @@
31
31
  import { afterEach, describe, it, expect } from 'vitest';
32
32
  import { req } from '../lib/requirement-ids.js';
33
33
  import { readErrorCode } from '../lib/error-envelope.js';
34
- import { softSkip } from '../lib/soft-skip.js';
34
+ import { blockedDespiteAssertions, softSkip } from '../lib/soft-skip.js';
35
35
  import { hitHeader, mintWhsec, startModalReceiver, STANDARD_WEBHOOKS_ID, type ModalHit } from '../lib/webhook-receiver.js';
36
36
  import { retryWaitCapMs, retryWaitFor } from '../lib/webhook-retry-window.js';
37
37
  import {
@@ -108,11 +108,19 @@ describe('RFC 0201 §C.10 — webhook-id is stable across retries, distinct acro
108
108
  req(ID, 'RFC 0201 §C.10', `webhook-id MUST be identical on every attempt of one (webhookId, runId, sequence) — ${attempts.length} attempts of ${key} carried ${ids.size} value(s)`),
109
109
  ).toHaveLength(1);
110
110
  }
111
+ // Unfailable-leg audit, 2026-09-26: the distinctness half asserted only
112
+ // `keys.size >= 2`, so a host coalescing the two subscriptions (or keeping a
113
+ // per-subscription constant id with one delivery each) passed on a partial
114
+ // set. Distinctness is judged only over the full expected set; fewer keys
115
+ // is unmeasured, and the retry-stability assertions above already ran.
116
+ if (keys.size < expectedKeys) {
117
+ return blockedDespiteAssertions(`only ${keys.size} of ${expectedKeys} expected deliveries (${subs.length} subscriptions × ${EVENTS.join(' + ')}) arrived inside the ${window}ms window — distinctness across deliveries cannot be judged on a partial set`);
118
+ }
111
119
  const perKey = [...keys.entries()].map(([key, a]) => ({ key, id: hitHeader(a[0]!, 'webhook-id') ?? '' }));
112
120
  for (const { key, id } of perKey) {
113
121
  expect(STANDARD_WEBHOOKS_ID.test(id), req(ID, 'RFC 0201 §C.10', `webhook-id MUST match ^[A-Za-z0-9_-]{16,128}$ (${key}: ${JSON.stringify(id)})`)).toBe(true);
114
122
  }
115
- expect(keys.size, req(ID, 'RFC 0201 §C.10', `two opted-in subscriptions on ${EVENTS.join(' + ')} MUST yield ${expectedKeys} distinct deliveries of one run`)).toBeGreaterThanOrEqual(2);
123
+ expect(keys.size, req(ID, 'RFC 0201 §C.10', `two opted-in subscriptions on ${EVENTS.join(' + ')} MUST yield ${expectedKeys} distinct deliveries of one run`)).toBe(expectedKeys);
116
124
  expect(
117
125
  new Set(perKey.map((k) => k.id)).size,
118
126
  req(ID, 'RFC 0201 §C.10', `webhook-id MUST differ between distinct deliveries, by event and by subscription — ${perKey.length} deliveries carried ${new Set(perKey.map((k) => k.id)).size} distinct id(s)`),
@@ -171,8 +171,13 @@ describe('RFC 0215 §B — unregistering stops the attempts (gated on webhooks)'
171
171
  targetLate.length,
172
172
  req(ID, DOC, `after unregisterWebhook answers 204 the host MUST NOT start another attempt for that subscription, including retries already scheduled — ${detail}`),
173
173
  ).toBe(0);
174
+ // unfailable-leg audit wave 2, 2026-09-27: a plain softSkip after the
175
+ // `targetLate === 0` assert recorded a partial-witness `executed-pass`, so a
176
+ // host that KEPT delivering to an unregistered subscription — but whose
177
+ // retries fell outside the window — certified. Without a late control
178
+ // attempt the target's silence proves nothing: `blocked`.
174
179
  if (controlLate.length === 0) {
175
- return softSkip('skipped', `partial-witness: the control subscription was not retried after the 204 + ${GRACE_MS}ms within ${windowMs}ms either, so the target's silence shows nothing — the schedule finished inside the grace or runs past the window (raise OPENWOP_WEBHOOK_RETRY_WAIT_MS above the host's backoff sum) — ${detail}`);
180
+ return blockedDespiteAssertions(`the control subscription was not retried after the 204 + ${GRACE_MS}ms within ${windowMs}ms either, so the target's silence shows nothing — the schedule finished inside the grace or runs past the window (raise OPENWOP_WEBHOOK_RETRY_WAIT_MS above the host's backoff sum) — ${detail}`);
176
181
  }
177
182
  }, CAP_MS * 2 + 90_000);
178
183
  });
@@ -74,7 +74,18 @@ describe('version-negotiation: Capabilities advertises a protocolVersion', () =>
74
74
  'capabilities.md §3 + version-negotiation.md',
75
75
  'Capabilities.protocolVersion MUST be a non-empty string',
76
76
  )).toBe('string');
77
- expect(String(caps.protocolVersion).length).toBeGreaterThan(0);
77
+ // unfailable-leg audit wave 2, 2026-09-27: only non-emptiness was checked,
78
+ // so `v1.0`, `1.0.0`, `1` and `banana` passed. RFC 0149 §C constrains
79
+ // protocolVersion to ASCII `<major>.<minor>` with no leading zeros —
80
+ // capabilities.schema.json `protocolVersion.pattern` (the same grammar
81
+ // lib/profiles.ts PROTOCOL_VERSION_GRAMMAR applies to the core predicate).
82
+ expect(
83
+ typeof caps.protocolVersion === 'string' && /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/.test(caps.protocolVersion),
84
+ req('openwop.it.version-negotiation.get-well-known-openwop-returns-capabilities-with-protocolversion-string',
85
+ 'RFC 0149 §C; capabilities.schema.json protocolVersion.pattern',
86
+ `Capabilities.protocolVersion MUST be ASCII <major>.<minor> with no leading zeros (got ${JSON.stringify(caps.protocolVersion)})`,
87
+ ),
88
+ ).toBe(true);
78
89
  });
79
90
  });
80
91
 
@@ -118,6 +118,8 @@ export const REQUIRES_HOST_CALLBACK = 'the host POSTs a signed webhook delivery
118
118
  interface DeliveredRequest {
119
119
  readonly headers: Record<string, string>;
120
120
  readonly body: string;
121
+ /** Suite clock (ms) when the receiver recorded the delivery. */
122
+ readonly receivedAtMs: number;
121
123
  }
122
124
 
123
125
  async function startReceiver(): Promise<ScopedReceiver & { received: DeliveredRequest[] }> {
@@ -141,7 +143,7 @@ async function startReceiver(): Promise<ScopedReceiver & { received: DeliveredRe
141
143
  if (typeof v === 'string') headers[k.toLowerCase()] = v;
142
144
  else if (Array.isArray(v)) headers[k.toLowerCase()] = v.join(',');
143
145
  }
144
- received.push({ headers, body: hit.body });
146
+ received.push({ headers, body: hit.body, receivedAtMs: Date.now() });
145
147
  res.writeHead(204);
146
148
  res.end();
147
149
  });
@@ -352,10 +354,23 @@ describe('webhook-signed-delivery: end-to-end HMAC v1', () => {
352
354
  ).toBe(sub.webhookId);
353
355
 
354
356
  const timestamp = first.headers['x-openwop-timestamp'];
357
+ // unfailable-leg audit wave 2, 2026-09-27: this only required a non-empty
358
+ // string, so an ISO-8601 date, a milliseconds value, or a stale constant
359
+ // passed "MUST be a Unix-seconds integer" (the HMAC below signs whatever
360
+ // string was sent, so it could not catch it). Now: digits only (≤ 11 —
361
+ // seconds, not ms) and within the ±5 min window webhooks.md §"Verification
362
+ // recipe" tells every subscriber to enforce (a delivery outside it is
363
+ // rejected by a conforming subscriber, so the host's "when the dispatcher
364
+ // signed the body" value must fall inside it at receipt).
355
365
  expect(
356
- typeof timestamp === 'string' && timestamp.length > 0,
357
- req('openwop.it.webhook-signed-delivery.host-posts-run-events-to-subscriber-with-valid-x-openwop-signature', 'webhooks.md §"Delivery headers"', 'X-openwop-Timestamp MUST be a Unix-seconds integer string'),
366
+ typeof timestamp === 'string' && /^\d{1,11}$/.test(timestamp),
367
+ req('openwop.it.webhook-signed-delivery.host-posts-run-events-to-subscriber-with-valid-x-openwop-signature', 'webhooks.md §"Delivery headers"', `X-openwop-Timestamp MUST be a Unix-seconds integer string (got ${JSON.stringify(timestamp)})`),
358
368
  ).toBe(true);
369
+ const skewSeconds = Math.abs(Number(timestamp) - first.receivedAtMs / 1000);
370
+ expect(
371
+ skewSeconds,
372
+ req('openwop.it.webhook-signed-delivery.host-posts-run-events-to-subscriber-with-valid-x-openwop-signature', 'webhooks.md §"Delivery headers"; §"Verification recipe" step 1 (±5 minutes)', `X-openwop-Timestamp MUST be the signing time — within ±300 s of receipt (off by ${Math.round(skewSeconds)} s)`),
373
+ ).toBeLessThan(300);
359
374
 
360
375
  const signature = first.headers['x-openwop-signature'] ?? '';
361
376
  expect(