@openwop/openwop-conformance 2.45.2 → 2.45.4

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 (56) hide show
  1. package/CHANGELOG.md +77 -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 +60 -9
  8. package/dist/spec-artifacts.lock.json +2 -2
  9. package/fixtures/conformance-budget-tool-calls.json +81 -0
  10. package/fixtures/conformance-fs-probe.json +25 -0
  11. package/fixtures/conformance-queue-consume.json +27 -0
  12. package/fixtures/conformance-queue-publish.json +29 -0
  13. package/fixtures/conformance-safefetch-probe.json +23 -0
  14. package/fixtures/conformance-secret-resolve-then-fail.json +33 -0
  15. package/fixtures/conformance-storage-probe.json +27 -0
  16. package/fixtures/conformance-tool-scope-probe.json +27 -0
  17. package/fixtures/openwop-secrets-run-witness.json +29 -0
  18. package/fixtures.md +113 -1
  19. package/package.json +2 -2
  20. package/requirement-aliases.json +72 -71
  21. package/requirements.json +1081 -43
  22. package/scenario-majors.json +45 -3
  23. package/schemas/CORPUS-STAMP.json +22 -22
  24. package/src/cli.ts +1 -1
  25. package/src/lib/backpressure-witness.ts +163 -0
  26. package/src/lib/budget-witness.ts +165 -0
  27. package/src/lib/certification-bundle-verify.ts +23 -2
  28. package/src/lib/driver.ts +61 -2
  29. package/src/lib/major-profile.ts +93 -0
  30. package/src/lib/memoryAttribution.ts +41 -6
  31. package/src/lib/polling.ts +2 -18
  32. package/src/lib/profiles.ts +26 -2
  33. package/src/lib/requirement-registry.ts +12 -0
  34. package/src/lib/run-secrets-witness.ts +240 -0
  35. package/src/lib/scenario-disposition.ts +54 -7
  36. package/src/lib/scratch-host.ts +130 -0
  37. package/src/lib/secret-scan.ts +141 -0
  38. package/src/lib/timeout-scale.ts +25 -0
  39. package/src/lib/triggerBridge.ts +69 -1
  40. package/src/scenarios/byok-roundtrip.test.ts +20 -4
  41. package/src/scenarios/runner-ledger.test.ts +2 -0
  42. package/src/scenarios/secrets-run-witness.test.ts +154 -0
  43. package/src/scenarios/trigger-bridge-delivery.test.ts +183 -126
  44. package/src/scenarios/trigger-refused-event-keeps-subscription.test.ts +141 -0
  45. package/src/scenarios/v2-budget-enforcement.test.ts +94 -0
  46. package/src/scenarios/v2-eval-mode-unadvertised-refused.test.ts +55 -0
  47. package/src/scenarios/v2-fs-sandbox-escape-refused.test.ts +161 -0
  48. package/src/scenarios/v2-memory-cross-tenant-isolation.test.ts +113 -0
  49. package/src/scenarios/v2-production-backpressure.test.ts +74 -0
  50. package/src/scenarios/v2-queue-cross-tenant-isolation.test.ts +118 -0
  51. package/src/scenarios/v2-safefetch-ssrf-refused.test.ts +198 -0
  52. package/src/scenarios/v2-secret-canary-absent.test.ts +211 -0
  53. package/src/scenarios/v2-secrets-run-witness.test.ts +167 -0
  54. package/src/scenarios/v2-storage-cross-tenant-isolation.test.ts +213 -0
  55. package/src/scenarios/v2-tool-authorization-fail-closed.test.ts +197 -0
  56. package/src/scenarios/v2-workspace-scope-from-identity.test.ts +146 -0
@@ -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
+ }
package/fixtures.md CHANGED
@@ -73,6 +73,7 @@ All fixtures MUST advertise:
73
73
  | Agent Identity | `conformance-agent-identity` | Phase 1 — `RunSnapshot.agent` / `runOrchestrator` AgentRef wire-shape | `completed` | ≤ 10s |
74
74
  | Agent Reasoning | `conformance-agent-reasoning` | Phase 1 / RFC 0023 — `agent.*` event family emission + `callId` pairing on `core.conformance.mock-agent` | `completed` | ≤ 15s |
