@openwop/openwop-conformance 2.45.1 → 2.45.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +2 -2
  3. package/dist/cli.js +1 -1
  4. package/dist/lib/certification-bundle-verify.js +22 -1
  5. package/dist/lib/profiles.js +17 -2
  6. package/dist/lib/requirement-registry.js +15 -0
  7. package/dist/lib/scenario-disposition.js +50 -9
  8. package/dist/spec-artifacts.lock.json +2 -2
  9. package/fixtures/conformance-fs-probe.json +25 -0
  10. package/fixtures/conformance-queue-consume.json +27 -0
  11. package/fixtures/conformance-queue-publish.json +29 -0
  12. package/fixtures/conformance-safefetch-probe.json +23 -0
  13. package/fixtures/conformance-secret-resolve-then-fail.json +33 -0
  14. package/fixtures/conformance-storage-probe.json +27 -0
  15. package/fixtures/conformance-tool-scope-probe.json +27 -0
  16. package/fixtures/openwop-secrets-run-witness.json +29 -0
  17. package/fixtures.md +102 -1
  18. package/package.json +2 -2
  19. package/requirement-aliases.json +72 -71
  20. package/requirements.json +1017 -49
  21. package/scenario-majors.json +39 -3
  22. package/schemas/CORPUS-STAMP.json +22 -22
  23. package/src/cli.ts +1 -1
  24. package/src/lib/certification-bundle-verify.ts +23 -2
  25. package/src/lib/driver.ts +61 -2
  26. package/src/lib/memoryAttribution.ts +41 -6
  27. package/src/lib/polling.ts +2 -18
  28. package/src/lib/profiles.ts +26 -2
  29. package/src/lib/requirement-registry.ts +12 -0
  30. package/src/lib/run-secrets-witness.ts +240 -0
  31. package/src/lib/scenario-disposition.ts +45 -7
  32. package/src/lib/secret-scan.ts +141 -0
  33. package/src/lib/timeout-scale.ts +25 -0
  34. package/src/lib/triggerBridge.ts +69 -1
  35. package/src/scenarios/byok-roundtrip.test.ts +20 -4
  36. package/src/scenarios/runner-ledger.test.ts +6 -1
  37. package/src/scenarios/secrets-run-witness.test.ts +154 -0
  38. package/src/scenarios/trigger-bridge-delivery.test.ts +183 -126
  39. package/src/scenarios/trigger-refused-event-keeps-subscription.test.ts +141 -0
  40. package/src/scenarios/v2-error-registry.test.ts +7 -10
  41. package/src/scenarios/v2-eval-mode-unadvertised-refused.test.ts +55 -0
  42. package/src/scenarios/v2-fs-sandbox-escape-refused.test.ts +161 -0
  43. package/src/scenarios/v2-memory-cross-tenant-isolation.test.ts +113 -0
  44. package/src/scenarios/v2-queue-cross-tenant-isolation.test.ts +118 -0
  45. package/src/scenarios/v2-safefetch-ssrf-refused.test.ts +198 -0
  46. package/src/scenarios/v2-secret-canary-absent.test.ts +211 -0
  47. package/src/scenarios/v2-secrets-run-witness.test.ts +167 -0
  48. package/src/scenarios/v2-storage-cross-tenant-isolation.test.ts +213 -0
  49. package/src/scenarios/v2-tool-authorization-fail-closed.test.ts +197 -0
  50. package/src/scenarios/v2-workspace-scope-from-identity.test.ts +146 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,67 @@
1
1
  # `@openwop/openwop-conformance` Changelog
2
2
 
