@openwop/openwop-conformance 2.40.0 → 2.40.2

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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # `@openwop/openwop-conformance` Changelog
2
2
 
3
+ ## [2.40.2] — 2026-09-26 — production bundles stop carrying harness-issuer blocked rows, and a colocated companion is marked
4
+
5
+ - **RFC 0216: `--as-colocated-companion`, and a companion witnesses only the harness-trust-anchor rows.** The flag marks a v3 bundle `host.deployment: "colocated-companion"`. It is refused without `--certify` and bundle version 3. `witnessDigest` takes the marker into the preimage when present (`{ rows, relaxations?, deployment }`), so a signed companion cannot shed it: `--verify` then refuses with `witness-digest`. New coherence scenario `v2-colocated-companion` (7 legs, one id each) drives `scripts/check-harness-trust-anchors.mjs` and `scripts/check-companion-pairing.mjs` on tmpdir fixtures signed by the suite's own `signBundleV3`. Five sabotages were each run red.
6
+ - **The harness OIDC issuer is an instrument a host claims, so an honest production bundle is no longer uncertifiable.** Before this, `v2-oidc-id-token-audience` (`0200.id-token-aud`) and `v2-lane-exp-only-bound` (the four `0210.exp-only-*` / `credential-lifetime-code` rows) recorded `blocked` whenever the host did not trust `OPENWOP_TEST_OIDC_ISSUER_URL`. A production host must never trust a test issuer, and one `blocked` row denies certification bundle-wide (RFC 0168 §E.1). Every production bundle advertising an `oidc` or `exp-only` lane was therefore uncertifiable (openwop-app ADR 0745, corpus defect 1). The rows are `seam-gated`, and RFC 0168 §C.1 already rules that an unclaimed suite instrument is `inapplicable`, with `blocked` reserved for a claimed instrument that does not answer. The new `lib/harness-issuer.ts` treats the harness as claimed when the gated lane's advertised `issuers[]` lists it. When the lane does not list it, the row is `inapplicable` and names the issuers the host does trust. When it does, the legs run as before (a failed serve or a refused control is still `blocked`). RFC 0200 and RFC 0210 carry dated correction notes, and `SECURITY/invariants.yaml` `lane-exp-only-lifetime-bounded` is updated. No MUST changes. The residual is stated in the RFC 0210 note: a production bundle does not witness these rows, just as it witnesses no seam-gated row. The witness comes from a cut whose lane lists the harness. **The issuer now has a bind knob:** it listens via `receiverBinding()`, which is loopback, or `0.0.0.0` under `OPENWOP_CONFORMANCE_HARNESS_HOST`, on 2.39.4's `issuerListenPort()`. It waits out `EADDRINUSE` while the other scenario holds the shared URL. Before this, it always bound `127.0.0.1`, so a host in a Linux container reaching it through `host-gateway` found nothing listening (ADR 0745 defect 2; 2.39.4 fixed the port half). **`requirements.json` resolves `const ID` declared inside a test body** (`generate-requirement-registry.mjs`). Before, only module-level constants resolved, so 14 real ids recorded `explicitId: null`: `0210.lane-rule-surfaces-agree` (ADR 0745 defect 4), three `0199.*`, six `0201.*` and four `0214.*`. Those rows were labelled by their title-derived id whenever the leg did not reach its assertion. ADR 0745 defect 5, the reused kid, was already fixed in 2.37.0: every instance mints `openwop-conformance-key-<instance>-<n>`, pinned by `oidc-issuer.test.ts`. New suite self-test `src/lib/harness-issuer.test.ts` covers the claim predicate and the port knob. It is sabotage-proved: dropping the `issuers[]` check makes it fail 2/4.
7
+ - **Version moved ahead of publication.** `@openwop/openwop-conformance` and its exact-pinned peer `@openwop/spec-artifacts` move to `2.40.2` because `2.40.1` is released, and `spec-artifacts` now carries a changed `spec/v2/core/oauth.md` (RFC 0199's text restored; editorial, `COMPATIBILITY.md` §3 entry of 2026-09-26). No scenario changes. Not tagged, not published.
8
+
9
+ ## [2.40.1] — 2026-09-26 — three declared-unwitnessable rows are reached and say why
10
+
11
+ - **Three Falsifiability rows declared non-executable now have a leg that records the reason** (RFC 0174 §B.1 rule 4, second branch: "the suite must reach the leg and record a reason"). Each RFC's table declares one of these rows unwitnessable, but no suite leg referenced it, so no bundle could account for it. Each of RFCs 0197, 0205 and 0209 was held from `Accepted` by that one row. No leg can record `executed-pass` without observing the requirement.
12
+ - `openwop.requirement.0197.retired-not-emitted` (`v2-capability-maturity-bounded`, new describe) is **conditional, not constant**. It reads `spec/v1/deprecations.json`, `spec/v2/migrations.json` and `spec/v2/release.json` from the shipped corpus. While no `v2-minor` row with `retirement.persistence: "persisted"` is due, it records `inapplicable` with RFC 0197 §A.3's reason; that is today's state, with 0 rows. Once one is due, it locates the member by the schema's `x-openwop-retired-in` annotation (a property name, or a `const` / single-`enum` value). It then drives a `conformance-noop` run and fails on any event that carries the member. A due row whose member it cannot locate records `blocked`. The branches no host can reach yet are self-tested in `src/lib/v2-retired-members.test.ts` (due / not-due / advertised-class / unannotated / caught emission / clean event).
13
+ - `openwop.requirement.0205.export-alias-equivalence` (`v2-artifact-a2a-shape`, new describe) records `inapplicable` on every host, before any request, with RFC 0205 §C.11's verdict: consumer matching logic and a pack-author call, neither on the wire.
14
+ - `openwop.requirement.0209.render-needs-root` (`v2-a2ui-v09-surface`, new describe) records `inapplicable` on every host, before any request or seam gate, with RFC 0209 §C.9's verdict. It names the reference-impl witness: openwop-app `frontend/react/src/chat/a2ui/__tests__/a2ui-v09-render-needs-root.test.tsx` (openwop-app#4121).
15
+ - **Version moved ahead of publication.** `@openwop/openwop-conformance` and its exact-pinned peer `@openwop/spec-artifacts` move to `2.40.1` because `2.40.0` is tagged. Not tagged, not published. 561 scenario files, unchanged, so this is a patch.
16
+
3
17
  ## [2.40.0] — 2026-09-26 — RFC 0215: one subscription's dead receiver cannot hold another's delivery, and unregistering stops the retries
4
18
 
5
19
  - **Two new scenarios, gated on `webhooks` (RFC 0215, `Active` 2026-09-25).** `v2-webhook-delivery-isolation` (`openwop.requirement.0215.no-head-of-line`) holds 8 subscriptions' attempts unanswered, then checks that a ninth subscription's attempt still starts. The 8 filter `run.started` and the ninth `run.completed` of a 2 s `conformance-delay` run, so contention exists before the healthy delivery falls due and the host's dispatch order cannot decide the row. It fails when the held attempts were still open at the due time and the healthy one waited for the host to release one. It records `partial-witness` when the host's own delivery timeout closed them first. `v2-webhook-unregister-stops-delivery` (`openwop.requirement.0215.unregister-stops-delivery`) answers `500` to two subscriptions on one run, unregisters one after its first attempt, and fails if that one is attempted more than 5 s after the `204`. It passes only when the other, the control, was retried after that point: a silence counts only when a retry demonstrably would have come. Sabotage on the v2 reference host: a sequential worker and a 5-slot pool each fail the first scenario, and an attempt that falls back to a cached subscription after the delete fails the second.
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.40.0 @openwop/spec-artifacts@2.40.0
14
+ npm install --legacy-peer-deps @openwop/openwop-conformance@2.40.2 @openwop/spec-artifacts@2.40.2
15
15
  # or run without install:
16
16
  npx @openwop/openwop-conformance --base-url https://api.example.com --api-key hk_test_...
17
17
  ```
package/dist/cli.js CHANGED
@@ -70,6 +70,7 @@ function parseArgs(argv) {
70
70
  let signingKeyPath, signingKeyId, verifierKeyPath, verifierKeyId;
71
71
  let targetMajor;
72
72
  let maxWorkers = parseMaxWorkers(process.env.OPENWOP_MAX_WORKERS, 'OPENWOP_MAX_WORKERS');
73
+ let colocatedCompanion = false;
73
74
  for (let i = 0; i < argv.length; i++) {
74
75
  const arg = argv[i] ?? '';
75
76
  if (arg === '-h' || arg === '--help') {
@@ -180,6 +181,12 @@ function parseArgs(argv) {
180
181
  case '--host-key':
181
182
  verifyHostKeyPath = nextValue();
182
183
  break;
184
+ // RFC 0216 §B — marks the bundle as cut from a colocated companion (the
185
+ // served host's image beside the suite, trusting a suite-held anchor).
186
+ // The marker is inside witnessSha256, so a signed companion cannot shed it.
187
+ case '--as-colocated-companion':
188
+ colocatedCompanion = true;
189
+ break;
183
190
  case '--require-behavior':
184
191
  process.env['OPENWOP_REQUIRE_BEHAVIOR'] = 'true';
185
192
  break;
@@ -215,6 +222,7 @@ function parseArgs(argv) {
215
222
  maxWorkers,
216
223
  verifyPath,
217
224
  verifyHostKeyPath,
225
+ colocatedCompanion,
218
226
  };
219
227
  }
220
228
  /** Parse a `--max-workers` / `OPENWOP_MAX_WORKERS` value: a positive integer, else exit 2. */
@@ -261,6 +269,10 @@ Certification (RFC 0089):
261
269
  §C) records per-requirement DISPOSITIONS instead of pass/fail/skip
262
270
  file lists, so "we could not check" stops being indistinguishable
263
271
  from "checked and it holds". See the note it prints.
272
+ --as-colocated-companion v3 (RFC 0216): mark the bundle as cut from a colocated companion — the
273
+ served host's image run beside the suite so it can trust a suite-held
274
+ trust anchor. Signed (inside witnessSha256). A companion is acceptance
275
+ evidence only for spec/v2/harness-trust-anchors.json rows.
264
276
  --max-workers <n> Cap concurrently running scenario files (vitest --maxWorkers).
265
277
  Default: one worker per CPU. Use a small number against a
266
278
  rate-limited production origin so 429s don't read as failures.
@@ -680,7 +692,7 @@ async function runCertify(args, baseUrl, apiKey) {
680
692
  bundleVersion: '3',
681
693
  generatedAt: new Date().toISOString(),
682
694
  suite: { name: '@openwop/openwop-conformance', version, targetMajor: target.major, specArtifactsVersion: lock?.version ?? 'repo-layout', ...(lock ? { stampSha256: lock.stampSha256 } : {}) },
683
- host: { name: host.name, version: host.version, ...(host.vendor ? { vendor: host.vendor } : {}), build, signingKeyId: keyId, ...(relaxations && relaxations.length ? { relaxations } : {}) },
695
+ host: { name: host.name, version: host.version, ...(host.vendor ? { vendor: host.vendor } : {}), build, signingKeyId: keyId, ...(relaxations && relaxations.length ? { relaxations } : {}), ...(args.colocatedCompanion ? { deployment: 'colocated-companion' } : {}) },
684
696
  // `document` is what makes `claimedProfiles[].certified` checkable by
685
697
  // someone other than this process (RFC 0148 §B(1)); v2 carried it and v3
686
698
  // dropped it. `sha256` is a digest of `canonicalJSON(document)`, so the
@@ -688,7 +700,7 @@ async function runCertify(args, baseUrl, apiKey) {
688
700
  discovery: { url: discoveryUrl, sha256, protocolVersions, preferredVersion, document },
689
701
  claimedProfiles: claimed3,
690
702
  results: { totals: totals3, requirements: rows3 },
691
- witnessSha256: witnessDigest(rows3, relaxations),
703
+ witnessSha256: witnessDigest(rows3, relaxations, args.colocatedCompanion ? 'colocated-companion' : undefined),
692
704
  assertionCount: rows3.reduce((n, r) => n + (r.assertions ?? 0), 0),
693
705
  ...(nonPass.length ? { detail: { nonPass: nonPass.map((r) => ({ id: r.id, result: r.result, reason: r.detail ?? '' })) } } : {}),
694
706
  // RFC 0158 §D: claimed ONLY when these rows support it. The verifier
@@ -856,6 +868,10 @@ async function runCertify(args, baseUrl, apiKey) {
856
868
  }
857
869
  async function main() {
858
870
  const args = parseArgs(process.argv.slice(2));
871
+ if (args.colocatedCompanion && (!args.certify || args.bundleVersion !== '3')) {
872
+ process.stderr.write('openwop-conformance: --as-colocated-companion marks a v3 certification bundle; it needs --certify with bundle version 3 (RFC 0216 §B)\n');
873
+ process.exit(2);
874
+ }
859
875
  // Refused before anything runs: a pinned-port certification that is not
860
876
  // single-worker loses the host's traffic to a worker nobody reads (lib/pinned-ports.ts).
861
877
  const pinnedConflict = pinnedPortWorkerConflict(process.env, args.maxWorkers, args.certify !== undefined);
@@ -49,12 +49,20 @@ export { canonicalJSON } from './jcs.js';
49
49
  * from the same bundle anywhere else (Czech sorts `ch` after `h`). Every
50
50
  * committed bundle's ids are `[a-z0-9.-]`, where the two orders coincide, so no
51
51
  * stored digest changes (v2-bundle-witness-preimage.test.ts pins that).
52
+ *
53
+ * RFC 0216 §B.7 — a colocated companion's `host.deployment` joins the preimage
54
+ * the same way, ONLY WHEN PRESENT (`{ rows, relaxations?, deployment }`), so a
55
+ * signed companion cannot shed the marker and pass as a served-host bundle, and
56
+ * every bundle without it digests exactly as before.
52
57
  */
53
- export function witnessDigest(rows, relaxations) {
58
+ export function witnessDigest(rows, relaxations, deployment) {
54
59
  const canonicalRows = [...rows].sort((a, b) => codeUnitCompare(a.id, b.id)).map((r) => ({ id: r.id, scenario: r.scenario, result: r.result, ...(r.assertions === undefined ? {} : { assertions: r.assertions }), ...(r.detail === undefined ? {} : { detail: r.detail }),
55
60
  // ONLY WHEN PRESENT: every bundle cut before 2.34.0 has no `evidence` and digests byte-identically.
56
61
  ...(r.evidence === undefined ? {} : { evidence: r.evidence }) }));
57
- const preimage = relaxations !== undefined && relaxations.length > 0 ? { rows: canonicalRows, relaxations } : canonicalRows;
62
+ const relaxed = relaxations !== undefined && relaxations.length > 0;
63
+ const preimage = relaxed || deployment !== undefined
64
+ ? { rows: canonicalRows, ...(relaxed ? { relaxations } : {}), ...(deployment !== undefined ? { deployment } : {}) }
65
+ : canonicalRows;
58
66
  return createHash('sha256').update(canonicalJSON(preimage), 'utf8').digest('hex');
59
67
  }
60
68
  /** The bytes the attestation covers. */
@@ -140,9 +148,9 @@ export function verifyBundleV3(bundle, opts = {}) {
140
148
  for (const k of Object.keys(expected))
141
149
  if (bundle.results?.totals?.[k] !== expected[k])
142
150
  rejections.push({ kind: 'totals-mismatch', detail: `totals.${k} is ${String(bundle.results?.totals?.[k])} but the rows count ${expected[k]}` });
143
- const digest = refusedAs(rejections, 'witness-digest', 'the rows or declared relaxations', () => witnessDigest(rows, bundle.host?.relaxations));
151
+ const digest = refusedAs(rejections, 'witness-digest', 'the rows or declared relaxations', () => witnessDigest(rows, bundle.host?.relaxations, bundle.host?.deployment));
144
152
  if (digest !== undefined && bundle.witnessSha256 !== digest)
145
- rejections.push({ kind: 'witness-digest', detail: `witnessSha256 ${String(bundle.witnessSha256).slice(0, 12)} does not equal the digest of the rows and declared relaxations (${digest.slice(0, 12)})` });
153
+ rejections.push({ kind: 'witness-digest', detail: `witnessSha256 ${String(bundle.witnessSha256).slice(0, 12)} does not equal the digest of the rows, declared relaxations and deployment marker (${digest.slice(0, 12)})` });
146
154
  const assertions = rows.reduce((n, r) => n + (r.assertions ?? 0), 0);
147
155
  if (bundle.assertionCount !== assertions)
148
156
  rejections.push({ kind: 'assertion-count', detail: `assertionCount is ${String(bundle.assertionCount)} but the rows sum to ${assertions}` });
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "package": "@openwop/spec-artifacts",
3
- "version": "2.40.0",
4
- "stampSha256": "f65c56d3a36eb7301c4baceef9ce0899091151fb12bc72c1508eb7a7d7b4ca4c"
3
+ "version": "2.40.2",
4
+ "stampSha256": "5b784776f9088d58f1fd29e14efa113715876aac5940b728f775a779c59a962a"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwop/openwop-conformance",
3
- "version": "2.40.0",
3
+ "version": "2.40.2",
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.40.0"
59
+ "@openwop/spec-artifacts": "2.40.2"
60
60
  }
61
61
  }
@@ -167,6 +167,8 @@
167
167
  "openwop.it.v2-security-defaults-table.the-obligation-table-has-a-witness-column-and-no-row-is-unwitnessable": "openwop.requirement.0173.security-defaults-table",
168
168
  "openwop.it.v2-spec-artifacts-digest.the-corpus-gate-script-exits-0": "openwop.requirement.0168.spec-artifacts-digest",
169
169
  "openwop.it.v2-threat-model-template.the-corpus-gate-script-exits-0": "openwop.requirement.0173.threat-model-template",
170
- "openwop.it.v2-witness-required.the-corpus-gate-script-exits-0": "openwop.requirement.0168.witness-required"
170
+ "openwop.it.v2-witness-required.the-corpus-gate-script-exits-0": "openwop.requirement.0168.witness-required",
171
+ "openwop.it.bundle-maturity-gate.the-committed-corpus-is-green-and-the-report-names-the-overstated-families-and-t": "openwop.it.bundle-maturity-gate.the-committed-corpus-is-green-and-the-gate-states-when-c-7-binds",
172
+ "openwop.it.bundle-maturity-gate.the-same-committed-bundle-re-labelled-onto-the-rfc-s-suite-fails-naming-every-ov": "openwop.it.bundle-maturity-gate.the-superseded-myndhyve-bundle-re-labelled-onto-the-rfc-s-suite-fails-naming-eve"
171
173
  }
172
174
  }