@openwop/openwop-conformance 1.106.0 → 1.123.0
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/README.md +3 -3
- package/coverage.md +6 -0
- package/dist/cli.js +89 -45
- package/dist/lib/certification-bundle-verify.js +274 -0
- package/dist/lib/profiles.js +102 -19
- package/dist/lib/requirement-ledger.js +182 -0
- package/dist/lib/requirement-registry.js +110 -0
- package/dist/lib/scenario-disposition.js +249 -0
- package/dist/lib/soft-skip.js +82 -0
- package/fixtures/conformance-a2a-task-roundtrip.json +1 -1
- package/fixtures.md +1 -1
- package/package.json +1 -1
- package/schemas/CORPUS-STAMP.json +2 -2
- package/schemas/README.md +1 -0
- package/schemas/capabilities.schema.json +2 -2
- package/schemas/compensation-policy.schema.json +64 -0
- package/schemas/run-snapshot.schema.json +5 -0
- package/schemas/workflow-chain-pack-manifest.schema.json +125 -0
- package/schemas/workflow-definition.schema.json +4 -0
- package/src/cli.ts +99 -45
- package/src/lib/a2a-fake-peer.ts +290 -110
- package/src/lib/certification-bundle-verify.ts +356 -0
- package/src/lib/llm-cache-key-recipe.ts +6 -5
- package/src/lib/mcp-fake-server.ts +235 -72
- package/src/lib/profiles.ts +132 -19
- package/src/lib/requirement-ledger.ts +98 -1
- package/src/lib/requirement-registry.ts +47 -4
- package/src/lib/scenario-disposition.ts +291 -0
- package/src/lib/soft-skip.ts +89 -0
- package/src/lib/workflow-chain-expansion.ts +189 -0
- package/src/scenarios/a2a-1-0-agent-card.test.ts +179 -0
- package/src/scenarios/a2a-1-0-task-roundtrip.test.ts +109 -0
- package/src/scenarios/a2a-card-runtime-consistency.test.ts +167 -0
- package/src/scenarios/a2a-peer-authority.test.ts +99 -0
- package/src/scenarios/a2a-task-roundtrip.test.ts +18 -6
- package/src/scenarios/agent-loop-iteration-monotonic.test.ts +3 -2
- package/src/scenarios/agent-loop-stateful-resume.test.ts +3 -2
- package/src/scenarios/agent-loop-version5-shape.test.ts +2 -1
- package/src/scenarios/agent-loop-workspace-snapshot.test.ts +4 -3
- package/src/scenarios/approval-gate-flow.test.ts +5 -4
- package/src/scenarios/artifact-type-store-without-render.test.ts +2 -1
- package/src/scenarios/auth-scim-profile.test.ts +5 -4
- package/src/scenarios/authorization-fail-closed.test.ts +4 -3
- package/src/scenarios/certification-bundle-non-vacuous.test.ts +249 -0
- package/src/scenarios/certification-bundle-redaction.test.ts +182 -0
- package/src/scenarios/certification-floor-enforcement.test.ts +30 -6
- package/src/scenarios/chain-compensation-expansion.test.ts +243 -0
- package/src/scenarios/commitment-fired.test.ts +3 -2
- package/src/scenarios/compensation-behavior.test.ts +63 -0
- package/src/scenarios/compensation-profile.test.ts +144 -1
- package/src/scenarios/conformance-advertised-seam-required.test.ts +118 -0
- package/src/scenarios/conformance-execution-witness.test.ts +104 -0
- package/src/scenarios/core-manifest-and-extension-registry.test.ts +26 -0
- package/src/scenarios/credential-payload-redaction.test.ts +4 -3
- package/src/scenarios/credentials-capability-shape.test.ts +4 -3
- package/src/scenarios/cross-engine-append-behavior.test.ts +5 -4
- package/src/scenarios/cross-engine-append-ordering.test.ts +3 -2
- package/src/scenarios/cross-host-ancestry-endpoint.test.ts +5 -4
- package/src/scenarios/cross-host-causation-shape.test.ts +3 -2
- package/src/scenarios/cross-workspace-isolation.test.ts +4 -3
- package/src/scenarios/deadletter-retry-exhaustion.test.ts +5 -4
- package/src/scenarios/distillation-index-roundtrip.test.ts +3 -2
- package/src/scenarios/distillation-secret-carryforward.test.ts +3 -2
- package/src/scenarios/distillation-shape.test.ts +2 -1
- package/src/scenarios/distillation-stable-archive.test.ts +4 -3
- package/src/scenarios/distillation-token-budget.test.ts +4 -3
- package/src/scenarios/feedback-correction-redaction.test.ts +4 -3
- package/src/scenarios/feedback-cross-tenant-isolation.test.ts +4 -3
- package/src/scenarios/feedback-fork-not-copied.test.ts +6 -5
- package/src/scenarios/feedback-on-terminal-run.test.ts +4 -3
- package/src/scenarios/feedback-record-and-list.test.ts +4 -3
- package/src/scenarios/feedback-unsupported-501.test.ts +3 -2
- package/src/scenarios/fixtures-valid.test.ts +16 -18
- package/src/scenarios/heartbeat-capability-shape.test.ts +2 -1
- package/src/scenarios/heartbeat-fires-once-per-tick.test.ts +4 -3
- package/src/scenarios/heartbeat-idempotent-no-spam.test.ts +6 -5
- package/src/scenarios/heartbeat-runtime-bound.test.ts +4 -3
- package/src/scenarios/mcp-2026-07-28-discover.test.ts +207 -0
- package/src/scenarios/mcp-cache-tenant-scope.test.ts +66 -0
- package/src/scenarios/mcp-current-auth-boundary.test.ts +55 -0
- package/src/scenarios/mcp-extension-opacity.test.ts +79 -0
- package/src/scenarios/mcp-mrtr-roundtrip.test.ts +140 -0
- package/src/scenarios/mcp-stateless-request.test.ts +77 -0
- package/src/scenarios/mcp-tool-roundtrip.test.ts +4 -3
- package/src/scenarios/mcp-toolcall-redaction.test.ts +2 -1
- package/src/scenarios/memory-attribution-emits-on-write.test.ts +6 -5
- package/src/scenarios/memory-attribution-no-content.test.ts +4 -3
- package/src/scenarios/memory-attribution-replay-stable.test.ts +6 -5
- package/src/scenarios/memory-attribution-tenant-scoped.test.ts +4 -3
- package/src/scenarios/memory-compaction-event-emitted.test.ts +3 -2
- package/src/scenarios/memory-compaction-provenance-tag.test.ts +6 -5
- package/src/scenarios/memory-compaction-sr1-carry-forward.test.ts +4 -3
- package/src/scenarios/memory-consolidation-idempotent.test.ts +4 -3
- package/src/scenarios/memory-injection-budget.test.ts +4 -3
- package/src/scenarios/metric-emission.test.ts +4 -3
- package/src/scenarios/multi-agent-confidence-escalation.test.ts +5 -4
- package/src/scenarios/multi-agent-handoff-state-machine.test.ts +4 -3
- package/src/scenarios/multi-agent-memory-lifecycle.test.ts +5 -4
- package/src/scenarios/multi-party-conversation-behavioral.test.ts +3 -2
- package/src/scenarios/multi-region-idempotency-behavior.test.ts +7 -6
- package/src/scenarios/multi-region-idempotency.test.ts +4 -3
- package/src/scenarios/normative-example-extraction.test.ts +242 -0
- package/src/scenarios/oauth-authorization-code-roundtrip.test.ts +4 -3
- package/src/scenarios/oauth-capability-shape.test.ts +4 -3
- package/src/scenarios/oauth-connector-redaction.test.ts +4 -3
- package/src/scenarios/openapi-asyncapi-sdk-parity.test.ts +309 -0
- package/src/scenarios/org-position-no-authority-escalation.test.ts +2 -1
- package/src/scenarios/otel-emission-grpc.test.ts +4 -3
- package/src/scenarios/otel-emission.test.ts +4 -3
- package/src/scenarios/otel-trace-propagation-subworkflow.test.ts +5 -4
- package/src/scenarios/otel-trace-propagation.test.ts +4 -3
- package/src/scenarios/pack-registry-isolation.test.ts +4 -3
- package/src/scenarios/pack-registry-publish.test.ts +65 -3
- package/src/scenarios/pack-registry.test.ts +22 -0
- package/src/scenarios/profile-discovery-core-alias.test.ts +137 -0
- package/src/scenarios/profileDerivation.test.ts +2 -1
- package/src/scenarios/rate-limit-envelope.test.ts +2 -1
- package/src/scenarios/registry-public.test.ts +6 -5
- package/src/scenarios/replay-divergence-at-refusal.test.ts +9 -8
- package/src/scenarios/replay-llm-cache-key-portable.test.ts +19 -13
- package/src/scenarios/replay-llm-cache-key.test.ts +120 -47
- package/src/scenarios/replay-observable-sequence-determinism.test.ts +5 -4
- package/src/scenarios/replay-side-effect-suppression.test.ts +20 -1
- package/src/scenarios/requirement-ledger.test.ts +29 -6
- package/src/scenarios/runner-ledger.test.ts +279 -0
- package/src/scenarios/runtime-requires-install-gate.test.ts +6 -5
- package/src/scenarios/sandbox-memory-cap.test.ts +3 -2
- package/src/scenarios/sandbox-mvp-behavior.test.ts +11 -10
- package/src/scenarios/sandbox-no-host-fs-escape.test.ts +2 -1
- package/src/scenarios/sandbox-timeout-cap.test.ts +3 -2
- package/src/scenarios/stream-text-fixture.test.ts +5 -4
- package/src/scenarios/subrun-approval-fail-closed.test.ts +3 -2
- package/src/scenarios/subrun-approval-gate.test.ts +4 -3
- package/src/scenarios/subrun-attestation-shape.test.ts +2 -1
- package/src/scenarios/subrun-checksum-stable.test.ts +4 -3
- package/src/scenarios/test-seam-unauthenticated.test.ts +61 -11
- package/src/scenarios/tool-hooks-authorization-fail-closed.test.ts +3 -2
- package/src/scenarios/tool-hooks-content-free.test.ts +3 -2
- package/src/scenarios/tool-hooks-rate-limit.test.ts +3 -2
- package/src/scenarios/tool-hooks-secret-redaction.test.ts +3 -2
- package/src/scenarios/tool-hooks-shape.test.ts +2 -1
- package/src/scenarios/trigger-bridge-delivery.test.ts +3 -2
- package/src/scenarios/version-fold.test.ts +5 -4
- package/src/scenarios/wasm-pack-abi-version-rejection.test.ts +5 -4
- package/src/scenarios/wasm-pack-invoke-completed.test.ts +3 -2
- package/src/scenarios/wasm-pack-invoke-suspended.test.ts +3 -2
- package/src/scenarios/wasm-pack-load.test.ts +4 -3
- package/src/scenarios/wasm-pack-memory-cap.test.ts +4 -3
- package/src/scenarios/wasm-pack-replay-determinism.test.ts +3 -2
- package/src/scenarios/webhook-signed-delivery.test.ts +4 -3
- package/src/scenarios/workflow-primary-output-annotation.test.ts +13 -23
- package/src/scenarios/workspace-cross-tenant-isolation-blackbox.test.ts +3 -2
- package/src/scenarios/workspace-cross-tenant-isolation.test.ts +3 -2
- package/src/setup.ts +95 -1
|
@@ -10,23 +10,24 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { describe, it, expect } from 'vitest';
|
|
13
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
13
14
|
import { driver } from '../lib/driver.js';
|
|
14
15
|
import { readSubRunAttestationCap, invokeSubRunAttest } from '../lib/subRunAttestation.js';
|
|
15
16
|
|
|
16
17
|
describe('subrun-approval-gate (RFC 0063 §C)', () => {
|
|
17
18
|
it('accept merges the child outputs; reject does not', async () => {
|
|
18
|
-
if ((await readSubRunAttestationCap()) !== true) return;
|
|
19
|
+
if ((await readSubRunAttestationCap()) !== true) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `(await readSubRunAttestationCap()) !== true` returned early');
|
|
19
20
|
const base = { childOutputs: { artifact: 'x' }, outputAttestation: { requireApproval: true } };
|
|
20
21
|
|
|
21
22
|
const accepted = await invokeSubRunAttest({ ...base, approvalAction: 'accept' });
|
|
22
|
-
if (accepted === null) return
|
|
23
|
+
if (accepted === null) return softSkip('blocked', 'seam absent — soft-skip');
|
|
23
24
|
expect(
|
|
24
25
|
accepted.merged,
|
|
25
26
|
driver.describe('RFC 0063 §C', 'an `accept` approval MUST merge the child outputs'),
|
|
26
27
|
).toBe(true);
|
|
27
28
|
|
|
28
29
|
const rejected = await invokeSubRunAttest({ ...base, approvalAction: 'reject' });
|
|
29
|
-
if (rejected === null) return;
|
|
30
|
+
if (rejected === null) return softSkip('blocked', 'precondition not met — `rejected === null` returned early (seam, prior step, or fixture unavailable)');
|
|
30
31
|
expect(
|
|
31
32
|
rejected.merged,
|
|
32
33
|
driver.describe('RFC 0063 §C', 'a `reject` approval MUST NOT merge the child outputs'),
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { describe, it, expect } from 'vitest';
|
|
14
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
14
15
|
import { driver } from '../lib/driver.js';
|
|
15
16
|
import { readSubRunAttestationCap } from '../lib/subRunAttestation.js';
|
|
16
17
|
|
|
@@ -18,7 +19,7 @@ describe('subrun-attestation-shape: advertisement (RFC 0063 §A)', () => {
|
|
|
18
19
|
it('capabilities.agents.subRunAttestation is absent or a boolean', async () => {
|
|
19
20
|
const cap = await readSubRunAttestationCap();
|
|
20
21
|
// null = unadvertised (no agents block OR flag omitted) — valid.
|
|
21
|
-
if (cap === null) return;
|
|
22
|
+
if (cap === null) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `cap === null` returned early');
|
|
22
23
|
expect(
|
|
23
24
|
typeof cap,
|
|
24
25
|
driver.describe(
|
|
@@ -11,21 +11,22 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { describe, it, expect } from 'vitest';
|
|
14
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
14
15
|
import { driver } from '../lib/driver.js';
|
|
15
16
|
import { readSubRunAttestationCap, invokeSubRunAttest } from '../lib/subRunAttestation.js';
|
|
16
17
|
|
|
17
18
|
describe('subrun-checksum-stable (RFC 0063 §B)', () => {
|
|
18
19
|
it('identical child outputs produce an identical sha256 attestation checksum', async () => {
|
|
19
|
-
if ((await readSubRunAttestationCap()) !== true) return;
|
|
20
|
+
if ((await readSubRunAttestationCap()) !== true) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `(await readSubRunAttestationCap()) !== true` returned early');
|
|
20
21
|
const childOutputs = { report: 'done', score: 0.9, tags: ['a', 'b'] };
|
|
21
22
|
const a = await invokeSubRunAttest({ childOutputs, outputAttestation: { checksum: true } });
|
|
22
|
-
if (a === null) return
|
|
23
|
+
if (a === null) return softSkip('blocked', 'seam absent — soft-skip');
|
|
23
24
|
// Key-reordered but value-identical: JCS canonicalization MUST yield the same hash.
|
|
24
25
|
const b = await invokeSubRunAttest({
|
|
25
26
|
childOutputs: { tags: ['a', 'b'], score: 0.9, report: 'done' },
|
|
26
27
|
outputAttestation: { checksum: true },
|
|
27
28
|
});
|
|
28
|
-
if (b === null) return;
|
|
29
|
+
if (b === null) return softSkip('blocked', 'precondition not met — `b === null` returned early (seam, prior step, or fixture unavailable)');
|
|
29
30
|
const att = a.attestation ?? {};
|
|
30
31
|
expect(
|
|
31
32
|
typeof att.checksum === 'string' && (att.checksum as string).length > 0,
|
|
@@ -67,11 +67,41 @@ const SEAM_PROBES: readonly string[] = [
|
|
|
67
67
|
/** 200 is the only unambiguous failure: the seam answered a stranger. */
|
|
68
68
|
const ACCEPTABLE = new Set([401, 403, 404, 405, 501]);
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* A `200` can arrive from two very different places, and the difference decides
|
|
72
|
+
* what you should go fix.
|
|
73
|
+
*
|
|
74
|
+
* Every seam in `host-sample-test-seams.md` answers with a JSON body — the doc
|
|
75
|
+
* specifies each one's response shape as an object (`{ released, event }`,
|
|
76
|
+
* `{ ok: true, packId }`, `{ entries: … }`). **No seam answers with an HTML
|
|
77
|
+
* document.** So a `200 text/html` is not a seam that talked to a stranger; it
|
|
78
|
+
* is a static-hosting catch-all — an SPA rewrite in front of the API — matching
|
|
79
|
+
* a path the backend never saw.
|
|
80
|
+
*
|
|
81
|
+
* This was found by probing `app.openwop.dev` (Firebase Hosting, `**` rewrite
|
|
82
|
+
* to the SPA shell) instead of the backend's own origin. Every probe returned
|
|
83
|
+
* `200 text/html` with the app's `index.html`, which under the previous message
|
|
84
|
+
* read as *"an open control surface on a public origin"* — alarming, and wrong.
|
|
85
|
+
* The same paths on `…-backend-….run.app` return `404 application/json`.
|
|
86
|
+
*
|
|
87
|
+
* **Both cases still fail.** Nothing here relaxes the assertion, because the
|
|
88
|
+
* cheap way to silence a false alarm is to stop failing on `200`, and that would
|
|
89
|
+
* delete the check. What changes is the diagnosis: an HTML catch-all means the
|
|
90
|
+
* suite is pointed at the CDN rather than the API origin, which is its own
|
|
91
|
+
* defect — a run against the wrong origin cannot witness anything about the
|
|
92
|
+
* host, and every other leg in the suite is equally blind at that base URL.
|
|
93
|
+
*/
|
|
94
|
+
function classify200(contentType: string | null): 'seam' | 'catchall' {
|
|
95
|
+
return (contentType ?? '').toLowerCase().includes('text/html') ? 'catchall' : 'seam';
|
|
96
|
+
}
|
|
97
|
+
|
|
70
98
|
describe('test-seam-unauthenticated: an enabled seam still authenticates', () => {
|
|
71
99
|
it('no /v1/host/sample/* seam answers an unauthenticated request with 200', async () => {
|
|
72
100
|
const env = loadEnv();
|
|
73
101
|
const answered: string[] = [];
|
|
74
102
|
let probed = 0;
|
|
103
|
+
let seamAnswers = 0;
|
|
104
|
+
let catchAllAnswers = 0;
|
|
75
105
|
|
|
76
106
|
for (const path of SEAM_PROBES) {
|
|
77
107
|
// node:fetch directly with NO Authorization header — the driver's
|
|
@@ -83,8 +113,18 @@ describe('test-seam-unauthenticated: an enabled seam still authenticates', () =>
|
|
|
83
113
|
continue; // connection-level refusal is a stronger answer than 404
|
|
84
114
|
}
|
|
85
115
|
probed += 1;
|
|
86
|
-
if (res.status === 200)
|
|
87
|
-
|
|
116
|
+
if (res.status === 200) {
|
|
117
|
+
const contentType = res.headers.get('content-type');
|
|
118
|
+
const kind = classify200(contentType);
|
|
119
|
+
if (kind === 'seam') seamAnswers += 1;
|
|
120
|
+
else catchAllAnswers += 1;
|
|
121
|
+
answered.push(
|
|
122
|
+
`${path} -> 200 (${contentType ?? 'no content-type'}) — ` +
|
|
123
|
+
(kind === 'seam'
|
|
124
|
+
? 'SEAM ANSWERED A CREDENTIAL-LESS CALLER'
|
|
125
|
+
: 'HTML body: a static-hosting catch-all, not the API origin'),
|
|
126
|
+
);
|
|
127
|
+
} else if (!ACCEPTABLE.has(res.status)) {
|
|
88
128
|
// Not a pass and not the known failure — record it rather than
|
|
89
129
|
// silently tolerating a status nobody reasoned about.
|
|
90
130
|
answered.push(`${path} -> ${res.status} (unexpected; expected one of ${[...ACCEPTABLE].join('/')})`);
|
|
@@ -100,17 +140,27 @@ describe('test-seam-unauthenticated: an enabled seam still authenticates', () =>
|
|
|
100
140
|
'not looking, which is the failure mode it exists to prevent',
|
|
101
141
|
).toBeGreaterThan(0);
|
|
102
142
|
|
|
143
|
+
// The diagnosis is chosen from what the answers actually were. An HTML
|
|
144
|
+
// catch-all and an open seam are both failures and they are not the same
|
|
145
|
+
// bug, so the message must not name the wrong one — a check that reports a
|
|
146
|
+
// security finding for a misrouted base URL trains its reader to distrust it.
|
|
147
|
+
const diagnosis =
|
|
148
|
+
seamAnswers === 0 && catchAllAnswers > 0
|
|
149
|
+
? 'WRONG ORIGIN, not an open seam. Every 200 above carried an HTML body, and no seam in ' +
|
|
150
|
+
'host-sample-test-seams.md answers with HTML — so these are a static-hosting rewrite ' +
|
|
151
|
+
'(an SPA `**` catch-all) matching paths the backend never received. Point ' +
|
|
152
|
+
'OPENWOP_BASE_URL at the API origin itself. This is still a failure: at this base URL ' +
|
|
153
|
+
'no leg in the suite is witnessing the host, so a green run here would mean nothing.'
|
|
154
|
+
: 'An ENABLED seam MUST require an authenticated, NON-ANONYMOUS principal. A host that ' +
|
|
155
|
+
'mints an anonymous identity for credential-less callers MUST NOT treat it as ' +
|
|
156
|
+
'satisfying that. The env-gate governs whether a seam EXISTS; it does not govern who ' +
|
|
157
|
+
'may call it. A seam answering a credential-less request with 200 is an open control ' +
|
|
158
|
+
'surface on a public origin — and staging keys such as `nodeId` are not secrets, they ' +
|
|
159
|
+
'ship inside chain packs.';
|
|
160
|
+
|
|
103
161
|
expect(
|
|
104
162
|
answered,
|
|
105
|
-
driver.describe(
|
|
106
|
-
'host-sample-test-seams.md §"Production safety (normative)"',
|
|
107
|
-
'An ENABLED seam MUST require an authenticated, NON-ANONYMOUS principal. A host that mints ' +
|
|
108
|
-
'an anonymous identity for credential-less callers MUST NOT treat it as satisfying that. ' +
|
|
109
|
-
'The env-gate governs whether a seam EXISTS; it does not govern who may call it. A seam ' +
|
|
110
|
-
'answering a credential-less request with 200 is an open control surface on a public ' +
|
|
111
|
-
'origin — and staging keys such as `nodeId` are not secrets, they ship inside chain ' +
|
|
112
|
-
'packs.\n ' + answered.join('\n '),
|
|
113
|
-
),
|
|
163
|
+
driver.describe('host-sample-test-seams.md §"Production safety (normative)"', `${diagnosis}\n ${answered.join('\n ')}`),
|
|
114
164
|
).toEqual([]);
|
|
115
165
|
});
|
|
116
166
|
});
|
|
@@ -12,13 +12,14 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { describe, it, expect } from 'vitest';
|
|
15
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
15
16
|
import { driver } from '../lib/driver.js';
|
|
16
17
|
import { readToolHooksCap, invokeToolHook } from '../lib/toolHooks.js';
|
|
17
18
|
|
|
18
19
|
describe('tool-hooks-authorization-fail-closed (RFC 0064 §C)', () => {
|
|
19
20
|
it('a principal lacking a tool scope is denied and the tool is not invoked', async () => {
|
|
20
21
|
const cap = await readToolHooksCap();
|
|
21
|
-
if (cap?.perToolAuthorization !== true) return;
|
|
22
|
+
if (cap?.perToolAuthorization !== true) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `cap?.perToolAuthorization !== true` returned early');
|
|
22
23
|
// A principal with no scopes against a tool requiring one MUST be denied.
|
|
23
24
|
const res = await invokeToolHook({
|
|
24
25
|
principal: 'conformance-unprivileged',
|
|
@@ -26,7 +27,7 @@ describe('tool-hooks-authorization-fail-closed (RFC 0064 §C)', () => {
|
|
|
26
27
|
requiredScopes: ['db:write'],
|
|
27
28
|
args: {},
|
|
28
29
|
});
|
|
29
|
-
if (res === null) return
|
|
30
|
+
if (res === null) return softSkip('blocked', 'seam absent — soft-skip');
|
|
30
31
|
expect(
|
|
31
32
|
(res.toolReturned ?? {}).status,
|
|
32
33
|
driver.describe('RFC 0064 §C', 'a missing/unevaluable tool scope MUST fail closed → status:"forbidden"'),
|
|
@@ -11,15 +11,16 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { describe, it, expect } from 'vitest';
|
|
14
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
14
15
|
import { driver } from '../lib/driver.js';
|
|
15
16
|
import { readToolHooksCap, invokeToolHook } from '../lib/toolHooks.js';
|
|
16
17
|
|
|
17
18
|
describe('tool-hooks-content-free (RFC 0064 §B)', () => {
|
|
18
19
|
it('toolCalled carries argsHash; toolReturned carries status + durationMs', async () => {
|
|
19
20
|
const cap = await readToolHooksCap();
|
|
20
|
-
if (cap?.prePostEvents !== true) return;
|
|
21
|
+
if (cap?.prePostEvents !== true) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `cap?.prePostEvents !== true` returned early');
|
|
21
22
|
const res = await invokeToolHook({ principal: 'core.system', toolName: 'web.search', args: { q: 'openwop' } });
|
|
22
|
-
if (res === null) return
|
|
23
|
+
if (res === null) return softSkip('blocked', 'seam absent — soft-skip');
|
|
23
24
|
const called = res.toolCalled ?? {};
|
|
24
25
|
const returned = res.toolReturned ?? {};
|
|
25
26
|
expect(
|
|
@@ -10,20 +10,21 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { describe, it, expect } from 'vitest';
|
|
13
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
13
14
|
import { driver } from '../lib/driver.js';
|
|
14
15
|
import { readToolHooksCap, invokeToolHook } from '../lib/toolHooks.js';
|
|
15
16
|
|
|
16
17
|
describe('tool-hooks-rate-limit (RFC 0064 §D)', () => {
|
|
17
18
|
it('an exhausted (principal, tool) bucket yields status:"rate_limited"', async () => {
|
|
18
19
|
const cap = await readToolHooksCap();
|
|
19
|
-
if (cap?.perToolRateLimit !== true) return;
|
|
20
|
+
if (cap?.perToolRateLimit !== true) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `cap?.perToolRateLimit !== true` returned early');
|
|
20
21
|
const res = await invokeToolHook({
|
|
21
22
|
principal: 'core.system',
|
|
22
23
|
toolName: 'web.search',
|
|
23
24
|
args: { q: 'x' },
|
|
24
25
|
simulateRateLimitExhausted: true,
|
|
25
26
|
});
|
|
26
|
-
if (res === null) return
|
|
27
|
+
if (res === null) return softSkip('blocked', 'seam absent — soft-skip');
|
|
27
28
|
expect(
|
|
28
29
|
(res.toolReturned ?? {}).status,
|
|
29
30
|
driver.describe('RFC 0064 §D', 'an exhausted token bucket MUST yield status:"rate_limited" without invoking the tool'),
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { describe, it, expect } from 'vitest';
|
|
14
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
14
15
|
import { driver } from '../lib/driver.js';
|
|
15
16
|
import { readToolHooksCap, invokeToolHook } from '../lib/toolHooks.js';
|
|
16
17
|
|
|
@@ -19,13 +20,13 @@ const CANARY = 'sk-canary-rfc0064-do-not-leak-xyz789';
|
|
|
19
20
|
describe('tool-hooks-secret-redaction (RFC 0064 §E)', () => {
|
|
20
21
|
it('a secret-shaped tool arg never appears in the emitted events', async () => {
|
|
21
22
|
const cap = await readToolHooksCap();
|
|
22
|
-
if (cap?.prePostEvents !== true) return;
|
|
23
|
+
if (cap?.prePostEvents !== true) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `cap?.prePostEvents !== true` returned early');
|
|
23
24
|
const res = await invokeToolHook({
|
|
24
25
|
principal: 'core.system',
|
|
25
26
|
toolName: 'web.search',
|
|
26
27
|
args: { apiKey: CANARY, q: 'openwop' },
|
|
27
28
|
});
|
|
28
|
-
if (res === null) return
|
|
29
|
+
if (res === null) return softSkip('blocked', 'seam absent — soft-skip');
|
|
29
30
|
expect(
|
|
30
31
|
JSON.stringify(res).includes(CANARY),
|
|
31
32
|
driver.describe('RFC 0064 §B', 'a resolved secret MUST be redacted before hashing; the raw value MUST NOT appear in argsHash or any emitted field (SR-1)'),
|
|
@@ -11,13 +11,14 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { describe, it, expect } from 'vitest';
|
|
14
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
14
15
|
import { driver } from '../lib/driver.js';
|
|
15
16
|
import { readToolHooksCap } from '../lib/toolHooks.js';
|
|
16
17
|
|
|
17
18
|
describe('tool-hooks-shape: advertisement (RFC 0064 §A)', () => {
|
|
18
19
|
it('capabilities.toolHooks is absent or a well-formed object', async () => {
|
|
19
20
|
const cap = await readToolHooksCap();
|
|
20
|
-
if (cap === null) return
|
|
21
|
+
if (cap === null) return softSkip('inapplicable', 'not advertised — valid');
|
|
21
22
|
expect(
|
|
22
23
|
typeof cap.supported,
|
|
23
24
|
driver.describe('capabilities.schema.json §toolHooks', 'toolHooks.supported MUST be a boolean when the block is present'),
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
import { describe, it, expect } from 'vitest';
|
|
31
|
+
import { seamAbsent } from '../lib/soft-skip.js';
|
|
31
32
|
import { driver } from '../lib/driver.js';
|
|
32
33
|
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
33
34
|
import {
|
|
@@ -52,11 +53,11 @@ function expectContentFree(payload: Record<string, unknown>, where: string): voi
|
|
|
52
53
|
describe('trigger-bridge-delivery (RFC 0083 §C)', () => {
|
|
53
54
|
it('de-dups by dedupKey, retries to dead-letter, and links delivery→run causation', async () => {
|
|
54
55
|
if (!behaviorGate('openwop-trigger-bridge', await isTriggerBridgeProfileAdvertised())) return;
|
|
55
|
-
if (!(await isEventLogSeamAvailable())) return
|
|
56
|
+
if (!(await isEventLogSeamAvailable())) return seamAbsent('host advertises openwop-trigger-bridge but the event-log seam is absent');
|
|
56
57
|
|
|
57
58
|
// ---- Leg 1: dedup → effectively-once (§C-1) ---------------------------
|
|
58
59
|
const dedup = await driveDelivery({ scenario: 'dedup', dedupKey: 'conformance-dedup-key', source: 'queue' });
|
|
59
|
-
if (dedup === null) return
|
|
60
|
+
if (dedup === null) return seamAbsent('host advertises openwop-trigger-bridge but the delivery seam is unwired');
|
|
60
61
|
|
|
61
62
|
// The profile is derived AND the seam is wired — missing evidence is a
|
|
62
63
|
// FAILURE, not a soft-skip. A repeated dedupKey MUST be effectively-once:
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
36
|
import { describe, it, expect } from 'vitest';
|
|
37
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
37
38
|
import { driver, type OpenWOPResponse } from '../lib/driver.js';
|
|
38
39
|
import { pollUntilTerminal } from '../lib/polling.js';
|
|
39
40
|
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
@@ -84,7 +85,7 @@ describe.skipIf(SKIP_NO_FIXTURE)('version-fold: forced engine versions fold-best
|
|
|
84
85
|
'[version-fold] host does not advertise Capabilities.testing.forceEngineVersionRange; ' +
|
|
85
86
|
'skipping (the X-Force-Engine-Version seam is test-keys-only and optional)',
|
|
86
87
|
);
|
|
87
|
-
return;
|
|
88
|
+
return softSkip('blocked', 'precondition not met — `seam === null` returned early (seam, prior step, or fixture unavailable)');
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
const candidates = [seam.range.min, ...(seam.current === null ? [] : [seam.current]), seam.range.max];
|
|
@@ -104,7 +105,7 @@ describe.skipIf(SKIP_NO_FIXTURE)('version-fold: forced engine versions fold-best
|
|
|
104
105
|
if (create.status === 403 && errCode(create.json) === 'force_engine_version_forbidden') {
|
|
105
106
|
// eslint-disable-next-line no-console
|
|
106
107
|
console.warn('[version-fold] API key is production-scoped (403 force_engine_version_forbidden); skipping');
|
|
107
|
-
return;
|
|
108
|
+
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\')');
|
|
108
109
|
}
|
|
109
110
|
expect(
|
|
110
111
|
create.status,
|
|
@@ -165,7 +166,7 @@ describe.skipIf(SKIP_NO_FIXTURE)('version-fold: forced engine versions fold-best
|
|
|
165
166
|
if (seam === null) {
|
|
166
167
|
// eslint-disable-next-line no-console
|
|
167
168
|
console.warn('[version-fold] forceEngineVersionRange not advertised; skipping negative leg');
|
|
168
|
-
return;
|
|
169
|
+
return softSkip('inapplicable', '[version-fold] forceEngineVersionRange not advertised; skipping negative leg');
|
|
169
170
|
}
|
|
170
171
|
|
|
171
172
|
const outOfRange = seam.range.max + 1;
|
|
@@ -173,7 +174,7 @@ describe.skipIf(SKIP_NO_FIXTURE)('version-fold: forced engine versions fold-best
|
|
|
173
174
|
if (create.status === 403 && errCode(create.json) === 'force_engine_version_forbidden') {
|
|
174
175
|
// eslint-disable-next-line no-console
|
|
175
176
|
console.warn('[version-fold] API key is production-scoped; skipping negative leg');
|
|
176
|
-
return;
|
|
177
|
+
return softSkip('blocked', '[version-fold] API key is production-scoped; skipping negative leg (create.status === 403 && errCode(create.json) === \'force_engine_version_forbidden\')');
|
|
177
178
|
}
|
|
178
179
|
expect(
|
|
179
180
|
create.status,
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
import { describe, it, expect } from 'vitest';
|
|
28
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
28
29
|
import { driver } from '../lib/driver.js';
|
|
29
30
|
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
30
31
|
|
|
@@ -37,7 +38,7 @@ describe('wasm-pack-abi-version-rejection: host advertises supported ABI version
|
|
|
37
38
|
const wasm =
|
|
38
39
|
capabilityFamily<{ wasm?: Record<string, unknown> }>(disco.json, 'nodePackRuntimes')?.wasm;
|
|
39
40
|
|
|
40
|
-
if (!wasm?.supported) return;
|
|
41
|
+
if (!wasm?.supported) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!wasm?.supported` returned early');
|
|
41
42
|
|
|
42
43
|
expect(Array.isArray(wasm.abiVersions), driver.describe(
|
|
43
44
|
'RFCS/0008-wasm-abi.md §H',
|
|
@@ -63,7 +64,7 @@ describe('wasm-pack-abi-version-rejection: positive path via misbehaving pack',
|
|
|
63
64
|
const wasm =
|
|
64
65
|
capabilityFamily<{ wasm?: Record<string, unknown> }>(disco.json, 'nodePackRuntimes')?.wasm;
|
|
65
66
|
|
|
66
|
-
if (!wasm?.supported) return;
|
|
67
|
+
if (!wasm?.supported) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!wasm?.supported` returned early');
|
|
67
68
|
|
|
68
69
|
if (!Array.isArray(wasm.loadedPacks)) {
|
|
69
70
|
// eslint-disable-next-line no-console
|
|
@@ -71,7 +72,7 @@ describe('wasm-pack-abi-version-rejection: positive path via misbehaving pack',
|
|
|
71
72
|
'[wasm-pack-abi-version-rejection] host does not advertise capabilities.nodePackRuntimes.wasm.loadedPacks[]; skipping positive path. ' +
|
|
72
73
|
'Hosts MAY advertise loadedPacks[] to surface ABI rejection observably (RFC 0008 §H + Track 7).',
|
|
73
74
|
);
|
|
74
|
-
return;
|
|
75
|
+
return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!Array.isArray(wasm.loadedPacks)` returned early');
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
// Soft-skip when the well-behaved reference pack isn't loaded. If
|
|
@@ -84,7 +85,7 @@ describe('wasm-pack-abi-version-rejection: positive path via misbehaving pack',
|
|
|
84
85
|
`[wasm-pack-abi-version-rejection] reference pack ${WELL_BEHAVED_PACK_NAME} not in loadedPacks[]; ` +
|
|
85
86
|
'skipping positive path (build examples/packs/rust-hello first or run against a host that ships it).',
|
|
86
87
|
);
|
|
87
|
-
return;
|
|
88
|
+
return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!wasm.loadedPacks.includes(WELL_BEHAVED_PACK_NAME)` returned early');
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
// Core assertion: ABI 999 pack MUST NOT be in loadedPacks[]
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { describe, it, expect } from 'vitest';
|
|
16
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
16
17
|
import { driver } from '../lib/driver.js';
|
|
17
18
|
import { pollUntilTerminal } from '../lib/polling.js';
|
|
18
19
|
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
@@ -29,11 +30,11 @@ async function isWasmSupported(): Promise<boolean> {
|
|
|
29
30
|
|
|
30
31
|
describe('wasm-pack-invoke-completed: round-trip output', () => {
|
|
31
32
|
it('greet node returns Hello, <name>! and run reaches completed', async () => {
|
|
32
|
-
if (!isFixtureAdvertised(FIXTURE)) return;
|
|
33
|
+
if (!isFixtureAdvertised(FIXTURE)) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!isFixtureAdvertised(FIXTURE)` returned early');
|
|
33
34
|
if (!(await isWasmSupported())) {
|
|
34
35
|
// eslint-disable-next-line no-console
|
|
35
36
|
console.warn('[wasm-pack-invoke-completed] WASM not advertised; skipping');
|
|
36
|
-
return;
|
|
37
|
+
return softSkip('inapplicable', '[wasm-pack-invoke-completed] WASM not advertised; skipping');
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
const create = await driver.post('/v1/runs', {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { describe, it, expect } from 'vitest';
|
|
20
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
20
21
|
import { driver } from '../lib/driver.js';
|
|
21
22
|
import { pollUntilTerminal } from '../lib/polling.js';
|
|
22
23
|
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
@@ -33,8 +34,8 @@ async function isWasmSupported(): Promise<boolean> {
|
|
|
33
34
|
|
|
34
35
|
describe('wasm-pack-invoke-suspended: suspend → resume round-trip', () => {
|
|
35
36
|
it('host either suspends the run or explicitly reports wasm_suspend_not_implemented', async () => {
|
|
36
|
-
if (!isFixtureAdvertised(FIXTURE)) return;
|
|
37
|
-
if (!(await isWasmSupported())) return;
|
|
37
|
+
if (!isFixtureAdvertised(FIXTURE)) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!isFixtureAdvertised(FIXTURE)` returned early');
|
|
38
|
+
if (!(await isWasmSupported())) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!(await isWasmSupported())` returned early');
|
|
38
39
|
|
|
39
40
|
// The reference rust-hello pack does NOT itself suspend (it always
|
|
40
41
|
// returns `completed`), so against that pack this scenario can only
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { describe, it, expect } from 'vitest';
|
|
16
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
16
17
|
import { driver } from '../lib/driver.js';
|
|
17
18
|
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
18
19
|
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
@@ -39,7 +40,7 @@ describe('wasm-pack-load: discovery surfaces WASM runtime support', () => {
|
|
|
39
40
|
if (!wasm?.supported) {
|
|
40
41
|
// eslint-disable-next-line no-console
|
|
41
42
|
console.warn('[wasm-pack-load] host does not advertise WASM support; skipping');
|
|
42
|
-
return;
|
|
43
|
+
return softSkip('inapplicable', '[wasm-pack-load] host does not advertise WASM support; skipping');
|
|
43
44
|
}
|
|
44
45
|
expect(Array.isArray(wasm.abiVersions), driver.describe(
|
|
45
46
|
'RFCS/0008-wasm-abi.md §H',
|
|
@@ -55,11 +56,11 @@ describe('wasm-pack-load: discovery surfaces WASM runtime support', () => {
|
|
|
55
56
|
describe('wasm-pack-load: loaded pack typeIds are dispatchable', () => {
|
|
56
57
|
it('host accepts a workflow whose node typeId is provided by a loaded WASM pack', async () => {
|
|
57
58
|
const wasm = await getWasmCaps();
|
|
58
|
-
if (!wasm?.supported) return;
|
|
59
|
+
if (!wasm?.supported) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!wasm?.supported` returned early');
|
|
59
60
|
if (!isFixtureAdvertised(FIXTURE)) {
|
|
60
61
|
// eslint-disable-next-line no-console
|
|
61
62
|
console.warn(`[wasm-pack-load] fixture ${FIXTURE} not advertised; skipping`);
|
|
62
|
-
return;
|
|
63
|
+
return softSkip('inapplicable', '[wasm-pack-load] fixture … not advertised; skipping');
|
|
63
64
|
}
|
|
64
65
|
// Creating a run against the fixture proves the host knows about the
|
|
65
66
|
// WASM-provided typeId. A host that loaded the pack accepts the
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import { describe, it, expect } from 'vitest';
|
|
26
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
26
27
|
import { driver } from '../lib/driver.js';
|
|
27
28
|
import { pollUntilTerminal } from '../lib/polling.js';
|
|
28
29
|
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
@@ -36,7 +37,7 @@ describe('wasm-pack-memory-cap: host advertises maxMemoryBytes', () => {
|
|
|
36
37
|
const wasm =
|
|
37
38
|
capabilityFamily<{ wasm?: Record<string, unknown> }>(disco.json, 'nodePackRuntimes')?.wasm;
|
|
38
39
|
|
|
39
|
-
if (!wasm?.supported) return;
|
|
40
|
+
if (!wasm?.supported) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!wasm?.supported` returned early');
|
|
40
41
|
|
|
41
42
|
// The cap is REQUIRED to enforce §K. Hosts MUST surface the configured
|
|
42
43
|
// ceiling so clients can size their packs accordingly.
|
|
@@ -59,12 +60,12 @@ describe('wasm-pack-memory-cap: positive path via misbehaving pack', () => {
|
|
|
59
60
|
`[wasm-pack-memory-cap] fixture ${CAP_BREACH_FIXTURE} not advertised; skipping positive path. ` +
|
|
60
61
|
'Build the misbehaving pack at examples/packs/rust-misbehaving-memory/ and ensure the host serves the fixture.',
|
|
61
62
|
);
|
|
62
|
-
return;
|
|
63
|
+
return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!isFixtureAdvertised(CAP_BREACH_FIXTURE)` returned early');
|
|
63
64
|
}
|
|
64
65
|
const disco = await driver.get('/.well-known/openwop');
|
|
65
66
|
const wasm =
|
|
66
67
|
capabilityFamily<{ wasm?: Record<string, unknown> }>(disco.json, 'nodePackRuntimes')?.wasm;
|
|
67
|
-
if (!wasm?.supported) return;
|
|
68
|
+
if (!wasm?.supported) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!wasm?.supported` returned early');
|
|
68
69
|
|
|
69
70
|
const create = await driver.post('/v1/runs', {
|
|
70
71
|
workflowId: CAP_BREACH_FIXTURE,
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { describe, it, expect } from 'vitest';
|
|
14
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
14
15
|
import { driver } from '../lib/driver.js';
|
|
15
16
|
import { pollUntilTerminal } from '../lib/polling.js';
|
|
16
17
|
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
@@ -33,8 +34,8 @@ function extractGreeting(events: Array<{ type: string; data?: unknown }>): strin
|
|
|
33
34
|
|
|
34
35
|
describe('wasm-pack-replay-determinism: same inputs → same output', () => {
|
|
35
36
|
it('two independent runs with same inputs produce same WASM output', async () => {
|
|
36
|
-
if (!isFixtureAdvertised(FIXTURE)) return;
|
|
37
|
-
if (!(await isWasmSupported())) return;
|
|
37
|
+
if (!isFixtureAdvertised(FIXTURE)) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!isFixtureAdvertised(FIXTURE)` returned early');
|
|
38
|
+
if (!(await isWasmSupported())) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!(await isWasmSupported())` returned early');
|
|
38
39
|
|
|
39
40
|
const inputs = { name: 'determinism-probe' };
|
|
40
41
|
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import { afterEach, describe, expect, it } from 'vitest';
|
|
27
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
27
28
|
import { createHmac } from 'node:crypto';
|
|
28
29
|
import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';
|
|
29
30
|
import { driver } from '../lib/driver.js';
|
|
@@ -78,12 +79,12 @@ describe('webhook-signed-delivery: end-to-end HMAC v1', () => {
|
|
|
78
79
|
if (!(await isWebhookSupported())) {
|
|
79
80
|
// eslint-disable-next-line no-console
|
|
80
81
|
console.warn('[webhook-signed-delivery] host does not advertise webhook support; skipping');
|
|
81
|
-
return;
|
|
82
|
+
return softSkip('inapplicable', '[webhook-signed-delivery] host does not advertise webhook support; skipping');
|
|
82
83
|
}
|
|
83
84
|
if (!isFixtureAdvertised('conformance-noop')) {
|
|
84
85
|
// eslint-disable-next-line no-console
|
|
85
86
|
console.warn('[webhook-signed-delivery] conformance-noop not advertised; skipping');
|
|
86
|
-
return;
|
|
87
|
+
return softSkip('inapplicable', '[webhook-signed-delivery] conformance-noop not advertised; skipping');
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
const receiver = await startReceiver();
|
|
@@ -114,7 +115,7 @@ describe('webhook-signed-delivery: end-to-end HMAC v1', () => {
|
|
|
114
115
|
'[webhook-signed-delivery] host SSRF guard rejected the loopback receiver; ' +
|
|
115
116
|
'set OPENWOP_WEBHOOK_ALLOW_PRIVATE=true on the host (or equivalent) to run',
|
|
116
117
|
);
|
|
117
|
-
return;
|
|
118
|
+
return softSkip('blocked', 'precondition not met — `body.error === \'webhook_url_rejected\'` returned early (seam, prior step, or fixture unavailable)');
|
|
118
119
|
}
|
|
119
120
|
}
|
|
120
121
|
|
|
@@ -20,35 +20,25 @@
|
|
|
20
20
|
import { describe, it, expect } from 'vitest';
|
|
21
21
|
import Ajv2020 from 'ajv/dist/2020.js';
|
|
22
22
|
import addFormats from 'ajv-formats';
|
|
23
|
-
import { readFileSync } from 'node:fs';
|
|
23
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
24
24
|
import { join } from 'node:path';
|
|
25
25
|
import { SCHEMAS_DIR } from '../lib/paths.js';
|
|
26
26
|
|
|
27
27
|
function compileWorkflowDefinition(): ReturnType<Ajv2020['compile']> {
|
|
28
28
|
const ajv = new Ajv2020({ strict: false, allErrors: true });
|
|
29
29
|
addFormats(ajv);
|
|
30
|
-
// Register
|
|
31
|
-
//
|
|
32
|
-
// `
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
) as Record<string, unknown>;
|
|
43
|
-
ajv.addSchema(agentRefSchema, 'agent-ref.schema.json');
|
|
44
|
-
ajv.addSchema(promptRefSchema, 'prompt-ref.schema.json');
|
|
45
|
-
ajv.addSchema(promptRefSchema, './prompt-ref.schema.json');
|
|
46
|
-
ajv.addSchema(promptKindSchema, 'prompt-kind.schema.json');
|
|
47
|
-
ajv.addSchema(promptKindSchema, './prompt-kind.schema.json');
|
|
48
|
-
const schema = JSON.parse(
|
|
49
|
-
readFileSync(join(SCHEMAS_DIR, 'workflow-definition.schema.json'), 'utf8'),
|
|
50
|
-
) as Record<string, unknown>;
|
|
51
|
-
return ajv.compile(schema);
|
|
30
|
+
// Register EVERY schema in the corpus (S14, 2026-08-16) — a fixed ref list is
|
|
31
|
+
// a claim about the schema graph that nothing keeps true; the fourth cross-file
|
|
32
|
+
// `$ref` (#1009) broke the previous list at describe time.
|
|
33
|
+
for (const file of readdirSync(SCHEMAS_DIR).filter((f) => f.endsWith('.schema.json'))) {
|
|
34
|
+
const s = JSON.parse(readFileSync(join(SCHEMAS_DIR, file), 'utf8')) as Record<string, unknown>;
|
|
35
|
+
ajv.addSchema(s, file);
|
|
36
|
+
ajv.addSchema(s, `./${file}`);
|
|
37
|
+
}
|
|
38
|
+
return (
|
|
39
|
+
ajv.getSchema('workflow-definition.schema.json') ??
|
|
40
|
+
ajv.compile(JSON.parse(readFileSync(join(SCHEMAS_DIR, 'workflow-definition.schema.json'), 'utf8')) as Record<string, unknown>)
|
|
41
|
+
);
|
|
52
42
|
}
|
|
53
43
|
|
|
54
44
|
/** Build the minimal-required shape of a WorkflowDefinition. Tests
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* @see SECURITY/invariants.yaml workspace-cross-tenant-isolation
|
|
25
25
|
*/
|
|
26
26
|
import { describe, it, expect } from 'vitest';
|
|
27
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
27
28
|
import { randomUUID } from 'node:crypto';
|
|
28
29
|
import { driver } from '../lib/driver.js';
|
|
29
30
|
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
@@ -42,11 +43,11 @@ const asTenantB = { headers: { Authorization: `Bearer ${tenantBKey ?? ''}` } };
|
|
|
42
43
|
|
|
43
44
|
describe('workspace-cross-tenant-isolation (black-box): a §C file MUST NOT leak across owners (RFC 0059 §E WCT-1)', () => {
|
|
44
45
|
it('a file written by owner A is unreadable + un-enumerable by a second-tenant credential', async () => {
|
|
45
|
-
if (!(await workspaceSupported())) return
|
|
46
|
+
if (!(await workspaceSupported())) return softSkip('inapplicable', 'capability not advertised — skip');
|
|
46
47
|
if (!tenantBKey) {
|
|
47
48
|
// eslint-disable-next-line no-console
|
|
48
49
|
console.warn('[workspace-cross-tenant-isolation-blackbox] OPENWOP_TEST_TENANT_B_API_KEY not supplied; skipping the production-path cross-tenant assertion');
|
|
49
|
-
return;
|
|
50
|
+
return softSkip('blocked', '[workspace-cross-tenant-isolation-blackbox] OPENWOP_TEST_TENANT_B_API_KEY not supplied; skipping the production-path cross-tenant assertion');
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
const path = `wct-blackbox-${randomUUID()}.md`;
|