75
75
  | Agent Reasoning Streaming | `conformance-agent-reasoning-streaming` | RFC 0024 — `core.conformance.mock-agent` with `mockReasoning.streamChunks` drives incremental `agent.reasoning.delta` events (sequence 0..N-1) followed by exactly one closing `agent.reasoned` whose `reasoning` equals the concatenation. Gated on `capabilities.agents.reasoning.streaming: true`. | `completed` | ≤ 15s |
76
+ | Budget (tool calls) | `conformance-budget-tool-calls` | `runs.md` §`budget` section — one `core.conformance.mock-agent` makes three scripted tool calls, so a run budget is spent with no model and no seam. Seeding it opts the host in to `v2-budget-enforcement` | `completed`; `failed` (`budget_exhausted`) under a hard budget below 3 calls | ≤ 15s |
76
77
  | Agent Low-Confidence | `conformance-agent-low-confidence` | Phase 1 / CP-1 / RFC 0023 — `core.conformance.mock-agent` emits `agent.decided` with confidence < threshold; host MUST follow with `node.suspended { reason: 'low-confidence' }` | `waiting-approval` (suspends) | unbounded (suspends) |
77
78
  | Message Reducer | `conformance-message-reducer` | Phase 1 — `message` reducer idempotency on duplicate `messageId` | `completed` | ≤ 10s |
78
79
  | Agent Pack Install | `conformance-agent-pack-install` | Phase 2 — pack `agents[]` surface as AgentManifest at `GET /v1/packs` | `completed` | ≤ 5s |
@@ -100,7 +101,7 @@ All fixtures MUST advertise:
100
101
  | Multi-Agent Handoff (child) | `conformance-multi-agent-handoff-child` | RFC 0037 (`version: 1`) — child for `conformance-multi-agent-handoff`. Declares `childOutcome.defaultValue='handoff-complete'`; the parent's outputMapping harvests it onto `parentResult`, triggering the `output.harvested` transition event. | `completed` | ≤ 5s |
101
102
  | Multi-Agent Confidence Escalation | `conformance-multi-agent-confidence-escalation` | RFC 0039 §A (`version: 2`) — exercises the confidence-floor escalation contract. Supervisor's `mockDispatchPlan` carries ONE decision with `confidence: 0.3` (below the 0.5 spec floor). The host MUST emit `core.workflowChain.confidence-escalated` AND suspend with a clarification interrupt BEFORE any dispatch.began fires; conformance asserts zero `core.workflowChain.event` records (no dispatch). Capability-gated on `capabilities.multiAgent.executionModel.version >= 2`. | `waiting-clarification` | ≤ 30s |
102
103
  | Agent Memory Round-Trip | `conformance-agent-memory-roundtrip` | Phase 3 — `MemoryAdapter.list/get` write → read | `completed` | ≤ 15s |
103
- | Agent Memory Cross-Tenant | `conformance-agent-memory-cross-tenant` | Phase 3 / CTI-1 — TWO-SIDED (S35, 2026-08-17): owner write+list lands `ownerEntryId` + non-empty `ownerProbe` (positive control); the cross-tenant probe at `config.probeMemoryRef` lands `crossTenantProbe`, which MUST be exactly `[]` / `null` and MUST be set | `completed` | ≤ 10s |
104
+ | Agent Memory Cross-Tenant | `conformance-agent-memory-cross-tenant` | Phase 3 / CTI-1 — TWO-SIDED (S35, 2026-08-17): owner write+list lands `ownerEntryId` + non-empty `ownerProbe` (positive control); the cross-tenant probe at `config.probeMemoryRef` lands `crossTenantProbe`, which MUST be exactly `[]` / `null` and MUST be set. At major 2, `v2-memory-cross-tenant-isolation` runs it once per tenant (`OPENWOP_API_KEY`, then `OPENWOP_TEST_TENANT_B_API_KEY`) at the same `memoryRef`: a host advertising it MUST run it for the second-tenant credential too, and entry ids MUST NOT repeat across tenants | `completed` | ≤ 10s |
104
105
  | Agent Memory Redaction | `conformance-agent-memory-redaction` | Phase 3 / SR-1 — BYOK plaintext surfaces as `[REDACTED:<id>]` on read | `completed` | ≤ 15s |
105
106
  | Agent Memory TTL | `conformance-agent-memory-ttl` | Phase 3 — `expiresAt` excludes expired entries from list/get; TWO-SIDED (S35, 2026-08-17): host lands `freshId` / `expiredId`, `memoryList` MUST include the fresh id and exclude the expired id | `completed` | ≤ 10s |
