@openwop/openwop-conformance 2.42.3 → 2.42.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 (54) hide show
  1. package/CHANGELOG.md +22 -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 +292 -103
  6. package/schemas/CORPUS-STAMP.json +9 -9
  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/mcp-server-untrusted-args.test.ts +20 -5
  32. package/src/scenarios/mcp-tool-roundtrip.test.ts +7 -21
  33. package/src/scenarios/memory-compaction-provenance-tag.test.ts +11 -3
  34. package/src/scenarios/orchestratorDispatch.test.ts +18 -2
  35. package/src/scenarios/pack-registry.test.ts +8 -1
  36. package/src/scenarios/production-backpressure.test.ts +17 -11
  37. package/src/scenarios/redaction.test.ts +8 -2
  38. package/src/scenarios/replay-fanout-suppression.test.ts +18 -2
  39. package/src/scenarios/runner-ledger.test.ts +7 -2
  40. package/src/scenarios/stream-modes.test.ts +27 -0
  41. package/src/scenarios/tool-catalog-projection.test.ts +35 -12
  42. package/src/scenarios/v2-effect-identity-business-key.test.ts +40 -6
  43. package/src/scenarios/v2-era-2-append-vocabulary.test.ts +12 -3
  44. package/src/scenarios/v2-idempotency-in-flight.test.ts +16 -0
  45. package/src/scenarios/v2-mcp-tasks.test.ts +17 -3
  46. package/src/scenarios/v2-min-client-version.test.ts +11 -1
  47. package/src/scenarios/v2-run-diff-identical.test.ts +13 -5
  48. package/src/scenarios/v2-run-pause-resume.test.ts +4 -1
  49. package/src/scenarios/v2-sse-last-event-id.test.ts +7 -0
  50. package/src/scenarios/v2-version-header-honored.test.ts +22 -3
  51. package/src/scenarios/v2-webhook-durable-delivery.test.ts +8 -0
  52. package/src/scenarios/v2-webhook-unregister-stops-delivery.test.ts +6 -1
  53. package/src/scenarios/version-negotiation.test.ts +12 -1
  54. 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) {
@@ -459,6 +459,14 @@ describe('RFC 0173 §B — webhook-durable-delivery (gated on webhooks)', () =>
459
459
  ).toBeLessThanOrEqual(policy.maxAttempts);
460
460
  }
461
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.
462
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)');
463
471
  }
464
472
  expect(
@@ -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(