3
+ ## [2.45.3] — 2026-10-01 — RFC 0229 (Active): a production host can witness secret resolution without an oracle; v2 tenant-isolation witnesses for storage, fs, memory, workspace, queues and secrets
4
+
5
+ - **Two self-checks that failed on every host are fixed before the cut.** Found by running the candidate on openwop-app at major 1 against 2.45.2: four rows went `executed-pass` → `executed-fail`, and both files failed the same way with no host.
6
+ - `host-callback-declaration`: `v2-tool-authorization-fail-closed` imports `lib/effect-receiver` and now exports `REQUIRES_HOST_CALLBACK`.
7
+ - `runner-ledger`: the derivation emits the any-of summary row for `openwop-secrets` whatever profile is claimed, as it does the prefix rows. The test's synthetic report holds neither member, so the row is `blocked`; the test now asserts that and excludes it from the claimed floor's pass count. The derivation is unchanged.
8
+ - The rest of that diff was as intended: no row newly `blocked`, the four `0083.trigger-delivery.*` rows and the any-of row `executed-pass`, `secrets-run-witness` and `trigger-refused-event-keeps-subscription` `inapplicable`.
9
+ - **The 2.45.3 cycle opens.** RFC 0229's gap register changes the packed `spec/v1/gaps.json`.
10
+ - **A lost response is `blocked`, not `executed-fail`** (#1829).
11
+ - **Bound.** `driver.request` gives each request 20 s (`REQUEST_TIMEOUT_MS`, scaled by `OPENWOP_POLL_TIMEOUT_SCALE`), below vitest's 30 s `testTimeout`. The body read is under the same bound. A caller's own `signal` replaces the bound, and its abort is rethrown as-is.
12
+ - **Named.** A timeout or a failed connection throws `TransportError` (`transport-loss: <METHOD> <origin+path> got no response …`). The query string is dropped from the message.
13
+ - **Recorded.** A test that fails on a transport loss before its first assertion records `blocked`; the file row follows when every failure in it is one and nothing was asserted. A loss after an assertion stays `executed-fail`, and an assertion failure is never excused.
14
+ - **Why.** On MyndHyve's 2.45.2 cut the runner's network dropped for about 9 s. `v2-mcp-client-results` hung to the harness timeout and recorded `executed-fail` with 0 assertions against a host that had answered in 2.6 ms. `blocked` still denies certification, so a flaky runner cannot certify; it can no longer convict the host.
15
+ - **Proof.** `src/lib/driver-transport-loss.test.ts` (10 cases): a local server that never answers yields a timeout `TransportError` well inside the bound; without the classification the same failure is `executed-fail`.
16
+ - **New scenario `v2-eval-mode-unadvertised-refused`** (major 2, unaided): a host that does not advertise `agents.evalSuite` answers `POST /runs {mode: "eval"}` with `422 capability_not_provided` (`runs.md` §Refusals; `openwop.requirement.runs.eval-mode-unadvertised-refused`). The eval scenarios ran at major 1 only, so nothing measured this at major 2. A host that advertises the facet records `inapplicable`. If a host wrongly accepts the create, the leg cancels the run before asserting.
17
+ - **Proof.** Passes on the v2 reference host with openwop-examples #143; fails on the host without it, which answered `capability_required`.
18
+ - **`trigger-bridge-delivery`: a normative-surface witness path (RFC 0230), and one requirement per leg.**
19
+ - **Split.** The single combined `it` becomes four, each with its own requirement id: `openwop.requirement.0083.trigger-delivery.{dedup,dead-letter,causation,runless-content-free}`. The old per-`it` id aliases to the dedup leg.
20
+ - **Seam path unchanged.** It stays the primary witness whenever the host serves the seams.
21
+ - **Normative-surface path.** When the seams are absent and the host advertises `triggerBridge.ingestion.inboundSigning: ["standard-webhooks-1"]`, legs 1–3 run it. The suite registers a webhook subscription, then POSTs Standard-Webhooks-signed bodies to its `ingestUrl` with no OpenWOP credential:
22
+ - dedup: 202, then `200 duplicate` with the same `runId`;
23
+ - dead-letter: a bad signature gets 401 with no run, the subscription stays `active` (RFC 0230 §C.1), and a signed post then gets 202;
24
+ - causation: `run.started.causationId` via the run's own event poll.
25
+ - **Run-less content-freeness** stays seam-only, since those events are on no run's log. It **stays in the floor**, which never narrows: a seam-free host records one honest red row instead of three.
26
+ - **Proof.** Measured on openwop-app with the RFC 0230 route on and the seams off: legs 1–3 pass in strict mode on the new path. Sabotaged host builds each fail only their own leg:
27
+ - a duplicate `webhook-id` starting a new run fails dedup;
28
+ - a bad signature accepted fails dead-letter;
29
+ - a refused post dead-lettering the subscription fails dead-letter;
30
+ - a missing `causationId` fails causation.
31
+ - **RFC 0229 `Active`** (the window is waived by steward override of RFC 0147 §A.6). Two new scenarios, `secrets-run-witness` (major 1) and `v2-secrets-run-witness` (major 2), share `lib/run-secrets-witness.ts`. Each draws a fresh 64-character value `C` per run, supplies it as `createRun.runSecrets` under `run:openwop-witness`, and runs the fixture `openwop-secrets-run-witness`. Four rows, the same ids at both majors:
32
+ - `openwop.requirement.secrets.run-witness-resolves`: `matched: true` for `C`, and `false` when `expectedSha256` names another value;
33
+ - `openwop.requirement.secrets.run-witness-redacted`: `C` is absent from the create answer, snapshot, polled and `debug`-mode events, the run list and the debug bundle where served. The scan reads raw text and covers every encoding RFC 0229 §F lists. It also covers the digest, wherever the suite did not send that digest itself;
34
+ - `openwop.requirement.secrets.run-witness-scope-bound`: a non-`run:` ref fails `credential_forbidden`; an unsupplied or earlier run's `run:` ref fails `credential_not_found`; a branch fork, where served, fails `credential_not_found`;
35
+ - `openwop.requirement.secrets.run-secrets-outside-request-digest`: a same-key retry differing only in `runSecrets` replays with the original `runId` and `OpenWOP-Idempotent-Replay: true`, is never `409 idempotency_key_mismatch`, and its value appears nowhere.
36
+
37
+ **Dispositions.** Facet absent: `inapplicable`. Facet without the fixture: `blocked`. A refused well-formed `runSecrets`: `executed-fail`.
38
+
39
+ **Sabotage.** Each row failed against a scratch host with its defect: a witness that resolves a stored secret by a non-`run:` name, an echoed value, an emitted digest, a digest-covering idempotency record, a fork that inherits, and a cross-run fallback. The clean host passed all four rows at both majors.
40
+ - **The `openwop-secrets` floor is an any-of group** (`requiredAnyOf`, RFC 0229 §E). It is satisfied by a witnessed pass of `byok-roundtrip` or `secrets-run-witness` with no member failing, and never by an `inapplicable` one. The runner writes one summary row, `openwop.floor.anyof.byok-roundtrip+secrets-run-witness`, and both bundle verifiers and the `--certify` witness count read it. `src/lib/floor-any-of.test.ts` pins this.
41
+ - `byok-roundtrip` records `inapplicable` instead of `blocked` when the canary fixture is withheld and the host advertises `secrets.runSecrets` and the witness fixture. A host that takes the other path is not denied the bundle over a canary it does not serve.
42
+ - Otherwise `byok-roundtrip` is unchanged.
43
+ - **Host impact: none today.** No committed bundle and neither live production host advertises `secrets.runSecrets`, so both new files record `inapplicable` everywhere, and `byok-roundtrip` keeps its current disposition.
44
+ - **New v2 tenant-isolation witnesses** from `docs/V2-WITNESS-COVERAGE.md`, each sabotage-proved against a host with the exact defect: `v2-storage-cross-tenant-isolation` (eight families, one id each), `v2-fs-sandbox-escape-refused` (absolute, `..`, symlink), `v2-memory-cross-tenant-isolation`, `v2-workspace-scope-from-identity`, `v2-queue-cross-tenant-isolation`, and `v2-secret-canary-absent` (a host-resolved secret on no readable surface, including after a failure and in a fork). Each is `inapplicable` where its family is not advertised; MyndHyve runs the memory and secret-canary legs.
45
+ - **`memory-attribution-replay-stable` runs on v2 hosts.** `emitsWriteEvents()` (`lib/memoryAttribution.ts`) required `attribution.supported === true`, a field a v2 family record does not have (presence is the claim, RFC 0169 §A.2), so the file recorded `inapplicable` on every v2 host, including the two that advertise `memory.attribution.emitsWriteEvents: true`. At major 2 it now reads `emitsWriteEvents` on the present record; major 1 still requires `supported`. `memoryWrittenEvents()` read `GET /runs/{runId}/events` at major 2, which is the SSE stream, so once the gate opened every leg would have recorded `blocked` ("run wrote no memory"). It now reads `GET /runs/{runId}/events/poll` and pages on `afterSequence`. No other `conformance/src/lib` helper that a major-2 scenario imports has the same bug (`toolCatalog`, `context-budget` and `run-secrets-witness` were already major-aware). Sabotage: against a loopback v2 stub, the clean host passes, and a replay that re-mints ids, one that suppresses them, and one that adds a fresh id each fail. A host without the facet records `inapplicable`. Self-test: `lib/memoryAttribution.test.ts`.
46
+ - **New scenario `trigger-refused-event-keeps-subscription` (major 1), for the §F.2 Class 3 correction (RFC 0230 risk R3).** Row `openwop.requirement.trigger-bridge.refused-event-keeps-subscription`. The row is gated on the `openwop-trigger-bridge` profile and on `webhook` in `triggerBridge.ingestion.externalSources`. It drives the delivery seam's new OPTIONAL `scenario: "refused"`, then `deliver` on the same `subscriptionId`, and asserts three things:
47
+ - the refused event is a dead-lettered `trigger.delivery.attempted` and starts no run;
48
+ - no `trigger.subscription.state.changed` is emitted, and the subscription reads back `active`;
49
+ - the next verified event on that subscription is delivered.
50
+
51
+ **Dispositions.** Seam without `refused` (`400`): `blocked` (fails under strict mode). No `webhook` ingestion: `inapplicable`.
52
+
53
+ **Sabotage.** Proved against a loopback stub. The corrected host passes (default and strict). Each defective variant fails: the old subscription-level transition with its `state.changed`; the same transition with no event (the read shows `dead-lettered`); a refused event that starts a run. **Host impact:** trigger scenarios are major-1 only and no committed bundle carries one; a major-1 host that derives the profile with `webhook` ingestion records `blocked` until its seam serves `refused`.
54
+
55
+ ## [2.45.2] — 2026-09-30 — a run-failure code must be registered or a vendor code (no longer advisory)
56
+
57
+ - **`errors.event-code-registered` is required** (#1698). The leg in `v2-error-registry` shipped ADVISORY in 2.43.1: an unregistered, non-vendor code on `run.failed`, `node.failed` or the snapshot's `error` recorded a partial witness naming the codes, while hosts remapped. That branch is removed, so such a code now fails the row, which names each `where=code`.
58
+ - **Evidence.** All three hosts' latest certified bundles record it as a clean `executed-pass` (2 assertions, no partial-witness detail), each verified under the key its host serves:
59
+ - the v2 reference host at 2.44.6 (witness `188a23d56886`);
60
+ - MyndHyve at 2.44.3 (`58da9407e616`);
61
+ - openwop-app at 2.44.5 (build `ab77c3aa`, `cb16716e1be6`).
62
+ - **Negative control.** A stub host whose failed run carries `totally_unregistered_code` passed the advisory leg and now fails it. A vendor code (`example.conformance_failure`) still passes.
63
+ - **Ids.** The title drops "(advisory in 2.43.1)". The per-`it` id is unchanged, since the slug is capped before those words.
64
+
3
65
  ## [2.45.1] — 2026-09-29 — the run-list legs stop walking once they find their runs, and no scenario skips every test at describe level
4
66
 
5
67
  - **No scenario file skips every test at describe level any more, and the guard is a hard rule** (patch; no requirement added or removed; the gates are identical, and only their placement moves).
package/README.md CHANGED
@@ -135,7 +135,7 @@ Exit code is non-zero on any failed assertion. `--certify` distinguishes: `0`
135
135
 
136
136
  ## What's Covered
137
137
 
138
- The current suite has 575 scenario files under `src/scenarios/`.
138
+ The current suite has 587 scenario files under `src/scenarios/`.
139
139
  - 2026-09-29 (suite 2.45.0, openwop#1763): NEW `v2-cors-preflight.test.ts` — `headers.md` §Cross-origin preflight. For every operation it preflights with `Origin`, the method, and the operation's declared request headers (plus `authorization` / `content-type` where they apply, from `spec/v2/path-manifest.json`), and reads the answer as the Fetch standard's CORS-preflight check does. Self-gated: `inapplicable` when no operation's preflight grants `OPENWOP_CORS_ORIGIN` (default `https://conformance.invalid`).
140
140
  - 2026-09-23 (suite 2.37.0 cycle, RFC 0213): NEW `v2-sse-last-event-id-cursor.test.ts` (a `Last-Event-ID` past the log is an exclusive cursor; a malformed id, when refused, is `400 validation_error`; the cursor never changes the answer for an unknown or foreign-tenant run — public test of `event-cursor-after-authorization`), `v2-idempotency-in-flight.test.ts` (five concurrent same-key creates yield one run; each loser is a marked replay or `409 idempotency_in_flight` with no retry timing in `details`; `partial-witness` when no loser was refused in flight) and `v2-interrupt-resolve-terminal.test.ts` (a run-scoped resolve after cancel or completion is `409 interrupt_already_resolved`, never `interrupt_cancelled`). All three sit off the core-standard floor until measured on the three bundle hosts.
141
141
  - 2026-09-03 (suite `1.157.0 -> 1.158.0`, gap G17): NEW `idempotency-concurrent-claim.test.ts` — drives the new `host-sample-test-seams.md` §25 concurrent duplicate-delivery seam for the RFC 0150 §B / `idempotency.md` §"Concurrent duplicates (Layer 2)" atomic-claim MUST, which is unconditional and had no witness of any kind. Asserts every executor mints the SAME `logicalInvocationId` **before** asserting `delivered === 1` — without the identity check a host passes by minting different ids and never colliding, one effect because nothing raced. Not profile-gated and so not opt-out-able (the obligation is unconditional); an unmounted seam records `blocked`, which is not certifiable. Graduates `layer2-invocation-claim-atomic` reference-impl -> protocol.
@@ -482,7 +482,7 @@ Server-required (added in 1.7.0):
482
482
  | ------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
483
483
  | **Redaction** | [`capabilities.md`](../spec/v1/capabilities.md) §"Secrets" + NFR-7 + §"aiProviders" | Vendor-neutral assertions that the server doesn't leak secret material. Three scenario groups: (a) discovery shape contract — `secrets` + `aiProviders` advertisements are well-formed regardless of `secrets.supported`; when `supported === true`, scopes MUST be non-empty + `resolution === 'host-managed'`; `byok ⊆ supported`. (b) bearer-token redaction — invalid Bearer canary in `Authorization` header is not echoed in the 401 response body. (c) credentialRef echo control — gated on `secrets.supported === true`; canary planted in `configurable.ai.credentialRef` MUST NOT appear in any RunEvent payload (poll-based capture; transport-agnostic). Uses runtime-built canary fixtures (`lib/canaries.ts`) that defeat static secret scanners. 6 scenarios. |
484
484
 
485
- Current source tree: 575 scenario files. Use [`coverage.md`](./coverage.md) for current grade/gap tracking.
485
+ Current source tree: 587 scenario files. Use [`coverage.md`](./coverage.md) for current grade/gap tracking.
486
486
 
487
487
  ## Remaining Gaps
488
488
 
package/dist/cli.js CHANGED
@@ -646,7 +646,7 @@ async function runCertify(args, baseUrl, apiKey) {
646
646
  const floor = PROFILE_FLOOR_SCENARIOS[profile];
647
647
  if (!floor)
648
648
  return 0;
649
- const onFloor = (scenario) => floor.required.includes(scenario) || (floor.requiredAnyPrefix ?? []).some((pre) => scenario.startsWith(pre));
649
+ const onFloor = (scenario) => floor.required.includes(scenario) || (floor.requiredAnyPrefix ?? []).some((pre) => scenario.startsWith(pre)) || (floor.requiredAnyOf ?? []).some((g) => g.includes(scenario));
650
650
  return derived.requirements.filter((r) => r.disposition === 'executed-pass' && onFloor(r.scenarioId)).length;
651
651
  };
652
652
  // `certified` IS the verdict (RFC 0148 §A; RFC 0168 §E.1 adds the bundle-wide
@@ -26,7 +26,7 @@
26
26
  import { createHash } from 'node:crypto';
27
27
  import { PROFILE_FLOOR_SCENARIOS, profileDerivable, } from './profiles.js';
28
28
  import { CERTIFIABLE, DISPOSITIONS } from './requirement-ledger.js';
29
- import { floorFilesFor, requirementIdForPrefix, requirementIdForScenario } from './requirement-registry.js';
29
+ import { floorFilesFor, requirementIdForAnyOf, requirementIdForPrefix, requirementIdForScenario } from './requirement-registry.js';
30
30
  import { UNCLASSIFIED_RETURN_DETAIL } from './soft-skip.js';
31
31
  /**
32
32
  * The BYOK conformance canary (`fixtures.md` §conformance-secrets-roundtrip;
@@ -84,6 +84,7 @@ function requiredFor(profile, document) {
84
84
  return {
85
85
  files: [...files],
86
86
  prefixes: [...(floor.requiredAnyPrefix ?? [])],
87
+ anyOf: [...(floor.requiredAnyOf ?? [])],
87
88
  discoveryOnly: floor.discoveryOnly === true,
88
89
  };
89
90
  }
@@ -192,6 +193,26 @@ export function verifyBundleV2(bundle) {
192
193
  if (!considered.some((r) => isWitnessedPass(r)))
193
194
  notCertifiable.push(id);
194
195
  }
196
+ for (const group of req.anyOf) {
197
+ // RFC 0229 §E: one requirement, satisfied by a WITNESSED pass of any
198
+ // member with no member failing. The emitter writes a summary row under
199
+ // the group id; member rows are read when it is absent.
200
+ const id = requirementIdForAnyOf(group);
201
+ required.push(id);
202
+ const summary = byId.get(id)?.[0];
203
+ const members = group.map((f) => byId.get(requirementIdForScenario(scenarioBasename(f)))?.[0]).filter((r) => r !== undefined);
204
+ if (summary === undefined && members.length === 0) {
205
+ profileRejections.push({ kind: 'unwitnessed-requirement', profile, requirementId: id, detail: `${id}: no alternative (${group.join(', ')}) recorded a row` });
206
+ continue;
207
+ }
208
+ const considered = summary !== undefined ? [summary] : members;
209
+ if (considered.some((r) => r.disposition === 'executed-pass' && !isWitnessedPass(r))) {
210
+ profileRejections.push({ kind: 'vacuous-pass', profile, requirementId: id, detail: `${id}: executed-pass with no witnessed assertion` });
211
+ continue;
212
+ }
213
+ if (considered.some((r) => r.disposition === 'executed-fail') || !considered.some((r) => isWitnessedPass(r)))
214
+ notCertifiable.push(id);
215
+ }
195
216
  const evidenceValid = profileRejections.length === 0;
196
217
  const certified = derivable && evidenceValid && notCertifiable.length === 0 && (req.discoveryOnly || required.length > 0);
197
218
  return { profile, derivable, floorUnspecified: false, required, rejections: profileRejections, notCertifiable, evidenceValid, certified };
@@ -462,6 +462,10 @@ export function hasProfile(c, profile) {
462
462
  return isTriggerBridge(c);
463
463
  }
464
464
  }
465
+ /** Every scenario FILE a floor names: `required`, every `conditional` branch, and every `requiredAnyOf` member. */
466
+ export function floorMemberFiles(floor) {
467
+ return [...floor.required, ...(floor.conditional ?? []).flatMap((c) => c.required), ...(floor.requiredAnyOf ?? []).flat()];
468
+ }
465
469
  export const PROFILE_FLOOR_SCENARIOS = {
466
470
  'openwop-core-standard': {
467
471
  required: [
@@ -531,7 +535,11 @@ export const PROFILE_FLOOR_SCENARIOS = {
531
535
  // `profiles.md` §`openwop-secrets`: credential resolution per `run-options.md`
532
536
  // §"Credential references" — the BYOK canary round-trip (`fixtures.md`
533
537
  // §conformance-secrets-roundtrip, SR-1) is the profile's proof.
534
- 'openwop-secrets': { required: ['byok-roundtrip.test.ts'] },
538
+ // RFC 0229 §E (2026-09-30): a SECOND floor path, as an any-of group. The
539
+ // canary fixture's node is an oracle over whatever its name reaches, so a
540
+ // production host rightly withholds it; the run-witness reads only a value
541
+ // the caller supplied with the same run and outputs only `matched`.
542
+ 'openwop-secrets': { required: [], requiredAnyOf: [['byok-roundtrip.test.ts', 'secrets-run-witness.test.ts']] },
535
543
  // `profiles.md` §`openwop-provider-policy`: the four-mode taxonomy shape and
536
544
  // its enforcement on the wire.
537
545
  'openwop-provider-policy': { required: ['policies.test.ts', 'providerPolicyEnforcement.test.ts'] },
@@ -650,9 +658,16 @@ export function verifyBundleProfile(bundle, profile) {
650
658
  }
651
659
  const missingFloor = requiredFiles.filter((r) => !passed.has(scenarioBasename(r)));
652
660
  const prefixOk = (floor.requiredAnyPrefix ?? []).every((p) => [...passed].some((s) => s.startsWith(p)));
661
+ // RFC 0229 §E any-of groups: a v1 bundle lists only what passed, so a group
662
+ // holds when one member is in `results.passed`; an unmet group is reported
663
+ // as missing, spelled as its alternatives.
664
+ const groups = floor.requiredAnyOf ?? [];
665
+ for (const g of groups)
666
+ if (!g.some((f) => passed.has(scenarioBasename(f))))
667
+ missingFloor.push(g.join(' | '));
653
668
  // A conditional floor none of whose branches matched requires nothing — that
654
669
  // is unprovable for a non-discovery-only profile, not proven.
655
- const evaluable = floor.discoveryOnly === true || requiredFiles.length > 0 || (floor.requiredAnyPrefix ?? []).length > 0;
670
+ const evaluable = floor.discoveryOnly === true || requiredFiles.length > 0 || (floor.requiredAnyPrefix ?? []).length > 0 || groups.length > 0;
656
671
  const floorProven = evaluable && missingFloor.length === 0 && prefixOk;
657
672
  return {
658
673
  profile,
@@ -32,6 +32,14 @@ export function requirementIdForScenario(scenarioFile) {
32
32
  export function requirementIdForPrefix(prefix) {
33
33
  return `openwop.floor.any.${prefix}`;
34
34
  }
35
+ /**
36
+ * Any-of groups (RFC 0229 §E) become one requirement:
37
+ * `['byok-roundtrip.test.ts', 'secrets-run-witness.test.ts']` →
38
+ * `openwop.floor.anyof.byok-roundtrip+secrets-run-witness`.
39
+ */
40
+ export function requirementIdForAnyOf(files) {
41
+ return `openwop.floor.anyof.${files.map((f) => f.replace(/\.test\.ts$/, '')).join('+')}`;
42
+ }
35
43
  /**
36
44
  * The requirement IDs a profile's certification rests on.
37
45
  *
@@ -121,6 +129,7 @@ export function requirementsFor(profile, document) {
121
129
  return [
122
130
  ...files.map(requirementIdForScenario),
123
131
  ...(floor.requiredAnyPrefix ?? []).map(requirementIdForPrefix),
132
+ ...(floor.requiredAnyOf ?? []).map(requirementIdForAnyOf),
124
133
  ];
125
134
  }
126
135
  /** Every registered requirement ID across every profile with a runtime floor. */
@@ -137,6 +146,12 @@ export function allRequirements() {
137
146
  ids.add(requirementIdForScenario(f));
138
147
  for (const p of floor.requiredAnyPrefix ?? [])
139
148
  ids.add(requirementIdForPrefix(p));
149
+ // an any-of group is one requirement, and each member file records under its own floor id
150
+ for (const g of floor.requiredAnyOf ?? []) {
151
+ ids.add(requirementIdForAnyOf(g));
152
+ for (const f of g)
153
+ ids.add(requirementIdForScenario(f));
154
+ }
140
155
  void profile;
141
156
  }
142
157
  return [...ids].sort();
@@ -26,24 +26,20 @@
26
26
  * host or a vitest subprocess.
27
27
  */
28
28
  import { scenarioFileOfItId } from './requirement-ids.js';
29
- import { PROFILE_FLOOR_SCENARIOS } from './profiles.js';
29
+ import { PROFILE_FLOOR_SCENARIOS, floorMemberFiles } from './profiles.js';
30
30
  import { targetMajor } from './seams.js';
31
31
  import { PKG_ROOT_PATH } from './paths.js';
32
32
  import { v2ProfileFloorFiles } from './requirement-registry.js';
33
- import { requirementIdForScenario, requirementIdForPrefix, requirementsFor, v2FloorsActive } from './requirement-registry.js';
33
+ import { requirementIdForScenario, requirementIdForPrefix, requirementIdForAnyOf, requirementsFor, v2FloorsActive } from './requirement-registry.js';
34
34
  import { UNCLASSIFIED_RETURN_DETAIL } from './soft-skip.js';
35
35
  import { SPEC_COHERENCE_SCENARIOS, SPEC_COHERENCE_DETAIL } from './spec-coherence.js';
36
36
  import { CERTIFIABLE } from './requirement-ledger.js';
37
37
  /** All scenario basenames that appear in some profile's runtime floor. */
38
38
  export function floorScenarioFiles() {
39
39
  const out = new Set();
40
- for (const floor of Object.values(PROFILE_FLOOR_SCENARIOS)) {
41
- for (const f of floor.required)
40
+ for (const floor of Object.values(PROFILE_FLOOR_SCENARIOS))
41
+ for (const f of floorMemberFiles(floor))
42
42
  out.add(f);
43
- for (const c of floor.conditional ?? [])
44
- for (const f of c.required)
45
- out.add(f);
46
- }
47
43
  // At target major 2 the floors come from the declaration, not the v1 hand
48
44
  // table. The ledger and --certify MUST agree on this set, or a floor file is
49
45
  // minted `openwop.scenario.*` and looked up as `openwop.floor.*` — which is
@@ -77,10 +73,13 @@ export function requirementIdForFile(basename) {
77
73
  * the thing that passed.
78
74
  */
79
75
  export const PARTIAL_WITNESS_PREFIX = 'partial-witness: ';
76
+ /** `driver.ts`'s `TRANSPORT_LOSS_PREFIX`, restated so this module stays free of the driver (a test pins the two equal). */
77
+ export const TRANSPORT_LOSS_MARK = 'transport-loss: ';
80
78
  /**
81
79
  * The per-`it` record (RFC 0148 §A at test granularity), as `setup.ts`
82
80
  * computes it in `afterEach`. Pure so a lib test can pin it:
83
81
  * - fail ⇒ executed-fail (detail = the first error message)
82
+ * - fail on a driver transport loss, 0 assertions ⇒ blocked (#1829: nothing was observed)
84
83
  * - pass with ≥ 1 assertion ⇒ executed-pass
85
84
  * - a behaviorGate entry journaled during the test ⇒ that gate's disposition
86
85
  * - pass with 0 assertions ⇒ the softSkip note written DURING THIS TEST
@@ -114,6 +113,15 @@ blockedStands = false,
114
113
  /** The `it` title, so a failed row says WHICH leg of the requirement failed. */
115
114
  testName) {
116
115
  if (state === 'fail') {
116
+ // #1829: the driver got no response (`TransportError`) before the test
117
+ // asserted anything. Nothing about the host was observed, so this is not a
118
+ // verdict on it. `blocked` still denies certification, so a flaky runner
119
+ // cannot certify; it just cannot convict the host either. A loss AFTER an
120
+ // assertion stays `executed-fail`: the test was mid-measurement, and a host
121
+ // that stops answering part-way is a finding.
122
+ if (assertionCalls === 0 && firstError !== undefined && firstError.includes(TRANSPORT_LOSS_MARK)) {
123
+ return { disposition: 'blocked', detail: firstError.slice(firstError.indexOf(TRANSPORT_LOSS_MARK), firstError.indexOf(TRANSPORT_LOSS_MARK) + 300) };
124
+ }
117
125
  const where = testName === undefined || testName.trim() === '' ? '' : ` in "${testName.slice(0, 120)}"`;
118
126
  return { disposition: 'executed-fail', detail: `the test executed and failed${where}: ${(firstError ?? 'no message').slice(0, 300)}` };
119
127
  }
@@ -198,6 +206,11 @@ export function failureDetail(failures, failedCount) {
198
206
  * the ONE disposition the file records. */
199
207
  export function fileDisposition(states, gateReason, assertionCount, failures = []) {
200
208
  const failed = states.filter((s) => s === 'fail').length;
209
+ // #1829, the file-level half of `resolveItRecord`'s rule: every failure was a
210
+ // driver transport loss and the file asserted nothing, so nothing was observed.
211
+ if (failed > 0 && assertionCount === 0 && failures.length === failed && failures.every((f) => f.message?.includes(TRANSPORT_LOSS_MARK) === true)) {
212
+ return { disposition: 'blocked', detail: `no response reached the suite in ${failed} test(s); first: ${failures[0].message.slice(failures[0].message.indexOf(TRANSPORT_LOSS_MARK)).slice(0, 300)}` };
213
+ }
201
214
  if (failed > 0)
202
215
  return { disposition: 'executed-fail', detail: failureDetail(failures, failed) };
203
216
  if (states.some((s) => s === 'pass')) {
@@ -381,6 +394,34 @@ document) {
381
394
  }
382
395
  rows.push(row);
383
396
  }
397
+ // Any-of requirements (RFC 0229 §E): one summary row per group, derived from
398
+ // its member files. Satisfied only by a WITNESSED pass (assertions > 0) with
399
+ // no member failing; `inapplicable`/`skipped` members never satisfy it. v1
400
+ // hand table only, like the prefix groups (the major-2 floors have none).
401
+ const groups = new Map();
402
+ if (!v2FloorsActive())
403
+ for (const floor of Object.values(PROFILE_FLOOR_SCENARIOS))
404
+ for (const g of floor.requiredAnyOf ?? [])
405
+ groups.set(requirementIdForAnyOf(g), g);
406
+ for (const [id, members] of [...groups.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
407
+ const scenarioId = `anyof:${members.join('|')}`;
408
+ const matching = members.map((f) => perFile.get(f)).filter((r) => r !== undefined);
409
+ const witnessed = matching.filter((r) => r.disposition === 'executed-pass' && (r.assertionCount ?? 0) > 0);
410
+ let row;
411
+ if (matching.some((r) => r.disposition === 'executed-fail')) {
412
+ row = { requirementId: id, scenarioId, disposition: 'executed-fail', detail: `an alternative failed: ${matching.filter((r) => r.disposition === 'executed-fail').map((r) => r.scenarioId).join(', ')}` };
413
+ }
414
+ else if (witnessed.length > 0) {
415
+ row = { requirementId: id, scenarioId, disposition: 'executed-pass', assertionCount: witnessed.reduce((n, r) => n + (r.assertionCount ?? 0), 0) };
416
+ }
417
+ else if (matching.length === 0) {
418
+ row = { requirementId: id, scenarioId, disposition: 'blocked', detail: `none of ${members.join(', ')} ran — unclassified return` };
419
+ }
420
+ else {
421
+ row = { requirementId: id, scenarioId, disposition: 'blocked', detail: `no alternative recorded a witnessed pass (${matching.map((r) => `${r.scenarioId}: ${r.disposition}`).join(', ')})` };
422
+ }
423
+ rows.push(row);
424
+ }
384
425
  // Per-`it` rows (suite 1.153.0): every ledger entry keyed `openwop.it.<file>.<slug>`
385
426
  // becomes its own bundle row, attributed to its scenario file. Additive — the
386
427
  // file-level and prefix rows above are unchanged, and the floors still key on
@@ -440,7 +481,7 @@ document) {
440
481
  const atMajor2 = v2FloorsActive();
441
482
  for (const id of ids) {
442
483
  const r = rowById.get(id);
443
- const fromLedger = byId.has(id) || (r !== undefined && r.scenarioId.endsWith('*'));
484
+ const fromLedger = byId.has(id) || (r !== undefined && (r.scenarioId.endsWith('*') || r.scenarioId.startsWith('anyof:')));
444
485
  if (r !== undefined && r.disposition === 'executed-pass' && (r.assertionCount ?? 0) > 0)
445
486
  witnessedPasses += 1;
446
487
  // Unclassified: no row, or a report-derived blocked (nothing recorded), or a
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "package": "@openwop/spec-artifacts",
3
- "version": "2.45.1",
4
- "stampSha256": "936dd4e3e65e05d7ee49e2507fe818d6adce84fce582c0f78ce7def2343caff6"
3
+ "version": "2.45.3",
4
+ "stampSha256": "2b7ce94326d2cb0fa88b311a7d6d89c0a0b68f03e66934ad49a19722904a75e7"
5
5
  }
@@ -0,0 +1,25 @@
1
+ {
2
+ "id": "conformance-fs-probe",
3
+ "name": "Conformance: v2 fs sandbox probe",
4
+ "version": "1.0",
5
+ "description": "Fixture for `v2-fs-sandbox-escape-refused.test.ts` (spec/v2/core/storage.md §`fs`). One node, `core.conformance.fs-probe` (conformance-RESERVED): a host that advertises `fs` maps it to a node that calls its OWN `ctx.fs` with the run's inputs and nothing else. `op: write` calls `write(path, utf8(content), \"text/plain\")` and completes with `outputs.result` = the resolved `{ path, sizeBytes }`; `op: read` calls `read(path)` and completes with `outputs.result = { content }`, the bytes decoded as UTF-8. The node passes `path` through UNCHANGED (no normalisation of its own, so the host's sandbox check is what is exercised). A rejection fails the node with the rejection's code and details, unchanged. Operator contract: `<sandboxRoot>/conformance/escape-link` is a symbolic link to a readable file OUTSIDE the root. A host that does not advertise `fs` MUST NOT advertise this fixture.",
6
+ "nodes": [
7
+ {
8
+ "id": "fs-probe",
9
+ "typeId": "core.conformance.fs-probe",
10
+ "name": "Probe ctx.fs",
11
+ "position": { "x": 0, "y": 0 },
12
+ "config": {},
13
+ "inputs": {}
14
+ }
15
+ ],
16
+ "edges": [],
17
+ "triggers": [{ "id": "manual", "type": "manual", "enabled": true }],
18
+ "variables": [
19
+ { "name": "op", "type": "string", "defaultValue": "read" },
20
+ { "name": "path", "type": "string" },
21
+ { "name": "content", "type": "string" }
22
+ ],
23
+ "metadata": { "tags": ["conformance", "storage", "fs", "security"] },
24
+ "settings": { "timeout": 0 }
25
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "id": "conformance-queue-consume",
3
+ "name": "Conformance: Queue Consume",
4
+ "version": "1.0",
5
+ "description": "host-services.md §queueBus — node 'consume' (conformance.queue.probe, action 'consume') calls ctx.queueBus.consume on `topic` under the run's own tenant for up to config.waitMs, acks every message it receives, and outputs { consumed: [<payload.message of each, in order>] } — [] when none arrived, never unset. Paired with conformance-queue-publish. See v2-queue-cross-tenant-isolation.test.ts.",
6
+ "nodes": [
7
+ {
8
+ "id": "consume",
9
+ "typeId": "conformance.queue.probe",
10
+ "name": "Consume",
11
+ "position": { "x": 0, "y": 0 },
12
+ "config": { "action": "consume", "waitMs": 1000 },
13
+ "inputs": {
14
+ "topic": { "type": "variable", "variableName": "topic" }
15
+ }
16
+ }
17
+ ],
18
+ "edges": [],
19
+ "triggers": [
20
+ { "id": "manual", "type": "manual", "enabled": true }
21
+ ],
22
+ "variables": [
23
+ { "name": "topic", "type": "string", "description": "Topic to consume from.", "required": true }
24
+ ],
25
+ "metadata": { "tags": ["conformance", "queue", "security"] },
26
+ "settings": { "timeout": 10000, "maxRetries": 0 }
27
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "id": "conformance-queue-publish",
3
+ "name": "Conformance: Queue Publish",
4
+ "version": "1.0",
5
+ "description": "host-services.md §queueBus — node 'publish' (conformance.queue.probe, action 'publish') calls ctx.queueBus.publish({ topic, payload: { message } }) under the run's own tenant and outputs { published: true }. Paired with conformance-queue-consume. See v2-queue-cross-tenant-isolation.test.ts.",
6
+ "nodes": [
7
+ {
8
+ "id": "publish",
9
+ "typeId": "conformance.queue.probe",
10
+ "name": "Publish",
11
+ "position": { "x": 0, "y": 0 },
12
+ "config": { "action": "publish" },
13
+ "inputs": {
14
+ "topic": { "type": "variable", "variableName": "topic" },
15
+ "message": { "type": "variable", "variableName": "message" }
16
+ }
17
+ }
18
+ ],
19
+ "edges": [],
20
+ "triggers": [
21
+ { "id": "manual", "type": "manual", "enabled": true }
22
+ ],
23
+ "variables": [
24
+ { "name": "topic", "type": "string", "description": "Topic to publish on.", "required": true },
25
+ { "name": "message", "type": "string", "description": "Opaque message string carried as payload.message.", "required": true }
26
+ ],
27
+ "metadata": { "tags": ["conformance", "queue", "security"] },
28
+ "settings": { "timeout": 10000, "maxRetries": 0 }
29
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "id": "conformance-safefetch-probe",
3
+ "name": "Conformance: v2 safeFetch SSRF probe",
4
+ "version": "1.0",
5
+ "description": "Fixture for `v2-safefetch-ssrf-refused.test.ts` (spec/v2/core/host-services.md §`httpClient`). One node, `core.conformance.safefetch-probe` (conformance-RESERVED): a host that advertises `httpClient.safeFetch` maps it to a node that calls its OWN `ctx.http.safeFetch(url)` with the run's `url` input, unchanged, and nothing else (method GET, no headers, no body). On success it completes with `outputs.result = { status }`, the response status only (never the body, so the fixture is not a read proxy). A rejection fails the node with the rejection's code and details, unchanged (a refused target is `egress_denied`, `details.reason: ssrf-blocked`; an unreachable one `upstream_unavailable`). A host that does not advertise `httpClient.safeFetch` MUST NOT advertise this fixture.",
6
+ "nodes": [
7
+ {
8
+ "id": "safefetch-probe",
9
+ "typeId": "core.conformance.safefetch-probe",
10
+ "name": "Probe ctx.http.safeFetch",
11
+ "position": { "x": 0, "y": 0 },
12
+ "config": {},
13
+ "inputs": {}
14
+ }
15
+ ],
16
+ "edges": [],
17
+ "triggers": [{ "id": "manual", "type": "manual", "enabled": true }],
18
+ "variables": [
19
+ { "name": "url", "type": "string" }
20
+ ],
21
+ "metadata": { "tags": ["conformance", "httpClient", "safeFetch", "ssrf", "security"] },
22
+ "settings": { "timeout": 0 }
23
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "id": "conformance-secret-resolve-then-fail",
3
+ "name": "Conformance: Secret Resolve Then Fail",
4
+ "version": "1.0",
5
+ "description": "host-services.md §secrets — node 'resolve-secret' (conformance.secret.echo) resolves the host-provisioned canary 'openwop-conformance-canary-secret' exactly as openwop-smoke-byok-roundtrip does and outputs only {secretSha256, secretLength}; node 'fail' (core.fail) then fails the run. The canary MUST appear on no readable surface of the failed run: snapshot, error, events, or any error envelope. See v2-secret-canary-absent.test.ts.",
6
+ "nodes": [
7
+ {
8
+ "id": "resolve-secret",
9
+ "typeId": "conformance.secret.echo",
10
+ "name": "Resolve Canary Secret",
11
+ "position": { "x": 0, "y": 0 },
12
+ "config": { "secretId": "openwop-conformance-canary-secret" },
13
+ "inputs": {}
14
+ },
15
+ {
16
+ "id": "fail",
17
+ "typeId": "core.fail",
18
+ "name": "Fail After Resolve",
19
+ "position": { "x": 200, "y": 0 },
20
+ "config": { "message": "Intentional failure after resolving the conformance canary" },
21
+ "inputs": {}
22
+ }
23
+ ],
24
+ "edges": [
25
+ { "id": "resolve-then-fail", "sourceNodeId": "resolve-secret", "targetNodeId": "fail" }
26
+ ],
27
+ "triggers": [
28
+ { "id": "manual", "type": "manual", "enabled": true }
29
+ ],
30
+ "variables": [],
31
+ "metadata": { "tags": ["conformance", "byok", "secrets", "security"] },
32
+ "settings": { "timeout": 10000, "maxRetries": 0 }
33
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "id": "conformance-storage-probe",
3
+ "name": "Conformance: v2 storage tenant-isolation probe",
4
+ "version": "1.0",
5
+ "description": "Fixture for `v2-storage-cross-tenant-isolation.test.ts` (spec/v2/core/storage.md §Shared rules, tenant isolation). One node, `core.conformance.storage-probe` (conformance-RESERVED): a host maps it to a node that calls its OWN storage service for the run's tenant, the one `family` names (kvStorage | tableStorage | blobStorage | cache | vectorStore | searchIndex | sql | nosql), with the run's inputs, and nothing else. `action: write` stores `value` under `key` and completes with `outputs.result = { written: true, ref? }`; `action: read` completes with `outputs.result = { values, keys }`, every value and every key the family's tenant-isolated read operations return for `key` (and for `ref`, when given); `action: delete` removes it. A rejection fails the node with the rejection's code, unchanged. The per-family mapping is in conformance/fixtures.md §\"The storage probe fixture\". A host MUST NOT advertise this fixture unless it implements it for every storage family it advertises.",
6
+ "nodes": [
7
+ {
8
+ "id": "storage-probe",
9
+ "typeId": "core.conformance.storage-probe",
10
+ "name": "Probe host storage",
11
+ "position": { "x": 0, "y": 0 },
12
+ "config": {},
13
+ "inputs": {}
14
+ }
15
+ ],
16
+ "edges": [],
17
+ "triggers": [{ "id": "manual", "type": "manual", "enabled": true }],
18
+ "variables": [
19
+ { "name": "family", "type": "string", "defaultValue": "kvStorage" },
20
+ { "name": "action", "type": "string", "defaultValue": "read" },
21
+ { "name": "key", "type": "string" },
22
+ { "name": "value", "type": "string" },
23
+ { "name": "ref", "type": "string" }
24
+ ],
25
+ "metadata": { "tags": ["conformance", "storage", "tenant-isolation"] },
26
+ "settings": { "timeout": 0 }
27
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "id": "conformance-tool-scope-probe",
3
+ "name": "Conformance: per-tool authorization probe",
4
+ "version": "1.0",
5
+ "description": "host-services.md §toolHooks (`perToolAuthorization`) — one node, `core.conformance.scoped-tool` (conformance-RESERVED): a host that advertises `toolHooks.perToolAuthorization` maps it to ONE external tool call, of the tool `conformance.scope-probe`, made through the host's ordinary tool-invocation path as the run's principal, with the tool's `requiredScopes` set to `[inputs.requiredScope]` (or `[]` when it is empty or absent). The tool, when invoked, POSTs `{}` to `inputs.url` and returns `{ invoked: true }`. The per-tool authorization check runs before it, as for any tool: a lacked or unevaluable scope MUST NOT invoke it, MUST emit `agent.tool-returned { status: forbidden }`, and fails the node with the refusal — `forbidden` with `details.scope: \"tool\"`, `toolName` and `requiredScopes` — unchanged. Operator contract: no principal holds `conformance:never-granted`, and evaluating `conformance:unevaluable` errors. See conformance/fixtures.md §`conformance-tool-scope-probe` and v2-tool-authorization-fail-closed.test.ts.",
6
+ "nodes": [
7
+ {
8
+ "id": "scoped-tool",
9
+ "typeId": "core.conformance.scoped-tool",
10
+ "name": "Call a scoped tool",
11
+ "position": { "x": 0, "y": 0 },
12
+ "config": { "toolName": "conformance.scope-probe" },
13
+ "inputs": {
14
+ "requiredScope": { "type": "variable", "variableName": "requiredScope" },
15
+ "url": { "type": "variable", "variableName": "url" }
16
+ }
17
+ }
18
+ ],
19
+ "edges": [],
20
+ "triggers": [{ "id": "manual", "type": "manual", "enabled": true }],
21
+ "variables": [
22
+ { "name": "requiredScope", "type": "string", "description": "The one scope the tool requires; empty for none (the positive control).", "defaultValue": "" },
23
+ { "name": "url", "type": "string", "description": "Where the tool POSTs when it is invoked — the suite's receiver.", "required": true }
24
+ ],
25
+ "metadata": { "tags": ["conformance", "toolHooks", "security"] },
26
+ "settings": { "timeout": 15000, "maxRetries": 0 }
27
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "id": "openwop-secrets-run-witness",
3
+ "name": "Conformance: Run-supplied secret witness",
4
+ "version": "1.0",
5
+ "description": "RFC 0229 §C. One node, 'witness' (typeId core.secret.witness), whose 'ref' and 'expectedSha256' inputs come from the run's inputs of the same names. The client supplies the value with the run (top-level runSecrets on createRun, under a reserved run: ref that resolves only within that run); the node compares the SHA-256 of the resolved value with expectedSha256 and outputs only { matched }. A ref without the run: prefix fails credential_forbidden without resolving anything; an unsupplied run: ref fails credential_not_found. It can only confirm a value the caller already holds, so it is safe to advertise in production. Advertised by every host that advertises secrets.runSecrets. Spec: host-services.md §Run-supplied secrets (v2), capabilities.md §Run-supplied secrets (v1).",
6
+ "nodes": [
7
+ {
8
+ "id": "witness",
9
+ "typeId": "core.secret.witness",
10
+ "name": "Witness Run Secret",
11
+ "position": { "x": 0, "y": 0 },
12
+ "config": {},
13
+ "inputs": {
14
+ "ref": { "type": "variable", "variableName": "ref" },
15
+ "expectedSha256": { "type": "variable", "variableName": "expectedSha256" }
16
+ }
17
+ }
18
+ ],
19
+ "edges": [],
20
+ "triggers": [
21
+ { "id": "manual", "type": "manual", "enabled": true }
22
+ ],
23
+ "variables": [
24
+ { "name": "ref", "type": "string", "required": true },
25
+ { "name": "expectedSha256", "type": "string", "required": true }
26
+ ],
27
+ "metadata": { "tags": ["conformance", "secrets", "run-witness"] },
28
+ "settings": { "timeout": 10000, "maxRetries": 0 }
29
+ }