106
107
  | Agent Memory Injection Budget | `conformance-agent-memory-injection-budget` | RFC 0113 — token-budgeted `MemoryAdapter.list` (`tokenBudget`/`rank`/`query`): cumulative tokens ≤ budget, over-budget single entry omitted, SR-1 + CTI-1 re-asserted on the budgeted path; `rank:'relevance'` delegates to `memory.search` semantic | `completed` | ≤ 15s |
@@ -115,11 +116,19 @@ All fixtures MUST advertise:
115
116
  | Orchestrator Low-Confidence | `conformance-orchestrator-low-confidence` | Phase 5 / CP-1 — supervisor low-confidence suspend | `waiting-approval` (suspends) | unbounded (suspends) |
116
117
  | MCP Tool Roundtrip | `conformance-mcp-tool-roundtrip` | Track 6 — host invokes a tool on the conformance suite's synthetic MCP server; trust-boundary visibility in the event log. Node `core.conformance.mcp-invoke` is a **conformance-RESERVED** typeId (renamed 2026-08-17 from `core.ai.callPrompt` with a `config.mcp` shape that vendor pack-tier node never read — it requires `promptId` and ignores `config.mcp`, so the fixture could not execute as declared on the first host that shipped the vendor node): a host that consumes MCP MUST map it to its own MCP invoke bridge node and honour `config.mcp { tool, arguments }`; a host that does not consume MCP MUST NOT advertise this fixture (the host-mediated leg then records inapplicable) — the same treatment as `core.conformance.a2a-invoke` | `completed` | ≤ 30s |
117
118
  | v2 `ctx.mcp` Client | `conformance-mcp-client` | RFC 0204 — one `core.conformance.mcp-client` node (conformance-RESERVED) calls the host's `ctx.mcp.<method>` with the run's inputs and completes with `outputs.result` = the resolved value verbatim; a rejection fails the node with its code. See §"The `ctx.mcp` fixture (RFC 0204)" | `completed`, or `failed` (`not_found`) for an unknown `serverId` | ≤ 30s |
119
+ | v2 Storage Tenant-Isolation Probe | `conformance-storage-probe` | `storage.md` §Shared rules — one `core.conformance.storage-probe` node (conformance-RESERVED) calls the host's own storage `family` for the run's tenant: `write` stores `value` at `key`, `read` returns every value and key the family's isolated reads return for `key`, `delete` removes it. See §"The storage probe fixtures" | `completed`, or `failed` (`forbidden` / `not_found`) for a refused read | ≤ 30s |
120
+ | v2 fs Sandbox Probe | `conformance-fs-probe` | `storage.md` §`fs` — one `core.conformance.fs-probe` node (conformance-RESERVED) calls the host's own `ctx.fs.read` / `write` with the run's `path`, unchanged; a rejection fails the node with its code and details. Operator symlink `<sandboxRoot>/conformance/escape-link` points outside the root. See §"The storage probe fixtures" | `completed` inside the root; `failed` (`forbidden`, `details.reason: path-outside-sandbox`) for an escape | ≤ 30s |
121
+ | v2 safeFetch SSRF Probe | `conformance-safefetch-probe` | `host-services.md` §`httpClient` — one `core.conformance.safefetch-probe` node (conformance-RESERVED) calls the host's own `ctx.http.safeFetch(url)` with the run's `url`, unchanged; completes with the response `status` only; a rejection fails the node with its code and details. See §"The safeFetch probe fixture" | `failed` (`egress_denied`, `details.reason: ssrf-blocked`) for a refused target | ≤ 60s |
118
122
  | A2A Task Roundtrip | `conformance-a2a-task-roundtrip` | Track 6 — host consumes the conformance suite's synthetic A2A peer; covers drift points #3 (`AUTH_REQUIRED`) and #4 (`REJECTED`). Node `core.conformance.a2a-invoke` is a **conformance-RESERVED** typeId (renamed 2026-08-16 from `core.a2a.invoke`, a multi-segment `core.*` id no host shipped and the corpus never defined): a host that consumes A2A peers MUST map it to its A2A bridge node (a host-extension node — `a2a-integration.md` §"State projection"); a host that does not consume A2A MUST NOT advertise this fixture | `failed` or `waiting-input` (per `driftScenario` input) | ≤ 30s |
119
123
  | WASM Pack Roundtrip | `conformance-wasm-pack-roundtrip` | RFC 0008 — invokes `vendor.openwop.rust-hello.greet` (loaded WASM pack); exercises required exports + at least one import | `completed` | ≤ 10s |
