@openwop/openwop-conformance 2.42.5 → 2.42.7
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 +26 -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/package.json +2 -2
- package/requirements.json +430 -162
- package/scenario-majors.json +16 -3
- package/schemas/CORPUS-STAMP.json +64 -63
- package/src/lib/audit-checkpoint.ts +72 -0
- package/src/lib/era2-seed.ts +30 -0
- package/src/lib/ext-claims.ts +96 -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/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-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-effect-identity-business-key.test.ts +12 -0
- package/src/scenarios/v2-era-2-append-vocabulary.test.ts +39 -24
- 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-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 +2 -0
- 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-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
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2-ext-rest-transport — RFC 0220 §C, the behavioral witness for the
|
|
3
|
+
* `restTransport` extension family (suite 2.42.7, target major 2).
|
|
4
|
+
*
|
|
5
|
+
* Gate: the host advertises `extensions["<org>.rest-transport"]` under a
|
|
6
|
+
* registered org (spec/v2/ext/restTransport/README.md §The claim). Absent ⇒
|
|
7
|
+
* `inapplicable`. A malformed claim fails the claim leg; it does not skip.
|
|
8
|
+
*
|
|
9
|
+
* Legs, both recorded under `openwop.family.restTransport` so the family row is
|
|
10
|
+
* the least certifiable of them (requirement-ledger fold):
|
|
11
|
+
*
|
|
12
|
+
* 1. `conditionalRunGet: true` — the `conformance-approval` fixture parks at
|
|
13
|
+
* `waiting-approval`, a stable state. The 200 carries a strong ETag, a
|
|
14
|
+
* matching If-None-Match gets 304 with no body, and once the approval is
|
|
15
|
+
* resolved and the run completes the ETag differs — a 304 across that
|
|
16
|
+
* transition would be stale (runs.md §"Caching and encoding").
|
|
17
|
+
* 2. `contentEncodings` — for each advertised coding, the response carries
|
|
18
|
+
* that `Content-Encoding` and decodes to the identity body byte for byte.
|
|
19
|
+
*
|
|
20
|
+
* A leg whose facet the record does not set records `inapplicable` for itself
|
|
21
|
+
* only: a host that claims one facet has promised nothing about the other.
|
|
22
|
+
*
|
|
23
|
+
* Ported from the v1 `run-transport-economy` scenario, which reads the v1 root
|
|
24
|
+
* key; this one reads the v2 extensions record.
|
|
25
|
+
*
|
|
26
|
+
* @see RFCS/0220-ext-families-graduate-on-evidence.md
|
|
27
|
+
* @see spec/v2/ext/restTransport/README.md
|
|
28
|
+
* @see spec/v2/core/runs.md §"Caching and encoding"
|
|
29
|
+
*/
|
|
30
|
+
import { describe, it, expect } from 'vitest';
|
|
31
|
+
import { gunzipSync, brotliDecompressSync } from 'node:zlib';
|
|
32
|
+
import * as zlib from 'node:zlib';
|
|
33
|
+
import { driver } from '../lib/driver.js';
|
|
34
|
+
import { loadEnv } from '../lib/env.js';
|
|
35
|
+
import { v2Discovery } from '../lib/v2.js';
|
|
36
|
+
import { extDeclaration, classifyExtClaim } from '../lib/ext-claims.js';
|
|
37
|
+
import { pollUntilStatus, pollUntilTerminal } from '../lib/polling.js';
|
|
38
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
39
|
+
import { req } from '../lib/requirement-ids.js';
|
|
40
|
+
|
|
41
|
+
type Json = Record<string, unknown>;
|
|
42
|
+
type Encoding = 'gzip' | 'br' | 'zstd';
|
|
43
|
+
const APPROVAL_FIXTURE = 'conformance-approval';
|
|
44
|
+
const APPROVAL_NODE_ID = 'gate';
|
|
45
|
+
const NOOP_FIXTURE = 'conformance-noop';
|
|
46
|
+
const SECTION = 'spec/v2/ext/restTransport/README.md §What the claim adds';
|
|
47
|
+
const R = (why: string): string => req('openwop.family.restTransport', SECTION, why);
|
|
48
|
+
|
|
49
|
+
type Claim = { ok: true; record: Json; key: string } | { ok: false; kind: 'inapplicable' | 'blocked'; reason: string };
|
|
50
|
+
|
|
51
|
+
/** The one well-formed rest-transport record, or why there is none. A malformed claim is asserted here, not skipped. */
|
|
52
|
+
async function claim(): Promise<Claim> {
|
|
53
|
+
const decl = extDeclaration();
|
|
54
|
+
if (!decl) return { ok: false, kind: 'blocked', reason: 'spec/v2/declaration.json is not resolvable in this layout — the extension name and the org registry come from it' };
|
|
55
|
+
const family = decl.families.find((f) => f.key === 'restTransport');
|
|
56
|
+
if (!family) return { ok: false, kind: 'blocked', reason: 'the declaration has no ext row for restTransport — this scenario is stale against the corpus' };
|
|
57
|
+
const doc = await v2Discovery();
|
|
58
|
+
if (!doc) return { ok: false, kind: 'blocked', reason: 'v2 discovery unreachable' };
|
|
59
|
+
const claims = classifyExtClaim(doc, family, decl);
|
|
60
|
+
if (claims.length === 1 && claims[0].state === 'absent') return { ok: false, kind: 'inapplicable', reason: 'no extensions["<org>.rest-transport"] record under a registered org — the host does not advertise restTransport' };
|
|
61
|
+
for (const c of claims) {
|
|
62
|
+
if (c.state === 'malformed') expect(c.state, R(`extensions["${c.key}"]: ${c.why}`)).toBe('well-formed');
|
|
63
|
+
}
|
|
64
|
+
const first = claims.find((c) => c.state === 'well-formed');
|
|
65
|
+
if (!first || first.state !== 'well-formed') return { ok: false, kind: 'blocked', reason: 'no well-formed rest-transport claim' };
|
|
66
|
+
return { ok: true, record: ((doc['extensions'] as Json)[first.key] ?? {}) as Json, key: first.key };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function advertisedEncodings(record: Json): Encoding[] {
|
|
70
|
+
const raw = record['contentEncodings'];
|
|
71
|
+
return Array.isArray(raw) ? raw.filter((e): e is Encoding => e === 'gzip' || e === 'br' || e === 'zstd') : [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
describe('RFC 0220 §C — restTransport: conditional GET on the run snapshot', () => {
|
|
75
|
+
it('a strong ETag on the 200, 304 on a match while the run is parked, and a new ETag once it completes', async () => {
|
|
76
|
+
const c = await claim();
|
|
77
|
+
if (!c.ok) return softSkip(c.kind, c.reason);
|
|
78
|
+
if (c.record['conditionalRunGet'] !== true) return softSkip('inapplicable', `extensions["${c.key}"].conditionalRunGet is not true — the host claims no validator`);
|
|
79
|
+
|
|
80
|
+
const create = await driver.post('/runs', { workflowId: APPROVAL_FIXTURE });
|
|
81
|
+
// Any non-201 is the suite's precondition failing (fixture, credential), not the claim — the v2-a2ui gate's convention.
|
|
82
|
+
if (create.status !== 201) return softSkip('blocked', `POST /runs {workflowId: ${APPROVAL_FIXTURE}} answered ${create.status} — the run the leg needs could not be created`);
|
|
83
|
+
const runId = (create.json as { runId: string }).runId;
|
|
84
|
+
const path = `/runs/${encodeURIComponent(runId)}`;
|
|
85
|
+
await pollUntilStatus(runId, 'waiting-approval', { timeoutMs: 10_000 });
|
|
86
|
+
|
|
87
|
+
const parked = await driver.get(path);
|
|
88
|
+
expect(parked.status, R('the parked run reads 200')).toBe(200);
|
|
89
|
+
const etagParked = parked.headers.get('etag');
|
|
90
|
+
expect(etagParked, R('a host whose record sets conditionalRunGet: true MUST carry an ETag on every 200 of GET /runs/{runId}')).toBeTruthy();
|
|
91
|
+
expect(etagParked?.startsWith('W/'), R('the ETag MUST be strong')).toBe(false);
|
|
92
|
+
|
|
93
|
+
const revalidate = await driver.get(path, { headers: { 'If-None-Match': etagParked as string } });
|
|
94
|
+
expect(revalidate.status, R('a matching If-None-Match MUST receive 304 while the run has not advanced (runs.md §"Caching and encoding")')).toBe(304);
|
|
95
|
+
expect(revalidate.text, R('the 304 MUST carry no body')).toBe('');
|
|
96
|
+
|
|
97
|
+
const resolve = await driver.post(`${path}/interrupts/${encodeURIComponent(APPROVAL_NODE_ID)}`, { resumeValue: { action: 'accept' } });
|
|
98
|
+
expect(resolve.status, R('the approval resolves (control: the run must advance for the rotation check to mean anything)')).toBe(200);
|
|
99
|
+
const terminal = await pollUntilTerminal(runId, { timeoutMs: 10_000 });
|
|
100
|
+
expect(terminal.status, R('the run completes')).toBe('completed');
|
|
101
|
+
|
|
102
|
+
const done = await driver.get(path);
|
|
103
|
+
const etagDone = done.headers.get('etag');
|
|
104
|
+
expect(etagDone, R('the completed run still carries an ETag')).toBeTruthy();
|
|
105
|
+
expect(etagDone, R('the ETag MUST change once the run advances; the parked ETag would otherwise answer 304 for a changed run')).not.toBe(etagParked);
|
|
106
|
+
const stale = await driver.get(path, { headers: { 'If-None-Match': etagParked as string } });
|
|
107
|
+
expect(stale.status, R('the parked ETag MUST NOT match the completed run')).toBe(200);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('RFC 0220 §C — restTransport: each advertised coding round-trips', () => {
|
|
112
|
+
it('each coding in contentEncodings is produced when asked for alone and decodes to the identity body', async () => {
|
|
113
|
+
const c = await claim();
|
|
114
|
+
if (!c.ok) return softSkip(c.kind, c.reason);
|
|
115
|
+
const encodings = advertisedEncodings(c.record);
|
|
116
|
+
if (encodings.length === 0) return softSkip('inapplicable', `extensions["${c.key}"].contentEncodings lists no coding — the host claims none`);
|
|
117
|
+
|
|
118
|
+
const create = await driver.post('/runs', { workflowId: NOOP_FIXTURE });
|
|
119
|
+
// Any non-201 is the suite's precondition failing (fixture, credential), not the claim — the v2-a2ui gate's convention.
|
|
120
|
+
if (create.status !== 201) return softSkip('blocked', `POST /runs {workflowId: ${NOOP_FIXTURE}} answered ${create.status} — the run the leg needs could not be created`);
|
|
121
|
+
const runId = (create.json as { runId: string }).runId;
|
|
122
|
+
await pollUntilTerminal(runId, { timeoutMs: 10_000 });
|
|
123
|
+
|
|
124
|
+
// A raw fetch, not the driver: the driver decodes JSON, and the oracle here is bytes.
|
|
125
|
+
const env = loadEnv();
|
|
126
|
+
const url = `${env.baseUrl}/runs/${encodeURIComponent(runId)}`;
|
|
127
|
+
const base = { Authorization: `Bearer ${env.apiKey}`, Accept: 'application/json', 'OpenWOP-Version': '2.0' };
|
|
128
|
+
const identity = await fetch(url, { headers: { ...base, 'Accept-Encoding': 'identity' } });
|
|
129
|
+
expect(identity.status, R('the identity read is 200')).toBe(200);
|
|
130
|
+
const identityBytes = Buffer.from(await identity.arrayBuffer());
|
|
131
|
+
expect(identityBytes.length, R('the identity body is non-empty')).toBeGreaterThan(0);
|
|
132
|
+
|
|
133
|
+
const zstdMaybe: { zstdDecompressSync?: (b: Buffer) => Buffer } = zlib;
|
|
134
|
+
for (const enc of encodings) {
|
|
135
|
+
const res = await fetch(url, { headers: { ...base, 'Accept-Encoding': enc } });
|
|
136
|
+
expect(res.status, R(`the ${enc} read is 200`)).toBe(200);
|
|
137
|
+
expect(res.headers.get('content-encoding'), R(`a coding listed in contentEncodings MUST be produced when it is the only one asked for (${enc})`)).toBe(enc);
|
|
138
|
+
expect((res.headers.get('vary') ?? '').toLowerCase(), R('a compressed response MUST carry Vary: Accept-Encoding (runs.md §"Caching and encoding")')).toContain('accept-encoding');
|
|
139
|
+
const bytes = Buffer.from(await res.arrayBuffer());
|
|
140
|
+
// Node's fetch decodes gzip and br in transit, so equal bytes already prove the round trip.
|
|
141
|
+
const decode = enc === 'gzip' ? gunzipSync : enc === 'br' ? brotliDecompressSync : zstdMaybe.zstdDecompressSync;
|
|
142
|
+
// No zstd decoder in this Node: the coding was produced (asserted above); the byte compare needs a decoder the suite lacks.
|
|
143
|
+
if (!bytes.equals(identityBytes) && !decode) continue;
|
|
144
|
+
let decoded = bytes;
|
|
145
|
+
if (!bytes.equals(identityBytes) && decode) {
|
|
146
|
+
try { decoded = Buffer.from(decode(bytes)); } catch { /* not compressed bytes either: a real mismatch, compared below */ }
|
|
147
|
+
}
|
|
148
|
+
expect(decoded.equals(identityBytes), R(`the ${enc}-decoded body MUST be byte-identical to the identity body`)).toBe(true);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -147,6 +147,9 @@ describe('v2 idempotency-in-flight (idempotency.md Concurrency, RFC 0213 §B)',
|
|
|
147
147
|
// (#1525's fix, kept verbatim; `r.refusals` is the split form's spelling of
|
|
148
148
|
// its `refusals`, and it now lands on the LOSER id it is about rather than
|
|
149
149
|
// on a row shared with the winner clause.)
|
|
150
|
+
// partial-witness-ok: the winner/replay clauses were asserted above; RFC 0213 §B lets every
|
|
151
|
+
// loser wait for a marked replay, so the 409 branch is an outcome this host did not take,
|
|
152
|
+
// not an unobserved obligation.
|
|
150
153
|
if (r.refusals.length === 0) return softSkip('inapplicable', `no loser was refused in flight — all ${N} answers were successes (each loser a marked replay, which §B permits), so the 409 branch did not run on this host`);
|
|
151
154
|
}, 60_000);
|
|
152
155
|
// The deterministic §B witness (host-sample-test-seams.md §26): the seam only
|
|
@@ -41,11 +41,10 @@ describe('RFC 0175 §C.1 — legacy-profiles-absent (unaided)', () => {
|
|
|
41
41
|
it('no -legacy id appears in a2a.profiles or mcp.profiles and each id matches its facet pattern', async () => {
|
|
42
42
|
const doc = await discovery();
|
|
43
43
|
if (!doc) return softSkip('blocked', 'discovery unreachable');
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
checked++;
|
|
44
|
+
const families = (['a2a', 'mcp'] as const).filter((family) => profilesOf(doc, family) !== null);
|
|
45
|
+
if (families.length === 0) return softSkip('inapplicable', 'host advertises neither a2a nor mcp — no profiles[] to check for a -legacy id');
|
|
46
|
+
for (const family of families) {
|
|
47
|
+
const profiles = profilesOf(doc, family)!;
|
|
49
48
|
for (const id of profiles) {
|
|
50
49
|
expect(
|
|
51
50
|
/-legacy$/.test(id),
|
|
@@ -64,7 +63,6 @@ describe('RFC 0175 §C.1 — legacy-profiles-absent (unaided)', () => {
|
|
|
64
63
|
req('openwop.requirement.0175.legacy-profiles-absent', `facets/${family}.schema.json`, `the ${family} facet MUST validate against its facet schema (capabilities.schema.json ${family}): ${check.errors}`),
|
|
65
64
|
).toBe(true);
|
|
66
65
|
}
|
|
67
|
-
if (checked === 0) softSkip('inapplicable', 'host advertises neither a2a nor mcp — no profiles[] to check for a -legacy id');
|
|
68
66
|
});
|
|
69
67
|
|
|
70
68
|
it('no root profiles[] exists in the v2 representation', async () => {
|
|
@@ -309,7 +309,7 @@ describe('RFC 0198 — v2-mcp-tasks (MCP Tasks on the server mount; disconnect c
|
|
|
309
309
|
if (f.result?.['resultType'] === 'task') terminal = await pollTask(m.url, String(f.result['taskId']), (s) => ['completed', 'failed', 'cancelled'].includes(s), 10_000);
|
|
310
310
|
else {
|
|
311
311
|
// unfailable-leg audit wave 2, 2026-09-27: this branch returned
|
|
312
|
-
//
|
|
312
|
+
// an inapplicable soft-skip after the input_required asserts — a
|
|
313
313
|
// partial-witness PASS at both majors even when the synchronous answer
|
|
314
314
|
// was NOT a tool-error outcome. The synchronous CallToolResult of a
|
|
315
315
|
// failed run MUST still carry isError true; the task projection itself
|
|
@@ -50,6 +50,8 @@ describe('v2 min-client-version (RFC 0172 §A.5 — gated on minClientVersion)',
|
|
|
50
50
|
if (res.status !== 426 && (res.status < 200 || res.status >= 300)) {
|
|
51
51
|
return blockedDespiteAssertions(`the host advertises minClientVersion ${String(floor)} and answered ${res.status} to a client announcing 0.0.1 — neither served (the MAY) nor refused with the registered 426 client_version_unsupported; the refusal form was not observed`);
|
|
52
52
|
}
|
|
53
|
+
// partial-witness-ok: the minClientVersion grammar MUST was asserted above; refusing a
|
|
54
|
+
// below-floor client is a MAY and this host served it, so the 426 shape binds nothing.
|
|
53
55
|
if (res.status !== 426) return softSkip('inapplicable', `the host advertises minClientVersion ${String(floor)} but served a client announcing 0.0.1 (${res.status}) — refusal is a MAY; nothing further is observable`);
|
|
54
56
|
expect(readErrorCode(res.json), req('openwop.requirement.0172.min-client-version', DOC, 'a 426 refusal MUST carry client_version_unsupported')).toBe('client_version_unsupported');
|
|
55
57
|
const r = v2Validator('error-envelope')(res.json);
|
|
@@ -38,12 +38,14 @@ describe('RFC 0175 §D.4 — refresh-sla (unaided; gated on a2a or mcp)', () =>
|
|
|
38
38
|
{ family: 'a2a', offered: 'versions', preferred: 'preferredVersion', floor: 'minimumVersion' },
|
|
39
39
|
{ family: 'mcp', offered: 'revisions', preferred: 'preferredVersion', floor: 'minimumRevision' },
|
|
40
40
|
];
|
|
41
|
-
|
|
42
|
-
const now = Date.now();
|
|
41
|
+
const advertised: Array<{ f: (typeof facets)[number]; rec: Record<string, unknown> }> = [];
|
|
43
42
|
for (const f of facets) {
|
|
44
43
|
const rec = await familyAdvertised(f.family);
|
|
45
|
-
if (
|
|
46
|
-
|
|
44
|
+
if (rec) advertised.push({ f, rec });
|
|
45
|
+
}
|
|
46
|
+
if (advertised.length === 0) return softSkip('inapplicable', 'host advertises neither a2a nor mcp — no refreshedAt to check');
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
for (const { f, rec } of advertised) {
|
|
47
49
|
const refreshedAt = rec['refreshedAt'];
|
|
48
50
|
expect(
|
|
49
51
|
typeof refreshedAt === 'string' && DATE.test(refreshedAt),
|
|
@@ -73,6 +75,5 @@ describe('RFC 0175 §D.4 — refresh-sla (unaided; gated on a2a or mcp)', () =>
|
|
|
73
75
|
req('openwop.requirement.0175.refresh-sla', 'interop.md §The facets', `${f.family}.${f.floor} MUST be one of the offered ${f.offered}[] — a floor below every offer refuses nothing`),
|
|
74
76
|
).toContain(rec[f.floor]);
|
|
75
77
|
}
|
|
76
|
-
if (checked === 0) return softSkip('inapplicable', 'host advertises neither a2a nor mcp — no refreshedAt to check');
|
|
77
78
|
});
|
|
78
79
|
});
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
* the caller is authorized to read the run: for a run the caller cannot read
|
|
11
11
|
* the answer MUST be the one the host gives without the header.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* On the core-standard floor since 2.42.7, beside `v2-sse-last-event-id`. It was
|
|
14
|
+
* held off until the three bundle hosts were measured (rc.59 precedent); all
|
|
15
|
+
* three recorded it executed-pass, two with partial-witness on the SHOULD leg only.
|
|
16
16
|
*
|
|
17
17
|
* Legs, on one completed noop run:
|
|
18
18
|
* 1. a future id (last + 1000) ⇒ 200, zero frames, closed by the server — a
|
|
@@ -92,6 +92,8 @@ describe('v2 sse-last-event-id (events.md §SSE frames)', () => {
|
|
|
92
92
|
// values: resumption MUST emit a state.snapshot first — only where the host serves values.
|
|
93
93
|
const probe = await http(() => driver.get(`/runs/${enc(c.runId)}/events?streamMode=bogus`, { headers: { Accept: 'text/event-stream' } }));
|
|
94
94
|
const supported = (probe?.json as { details?: { supported?: unknown } } | null)?.details?.supported;
|
|
95
|
+
// partial-witness-ok: every Last-Event-ID MUST was asserted above on the debug stream; the
|
|
96
|
+
// state.snapshot-first rule binds only a host serving values mode.
|
|
95
97
|
if (!Array.isArray(supported) || !supported.includes('values')) return softSkip('inapplicable', `values mode not served (details.supported: ${JSON.stringify(supported)}) — the state.snapshot-first resumption leg does not apply`);
|
|
96
98
|
const values = await subscribe(`/runs/${enc(c.runId)}/events?streamMode=values`, { timeoutMs: 8_000, extraHeaders: V2, lastEventId: String(first) });
|
|
97
99
|
expect(values.status, req(ID, DOC, `a values resume MUST answer 200 — got ${values.status}`)).toBe(200);
|
|
@@ -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
|
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
});
|