@openwop/openwop-conformance 2.42.6 → 2.42.8

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 (129) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +3 -3
  3. package/coverage.md +1 -1
  4. package/dist/lib/soft-skip.js +13 -0
  5. package/dist/spec-artifacts.lock.json +2 -2
  6. package/fixtures/conformance-envelope-retry-attempted.json +2 -1
  7. package/package.json +2 -2
  8. package/requirements.json +581 -175
  9. package/scenario-majors.json +22 -3
  10. package/schemas/CORPUS-STAMP.json +70 -67
  11. package/src/lib/audit-checkpoint.ts +72 -0
  12. package/src/lib/ext-claims.ts +96 -0
  13. package/src/lib/mock-ai-budgets.ts +40 -0
  14. package/src/lib/soft-skip.ts +14 -0
  15. package/src/scenarios/a2a-peer-authority.test.ts +6 -13
  16. package/src/scenarios/a2a-task-roundtrip.test.ts +6 -34
  17. package/src/scenarios/a2a-version-negotiation.test.ts +6 -12
  18. package/src/scenarios/a2ui-surface-delta-transport.test.ts +23 -12
  19. package/src/scenarios/agent-channel-dispatch.test.ts +13 -4
  20. package/src/scenarios/agent-live-invocation-bracket.test.ts +1 -1
  21. package/src/scenarios/agent-manifest-runtime.test.ts +3 -0
  22. package/src/scenarios/aiEnvelope.capBreached.test.ts +2 -2
  23. package/src/scenarios/aiEnvelope.contractRefusal.test.ts +2 -2
  24. package/src/scenarios/aiEnvelope.schemaDrift.test.ts +2 -2
  25. package/src/scenarios/aiEnvelope.universalKinds.test.ts +2 -2
  26. package/src/scenarios/aiproviders-selfhosted-honesty.test.ts +2 -2
  27. package/src/scenarios/append-ordering.test.ts +26 -21
  28. package/src/scenarios/artifact-auth.test.ts +3 -1
  29. package/src/scenarios/artifact-type-registration-source.test.ts +2 -2
  30. package/src/scenarios/audit-checkpoint-signature.test.ts +63 -0
  31. package/src/scenarios/audit-checkpoint-vectors.test.ts +93 -0
  32. package/src/scenarios/audit-log-integrity.test.ts +7 -16
  33. package/src/scenarios/auth-mtls.test.ts +7 -6
  34. package/src/scenarios/auth-subject-link-key-class.test.ts +3 -3
  35. package/src/scenarios/auth-subject-link.test.ts +19 -18
  36. package/src/scenarios/blob-presign-expiry.test.ts +2 -2
  37. package/src/scenarios/byok-roundtrip.test.ts +4 -1
  38. package/src/scenarios/compensation-behavior.test.ts +10 -5
  39. package/src/scenarios/conformance-advertised-seam-required.test.ts +6 -0
  40. package/src/scenarios/connection-pack-apihosts.test.ts +2 -2
  41. package/src/scenarios/connection-provider-resolution.test.ts +2 -2
  42. package/src/scenarios/context-budget-transcript-bound.test.ts +7 -3
  43. package/src/scenarios/context-summarization-replay.test.ts +2 -0
  44. package/src/scenarios/cost-attribution.test.ts +3 -1
  45. package/src/scenarios/debugBundle.test.ts +12 -4
  46. package/src/scenarios/discovery.test.ts +6 -2
  47. package/src/scenarios/dispatch-cross-worker-handoff.test.ts +2 -2
  48. package/src/scenarios/dispatch-input-mapping.test.ts +2 -2
  49. package/src/scenarios/dispatch-output-mapping.test.ts +15 -2
  50. package/src/scenarios/distillation-token-budget.test.ts +2 -2
  51. package/src/scenarios/durability-poison-exhaustion.test.ts +2 -2
  52. package/src/scenarios/envelope-completion-distinguishes-truncation.test.ts +51 -15
  53. package/src/scenarios/envelope-nl-to-format-engaged.test.ts +2 -2
  54. package/src/scenarios/envelope-recovery-applied.test.ts +2 -2
  55. package/src/scenarios/envelope-refusal-shape.test.ts +2 -2
  56. package/src/scenarios/envelope-retry-attempted.test.ts +2 -2
  57. package/src/scenarios/envelope-retry-exhausted.test.ts +2 -2
  58. package/src/scenarios/envelope-tier-one-subset-static.test.ts +1 -1
  59. package/src/scenarios/envelope-truncation-cap-exhaustion.test.ts +2 -2
  60. package/src/scenarios/eventOrdering.test.ts +17 -10
  61. package/src/scenarios/form-content-instantiation.test.ts +2 -2
  62. package/src/scenarios/heartbeat-idempotent-no-spam.test.ts +2 -2
  63. package/src/scenarios/highConcurrency.test.ts +9 -9
  64. package/src/scenarios/idempotency-concurrent-claim.test.ts +5 -5
  65. package/src/scenarios/idempotency-key-determinism.test.ts +8 -8
  66. package/src/scenarios/idempotency.test.ts +6 -6
  67. package/src/scenarios/idempotencyRetry.test.ts +16 -11
  68. package/src/scenarios/interrupt-token-matrix.test.ts +24 -11
  69. package/src/scenarios/mcp-cache-tenant-scope.test.ts +3 -3
  70. package/src/scenarios/mcp-current-auth-boundary.test.ts +9 -6
  71. package/src/scenarios/mcp-extension-opacity.test.ts +6 -5
  72. package/src/scenarios/mcp-mrtr-roundtrip.test.ts +9 -8
  73. package/src/scenarios/mcp-version-negotiation.test.ts +5 -10
  74. package/src/scenarios/media-url-inline-cap.test.ts +3 -2
  75. package/src/scenarios/memory-compaction-provenance-tag.test.ts +2 -0
  76. package/src/scenarios/memory-compaction-sr1-carry-forward.test.ts +2 -2
  77. package/src/scenarios/memory-consolidation-idempotent.test.ts +2 -2
  78. package/src/scenarios/memory-injection-budget.test.ts +2 -2
  79. package/src/scenarios/multi-party-conversation-behavioral.test.ts +2 -2
  80. package/src/scenarios/multi-region-idempotency-behavior.test.ts +9 -9
  81. package/src/scenarios/multi-region-idempotency.test.ts +2 -2
  82. package/src/scenarios/pack-registry-isolation.test.ts +13 -12
  83. package/src/scenarios/pack-registry.test.ts +3 -2
  84. package/src/scenarios/pause-resume.test.ts +30 -11
  85. package/src/scenarios/prompt-read-workspace-membership-enforced.test.ts +43 -34
  86. package/src/scenarios/provider-usage.test.ts +2 -2
  87. package/src/scenarios/redaction.test.ts +7 -5
  88. package/src/scenarios/redactionAdversarial.test.ts +5 -4
  89. package/src/scenarios/registry-public.test.ts +5 -1
  90. package/src/scenarios/replay-divergence-at-refusal.test.ts +10 -7
  91. package/src/scenarios/replay-fanout-suppression.test.ts +7 -4
  92. package/src/scenarios/replay-fork-arbitrary.test.ts +2 -2
  93. package/src/scenarios/replay-side-effect-suppression.test.ts +2 -2
  94. package/src/scenarios/replayDeterminism.test.ts +9 -4
  95. package/src/scenarios/runner-ledger.test.ts +4 -0
  96. package/src/scenarios/runtime-capabilities.test.ts +2 -0
  97. package/src/scenarios/speech-synthesis-roundtrip.test.ts +1 -1
  98. package/src/scenarios/streamReconnect.test.ts +2 -2
  99. package/src/scenarios/subrun-approval-gate.test.ts +2 -2
  100. package/src/scenarios/subworkflow-input-mapping.test.ts +2 -2
  101. package/src/scenarios/tool-catalog-projection.test.ts +10 -7
  102. package/src/scenarios/tool-session-lifecycle.test.ts +1 -1
  103. package/src/scenarios/trigger-ingestion.test.ts +2 -2
  104. package/src/scenarios/v2-a2ui-v09-surface.test.ts +15 -0
  105. package/src/scenarios/v2-client-version-header.test.ts +124 -0
  106. package/src/scenarios/v2-effect-identity-business-key.test.ts +12 -0
  107. package/src/scenarios/v2-ext-family-claims.test.ts +62 -0
  108. package/src/scenarios/v2-ext-rest-transport.test.ts +151 -0
  109. package/src/scenarios/v2-id-grammar.test.ts +17 -5
  110. package/src/scenarios/v2-idempotency-in-flight.test.ts +3 -0
  111. package/src/scenarios/v2-legacy-profiles-absent.test.ts +4 -6
  112. package/src/scenarios/v2-mcp-tasks.test.ts +1 -1
  113. package/src/scenarios/v2-min-client-version.test.ts +20 -14
  114. package/src/scenarios/v2-preferred-version-default.test.ts +19 -1
  115. package/src/scenarios/v2-refresh-sla.test.ts +6 -5
  116. package/src/scenarios/v2-sse-last-event-id-cursor.test.ts +3 -3
  117. package/src/scenarios/v2-sse-last-event-id.test.ts +2 -0
  118. package/src/scenarios/v2-version-header-honored.test.ts +3 -0
  119. package/src/scenarios/v2-webhook-durable-delivery.test.ts +2 -0
  120. package/src/scenarios/v2-webhook-generated-secret.test.ts +76 -0
  121. package/src/scenarios/v2-webhook-secret-rotation.test.ts +8 -1
  122. package/src/scenarios/verifier-gating.test.ts +2 -2
  123. package/src/scenarios/version-fold.test.ts +2 -2
  124. package/src/scenarios/wasm-pack-invoke-suspended.test.ts +7 -6
  125. package/src/scenarios/webhook-signed-delivery.test.ts +2 -0
  126. package/src/scenarios/workflow-chain-host-expansion.test.ts +10 -1
  127. package/src/scenarios/workflow-variable-format.test.ts +2 -2
  128. package/src/scenarios/workload-identity-chain-bounds.test.ts +6 -3
  129. package/vectors/audit-checkpoint-v1.json +260 -0