120
124
  | WASM Pack Memory-Cap Breach | `conformance-wasm-pack-memory-cap-breach` | RFC 0008 §K — invokes the deliberately-misbehaving `vendor.openwop.misbehaving.memory-bomb` pack (allocates 1 GiB beyond the host's `memoryPagesMax`). Host MUST emit `cap.breached` with `kind: "wasm-memory"` and drive the run to terminal `failed`. Misbehaving pack lives at `openwop-examples:examples/packs/rust-misbehaving-memory/` (repo-qualified per the 2026-06 monorepo split — the `openwop-examples` sibling repo) and is fixture-only (NOT signed for registry publication). | `failed` (with `cap.breached`) | ≤ 10s |
121
125
  | Configurable Schema | `conformance-configurable-schema` | Track 13 — workflow declares `configurableSchema` (`additionalProperties: false`, `recursionLimit: integer ≥ 1`). Suite verifies `GET /v1/workflows/{id}` surfaces the schema AND `POST /v1/runs` with a mismatched `configurable` returns `validation_error`. | `completed` (with accepted overlay) | ≤ 5s |
122
126
  | Smoke — BYOK Roundtrip | `openwop-smoke-byok-roundtrip` | End-to-end BYOK secret-resolution smoke. Single `conformance.secret.echo` node fetches the host-provisioned canary secret `openwop-conformance-canary-secret`, emits SHA-256 hex + byte length to variables — never the raw value. Spec: `run-options.md` §"Credential references" + `auth.md` §"Secret resolution" + `observability.md` §"Redaction". | `completed` | ≤ 10s |
127
+ | Secrets — Run Witness | `openwop-secrets-run-witness` | RFC 0229 §C — one `core.secret.witness` node. The client supplies a value with the run (`runSecrets`, a `run:` ref) and its SHA-256 as an input; the node outputs only `{ matched }`. It can confirm only a value the caller already holds, so it is safe to advertise in production. Advertised by every host that advertises `secrets.runSecrets`. | `completed` (`matched: true` or `false`); `failed` (`credential_forbidden` / `credential_not_found`) on the scope legs | ≤ 10s |
128
+ | Secret — resolve then fail | `conformance-secret-resolve-then-fail` | `host-services.md` §`secrets` — resolves the byok canary exactly as `openwop-smoke-byok-roundtrip`, then `core.fail` fails the run; the canary MUST appear on no surface of the failed run | `failed` | ≤ 10s |
129
+ | Queue — publish | `conformance-queue-publish` | `host-services.md` §`queueBus` — `conformance.queue.probe` publishes `inputs.message` on `inputs.topic` under the run's tenant | `completed` | ≤ 10s |
130
+ | Queue — consume | `conformance-queue-consume` | `host-services.md` §`queueBus` — `conformance.queue.probe` consumes `inputs.topic` under the run's tenant for `config.waitMs`, acks, outputs `consumed` | `completed` | ≤ 10s |
131
+ | Tool — scope probe | `conformance-tool-scope-probe` | `host-services.md` §`toolHooks` — `core.conformance.scoped-tool` calls the tool `conformance.scope-probe` (which POSTs to `inputs.url`) as the run's principal, with `requiredScopes: [inputs.requiredScope]` | `completed` (no scope) / `failed` `forbidden` (lacked or unevaluable scope) | ≤ 15s |
123
132
  | Smoke — Cost Emit (G6 allowlist) | `openwop-smoke-cost-emit` | End-to-end cost-attribute allowlist smoke (G6 / O4). Single `conformance.cost.emit` node configured with a mix of allowlisted `openwop.cost.*` attributes + one non-allowlisted key + one credential-shaped canary under a non-allowlisted name. Scenario reads the live OTel span (when `OPENWOP_OTEL_COLLECTOR=true`) and asserts the cost-namespace attrs ⊆ `OPENWOP_COST_ATTRIBUTE_NAMES` AND that no canary plaintext leaks. Pairs with the `cost-attribution-allowlist-redaction` SECURITY invariant. Spec: `observability.md §"Cost attribution attributes"`. | `completed` | ≤ 10s |
124
133
  | Model Capability Insufficient | `conformance-model-capability-insufficient` | RFC 0031 §B step 4 + §D — single `conformance.modelCapability.insufficient` node whose NodeModule declares `requiredModelCapabilities: ['nonexistent-capability-9b3f']`. Executor's gate MUST refuse at dispatch with `error.code = "capability_not_provided"` and emit `model.capability.insufficient` BEFORE `node.failed`. Capability-gated on `capabilities.modelCapabilities.supported: true`. | `failed` (`error.code='capability_not_provided'`) | ≤ 5s |
125
134
  | Envelope Retry Attempted | `conformance-envelope-retry-attempted` | RFC 0032 §B.1 — single `core.ai.structuredOutput` node calls the conformance mock provider with a pre-seeded `MockProgram[]` (POSTed to `/v1/host/sample/test/mock-ai/program` by nodeId BEFORE run start). Attempt 1 returns invalid JSON; attempt 2 returns a valid envelope. Host's `dispatchStructured()` retry loop MUST emit exactly one `envelope.retry.attempted` event between the two attempts. Pairs with `envelope-retry-attempted.test.ts`. Capability-gated on `capabilities.envelopes.reliability.supported: true` AND `capabilities.testing.mockProviders` advertised. | `completed` | ≤ 10s |
@@ -363,6 +372,16 @@ The `messages`-mode stream fixture (AI token streaming) is covered by the determ
363
372
 
364
373
  ---
365
374
 
375
+
376
+ ### `conformance-budget-tool-calls`
377
+
378
+ - **Purpose**: spend a run budget unaided, so `budget` can be witnessed without a model or a seam (`v2-budget-enforcement`).
379
+ - **Shape**: one `core.conformance.mock-agent` node with three `mockToolCalls`. Each call counts once against the `toolCalls` dimension.
380
+ - **Inputs**: none.
381
+ - **Expected behavior**: created with `configurable.budget = { maxToolCalls: 2, thresholdPercent: 50, onExhaustion: "fail" }`, the run emits `budget.reserved`, then `budget.threshold-crossed`, then `budget.exhausted`. A host advertising `enforce: "hard"` then emits `cap.breached` (`kind: "budget-tool-calls"`) and fails the run `budget_exhausted`; an `advisory` host lets it complete.
382
+ - **Terminal status**: `completed` with no budget; `failed` under a hard budget below three calls.
383
+ - **Opt-in**: a host that advertises `budget` without seeding this fixture records `inapplicable` for the witness.
384
+
366
385
  ## `conformance-version-fold` (closes F5)
367
386
 
368
387
  - **Consuming scenario**: `conformance/src/scenarios/version-fold.test.ts` (added 2026-06-11; previously this fixture had no consuming scenario).
@@ -518,6 +537,64 @@ Hosts that don't ship a BYOK SecretResolver MAY return `404` / `422` on the star
518
537
 
519
538
  ---
520
539
 
540
+ ## `openwop-secrets-run-witness` (production-safe secret witness, RFC 0229 §C)
541
+
542
+ > **Status: RFC 0229 `Active`.** Exercised by `src/scenarios/secrets-run-witness.test.ts` (v1) and `src/scenarios/v2-secrets-run-witness.test.ts` (v2). A host that advertises `secrets.runSecrets` MUST advertise it.
543
+
544
+ - **Purpose**: witness that a run-supplied secret resolves intact and never leaves the host, without an oracle over any stored secret. The `openwop-smoke-byok-roundtrip` node hashes a secret the workflow names and shows the digest and length to every run viewer, so a production host rightly withholds it. This fixture's node reads only a `run:` ref, which resolves only to a value supplied with the same run, and outputs only a boolean.
545
+ - **Topology**: single node `witness` with `typeId: core.secret.witness`. Its `ref` and `expectedSha256` inputs are bound to the run inputs of the same names (`variables` `ref` and `expectedSha256`).
546
+ - **Node contract** (`host-services.md` §Run-supplied secrets; v1 `capabilities.md` §"Run-supplied secrets"):
547
+ - a `ref` without the `run:` prefix fails `credential_forbidden` without resolving anything;
548
+ - an unsupplied `run:` ref fails `credential_not_found`;
549
+ - otherwise the output is `{ matched }`: whether the lowercase-hex SHA-256 of the value's UTF-8 bytes equals `expectedSha256`. No value, digest or length appears on any surface.
550
+ - **Conformance test driver** (per run the suite draws `C`, 48 CSPRNG bytes base64url-encoded, 64 characters, no fixed prefix):
551
+ 1. POST the create route with `{ workflowId: "openwop-secrets-run-witness", inputs: { ref: "run:openwop-witness", expectedSha256: sha256(C) }, runSecrets: [{ ref: "run:openwop-witness", value: C }] }`. **Assert** the run completes and `witness` outputs `matched: true`.
552
+ 2. The same with a different `expectedSha256`. **Assert** `matched: false`.
553
+ 3. **Assert** `C` is absent from the create response, the snapshot, every event (read under the most verbose stream mode served), node outputs and variables, the run-list entry and error bodies — raw, base64 (standard and URL-safe, padded and not), hex (both cases), percent-encoded, JSON-escaped, and as `sha256(C)`.
554
+ 4. Scope legs, each ending in `node.failed`: a non-`run:` ref (`openwop-conformance-canary-secret`, and a fresh random name) fails `credential_forbidden`; `run:openwop-witness` with no `runSecrets` fails `credential_not_found`; an earlier run's `ref` fails `credential_not_found`; where `replay` is advertised, a `branch` fork taken before `witness` fails `credential_not_found`.
555
+ 5. Idempotency: the create under a fresh `Idempotency-Key`, then the same key with a different value `C′`. **Assert** the retry replays (same `runId`, `OpenWOP-Idempotent-Replay: true`), is never `409 idempotency_key_mismatch`, and `C′` appears on no surface of the run.
556
+
557
+ ---
558
+
559
+ ## `conformance-secret-resolve-then-fail` (`host-services.md` §`secrets` — the canary through a failure)
560
+
561
+ > **Status: capability-gated (`secrets`).** Exercised by `src/scenarios/v2-secret-canary-absent.test.ts`. A host advertises it only when it also serves `openwop-smoke-byok-roundtrip` (the same node and canary).
562
+
563
+ - **Purpose**: a secret resolved from the host's own store MUST NOT reach any readable surface of a run that then fails — the snapshot `error`, `node.failed`, `run.failed`, or an error envelope answered about the run (`host-services.md` §`secrets`: "Raw key material MUST NOT appear in any event, log, trace, prompt, error, export or screenshot").
564
+ - **Topology**: `resolve-secret` (`conformance.secret.echo`, `config.secretId: "openwop-conformance-canary-secret"`, the node contract of `openwop-smoke-byok-roundtrip`: outputs only `{secretSha256, secretLength}`) → `fail` (`core.fail`).
565
+ - **Terminal**: `failed`.
566
+ - **Conformance test driver**: create the run, settle it, read the createRun answer, the snapshot, the `debug` poll log and SSE stream, the answer to cancelling the terminal run and (with `runList`) the list page; the canary MUST be on none of them. The suite recognises it by the surfaced digest (every `secretLength`-byte window hashed against `secretSha256`) and, when the operator supplies it, by `OPENWOP_CANARY_SECRET_VALUE` in every common encoding.
567
+
568
+ ---
569
+
570
+ ## `conformance-queue-publish` + `conformance-queue-consume` (`host-services.md` §`queueBus` — tenant isolation)
571
+
572
+ > **Status: capability-gated (`queueBus`).** Exercised by `src/scenarios/v2-queue-cross-tenant-isolation.test.ts`. A host advertises the pair together, or neither.
573
+
574
+ - **Purpose**: "A tenant's consumer MUST NOT receive another tenant's messages, even on the same topic." The tenant is the run's — the credential that created it — never a request field.
575
+ - **Node `conformance.queue.probe`**, one node per fixture, `config.action` selects:
576
+ - `publish` — `ctx.queueBus.publish({ topic: inputs.topic, payload: { message: inputs.message } })` under the run's tenant; outputs `{ published: true }`.
577
+ - `consume` — `ctx.queueBus.consume({ topic: inputs.topic })` under the run's tenant for up to `config.waitMs` (1000), `ack`s every message it receives, and outputs `{ consumed: [<payload.message of each, in order>] }`. It MUST output `consumed: []` when nothing arrived, never leave it unset, so an idle consume cannot pass the isolation check vacuously.
578
+ - **Terminal**: `completed` for both.
579
+ - **Conformance test driver**: tenant A (`OPENWOP_API_KEY`) publishes a fresh nonce on a fresh topic T; tenant B (`OPENWOP_TEST_TENANT_B_API_KEY`, a different tenant) consumes T — `consumed` MUST NOT contain the nonce; then A consumes T — `consumed` MUST contain it (the positive control).
580
+
581
+ ---
582
+
583
+ ## `conformance-tool-scope-probe` (`host-services.md` §`toolHooks` — per-tool authorization fails closed)
584
+
585
+ > **Status: capability-gated (`toolHooks.perToolAuthorization`).** Exercised by `src/scenarios/v2-tool-authorization-fail-closed.test.ts`. A host that advertises `perToolAuthorization` advertises this fixture; one that withholds it records `blocked`.
586
+
587
+ - **Purpose**: "Before invoking, the host MUST check the principal's scopes against the tool's `requiredScopes`. If one is missing or cannot be evaluated, it MUST NOT invoke, MUST emit `agent.toolReturned` with `status: forbidden`, and MUST answer `403 forbidden` with `details.scope: "tool"`, `toolName` and `requiredScopes`."
588
+ - **Node `core.conformance.scoped-tool`** (conformance-RESERVED). The host maps it to ONE external tool call, made through its ordinary tool-invocation path (the same authorization check, the same `agent.tool-called` / `agent.tool-returned` pair) as the run's principal — the credential that created the run, never a request field:
589
+ - the tool is `conformance.scope-probe` (`config.toolName`), and its `requiredScopes` is `[inputs.requiredScope]`, or `[]` when `requiredScope` is empty or absent;
590
+ - when invoked, the tool POSTs `{}` to `inputs.url` and returns `{ invoked: true }`; the node completes with `outputs.result = { invoked: true }`;
591
+ - a refusal fails the node with the refusal as its error, code and details unchanged (`errors.md` §Host-service refusals): `forbidden`, `details.scope: "tool"`, `details.toolName: "conformance.scope-probe"`, `details.requiredScopes`.
592
+ - **Operator contract.** No principal holds the scope `conformance:never-granted`. Evaluating the scope `conformance:unevaluable` errors (the operator binds its check to a resolver that throws or cannot be reached), so the host's evaluator reaches its "cannot be evaluated" branch rather than a plain "not held".
593
+ - **Terminal**: `completed` with no required scope; `failed` (`forbidden`) for either scope above.
594
+ - **Conformance test driver**: for each of `""` (control), `conformance:never-granted` and `conformance:unevaluable`, start a fresh receiver and run the fixture with its URL. The control MUST reach the receiver once and return `status: ok`; each scoped run MUST reach it zero times, emit `agent.tool-returned { toolName: "conformance.scope-probe", status: "forbidden" }` without `durationMs`, emit no `ok` return for the tool, and fail the node `forbidden` with the three `details` members.
595
+
596
+ ---
597
+
521
598
  ## `conformance-channel-ttl` (closes C3 — channel TTL reducer fold)
522
599
 
523
600
  > **Status: included in the v1.0 conformance baseline.** Server-side `core.channelWrite` support is exercised by `src/scenarios/channel-ttl.test.ts`.
@@ -625,6 +702,41 @@ For `v2-tool-catalog-annotations`' unclassified-MCP leg, a host that advertises
625
702
 
626
703
  ---
627
704
 
705
+ ## The storage probe fixtures (storage.md)
706
+
707
+ `conformance-storage-probe` is what `v2-storage-cross-tenant-isolation` drives, and `conformance-fs-probe` is what `v2-fs-sandbox-escape-refused` drives. Both nodes are conformance-RESERVED: a host maps each to a node that calls its OWN storage service (`spec/v2/core/storage.md` §Operations, reached through `ctx`) for the run's tenant and nothing else, and passes a rejection through as the node's failure, code and details unchanged (`errors.md` §Host-service refusals). They are fixtures, not seams: they run through `POST /runs` under the caller's own credential, so the tenant is the one the caller authenticated as.
708
+
709
+ **`core.conformance.storage-probe`.** Inputs `family`, `action` (`write` | `read` | `delete`), `key`, `value`, and optionally `ref`. The node's `node.completed.outputs.result` is `{ written: true, ref? }` for `write`, `{ values: string[], keys: string[] }` for `read` (every value and every key the family's read operations below return; empty arrays when nothing is found) and `{ deleted: true }` for `delete`. Target names (`conformance`, `conformance_probe`) are the same in every tenant, so two tenants collide on them by construction.
710
+
711
+ | `family` | `write` | `read` |
712
+ |---|---|---|
713
+ | `kvStorage` | `put(key, value)` | `get(key)` → its value; `list(prefix: key)` → its keys |
714
+ | `cache` | `put(key, value, min(300, maxTtlSeconds))` | `get(key)` → its value |
715
+ | `blobStorage` | `put("conformance", key, utf8(value), "text/plain")` | `get("conformance", key)` decoded as UTF-8; `list("conformance", prefix: key)` → its keys |
716
+ | `tableStorage` | `createTable("conformance_probe", { k: string, v: string })` once, then `insert` `{ k: key, v: value }`; `ref` = the `rowId` | `query("conformance_probe", { k: key })` → each row's `v` and `k`; with `ref`, also `get("conformance_probe", ref)` |
717
+ | `vectorStore` | `upsert("conformance", [{ id: key, embedding: E(key), metadata: { value } }])` | `query("conformance", E(key), 10)` → each match's `metadata.value` and `id` |
718
+ | `searchIndex` | `index("conformance", [{ id: key, fields: { value } }])` | `query("conformance", value, 10)` → each hit's `fields.value` and `id` (the scenario passes `value` on read) |
719
+ | `sql` | on datasource `conformance`: create table `conformance_probe (k, v)` if absent, then `execute` an INSERT with `[key, value]` bound through `params` | `query` a SELECT of `k, v` where `k` is the bound `key` |
720
+ | `nosql` | `insert("conformance", "conformance_probe", { k: key, v: value })`; `ref` = the id | `query(…, { k: key })` → each doc's `v` and `k`; with `ref`, also `get(…, ref)` |
721
+
722
+ `E(key)` is a deterministic embedding the host derives from `key` alone, so the same key gives the same vector in every tenant. The datasource, bucket, collection and index named `conformance` are bound by the operator for every tenant under that one name; the host keeps them tenant-scoped as `storage.md` requires. `delete` removes what `write` stored. A host MUST NOT advertise the fixture unless it implements every storage family it advertises.
723
+
724
+ **`core.conformance.fs-probe`.** Inputs `op` (`write` | `read`), `path`, `content`. `write` calls `ctx.fs.write(path, utf8(content), "text/plain")`; `read` calls `ctx.fs.read(path)` and completes with `outputs.result = { content }` (UTF-8). The node does no path handling of its own. **Operator contract:** `<sandboxRoot>/conformance/escape-link` is a symbolic link to a readable file outside `sandboxRoot`. When the link is missing the symlink leg records `blocked`.
725
+
726
+ ---
727
+
728
+ ## The safeFetch probe fixture (host-services.md)
729
+
730
+ `conformance-safefetch-probe` is what `v2-safefetch-ssrf-refused` drives. Its node, `core.conformance.safefetch-probe`, is conformance-RESERVED: a host that advertises `httpClient.safeFetch` maps it to a node that calls its OWN `ctx.http.safeFetch(url)` (`spec/v2/core/host-services.md` §`httpClient`) with the run's `url` input unchanged, method `GET`, no headers and no body, and nothing else. It is a fixture, not a seam: it runs through `POST /runs` under the caller's own credential.
731
+
732
+ - **Success.** `node.completed.outputs.result` is `{ status }`, the response status only. The body is never output, so advertising the fixture does not turn the host into a read proxy.
733
+ - **Rejection.** The node fails with the rejection's code and details unchanged (`errors.md` §Host-service refusals): a refused target is `egress_denied` with `details.reason: ssrf-blocked`; an unreachable one is `upstream_unavailable`.
734
+ - **No normalisation.** The node does not parse, resolve or rewrite `url`, so the host's own guard is what is exercised.
735
+
736
+ A host MUST NOT advertise the fixture unless it advertises `httpClient.safeFetch`.
737
+
738
+ ---
739
+
628
740
  ## The per-agent card pack (RFC 0202)
629
741
 
630
742
  `v2-a2a-agent-cards` reads the caller's `GET /agents`; it needs no workflow fixture. Two suite requirements (COMPATIBILITY §2.3), not spec MUSTs:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwop/openwop-conformance",
3
- "version": "2.45.2",
3
+ "version": "2.45.4",
4
4
  "description": "Production-ready black-box conformance suite for OpenWOP v1.0 compliant servers.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -56,6 +56,6 @@
56
56
  "@openwop/spec-artifacts": "file:../spec-artifacts"
57
57
  },
58
58
  "peerDependencies": {
59
- "@openwop/spec-artifacts": "2.45.2"
59
+ "@openwop/spec-artifacts": "2.45.4"
60
60
  }
61
61
  }