@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.
- package/CHANGELOG.md +34 -0
- package/README.md +3 -3
- package/coverage.md +1 -1
- package/dist/lib/soft-skip.js +13 -0
- package/dist/spec-artifacts.lock.json +2 -2
- package/fixtures/conformance-envelope-retry-attempted.json +2 -1
- package/package.json +2 -2
- package/requirements.json +581 -175
- package/scenario-majors.json +22 -3
- package/schemas/CORPUS-STAMP.json +70 -67
- package/src/lib/audit-checkpoint.ts +72 -0
- package/src/lib/ext-claims.ts +96 -0
- package/src/lib/mock-ai-budgets.ts +40 -0
- package/src/lib/soft-skip.ts +14 -0
- package/src/scenarios/a2a-peer-authority.test.ts +6 -13
- package/src/scenarios/a2a-task-roundtrip.test.ts +6 -34
- package/src/scenarios/a2a-version-negotiation.test.ts +6 -12
- package/src/scenarios/a2ui-surface-delta-transport.test.ts +23 -12
- package/src/scenarios/agent-channel-dispatch.test.ts +13 -4
- package/src/scenarios/agent-live-invocation-bracket.test.ts +1 -1
- package/src/scenarios/agent-manifest-runtime.test.ts +3 -0
- package/src/scenarios/aiEnvelope.capBreached.test.ts +2 -2
- package/src/scenarios/aiEnvelope.contractRefusal.test.ts +2 -2
- package/src/scenarios/aiEnvelope.schemaDrift.test.ts +2 -2
- package/src/scenarios/aiEnvelope.universalKinds.test.ts +2 -2
- package/src/scenarios/aiproviders-selfhosted-honesty.test.ts +2 -2
- package/src/scenarios/append-ordering.test.ts +26 -21
- package/src/scenarios/artifact-auth.test.ts +3 -1
- package/src/scenarios/artifact-type-registration-source.test.ts +2 -2
- package/src/scenarios/audit-checkpoint-signature.test.ts +63 -0
- package/src/scenarios/audit-checkpoint-vectors.test.ts +93 -0
- package/src/scenarios/audit-log-integrity.test.ts +7 -16
- package/src/scenarios/auth-mtls.test.ts +7 -6
- package/src/scenarios/auth-subject-link-key-class.test.ts +3 -3
- package/src/scenarios/auth-subject-link.test.ts +19 -18
- package/src/scenarios/blob-presign-expiry.test.ts +2 -2
- package/src/scenarios/byok-roundtrip.test.ts +4 -1
- package/src/scenarios/compensation-behavior.test.ts +10 -5
- package/src/scenarios/conformance-advertised-seam-required.test.ts +6 -0
- package/src/scenarios/connection-pack-apihosts.test.ts +2 -2
- package/src/scenarios/connection-provider-resolution.test.ts +2 -2
- package/src/scenarios/context-budget-transcript-bound.test.ts +7 -3
- package/src/scenarios/context-summarization-replay.test.ts +2 -0
- package/src/scenarios/cost-attribution.test.ts +3 -1
- package/src/scenarios/debugBundle.test.ts +12 -4
- package/src/scenarios/discovery.test.ts +6 -2
- package/src/scenarios/dispatch-cross-worker-handoff.test.ts +2 -2
- package/src/scenarios/dispatch-input-mapping.test.ts +2 -2
- package/src/scenarios/dispatch-output-mapping.test.ts +15 -2
- package/src/scenarios/distillation-token-budget.test.ts +2 -2
- package/src/scenarios/durability-poison-exhaustion.test.ts +2 -2
- package/src/scenarios/envelope-completion-distinguishes-truncation.test.ts +51 -15
- package/src/scenarios/envelope-nl-to-format-engaged.test.ts +2 -2
- package/src/scenarios/envelope-recovery-applied.test.ts +2 -2
- package/src/scenarios/envelope-refusal-shape.test.ts +2 -2
- package/src/scenarios/envelope-retry-attempted.test.ts +2 -2
- package/src/scenarios/envelope-retry-exhausted.test.ts +2 -2
- package/src/scenarios/envelope-tier-one-subset-static.test.ts +1 -1
- package/src/scenarios/envelope-truncation-cap-exhaustion.test.ts +2 -2
- package/src/scenarios/eventOrdering.test.ts +17 -10
- package/src/scenarios/form-content-instantiation.test.ts +2 -2
- package/src/scenarios/heartbeat-idempotent-no-spam.test.ts +2 -2
- package/src/scenarios/highConcurrency.test.ts +9 -9
- package/src/scenarios/idempotency-concurrent-claim.test.ts +5 -5
- package/src/scenarios/idempotency-key-determinism.test.ts +8 -8
- package/src/scenarios/idempotency.test.ts +6 -6
- package/src/scenarios/idempotencyRetry.test.ts +16 -11
- package/src/scenarios/interrupt-token-matrix.test.ts +24 -11
- package/src/scenarios/mcp-cache-tenant-scope.test.ts +3 -3
- package/src/scenarios/mcp-current-auth-boundary.test.ts +9 -6
- package/src/scenarios/mcp-extension-opacity.test.ts +6 -5
- package/src/scenarios/mcp-mrtr-roundtrip.test.ts +9 -8
- package/src/scenarios/mcp-version-negotiation.test.ts +5 -10
- package/src/scenarios/media-url-inline-cap.test.ts +3 -2
- package/src/scenarios/memory-compaction-provenance-tag.test.ts +2 -0
- package/src/scenarios/memory-compaction-sr1-carry-forward.test.ts +2 -2
- package/src/scenarios/memory-consolidation-idempotent.test.ts +2 -2
- package/src/scenarios/memory-injection-budget.test.ts +2 -2
- package/src/scenarios/multi-party-conversation-behavioral.test.ts +2 -2
- package/src/scenarios/multi-region-idempotency-behavior.test.ts +9 -9
- package/src/scenarios/multi-region-idempotency.test.ts +2 -2
- package/src/scenarios/pack-registry-isolation.test.ts +13 -12
- package/src/scenarios/pack-registry.test.ts +3 -2
- package/src/scenarios/pause-resume.test.ts +30 -11
- package/src/scenarios/prompt-read-workspace-membership-enforced.test.ts +43 -34
- package/src/scenarios/provider-usage.test.ts +2 -2
- package/src/scenarios/redaction.test.ts +7 -5
- package/src/scenarios/redactionAdversarial.test.ts +5 -4
- package/src/scenarios/registry-public.test.ts +5 -1
- package/src/scenarios/replay-divergence-at-refusal.test.ts +10 -7
- package/src/scenarios/replay-fanout-suppression.test.ts +7 -4
- package/src/scenarios/replay-fork-arbitrary.test.ts +2 -2
- package/src/scenarios/replay-side-effect-suppression.test.ts +2 -2
- package/src/scenarios/replayDeterminism.test.ts +9 -4
- package/src/scenarios/runner-ledger.test.ts +4 -0
- package/src/scenarios/runtime-capabilities.test.ts +2 -0
- package/src/scenarios/speech-synthesis-roundtrip.test.ts +1 -1
- package/src/scenarios/streamReconnect.test.ts +2 -2
- package/src/scenarios/subrun-approval-gate.test.ts +2 -2
- package/src/scenarios/subworkflow-input-mapping.test.ts +2 -2
- package/src/scenarios/tool-catalog-projection.test.ts +10 -7
- package/src/scenarios/tool-session-lifecycle.test.ts +1 -1
- package/src/scenarios/trigger-ingestion.test.ts +2 -2
- package/src/scenarios/v2-a2ui-v09-surface.test.ts +15 -0
- package/src/scenarios/v2-client-version-header.test.ts +124 -0
- package/src/scenarios/v2-effect-identity-business-key.test.ts +12 -0
- package/src/scenarios/v2-ext-family-claims.test.ts +62 -0
- package/src/scenarios/v2-ext-rest-transport.test.ts +151 -0
- package/src/scenarios/v2-id-grammar.test.ts +17 -5
- package/src/scenarios/v2-idempotency-in-flight.test.ts +3 -0
- package/src/scenarios/v2-legacy-profiles-absent.test.ts +4 -6
- package/src/scenarios/v2-mcp-tasks.test.ts +1 -1
- package/src/scenarios/v2-min-client-version.test.ts +20 -14
- package/src/scenarios/v2-preferred-version-default.test.ts +19 -1
- package/src/scenarios/v2-refresh-sla.test.ts +6 -5
- package/src/scenarios/v2-sse-last-event-id-cursor.test.ts +3 -3
- package/src/scenarios/v2-sse-last-event-id.test.ts +2 -0
- package/src/scenarios/v2-version-header-honored.test.ts +3 -0
- package/src/scenarios/v2-webhook-durable-delivery.test.ts +2 -0
- package/src/scenarios/v2-webhook-generated-secret.test.ts +76 -0
- package/src/scenarios/v2-webhook-secret-rotation.test.ts +8 -1
- package/src/scenarios/verifier-gating.test.ts +2 -2
- package/src/scenarios/version-fold.test.ts +2 -2
- package/src/scenarios/wasm-pack-invoke-suspended.test.ts +7 -6
- package/src/scenarios/webhook-signed-delivery.test.ts +2 -0
- package/src/scenarios/workflow-chain-host-expansion.test.ts +10 -1
- package/src/scenarios/workflow-variable-format.test.ts +2 -2
- package/src/scenarios/workload-identity-chain-bounds.test.ts +6 -3
- package/vectors/audit-checkpoint-v1.json +260 -0
|
@@ -48,7 +48,7 @@ import { SCHEMAS_DIR } from '../lib/paths.js';
|
|
|
48
48
|
import { driver } from '../lib/driver.js';
|
|
49
49
|
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
50
50
|
import { req } from '../lib/requirement-ids.js';
|
|
51
|
-
import { softSkip } from '../lib/soft-skip.js';
|
|
51
|
+
import { blockedDespiteAssertions, softSkip } from '../lib/soft-skip.js';
|
|
52
52
|
|
|
53
53
|
const ajv = new Ajv2020({ strict: false, allErrors: true });
|
|
54
54
|
|
|
@@ -518,28 +518,33 @@ describe.skipIf(HTTP_SKIP)('a2ui-surface-delta-transport: live host delta transp
|
|
|
518
518
|
const surfaceB = fullSurfaceMaterializedAfterUpdates();
|
|
519
519
|
|
|
520
520
|
// Baseline (surface A) through the host's REAL surface-emit path.
|
|
521
|
+
// The two emits and the JSON event read are SETUP for this leg, and each is
|
|
522
|
+
// an early return rather than an assertion, so that a host on which the
|
|
523
|
+
// agreement is unobservable records that honestly instead of a
|
|
524
|
+
// partial-witness PASS (until 2026-09-27 the emit statuses were asserted
|
|
525
|
+
// first). JSON content negotiation on the events endpoint is OPTIONAL
|
|
526
|
+
// (stream-modes.md §"Content negotiation") and delivering a delta frame is
|
|
527
|
+
// MAY even under the advertisement (§"A2UI delta transport"), so an
|
|
528
|
+
// SSE-only host, or one that sends no frame, is conformant and out of reach
|
|
529
|
+
// of this leg: `inapplicable`, never `blocked`.
|
|
521
530
|
const emitA = await driver.post('/v1/host/sample/a2ui/emit-surface', { runId, surface: surfaceA });
|
|
522
531
|
if (emitA.status === 404 || emitA.status === 405) return softSkip('blocked', 'precondition not met — `emitA.status === 404 || emitA.status === 405` returned early (seam absent — soft-skip the live leg) (seam, prior step, or fixture unavailable)'); // seam absent — soft-skip the live leg
|
|
523
|
-
|
|
524
|
-
emitA.status,
|
|
525
|
-
req('openwop.it.a2ui-surface-delta-transport.drives-the-emit-surface-seam-a-a2uidelta-1-subscriber-reconstruction-equals-the', 'RFC 0114 §15', 'the emit-surface seam MUST record the baseline full surface'),
|
|
526
|
-
).toBeLessThan(300);
|
|
532
|
+
if (emitA.status >= 300) return softSkip('blocked', `the emit-surface seam refused the baseline full surface (${emitA.status}) — it MUST record it (RFC 0114 §15), and nothing was transported`);
|
|
527
533
|
|
|
528
534
|
// Second full surface (surface B) — recorded full AND transported as a delta
|
|
529
535
|
// to any ?a2uiDelta=1 subscriber.
|
|
530
536
|
const emitB = await driver.post('/v1/host/sample/a2ui/emit-surface', { runId, surface: surfaceB });
|
|
531
|
-
if (emitB.status
|
|
532
|
-
expect(emitB.status, req('openwop.it.a2ui-surface-delta-transport.drives-the-emit-surface-seam-a-a2uidelta-1-subscriber-reconstruction-equals-the', 'RFC 0114 §15', 'the second emit MUST succeed')).toBeLessThan(300);
|
|
537
|
+
if (emitB.status >= 300) return softSkip('blocked', `the emit-surface seam accepted the baseline surface, then answered ${emitB.status} for the second — no update was transported`);
|
|
533
538
|
const refB = (emitB.json as EmitSurfaceResponse)?.surfaceRef;
|
|
534
539
|
|
|
535
540
|
// ?a2uiDelta=1 subscriber: locate the delta frame the host transported.
|
|
536
541
|
const deltaEvents = await getEvents(runId, true);
|
|
537
|
-
if (deltaEvents === null) return softSkip('
|
|
542
|
+
if (deltaEvents === null) return softSkip('inapplicable', 'GET /v1/runs/{runId}/events?a2uiDelta=1 with Accept: application/json did not answer a JSON 200 — the host serves the events endpoint as SSE only, which is conformant (stream-modes.md §"Content negotiation"), and this leg reads JSON');
|
|
538
543
|
const frames = collectMatching(deltaEvents, validateFrame)
|
|
539
544
|
.map(readFrame)
|
|
540
545
|
.filter((f): f is { surfaceRef: string; catalogVersion: string; patch: PatchOp[] } => f !== null)
|
|
541
546
|
.filter((f) => refB === undefined || f.surfaceRef === refB);
|
|
542
|
-
if (frames.length === 0) return softSkip('
|
|
547
|
+
if (frames.length === 0) return softSkip('inapplicable', 'the ?a2uiDelta=1 JSON read carried no delta frame for the update — delivering one is MAY (stream-modes.md §"A2UI delta transport"), so there is nothing to reconstruct');
|
|
543
548
|
const frame = frames[frames.length - 1];
|
|
544
549
|
|
|
545
550
|
// catalogVersion on the delta MUST equal the baseline full surface's.
|
|
@@ -557,9 +562,13 @@ describe.skipIf(HTTP_SKIP)('a2ui-surface-delta-transport: live host delta transp
|
|
|
557
562
|
|
|
558
563
|
// Non-negotiating subscriber: the host materializes the FULL surface for the same update.
|
|
559
564
|
const fullEvents = await getEvents(runId, false);
|
|
560
|
-
|
|
565
|
+
// The same endpoint just answered JSON for the ?a2uiDelta=1 read, so the host
|
|
566
|
+
// negotiates JSON; failing the plain read leaves the agreement unobserved.
|
|
567
|
+
if (fullEvents === null) return blockedDespiteAssertions('the non-negotiating GET /v1/runs/{runId}/events did not answer 200 with JSON — the materialized full surface was never read');
|
|
561
568
|
const fulls = collectMatching(fullEvents, validateRecorded);
|
|
562
|
-
|
|
569
|
+
// ai-envelope.md §"Delta transport": without ?a2uiDelta=1 the host MUST
|
|
570
|
+
// deliver the materialized full surface, so a conforming host always has one here.
|
|
571
|
+
if (fulls.length === 0) return blockedDespiteAssertions('the non-negotiating event read carried no full ui.a2ui-surface — the delta reconstruction had nothing to be compared against');
|
|
563
572
|
const materializedFull = fulls[fulls.length - 1];
|
|
564
573
|
|
|
565
574
|
// delta and full agree — the core RFC 0114 transport guarantee, witnessed live.
|
|
@@ -590,7 +599,9 @@ describe.skipIf(HTTP_SKIP)('a2ui-surface-delta-transport: live host delta transp
|
|
|
590
599
|
surface: { components: [{ component: 'iframe', src: 'https://evil.example/x' }] },
|
|
591
600
|
};
|
|
592
601
|
const rejected = await driver.post('/v1/host/sample/a2ui/emit-surface', { runId, surface: outOfCatalog });
|
|
593
|
-
|
|
602
|
+
// The seam accepted the baseline (asserted above), so a 404/405 here is the
|
|
603
|
+
// seam vanishing, not absent: the fail-closed rejection was never observed.
|
|
604
|
+
if (rejected.status === 404 || rejected.status === 405) return blockedDespiteAssertions(`the emit-surface seam accepted the baseline surface, then answered ${rejected.status} for the out-of-catalog one — the fail-closed rejection was never observed`);
|
|
594
605
|
expect(
|
|
595
606
|
rejected.status,
|
|
596
607
|
req('openwop.it.a2ui-surface-delta-transport.the-emit-surface-seam-rejects-an-out-of-catalog-surface-fail-closed-real-catalog',
|
|
@@ -153,8 +153,9 @@ describe.skipIf(HTTP_SKIP)('agent-channel-dispatch (RFC 0082 §B): production ru
|
|
|
153
153
|
{ fromSeq: 0, mode: 'replay' },
|
|
154
154
|
);
|
|
155
155
|
if (forkDeclined(fork1.status, 'channel-dispatch replay fork 1')) {
|
|
156
|
-
ctx.skip()
|
|
157
|
-
|
|
156
|
+
// forkDeclined() already noted `blocked` with the status; ctx.skip()
|
|
157
|
+
// throws, so the row is `blocked` — nothing after it runs.
|
|
158
|
+
return ctx.skip();
|
|
158
159
|
}
|
|
159
160
|
expect(
|
|
160
161
|
fork1.status,
|
|
@@ -198,6 +199,10 @@ describe.skipIf(HTTP_SKIP)('agent-channel-dispatch (RFC 0082 §B): production ru
|
|
|
198
199
|
// evidence; the cross-move proof needs the seam. Honest skip of Leg 3.
|
|
199
200
|
// eslint-disable-next-line no-console
|
|
200
201
|
console.warn('[agent-channel-dispatch] deployment seam absent — skipping the channel-move non-re-resolution leg (Leg 3)');
|
|
202
|
+
// partial-witness-ok: Legs 1+2 observed the §B pin on a real run and the
|
|
203
|
+
// replay re-reading it; the deployment-transition seam is OPTIONAL
|
|
204
|
+
// (conformance-only), so a conforming host without it cannot show the
|
|
205
|
+
// channel-move half.
|
|
201
206
|
return softSkip('blocked', 'precondition not met — `moved === null` returned early ([agent-channel-dispatch] deployment seam absent — skipping the channel-move non-re-resolution leg (Leg 3)) (seam, prior step, or fixture unavailable)');
|
|
202
207
|
}
|
|
203
208
|
// Confirm the move is OBSERVABLE: a fresh channel-bound run must now resolve
|
|
@@ -209,6 +214,9 @@ describe.skipIf(HTTP_SKIP)('agent-channel-dispatch (RFC 0082 §B): production ru
|
|
|
209
214
|
if (typeof movedVersion !== 'string' || movedVersion === pinnedVersion) {
|
|
210
215
|
// eslint-disable-next-line no-console
|
|
211
216
|
console.warn('[agent-channel-dispatch] channel did not observably move — skipping Leg 3 strict assertion');
|
|
217
|
+
// partial-witness-ok: Legs 1+2 observed the §B pin and its replay
|
|
218
|
+
// re-read; a promote that does not move the head (canary split, no-op)
|
|
219
|
+
// is conforming, and without a moved head non-re-resolution has no contrast.
|
|
212
220
|
return softSkip('blocked', 'precondition not met — `typeof movedVersion !== \'string\' || movedVersion === pinnedVersion` returned early ([agent-channel-dispatch] channel did not observably move — skipping Leg 3 strict assertion) (seam, prior step…');
|
|
213
221
|
}
|
|
214
222
|
const fork2 = await driver.post(
|
|
@@ -216,8 +224,9 @@ describe.skipIf(HTTP_SKIP)('agent-channel-dispatch (RFC 0082 §B): production ru
|
|
|
216
224
|
{ fromSeq: 0, mode: 'replay' },
|
|
217
225
|
);
|
|
218
226
|
if (forkDeclined(fork2.status, 'channel-dispatch replay fork 2')) {
|
|
219
|
-
ctx.skip()
|
|
220
|
-
|
|
227
|
+
// forkDeclined() already noted `blocked` with the status; ctx.skip()
|
|
228
|
+
// throws, so the row is `blocked` — nothing after it runs.
|
|
229
|
+
return ctx.skip();
|
|
221
230
|
}
|
|
222
231
|
expect(
|
|
223
232
|
fork2.status,
|
|
@@ -50,7 +50,7 @@ describe('agent-live-invocation-bracket (RFC 0077 §E)', () => {
|
|
|
50
50
|
started.length >= 1 && completed.length >= 1,
|
|
51
51
|
req('openwop.it.agent-live-invocation-bracket.brackets-a-live-invocation-with-started-first-completed-last-matching-invocation', 'multi-agent-execution.md §"Live manifest dispatch"', 'a live invocation MUST emit agent.invocation.started + agent.invocation.completed'),
|
|
52
52
|
).toBe(true);
|
|
53
|
-
if (started.length === 0 || completed.length === 0)
|
|
53
|
+
if (started.length === 0 || completed.length === 0) throw new Error('unreachable: the assertion above has already failed this leg');
|
|
54
54
|
|
|
55
55
|
const start = started[0]!;
|
|
56
56
|
const end = completed[completed.length - 1]!;
|
|
@@ -61,6 +61,9 @@ describe('agent-manifest-runtime (RFC 0070)', () => {
|
|
|
61
61
|
// Empty is conformant; the cross-tenant no-disclosure 404 is covered by the
|
|
62
62
|
// owner-triple isolation harness (RFC 0048/0059), not re-probed here. Nothing
|
|
63
63
|
// to dispatch.
|
|
64
|
+
// partial-witness-ok: installScope, the served inventory and its array
|
|
65
|
+
// shape were observed; an empty tenant-scoped inventory is conformant,
|
|
66
|
+
// so the dispatch half has nothing to dispatch on this host.
|
|
64
67
|
return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `agents.length === 0` returned early (RFC 0074 §A + Unresolved Q3 — tenant-scoped: GET /v1/agents is the authenticated principal\'s workspace set, which MAY be e…');
|
|
65
68
|
}
|
|
66
69
|
|
|
@@ -168,7 +168,7 @@ describe('aiEnvelope.capBreached: behavioral cap enforcement (FINAL v1.1)', () =
|
|
|
168
168
|
import { queryTestEvents, isEventLogSeamAvailable, resetTestSeam } from '../lib/event-log-query.js';
|
|
169
169
|
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
170
170
|
import { req } from '../lib/requirement-ids.js';
|
|
171
|
-
import { softSkip } from '../lib/soft-skip.js';
|
|
171
|
+
import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
|
|
172
172
|
|
|
173
173
|
describe('aiEnvelope.capBreached: engine projection via event-log seam (capabilities.md §"cap.breached")', () => {
|
|
174
174
|
it('breached outcome projects to cap.breached { kind: "envelopes" } event with causationId chain', async () => {
|
|
@@ -193,7 +193,7 @@ describe('aiEnvelope.capBreached: engine projection via event-log seam (capabili
|
|
|
193
193
|
expect(r.body.status).toBe('breached');
|
|
194
194
|
|
|
195
195
|
const events = await queryTestEvents(runId, { type: 'cap.breached' });
|
|
196
|
-
if (!events.ok) return
|
|
196
|
+
if (!events.ok) return blockedDespiteAssertions('the event-log seam is advertised but the cap.breached query failed — the projection is unobserved');
|
|
197
197
|
expect(
|
|
198
198
|
events.events.length,
|
|
199
199
|
req('openwop.it.aiEnvelope.capBreached.breached-outcome-projects-to-cap-breached-kind-envelopes-event-with-causationid', 'capabilities.md §"Engine-enforced limits and the cap.breached event"', 'breached outcome MUST project to exactly one cap.breached event'),
|
|
@@ -143,7 +143,7 @@ describe('aiEnvelope.contractRefusal: behavioral accept-gate (FINAL v1.1)', () =
|
|
|
143
143
|
// E.1 engine-projection via the test-only event-log seam.
|
|
144
144
|
import { queryTestEvents, isEventLogSeamAvailable, resetTestSeam } from '../lib/event-log-query.js';
|
|
145
145
|
import { req } from '../lib/requirement-ids.js';
|
|
146
|
-
import { softSkip } from '../lib/soft-skip.js';
|
|
146
|
+
import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
|
|
147
147
|
|
|
148
148
|
describe('aiEnvelope.contractRefusal: engine projection via event-log seam', () => {
|
|
149
149
|
it('gated (fail-node) → node.failed { error.code: "envelope_contract_violation" }', async () => {
|
|
@@ -167,7 +167,7 @@ describe('aiEnvelope.contractRefusal: engine projection via event-log seam', ()
|
|
|
167
167
|
if (r.status === 404) return softSkip('blocked', 'precondition not met — `r.status === 404` returned early (seam, prior step, or fixture unavailable)');
|
|
168
168
|
expect(r.body.status).toBe('gated');
|
|
169
169
|
const events = await queryTestEvents(runId, { type: 'node.failed' });
|
|
170
|
-
if (!events.ok || events.events.length === 0) return
|
|
170
|
+
if (!events.ok || events.events.length === 0) return blockedDespiteAssertions('the gated outcome was accepted but no node.failed was read back from the event-log seam — the envelope_contract_violation projection is unobserved');
|
|
171
171
|
const err = events.events[0]!.payload.error as { code?: string; details?: { refusedType?: string; acceptedTypes?: string[] } };
|
|
172
172
|
expect(
|
|
173
173
|
err.code,
|
|
@@ -191,7 +191,7 @@ import { queryTestSpans, isOtelSeamAvailable } from '../lib/otel-scrape.js';
|
|
|
191
191
|
import { resetTestSeam } from '../lib/event-log-query.js';
|
|
192
192
|
import { capabilityFamily, discoveryFamilies } from '../lib/discovery-capabilities.js';
|
|
193
193
|
import { req } from '../lib/requirement-ids.js';
|
|
194
|
-
import { softSkip } from '../lib/soft-skip.js';
|
|
194
|
+
import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
|
|
195
195
|
|
|
196
196
|
describe('aiEnvelope.schemaDrift: OTel drift attribute projection (E.2)', () => {
|
|
197
197
|
it('below-floor + strictness:warn → OTel span MUST carry envelope_schema_version_drift attribute', async () => {
|
|
@@ -216,7 +216,7 @@ describe('aiEnvelope.schemaDrift: OTel drift attribute projection (E.2)', () =>
|
|
|
216
216
|
expect(r.body.status).toBe('accepted');
|
|
217
217
|
|
|
218
218
|
const spans = await queryTestSpans({ runId });
|
|
219
|
-
if (!spans.ok) return
|
|
219
|
+
if (!spans.ok) return blockedDespiteAssertions('the OTel span seam is advertised but the span query failed — envelope_schema_version_drift is unobserved');
|
|
220
220
|
expect(
|
|
221
221
|
spans.data.some((s) => s.attributes.envelope_schema_version_drift === true),
|
|
222
222
|
req('openwop.it.aiEnvelope.schemaDrift.below-floor-strictness-warn-otel-span-must-carry-envelope-schema-version-drift-a',
|
|
@@ -168,7 +168,7 @@ describe('aiEnvelope.universalKinds: behavioral accept via /v1/host/sample/envel
|
|
|
168
168
|
import { queryTestEvents, isEventLogSeamAvailable, resetTestSeam } from '../lib/event-log-query.js';
|
|
169
169
|
import { capabilityFamily, discoveryFamilies } from '../lib/discovery-capabilities.js';
|
|
170
170
|
import { req } from '../lib/requirement-ids.js';
|
|
171
|
-
import { softSkip } from '../lib/soft-skip.js';
|
|
171
|
+
import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
|
|
172
172
|
|
|
173
173
|
describe('aiEnvelope.universalKinds: engine projection via event-log seam', () => {
|
|
174
174
|
it('clarification.request MUST be lifted to interrupt.requested { kind: "clarification" } per interrupt.md', async () => {
|
|
@@ -188,7 +188,7 @@ describe('aiEnvelope.universalKinds: engine projection via event-log seam', () =
|
|
|
188
188
|
if (r.status === 404) return softSkip('blocked', 'precondition not met — `r.status === 404` returned early (seam, prior step, or fixture unavailable)');
|
|
189
189
|
expect(r.body.status).toBe('accepted');
|
|
190
190
|
const events = await queryTestEvents(runId, { type: 'interrupt.requested' });
|
|
191
|
-
if (!events.ok) return
|
|
191
|
+
if (!events.ok) return blockedDespiteAssertions('the event-log seam is advertised but the interrupt.requested query failed — the clarification lift is unobserved');
|
|
192
192
|
expect(
|
|
193
193
|
events.events.length,
|
|
194
194
|
req('openwop.it.aiEnvelope.universalKinds.clarification-request-must-be-lifted-to-interrupt-requested-kind-clarification-p', 'ai-envelope.md §"Universal kinds"', 'accepted clarification.request MUST project to interrupt.requested per interrupt.md'),
|
|
@@ -40,7 +40,7 @@ import { driver } from '../lib/driver.js';
|
|
|
40
40
|
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
41
41
|
import { readCapabilityFamily } from '../lib/discovery-capabilities.js';
|
|
42
42
|
import { req } from '../lib/requirement-ids.js';
|
|
43
|
-
import { softSkip } from '../lib/soft-skip.js';
|
|
43
|
+
import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
|
|
44
44
|
|
|
45
45
|
const SEAM = '/v1/host/sample/ai/call';
|
|
46
46
|
|
|
@@ -118,7 +118,7 @@ describe('aiproviders-selfhosted-honesty (RFC 0108 §A.2/§D)', () => {
|
|
|
118
118
|
'[openwop-selfhosted-providers] §D disclosure check skipped — set OPENWOP_TEST_COMPAT_ENDPOINT ' +
|
|
119
119
|
"to the host's configured compat endpoint to assert non-disclosure on the wire.",
|
|
120
120
|
);
|
|
121
|
-
return
|
|
121
|
+
return blockedDespiteAssertions('§D non-disclosure is unobserved: OPENWOP_TEST_COMPAT_ENDPOINT is unset, so there is no configured endpoint to search the response for (§A.2 was observed)');
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
const serialized = JSON.stringify(res.json ?? {});
|
|
@@ -19,7 +19,7 @@ import { driver } from '../lib/driver.js';
|
|
|
19
19
|
import { pollUntilTerminal } from '../lib/polling.js';
|
|
20
20
|
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
21
21
|
import { req } from '../lib/requirement-ids.js';
|
|
22
|
-
import { softSkip } from '../lib/soft-skip.js';
|
|
22
|
+
import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
|
|
23
23
|
|
|
24
24
|
const CANDIDATES = ['conformance-append-ordering', 'conformance-multi-node'] as const;
|
|
25
25
|
const FIXTURE = CANDIDATES.find((id) => isFixtureAdvertised(id)) ?? null;
|
|
@@ -49,31 +49,36 @@ interface RunEvent {
|
|
|
49
49
|
describe.skipIf(SKIP)('append-ordering: folded channel reflects event sequence', () => {
|
|
50
50
|
it('append-reducer channels project entries in event-sequence order', async () => {
|
|
51
51
|
const create = await driver.post('/v1/runs', { workflowId: FIXTURE! });
|
|
52
|
-
|
|
53
|
-
const runId = (create.json as { runId: string }).runId;
|
|
54
|
-
|
|
55
|
-
await pollUntilTerminal(runId, { timeoutMs: 30_000 });
|
|
56
|
-
|
|
57
|
-
const events = await driver.get(`/v1/runs/${encodeURIComponent(runId)}/events`);
|
|
58
|
-
const list = (events.json as { events?: RunEvent[] }).events ?? [];
|
|
52
|
+
const runId = (create.json as { runId?: string } | undefined)?.runId;
|
|
59
53
|
|
|
60
54
|
// Group channel.written events by channel.
|
|
61
55
|
const byChannel = new Map<string, RunEvent[]>();
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
56
|
+
if (create.status === 201 && typeof runId === 'string') {
|
|
57
|
+
await pollUntilTerminal(runId, { timeoutMs: 30_000 });
|
|
58
|
+
const events = await driver.get(`/v1/runs/${encodeURIComponent(runId)}/events`);
|
|
59
|
+
const list = (events.json as { events?: RunEvent[] }).events ?? [];
|
|
60
|
+
for (const e of list) {
|
|
61
|
+
if (e.type !== 'channel.written') continue;
|
|
62
|
+
const ch = e.payload?.channel;
|
|
63
|
+
if (typeof ch !== 'string') continue;
|
|
64
|
+
const arr = byChannel.get(ch) ?? [];
|
|
65
|
+
arr.push(e);
|
|
66
|
+
byChannel.set(ch, arr);
|
|
67
|
+
}
|
|
68
|
+
// The conformance-multi-node fallback is not required to write an append
|
|
69
|
+
// channel, so when it wrote none the ordering rule has nothing to bind on this
|
|
70
|
+
// host. Decided before any assertion so the row is `inapplicable`, not a
|
|
71
|
+
// partial-witness pass on the run-created assertion.
|
|
72
|
+
if (byChannel.size === 0 && FIXTURE !== 'conformance-append-ordering') {
|
|
73
|
+
return softSkip('inapplicable', 'the conformance-multi-node fallback fixture wrote no append channel and the host advertises no conformance-append-ordering fixture');
|
|
74
|
+
}
|
|
69
75
|
}
|
|
76
|
+
expect(create.status).toBe(201);
|
|
70
77
|
|
|
71
78
|
if (byChannel.size === 0) {
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
);
|
|
76
|
-
return softSkip('blocked', 'precondition not met — `byChannel.size === 0` returned early ([append-ordering] fixture emitted no channel.written events; skipping ordering assertions) (seam, prior step, or fixture unavailable)');
|
|
79
|
+
// The dedicated fixture exists to write an append channel, so a run of it with
|
|
80
|
+
// no channel.written leaves the ordering rule unobserved: blocked, not passed.
|
|
81
|
+
return blockedDespiteAssertions('conformance-append-ordering ran but emitted no channel.written event — append ordering is unobserved');
|
|
77
82
|
}
|
|
78
83
|
|
|
79
84
|
// For each channel, sequence MUST be strictly increasing within the run.
|
|
@@ -122,7 +127,7 @@ describe.skipIf(SKIP)('append-ordering: folded channel reflects event sequence',
|
|
|
122
127
|
|
|
123
128
|
// Cross-check against the projected channel state on the run snapshot
|
|
124
129
|
// (when surfaced) — projected array length MUST equal the number of writes.
|
|
125
|
-
const snapshot = await driver.get(`/v1/runs/${encodeURIComponent(runId)}`);
|
|
130
|
+
const snapshot = await driver.get(`/v1/runs/${encodeURIComponent(runId!)}`);
|
|
126
131
|
const channels = (snapshot.json as { channels?: Record<string, unknown> }).channels ?? {};
|
|
127
132
|
for (const [channel, writes] of byChannel) {
|
|
128
133
|
const projected = (channels as Record<string, unknown>)[channel];
|
|
@@ -47,7 +47,9 @@ describe('artifact-auth: unauthenticated artifact requests are rejected', () =>
|
|
|
47
47
|
} catch {
|
|
48
48
|
// Some hosts return a bare 401 with no body — acceptable. Skip the
|
|
49
49
|
// envelope-shape assertion in that case.
|
|
50
|
-
|
|
50
|
+
// partial-witness-ok: the 401 refusal, the MUST, was asserted above; the
|
|
51
|
+
// `error: "unauthenticated"` envelope is a SHOULD and a bare 401 has no body.
|
|
52
|
+
return softSkip('inapplicable', 'the 401 carried no JSON body (acceptable), so the SHOULD envelope shape was not checked');
|
|
51
53
|
}
|
|
52
54
|
if (typeof body.error === 'string') {
|
|
53
55
|
expect(body.error, req('openwop.it.artifact-auth.get-v1-runs-runid-artifacts-artifactid-without-authorization-returns-401',
|
|
@@ -37,7 +37,7 @@ import { driver } from '../lib/driver.js';
|
|
|
37
37
|
import { behaviorGatePresent } from '../lib/behavior-gate.js';
|
|
38
38
|
import { readArtifactTypesCap } from '../lib/artifactTypes.js';
|
|
39
39
|
import { req } from '../lib/requirement-ids.js';
|
|
40
|
-
import { softSkip } from '../lib/soft-skip.js';
|
|
40
|
+
import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
|
|
41
41
|
const CAPS = join(SCHEMAS_DIR, 'capabilities.schema.json');
|
|
42
42
|
const EVENT_PAYLOADS = join(SCHEMAS_DIR, 'run-event-payloads.schema.json');
|
|
43
43
|
const readDoc = (name: string): string => (V1_DIR ? readFileSync(join(V1_DIR, name), 'utf8') : '');
|
|
@@ -206,7 +206,7 @@ describe('artifact-type-registration-source: the advert agrees with the event (R
|
|
|
206
206
|
const created = list.filter((e) => e['type'] === 'artifact.created');
|
|
207
207
|
// Emission itself is RFC 0142's MUST, reported by its own leg. Reaching here without an
|
|
208
208
|
// event means that leg is already red; don't restate its finding as a 0145 failure.
|
|
209
|
-
if (created.length === 0) return
|
|
209
|
+
if (created.length === 0) return blockedDespiteAssertions('the run emitted no artifact.created event, so registrationSource is unobserved (RFC 0142\'s emission leg reports the missing event)');
|
|
210
210
|
|
|
211
211
|
const payload = (created[0]?.['payload'] ?? created[0]?.['data'] ?? {}) as Record<string, unknown>;
|
|
212
212
|
expect(
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 0218 — a host's audit checkpoints are signed over their Merkle root
|
|
3
|
+
* (`spec/v1/auth-profiles.md` §"Audit-log integrity" 3).
|
|
4
|
+
*
|
|
5
|
+
* Gated on the `openwop-audit-log-integrity` profile. Every checkpoint
|
|
6
|
+
* `GET /v1/audit/verify` returns MUST verify under the advertised
|
|
7
|
+
* `checkpointPublicKey` as Ed25519 over its hex-decoded `merkleRoot`. The root
|
|
8
|
+
* itself cannot be recomputed from outside (the entries are not on the wire),
|
|
9
|
+
* so the black box witnesses the signature half only. The root half belongs to
|
|
10
|
+
* the vectors (`audit-checkpoint-vectors.test.ts`), the host-internal tamper
|
|
11
|
+
* test and the out-of-band verifier. A host with no checkpoint yet records
|
|
12
|
+
* `inapplicable` before any assertion. Sabotage: a host that signs the
|
|
13
|
+
* checkpoint's canonical JSON, or the 64 hex characters, fails the row.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, it, expect } from 'vitest';
|
|
17
|
+
import { driver } from '../lib/driver.js';
|
|
18
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
19
|
+
import { req } from '../lib/requirement-ids.js';
|
|
20
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
21
|
+
import { checkpointPublicKey, verifyCheckpointSignature } from '../lib/audit-checkpoint.js';
|
|
22
|
+
|
|
23
|
+
const SPEC = 'RFC 0218 · spec/v1/auth-profiles.md §"Audit-log integrity" 3';
|
|
24
|
+
const SIGNED = 'openwop.requirement.0218.checkpoint-signature-over-root';
|
|
25
|
+
|
|
26
|
+
interface AuditIntegrityCaps { checkpointPublicKey?: string }
|
|
27
|
+
interface AuthCaps { profiles?: string[]; auditLogIntegrity?: AuditIntegrityCaps }
|
|
28
|
+
|
|
29
|
+
describe('RFC 0218 — a host\'s checkpoints are signed over their root', () => {
|
|
30
|
+
it('every checkpoint GET /v1/audit/verify returns verifies under the advertised key', async () => {
|
|
31
|
+
const disco = await driver.get('/.well-known/openwop');
|
|
32
|
+
const auth = capabilityFamily<AuthCaps>(disco.json, 'auth') ?? {};
|
|
33
|
+
if (!(Array.isArray(auth.profiles) && auth.profiles.includes('openwop-audit-log-integrity'))) {
|
|
34
|
+
return softSkip('inapplicable', 'the host does not claim openwop-audit-log-integrity');
|
|
35
|
+
}
|
|
36
|
+
const advertised = auth.auditLogIntegrity?.checkpointPublicKey;
|
|
37
|
+
if (typeof advertised !== 'string' || advertised.length === 0) {
|
|
38
|
+
// A claimed profile without its key is the profile-shape leg's failure
|
|
39
|
+
// (audit-log-integrity.test.ts); this row cannot be observed without it.
|
|
40
|
+
return softSkip('blocked', 'the host claims the profile but advertises no auditLogIntegrity.checkpointPublicKey');
|
|
41
|
+
}
|
|
42
|
+
let key;
|
|
43
|
+
try { key = checkpointPublicKey(advertised); } catch (e) {
|
|
44
|
+
return softSkip('blocked', `auditLogIntegrity.checkpointPublicKey is not an Ed25519 SPKI key: ${(e as Error).message}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const verify = await driver.get('/v1/audit/verify?fromSeq=0&toSeq=1000000');
|
|
48
|
+
if (verify.status !== 200) {
|
|
49
|
+
return softSkip('blocked', `GET /v1/audit/verify answered ${verify.status}; the verify leg of audit-log-integrity owns that contract`);
|
|
50
|
+
}
|
|
51
|
+
const checkpoints = ((verify.json as { checkpoints?: Array<{ atSequence?: number; merkleRoot?: string; signature?: string }> }).checkpoints) ?? [];
|
|
52
|
+
if (checkpoints.length === 0) {
|
|
53
|
+
return softSkip('inapplicable', 'the host has minted no checkpoint yet (its interval has not elapsed)');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const cp of checkpoints) {
|
|
57
|
+
expect(
|
|
58
|
+
verifyCheckpointSignature(String(cp.merkleRoot ?? ''), String(cp.signature ?? ''), key),
|
|
59
|
+
req(SIGNED, SPEC, `checkpoint at ${cp.atSequence}: signature MUST be Ed25519 over the 32 bytes merkleRoot hex-decodes to, under the advertised checkpointPublicKey`),
|
|
60
|
+
).toBe(true);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 0218 — the audit checkpoint preimage, server-free
|
|
3
|
+
* (`spec/v1/auth-profiles.md` §"Audit-log integrity" 3).
|
|
4
|
+
*
|
|
5
|
+
* `conformance/vectors/audit-checkpoint-v1.json` is normative. Every leg
|
|
6
|
+
* RECOMPUTES from the vector input through `lib/audit-checkpoint.ts` and compares
|
|
7
|
+
* with the committed value; none compares two constants from the file. The
|
|
8
|
+
* vectors were produced by an implementation independent of that lib and
|
|
9
|
+
* cross-checked in a second language. Sabotages, each of which turns a leg red:
|
|
10
|
+
* duplicating a last odd node (the 12-sequence checkpoint has 5 leaves); hashing
|
|
11
|
+
* the concatenated DECODED bytes instead of the hex text; ranging leaves over
|
|
12
|
+
* `0..atSequence` instead of `(P, atSequence]`; signing the checkpoint's
|
|
13
|
+
* canonical JSON or the hex text instead of the 32 root bytes; dropping the
|
|
14
|
+
* leaf-count check (the interior-nodes-as-leaves refusal is then accepted).
|
|
15
|
+
*
|
|
16
|
+
* Server-free and always-on; the vectors ship with the conformance package. The
|
|
17
|
+
* black-box half is `audit-checkpoint-signature.test.ts`.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { describe, it, expect } from 'vitest';
|
|
21
|
+
import { readFileSync } from 'node:fs';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
import { dirname, resolve } from 'node:path';
|
|
24
|
+
import { req } from '../lib/requirement-ids.js';
|
|
25
|
+
import {
|
|
26
|
+
auditEntryHash,
|
|
27
|
+
checkpointMerkleRoot,
|
|
28
|
+
checkpointPublicKey,
|
|
29
|
+
verifyCheckpointSignature,
|
|
30
|
+
CheckpointPreimageError,
|
|
31
|
+
} from '../lib/audit-checkpoint.js';
|
|
32
|
+
|
|
33
|
+
interface VectorCheckpoint { atSequence: number; previousAtSequence: number; leafCount: number; merkleRoot: string; signature: string }
|
|
34
|
+
interface Refusal { id: string; why: string; leaves: string[]; expectedLeaves: number }
|
|
35
|
+
interface SigRefusal { id: string; why: string; merkleRoot: string; signature: string }
|
|
36
|
+
|
|
37
|
+
const VECTORS_PATH = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', 'vectors', 'audit-checkpoint-v1.json');
|
|
38
|
+
const doc = JSON.parse(readFileSync(VECTORS_PATH, 'utf8')) as {
|
|
39
|
+
publicKeySpkiBase64: string;
|
|
40
|
+
entries: Array<{ entry: { atSequence: number }; entryHash: string }>;
|
|
41
|
+
checkpoints: VectorCheckpoint[];
|
|
42
|
+
refusals: Refusal[];
|
|
43
|
+
signatureRefusals: SigRefusal[];
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const SPEC = 'RFC 0218 · spec/v1/auth-profiles.md §"Audit-log integrity" 3';
|
|
47
|
+
const VECTORS = 'openwop.requirement.0218.checkpoint-preimage-vectors';
|
|
48
|
+
|
|
49
|
+
describe('RFC 0218 — the audit checkpoint preimage (vectors)', () => {
|
|
50
|
+
it('the vector set is present and non-trivial', () => {
|
|
51
|
+
// An empty or truncated file would make every leg below vacuous.
|
|
52
|
+
expect(doc.entries.length, req(VECTORS, SPEC, 'the vector file carries entries')).toBeGreaterThanOrEqual(12);
|
|
53
|
+
expect(doc.checkpoints.map((c) => c.leafCount), req(VECTORS, SPEC, 'the checkpoints cover 1, 2, 4 and 5 leaves (odd promotion at 5)')).toEqual([1, 2, 4, 5]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('every entry hash is the SHA-256 of the entry\'s JCS bytes, and chains as prevHash', () => {
|
|
57
|
+
let prev: string | null = null;
|
|
58
|
+
for (const { entry, entryHash } of doc.entries) {
|
|
59
|
+
expect(auditEntryHash(entry), req(VECTORS, SPEC, `entry ${entry.atSequence}: entryHash = SHA-256(JCS(entry))`)).toBe(entryHash);
|
|
60
|
+
expect((entry as { prevHash?: string | null }).prevHash ?? null, req(VECTORS, SPEC, `entry ${entry.atSequence}: prevHash is the previous entry's hash`)).toBe(prev);
|
|
61
|
+
prev = entryHash;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('every root is recomputed over (P, atSequence], pairwise, odd node promoted', () => {
|
|
66
|
+
for (const c of doc.checkpoints) {
|
|
67
|
+
const leaves = doc.entries.filter((e) => e.entry.atSequence > c.previousAtSequence && e.entry.atSequence <= c.atSequence).map((e) => e.entryHash);
|
|
68
|
+
expect(checkpointMerkleRoot(leaves, c.atSequence - c.previousAtSequence), req(VECTORS, SPEC, `checkpoint at ${c.atSequence}: merkleRoot`)).toBe(c.merkleRoot);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('every signature verifies as Ed25519 over the 32 root bytes', () => {
|
|
73
|
+
const key = checkpointPublicKey(doc.publicKeySpkiBase64);
|
|
74
|
+
for (const c of doc.checkpoints) {
|
|
75
|
+
expect(verifyCheckpointSignature(c.merkleRoot, c.signature, key), req(VECTORS, SPEC, `checkpoint at ${c.atSequence}: signature over the root bytes`)).toBe(true);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('refuses a leaf list whose count is not atSequence − P, an uppercase leaf, and an empty range', () => {
|
|
80
|
+
for (const r of doc.refusals) {
|
|
81
|
+
let refused: unknown;
|
|
82
|
+
try { checkpointMerkleRoot(r.leaves, r.expectedLeaves); } catch (e) { refused = e; }
|
|
83
|
+
expect(refused instanceof CheckpointPreimageError, req(VECTORS, SPEC, `${r.id} MUST be refused — ${r.why}`)).toBe(true);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('a signature over any other preimage does not verify', () => {
|
|
88
|
+
const key = checkpointPublicKey(doc.publicKeySpkiBase64);
|
|
89
|
+
for (const s of doc.signatureRefusals) {
|
|
90
|
+
expect(verifyCheckpointSignature(s.merkleRoot, s.signature, key), req(VECTORS, SPEC, `${s.id} MUST NOT verify — ${s.why}`)).toBe(false);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -27,23 +27,14 @@ import { join } from 'node:path';
|
|
|
27
27
|
import { SCHEMAS_DIR } from '../lib/paths.js';
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
* audit-verify-result.schema.json
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
30
|
+
* audit-verify-result.schema.json, CLOSED: every `additionalProperties: false`
|
|
31
|
+
* is enforced. (Until 2026-09-27 the suite stripped them because the reference
|
|
32
|
+
* postgres/sqlite hosts emitted a per-checkpoint `verified` bit the schema does
|
|
33
|
+
* not name; the hosts now keep that bit host-internal. The aggregate verdict is
|
|
34
|
+
* the schema's OPTIONAL `checkpointsValid`.)
|
|
34
35
|
*/
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (node === null || typeof node !== 'object') return node;
|
|
38
|
-
const out: Record<string, unknown> = {};
|
|
39
|
-
for (const [k, v] of Object.entries(node as Record<string, unknown>)) {
|
|
40
|
-
if (k === 'additionalProperties' && v === false) continue;
|
|
41
|
-
out[k] = relaxAdditional(v);
|
|
42
|
-
}
|
|
43
|
-
return out;
|
|
44
|
-
}
|
|
45
|
-
const VERIFY_RESULT_SCHEMA = relaxAdditional(
|
|
46
|
-
JSON.parse(readFileSync(join(SCHEMAS_DIR, 'audit-verify-result.schema.json'), 'utf8')),
|
|
36
|
+
const VERIFY_RESULT_SCHEMA = JSON.parse(
|
|
37
|
+
readFileSync(join(SCHEMAS_DIR, 'audit-verify-result.schema.json'), 'utf8'),
|
|
47
38
|
) as Record<string, unknown>;
|
|
48
39
|
|
|
49
40
|
interface AuditIntegrityCaps {
|
|
@@ -266,12 +266,13 @@ describe.skipIf(!RUN_BEHAVIOR)('auth-mtls: client cert behavior', () => {
|
|
|
266
266
|
'auth-profiles.md §`openwop-auth-mtls`',
|
|
267
267
|
'mtls.required: true MUST reject no-cert requests (TLS handshake failure is conformant)',
|
|
268
268
|
)).toBe(true);
|
|
269
|
-
|
|
269
|
+
// The TLS-layer rejection IS the conformant refusal: the requirement was observed.
|
|
270
|
+
// (This branch used to end in a blocked note, a partial-witness pass at major 1.)
|
|
271
|
+
} else {
|
|
272
|
+
expect(res.status >= 400, req('openwop.it.auth-mtls.no-client-cert-against-mtls-required-true-non-2xx-or-tls-failure',
|
|
273
|
+
'auth-profiles.md §`openwop-auth-mtls`',
|
|
274
|
+
'mtls.required: true MUST reject no-cert requests at the auth layer (4xx) when not rejected at the TLS layer',
|
|
275
|
+
)).toBe(true);
|
|
270
276
|
}
|
|
271
|
-
|
|
272
|
-
expect(res.status >= 400, req('openwop.it.auth-mtls.no-client-cert-against-mtls-required-true-non-2xx-or-tls-failure',
|
|
273
|
-
'auth-profiles.md §`openwop-auth-mtls`',
|
|
274
|
-
'mtls.required: true MUST reject no-cert requests at the auth layer (4xx) when not rejected at the TLS layer',
|
|
275
|
-
)).toBe(true);
|
|
276
277
|
});
|
|
277
278
|
});
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
41
|
import { describe, it, expect } from 'vitest';
|
|
42
|
-
import { softSkip } from '../lib/soft-skip.js';
|
|
42
|
+
import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
|
|
43
43
|
import { driver } from '../lib/driver.js';
|
|
44
44
|
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
45
45
|
import { req } from '../lib/requirement-ids.js';
|
|
@@ -150,7 +150,7 @@ describe('auth-subject-link-key-class: same-IdP trust root (RFC 0163 §B — beh
|
|
|
150
150
|
expect(provSame.status, req('openwop.it.auth-subject-link-key-class.a-same-idp-link-forms-control-but-a-cross-idp-collision-must-not-link', 'auth-profiles.md §Subject linking', 'SCIM provisioning MUST succeed')).toBeLessThan(400);
|
|
151
151
|
|
|
152
152
|
const sameIdp = await driver.post('/v1/host/sample/auth/saml/validate', { idpUrl: idpAUrl, variant: 'valid', nameId: sameId });
|
|
153
|
-
if (sameIdp.status === 404) return
|
|
153
|
+
if (sameIdp.status === 404) return blockedDespiteAssertions('SAML validate seam answered 404 after SCIM provisioning succeeded — the same-IdP control and the cross-IdP MUST are unobserved');
|
|
154
154
|
expect(
|
|
155
155
|
(sameIdp.json as { authenticated?: boolean } | undefined)?.authenticated,
|
|
156
156
|
req('openwop.it.auth-subject-link-key-class.a-same-idp-link-forms-control-but-a-cross-idp-collision-must-not-link', 'auth-profiles.md §Subject linking', 'RFC 0163 §B positive control: a SAML assertion from the SAME IdP trust root as the SCIM lane links and authenticates'),
|
|
@@ -172,7 +172,7 @@ describe('auth-subject-link-key-class: same-IdP trust root (RFC 0163 §B — beh
|
|
|
172
172
|
expect(provCross.status, req('openwop.it.auth-subject-link-key-class.a-same-idp-link-forms-control-but-a-cross-idp-collision-must-not-link', 'auth-profiles.md §Subject linking', 'SCIM provisioning MUST succeed')).toBeLessThan(400);
|
|
173
173
|
|
|
174
174
|
const crossIdp = await driver.post('/v1/host/sample/auth/saml/validate', { idpUrl: idpBUrl, variant: 'valid', nameId: collideId });
|
|
175
|
-
if (crossIdp.status === 404) return
|
|
175
|
+
if (crossIdp.status === 404) return blockedDespiteAssertions('SAML validate seam answered 404 on the cross-IdP probe — RFC 0163 §B.1 is unobserved');
|
|
176
176
|
expect(
|
|
177
177
|
(crossIdp.json as { authenticated?: boolean } | undefined)?.authenticated === true,
|
|
178
178
|
req('openwop.it.auth-subject-link-key-class.a-same-idp-link-forms-control-but-a-cross-idp-collision-must-not-link',
|