@@ -168,6 +168,9 @@ describe('v2-version-header-honored (RFC 0172 §A.3)', () => {
168
168
  },
169
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
170
  ).toEqual({ status: 406, code: 'protocol_version_unsupported', echoed: [...advertised].sort(), listsMajor2: false });
171
+ // partial-witness-ok: the 406 refusal was held to §1.3 row 2 in full above. It is a
172
+ // complete, correct answer, and there is no second representation to compare bytes
173
+ // against.
171
174
  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');
172
175
  }
173
176
  if (asked.status !== 200) {
@@ -366,6 +366,8 @@ describe('RFC 0173 §B — webhook-durable-delivery (gated on webhooks)', () =>
366
366
  }
367
367
  }
368
368
  } else {
369
+ // partial-witness-ok: delivery and retry were observed above; the spacing leg binds only
370
+ // an advertised backoff other than none.
369
371
  softSkip('inapplicable', 'no advertised retryPolicy.backoff other than none — the spacing leg is not asserted');
370
372
  }
371
373
 
@@ -0,0 +1,76 @@
1
+ /**
2
+ * RFC 0221 — a secret the host generates is returned once (`spec/v2/core/webhooks.md`
3
+ * §Surfaces; suite 2.42.8, target major 2; gated on the `webhooks` family and the
4
+ * `conformance-noop` fixture).
5
+ *
6
+ * `registerWebhook` makes `secret` optional. When the request omits it, the host
7
+ * MUST generate one and return it in the `201` as `secret`, the only time it
8
+ * appears on the wire, and every delivery to that subscription MUST verify under
9
+ * it (`OpenWOP-Signature`, scheme `v1`). When the request supplies a secret, the
10
+ * `201` MUST NOT echo it (RFC 0201 §B.6, now for every subscription).
11
+ *
12
+ * How it FAILS: a `201` without `secret` for a secret-less registration (the
13
+ * subscriber can never verify a delivery); a returned secret the deliveries are
14
+ * not signed with; a `201` that echoes a supplied secret.
15
+ *
16
+ * @see spec/v2/core/webhooks.md §Surfaces
17
+ * @see RFCS/0221-generated-webhook-secret-returned-once.md
18
+ */
19
+
20
+ import { afterEach, describe, it, expect } from 'vitest';
21
+ import { createHmac } from 'node:crypto';
22
+ import { req } from '../lib/requirement-ids.js';
23
+ import { readErrorCode } from '../lib/error-envelope.js';
24
+ import { softSkip } from '../lib/soft-skip.js';
25
+ import { v2Discovery, gateFamily } from '../lib/v2.js';
26
+ import { hitHeader, startModalReceiver, type ModalHit } from '../lib/webhook-receiver.js';
27
+ import { SW_FIXTURE, deliveriesFor, driveRun, loopbackRefusal, registerSw, unregisterAllSw, waitFor } from '../lib/standard-webhooks.js';
28
+
29
+ const ID = 'openwop.requirement.0221.generated-secret-returned';
30
+ const SPEC = 'RFC 0221 · webhooks.md §Surfaces';
31
+
32
+ let closeReceiver: (() => Promise<void>) | null = null;
33
+ afterEach(async () => {
34
+ await unregisterAllSw();
35
+ if (closeReceiver) { const c = closeReceiver; closeReceiver = null; await c(); }
36
+ });
37
+
38
+ function v1SignedBy(secret: string, h: ModalHit): boolean {
39
+ const ts = hitHeader(h, 'openwop-timestamp');
40
+ return ts !== undefined && hitHeader(h, 'openwop-signature') === `sha256=${createHmac('sha256', secret).update(`${ts}.${h.body}`, 'utf8').digest('hex')}`;
41
+ }
42
+
43
+ describe('RFC 0221 — a host-generated webhook secret is returned once', () => {
44
+ it('a secret-less registration returns the generated secret, and deliveries verify under it; a supplied secret is never echoed', async () => {
45
+ let doc: Record<string, unknown> | null = null;
46
+ try { doc = await v2Discovery(); } catch { doc = null; }
47
+ if (!doc) return softSkip('blocked', 'v2 discovery unreachable');
48
+ if (!(await gateFamily('webhooks'))) return softSkip('inapplicable', 'webhooks family not advertised (gate recorded under openwop.family.webhooks)');
49
+ const fixtures = Array.isArray(doc['fixtures']) ? (doc['fixtures'] as unknown[]) : [];
50
+ if (!fixtures.includes(SW_FIXTURE)) return softSkip('inapplicable', `${SW_FIXTURE} fixture not advertised — no run to deliver`);
51
+
52
+ const rx = await startModalReceiver();
53
+ closeReceiver = rx.close;
54
+ const target = rx.urlFor('no-echo');
55
+ const reg = await registerSw({ url: target.url, events: ['run.completed'] });
56
+ const refused = loopbackRefusal(reg, target.tunnelled);
57
+ if (refused) return softSkip('blocked', refused);
58
+ if (reg.status !== 201) return softSkip('blocked', `registerWebhook answered ${reg.status} ${readErrorCode(reg.json) ?? ''} — webhooks.md §Surfaces owns that contract`);
59
+
60
+ const body = reg.json as { webhookId: string; secret?: unknown };
61
+ expect(typeof body.secret === 'string' && body.secret.length > 0, req(ID, SPEC, 'a registration that omits secret MUST get the host-generated secret back in the 201 as a non-empty string')).toBe(true);
62
+ const secret = body.secret as string;
63
+
64
+ const run = await driveRun();
65
+ expect(run.status, req(ID, 'runs.md §Create', 'POST /runs MUST answer 201 for the noop fixture')).toBe(201);
66
+ await waitFor(() => deliveriesFor(rx.hits, body.webhookId, run.runId).length > 0, 15_000);
67
+ const delivery = deliveriesFor(rx.hits, body.webhookId, run.runId)[0];
68
+ expect(delivery, req(ID, 'webhooks.md §Durability', 'the subscription MUST receive the run.completed delivery')).toBeDefined();
69
+ expect(v1SignedBy(secret, delivery!), req(ID, SPEC, 'the delivery MUST verify (OpenWOP-Signature, scheme v1) under the secret the 201 returned')).toBe(true);
70
+
71
+ const supplied = `conformance-${'s'.repeat(24)}`;
72
+ const own = await registerSw({ url: rx.urlFor('no-echo').url, events: ['run.completed'], secret: supplied });
73
+ expect(own.status, req(ID, SPEC, `a registration with a supplied secret MUST answer 201 (got ${own.status} ${readErrorCode(own.json) ?? ''})`)).toBe(201);
74
+ expect(own.text.includes(supplied) || (own.json as { secret?: unknown }).secret !== undefined, req(ID, 'RFC 0221 · RFC 0201 §B.6', 'the 201 MUST NOT echo a supplied secret')).toBe(false);
75
+ });
76
+ });
@@ -117,7 +117,14 @@ describe('RFC 0201 §E — secret rotation overlaps, then retires (gated on webh
117
117
  // After the overlap: only when it fits the operator's wait cap.
118
118
  const expiresAt = Date.parse(body.previousSecretExpiresAt ?? '');
119
119
  if (expiresAt - Date.now() > CAP_MS) {
120
- return softSkip('inapplicable', `the post-overlap leg is not observed: overlapSeconds (${overlapSeconds}) exceeds the suite wait cap (${CAP_MS}ms; raise OPENWOP_WEBHOOK_RETRY_WAIT_MS to witness it)`);
120
+ // partial-witness-ok: the tenant check, the rotate response and the dual signature during
121
+ // the overlap were asserted above. "The previous secret MUST NOT sign after
122
+ // previousSecretExpiresAt" cannot be observed inside a bounded run when the host's
123
+ // advertised webhooks.secretRotation.overlapSeconds outlasts the suite's wait cap, and
124
+ // blocking every long-overlap host would deny certification to conforming hosts
125
+ // (steward ruling, 2026-09-27). The row detail names both numbers. Raise
126
+ // OPENWOP_WEBHOOK_RETRY_WAIT_MS past the overlap to witness it.
127
+ return softSkip('inapplicable', `the post-overlap leg is not observed: the advertised webhooks.secretRotation.overlapSeconds is ${overlapSeconds}s (expiry ${new Date(expiresAt).toISOString()}) and the suite wait cap is ${CAP_MS}ms; raise OPENWOP_WEBHOOK_RETRY_WAIT_MS past the overlap to witness it`);
121
128
  }
122
129
  await new Promise((r) => setTimeout(r, Math.max(0, expiresAt - Date.now()) + 1_500));
123
130
  const after = await oneDelivery(rx, webhookId);
@@ -24,7 +24,7 @@ import { driver } from '../lib/driver.js';
24
24
  import { behaviorGate } from '../lib/behavior-gate.js';
25
25
  import { readCapabilityFamily } from '../lib/discovery-capabilities.js';
26
26
  import { req } from '../lib/requirement-ids.js';
27
- import { softSkip } from '../lib/soft-skip.js';
27
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
28
28
 
29
29
  const SEAM = '/v1/host/sample/agents/verify-run';
30
30
 
@@ -62,7 +62,7 @@ describe('verifier-gating (RFC 0090 §B)', () => {
62
62
 
63
63
  // PASS verdict → completes normally.
64
64
  const passRes = await driver.post(SEAM, { simulateVerdict: 'pass' });
65
- if (passRes.status === 404) return softSkip('blocked', 'precondition not met — `passRes.status === 404` returned early (seam, prior step, or fixture unavailable)');
65
+ if (passRes.status === 404) return blockedDespiteAssertions('the verify seam answered the fail verdict but not the pass verdict — pass-completes is unobserved');
66
66
  expect(
67
67
  verifiedVerdict(passRes.json) === 'pass',
68
68
  req('openwop.it.verifier-gating.a-fail-verdict-blocks-commit-on-a-gating-host-a-pass-verdict-completes', 'RFC 0090 §A', 'a verify-run forcing a pass MUST emit agent.verified{verdict:"pass"}'),
@@ -34,7 +34,7 @@
34
34
  */
35
35
 
36
36
  import { describe, it, expect } from 'vitest';
37
- import { softSkip } from '../lib/soft-skip.js';
37
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
38
38
  import { driver, type OpenWOPResponse } from '../lib/driver.js';
39
39
  import { pollUntilTerminal } from '../lib/polling.js';
40
40
  import { isFixtureAdvertised } from '../lib/fixtures.js';
@@ -106,7 +106,7 @@ describe.skipIf(SKIP_NO_FIXTURE)('version-fold: forced engine versions fold-best
106
106
  if (create.status === 403 && errCode(create.json) === 'force_engine_version_forbidden') {
107
107
  // eslint-disable-next-line no-console
108
108
  console.warn('[version-fold] API key is production-scoped (403 force_engine_version_forbidden); skipping');
109
- return softSkip('blocked', '[version-fold] API key is production-scoped (403 force_engine_version_forbidden); skipping (create.status === 403 && errCode(create.json) === \'force_engine_version_forbidden\')');
109
+ return blockedDespiteAssertions('[version-fold] API key is production-scoped (403 force_engine_version_forbidden) — the advertised forceEngineVersionRange cannot be exercised; run with a test key');
110
110
  }
111
111
  expect(
112
112
  create.status,
@@ -65,12 +65,13 @@ describe('wasm-pack-invoke-suspended: suspend → resume round-trip', () => {
65
65
  'RFCS/0008-wasm-abi.md §D',
66
66
  "if a host doesn't implement WASM-driven suspends it MUST surface a recognizable code",
67
67
  )).toBe(true);
68
- return softSkip('blocked', 'precondition not met — `terminal.status === \'failed\'` returned early (seam, prior step, or fixture unavailable)');
68
+ // A failed run carrying the recognizable code IS one of the two conformant
69
+ // outcomes: the requirement was observed.
70
+ } else {
71
+ // Completed path: the reference pack never suspends. Asserting
72
+ // 'completed' confirms the host did not spuriously enter a
73
+ // waiting-* state.
74
+ expect(terminal.status).toBe('completed');
69
75
  }
70
-
71
- // Completed path: the reference pack never suspends. Asserting
72
- // 'completed' confirms the host did not spuriously enter a
73
- // waiting-* state.
74
- expect(terminal.status).toBe('completed');
75
76
  });
76
77
  });
@@ -390,6 +390,8 @@ describe('webhook-signed-delivery: end-to-end HMAC v1', () => {
390
390
  // `X-openwop-*` counterpart and the signature verifies reading either.
391
391
  const dual = first.headers['openwop-signature'];
392
392
  if (dual === undefined) {
393
+ // partial-witness-ok: every X-openwop-* delivery MUST was asserted above; dual-emitting
394
+ // the OpenWOP-* family is an RFC 0165 §C.1 SHOULD.
393
395
  softSkip('inapplicable', 'host does not yet dual-emit the OpenWOP-* webhook header family (RFC 0165 §C.1 — SHOULD)');
394
396
  } else {
395
397
  for (const [neu, old] of [
@@ -169,7 +169,10 @@ describe('workflow-chain-host-expansion: live host wraps expansion algorithm cor
169
169
  const [fm, fn] = PACK.version.split('.').map(Number);
170
170
  const older = hm! < fm! || (hm === fm && hn! < fn!);
171
171
  expect(older, req('openwop.it.workflow-chain-host-expansion.positive-1-node-chain-expansion-matches-the-reference-library-for-the-bundled-pa', 'workflow-chain-host-expansion', `host bundles workflow-chain-sample ${body.packVersion}; this suite's fixture is ${PACK.version} — a host pack NEWER than the suite is unaccounted for`)).toBe(true);
172
- softSkip('blocked', `host bundles workflow-chain-sample ${body.packVersion} (suite fixture ${PACK.version}) — the RFC 0157 chains are absent there until its conformance pin catches up`);
172
+ // No note here (until 2026-09-27 a blocked soft-skip turned this leg into
173
+ // a partial-witness pass): the chain this leg expands is identical in the
174
+ // older pack, so the comparison below is complete. The RFC 0157 legs,
175
+ // whose chains an older pack lacks, record their own `blocked`.
173
176
  }
174
177
  expect(typeof body.expansionId).toBe('string');
175
178
  expect(body.expansionId.length).toBeGreaterThan(0);
@@ -321,6 +324,9 @@ describe('workflow-chain-host-expansion: live host wraps expansion algorithm cor
321
324
  // the sign flipped.
322
325
  expect(res.status >= 400, req('openwop.it.workflow-chain-host-expansion.rfc-0157-a-chain-carrying-an-unwind-policy-is-refused-with-capability-required-o', 'compensation.md §"Workflow policy"', 'a non-advertising host MUST refuse a chain carrying `compensation` (policy) — 4xx, not 200')).toBe(true);
323
326
  expect(readErrorCode(res.json), req('openwop.it.workflow-chain-host-expansion.rfc-0157-a-chain-carrying-an-unwind-policy-is-refused-with-capability-required-o', 'capabilities.md §"Unsupported capability — refusal contract"', '`capability_required`')).toBe('capability_required');
327
+ // partial-witness-ok: the half of this requirement that binds a
328
+ // non-advertising host — refuse with capability_required — was observed
329
+ // above; the expand-on-an-advertiser half does not apply to this host.
324
330
  return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!advertises` returned early');
325
331
  }
326
332
  expect(res.status, req('openwop.it.workflow-chain-host-expansion.rfc-0157-a-chain-carrying-an-unwind-policy-is-refused-with-capability-required-o', 'workflow-chain-packs.md §"Compensation (RFC 0157)"', 'an advertising host expands the policy-carrying chain')).toBe(200);
@@ -338,6 +344,9 @@ describe('workflow-chain-host-expansion: live host wraps expansion algorithm cor
338
344
  if (body.settings?.compensation !== undefined) {
339
345
  expect(body.settings.compensation, req('openwop.it.workflow-chain-host-expansion.rfc-0157-a-chain-carrying-an-unwind-policy-is-refused-with-capability-required-o', 'workflow-chain-packs.md §"Compensation (RFC 0157)" rule 9b', 'the chain policy becomes settings.compensation, copied verbatim')).toEqual(expected.settingsCompensation);
340
346
  } else {
347
+ // partial-witness-ok: the node carry was observed above; echoing
348
+ // `settings` on the expand seam is host-optional, so the rule 9b policy
349
+ // carry is unobservable through it on a conforming host.
341
350
  softSkip('blocked', 'the expand seam does not echo `settings`, so the policy → settings.compensation carry (rule 9b) is unobservable through it; the node carry above was witnessed');
342
351
  }
343
352
  });
@@ -48,7 +48,7 @@ import { behaviorGate } from '../lib/behavior-gate.js';
48
48
  import { readCapabilityFamily } from '../lib/discovery-capabilities.js';
49
49
  import { isFixtureAdvertised } from '../lib/fixtures.js';
50
50
  import { driver } from '../lib/driver.js';
51
- import { softSkip } from '../lib/soft-skip.js';
51
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
52
52
  import { req } from '../lib/requirement-ids.js';
53
53
  const WORKFLOW_DEF = join(SCHEMAS_DIR, 'workflow-definition.schema.json');
54
54
  // S38 (2026-08-17): `spec/` is NOT in the published package (`files`), so a path built
@@ -193,7 +193,7 @@ describe('workflow-variable-format §B: host behaviour (RFC 0136, capability-gat
193
193
  if (res.status === 404) return softSkip('blocked', 'precondition not met — `res.status === 404` returned early (A host advertising deferredParameters but not yet serving the variables[]-returning seam extension soft-skips (404), as does a host with the seam disabled in…');
194
194
  expect(res.status, req('openwop.it.workflow-variable-format.b1-deferred-expansion-mints-format-onto-the-workflowvariable-string-copied-req-7', '§Deferred-parameter expansion', 'the deferred-expand seam returns 200')).toBe(200);
195
195
  const variables = (res.json as { variables?: Array<{ name: string; type?: string; format?: string }> }).variables;
196
- if (!Array.isArray(variables)) return softSkip('blocked', 'precondition not met — `!Array.isArray(variables)` returned early (seam present but not returning variables[] yet — soft-skip) (seam, prior step, or fixture unavailable)'); // seam present but not returning variables[] yet — soft-skip
196
+ if (!Array.isArray(variables)) return blockedDespiteAssertions('the deferred-expand seam answered 200 without variables[] — the minted WorkflowVariable.format is unobserved');
197
197
  const fmt = (n: string): string | undefined => variables.find((v) => v.name === n)?.format;
198
198
 
199
199
  expect(fmt('email'), req('openwop.it.workflow-variable-format.b1-deferred-expansion-mints-format-onto-the-workflowvariable-string-copied-req-7', '§WorkflowVariable', 'req 7: a string param\'s recognised `format` is minted verbatim')).toBe('email');
@@ -36,7 +36,7 @@ import { readErrorCode, readRetriable } from '../lib/error-envelope.js';
36
36
  import { driver } from '../lib/driver.js';
37
37
  import { behaviorGate } from '../lib/behavior-gate.js';
38
38
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
39
- import { seamAbsent, softSkip } from '../lib/soft-skip.js';
39
+ import { seamAbsent, softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
40
40
  import { req } from '../lib/requirement-ids.js';
41
41
 
42
42
  const DELEGATION_PROFILE = 'openwop-workload-identity-delegation';
@@ -94,7 +94,7 @@ describe('RFC 0154 §B — delegation chain bounds (capability-gated behavior)',
94
94
  const depth = caps.maxChainDepth as number;
95
95
  const chain = Array.from({ length: depth + 1 }, (_, i) => hop(i + 1));
96
96
  const r = await resolve({ identity: { ...IDENTITY, delegation: { chain, audience: 'openwop-host', expiresAt: LIVE } }, expectedAudience: 'openwop-host' });
97
- if (r === null) return softSkip('blocked', 'precondition not met — `r === null` returned early (seam, prior step, or fixture unavailable)');
97
+ if (r === null) return blockedDespiteAssertions('the identity-resolve seam is unwired — the maxChainDepth refusal is unobserved');
98
98
  expectRefusal('openwop.it.workload-identity-chain-bounds.a-chain-longer-than-the-advertised-maxchaindepth-is-refused', r, 'delegation_chain_too_long', `a chain of ${depth + 1} hops exceeds the advertised bound of ${depth} — each hop is another party the host trusts transitively`);
99
99
  });
100
100
 
@@ -113,7 +113,10 @@ describe('RFC 0154 §B — delegation chain bounds (capability-gated behavior)',
113
113
  expect(r.status >= 400, req('openwop.it.workload-identity-chain-bounds.a-chain-that-revisits-a-subject-is-refused-as-cyclic', 'RFCS/0154 §B', 'a cyclic chain is refused')).toBe(true);
114
114
  expect(['delegation_chain_cyclic', 'delegation_chain_too_long'], req('openwop.it.workload-identity-chain-bounds.a-chain-that-revisits-a-subject-is-refused-as-cyclic', 'spec/v1/host-sample-test-seams.md §20', 'cyclic or too-long — the bound is below the cycle length')).toContain(readErrorCode(r.json));
115
115
  expect(readRetriable(r.json)).toBe(false);
116
- return softSkip('blocked', 'precondition not met — `bound < 3` returned early (seam, prior step, or fixture unavailable)');
116
+ // partial-witness-ok: with maxChainDepth < 3 a 3-hop cyclic chain is also too long. The
117
+ // non-retriable refusal (either code) was asserted above; the cyclic-specific code cannot
118
+ // be told apart here.
119
+ return softSkip('inapplicable', 'maxChainDepth < 3: the cyclic chain is also too long, so the refusal was checked for either code');
117
120
  }
118
121
  expectRefusal('openwop.it.workload-identity-chain-bounds.a-chain-that-revisits-a-subject-is-refused-as-cyclic', r, 'delegation_chain_cyclic', 'a subject appearing twice is a chain that loops back through authority it already spent — unbounded laundering with a bounded length');
119
122
  });
@@ -0,0 +1,260 @@
1
+ {
2
+ "$comment": [
3
+ "RFC 0218 — vectors for the audit checkpoint preimage (spec/v1/auth-profiles.md §\"Audit-log integrity\" 3).",
4
+ "A conforming producer or verifier MUST reproduce every entryHash, merkleRoot and signature here, and MUST refuse every `refusals` case.",
5
+ "The key is RFC 8032 §7.1 TEST 1. It is a published test key: never use it for anything else."
6
+ ],
7
+ "spec": "RFC 0218; RFC 8785; RFC 8032",
8
+ "construction": {
9
+ "entryHash": "lowercase-hex SHA-256 of the entry's RFC 8785 JCS bytes (the value the next entry carries as prevHash)",
10
+ "range": "entries with sequence in (P, atSequence], P = the previous checkpoint's atSequence, or 0 for the first",
11
+ "parent": "lowercase-hex SHA-256 of the ASCII bytes of left ‖ right (each 64 lowercase hex)",
12
+ "odd": "a last odd node is promoted unchanged, never duplicated",
13
+ "signature": "Ed25519 over the 32 bytes merkleRoot hex-decodes to; base64 (RFC 4648 §4, padded)"
14
+ },
15
+ "publicKeySpkiBase64": "MCowBQYDK2VwAyEA11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=",
16
+ "entries": [
17
+ {
18
+ "entry": {
19
+ "action": "run.created",
20
+ "actor": "key:vector-1",
21
+ "atSequence": 1,
22
+ "details": {
23
+ "runId": "run-1"
24
+ },
25
+ "occurredAt": "2026-09-27T00:00:01.000Z",
26
+ "prevHash": null,
27
+ "target": "run-1"
28
+ },
29
+ "entryHash": "2bb3eb03f572491c7334c1a5a12a43e5786663fd4df773b0beb9d05a5d1b496e"
30
+ },
31
+ {
32
+ "entry": {
33
+ "action": "run.created",
34
+ "actor": "key:vector-0",
35
+ "atSequence": 2,
36
+ "details": {
37
+ "runId": "run-2"
38
+ },
39
+ "occurredAt": "2026-09-27T00:00:02.000Z",
40
+ "prevHash": "2bb3eb03f572491c7334c1a5a12a43e5786663fd4df773b0beb9d05a5d1b496e",
41
+ "target": "run-2"
42
+ },
43
+ "entryHash": "66892fdd522ad21a99968866d05cb92f4c1db98ddfebf747817891d744060fda"
44
+ },
45
+ {
46
+ "entry": {
47
+ "action": "interrupt.resolved",
48
+ "actor": "key:vector-1",
49
+ "atSequence": 3,
50
+ "details": {
51
+ "runId": "run-3"
52
+ },
53
+ "occurredAt": "2026-09-27T00:00:03.000Z",
54
+ "prevHash": "66892fdd522ad21a99968866d05cb92f4c1db98ddfebf747817891d744060fda",
55
+ "target": "run-3"
56
+ },
57
+ "entryHash": "5873ed119eb591fa8c4af1d7d9d947662c78ca5569c57733ebab3d2b399da6fb"
58
+ },
59
+ {
60
+ "entry": {
61
+ "action": "run.created",
62
+ "actor": "key:vector-0",
63
+ "atSequence": 4,
64
+ "details": {
65
+ "runId": "run-4"
66
+ },
67
+ "occurredAt": "2026-09-27T00:00:04.000Z",
68
+ "prevHash": "5873ed119eb591fa8c4af1d7d9d947662c78ca5569c57733ebab3d2b399da6fb",
69
+ "target": "run-4"
70
+ },
71
+ "entryHash": "4e48e5dd88bf081edfdbb176a89cfb75697fd4adec954272d65f804ce5643518"
72
+ },
73
+ {
74
+ "entry": {
75
+ "action": "run.created",
76
+ "actor": "key:vector-1",
77
+ "atSequence": 5,
78
+ "details": {
79
+ "note": "ünïcode ✓",
80
+ "votes": [
81
+ 1,
82
+ 2
83
+ ]
84
+ },
85
+ "occurredAt": "2026-09-27T00:00:05.000Z",
86
+ "prevHash": "4e48e5dd88bf081edfdbb176a89cfb75697fd4adec954272d65f804ce5643518",
87
+ "target": "run-5"
88
+ },
89
+ "entryHash": "9930634e696476f67dce8ea69bd89e6ef1d77dbe3279ec694e47e7dacda47f8a"
90
+ },
91
+ {
92
+ "entry": {
93
+ "action": "interrupt.resolved",
94
+ "actor": "key:vector-0",
95
+ "atSequence": 6,
96
+ "details": {
97
+ "runId": "run-6"
98
+ },
99
+ "occurredAt": "2026-09-27T00:00:06.000Z",
100
+ "prevHash": "9930634e696476f67dce8ea69bd89e6ef1d77dbe3279ec694e47e7dacda47f8a",
101
+ "target": "run-6"
102
+ },
103
+ "entryHash": "eda0c73fa14541ca52f2fa9446f07f319a3192c54e66a9a23a48fe22f5c5316f"
104
+ },
105
+ {
106
+ "entry": {
107
+ "action": "run.created",
108
+ "actor": "key:vector-1",
109
+ "atSequence": 7,
110
+ "details": {
111
+ "runId": "run-7"
112
+ },
113
+ "occurredAt": "2026-09-27T00:00:07.000Z",
114
+ "prevHash": "eda0c73fa14541ca52f2fa9446f07f319a3192c54e66a9a23a48fe22f5c5316f",
115
+ "target": "run-7"
116
+ },
117
+ "entryHash": "5d2e74535536c835517898427b6e152fcdd5ed79751d165ffe736d66529ea2c0"
118
+ },
119
+ {
120
+ "entry": {
121
+ "action": "run.created",
122
+ "actor": "key:vector-0",
123
+ "atSequence": 8,
124
+ "details": {
125
+ "runId": "run-8"
126
+ },
127
+ "occurredAt": "2026-09-27T00:00:08.000Z",
128
+ "prevHash": "5d2e74535536c835517898427b6e152fcdd5ed79751d165ffe736d66529ea2c0",
129
+ "target": "run-8"
130
+ },
131
+ "entryHash": "ebe7b92183a8fe2712cb524d7b5ebf052ef85b1c0cffd8b9a977a33d80cb8a4c"
132
+ },
133
+ {
134
+ "entry": {
135
+ "action": "interrupt.resolved",
136
+ "actor": "key:vector-1",
137
+ "atSequence": 9,
138
+ "details": {
139
+ "runId": "run-9"
140
+ },
141
+ "occurredAt": "2026-09-27T00:00:09.000Z",
142
+ "prevHash": "ebe7b92183a8fe2712cb524d7b5ebf052ef85b1c0cffd8b9a977a33d80cb8a4c",
143
+ "target": "run-9"
144
+ },
145
+ "entryHash": "6c3648ec20243c855823056e5ae63b52e7d3e570feef5f6d7a182e084bde6c08"
146
+ },
147
+ {
148
+ "entry": {
149
+ "action": "run.created",
150
+ "actor": "key:vector-0",
151
+ "atSequence": 10,
152
+ "details": {
153
+ "runId": "run-10"
154
+ },
155
+ "occurredAt": "2026-09-27T00:00:10.000Z",
156
+ "prevHash": "6c3648ec20243c855823056e5ae63b52e7d3e570feef5f6d7a182e084bde6c08",
157
+ "target": "run-10"
158
+ },
159
+ "entryHash": "740826be7291e62cd84199d8702eb7b32db57f12376a6bc13f680acdaba61bce"
160
+ },
161
+ {
162
+ "entry": {
163
+ "action": "run.created",
164
+ "actor": "key:vector-1",
165
+ "atSequence": 11,
166
+ "details": {
167
+ "runId": "run-11"
168
+ },
169
+ "occurredAt": "2026-09-27T00:00:11.000Z",
170
+ "prevHash": "740826be7291e62cd84199d8702eb7b32db57f12376a6bc13f680acdaba61bce",
171
+ "target": "run-11"
172
+ },
173
+ "entryHash": "31d04cd504983f8c9c740128167063a6c4708139e9a9d1b9891e8933792390e4"
174
+ },
175
+ {
176
+ "entry": {
177
+ "action": "interrupt.resolved",
178
+ "actor": "key:vector-0",
179
+ "atSequence": 12,
180
+ "details": {
181
+ "runId": "run-12"
182
+ },
183
+ "occurredAt": "2026-09-27T00:00:12.000Z",
184
+ "prevHash": "31d04cd504983f8c9c740128167063a6c4708139e9a9d1b9891e8933792390e4",
185
+ "target": "run-12"
186
+ },
187
+ "entryHash": "213f18d0c231ca4206f7aba10ed4b5a11a7b51f71d77dd4c4f04455a24238fe1"
188
+ }
189
+ ],
190
+ "checkpoints": [
191
+ {
192
+ "atSequence": 1,
193
+ "previousAtSequence": 0,
194
+ "leafCount": 1,
195
+ "merkleRoot": "2bb3eb03f572491c7334c1a5a12a43e5786663fd4df773b0beb9d05a5d1b496e",
196
+ "signature": "+O3og3Lozimsu+l0X6NZ3bTpFhvE6bFdVNqOUpAeHGoRSgeAyuufmXPKwoHcnAr4SqxdUtaXhjiyp4jicjYnAg=="
197
+ },
198
+ {
199
+ "atSequence": 3,
200
+ "previousAtSequence": 1,
201
+ "leafCount": 2,
202
+ "merkleRoot": "42750e6edbe7a4e7f10f5af0ebc0a25c45db6f89193dd2688cca5cc405d95b32",
203
+ "signature": "9Yo8N2iXAYLIXIlysqH0OfLkSlgzX4Za4fM/S/1MtIdS1khdBUZ6kTDAYT2Z0XhH0/Qq/KMJA4qjfJekx4oHDg=="
204
+ },
205
+ {
206
+ "atSequence": 7,
207
+ "previousAtSequence": 3,
208
+ "leafCount": 4,
209
+ "merkleRoot": "5e277e32a11f6a3ae377850cdf199e6726daa32e0d83873b698f07aa3096c3e2",
210
+ "signature": "rcjz09tb2ltcoaMYUKjZbp7iY2AHo0WXrJyOPny8T5u/uryyYIgVyzBaEZ3BT3kbAT1y+x7MHtStEt1sIsn2Dw=="
211
+ },
212
+ {
213
+ "atSequence": 12,
214
+ "previousAtSequence": 7,
215
+ "leafCount": 5,
216
+ "merkleRoot": "44a4232543a863aa7fc48477ceb0872e5513dd18db568201990b1b83b4fbbf96",
217
+ "signature": "axFFc3cFhUONP0Y7Is1vWMg12nNvEiT1aZV54XBcj7LN58VbVqGSWQS5jJ5fdzYM0DjfdPJK1x+pKR4KAuCfCg=="
218
+ }
219
+ ],
220
+ "refusals": [
221
+ {
222
+ "id": "interior-nodes-as-leaves",
223
+ "why": "the 4-leaf checkpoint at 7 recomputed from its two level-1 nodes gives the same root; a verifier that does not hold the leaf count to atSequence − P accepts it",
224
+ "checkpointAtSequence": 7,
225
+ "leaves": [
226
+ "62cf43b39d9b7ad76060add3967e09f876aa8d962bafd224e1190c3bb7495f3d",
227
+ "a4c9291d9fa45b513d9f22ad3fb8f3d9716869e747c822e49845d4f164328de9"
228
+ ],
229
+ "expectedLeaves": 4
230
+ },
231
+ {
232
+ "id": "uppercase-leaf",
233
+ "why": "leaves are lowercase hex; an uppercase leaf hashes to a different parent",
234
+ "leaves": [
235
+ "2BB3EB03F572491C7334C1A5A12A43E5786663FD4DF773B0BEB9D05A5D1B496E"
236
+ ],
237
+ "expectedLeaves": 1
238
+ },
239
+ {
240
+ "id": "empty-range",
241
+ "why": "a checkpoint anchors at least one entry",
242
+ "leaves": [],
243
+ "expectedLeaves": 0
244
+ }
245
+ ],
246
+ "signatureRefusals": [
247
+ {
248
+ "id": "signed-canonical-json",
249
+ "why": "a signature over the checkpoint object's canonical JSON instead of the root bytes (the reading the schema description once gave) does not verify",
250
+ "merkleRoot": "42750e6edbe7a4e7f10f5af0ebc0a25c45db6f89193dd2688cca5cc405d95b32",
251
+ "signature": "PNlGy3uTxxx3RXhFHBTV1wCm4XpluOy5QXDR4gWHQjh4tNBEYKN0ur7KJxvNXIHTWdKZSWK2Yk0SB4cJk+ZhBw=="
252
+ },
253
+ {
254
+ "id": "signed-hex-text",
255
+ "why": "a signature over the 64 ASCII hex characters instead of the 32 decoded bytes does not verify",
256
+ "merkleRoot": "42750e6edbe7a4e7f10f5af0ebc0a25c45db6f89193dd2688cca5cc405d95b32",
257
+ "signature": "niqG8/dnZLpwv8WfBW082VDfTi5UCrH7uck+hSh3a6ne3RhO+KWvPbyLK++Fw2vumLqD6P4zfTfEgamjxM4ZDA=="
258
+ }
259
+ ]
260
+ }