@openwop/openwop-conformance 2.40.1 → 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 +6 -0
- package/README.md +1 -1
- package/dist/cli.js +18 -2
- package/dist/lib/certification-bundle-v3.js +12 -4
- package/dist/spec-artifacts.lock.json +2 -2
- package/package.json +2 -2
- package/requirements.json +198 -38
- package/schemas/CORPUS-STAMP.json +15 -13
- package/src/cli.ts +20 -2
- package/src/lib/certification-bundle-v3.ts +15 -5
- package/src/lib/harness-issuer.ts +101 -0
- package/src/scenarios/v2-lane-exp-only-bound.test.ts +22 -19
- package/src/scenarios/v2-oidc-id-token-audience.test.ts +24 -23
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
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
|
+
|
|
3
9
|
## [2.40.1] — 2026-09-26 — three declared-unwitnessable rows are reached and say why
|
|
4
10
|
|
|
5
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.
|
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.
|
|
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
|
|
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
|
|
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}` });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openwop/openwop-conformance",
|
|
3
|
-
"version": "2.40.
|
|
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.
|
|
59
|
+
"@openwop/spec-artifacts": "2.40.2"
|
|
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": 636,
|
|
6
|
+
"tests": 2437,
|
|
7
|
+
"withStableId": 2437,
|
|
8
8
|
"interpolatedTitles": 0,
|
|
9
|
-
"explicitIds":
|
|
9
|
+
"explicitIds": 2331
|
|
10
10
|
},
|
|
11
11
|
"records": [
|
|
12
12
|
{
|
|
@@ -26810,7 +26810,7 @@
|
|
|
26810
26810
|
"file": "v2-a2a-operation-map.test.ts",
|
|
26811
26811
|
"line": 275,
|
|
26812
26812
|
"title": "a host not advertising a2a.pushNotifications refuses all four push-config operations with PushNotificationNotSupportedError",
|
|
26813
|
-
"explicitId":
|
|
26813
|
+
"explicitId": "openwop.requirement.0214.a2a-push-unadvertised-refused",
|
|
26814
26814
|
"citations": [
|
|
26815
26815
|
{
|
|
26816
26816
|
"section": "spec/v2/core/interop.md §\"The operation mappings\" (RFC 0208)",
|
|
@@ -26823,7 +26823,7 @@
|
|
|
26823
26823
|
"interpolated": true
|
|
26824
26824
|
},
|
|
26825
26825
|
{
|
|
26826
|
-
"section":
|
|
26826
|
+
"section": "interop-map.json a2a.errors PushNotificationNotSupportedError (serverWhen: push config without a2a.pushNotifications) and the push-config operation rows; A2A v1.0.1 §3.3.4",
|
|
26827
26827
|
"requirement": null,
|
|
26828
26828
|
"interpolated": true
|
|
26829
26829
|
}
|
|
@@ -26834,7 +26834,7 @@
|
|
|
26834
26834
|
"file": "v2-a2a-operation-map.test.ts",
|
|
26835
26835
|
"line": 306,
|
|
26836
26836
|
"title": "RFC 0214 — CreateTaskPushNotificationConfig refuses a non-https or private destination",
|
|
26837
|
-
"explicitId":
|
|
26837
|
+
"explicitId": "openwop.requirement.0214.a2a-push-register-ssrf",
|
|
26838
26838
|
"citations": [
|
|
26839
26839
|
{
|
|
26840
26840
|
"section": "spec/v2/core/interop.md §\"The operation mappings\" (RFC 0208)",
|
|
@@ -26842,12 +26842,12 @@
|
|
|
26842
26842
|
"interpolated": true
|
|
26843
26843
|
},
|
|
26844
26844
|
{
|
|
26845
|
-
"section":
|
|
26845
|
+
"section": "interop-map.json a2a.operations CreateTaskPushNotificationConfig (\"the url MUST pass the webhooks.md egress guard\"); webhooks.md §Egress; RFC 0214 §B",
|
|
26846
26846
|
"requirement": null,
|
|
26847
26847
|
"interpolated": true
|
|
26848
26848
|
},
|
|
26849
26849
|
{
|
|
26850
|
-
"section":
|
|
26850
|
+
"section": "interop-map.json a2a.operations CreateTaskPushNotificationConfig (\"the url MUST pass the webhooks.md egress guard\"); webhooks.md §Egress; RFC 0214 §B",
|
|
26851
26851
|
"requirement": null,
|
|
26852
26852
|
"interpolated": true
|
|
26853
26853
|
}
|
|
@@ -26858,41 +26858,40 @@
|
|
|
26858
26858
|
"file": "v2-a2a-operation-map.test.ts",
|
|
26859
26859
|
"line": 324,
|
|
26860
26860
|
"title": "RFC 0214 §E — a push config of another tenant's task, of another task, or unknown, is answered identically",
|
|
26861
|
-
"explicitId":
|
|
26861
|
+
"explicitId": "openwop.requirement.0214.a2a-push-config-isolation",
|
|
26862
26862
|
"citations": [
|
|
26863
26863
|
{
|
|
26864
26864
|
"section": "spec/v2/core/interop.md §\"The operation mappings\" (RFC 0208)",
|
|
26865
26865
|
"requirement": "SendMessage MUST start two tasks with tenant-bound ids"
|
|
26866
26866
|
},
|
|
26867
26867
|
{
|
|
26868
|
-
"section":
|
|
26868
|
+
"section": "interop.md §\"A2A push delivery\" (RFC 0214 §E); interop.md §\"The operation mappings\" Isolation",
|
|
26869
26869
|
"requirement": null,
|
|
26870
26870
|
"interpolated": true
|
|
26871
26871
|
},
|
|
26872
26872
|
{
|
|
26873
|
-
"section":
|
|
26873
|
+
"section": "interop.md §\"A2A push delivery\" (RFC 0214 §E); interop.md §\"The operation mappings\" Isolation",
|
|
26874
26874
|
"requirement": null,
|
|
26875
26875
|
"interpolated": true
|
|
26876
26876
|
},
|
|
26877
26877
|
{
|
|
26878
|
-
"section":
|
|
26878
|
+
"section": "interop.md §\"A2A push delivery\" (RFC 0214 §E); interop.md §\"The operation mappings\" Isolation",
|
|
26879
26879
|
"requirement": null,
|
|
26880
26880
|
"interpolated": true
|
|
26881
26881
|
},
|
|
26882
26882
|
{
|
|
26883
|
-
"section":
|
|
26883
|
+
"section": "interop.md §\"A2A push delivery\" (RFC 0214 §E); interop.md §\"The operation mappings\" Isolation",
|
|
26884
26884
|
"requirement": null,
|
|
26885
26885
|
"interpolated": true
|
|
26886
26886
|
},
|
|
26887
26887
|
{
|
|
26888
|
-
"section":
|
|
26888
|
+
"section": "interop.md §\"A2A push delivery\" (RFC 0214 §E); interop.md §\"The operation mappings\" Isolation",
|
|
26889
26889
|
"requirement": null,
|
|
26890
26890
|
"interpolated": true
|
|
26891
26891
|
},
|
|
26892
26892
|
{
|
|
26893
|
-
"section":
|
|
26894
|
-
"requirement": "a delete naming another tenant's task MUST NOT delete the caller's config"
|
|
26895
|
-
"interpolated": true
|
|
26893
|
+
"section": "interop.md §\"A2A push delivery\" (RFC 0214 §E); interop.md §\"The operation mappings\" Isolation",
|
|
26894
|
+
"requirement": "a delete naming another tenant's task MUST NOT delete the caller's config"
|
|
26896
26895
|
},
|
|
26897
26896
|
{
|
|
26898
26897
|
"section": "A2A v1.0.1 §3.1.10 (\"MUST be idempotent\")",
|
|
@@ -26906,7 +26905,7 @@
|
|
|
26906
26905
|
"file": "v2-a2a-operation-map.test.ts",
|
|
26907
26906
|
"line": 368,
|
|
26908
26907
|
"title": "RFC 0214 §A — a push config's token and credentials are never returned, and never reach task, events or state",
|
|
26909
|
-
"explicitId":
|
|
26908
|
+
"explicitId": "openwop.requirement.0214.a2a-push-secrets-not-returned",
|
|
26910
26909
|
"citations": [
|
|
26911
26910
|
{
|
|
26912
26911
|
"section": "spec/v2/core/interop.md §\"The operation mappings\" (RFC 0208)",
|
|
@@ -26914,12 +26913,12 @@
|
|
|
26914
26913
|
"interpolated": true
|
|
26915
26914
|
},
|
|
26916
26915
|
{
|
|
26917
|
-
"section":
|
|
26916
|
+
"section": "interop-map.json a2a.operations push rows (\"secrets are never returned\"); security-defaults.md §\"Onward hops\" (RFC 0214 §A)",
|
|
26918
26917
|
"requirement": null,
|
|
26919
26918
|
"interpolated": true
|
|
26920
26919
|
},
|
|
26921
26920
|
{
|
|
26922
|
-
"section":
|
|
26921
|
+
"section": "interop-map.json a2a.operations push rows (\"secrets are never returned\"); security-defaults.md §\"Onward hops\" (RFC 0214 §A)",
|
|
26923
26922
|
"requirement": null,
|
|
26924
26923
|
"interpolated": true
|
|
26925
26924
|
}
|
|
@@ -27062,7 +27061,7 @@
|
|
|
27062
27061
|
"file": "v2-a2a-operation-map.test.ts",
|
|
27063
27062
|
"line": 509,
|
|
27064
27063
|
"title": "a run suspended on a credential interrupt projects to TASK_STATE_AUTH_REQUIRED, naming the provider and carrying connectUrl",
|
|
27065
|
-
"explicitId":
|
|
27064
|
+
"explicitId": "openwop.requirement.0199.a2a-auth-required",
|
|
27066
27065
|
"citations": [
|
|
27067
27066
|
{
|
|
27068
27067
|
"section": "runs.md createRun",
|
|
@@ -29657,7 +29656,7 @@
|
|
|
29657
29656
|
{
|
|
29658
29657
|
"id": "openwop.it.v2-lane-exp-only-bound.a-credential-inside-the-advertised-window-is-accepted-the-control-the-two-refusa",
|
|
29659
29658
|
"file": "v2-lane-exp-only-bound.test.ts",
|
|
29660
|
-
"line":
|
|
29659
|
+
"line": 152,
|
|
29661
29660
|
"title": "a credential inside the advertised window is accepted — the control the two refusals rest on",
|
|
29662
29661
|
"explicitId": "openwop.requirement.0210.exp-only-control-accepted",
|
|
29663
29662
|
"citations": [
|
|
@@ -29671,7 +29670,7 @@
|
|
|
29671
29670
|
{
|
|
29672
29671
|
"id": "openwop.it.v2-lane-exp-only-bound.a-credential-whose-total-lifetime-exceeds-the-window-is-refused-401-and-so-is-on",
|
|
29673
29672
|
"file": "v2-lane-exp-only-bound.test.ts",
|
|
29674
|
-
"line":
|
|
29673
|
+
"line": 167,
|
|
29675
29674
|
"title": "a credential whose TOTAL lifetime exceeds the window is refused 401, and so is one carrying no iat",
|
|
29676
29675
|
"explicitId": "openwop.requirement.0210.exp-only-lifetime-bound",
|
|
29677
29676
|
"citations": [
|
|
@@ -29690,7 +29689,7 @@
|
|
|
29690
29689
|
{
|
|
29691
29690
|
"id": "openwop.it.v2-lane-exp-only-bound.the-over-lifetime-refusal-names-credential-lifetime-exceeded-not-a-generic-unaut",
|
|
29692
29691
|
"file": "v2-lane-exp-only-bound.test.ts",
|
|
29693
|
-
"line":
|
|
29692
|
+
"line": 202,
|
|
29694
29693
|
"title": "the over-lifetime refusal names credential_lifetime_exceeded, not a generic unauthenticated",
|
|
29695
29694
|
"explicitId": "openwop.requirement.0210.credential-lifetime-code",
|
|
29696
29695
|
"citations": [
|
|
@@ -29704,7 +29703,7 @@
|
|
|
29704
29703
|
{
|
|
29705
29704
|
"id": "openwop.it.v2-lane-exp-only-bound.a-credential-whose-remaining-lifetime-exceeds-the-window-is-refused-though-its-t",
|
|
29706
29705
|
"file": "v2-lane-exp-only-bound.test.ts",
|
|
29707
|
-
"line":
|
|
29706
|
+
"line": 220,
|
|
29708
29707
|
"title": "a credential whose REMAINING lifetime exceeds the window is refused, though its total lifetime does not",
|
|
29709
29708
|
"explicitId": "openwop.requirement.0210.exp-only-remaining-bound",
|
|
29710
29709
|
"citations": [
|
|
@@ -30394,7 +30393,7 @@
|
|
|
30394
30393
|
"file": "v2-mcp-mount-map.test.ts",
|
|
30395
30394
|
"line": 287,
|
|
30396
30395
|
"title": "a credential interrupt is answered in URL mode (url = connectUrl), isError without URL support, and input_required again on an accept retry with no credential",
|
|
30397
|
-
"explicitId":
|
|
30396
|
+
"explicitId": "openwop.requirement.0199.mcp-url-mode",
|
|
30398
30397
|
"citations": [
|
|
30399
30398
|
{
|
|
30400
30399
|
"section": "interop-map.json mcp.mrtr InputRequiredResult (host as server)",
|
|
@@ -30432,7 +30431,7 @@
|
|
|
30432
30431
|
"file": "v2-mcp-mount-map.test.ts",
|
|
30433
30432
|
"line": 328,
|
|
30434
30433
|
"title": "form mode is never emitted for a nested schema or a sensitive (format password) field",
|
|
30435
|
-
"explicitId":
|
|
30434
|
+
"explicitId": "openwop.requirement.0199.form-mode-no-secret",
|
|
30436
30435
|
"citations": [
|
|
30437
30436
|
{
|
|
30438
30437
|
"section": "interop-map.json mcp.mrtr InputRequiredResult (host as server)",
|
|
@@ -31251,7 +31250,7 @@
|
|
|
31251
31250
|
{
|
|
31252
31251
|
"id": "openwop.it.v2-oidc-id-token-audience.a-same-audience-id-token-is-admissible-and-one-minted-for-another-relying-party",
|
|
31253
31252
|
"file": "v2-oidc-id-token-audience.test.ts",
|
|
31254
|
-
"line":
|
|
31253
|
+
"line": 68,
|
|
31255
31254
|
"title": "a same-audience ID token is admissible, and one minted for another relying party is audience_mismatch",
|
|
31256
31255
|
"explicitId": "openwop.requirement.0200.id-token-aud",
|
|
31257
31256
|
"citations": [
|
|
@@ -33419,7 +33418,7 @@
|
|
|
33419
33418
|
"file": "v2-webhook-endpoint-verification.test.ts",
|
|
33420
33419
|
"line": 64,
|
|
33421
33420
|
"title": "no-echo, wrong-echo and a redirect are refused and persist nothing; an echo is accepted",
|
|
33422
|
-
"explicitId":
|
|
33421
|
+
"explicitId": "openwop.requirement.0201.endpoint-verification",
|
|
33423
33422
|
"citations": [
|
|
33424
33423
|
{
|
|
33425
33424
|
"section": "RFC 0201 §D.14",
|
|
@@ -33487,7 +33486,7 @@
|
|
|
33487
33486
|
"file": "v2-webhook-message-id-stable.test.ts",
|
|
33488
33487
|
"line": 69,
|
|
33489
33488
|
"title": "every attempt of one delivery carries one webhook-id, and distinct deliveries carry distinct ids",
|
|
33490
|
-
"explicitId":
|
|
33489
|
+
"explicitId": "openwop.requirement.0201.message-id-stable",
|
|
33491
33490
|
"citations": [
|
|
33492
33491
|
{
|
|
33493
33492
|
"section": "RFC 0201 §C.10",
|
|
@@ -33520,7 +33519,7 @@
|
|
|
33520
33519
|
"file": "v2-webhook-secret-rotation.test.ts",
|
|
33521
33520
|
"line": 67,
|
|
33522
33521
|
"title": "a rotation dual-signs for overlapSeconds, keeps OpenWOP-Signature on the previous secret, and is tenant-checked",
|
|
33523
|
-
"explicitId":
|
|
33522
|
+
"explicitId": "openwop.requirement.0201.secret-rotation",
|
|
33524
33523
|
"citations": [
|
|
33525
33524
|
{
|
|
33526
33525
|
"section": "RFC 0201 §E.18 / identity.md §5",
|
|
@@ -33603,7 +33602,7 @@
|
|
|
33603
33602
|
"file": "v2-webhook-standard-webhooks-delivery.test.ts",
|
|
33604
33603
|
"line": 77,
|
|
33605
33604
|
"title": "a subscription that did not opt in is neither verified nor given webhook-* headers",
|
|
33606
|
-
"explicitId":
|
|
33605
|
+
"explicitId": "openwop.requirement.0201.opt-in-only",
|
|
33607
33606
|
"citations": [
|
|
33608
33607
|
{
|
|
33609
33608
|
"section": "RFC 0201 §B.8 / §D.15",
|
|
@@ -33637,7 +33636,7 @@
|
|
|
33637
33636
|
"file": "v2-webhook-standard-webhooks-delivery.test.ts",
|
|
33638
33637
|
"line": 110,
|
|
33639
33638
|
"title": "the opt-in is validated, its 201 echoes the applied list, and no response carries the secret",
|
|
33640
|
-
"explicitId":
|
|
33639
|
+
"explicitId": "openwop.requirement.0201.registration-validated",
|
|
33641
33640
|
"citations": [
|
|
33642
33641
|
{
|
|
33643
33642
|
"section": "RFC 0201 §B.5–§B.6",
|
|
@@ -33669,7 +33668,7 @@
|
|
|
33669
33668
|
"file": "v2-webhook-standard-webhooks-delivery.test.ts",
|
|
33670
33669
|
"line": 155,
|
|
33671
33670
|
"title": "an opted-in delivery is dual-signed and its Standard Webhooks signature covers the id",
|
|
33672
|
-
"explicitId":
|
|
33671
|
+
"explicitId": "openwop.requirement.0201.delivery-signed",
|
|
33673
33672
|
"citations": [
|
|
33674
33673
|
{
|
|
33675
33674
|
"section": "RFC 0201 §B",
|
|
@@ -41095,7 +41094,7 @@
|
|
|
41095
41094
|
{
|
|
41096
41095
|
"id": "openwop.it.v2-bundle-witness-preimage.every-committed-v3-bundle-re-derives-its-witnesssha256-from-the-prose-preimage",
|
|
41097
41096
|
"file": "v2-bundle-witness-preimage.test.ts",
|
|
41098
|
-
"line":
|
|
41097
|
+
"line": 69,
|
|
41099
41098
|
"title": "every committed v3 bundle re-derives its witnessSha256 from the prose preimage",
|
|
41100
41099
|
"explicitId": "openwop.it.v2-bundle-witness-preimage.every-committed-v3-bundle-re-derives-its-witnesssha256-from-the-prose-preimage",
|
|
41101
41100
|
"citations": [
|
|
@@ -41122,7 +41121,7 @@
|
|
|
41122
41121
|
{
|
|
41123
41122
|
"id": "openwop.it.v2-bundle-witness-preimage.the-row-comparator-is-code-unit-order-which-a-locale-collation-contradicts",
|
|
41124
41123
|
"file": "v2-bundle-witness-preimage.test.ts",
|
|
41125
|
-
"line":
|
|
41124
|
+
"line": 83,
|
|
41126
41125
|
"title": "the row comparator is code-unit order, which a locale collation contradicts",
|
|
41127
41126
|
"explicitId": "openwop.it.v2-bundle-witness-preimage.the-row-comparator-is-code-unit-order-which-a-locale-collation-contradicts",
|
|
41128
41127
|
"citations": [
|
|
@@ -41136,6 +41135,167 @@
|
|
|
41136
41135
|
}
|
|
41137
41136
|
]
|
|
41138
41137
|
},
|
|
41138
|
+
{
|
|
41139
|
+
"id": "openwop.it.v2-colocated-companion.a-3-the-anchor-list-is-closed-and-exact",
|
|
41140
|
+
"file": "v2-colocated-companion.test.ts",
|
|
41141
|
+
"line": 157,
|
|
41142
|
+
"title": "§A.3 the anchor list is closed and exact",
|
|
41143
|
+
"explicitId": "openwop.requirement.0216.anchor-list-closed",
|
|
41144
|
+
"citations": [
|
|
41145
|
+
{
|
|
41146
|
+
"section": "RFC 0216 · spec/v2/core/conformance.md §\"Bundle v3\"",
|
|
41147
|
+
"requirement": "the base fixture list MUST pass the anchor gate, or every sabotage below proves nothing"
|
|
41148
|
+
},
|
|
41149
|
+
{
|
|
41150
|
+
"section": "RFC 0216 · spec/v2/core/conformance.md §\"Bundle v3\"",
|
|
41151
|
+
"requirement": null,
|
|
41152
|
+
"interpolated": true
|
|
41153
|
+
},
|
|
41154
|
+
{
|
|
41155
|
+
"section": "RFC 0216 · spec/v2/core/conformance.md §\"Bundle v3\"",
|
|
41156
|
+
"requirement": null,
|
|
41157
|
+
"interpolated": true
|
|
41158
|
+
}
|
|
41159
|
+
]
|
|
41160
|
+
},
|
|
41161
|
+
{
|
|
41162
|
+
"id": "openwop.it.v2-colocated-companion.b-7-the-marker-is-inside-the-signed-digest",
|
|
41163
|
+
"file": "v2-colocated-companion.test.ts",
|
|
41164
|
+
"line": 175,
|
|
41165
|
+
"title": "§B.7 the marker is inside the signed digest",
|
|
41166
|
+
"explicitId": "openwop.requirement.0216.deployment-in-preimage",
|
|
41167
|
+
"citations": [
|
|
41168
|
+
{
|
|
41169
|
+
"section": "RFC 0216 · spec/v2/core/conformance.md §\"Bundle v3\"",
|
|
41170
|
+
"requirement": "host.deployment MUST enter the witnessSha256 preimage when present"
|
|
41171
|
+
},
|
|
41172
|
+
{
|
|
41173
|
+
"section": "RFC 0216 · spec/v2/core/conformance.md §\"Bundle v3\"",
|
|
41174
|
+
"requirement": "a bundle without the marker MUST digest exactly as before RFC 0216"
|
|
41175
|
+
},
|
|
41176
|
+
{
|
|
41177
|
+
"section": "RFC 0216 · spec/v2/core/conformance.md §\"Bundle v3\"",
|
|
41178
|
+
"requirement": "a signed companion that sheds host.deployment MUST fail verification with witness-digest"
|
|
41179
|
+
},
|
|
41180
|
+
{
|
|
41181
|
+
"section": "RFC 0216 · spec/v2/core/conformance.md §\"Bundle v3\"",
|
|
41182
|
+
"requirement": "the unaltered companion MUST NOT be refused for its digest"
|
|
41183
|
+
}
|
|
41184
|
+
]
|
|
41185
|
+
},
|
|
41186
|
+
{
|
|
41187
|
+
"id": "openwop.it.v2-colocated-companion.c-9-a-companion-counts-only-for-listed-rows",
|
|
41188
|
+
"file": "v2-colocated-companion.test.ts",
|
|
41189
|
+
"line": 187,
|
|
41190
|
+
"title": "§C.9 a companion counts only for listed rows",
|
|
41191
|
+
"explicitId": "openwop.requirement.0216.companion-anchor-rows-only",
|
|
41192
|
+
"citations": [
|
|
41193
|
+
{
|
|
41194
|
+
"section": "RFC 0216 · spec/v2/core/conformance.md §\"Bundle v3\"",
|
|
41195
|
+
"requirement": "the base companion MUST pair with the served-host bundle of the same image, key and configuration"
|
|
41196
|
+
},
|
|
41197
|
+
{
|
|
41198
|
+
"section": "RFC 0216 · spec/v2/core/conformance.md §\"Bundle v3\"",
|
|
41199
|
+
"requirement": "a paired companion MUST witness its harness-trust-anchor rows"
|
|
41200
|
+
},
|
|
41201
|
+
{
|
|
41202
|
+
"section": "RFC 0216 · spec/v2/core/conformance.md §\"Bundle v3\"",
|
|
41203
|
+
"requirement": "a paired companion MUST NOT witness a row off the anchor list, however it passed"
|
|
41204
|
+
},
|
|
41205
|
+
{
|
|
41206
|
+
"section": "RFC 0216 · spec/v2/core/conformance.md §\"Bundle v3\"",
|
|
41207
|
+
"requirement": "an uncertified companion MUST witness nothing"
|
|
41208
|
+
}
|
|
41209
|
+
]
|
|
41210
|
+
},
|
|
41211
|
+
{
|
|
41212
|
+
"id": "openwop.it.v2-colocated-companion.c-9-a-b-the-same-host-and-the-same-image-digest",
|
|
41213
|
+
"file": "v2-colocated-companion.test.ts",
|
|
41214
|
+
"line": 197,
|
|
41215
|
+
"title": "§C.9(a)/(b) the same host and the same image digest",
|
|
41216
|
+
"explicitId": "openwop.requirement.0216.companion-same-image",
|
|
41217
|
+
"citations": [
|
|
41218
|
+
{
|
|
41219
|
+
"section": "RFC 0216 · spec/v2/core/conformance.md §\"Bundle v3\"",
|
|
41220
|
+
"requirement": "the base MUST pair"
|
|
41221
|
+
},
|
|
41222
|
+
{
|
|
41223
|
+
"section": "RFC 0216 · spec/v2/core/conformance.md §\"Bundle v3\"",
|
|
41224
|
+
"requirement": "a commit-kind build MUST NOT pair — a commit does not name the bytes that ran"
|
|
41225
|
+
},
|
|
41226
|
+
{
|
|
41227
|
+
"section": "RFC 0216 · spec/v2/core/conformance.md §\"Bundle v3\"",
|
|
41228
|
+
"requirement": "image digests differing in one byte MUST NOT pair"
|
|
41229
|
+
},
|
|
41230
|
+
{
|
|
41231
|
+
"section": "RFC 0216 · spec/v2/core/conformance.md §\"Bundle v3\"",
|
|
41232
|
+
"requirement": "a companion of another host MUST NOT pair"
|
|
41233
|
+
}
|
|
41234
|
+
]
|
|
41235
|
+
},
|
|
41236
|
+
{
|
|
41237
|
+
"id": "openwop.it.v2-colocated-companion.c-9-c-signed-under-a-key-the-served-bundle-publishes",
|
|
41238
|
+
"file": "v2-colocated-companion.test.ts",
|
|
41239
|
+
"line": 209,
|
|
41240
|
+
"title": "§C.9(c) signed under a key the served bundle publishes",
|
|
41241
|
+
"explicitId": "openwop.requirement.0216.companion-attributable",
|
|
41242
|
+
"citations": [
|
|
41243
|
+
{
|
|
41244
|
+
"section": "RFC 0216 · spec/v2/core/conformance.md §\"Bundle v3\"",
|
|
41245
|
+
"requirement": "the base MUST pair"
|
|
41246
|
+
},
|
|
41247
|
+
{
|
|
41248
|
+
"section": "RFC 0216 · spec/v2/core/conformance.md §\"Bundle v3\"",
|
|
41249
|
+
"requirement": "a companion whose attestation does not verify under the named published key MUST NOT pair"
|
|
41250
|
+
},
|
|
41251
|
+
{
|
|
41252
|
+
"section": "RFC 0216 · spec/v2/core/conformance.md §\"Bundle v3\"",
|
|
41253
|
+
"requirement": "a companion signed under a key the served bundle does not publish MUST NOT pair"
|
|
41254
|
+
}
|
|
41255
|
+
]
|
|
41256
|
+
},
|
|
41257
|
+
{
|
|
41258
|
+
"id": "openwop.it.v2-colocated-companion.c-9-d-equivalent-discovery-origin-and-oidc-issuers-aside",
|
|
41259
|
+
"file": "v2-colocated-companion.test.ts",
|
|
41260
|
+
"line": 218,
|
|
41261
|
+
"title": "§C.9(d) equivalent discovery, origin and oidc issuers aside",
|
|
41262
|
+
"explicitId": "openwop.requirement.0216.companion-equivalent",
|
|
41263
|
+
"citations": [
|
|
41264
|
+
{
|
|
41265
|
+
"section": "RFC 0216 · spec/v2/core/conformance.md §\"Bundle v3\"",
|
|
41266
|
+
"requirement": "a companion differing only in origin and oidc issuers MUST pair"
|
|
41267
|
+
},
|
|
41268
|
+
{
|
|
41269
|
+
"section": "RFC 0216 · spec/v2/core/conformance.md §\"Bundle v3\"",
|
|
41270
|
+
"requirement": "a different revocationWindowSeconds MUST NOT pair — it is a different claim"
|
|
41271
|
+
},
|
|
41272
|
+
{
|
|
41273
|
+
"section": "RFC 0216 · spec/v2/core/conformance.md §\"Bundle v3\"",
|
|
41274
|
+
"requirement": "a difference in a non-oidc lane MUST NOT pair"
|
|
41275
|
+
}
|
|
41276
|
+
]
|
|
41277
|
+
},
|
|
41278
|
+
{
|
|
41279
|
+
"id": "openwop.it.v2-colocated-companion.c-10-the-committed-unmarked-companion-witnesses-nothing",
|
|
41280
|
+
"file": "v2-colocated-companion.test.ts",
|
|
41281
|
+
"line": 227,
|
|
41282
|
+
"title": "§C.10 the committed unmarked companion witnesses nothing",
|
|
41283
|
+
"explicitId": "openwop.requirement.0216.legacy-companion",
|
|
41284
|
+
"citations": [
|
|
41285
|
+
{
|
|
41286
|
+
"section": "RFC 0216 · spec/v2/core/conformance.md §\"Bundle v3\"",
|
|
41287
|
+
"requirement": "the committed 2026-09-25 companion MUST be recognised as a companion by its witnessSha256"
|
|
41288
|
+
},
|
|
41289
|
+
{
|
|
41290
|
+
"section": "RFC 0216 · spec/v2/core/conformance.md §\"Bundle v3\"",
|
|
41291
|
+
"requirement": "the committed unmarked companion MUST witness nothing (commit-kind build, throwaway key)"
|
|
41292
|
+
},
|
|
41293
|
+
{
|
|
41294
|
+
"section": "RFC 0216 · spec/v2/core/conformance.md §\"Bundle v3\"",
|
|
41295
|
+
"requirement": "the legacy file carries no marker — it is caught by digest, not by a field it could drop"
|
|
41296
|
+
}
|
|
41297
|
+
]
|
|
41298
|
+
},
|
|
41139
41299
|
{
|
|
41140
41300
|
"id": "openwop.it.v2-corpus-tag-pinned.every-vendoring-consumer-in-the-cross-repo-inventory-records-a-published-corpus",
|
|
41141
41301
|
"file": "v2-corpus-tag-pinned.test.ts",
|
|
@@ -41522,7 +41682,7 @@
|
|
|
41522
41682
|
"file": "v2-lane-exp-only-schema.test.ts",
|
|
41523
41683
|
"line": 127,
|
|
41524
41684
|
"title": "the gate refuses an enum member with no §2.2 row, and a suite map the table does not license",
|
|
41525
|
-
"explicitId":
|
|
41685
|
+
"explicitId": "openwop.requirement.0210.lane-rule-surfaces-agree",
|
|
41526
41686
|
"citations": [
|
|
41527
41687
|
{
|
|
41528
41688
|
"section": "scripts/check-lane-revocation-rules.mjs",
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_comment": "Provenance of @openwop/spec-artifacts (RFC 0168 §D.2). files: SHA-256 per file; the conformance suite compares the installed peer against dist/spec-artifacts.lock.json at start.",
|
|
3
3
|
"package": "@openwop/spec-artifacts",
|
|
4
|
-
"version": "2.40.
|
|
5
|
-
"corpusTag": "v2.40.
|
|
4
|
+
"version": "2.40.2",
|
|
5
|
+
"corpusTag": "v2.40.2",
|
|
6
6
|
"files": {
|
|
7
7
|
"api/.redocly.lint-ignore.yaml": "bf5a8350b88a72fa43f59605ed8d903ed24b6cfccda5e45509c9f6ed9ee4e712",
|
|
8
8
|
"api/asyncapi.yaml": "d5ecb9ee6114582be3b1f662c84bfac9ae96dae7bacb853e461168f70a8e1c7d",
|
|
9
9
|
"api/grpc/openwop.proto": "c3e72bb17cba514ee98feb6434e6c9b6ea6795bfd086489ec69fd882dd1ad977",
|
|
10
10
|
"api/openapi.yaml": "6366b5514be71098e9ea41fc81ff52bc39db1a547b353c0b344cc2a2bf5370aa",
|
|
11
11
|
"api/redocly.yaml": "b0604c89b2ca6d5076ec25725c539dad44a741a811fe524439ee6daef8baa09f",
|
|
12
|
-
"api/seams-v2.yaml": "
|
|
13
|
-
"api/v2/asyncapi.yaml": "
|
|
14
|
-
"api/v2/openapi.yaml": "
|
|
12
|
+
"api/seams-v2.yaml": "c92bde1fb2ac949edfb50519651ac3755cfc88b8a2a8785eab0e15bcb6a8dbb9",
|
|
13
|
+
"api/v2/asyncapi.yaml": "87d5b9de8e3fa920a0ddecce7f51018f8f573b5ef14f954973a0776d3b9d9a0f",
|
|
14
|
+
"api/v2/openapi.yaml": "532467b699c179c355349c8bb483a801156a68b003d02aa9d7cf576f867c3097",
|
|
15
15
|
"api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
|
|
16
16
|
"schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
|
|
17
17
|
"schemas/a2a-task-state.schema.json": "75d5049dea8bd873ff0e7546f1c60c8d36c219bec7084264360be54a8be30ae1",
|
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
"schemas/v2/audit-verify-result.schema.json": "a748dfadb5155adfde8a91045e76b0db6695005fe559035d71670fdd55edb3ba",
|
|
118
118
|
"schemas/v2/budget-policy.schema.json": "c7449daeb6e1d95e7a047b8c2814d3d54748697c27046972834874867d9b5f4c",
|
|
119
119
|
"schemas/v2/capabilities.schema.json": "740944d7f0a00c91308449f3a3f52fa7b91b16b8fd36a0171b917afe9f4b0f09",
|
|
120
|
-
"schemas/v2/certification-bundle.schema.json": "
|
|
120
|
+
"schemas/v2/certification-bundle.schema.json": "82ce31a1ed6c7e21b0ba88ff62a7b8b768fc3b176b1d06fce023203830b94725",
|
|
121
121
|
"schemas/v2/channel-presence-payload.schema.json": "4eeb224910e6f7eee3ab5e7c3899559c25c83a26d0022e0ad195c4672611977a",
|
|
122
122
|
"schemas/v2/channel-written-payload.schema.json": "ccecff3c71a3275ad8db035ac0e04db25e6f8d23cf091ddbcd5a60d1be1cbb6b",
|
|
123
123
|
"schemas/v2/chat-card-pack-manifest.schema.json": "92a57db2fd4b2463f14ef155fef8e3f18afc78960b646459dcf32efc79ff6bf5",
|
|
@@ -204,13 +204,13 @@
|
|
|
204
204
|
"schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
|
|
205
205
|
"spec/v1/alias-detectors.json": "2401fcb1c18cdd688c018b3d716ae6bca85c5e356220ee2b793bd9d81872412d",
|
|
206
206
|
"spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
|
|
207
|
-
"spec/v1/core-standard-manifest.json": "
|
|
207
|
+
"spec/v1/core-standard-manifest.json": "a16e3a91b9121d7b6d58fd9f65a348f92a07636417406f56bc3173571e9d55ea",
|
|
208
208
|
"spec/v1/deprecations.json": "2d03f4729810280147ea08c630ea65434f2dc5370567d0aac41c3595f8337d40",
|
|
209
209
|
"spec/v1/deprecations.schema.json": "3e393c405d2a41b467d8c5e3c468549078df1ce6a6d2588fc488097b95d9b55b",
|
|
210
210
|
"spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
|
|
211
211
|
"spec/v1/event-codemap.schema.json": "d05933b2e88103aff51a2f774df97b9bda065e0fc88dbd9b114cc47a32189174",
|
|
212
212
|
"spec/v1/extensions.json": "79a60754aa16cbdbdb604f8e5c00038af535a1690a4d07bd5ff7c38f0256a7bc",
|
|
213
|
-
"spec/v1/gaps.json": "
|
|
213
|
+
"spec/v1/gaps.json": "3decfec271c30f796ba76586ca06ab31e6ea3b8bcae174c3af0b8bb1c19709b5",
|
|
214
214
|
"spec/v1/gaps.schema.json": "8fd83259f556553c9df0f53e7a82ca8c2a4771d471197f69b4e2ae0ceeacfd66",
|
|
215
215
|
"spec/v1/migrations.json": "8839497d6a4830b53fa7867014a8336f0d725ef2346352e54fb580b7947897af",
|
|
216
216
|
"spec/v1/migrations.schema.json": "886779aa6c22e646db097f5df210adb018a4dd14a7b815465a18c8a7056c8f72",
|
|
@@ -218,7 +218,7 @@
|
|
|
218
218
|
"spec/v1/spec-gaps.json": "ea9612af55bea9dae3e82223d4f714243f8f590b0193f1188257b13491a38afc",
|
|
219
219
|
"spec/v2/README.md": "ccfed7d978ce7293a6d8f3c0f6045835dfb7e488f2b67a68f6b04d67c4b0cfc9",
|
|
220
220
|
"spec/v2/core/capabilities.md": "e22381fed3a93376ad478fa7f07571fdad9653aefd7c05f189a2ef45a49f6c63",
|
|
221
|
-
"spec/v2/core/conformance.md": "
|
|
221
|
+
"spec/v2/core/conformance.md": "e4d3b34ef5b7c6a34675ed59a086143e8bfd0e00d0d04ce0f2a2ec6245275084",
|
|
222
222
|
"spec/v2/core/connection-packs.md": "7257339a4fc125011bf89961588c949cb058a0c0f7d9808ad08a5288deb3f8fc",
|
|
223
223
|
"spec/v2/core/conversation.md": "c9a937e410fdb4e092cec2f05a7cab8b5232745090cfb20041c03c70ccbb5cdc",
|
|
224
224
|
"spec/v2/core/errors.md": "619cd74381c41acf33d012eb1afd834e148ac292663a1a6fa6744dbaff9661d9",
|
|
@@ -231,7 +231,7 @@
|
|
|
231
231
|
"spec/v2/core/interop.md": "9a6b37e07c0235856199ace6b3a84f592fa5903fcf6c442f8a18f4d6b6aa5525",
|
|
232
232
|
"spec/v2/core/interrupt.md": "1dc94f7c791dd5a547b1abafde6a8a76acd3d2993d288057f302f57911dc83bf",
|
|
233
233
|
"spec/v2/core/node-pack-runtimes.md": "c435ad506f618831b6c0a9fd9150d83a5dc6caa1e3d755b949af2ff8949f1140",
|
|
234
|
-
"spec/v2/core/oauth.md": "
|
|
234
|
+
"spec/v2/core/oauth.md": "38ec0e70d46f328430138559095f261ae4fcd3d32c2d994a2d3e630a062c0e6c",
|
|
235
235
|
"spec/v2/core/overview.md": "8f9d4a68b6185664e40d226cb82d9a9270a6c6630e3db59bddff300be781947d",
|
|
236
236
|
"spec/v2/core/packs.md": "91cd577b4a636c1587ef78742e583a203244b8508b289b01a18a8ef2ef0a9b3e",
|
|
237
237
|
"spec/v2/core/persistence.md": "d32736bd74e51df7c552565b4105a8ec801ece6d2292c8676421368ee9bffec1",
|
|
@@ -284,6 +284,8 @@
|
|
|
284
284
|
"spec/v2/facets/supportedEnvelopes.schema.json": "644f22cc2236e30a611b5b7f1ba0458ff1925d68b94c514d9672583224e126bd",
|
|
285
285
|
"spec/v2/facets/webhooks.schema.json": "2fac635ee5fbc10a14a1a46cf512a11bac660bfbabe0aea79f6c7b657ecb9e4e",
|
|
286
286
|
"spec/v2/facets/workflowChainPacks.schema.json": "ea625ce818fbeb746478f00f31f96ee9613d538e4409c258c1bfb517acdeaba9",
|
|
287
|
+
"spec/v2/harness-trust-anchors.json": "6007b92eae16a070fbc3777424efb0abfe865fda89d096963109b96282b91205",
|
|
288
|
+
"spec/v2/harness-trust-anchors.schema.json": "372b5d92337c893d3449eff3f6bfd009ac09fdac285296d8bdbe0a08fff9e34c",
|
|
287
289
|
"spec/v2/id-field-bindings.json": "9f6fd280250447e26a6704811c352bb8f2ff6c4bd242befe3da0ba38962370e9",
|
|
288
290
|
"spec/v2/interop-map.json": "02255dafe7eb55f61ca7f9ae7c21f079d1848a6756991343088975ac9d184b8d",
|
|
289
291
|
"spec/v2/interop-map.schema.json": "0017c882df34ee215e2e0729de051490e09d43aac3d2c949285698ed51832f12",
|
|
@@ -292,9 +294,9 @@
|
|
|
292
294
|
"spec/v2/path-manifest.json": "ae9b56d03a701061fd48ad0b73cb0373493f6a4547da92763b422295b5e19535",
|
|
293
295
|
"spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
|
|
294
296
|
"spec/v2/profiles.json": "0636f19fceae625390003a347e70ef4797d84766b5c24ce8a02cea52aadebca4",
|
|
295
|
-
"spec/v2/release.json": "
|
|
297
|
+
"spec/v2/release.json": "f04bc2406dfc3af40a495d8750349409f053bb56f009359ebc62a8877251c90f",
|
|
296
298
|
"spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624",
|
|
297
|
-
"spec/v2/surface-baseline.json": "
|
|
299
|
+
"spec/v2/surface-baseline.json": "26f2a03c650413ac9c0d0b5facee47ee0eaebeb01d377c808fe6e737bb08687c"
|
|
298
300
|
},
|
|
299
|
-
"corpusCommit": "
|
|
301
|
+
"corpusCommit": "70c93c0fcd5c595893c1cac7cf1020d5c5835ec2"
|
|
300
302
|
}
|
package/src/cli.ts
CHANGED
|
@@ -83,6 +83,8 @@ interface ParsedArgs {
|
|
|
83
83
|
readonly verifyPath: string | undefined;
|
|
84
84
|
/** `--host-key <pem>` — the key `--verify` checks the host signature under. Absent ⇒ the verdict is INCOMPLETE, not clean. */
|
|
85
85
|
readonly verifyHostKeyPath: string | undefined;
|
|
86
|
+
/** RFC 0216 §B — `--as-colocated-companion`. */
|
|
87
|
+
readonly colocatedCompanion: boolean;
|
|
86
88
|
readonly verifierKeyId: string | undefined;
|
|
87
89
|
/**
|
|
88
90
|
* S43 (2026-08-18) — cap on concurrently running scenario FILES, forwarded to
|
|
@@ -112,6 +114,7 @@ function parseArgs(argv: readonly string[]): ParsedArgs {
|
|
|
112
114
|
let signingKeyPath: string | undefined, signingKeyId: string | undefined, verifierKeyPath: string | undefined, verifierKeyId: string | undefined;
|
|
113
115
|
let targetMajor: 1 | 2 | undefined;
|
|
114
116
|
let maxWorkers: number | undefined = parseMaxWorkers(process.env.OPENWOP_MAX_WORKERS, 'OPENWOP_MAX_WORKERS');
|
|
117
|
+
let colocatedCompanion = false;
|
|
115
118
|
|
|
116
119
|
for (let i = 0; i < argv.length; i++) {
|
|
117
120
|
const arg = argv[i] ?? '';
|
|
@@ -202,6 +205,12 @@ function parseArgs(argv: readonly string[]): ParsedArgs {
|
|
|
202
205
|
case '--host-key':
|
|
203
206
|
verifyHostKeyPath = nextValue();
|
|
204
207
|
break;
|
|
208
|
+
// RFC 0216 §B — marks the bundle as cut from a colocated companion (the
|
|
209
|
+
// served host's image beside the suite, trusting a suite-held anchor).
|
|
210
|
+
// The marker is inside witnessSha256, so a signed companion cannot shed it.
|
|
211
|
+
case '--as-colocated-companion':
|
|
212
|
+
colocatedCompanion = true;
|
|
213
|
+
break;
|
|
205
214
|
case '--require-behavior':
|
|
206
215
|
process.env['OPENWOP_REQUIRE_BEHAVIOR'] = 'true';
|
|
207
216
|
break;
|
|
@@ -240,6 +249,7 @@ function parseArgs(argv: readonly string[]): ParsedArgs {
|
|
|
240
249
|
maxWorkers,
|
|
241
250
|
verifyPath,
|
|
242
251
|
verifyHostKeyPath,
|
|
252
|
+
colocatedCompanion,
|
|
243
253
|
};
|
|
244
254
|
}
|
|
245
255
|
|
|
@@ -288,6 +298,10 @@ Certification (RFC 0089):
|
|
|
288
298
|
§C) records per-requirement DISPOSITIONS instead of pass/fail/skip
|
|
289
299
|
file lists, so "we could not check" stops being indistinguishable
|
|
290
300
|
from "checked and it holds". See the note it prints.
|
|
301
|
+
--as-colocated-companion v3 (RFC 0216): mark the bundle as cut from a colocated companion — the
|
|
302
|
+
served host's image run beside the suite so it can trust a suite-held
|
|
303
|
+
trust anchor. Signed (inside witnessSha256). A companion is acceptance
|
|
304
|
+
evidence only for spec/v2/harness-trust-anchors.json rows.
|
|
291
305
|
--max-workers <n> Cap concurrently running scenario files (vitest --maxWorkers).
|
|
292
306
|
Default: one worker per CPU. Use a small number against a
|
|
293
307
|
rate-limited production origin so 429s don't read as failures.
|
|
@@ -709,7 +723,7 @@ async function runCertify(args: ParsedArgs, baseUrl: string, apiKey: string): Pr
|
|
|
709
723
|
bundleVersion: '3',
|
|
710
724
|
generatedAt: new Date().toISOString(),
|
|
711
725
|
suite: { name: '@openwop/openwop-conformance', version, targetMajor: target.major, specArtifactsVersion: lock?.version ?? 'repo-layout', ...(lock ? { stampSha256: lock.stampSha256 } : {}) },
|
|
712
|
-
host: { name: host.name, version: host.version, ...(host.vendor ? { vendor: host.vendor } : {}), build, signingKeyId: keyId, ...(relaxations && relaxations.length ? { relaxations } : {}) },
|
|
726
|
+
host: { name: host.name, version: host.version, ...(host.vendor ? { vendor: host.vendor } : {}), build, signingKeyId: keyId, ...(relaxations && relaxations.length ? { relaxations } : {}), ...(args.colocatedCompanion ? { deployment: 'colocated-companion' as const } : {}) },
|
|
713
727
|
// `document` is what makes `claimedProfiles[].certified` checkable by
|
|
714
728
|
// someone other than this process (RFC 0148 §B(1)); v2 carried it and v3
|
|
715
729
|
// dropped it. `sha256` is a digest of `canonicalJSON(document)`, so the
|
|
@@ -717,7 +731,7 @@ async function runCertify(args: ParsedArgs, baseUrl: string, apiKey: string): Pr
|
|
|
717
731
|
discovery: { url: discoveryUrl, sha256, protocolVersions, preferredVersion, document },
|
|
718
732
|
claimedProfiles: claimed3,
|
|
719
733
|
results: { totals: totals3, requirements: rows3 },
|
|
720
|
-
witnessSha256: witnessDigest(rows3, relaxations),
|
|
734
|
+
witnessSha256: witnessDigest(rows3, relaxations, args.colocatedCompanion ? 'colocated-companion' : undefined),
|
|
721
735
|
assertionCount: rows3.reduce((n, r) => n + (r.assertions ?? 0), 0),
|
|
722
736
|
...(nonPass.length ? { detail: { nonPass: nonPass.map((r) => ({ id: r.id, result: r.result, reason: r.detail ?? '' })) } } : {}),
|
|
723
737
|
// RFC 0158 §D: claimed ONLY when these rows support it. The verifier
|
|
@@ -908,6 +922,10 @@ async function runCertify(args: ParsedArgs, baseUrl: string, apiKey: string): Pr
|
|
|
908
922
|
|
|
909
923
|
async function main(): Promise<never> {
|
|
910
924
|
const args = parseArgs(process.argv.slice(2));
|
|
925
|
+
if (args.colocatedCompanion && (!args.certify || args.bundleVersion !== '3')) {
|
|
926
|
+
process.stderr.write('openwop-conformance: --as-colocated-companion marks a v3 certification bundle; it needs --certify with bundle version 3 (RFC 0216 §B)\n');
|
|
927
|
+
process.exit(2);
|
|
928
|
+
}
|
|
911
929
|
// Refused before anything runs: a pinned-port certification that is not
|
|
912
930
|
// single-worker loses the host's traffic to a worker nobody reads (lib/pinned-ports.ts).
|
|
913
931
|
const pinnedConflict = pinnedPortWorkerConflict(process.env, args.maxWorkers, args.certify !== undefined);
|
|
@@ -41,12 +41,14 @@ export interface BundleV3Profile {
|
|
|
41
41
|
readonly certified: boolean;
|
|
42
42
|
}
|
|
43
43
|
export interface BundleV3Relaxation { readonly obligation: string; readonly durability: 'session' | 'deployment' | 'persisted'; readonly reason: string }
|
|
44
|
+
/** RFC 0216 §B — absent means the bundle measures the served host. */
|
|
45
|
+
export type BundleV3Deployment = 'colocated-companion';
|
|
44
46
|
export interface BundleV3Signature { readonly alg: 'ed25519'; readonly keyId: string; readonly sig: string; readonly over: readonly string[]; readonly verifierKeyId?: string }
|
|
45
47
|
export interface BundleV3 {
|
|
46
48
|
bundleVersion: '3';
|
|
47
49
|
generatedAt: string;
|
|
48
50
|
suite: { name: '@openwop/openwop-conformance'; version: string; targetMajor: 1 | 2; specArtifactsVersion: string; stampSha256?: string };
|
|
49
|
-
host: { name: string; version: string; vendor?: string; build: { kind: 'image-digest' | 'commit' | 'artifact-sha256'; id: string }; signingKeyId?: string; relaxations?: BundleV3Relaxation[] };
|
|
51
|
+
host: { name: string; version: string; vendor?: string; build: { kind: 'image-digest' | 'commit' | 'artifact-sha256'; id: string }; signingKeyId?: string; relaxations?: BundleV3Relaxation[]; deployment?: BundleV3Deployment };
|
|
50
52
|
/**
|
|
51
53
|
* `document` is the captured `/.well-known/openwop` payload, OPTIONAL.
|
|
52
54
|
*
|
|
@@ -106,12 +108,20 @@ export { canonicalJSON } from './jcs.js';
|
|
|
106
108
|
* from the same bundle anywhere else (Czech sorts `ch` after `h`). Every
|
|
107
109
|
* committed bundle's ids are `[a-z0-9.-]`, where the two orders coincide, so no
|
|
108
110
|
* stored digest changes (v2-bundle-witness-preimage.test.ts pins that).
|
|
111
|
+
*
|
|
112
|
+
* RFC 0216 §B.7 — a colocated companion's `host.deployment` joins the preimage
|
|
113
|
+
* the same way, ONLY WHEN PRESENT (`{ rows, relaxations?, deployment }`), so a
|
|
114
|
+
* signed companion cannot shed the marker and pass as a served-host bundle, and
|
|
115
|
+
* every bundle without it digests exactly as before.
|
|
109
116
|
*/
|
|
110
|
-
export function witnessDigest(rows: readonly BundleV3Requirement[], relaxations?: readonly BundleV3Relaxation[]): string {
|
|
117
|
+
export function witnessDigest(rows: readonly BundleV3Requirement[], relaxations?: readonly BundleV3Relaxation[], deployment?: BundleV3Deployment): string {
|
|
111
118
|
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 }),
|
|
112
119
|
// ONLY WHEN PRESENT: every bundle cut before 2.34.0 has no `evidence` and digests byte-identically.
|
|
113
120
|
...(r.evidence === undefined ? {} : { evidence: r.evidence }) }));
|
|
114
|
-
const
|
|
121
|
+
const relaxed = relaxations !== undefined && relaxations.length > 0;
|
|
122
|
+
const preimage = relaxed || deployment !== undefined
|
|
123
|
+
? { rows: canonicalRows, ...(relaxed ? { relaxations } : {}), ...(deployment !== undefined ? { deployment } : {}) }
|
|
124
|
+
: canonicalRows;
|
|
115
125
|
return createHash('sha256').update(canonicalJSON(preimage), 'utf8').digest('hex');
|
|
116
126
|
}
|
|
117
127
|
|
|
@@ -219,8 +229,8 @@ export function verifyBundleV3(bundle: BundleV3, opts: VerifyV3Options = {}): V3
|
|
|
219
229
|
const count = (d: BundleV3Result): number => rows.filter((r) => r.result === d).length;
|
|
220
230
|
const expected = { executedPass: count('executed-pass'), executedFail: count('executed-fail'), skipped: count('skipped'), inapplicable: count('inapplicable'), blocked: count('blocked') };
|
|
221
231
|
for (const k of Object.keys(expected) as (keyof typeof expected)[]) if (bundle.results?.totals?.[k] !== expected[k]) rejections.push({ kind: 'totals-mismatch', detail: `totals.${k} is ${String(bundle.results?.totals?.[k])} but the rows count ${expected[k]}` });
|
|
222
|
-
const digest = refusedAs(rejections, 'witness-digest', 'the rows or declared relaxations', () => witnessDigest(rows, bundle.host?.relaxations));
|
|
223
|
-
if (digest !== undefined && bundle.witnessSha256 !== digest) rejections.push({ kind: 'witness-digest', detail: `witnessSha256 ${String(bundle.witnessSha256).slice(0, 12)} does not equal the digest of the rows
|
|
232
|
+
const digest = refusedAs(rejections, 'witness-digest', 'the rows or declared relaxations', () => witnessDigest(rows, bundle.host?.relaxations, bundle.host?.deployment));
|
|
233
|
+
if (digest !== undefined && bundle.witnessSha256 !== digest) 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)})` });
|
|
224
234
|
const assertions = rows.reduce((n, r) => n + (r.assertions ?? 0), 0);
|
|
225
235
|
if (bundle.assertionCount !== assertions) rejections.push({ kind: 'assertion-count', detail: `assertionCount is ${String(bundle.assertionCount)} but the rows sum to ${assertions}` });
|
|
226
236
|
const nonPass = rows.filter((r) => r.result !== 'executed-pass');
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The suite's harness OIDC issuer, as an INSTRUMENT a host claims.
|
|
3
|
+
*
|
|
4
|
+
* Two major-2 scenarios need a token the host would accept, and the suite can
|
|
5
|
+
* mint one only with a key it holds: `v2-oidc-id-token-audience` (RFC 0200 §D)
|
|
6
|
+
* and `v2-lane-exp-only-bound` (RFC 0210 §B). The suite holds the key of exactly
|
|
7
|
+
* one issuer, the one it stands up at `OPENWOP_TEST_OIDC_ISSUER_URL`.
|
|
8
|
+
*
|
|
9
|
+
* ── Why the gate reads the host's lane, not only the environment ────────────
|
|
10
|
+
* Until 2.40.2 both files recorded `blocked` whenever the harness was not the
|
|
11
|
+
* host's trust root. A PRODUCTION host MUST NOT trust a test issuer — a deployed
|
|
12
|
+
* service that accepts tokens signed by a key living on a test runner has an
|
|
13
|
+
* authentication bypass — so every honest production bundle advertising an
|
|
14
|
+
* `oidc` or `exp-only` lane carried `blocked` rows, and RFC 0168 §E.1 makes one
|
|
15
|
+
* `blocked` row deny certification bundle-wide. Measured on openwop-app
|
|
16
|
+
* (ADR 0745 corpus defect 1); MyndHyve is in the same position.
|
|
17
|
+
*
|
|
18
|
+
* RFC 0168 §C.1 already rules on this shape for the other suite instrument a
|
|
19
|
+
* host has to be configured for, the seams profile: "a scenario that finds the
|
|
20
|
+
* exact advert absent records `inapplicable` — never `blocked`, which is
|
|
21
|
+
* reserved for an advertised profile whose seam does not answer". The harness
|
|
22
|
+
* issuer is claimed the same way, by the host's own advertisement: a lane's
|
|
23
|
+
* `issuers[]` (`spec/v2/facets/auth.schema.json`) names the trust roots that
|
|
24
|
+
* lane accepts. So:
|
|
25
|
+
*
|
|
26
|
+
* - the lane's `issuers[]` does not name the harness URL (or no harness URL is
|
|
27
|
+
* configured) → `inapplicable`, and the reason names the issuers the host
|
|
28
|
+
* DOES trust — a fact about the host, not about the suite;
|
|
29
|
+
* - the lane names it → the host has claimed the instrument; a harness that
|
|
30
|
+
* cannot be served, or a control token the host then refuses, is `blocked`
|
|
31
|
+
* or a failure exactly as before.
|
|
32
|
+
*
|
|
33
|
+
* The cost, stated rather than hidden: a production bundle no longer witnesses
|
|
34
|
+
* these rows at all, just as it witnesses no seam-gated row. The witness comes
|
|
35
|
+
* from a cut whose lane advertises the harness issuer — the reference host, or
|
|
36
|
+
* a colocated boot of the same release image.
|
|
37
|
+
*
|
|
38
|
+
* ── Reachability ─────────────────────────────────────────────────────────────
|
|
39
|
+
* The PORT is `issuerListenPort()` (2.39.4: `OPENWOP_TEST_OIDC_ISSUER_PORT`,
|
|
40
|
+
* else the URL's port). The BIND is `receiverBinding()`, as for the webhook
|
|
41
|
+
* receiver and the OAuth doubles: loopback, or `0.0.0.0` when
|
|
42
|
+
* `OPENWOP_CONFORMANCE_HARNESS_HOST` says the host is in a container or on
|
|
43
|
+
* another box. Until 2.40.2 the issuer always bound `127.0.0.1`, so a host in a
|
|
44
|
+
* Linux container reaching it through `host-gateway` found nothing listening on
|
|
45
|
+
* the interface it arrived at (openwop-app ADR 0745 defect 2). The URL is what
|
|
46
|
+
* the host was configured with and is what `iss` and discovery carry; only the
|
|
47
|
+
* listening socket moves.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
import { createServer, type Server } from 'node:http';
|
|
51
|
+
import { receiverBinding } from './webhook-receiver.js';
|
|
52
|
+
import { issuerListenPort, type SyntheticOIDCIssuer } from './oidc-issuer.js';
|
|
53
|
+
|
|
54
|
+
const norm = (u: string): string => u.trim().replace(/\/+$/, '');
|
|
55
|
+
|
|
56
|
+
export type HarnessClaim =
|
|
57
|
+
| { readonly ok: true; readonly url: string }
|
|
58
|
+
| { readonly ok: false; readonly kind: 'inapplicable'; readonly reason: string };
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Has the host claimed the harness issuer on `lane`? Pure: decided from the
|
|
62
|
+
* advertised lane record and the configured harness URL.
|
|
63
|
+
*/
|
|
64
|
+
export function harnessClaimed(lane: Readonly<Record<string, unknown>>, harnessUrl: string | undefined): HarnessClaim {
|
|
65
|
+
const name = String(lane['lane']);
|
|
66
|
+
const issuers = Array.isArray(lane['issuers']) ? (lane['issuers'] as unknown[]).filter((x): x is string => typeof x === 'string') : [];
|
|
67
|
+
const listed = issuers.length > 0 ? issuers.join(', ') : 'none';
|
|
68
|
+
const url = harnessUrl?.trim();
|
|
69
|
+
if (!url) {
|
|
70
|
+
return { ok: false, kind: 'inapplicable', reason: `lane ${name} trusts ${listed}, and no harness issuer is configured (OPENWOP_TEST_OIDC_ISSUER_URL unset): the suite holds a signing key for none of the host's trust roots, so no token it mints could be accepted. A host claims the harness by listing it in the lane's issuers[] (RFC 0168 §C.1's reading for suite instruments)` };
|
|
71
|
+
}
|
|
72
|
+
if (!issuers.some((i) => norm(i) === norm(url))) {
|
|
73
|
+
return { ok: false, kind: 'inapplicable', reason: `lane ${name} trusts ${listed}, which does not include the harness issuer ${url}: this host has not claimed the suite's issuer as a trust root (a production host must not), so the suite holds no key the host accepts. RFC 0168 §C.1's reading for suite instruments — witnessed on a cut whose lane lists the harness` };
|
|
74
|
+
}
|
|
75
|
+
return { ok: true, url: norm(url) };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Serve `issuer`'s discovery + JWKS for the host to fetch. Resolves to the listening server. */
|
|
79
|
+
export async function serveHarnessIssuer(issuer: SyntheticOIDCIssuer, url: string): Promise<Server> {
|
|
80
|
+
const port = issuerListenPort(url);
|
|
81
|
+
const { bind } = receiverBinding();
|
|
82
|
+
const srv = createServer((r, res) => {
|
|
83
|
+
if (r.url === '/.well-known/jwks.json') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(issuer.jwksJson); return; }
|
|
84
|
+
if (r.url === '/.well-known/openid-configuration') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(issuer.discoveryJson); return; }
|
|
85
|
+
res.writeHead(404); res.end();
|
|
86
|
+
});
|
|
87
|
+
// Two scenario files serve the one issuer URL, and vitest runs files in
|
|
88
|
+
// parallel: wait for the holder to close rather than fail (each instance has
|
|
89
|
+
// its own kid, so a host that cached the other file's JWKS re-fetches).
|
|
90
|
+
const deadline = Date.now() + 25_000;
|
|
91
|
+
for (;;) {
|
|
92
|
+
const e = await new Promise<NodeJS.ErrnoException | null>((ok) => {
|
|
93
|
+
const onErr = (err: NodeJS.ErrnoException): void => ok(err);
|
|
94
|
+
srv.once('error', onErr);
|
|
95
|
+
srv.listen(port, bind, () => { srv.off('error', onErr); ok(null); });
|
|
96
|
+
});
|
|
97
|
+
if (e === null) return srv;
|
|
98
|
+
if (e.code !== 'EADDRINUSE' || Date.now() > deadline) throw e;
|
|
99
|
+
await new Promise((ok) => setTimeout(ok, 250));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -23,9 +23,14 @@
|
|
|
23
23
|
* test.
|
|
24
24
|
*
|
|
25
25
|
* **Gate:** an advertised `auth.lanes[]` member with `revocation: "exp-only"`
|
|
26
|
-
* (`inapplicable`, naming that, otherwise)
|
|
27
|
-
* synthetic issuer the
|
|
28
|
-
*
|
|
26
|
+
* (`inapplicable`, naming that, otherwise) whose `issuers[]` lists
|
|
27
|
+
* `OPENWOP_TEST_OIDC_ISSUER_URL`, the synthetic issuer the suite holds the key for. A lane
|
|
28
|
+
* that does not list it records `inapplicable` naming the issuers it does trust: the
|
|
29
|
+
* harness is a suite instrument the host claims by advertising it (RFC 0168 §C.1's reading
|
|
30
|
+
* for the seams profile; lib/harness-issuer.ts), and a production host must never trust a
|
|
31
|
+
* test issuer. Corrected in 2.40.1; this was `blocked`, which denied certification to
|
|
32
|
+
* every honest production bundle. A claimed harness that cannot be served is `blocked` —
|
|
33
|
+
* without a token the host would ever accept, "it refused a string" witnesses nothing.
|
|
29
34
|
* A window under 120 s is `blocked`: the control token needs room inside it.
|
|
30
35
|
*
|
|
31
36
|
* **Each refusal leg is skewed so it isolates ONE bound.** A token minted at `now` with
|
|
@@ -54,13 +59,14 @@
|
|
|
54
59
|
|
|
55
60
|
import { afterAll, describe, it, expect } from 'vitest';
|
|
56
61
|
import { randomBytes } from 'node:crypto';
|
|
57
|
-
import {
|
|
62
|
+
import type { Server } from 'node:http';
|
|
58
63
|
import { driver } from '../lib/driver.js';
|
|
59
64
|
import { softSkip } from '../lib/soft-skip.js';
|
|
60
65
|
import { req } from '../lib/requirement-ids.js';
|
|
61
66
|
import { readErrorCode } from '../lib/error-envelope.js';
|
|
62
67
|
import { v2Discovery, familyAdvertised } from '../lib/v2.js';
|
|
63
|
-
import { createSyntheticOIDCIssuer,
|
|
68
|
+
import { createSyntheticOIDCIssuer, type SyntheticOIDCIssuer } from '../lib/oidc-issuer.js';
|
|
69
|
+
import { harnessClaimed, serveHarnessIssuer } from '../lib/harness-issuer.js';
|
|
64
70
|
|
|
65
71
|
export const HOST_CALLBACK_NOT_REQUIRED =
|
|
66
72
|
'the suite stands up the synthetic OIDC issuer and the host fetches its JWKS; no request returns to the suite\'s own API, so no host-reachable callback is needed';
|
|
@@ -113,23 +119,20 @@ async function gate(): Promise<Gate | { readonly kind: 'inapplicable' | 'blocked
|
|
|
113
119
|
if (window < 2 * MARGIN) {
|
|
114
120
|
return { kind: 'blocked', reason: `lane ${String(lane['lane'])} advertises a ${window}s window; the control token needs ${MARGIN}s of room inside it and the sabotage tokens ${MARGIN}s outside, so a window under ${2 * MARGIN}s cannot be probed without the two cases overlapping` };
|
|
115
121
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
122
|
+
// The harness is an instrument the host claims by listing it in THIS lane's issuers[]
|
|
123
|
+
// (RFC 0168 §C.1's reading; lib/harness-issuer.ts). A production host lists its real
|
|
124
|
+
// IdP and must never list a test issuer, so it records `inapplicable` naming the issuers
|
|
125
|
+
// it does trust. Before 2.40.1 this was `blocked`, which denied certification to every
|
|
126
|
+
// honest production bundle advertising an exp-only lane.
|
|
127
|
+
const claim = harnessClaimed(lane, process.env['OPENWOP_TEST_OIDC_ISSUER_URL']);
|
|
128
|
+
if (!claim.ok) return { kind: claim.kind, reason: claim.reason };
|
|
129
|
+
const url = claim.url;
|
|
120
130
|
const audience = process.env['OPENWOP_TEST_OIDC_AUDIENCE']?.trim() ?? 'openwop-conformance';
|
|
121
131
|
if (issuer === null) {
|
|
122
132
|
const made = createSyntheticOIDCIssuer({ issuer: url, audience, algorithm: 'RS256' });
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
res.writeHead(404); res.end();
|
|
127
|
-
});
|
|
128
|
-
await new Promise<void>((resolve, reject) => {
|
|
129
|
-
srv.once('error', reject);
|
|
130
|
-
srv.listen(issuerListenPort(url), '127.0.0.1', () => resolve());
|
|
131
|
-
});
|
|
132
|
-
server = srv;
|
|
133
|
+
try { server = await serveHarnessIssuer(made, url); } catch (e) {
|
|
134
|
+
return { kind: 'blocked', reason: `lane ${String(lane['lane'])} lists the harness issuer, but it could not be served for ${url}: ${(e as Error).message}` };
|
|
135
|
+
}
|
|
133
136
|
issuer = made;
|
|
134
137
|
}
|
|
135
138
|
return { issuer, url, window, lane: String(lane['lane']) };
|
|
@@ -10,10 +10,14 @@
|
|
|
10
10
|
* ID-token case explicitly as admissible under the audience MUST `identity.md` §2.1
|
|
11
11
|
* already carries, and refuses everything else.
|
|
12
12
|
*
|
|
13
|
-
* **Gate:** an advertised `oidc` lane
|
|
14
|
-
* synthetic issuer the
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* **Gate:** an advertised `oidc` lane whose `issuers[]` lists `OPENWOP_TEST_OIDC_ISSUER_URL`,
|
|
14
|
+
* the synthetic issuer the suite holds the key for. A lane that does not list it (every
|
|
15
|
+
* production host, which must not trust a test issuer) records `inapplicable`, naming the
|
|
16
|
+
* issuers it does trust — the harness is a suite instrument the host claims by advertising
|
|
17
|
+
* it, RFC 0168 §C.1's reading for the seams profile (lib/harness-issuer.ts; corrected in
|
|
18
|
+
* 2.40.1, when this was `blocked` and denied certification to every production bundle).
|
|
19
|
+
* A claimed harness that cannot be served, or whose same-audience control is refused, is
|
|
20
|
+
* `blocked`: an assertion that a random string is refused would witness nothing.
|
|
17
21
|
*
|
|
18
22
|
* `HOST_CALLBACK_NOT_REQUIRED`: the suite stands the issuer up itself and the host reaches
|
|
19
23
|
* it for JWKS; nothing calls back into the suite's own API.
|
|
@@ -29,13 +33,14 @@
|
|
|
29
33
|
|
|
30
34
|
import { afterAll, describe, it, expect } from 'vitest';
|
|
31
35
|
import { randomBytes } from 'node:crypto';
|
|
32
|
-
import {
|
|
36
|
+
import type { Server } from 'node:http';
|
|
33
37
|
import { driver } from '../lib/driver.js';
|
|
34
38
|
import { softSkip } from '../lib/soft-skip.js';
|
|
35
39
|
import { req } from '../lib/requirement-ids.js';
|
|
36
40
|
import { readErrorCode } from '../lib/error-envelope.js';
|
|
37
|
-
import { createSyntheticOIDCIssuer,
|
|
41
|
+
import { createSyntheticOIDCIssuer, type SyntheticOIDCIssuer } from '../lib/oidc-issuer.js';
|
|
38
42
|
import { prmGate } from '../lib/protected-resource.js';
|
|
43
|
+
import { harnessClaimed, serveHarnessIssuer } from '../lib/harness-issuer.js';
|
|
39
44
|
|
|
40
45
|
export const HOST_CALLBACK_NOT_REQUIRED =
|
|
41
46
|
'the suite stands up the synthetic OIDC issuer and the host fetches its JWKS; no request returns to the suite\'s own API, so no host-reachable callback is needed';
|
|
@@ -50,22 +55,11 @@ afterAll(async () => {
|
|
|
50
55
|
issuer = null;
|
|
51
56
|
});
|
|
52
57
|
|
|
53
|
-
/** Stand the issuer up on the URL the
|
|
54
|
-
async function harness(audience: string): Promise<{ url: string; issuer: SyntheticOIDCIssuer } |
|
|
55
|
-
const url = process.env['OPENWOP_TEST_OIDC_ISSUER_URL']?.trim();
|
|
56
|
-
if (!url) return null;
|
|
58
|
+
/** Stand the issuer up on the URL the host's oidc lane lists (the claim is checked by the caller). */
|
|
59
|
+
async function harness(url: string, audience: string): Promise<{ url: string; issuer: SyntheticOIDCIssuer } | string> {
|
|
57
60
|
if (issuer !== null) return { url, issuer };
|
|
58
61
|
const made = createSyntheticOIDCIssuer({ issuer: url, audience, algorithm: 'RS256' });
|
|
59
|
-
|
|
60
|
-
if (r.url === '/.well-known/jwks.json') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(made.jwksJson); return; }
|
|
61
|
-
if (r.url === '/.well-known/openid-configuration') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(made.discoveryJson); return; }
|
|
62
|
-
res.writeHead(404); res.end();
|
|
63
|
-
});
|
|
64
|
-
await new Promise<void>((resolve, reject) => {
|
|
65
|
-
srv.once('error', reject);
|
|
66
|
-
srv.listen(issuerListenPort(url), '127.0.0.1', () => resolve());
|
|
67
|
-
});
|
|
68
|
-
server = srv;
|
|
62
|
+
try { server = await serveHarnessIssuer(made, url); } catch (e) { return `the harness issuer could not be served for ${url}: ${(e as Error).message}`; }
|
|
69
63
|
issuer = made;
|
|
70
64
|
return { url, issuer: made };
|
|
71
65
|
}
|
|
@@ -76,9 +70,16 @@ describe('RFC 0200 §D — v2-oidc-id-token-audience (gated on an oidc lane and
|
|
|
76
70
|
if (!g.ok) return softSkip(g.kind, g.reason);
|
|
77
71
|
const oidc = g.lanes.find((l) => l.lane === 'oidc');
|
|
78
72
|
if (oidc === undefined) return softSkip('inapplicable', 'no oidc lane advertised — §D names the ID-token case ON that lane and binds no other');
|
|
73
|
+
// The harness is an instrument the host claims by listing it in the lane's issuers[]
|
|
74
|
+
// (RFC 0168 §C.1's reading; lib/harness-issuer.ts). A production host lists its real
|
|
75
|
+
// IdP and must never list a test issuer, so that host records `inapplicable` with the
|
|
76
|
+
// issuers it does trust, not `blocked` — before 2.40.1 this was `blocked` and denied
|
|
77
|
+
// certification to every honest production bundle with an oidc lane.
|
|
78
|
+
const claim = harnessClaimed(oidc as unknown as Record<string, unknown>, process.env['OPENWOP_TEST_OIDC_ISSUER_URL']);
|
|
79
|
+
if (!claim.ok) return softSkip(claim.kind, claim.reason);
|
|
79
80
|
const audience = process.env['OPENWOP_TEST_OIDC_AUDIENCE']?.trim() ?? 'openwop-conformance';
|
|
80
|
-
const h = await harness(audience);
|
|
81
|
-
if (h ===
|
|
81
|
+
const h = await harness(claim.url, audience);
|
|
82
|
+
if (typeof h === 'string') return softSkip('blocked', `the oidc lane lists the harness issuer, but ${h}`);
|
|
82
83
|
|
|
83
84
|
// An ID token, not an access token: `nonce` and `auth_time` are what make it one.
|
|
84
85
|
const good = h.issuer.mint({ sub: `conformance-${randomBytes(6).toString('hex')}`, nonce: randomBytes(8).toString('hex'), auth_time: Math.floor(Date.now() / 1000) });
|
|
@@ -90,7 +91,7 @@ describe('RFC 0200 §D — v2-oidc-id-token-audience (gated on an oidc lane and
|
|
|
90
91
|
// acceptance DOES establish, once it holds, is that the refusal below is caused by the
|
|
91
92
|
// audience and by nothing else: same issuer, same key, same token shape.
|
|
92
93
|
if (probe.status === 401) {
|
|
93
|
-
return softSkip('blocked', `a same-audience ID token from ${h.url} was refused (401 ${readErrorCode(probe.json) ?? ''}) — either the host
|
|
94
|
+
return softSkip('blocked', `a same-audience ID token from ${h.url} was refused (401 ${readErrorCode(probe.json) ?? ''}) although the oidc lane lists that issuer — either the host's trust configuration does not match its advertisement, or OPENWOP_TEST_OIDC_AUDIENCE does not name the audience it is configured with; with no accepted token the foreign-audience refusal would prove nothing`);
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
const foreign = h.issuer.mint({ sub: `conformance-${randomBytes(6).toString('hex')}`, aud: `other-client-${randomBytes(6).toString('hex')}`, nonce: randomBytes(8).toString('hex'), auth_time: Math.floor(Date.now() / 1000) });
|