@openwop/openwop-conformance 2.31.1 → 2.33.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/CHANGELOG.md +26 -0
- package/README.md +5 -4
- package/coverage.md +2 -1
- package/dist/cli.js +7 -4
- package/dist/lib/certification-bundle-v3.js +12 -2
- package/dist/lib/child-env.js +29 -0
- package/dist/lib/v2-profiles.js +66 -0
- package/dist/spec-artifacts.lock.json +2 -2
- package/package.json +2 -2
- package/requirements.json +130 -12
- package/scenario-majors.json +5 -2
- package/schemas/CORPUS-STAMP.json +17 -17
- package/src/cli.ts +7 -4
- package/src/lib/a2a-fake-peer.ts +16 -3
- package/src/lib/certification-bundle-v3.ts +11 -2
- package/src/lib/child-env.ts +29 -0
- package/src/lib/mcp-fake-server.ts +13 -0
- package/src/lib/v2-profiles.ts +66 -0
- package/src/lib/webhook-receiver.ts +24 -5
- package/src/scenarios/a2a-1-0-agent-card.test.ts +1 -1
- package/src/scenarios/a2a-peer-authority.test.ts +1 -1
- package/src/scenarios/a2a-task-roundtrip.test.ts +1 -1
- package/src/scenarios/a2a-version-negotiation.test.ts +3 -3
- package/src/scenarios/mcp-extension-opacity.test.ts +1 -1
- package/src/scenarios/mcp-mrtr-roundtrip.test.ts +1 -1
- package/src/scenarios/mcp-tool-roundtrip.test.ts +1 -1
- package/src/scenarios/mcp-version-negotiation.test.ts +3 -3
- package/src/scenarios/v2-durability-recovery.test.ts +245 -55
- package/src/scenarios/v2-minimum-version-refused.test.ts +2 -2
- package/src/scenarios/v2-mrtr-rounds-ceiling.test.ts +1 -1
- package/src/scenarios/v2-negotiation-authenticated.test.ts +2 -2
- package/src/scenarios/v2-negotiation-decided-emitted.test.ts +3 -3
- package/src/scenarios/v2-relaxation-recorded.test.ts +37 -2
- package/src/scenarios/v2-webhook-egress-refusal.test.ts +115 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# `@openwop/openwop-conformance` Changelog
|
|
2
2
|
|
|
3
|
+
## [2.33.0] — 2026-09-21 — a declared relaxation denied nothing, an undeclared one was invisible, and two fakes made both unavoidable
|
|
4
|
+
|
|
5
|
+
Step 2 of closing `docs/KNOWN-LIMITS.md` §"The reference host's certified bundles were cut under an undeclared relaxation". Three suite defects, all the corpus's.
|
|
6
|
+
|
|
7
|
+
- **A host advertising `a2a` or `mcp` could not be cut relaxation-free.** The A2A fake peer and MCP fake server advertised `http://127.0.0.1:<port>` and nothing else; only the webhook receiver had a public front. So such a host could be measured ONLY with its egress guard relaxed, and a relaxed guard cannot certify — the rule made a host implementing *more* of the protocol *less* able to certify. The two production hosts cut cleanly only because they advertise neither family. New `OPENWOP_A2A_FAKE_PEER_URL` / `OPENWOP_MCP_FAKE_SERVER_URL`, validated by the same rule as `OPENWOP_WEBHOOK_RECEIVER_URL` (now `resolvePublicFront(envName, …)`; `resolveRegistrationUrl` is a wrapper). `hostFacingEndpoint()` is what is handed to the host — every invoke seam's `peerUrl` / `serverUrl`, and the AgentCard's `url` fields — while the suite's own requests to its own fake keep using the loopback `endpoint()`. Twelve scenario files updated.
|
|
8
|
+
- **A DECLARED relaxation denied nothing on any real host.** `verifyBundleV3` matched a relaxed obligation to a profile by testing whether the profile **id** contained the family name; the emitter tested whether it contained the **whole obligation string** — two different rules under a comment saying they matched. No real v2 profile id contains a family name, so `webhooks.egress-guard` denied neither `openwop-core-standard` nor anything else. `v2-relaxation-recorded` stayed green because its fixture profile was called `openwop-webhooks`, the one id the rule could match. New `profilesRelaxedBy()` in `lib/v2-profiles.ts` reads ownership from where it was always stated — a profile's `predicate.families` in `spec/v2/profiles.json` — and is the ONE function both the emitter and the verifier call. The id-named arm is kept, so nothing denied today is un-denied. New leg `openwop.requirement.0173.relaxation-recorded.owned-by-registry` asserts against the real `openwop-core-standard`. **Sabotage-proven: restoring the old rule leaves the old leg green and reddens only the new one** — which is the blindness, reproduced.
|
|
9
|
+
- **NEW `v2-webhook-egress-refusal.test.ts` — the egress guard's refusal, at major 2, for the first time.** `webhooks.md` §SSRF is a plain MUST with a registered code, and no major-2 row asserted it: `http-client-ssrf` and `webhook-negative` never left the major-1 lane, so a v2 bundle could certify `openwop-core-standard` on a host whose guard refused nothing. Eight registration probes, one per class the sentence names (non-`https`, IPv4 and IPv6 loopback, `localhost`, two RFC 1918 ranges, link-local / cloud metadata, IPv6 ULA); each MUST answer `400 webhook_url_rejected`. Registration-time only — no probe is a destination the suite owns, nothing is delivered, an accepted registration is deleted at once. New id `openwop.requirement.0171.webhook-egress-refused`.
|
|
10
|
+
- **An UNDECLARED relaxation is now detectable, and denied.** This README said the suite "cannot detect an undeclared one". For the egress guard that was never true: a host that accepts `https://127.0.0.1/…` has shown on the wire that its guard is open. Three outcomes — all refused → `executed-pass`; accepted with a `webhooks.*` relaxation DECLARED → `inapplicable`, the declaration quoted, the owning profile denied by the declaration; accepted with NOTHING declared → `executed-fail`, which `profilesDeniedByObservedRelaxation()` turns into a denial of every profile built on `webhooks` in both the emitter (`certified: false`) and `--verify` (new rejection `undeclared-relaxation-observed`, scoped to the profile). Declaring nothing is no longer a way around the rule.
|
|
11
|
+
- **Measured on the v2 reference host in all three postures:** guarded → pass (8/8 refused, right code); `OPENWOP_WEBHOOK_ALLOW_PRIVATE=1` with nothing declared → **fail, "ACCEPTED 8 of 8"** — that flag opens the scheme check and the metadata address too, not only loopback; same flag with the relaxation declared → `inapplicable`. The first attempt at that measurement reported pass/pass/pass, because the harness had not killed the first host and all three runs hit the guarded one; it now refuses to measure while the port is held.
|
|
12
|
+
- `coverage.md`: new row; and the `v2-durability-recovery` row is corrected — it still said `duplicate-delivery` was "counted per effect identity" and that the log was read from `/events`, both wrong since 2.32.0.
|
|
13
|
+
- **Expect this release to un-certify a loopback cut.** That is its purpose. A host cut through public fronts (both production hosts) is unaffected.
|
|
14
|
+
- Suite pinned to corpus `2.33.0`.
|
|
15
|
+
|
|
16
|
+
## [2.32.0] — 2026-09-20 — every published v2 pack failed the corpus schema, and two kill rows that mandated a fast bound
|
|
17
|
+
|
|
18
|
+
- **`v2-one-signing-scheme` now covers every bare `*-pack-manifest`, enumerated rather than listed** (openwop#1367). It pinned one file — `registry-version-manifest` — while six bare manifests kept the v1 `{ publicKeyRef, signatureRef, method }` block, closed, and `connection-pack-manifest` had no `signing` seat at all. Measured against the published v2 registry: **0 of 190** `pack.json` documents validated. New id `openwop.requirement.0177.one-signing-scheme.bare-manifests`. Sabotage-proven: fails on the 2.31.1 `node-pack-manifest`.
|
|
19
|
+
- **`openwop.requirement.0177.chain-fragment-config-open`** — `FragmentNode.config` is open exactly as the `WorkflowNode.config` it expands into. The v1→v2 seed closed it because it documents one key (`subChainRef`); 73 of 81 published chain packs failed on `config must NOT have additional properties`. With both corrections: **185 of 190** validate; the remaining five fail on constraints identical in v1 (registry data, not schema).
|
|
20
|
+
- **`v2-durability-recovery`: both kill rows observe until the host's DECLARED recovery bound elapses.** They read the log once, the instant discovery answered again — demanding resumption within ~0 ms of the listener returning, a fast bound RFC 0158 rejects. A host whose sweeper first ticks 5 s after boot against a derived 65 s bound failed, then resumed correctly ten seconds later. The bound (`GET /host/durability/bound`) is a ceiling for waiting only; 60 s fallback when unserved; the governing per-class figure is read off the seam's own response (`recoveryBoundMs`) when the host names one. The observation ceiling is 240 s by default and **operator-raisable** (`OPENWOP_DURABILITY_OBSERVATION_CEILING_MS`): a fixed ceiling would have moved the single read's defect from 0 s to 240 s and made a §B.6-conformant 12.5-minute leased-class bound uncertifiable. A declared bound over the ceiling records `blocked` with that precondition named, not fail. The row detail records the observed kill → resumption interval.
|
|
21
|
+
- **`kill-during-execution` asserts both clauses of §E item 11.** `status !== 'completed' || starts > 1` held forever for a run that was never resumed, so the row passed on a host that lost the work. Now: never observable as completed un-re-executed (latched across every observation) **and** resumed within the bound.
|
|
22
|
+
- **A resumption signal is a further `run.started` or the registry's own recovery event** (`workflow.restored`, `run.restored-from-snapshot`) — item 11's "equivalent progress-past-the-pre-kill-point signal", named.
|
|
23
|
+
- **`duplicate-delivery` counts the effect where it lands.** It counted rows per identity on `GET /runs/{id}/effects` — and a ledger keyed on effect identity admits at most one row per identity *by construction*, so the assertion held whatever the effect did. openwop-app sabotage-tested it before building to it: dedup claim forced to always win, then the effect emitted **twice per fire**; the per-identity count stayed 1 and the row stayed green. §C asks for invocation counts; the projection exposes identities. The scenario now starts its own receiver, passes `effectUrl` to the seam, and asserts **exactly one arrival**; the seam MUST stage work performing exactly one outbound effect addressed there and deliver it twice. The ledger read stays as a labelled consistency check. It also sends no `workflowId` (`conformance-noop` records no effect, so the row could only record `blocked`). Receiver reachability is a declared operator precondition, as for the webhook rows.
|
|
24
|
+
- **The run log is read from `GET /runs/{runId}/events/poll`, not `/events`.** At major 2 `/events` is `streamRunEvents`, SSE only; `pollRunEvents` is the JSON read. The scenario parsed the stream path as JSON, so on a host serving exactly what the OpenAPI states the read came back null and **`poison-exhaustion` resolved `executed-pass` with `partial-witness: blocked`** — its load-bearing clause never ran. Measured on the v2 reference host; openwop-app content-negotiates the stream path, which is why it never showed there. An unreadable log on a kill row is now `blocked`, never "nothing resumed".
|
|
25
|
+
- Four of the five durability defects were found by the openwop-app host reading the scenario against its own mechanism **before building the seam**; the fifth by the v2 reference host's first run. No host had witnessed the seam rows, so no bundle's seam rows are invalidated — but the reference host's committed `poison-exhaustion` pass was vacuous and is superseded by its next cut.
|
|
26
|
+
- **`--certify` no longer hands the bundle signing key to the vitest child.** The child's environment was `{ ...process.env }`, so `OPENWOP_BUNDLE_SIGNING_KEY` — the PKCS8 PEM — went with it, though only the parent signs. Reported by a host operator mid-cut: on macOS `npm exec` rewrites `process.title` and a process listing spills into the child's environ, so `ps` printed the signing key in full into a terminal and transcript. New `src/lib/child-env.ts`; both spawn sites use it, and a self-test asserts on `cli.ts`'s source that no child environment is a bare spread of `process.env`. The key **id** is not secret and stays. **If you listed processes during a `--certify` on ≤ 2.31.1, treat that key as seen and rotate it.**
|
|
27
|
+
- Suite pinned to corpus `2.32.0`.
|
|
28
|
+
|
|
3
29
|
## [2.31.1] — 2026-09-20 — major-correct shared polling
|
|
4
30
|
|
|
5
31
|
- Fixed the shared run polling helper to use `/runs/{runId}` in the major-2 lane and retain `/v1/runs/{runId}` only in the major-1 lane. The old helper combined a `/v1` path with `OpenWOP-Version: 2`, so a conforming host correctly answered `protocol_version_mismatch`; `v2-durability-recovery` then misreported an unreachable run even though it had already failed terminally.
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
# --legacy-peer-deps is REQUIRED, not optional: the exact peer pin is what npm's
|
|
12
12
|
# default resolver refuses. npm 10.9 fails outright with
|
|
13
13
|
# "Cannot read properties of null (reading 'edgesOut')" — use npm >= 11.
|
|
14
|
-
npm install --legacy-peer-deps @openwop/openwop-conformance@2.
|
|
14
|
+
npm install --legacy-peer-deps @openwop/openwop-conformance@2.33.0 @openwop/spec-artifacts@2.33.0
|
|
15
15
|
# or run without install:
|
|
16
16
|
npx @openwop/openwop-conformance --base-url https://api.example.com --api-key hk_test_...
|
|
17
17
|
```
|
|
@@ -99,7 +99,7 @@ Run `npm run test` for normal CI cadence; `npm run test:strict` when claiming fu
|
|
|
99
99
|
|
|
100
100
|
| Variable | Effect |
|
|
101
101
|
| ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
102
|
-
| `OPENWOP_HOST_RELAXATIONS` | Sets `host.relaxations[]` in the bundle. **It relaxes nothing in the suite** — it is your declaration that the HOST runs relaxed, and the suite cannot detect an undeclared one. A profile carrying a relaxation **cannot certify** (`security-defaults.md` §Relaxations, RFC 0173 §A.2), and since 2.6.0 the emitter enforces that rather than writing `certified: true` and leaving the contradiction to `--verify`. JSON array of `{obligation, durability, reason}`; `durability` is `session \| deployment \| persisted` — **not** `permanent`. Unparseable JSON exits 2; a wrong enum value exits 2 later, at schema validation. |
|
|
102
|
+
| `OPENWOP_HOST_RELAXATIONS` | Sets `host.relaxations[]` in the bundle. **It relaxes nothing in the suite** — it is your declaration that the HOST runs relaxed, and the suite cannot detect an undeclared one **in general**. It can detect ONE: since 2.33.0 `v2-webhook-egress-refusal` registers eight destinations `webhooks.md` §SSRF says a host MUST refuse, and a host that accepts one with no `webhooks.*` relaxation declared here records `executed-fail` — which the emitter and `--verify` read as an observed undeclared relaxation, denying every profile built on `webhooks`. An obligation is `<family>.<name>`; it denies each profile whose `spec/v2/profiles.json` predicate lists that family (so `webhooks.egress-guard` denies `openwop-core-standard`). Until 2.33.0 the match was on the profile *id*, which contains no family name, so a declared relaxation denied nothing on any real host. A profile carrying a relaxation **cannot certify** (`security-defaults.md` §Relaxations, RFC 0173 §A.2), and since 2.6.0 the emitter enforces that rather than writing `certified: true` and leaving the contradiction to `--verify`. JSON array of `{obligation, durability, reason}`; `durability` is `session \| deployment \| persisted` — **not** `permanent`. Unparseable JSON exits 2; a wrong enum value exits 2 later, at schema validation. |
|
|
103
103
|
| `OPENWOP_TARGET_MAJOR` | Selects the scenario lane. **Only the literal `"2"` selects major 2** — unlike `--target-major`, which validates and exits 2 on a bad value, the env var falls back to major 1 for `"1"`, `"3"`, `"two"` or empty. The runner and every vitest worker MUST see the same value: when they disagreed, the worker minted `openwop.floor.*` while the runner looked up `openwop.scenario.*`, producing a vacuous-pass emitter defect and **no bundle at all**. Prefer `--target-major`; set this only when driving `vitest` directly. |
|
|
104
104
|
| `OPENWOP_REQUIRE_BEHAVIOR=true` | Capability-gated scenarios (audit-log integrity, rate-limit envelope, multi-region idempotency, `configurableSchema`, webhook sig versioning, etc.) FAIL instead of skipping when the host doesn't advertise the profile. Lets a host claim "full coverage" mechanically. See [`coverage.md`](./coverage.md) §"Capability-gated scenarios". |
|
|
105
105
|
| `OPENWOP_TEST_PUBLIC_REGISTRY=true` | Runs `registry-public.test.ts` against the hosted registry at `packs.openwop.dev`. Skipped by default so the suite doesn't depend on outbound connectivity. |
|
|
@@ -113,6 +113,7 @@ Run `npm run test` for normal CI cadence; `npm run test:strict` when claiming fu
|
|
|
113
113
|
| `OPENWOP_MCP_FAKE_SERVER=true` | Boots the synthetic MCP peer for `mcp-tool-roundtrip.test.ts`. |
|
|
114
114
|
| `OPENWOP_MCP_REAL_SERVER_URL=<base-url>` | Points the MCP wire-shape probe at a real MCP server. The probe POSTs JSON-RPC and reads a single-JSON response — matches MCP's `streamable-http` transport in single-response mode. **Does NOT support** stdio transport (which is what most `modelcontextprotocol/servers` references default to) or SSE-streamed responses; an operator collecting interop evidence today runs a custom `StreamableHTTPServerTransport`-style server that returns a single JSON body per request. Adding SSE-frame parsing is tracked in `docs/PROTOCOL-GAP-CLOSURE-PLAN.md` Track 6. Assertions relax to shape-only. When both this and `OPENWOP_MCP_FAKE_SERVER` are set, the real URL wins. Phase 3 T3.4 interop-evidence path. |
|
|
115
115
|
| `OPENWOP_A2A_FAKE_PEER=true` | Boots the synthetic A2A peer for `a2a-task-roundtrip.test.ts`. |
|
|
116
|
+
| `OPENWOP_A2A_FAKE_PEER_URL=<https-url>` / `OPENWOP_MCP_FAKE_SERVER_URL=<https-url>` | **The public front for each fake — the route to an A2A / MCP witness that relaxes nothing** (2.33.0). Same rule and the same loud validation as `OPENWOP_WEBHOOK_RECEIVER_URL`: `https:`, publicly resolvable. The fake still LISTENS on loopback; pin it with the matching `_PORT` so the tunnel has a fixed port to forward to. When set, the URL handed to the host under test — the `peerUrl` / `serverUrl` of every invoke seam, and the `url` fields of the A2A AgentCard — is the front; the suite's own requests to its own fake stay local. Until 2.33.0 neither fake had a front, so a host advertising `a2a` or `mcp` could be measured ONLY with its egress guard relaxed, and a relaxed guard cannot certify: the rule made a host implementing more of the protocol less able to certify. |
|
|
116
117
|
| `OPENWOP_A2A_REAL_PEER_URL=<base-url>` | Points the A2A AgentCard + task-lifecycle probe at a real reference A2A peer. Drift-point subtests (AUTH_REQUIRED / REJECTED) stay fake-peer-only — real peers don't expose a state-forcing API. Phase 3 T3.4 interop-evidence path. |
|
|
117
118
|
| `OPENWOP_FORCE_RATE_LIMIT=true` | Signals the host (test-only key) to fabricate a 429 so `rate-limit-envelope.test.ts` can exercise envelope shape deterministically. |
|
|
118
119
|
| `OPENWOP_TEST_PROMPT_PACK_INSTALLED=true` | Promotes the `prompt-pack-install.test.ts` existence claim from soft-skip to hard assertion (RFC 0028 §B). Set when the target host is known to have at least one prompt pack installed at boot — RFC 0028 §B does NOT require any host with `endpointsSupported: true` to have packs installed, so the suite stays conformant against a fresh production host with no pack subscriptions when the flag is unset. The in-tree workflow-engine sample auto-installs `vendor.openwop.prompt-sample` via `promptPackLoader.ts`, so operators running against it should set the flag. |
|
|
@@ -132,7 +133,7 @@ Exit code is non-zero on any failed assertion. `--certify` distinguishes: `0`
|
|
|
132
133
|
|
|
133
134
|
## What's Covered
|
|
134
135
|
|
|
135
|
-
The current suite has
|
|
136
|
+
The current suite has 524 scenario files under `src/scenarios/`.
|
|
136
137
|
- 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.
|
|
137
138
|
- 2026-08-19 (suite `1.137.0 → 1.138.0`): NEW `durability-poison-exhaustion.test.ts` — RFC 0158 §C.8, the FIRST row of that RFC's conformance table to land. Asserts what `failure-path.test.ts` cannot: not just that deterministically failing work reaches terminal, but that attempts STOP — counted on the log, re-counted after a scaled quiet window, asserted unchanged. A host still redelivering records more. Seam-gated on the existing event-log seam (`blocked` = unobservable, not unmet) and outside every profile floor.
|
|
138
139
|
- 2026-08-19 (suite `1.136.15 → 1.137.0`): NEW `replay-fanout-suppression.test.ts` — capability-gated on `webhooks.supported`, **outside every profile floor**; witnesses `replay.md` §"Host-initiated fan-out is an external effect", which was the largest normative MUST NOT on the replay surface with no scenario and no SECURITY invariant. Three legs in ONE `it` against ONE receiver and ONE subscription — a positive control, the MUST NOT, and a `branch` boundary leg — because "no delivery arrived" passes identically when delivery never worked, so absence is asserted only after presence is proven on that exact wiring. A host with an SSRF guard correctly refuses the loopback receiver and records `blocked`: **unobservable, not unmet.**
|
|
@@ -477,7 +478,7 @@ Server-required (added in 1.7.0):
|
|
|
477
478
|
| ------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
478
479
|
| **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. |
|
|
479
480
|
|
|
480
|
-
Current source tree:
|
|
481
|
+
Current source tree: 524 scenario files. Use [`coverage.md`](./coverage.md) for current grade/gap tracking.
|
|
481
482
|
|
|
482
483
|
## Remaining Gaps
|
|
483
484
|
|
package/coverage.md
CHANGED
|
@@ -144,7 +144,8 @@ The scenario groups in the table below (one row per group; count the rows — th
|
|
|
144
144
|
| `tool-session-lifecycle.test.ts` | `capabilities.toolCatalog.sessionLifecycle` (RFC 0078 §D, `tool-catalog.md`) | A (the §D bracket via `POST /v1/host/sample/tools/session-run` + the test event-log seam: `tool.session.opened` before the first RFC 0064 call event → `tool.session.closed` after the last, one shared `sessionId`, each carrying a `toolId`, `closed.outcome` ∈ {completed,failed,aborted,expired}, both content-free) | `host-pending` | `behaviorGate('openwop-tool-session-lifecycle', …)`. Seam-gated; soft-skips on 404. **Part of the RFC 0078 → Accepted bar.** First adopter: MyndHyve `toolCatalog`. |
|
|
145
145
|
| `egress-audience-binding.test.ts` | `capabilities.httpClient.egressPolicy.supported` (RFC 0079 §C, `host-capabilities.md`) + `SECURITY/invariants.yaml` `egress-credential-audience-bound` | A (KEYSTONE — the §C confused-deputy MUST via `POST /v1/host/sample/egress/decide`: an out-of-audience egress is `denied`/`downgraded` with `reason:"out-of-audience"` and the credential is NOT attached (`credentialAttached !== true`); a provenance-unevaluable egress fails closed `denied`+`reason:"provenance-unevaluable"`; decision/reason ∈ the closed enums) | `host-pending` | `behaviorGate('openwop-egress-audience-binding', …)`. Seam-gated; soft-skips on 404. **This is the RFC 0079 → Accepted bar** (the `egress-credential-audience-bound` invariant graduates reference-impl → protocol tier when this passes against a host). First adopter: MyndHyve `httpClient.egressPolicy`. |
|
|
146
146
|
| `egress-decision-content-free.test.ts` | `capabilities.httpClient.egressPolicy.supported` (RFC 0079 §F / SR-1) | A (the secret non-leak — a `canary` credential's sentinel never surfaces in the decision (`canaryLeaked !== true`), the `egress.decided` payload carries no forbidden content key, and `reason` stays in the CLOSED vocabulary so no blocked destination spills into a free-form field) | `host-pending` | `behaviorGate('openwop-egress-decision-content-free', …)`. Seam-gated; soft-skips on 404. **Part of the RFC 0079 → Accepted bar.** First adopter: MyndHyve `httpClient.egressPolicy`. |
|
|
147
|
-
| `v2-
|
|
147
|
+
| `v2-webhook-egress-refusal.test.ts` | the `webhooks` family (capability-gated) | A (`webhooks.md` §SSRF at MAJOR 2 — eight registration probes, one per class the sentence names: non-`https`, IPv4/IPv6 loopback, `localhost`, two RFC 1918 ranges, link-local / cloud metadata, IPv6 ULA; each MUST answer `400 webhook_url_rejected`. Until 2.33.0 no major-2 row asserted the guard refuses anything — the refusal scenarios were major-1 only. THREE OUTCOMES: all refused → `executed-pass`; a probe accepted with a `webhooks.*` relaxation DECLARED in `OPENWOP_HOST_RELAXATIONS` → `inapplicable`, and the declared relaxation denies the owning profile; a probe accepted with NOTHING declared → `executed-fail`, which the emitter and verifier read as an OBSERVED undeclared relaxation and deny every profile built on `webhooks` (`undeclared-relaxation-observed`). Registration-time only: no probe is a destination the suite owns, nothing is delivered, an accepted registration is deleted at once) |
|
|
148
|
+
| `v2-durability-recovery.test.ts` | the RFC 0158 host-extension durability seam `POST /host/durability/kill` (non-normative; §E mints no capability field, so seam PRESENCE is how a host says it claims a rung) | A (all five `durable-single-instance` rows: `kill-after-accept` as a HOLD-DISPATCH exercise, `kill-during-execution` asserting that work executing at a real process death is never observable as completed without re-execution, `duplicate-delivery` counted AT THE EFFECT'S DESTINATION — a receiver the suite owns, `effectUrl` — because a ledger keyed on effect identity admits one row per identity by construction and cannot show a double-fire (2.32.0), `bound-is-derived` as an explicitly-labelled paper check, and `poison-exhaustion` ported from the major-1 scenario — at major 2 it reads the canonical JSON log read `GET /runs/{runId}/events/poll` (NOT `/events`, which is SSE-only at major 2 — reading it as JSON made this row pass vacuously on a host serving exactly the OpenAPI; 2.32.0), so unlike its v1 twin it needs NO seam and can never record `blocked` for want of one a host did not wire) | `host-pending` | **Disposition rule, and it protects fleet certification:** no seam route at all ⇒ `inapplicable` (the host claims no rung); seam present but an operator precondition unmet — a restart supervisor, which a black-box suite cannot supply — ⇒ `blocked` with the precondition NAMED. Collapsing the two would deny certification to every host the day these rows entered the lane. `peer-resume` is deliberately NOT written: §E makes it bundle-witnessed, so a scenario could only ever record `blocked`. **This is the RFC 0158 → Accepted bar, and it is now complete at major 2** — before the port, a major-2 bundle could carry at most four of the five rows and the rung was unwitnessable no matter what a host did, and its witness is openwop-app, which the RFC records as having observed `kill-during-execution` across a real `SIGKILL`. |
|
|
148
149
|
| `memory-degraded-projection.test.ts` | `capabilities.agents.manifestRuntime.supported` + `capabilities.memory.supported` (RFC 0080 §C, `agent-memory.md`) | A (the §C iff-contract on the NORMATIVE `GET /v1/agents`: a degraded entry MUST carry `memoryDegraded:true` + a non-empty, unique `degradedMemoryDimensions[]` drawn from the closed §A-name enum [read/write/search/long-term-durability/compaction/attribution/replay-snapshot/retention]; a non-degraded entry MUST NOT carry a non-empty list; the inventory is non-empty; the degraded branch runs non-vacuously when `OPENWOP_DEGRADED_AGENT_ID` names a known-degraded agent) | `host-pending` | `behaviorGate('openwop-memory-degraded', …)`. Black-box on the normative path (no POST seam); soft-skips on 404 / when the host computes no degradation. **This is the RFC 0080 → Accepted bar.** First adopter: MyndHyve `memory`. |
|
|
149
150
|
| `budget-enforcement.test.ts` | `capabilities.budget.supported` (RFC 0084 §C/§D, `budget-policy.md`) + `SECURITY/invariants.yaml` `budget-no-pricing-leak` | A (the §C/§D enforcement via `POST /v1/host/sample/budget/run` + the test event-log seam: a `hard-cost-exhaust` run emits the strict-ordered `budget.reserved → budget.consumed → budget.threshold.crossed{percent} → budget.exhausted → cap.breached{kind:"budget-cost"} → run.failed{error:"budget_exhausted"}` chain; a `model-denied` run is refused `budget_model_denied` BEFORE the provider call (fail-closed); an `advisory` host emits the `budget.*` events without stopping; every `budget.*` payload content-free — no pricing/rate) | `host-pending` | `behaviorGate('openwop-budget-enforcement', …)`. Seam-gated; soft-skips on 404. **This is the RFC 0084 → Accepted bar.** First adopter: MyndHyve `budget`. |
|
|
150
151
|
| `agent-platform-aggregate-evidence.test.ts` | `openwop-agent-platform` claim — live discovery `profiles[]` includes it (RFC 0085 §C, `agent-platform-profile.md`) | A (the §C/§D honest-advertisement on live `/.well-known/openwop`: a host claiming `openwop-agent-platform` MUST satisfy the §B floor predicate (`isAgentPlatformPartial` → `partial`/`full`, never `none`), the claim backed by per-capability evidence not the profile string; `OPENWOP_AGENT_PLATFORM_TIER=full` forces the full-predicate bar — all governance terms + tenant installScope + all 16 §D terms) | `host-pending` | `behaviorGate('openwop-agent-platform', …)`. Black-box on the discovery doc (no POST seam); soft-skips until a host claims the profile. **This is the RFC 0085 → Accepted bar.** First adopter: MyndHyve (after the memory batch surfaces the floor's `memory.supported`). |
|
package/dist/cli.js
CHANGED
|
@@ -46,7 +46,8 @@ import { scrubEvidence, evidenceSecretsFromEnv, verifyBundleV2 } from './lib/cer
|
|
|
46
46
|
import { publicKeyFromPrivate, signBundleV3, verifierSign, verifyBundleV3, witnessDigest } from './lib/certification-bundle-v3.js';
|
|
47
47
|
import { deriveProfiles, isCoreStandard, agentPlatformStatus, DEPRECATED_PROFILE_ALIASES, PROFILE_FLOOR_SCENARIOS, } from './lib/profiles.js';
|
|
48
48
|
import { setV2ProfileFloors, v2ProfileFloorFiles } from './lib/requirement-registry.js';
|
|
49
|
-
import { v2ProfileIds } from './lib/v2-profiles.js';
|
|
49
|
+
import { profilesDeniedByObservedRelaxation, profilesRelaxedBy, v2ProfileIds } from './lib/v2-profiles.js';
|
|
50
|
+
import { childEnv } from './lib/child-env.js';
|
|
50
51
|
function parseArgs(argv) {
|
|
51
52
|
let baseUrl;
|
|
52
53
|
let apiKey;
|
|
@@ -493,7 +494,7 @@ async function runCertify(args, baseUrl, apiKey) {
|
|
|
493
494
|
// (and assertion count) here; the runner reads it after the run so bundle v2
|
|
494
495
|
// rows come from what scenarios RECORDED, not from per-file pass/fail/skip.
|
|
495
496
|
const ledgerFile = join(reportDir, 'requirement-ledger.jsonl');
|
|
496
|
-
const env =
|
|
497
|
+
const env = childEnv(process.env);
|
|
497
498
|
env.OPENWOP_BASE_URL = baseUrl;
|
|
498
499
|
env.OPENWOP_API_KEY = apiKey;
|
|
499
500
|
env.OPENWOP_LEDGER_PATH = ledgerFile;
|
|
@@ -664,7 +665,9 @@ async function runCertify(args, baseUrl, apiKey) {
|
|
|
664
665
|
// belongs to. Matched the same way the verifier matches it, so the emitter
|
|
665
666
|
// and `verifyBundleV3` cannot disagree about the same bundle.
|
|
666
667
|
const relaxedObligations = new Set((relaxations ?? []).map((r) => r.obligation));
|
|
667
|
-
const
|
|
668
|
+
const relaxedProfileIds = profilesRelaxedBy([...relaxedObligations], claimedProfiles);
|
|
669
|
+
const observedRelaxed = profilesDeniedByObservedRelaxation(rows3, claimedProfiles).profiles;
|
|
670
|
+
const relaxedProfile = (p) => relaxedProfileIds.has(p) || observedRelaxed.has(p);
|
|
668
671
|
const claimed3 = claimedProfiles.filter((p) => !(p in DEPRECATED_PROFILE_ALIASES)).map((p) => ({ id: p, evidenceTier: args.evidenceTier, witnessCount: witnessCountFor(p), certified: !relaxedProfile(p) && (verdictFor(p)?.certifiable ?? false) && !notHeld.has(p) && !rejectedProfiles.some((v) => v.profile === p) && totals3.blocked === 0 }));
|
|
669
672
|
const lockPath = resolvePath(conformanceRoot, 'dist', 'spec-artifacts.lock.json');
|
|
670
673
|
const lock = existsSync(lockPath) ? JSON.parse(readFileSync(lockPath, 'utf8')) : undefined;
|
|
@@ -924,7 +927,7 @@ async function main() {
|
|
|
924
927
|
}
|
|
925
928
|
// Env vars OVERRIDE flags only when the flag was unset (consistent
|
|
926
929
|
// with the rest of the harness — env wins on the absence of CLI input).
|
|
927
|
-
const env =
|
|
930
|
+
const env = childEnv(process.env);
|
|
928
931
|
if (args.baseUrl)
|
|
929
932
|
env.OPENWOP_BASE_URL = args.baseUrl;
|
|
930
933
|
if (args.apiKey)
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { createHash, createPrivateKey, createPublicKey, sign as edSign, verify as edVerify } from 'node:crypto';
|
|
21
21
|
import { profileDerivable } from './profiles.js';
|
|
22
|
-
import { v2RegistryAvailable } from './v2-profiles.js';
|
|
22
|
+
import { profilesDeniedByObservedRelaxation, profilesRelaxedBy, v2RegistryAvailable } from './v2-profiles.js';
|
|
23
23
|
export const SIGNATURE_OVER = ['witnessSha256', 'host.build', 'suite.version', 'discovery.sha256'];
|
|
24
24
|
/** Deterministic JSON: keys sorted at every level, no whitespace. */
|
|
25
25
|
export function canonicalJSON(value) {
|
|
@@ -118,9 +118,19 @@ export function verifyBundleV3(bundle, opts = {}) {
|
|
|
118
118
|
}
|
|
119
119
|
// Relaxations: a relaxed obligation's profile cannot certify (RFC 0173 §A.2).
|
|
120
120
|
const relaxed = new Set((bundle.host?.relaxations ?? []).map((r) => r.obligation.split('.')[0]));
|
|
121
|
+
// Ownership comes from the profile registry, not from the profile's name — see profilesRelaxedBy.
|
|
122
|
+
const relaxedProfiles = profilesRelaxedBy((bundle.host?.relaxations ?? []).map((r) => r.obligation), (bundle.claimedProfiles ?? []).map((p) => p.id));
|
|
123
|
+
// An OBSERVED relaxation nobody declared: the bundle's own results show the
|
|
124
|
+
// host accepting a destination its egress guard MUST refuse. Same denial as a
|
|
125
|
+
// declared one, so declaring nothing is not a way around the rule.
|
|
126
|
+
const observed = profilesDeniedByObservedRelaxation(bundle.results?.requirements ?? [], (bundle.claimedProfiles ?? []).map((p) => p.id));
|
|
127
|
+
for (const p of bundle.claimedProfiles ?? []) {
|
|
128
|
+
if (p.certified && observed.profiles.has(p.id) && !relaxedProfiles.has(p.id))
|
|
129
|
+
rejections.push({ kind: 'undeclared-relaxation-observed', profile: p.id, detail: `${p.id} is marked certified, but this bundle's own results show the host running relaxed on ${observed.obligations.join(', ')} with no host.relaxations[] entry recording it (security-defaults.md §Relaxations: every relaxation a host runs under MUST be recorded, and its profile MUST NOT certify)` });
|
|
130
|
+
}
|
|
121
131
|
const certifiedProfiles = [];
|
|
122
132
|
for (const p of bundle.claimedProfiles ?? []) {
|
|
123
|
-
if (p.certified &&
|
|
133
|
+
if (p.certified && relaxedProfiles.has(p.id))
|
|
124
134
|
rejections.push({ kind: 'relaxed-profile-certified', profile: p.id, detail: `${p.id} is marked certified while a relaxation on ${[...relaxed].join(', ')} is recorded (RFC 0173 §A.2)` });
|
|
125
135
|
else if (p.certified)
|
|
126
136
|
certifiedProfiles.push(p.id);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The environment handed to the vitest child the CLI spawns.
|
|
3
|
+
*
|
|
4
|
+
* `{ ...process.env }` handed the child EVERYTHING the operator exported,
|
|
5
|
+
* including `OPENWOP_BUNDLE_SIGNING_KEY` — the PKCS8 PEM of the key that signs
|
|
6
|
+
* the certification bundle. Nothing in the child reads it: the signer runs in
|
|
7
|
+
* the PARENT, after the run (`cli.ts` `--certify`). It was there only because
|
|
8
|
+
* nothing took it out.
|
|
9
|
+
*
|
|
10
|
+
* Why that is a leak and not a tidiness point. Measured by a host operator
|
|
11
|
+
* during a production cut (2026-09-20): on macOS `npm exec` rewrites
|
|
12
|
+
* `process.title`, and a process listing (`ps`, `pgrep -fl`) then spills into
|
|
13
|
+
* the child's environ — so listing processes mid-cut printed the signing key,
|
|
14
|
+
* in full, into the operator's terminal and transcript. Same-uid or root only,
|
|
15
|
+
* but that is exactly who is looking at the terminal. A scenario, a reporter, a
|
|
16
|
+
* vitest plugin or a crash dump in the child had the same access for no reason.
|
|
17
|
+
*
|
|
18
|
+
* The rule: material only the parent uses never enters the child. The key IDS
|
|
19
|
+
* are not secret and stay (a scenario may legitimately name the key it expects).
|
|
20
|
+
*/
|
|
21
|
+
export const PARENT_ONLY_ENV = [
|
|
22
|
+
'OPENWOP_BUNDLE_SIGNING_KEY',
|
|
23
|
+
];
|
|
24
|
+
export function childEnv(parent) {
|
|
25
|
+
const env = { ...parent };
|
|
26
|
+
for (const name of PARENT_ONLY_ENV)
|
|
27
|
+
delete env[name];
|
|
28
|
+
return env;
|
|
29
|
+
}
|
package/dist/lib/v2-profiles.js
CHANGED
|
@@ -104,3 +104,69 @@ export function v2ProfileIds(doc) {
|
|
|
104
104
|
export function v2ProfileDerivable(doc, profile) {
|
|
105
105
|
return v2ProfileIds(doc)?.includes(profile) ?? false;
|
|
106
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Which of `profileIds` a relaxed obligation denies (RFC 0173 §A.2;
|
|
109
|
+
* `security-defaults.md` §Relaxations: "a bundle that records a relaxation MUST
|
|
110
|
+
* NOT certify the profile the relaxed obligation belongs to").
|
|
111
|
+
*
|
|
112
|
+
* An obligation is `<family>.<name>`. A profile OWNS a family when its registry
|
|
113
|
+
* predicate lists it — `openwop-core-standard` is built on `interrupt`, `replay`,
|
|
114
|
+
* `webhooks`, `idempotency`, `eventLog`, so a relaxation on any of those denies
|
|
115
|
+
* it. That ownership was always stated in `spec/v2/profiles.json`; nothing read
|
|
116
|
+
* it. Until 2.33.0 the verifier tested whether the profile ID contained the
|
|
117
|
+
* family name, and the emitter tested whether it contained the WHOLE obligation
|
|
118
|
+
* string — two different rules under a comment saying they matched — and since
|
|
119
|
+
* no real v2 profile id contains a family name, a declared relaxation denied
|
|
120
|
+
* nothing on any real host. The one scenario pinning the rule used a fixture
|
|
121
|
+
* profile called `openwop-webhooks`, which is why it stayed green.
|
|
122
|
+
*
|
|
123
|
+
* The id-substring rule is KEPT as a second arm, not replaced: it is how a
|
|
124
|
+
* non-registry profile id (a v1 alias, a fixture) names its family, and
|
|
125
|
+
* dropping it would un-deny something that is denied today. When the registry
|
|
126
|
+
* is unavailable only that arm can run; the caller already records that layout
|
|
127
|
+
* as `derivabilityChecked: false`.
|
|
128
|
+
*
|
|
129
|
+
* ONE function, called by the emitter and the verifier, so the file and the
|
|
130
|
+
* verdict on the file cannot disagree again.
|
|
131
|
+
*/
|
|
132
|
+
export function profilesRelaxedBy(obligations, profileIds) {
|
|
133
|
+
const families = new Set(obligations.map((o) => o.split('.')[0] ?? '').filter((f) => f.length > 0));
|
|
134
|
+
const out = new Set();
|
|
135
|
+
if (families.size === 0)
|
|
136
|
+
return out;
|
|
137
|
+
const registry = readRegistry();
|
|
138
|
+
const owned = new Map();
|
|
139
|
+
for (const p of registry ?? []) {
|
|
140
|
+
if (typeof p.id === 'string')
|
|
141
|
+
owned.set(p.id, Array.isArray(p.predicate?.families) ? p.predicate.families.map(String) : []);
|
|
142
|
+
}
|
|
143
|
+
for (const id of profileIds) {
|
|
144
|
+
const ownsOne = (owned.get(id) ?? []).some((f) => families.has(f));
|
|
145
|
+
const namesOne = [...families].some((f) => id.includes(f));
|
|
146
|
+
if (ownsOne || namesOne)
|
|
147
|
+
out.add(id);
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The requirement whose FAILURE is an observed, undeclared relaxation.
|
|
153
|
+
*
|
|
154
|
+
* `conformance/README.md` said of `OPENWOP_HOST_RELAXATIONS` that "the suite
|
|
155
|
+
* cannot detect an undeclared one". For the egress guard that was never true:
|
|
156
|
+
* a host that ACCEPTS `https://127.0.0.1/…` as a webhook destination has shown
|
|
157
|
+
* on the wire that its guard is open (`webhooks.md`: "a host MUST reject (400
|
|
158
|
+
* webhook_url_rejected) … RFC 1918 and loopback and link-local ranges").
|
|
159
|
+
* `v2-webhook-egress-refusal` records that as `executed-fail` on this id unless
|
|
160
|
+
* the operator DECLARED the relaxation — in which case {@link profilesRelaxedBy}
|
|
161
|
+
* already denies the profile. Either way a profile built on `webhooks` does not
|
|
162
|
+
* certify on a host whose guard is open; this closes the path where it did,
|
|
163
|
+
* silently, because nobody declared anything.
|
|
164
|
+
*/
|
|
165
|
+
export const UNDECLARED_RELAXATION_WITNESSES = [
|
|
166
|
+
{ requirementId: 'openwop.requirement.0171.webhook-egress-refused', obligation: 'webhooks.egress-guard' },
|
|
167
|
+
];
|
|
168
|
+
/** Profiles that MUST NOT certify because a row above is `executed-fail` in this bundle's own results. */
|
|
169
|
+
export function profilesDeniedByObservedRelaxation(rows, profileIds) {
|
|
170
|
+
const obligations = UNDECLARED_RELAXATION_WITNESSES.filter((w) => rows.some((r) => r.id === w.requirementId && r.result === 'executed-fail')).map((w) => w.obligation);
|
|
171
|
+
return { profiles: profilesRelaxedBy(obligations, profileIds), obligations };
|
|
172
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openwop/openwop-conformance",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.33.0",
|
|
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.
|
|
59
|
+
"@openwop/spec-artifacts": "2.33.0"
|
|
60
60
|
}
|
|
61
61
|
}
|
package/requirements.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"$comment": "GENERATED by conformance/scripts/generate-requirement-registry.mjs — do not edit. One record per it()/test() in src/scenarios. Ids: openwop.it.<file-stem>.<title-slug>[~n] (src/lib/requirement-ids.ts). A record with id null has an interpolated title; its run-time row is keyed by the rendered title and maps here by file+line only. Renamed ids need a row in requirement-aliases.json.",
|
|
3
3
|
"generatedFrom": "src/scenarios/*.test.ts",
|
|
4
4
|
"counts": {
|
|
5
|
-
"files":
|
|
6
|
-
"tests":
|
|
7
|
-
"withStableId":
|
|
5
|
+
"files": 582,
|
|
6
|
+
"tests": 2209,
|
|
7
|
+
"withStableId": 2209,
|
|
8
8
|
"interpolatedTitles": 0,
|
|
9
|
-
"explicitIds":
|
|
9
|
+
"explicitIds": 2163
|
|
10
10
|
},
|
|
11
11
|
"records": [
|
|
12
12
|
{
|
|
@@ -26650,7 +26650,7 @@
|
|
|
26650
26650
|
{
|
|
26651
26651
|
"id": "openwop.it.v2-durability-recovery.accepted-work-survives-a-kill-before-dispatch-and-dispatches-on-resume",
|
|
26652
26652
|
"file": "v2-durability-recovery.test.ts",
|
|
26653
|
-
"line":
|
|
26653
|
+
"line": 262,
|
|
26654
26654
|
"title": "accepted work survives a kill before dispatch and dispatches on resume",
|
|
26655
26655
|
"explicitId": "openwop.requirement.0158.kill-after-accept",
|
|
26656
26656
|
"citations": [
|
|
@@ -26664,10 +26664,15 @@
|
|
|
26664
26664
|
{
|
|
26665
26665
|
"id": "openwop.it.v2-durability-recovery.work-executing-at-a-real-process-death-is-never-reported-complete-and-resumes",
|
|
26666
26666
|
"file": "v2-durability-recovery.test.ts",
|
|
26667
|
-
"line":
|
|
26667
|
+
"line": 307,
|
|
26668
26668
|
"title": "work executing at a real process death is never reported complete, and resumes",
|
|
26669
26669
|
"explicitId": "openwop.requirement.0158.kill-during-execution",
|
|
26670
26670
|
"citations": [
|
|
26671
|
+
{
|
|
26672
|
+
"section": "RFC 0158 §B.4 / §E item 11",
|
|
26673
|
+
"requirement": null,
|
|
26674
|
+
"interpolated": true
|
|
26675
|
+
},
|
|
26671
26676
|
{
|
|
26672
26677
|
"section": "RFC 0158 §B.4 / §E item 11",
|
|
26673
26678
|
"requirement": null,
|
|
@@ -26678,10 +26683,15 @@
|
|
|
26678
26683
|
{
|
|
26679
26684
|
"id": "openwop.it.v2-durability-recovery.the-same-accepted-work-delivered-twice-fires-each-effect-exactly-once",
|
|
26680
26685
|
"file": "v2-durability-recovery.test.ts",
|
|
26681
|
-
"line":
|
|
26686
|
+
"line": 354,
|
|
26682
26687
|
"title": "the same accepted work delivered twice fires each effect exactly once",
|
|
26683
26688
|
"explicitId": "openwop.requirement.0158.duplicate-delivery",
|
|
26684
26689
|
"citations": [
|
|
26690
|
+
{
|
|
26691
|
+
"section": "RFC 0158 §C",
|
|
26692
|
+
"requirement": null,
|
|
26693
|
+
"interpolated": true
|
|
26694
|
+
},
|
|
26685
26695
|
{
|
|
26686
26696
|
"section": "RFC 0158 §C",
|
|
26687
26697
|
"requirement": null,
|
|
@@ -26692,7 +26702,7 @@
|
|
|
26692
26702
|
{
|
|
26693
26703
|
"id": "openwop.it.v2-durability-recovery.the-declared-recovery-bound-is-derived-from-the-mechanism-that-enforces-it",
|
|
26694
26704
|
"file": "v2-durability-recovery.test.ts",
|
|
26695
|
-
"line":
|
|
26705
|
+
"line": 428,
|
|
26696
26706
|
"title": "the declared recovery bound is derived from the mechanism that enforces it",
|
|
26697
26707
|
"explicitId": "openwop.requirement.0158.bound-is-derived",
|
|
26698
26708
|
"citations": [
|
|
@@ -26706,7 +26716,7 @@
|
|
|
26706
26716
|
{
|
|
26707
26717
|
"id": "openwop.it.v2-durability-recovery.deterministically-failing-work-reaches-a-terminal-state-and-stops-being-retried",
|
|
26708
26718
|
"file": "v2-durability-recovery.test.ts",
|
|
26709
|
-
"line":
|
|
26719
|
+
"line": 458,
|
|
26710
26720
|
"title": "deterministically failing work reaches a terminal state and stops being retried",
|
|
26711
26721
|
"explicitId": "openwop.requirement.0158.poison-exhaustion",
|
|
26712
26722
|
"citations": [
|
|
@@ -28377,7 +28387,7 @@
|
|
|
28377
28387
|
{
|
|
28378
28388
|
"id": "openwop.it.v2-relaxation-recorded.a-bundle-recording-a-relaxation-cannot-certify-the-relaxed-profile",
|
|
28379
28389
|
"file": "v2-relaxation-recorded.test.ts",
|
|
28380
|
-
"line":
|
|
28390
|
+
"line": 70,
|
|
28381
28391
|
"title": "a bundle recording a relaxation cannot certify the relaxed profile",
|
|
28382
28392
|
"explicitId": "openwop.requirement.0173.relaxation-recorded",
|
|
28383
28393
|
"citations": [
|
|
@@ -28411,10 +28421,35 @@
|
|
|
28411
28421
|
}
|
|
28412
28422
|
]
|
|
28413
28423
|
},
|
|
28424
|
+
{
|
|
28425
|
+
"id": "openwop.it.v2-relaxation-recorded.a-relaxation-denies-the-real-profile-that-owns-its-family-not-only-one-named-aft",
|
|
28426
|
+
"file": "v2-relaxation-recorded.test.ts",
|
|
28427
|
+
"line": 118,
|
|
28428
|
+
"title": "a relaxation denies the REAL profile that owns its family, not only one named after it",
|
|
28429
|
+
"explicitId": "openwop.requirement.0173.relaxation-recorded.owned-by-registry",
|
|
28430
|
+
"citations": [
|
|
28431
|
+
{
|
|
28432
|
+
"section": "security-defaults.md §Relaxations",
|
|
28433
|
+
"requirement": "control: with no relaxation recorded, the real profile certifies — so the rejection below is the relaxation's"
|
|
28434
|
+
},
|
|
28435
|
+
{
|
|
28436
|
+
"section": "security-defaults.md §Relaxations",
|
|
28437
|
+
"requirement": "a relaxation on `webhooks.*` MUST deny `openwop-core-standard`, whose registry predicate is built on the `webhooks` family — ownership is read from spec/v2/profiles.json, never from the spelling of the profile id"
|
|
28438
|
+
},
|
|
28439
|
+
{
|
|
28440
|
+
"section": "security-defaults.md §Relaxations",
|
|
28441
|
+
"requirement": "the owning profile is not certified, and a profile that does not own the family still is"
|
|
28442
|
+
},
|
|
28443
|
+
{
|
|
28444
|
+
"section": "security-defaults.md §Relaxations",
|
|
28445
|
+
"requirement": "a relaxation on a family outside every claimed profile's predicate is scoped to nothing claimed — it does not poison an unrelated profile"
|
|
28446
|
+
}
|
|
28447
|
+
]
|
|
28448
|
+
},
|
|
28414
28449
|
{
|
|
28415
28450
|
"id": "openwop.it.v2-relaxation-recorded.durability-is-a-closed-set-session-deployment-persisted",
|
|
28416
28451
|
"file": "v2-relaxation-recorded.test.ts",
|
|
28417
|
-
"line":
|
|
28452
|
+
"line": 143,
|
|
28418
28453
|
"title": "durability is a closed set: session | deployment | persisted",
|
|
28419
28454
|
"explicitId": "openwop.requirement.0173.relaxation-recorded.durability-closed",
|
|
28420
28455
|
"citations": [
|
|
@@ -29553,6 +29588,25 @@
|
|
|
29553
29588
|
}
|
|
29554
29589
|
]
|
|
29555
29590
|
},
|
|
29591
|
+
{
|
|
29592
|
+
"id": "openwop.it.v2-webhook-egress-refusal.a-webhook-registration-naming-a-destination-the-guard-must-refuse-is-refused-400",
|
|
29593
|
+
"file": "v2-webhook-egress-refusal.test.ts",
|
|
29594
|
+
"line": 82,
|
|
29595
|
+
"title": "a webhook registration naming a destination the guard MUST refuse is refused 400 webhook_url_rejected",
|
|
29596
|
+
"explicitId": "openwop.requirement.0171.webhook-egress-refused",
|
|
29597
|
+
"citations": [
|
|
29598
|
+
{
|
|
29599
|
+
"section": "spec/v2/core/webhooks.md §SSRF",
|
|
29600
|
+
"requirement": null,
|
|
29601
|
+
"interpolated": true
|
|
29602
|
+
},
|
|
29603
|
+
{
|
|
29604
|
+
"section": "spec/v2/core/webhooks.md §SSRF",
|
|
29605
|
+
"requirement": null,
|
|
29606
|
+
"interpolated": true
|
|
29607
|
+
}
|
|
29608
|
+
]
|
|
29609
|
+
},
|
|
29556
29610
|
{
|
|
29557
29611
|
"id": "openwop.it.v2-well-known-one-resource.a-header-less-get-and-an-openwop-version-2-0-get-are-two-representations-of-one",
|
|
29558
29612
|
"file": "v2-well-known-one-resource.test.ts",
|
|
@@ -36398,7 +36452,7 @@
|
|
|
36398
36452
|
{
|
|
36399
36453
|
"id": "openwop.it.v2-one-signing-scheme.registry-version-manifest-signing-has-exactly-one-scheme-ed25519-canonical-json",
|
|
36400
36454
|
"file": "v2-one-signing-scheme.test.ts",
|
|
36401
|
-
"line":
|
|
36455
|
+
"line": 42,
|
|
36402
36456
|
"title": "registry-version-manifest signing has exactly one scheme, ed25519-canonical-json, and no method / publicKeyRef",
|
|
36403
36457
|
"explicitId": "openwop.requirement.0177.one-signing-scheme",
|
|
36404
36458
|
"citations": [
|
|
@@ -36428,6 +36482,70 @@
|
|
|
36428
36482
|
}
|
|
36429
36483
|
]
|
|
36430
36484
|
},
|
|
36485
|
+
{
|
|
36486
|
+
"id": "openwop.it.v2-one-signing-scheme.every-bare-pack-manifest-carries-the-same-closed-keyid-scheme-signing-block-and",
|
|
36487
|
+
"file": "v2-one-signing-scheme.test.ts",
|
|
36488
|
+
"line": 62,
|
|
36489
|
+
"title": "every bare *-pack-manifest carries the same closed { keyId, scheme } signing block, and none keeps the v1 one",
|
|
36490
|
+
"explicitId": "openwop.requirement.0177.one-signing-scheme.bare-manifests",
|
|
36491
|
+
"citations": [
|
|
36492
|
+
{
|
|
36493
|
+
"section": "RFC 0177 §C.3",
|
|
36494
|
+
"requirement": "schemas/v2/ MUST hold the bare pack-manifest schemas this rule covers"
|
|
36495
|
+
},
|
|
36496
|
+
{
|
|
36497
|
+
"section": "RFC 0177 §C.3",
|
|
36498
|
+
"requirement": null,
|
|
36499
|
+
"interpolated": true
|
|
36500
|
+
},
|
|
36501
|
+
{
|
|
36502
|
+
"section": "RFC 0177 §C.3",
|
|
36503
|
+
"requirement": null,
|
|
36504
|
+
"interpolated": true
|
|
36505
|
+
},
|
|
36506
|
+
{
|
|
36507
|
+
"section": "RFC 0177 §C.3",
|
|
36508
|
+
"requirement": null,
|
|
36509
|
+
"interpolated": true
|
|
36510
|
+
},
|
|
36511
|
+
{
|
|
36512
|
+
"section": "RFC 0177 §C.3",
|
|
36513
|
+
"requirement": null,
|
|
36514
|
+
"interpolated": true
|
|
36515
|
+
},
|
|
36516
|
+
{
|
|
36517
|
+
"section": "RFC 0177 §C.3",
|
|
36518
|
+
"requirement": null,
|
|
36519
|
+
"interpolated": true
|
|
36520
|
+
},
|
|
36521
|
+
{
|
|
36522
|
+
"section": "RFC 0177 §C.3",
|
|
36523
|
+
"requirement": null,
|
|
36524
|
+
"interpolated": true
|
|
36525
|
+
}
|
|
36526
|
+
]
|
|
36527
|
+
},
|
|
36528
|
+
{
|
|
36529
|
+
"id": "openwop.it.v2-one-signing-scheme.fragmentnode-config-is-open-exactly-as-the-workflownode-config-it-expands-into-a",
|
|
36530
|
+
"file": "v2-one-signing-scheme.test.ts",
|
|
36531
|
+
"line": 90,
|
|
36532
|
+
"title": "FragmentNode.config is open exactly as the WorkflowNode.config it expands into, and keeps its documented subChainRef key",
|
|
36533
|
+
"explicitId": "openwop.requirement.0177.chain-fragment-config-open",
|
|
36534
|
+
"citations": [
|
|
36535
|
+
{
|
|
36536
|
+
"section": "spec/v2/core/workflow-chain-packs.md §Parameter substitution",
|
|
36537
|
+
"requirement": "WorkflowNode.config is a free-form map validated against the typeId's own config schema — the premise this rule compares against"
|
|
36538
|
+
},
|
|
36539
|
+
{
|
|
36540
|
+
"section": "spec/v2/core/workflow-chain-packs.md §Parameter substitution",
|
|
36541
|
+
"requirement": "FragmentNode.config MUST be open: a fragment node expands into a WorkflowNode, so a config the expanded node may carry MUST be expressible in the fragment"
|
|
36542
|
+
},
|
|
36543
|
+
{
|
|
36544
|
+
"section": "spec/v2/core/workflow-chain-packs.md §Parameter substitution",
|
|
36545
|
+
"requirement": "opening the map MUST NOT drop the documented `subChainRef` key (RFC 0133)"
|
|
36546
|
+
}
|
|
36547
|
+
]
|
|
36548
|
+
},
|
|
36431
36549
|
{
|
|
36432
36550
|
"id": "openwop.it.v2-path-parity.the-corpus-gate-script-exits-0",
|
|
36433
36551
|
"file": "v2-path-parity.test.ts",
|
package/scenario-majors.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$comment": "GENERATED by conformance/scripts/generate-scenario-majors.mjs (RFC 0168 §D.3). Do not edit; add a file to BOTH_MAJORS in the generator to target both majors.",
|
|
3
3
|
"counts": {
|
|
4
|
-
"files":
|
|
4
|
+
"files": 524,
|
|
5
5
|
"v1": 445,
|
|
6
|
-
"v2":
|
|
6
|
+
"v2": 81
|
|
7
7
|
},
|
|
8
8
|
"majors": {
|
|
9
9
|
"a2a-1-0-agent-card.test.ts": [
|
|
@@ -1463,6 +1463,9 @@
|
|
|
1463
1463
|
"v2-webhook-durable-delivery.test.ts": [
|
|
1464
1464
|
2
|
|
1465
1465
|
],
|
|
1466
|
+
"v2-webhook-egress-refusal.test.ts": [
|
|
1467
|
+
2
|
|
1468
|
+
],
|
|
1466
1469
|
"v2-well-known-one-resource.test.ts": [
|
|
1467
1470
|
2
|
|
1468
1471
|
],
|