@openwop/openwop-conformance 2.4.6 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +11 -1
- package/dist/cli.js +93 -0
- package/dist/spec-artifacts.lock.json +2 -2
- package/package.json +2 -2
- package/schemas/CORPUS-STAMP.json +15 -15
- package/src/cli.ts +92 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# `@openwop/openwop-conformance` Changelog
|
|
2
2
|
|
|
3
|
+
## [2.5.0] — 2026-09-18 — `--verify`, for auditing a bundle you did not cut
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`--verify <bundle> [--host-key <pem>]`** audits a certification bundle with
|
|
8
|
+
no host and no corpus clone. Exit `0` verified · `1` rejected · **`2` coherent
|
|
9
|
+
but NOT independently verified** · `3` not a bundle. Exit 2 is load-bearing:
|
|
10
|
+
without it, `--verify` with no key returns green and the program re-creates
|
|
11
|
+
the vacuous pass that bundle v3 exists to eliminate. The output states what
|
|
12
|
+
the command does NOT do — it never re-runs anything, so a host that measured
|
|
13
|
+
itself wrongly and signed the result verifies clean here.
|
|
14
|
+
|
|
15
|
+
## [2.4.7] — 2026-09-18 — schema examples are validated now
|
|
16
|
+
|
|
17
|
+
**Why a patch.** No conformance code changes; the corpus version moves because
|
|
18
|
+
`schemas/v2/` content and a root gate script changed.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- `scripts/check-v2-schemas.mjs` now validates every root-level `examples` entry
|
|
23
|
+
against its own schema. It found **13 invalid examples across 6 v2 schemas**,
|
|
24
|
+
9 of them ids minted before RFC 0184/0187 made their kind tenant-bound. All
|
|
25
|
+
re-minted.
|
|
26
|
+
|
|
3
27
|
## [2.4.6] — 2026-09-18 — the npm page told the world to install a release candidate
|
|
4
28
|
|
|
5
29
|
### Fixed
|
package/README.md
CHANGED
|
@@ -11,11 +11,21 @@
|
|
|
11
11
|
# --legacy-peer-deps is REQUIRED, not optional: the exact peer pin is what npm's
|
|
12
12
|
# default resolver refuses. npm 10.9 fails outright with
|
|
13
13
|
# "Cannot read properties of null (reading 'edgesOut')" — use npm >= 11.
|
|
14
|
-
npm install --legacy-peer-deps @openwop/openwop-conformance@2.
|
|
14
|
+
npm install --legacy-peer-deps @openwop/openwop-conformance@2.5.0 @openwop/spec-artifacts@2.5.0
|
|
15
15
|
# or run without install:
|
|
16
16
|
npx @openwop/openwop-conformance --base-url https://api.example.com --api-key hk_test_...
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
### Auditing someone else's bundle
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx @openwop/openwop-conformance --verify bundle-v3.json --host-key host.pub.pem
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
No host, no clone. Exit `0` verified · `1` rejected · **`2` coherent but not independently verified** · `3` not a bundle.
|
|
26
|
+
|
|
27
|
+
Exit 2 is the one that matters: without `--host-key` the signature is unchecked, and a bundle that merely *hangs together* is not a bundle that has been *verified*. The command also prints what it does **not** do — it never re-runs anything, so a host that measured itself wrongly and signed the result verifies clean here. It audits an attestation, not a host.
|
|
28
|
+
|
|
19
29
|
> **Spec:** [github.com/openwop/openwop](https://github.com/openwop/openwop) · See `CHANGELOG.md` below for release history.
|
|
20
30
|
|
|
21
31
|
The suite is intentionally self-contained — it does NOT depend on the reference implementation. A spec-compliant server written in any language can run this suite against itself by spinning up its server, exporting the env vars, and running `npx vitest run`.
|
package/dist/cli.js
CHANGED
|
@@ -62,6 +62,7 @@ function parseArgs(argv) {
|
|
|
62
62
|
let bundleVersion = '3';
|
|
63
63
|
let hostBuild;
|
|
64
64
|
let evidenceTier = 'self';
|
|
65
|
+
let verifyPath, verifyHostKeyPath;
|
|
65
66
|
let signingKeyPath, signingKeyId, verifierKeyPath, verifierKeyId;
|
|
66
67
|
let targetMajor;
|
|
67
68
|
let maxWorkers = parseMaxWorkers(process.env.OPENWOP_MAX_WORKERS, 'OPENWOP_MAX_WORKERS');
|
|
@@ -169,6 +170,12 @@ function parseArgs(argv) {
|
|
|
169
170
|
// `OPENWOP_REQUIRE_BEHAVIOR=true` did anything. So a cut that named the
|
|
170
171
|
// flag measured the LOOSE answer and reported the strict one — the exact
|
|
171
172
|
// failure shape the runbook warns about, in the tool that measures it.
|
|
173
|
+
case '--verify':
|
|
174
|
+
verifyPath = nextValue();
|
|
175
|
+
break;
|
|
176
|
+
case '--host-key':
|
|
177
|
+
verifyHostKeyPath = nextValue();
|
|
178
|
+
break;
|
|
172
179
|
case '--require-behavior':
|
|
173
180
|
process.env['OPENWOP_REQUIRE_BEHAVIOR'] = 'true';
|
|
174
181
|
break;
|
|
@@ -202,6 +209,8 @@ function parseArgs(argv) {
|
|
|
202
209
|
verifierKeyPath,
|
|
203
210
|
verifierKeyId,
|
|
204
211
|
maxWorkers,
|
|
212
|
+
verifyPath,
|
|
213
|
+
verifyHostKeyPath,
|
|
205
214
|
};
|
|
206
215
|
}
|
|
207
216
|
/** Parse a `--max-workers` / `OPENWOP_MAX_WORKERS` value: a positive integer, else exit 2. */
|
|
@@ -260,6 +269,17 @@ Certification (RFC 0089):
|
|
|
260
269
|
to <out.json>. Requires --base-url (and --api-key as usual).
|
|
261
270
|
|
|
262
271
|
Other:
|
|
272
|
+
--verify <bundle> Audit a certification bundle WITHOUT a host. Exit codes:
|
|
273
|
+
0 verified — no rejections, host signature checked,
|
|
274
|
+
certified list re-derived from the bundle's own
|
|
275
|
+
discovery document
|
|
276
|
+
1 rejected — one or more problems, each printed
|
|
277
|
+
2 coherent but NOT independently verified (no
|
|
278
|
+
--host-key, or the certified list is the emitter's
|
|
279
|
+
word). Do not quote this as "verified"
|
|
280
|
+
3 the file is missing or is not a bundle
|
|
281
|
+
--host-key <pem> The key --verify checks the host signature under. Without
|
|
282
|
+
it the signature is unverified and --verify exits 2.
|
|
263
283
|
--require-behavior Strict mode: an ADVERTISED behaviour that cannot be
|
|
264
284
|
observed FAILS instead of soft-skipping (RFC 0148 §B).
|
|
265
285
|
Equivalent to OPENWOP_REQUIRE_BEHAVIOR=true.
|
|
@@ -811,6 +831,79 @@ async function main() {
|
|
|
811
831
|
process.stdout.write(HELP_TEXT);
|
|
812
832
|
process.exit(0);
|
|
813
833
|
}
|
|
834
|
+
// `--verify <bundle.json>` — audit a bundle WITHOUT a host and without cloning
|
|
835
|
+
// the corpus. `verifyBundleV3` was exported but only ever called as the
|
|
836
|
+
// emitter's self-check, so auditing a third party's certification claim meant
|
|
837
|
+
// writing a driver script — which means in practice nobody did, and a
|
|
838
|
+
// certification program whose verifier ships only as a library function is
|
|
839
|
+
// asking to be taken on trust.
|
|
840
|
+
//
|
|
841
|
+
// The exit contract has FOUR values on purpose. "Structurally coherent" and
|
|
842
|
+
// "verified" are different facts, and collapsing them is how the vacuous pass
|
|
843
|
+
// gets rebuilt: without exit 2, every CI that runs `--verify` with no key
|
|
844
|
+
// prints green and reports "verified".
|
|
845
|
+
if (args.verifyPath !== undefined) {
|
|
846
|
+
if (!existsSync(args.verifyPath)) {
|
|
847
|
+
process.stderr.write(`openwop-conformance --verify: no such file: ${args.verifyPath}\n`);
|
|
848
|
+
process.exit(3);
|
|
849
|
+
}
|
|
850
|
+
let bundle;
|
|
851
|
+
try {
|
|
852
|
+
bundle = JSON.parse(readFileSync(args.verifyPath, 'utf8'));
|
|
853
|
+
}
|
|
854
|
+
catch (e) {
|
|
855
|
+
process.stderr.write(`openwop-conformance --verify: ${args.verifyPath} is not readable JSON — ${e.message}\n`);
|
|
856
|
+
process.exit(3);
|
|
857
|
+
}
|
|
858
|
+
const hostKey = args.verifyHostKeyPath !== undefined ? readFileSync(args.verifyHostKeyPath, 'utf8') : undefined;
|
|
859
|
+
const verifierKey = args.verifierKeyPath !== undefined ? readFileSync(args.verifierKeyPath, 'utf8') : undefined;
|
|
860
|
+
const verdict = verifyBundleV3(bundle, {
|
|
861
|
+
...(hostKey !== undefined ? { hostPublicKeyPem: hostKey } : {}),
|
|
862
|
+
...(verifierKey !== undefined ? { verifierPublicKeyPem: verifierKey } : {}),
|
|
863
|
+
});
|
|
864
|
+
const t = bundle.results?.totals ?? {};
|
|
865
|
+
const out = [
|
|
866
|
+
`bundle: ${args.verifyPath}`,
|
|
867
|
+
`host: ${bundle.host?.name ?? '?'} build ${bundle.host?.build?.kind ?? '?'}:${String(bundle.host?.build?.id ?? '?').slice(0, 12)}`,
|
|
868
|
+
`suite: ${bundle.suite?.version ?? '?'} (this CLI is ${suiteVersion()})`,
|
|
869
|
+
`totals: executedPass=${t.executedPass ?? '?'} executedFail=${t.executedFail ?? '?'} blocked=${t.blocked ?? '?'} inapplicable=${t.inapplicable ?? '?'} skipped=${t.skipped ?? '?'}`,
|
|
870
|
+
`certified: ${verdict.certifiedProfiles.length > 0 ? verdict.certifiedProfiles.join(', ') : '(none)'}`,
|
|
871
|
+
'',
|
|
872
|
+
'What this command does NOT do:',
|
|
873
|
+
' · It does not re-run anything. A host that measured itself wrongly, and signed',
|
|
874
|
+
' the result, verifies clean here. This audits an attestation, not a host.',
|
|
875
|
+
' · It does not resolve keyId against the live host. It checks the signature under',
|
|
876
|
+
' the key YOU supply with --host-key; it cannot tell you that key is the host\u2019s.',
|
|
877
|
+
' · It does not judge suite currency. The suite version is reported above as data:',
|
|
878
|
+
' an older bundle measured less, and whose fact that is belongs to its emitter.',
|
|
879
|
+
'',
|
|
880
|
+
];
|
|
881
|
+
if (verdict.rejections.length > 0) {
|
|
882
|
+
out.push(`REJECTED \u2014 ${verdict.rejections.length} problem(s):`);
|
|
883
|
+
for (const r of verdict.rejections)
|
|
884
|
+
out.push(` [${r.kind}]${r.profile ? ` (${r.profile})` : ''} ${r.detail}`);
|
|
885
|
+
process.stdout.write(out.join('\n') + '\n');
|
|
886
|
+
process.exit(1);
|
|
887
|
+
}
|
|
888
|
+
const gaps = [];
|
|
889
|
+
if (!verdict.signatureVerified)
|
|
890
|
+
gaps.push('the host signature was NOT verified (no --host-key supplied)');
|
|
891
|
+
if (!verdict.derivabilityChecked)
|
|
892
|
+
gaps.push('the certified list was NOT re-derived from the bundle\u2019s discovery document \u2014 it is the emitter\u2019s word');
|
|
893
|
+
if ((bundle.claimedProfiles ?? []).some((p) => p.evidenceTier === 'independent') && !verdict.verifierSignatureVerified)
|
|
894
|
+
gaps.push('an `independent` tier is claimed but the verifier signature was NOT verified (no --verifier-key)');
|
|
895
|
+
if (gaps.length > 0) {
|
|
896
|
+
out.push('COHERENT, NOT INDEPENDENTLY VERIFIED \u2014 zero rejections, but:');
|
|
897
|
+
for (const g of gaps)
|
|
898
|
+
out.push(` \u00b7 ${g}`);
|
|
899
|
+
out.push('', 'Do not quote this as "verified". Exit 2 exists to keep that distinction.');
|
|
900
|
+
process.stdout.write(out.join('\n') + '\n');
|
|
901
|
+
process.exit(2);
|
|
902
|
+
}
|
|
903
|
+
out.push('VERIFIED \u2014 zero rejections, host signature checked, certified list re-derived.');
|
|
904
|
+
process.stdout.write(out.join('\n') + '\n');
|
|
905
|
+
process.exit(0);
|
|
906
|
+
}
|
|
814
907
|
// Env vars OVERRIDE flags only when the flag was unset (consistent
|
|
815
908
|
// with the rest of the harness — env wins on the absence of CLI input).
|
|
816
909
|
const env = { ...process.env };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openwop/openwop-conformance",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "Production-ready black-box conformance suite for OpenWOP v1.0 compliant servers.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -56,6 +56,6 @@
|
|
|
56
56
|
"@openwop/spec-artifacts": "file:../spec-artifacts"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
|
-
"@openwop/spec-artifacts": "2.
|
|
59
|
+
"@openwop/spec-artifacts": "2.5.0"
|
|
60
60
|
}
|
|
61
61
|
}
|
|
@@ -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.
|
|
5
|
-
"corpusTag": "v2.
|
|
4
|
+
"version": "2.5.0",
|
|
5
|
+
"corpusTag": "v2.5.0",
|
|
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": "39081c59fb696159806b0f2f9a42e7e9ff830d622fcf2ad4159b21357580a955",
|
|
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": "f35db54139898b07150e0a2a40ce6ef380aac3768b1bd72e41ac2e32dc102daf",
|
|
13
|
+
"api/v2/asyncapi.yaml": "b5d7c42d6d2b7aa3b392762b4544e90017fe9ab53cfbcb28488c96f1b2ede93d",
|
|
14
|
+
"api/v2/openapi.yaml": "a026b36f9228b5216b544c105e99bc1d0eb27e4702fb05c696576ce79d52c9cc",
|
|
15
15
|
"api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
|
|
16
16
|
"schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
|
|
17
17
|
"schemas/a2a-task-state.schema.json": "c9365918f993f943b4b619d42551eb066a1ed33a08d895d51b395432a5b1f1bc",
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
"schemas/trigger-subscription-registration.schema.json": "929d628bad08e1def696e47e9a148aba00fa801e28d49bbd0dc2360b08673435",
|
|
99
99
|
"schemas/trigger-subscription.schema.json": "4f7b067f20032073b140c4bb15dc9a9914c4f6421a252ff1ff261cbdd8edf270",
|
|
100
100
|
"schemas/ui-plugin-message.schema.json": "346638babf1f8ea7b161cde75c0038b518cd2cc5bf0631772e20a9b74c96b5dd",
|
|
101
|
-
"schemas/v2/a2a-task-state.schema.json": "
|
|
101
|
+
"schemas/v2/a2a-task-state.schema.json": "e19e8770f2f9c77cb914eca181a5cd105f1fc8bd8ccffd821e36137bf3ba516e",
|
|
102
102
|
"schemas/v2/a2ui-surface-delta-frame.schema.json": "6889c5da3fe7e76cb614038c0eea6774776fa640448558b258eb87fb50cb9b64",
|
|
103
103
|
"schemas/v2/agent-deployment-transition.schema.json": "5fb65556576e3d8705a4834d6ab8ab47389123c40f206d39ba672bc03302c544",
|
|
104
104
|
"schemas/v2/agent-deployment.schema.json": "5581fea8ed8bf99ff05c8df9d3cc007fa2b94a446e7a37a68ea73a513d55b6cd",
|
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
"schemas/v2/budget-policy.schema.json": "c7449daeb6e1d95e7a047b8c2814d3d54748697c27046972834874867d9b5f4c",
|
|
118
118
|
"schemas/v2/capabilities.schema.json": "fa9b80a9107fc322ecadbac808e7fbe5e16ccf0af1a6bf673181faa8379dde33",
|
|
119
119
|
"schemas/v2/certification-bundle.schema.json": "d2cbc5cadddff13630a528dc9673162f582c035c6a6663ca02c1ac53e8fec40c",
|
|
120
|
-
"schemas/v2/channel-presence-payload.schema.json": "
|
|
120
|
+
"schemas/v2/channel-presence-payload.schema.json": "1c20ad810cb311d47aa0b95d928490826b2468fd161f884cd29e867053e6782f",
|
|
121
121
|
"schemas/v2/channel-written-payload.schema.json": "ccecff3c71a3275ad8db035ac0e04db25e6f8d23cf091ddbcd5a60d1be1cbb6b",
|
|
122
122
|
"schemas/v2/chat-card-pack-manifest.schema.json": "8df0d9d273ae22efc6876d5586833f1d4c2e551edc3ac6cf6e8f0719329f03dd",
|
|
123
123
|
"schemas/v2/compact-tool-descriptor.schema.json": "b5c418d72082d5b713bf318212a83397e5be98e56d25ca056302b41b8dd8273b",
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
"schemas/v2/configurable.schema.json": "33a6884047e2490f723733bf8b74d3b9164e00374cfea6dc3eac5a70dc368861",
|
|
127
127
|
"schemas/v2/connection-pack-manifest.schema.json": "7410a3011b28829b286e3ff764bdb283854dca69ea4b71a8441f8c1c4f1cc948",
|
|
128
128
|
"schemas/v2/conversation-event.schema.json": "e87b7fb36ab4ca0afe9f77993654065b28b67e5f24b20175907f90e238942025",
|
|
129
|
-
"schemas/v2/conversation-turn.schema.json": "
|
|
129
|
+
"schemas/v2/conversation-turn.schema.json": "a15b43dc15dbcc6aaf216bfab7424c540b514d7e40e661a732384aad45387006",
|
|
130
130
|
"schemas/v2/credential-provenance.schema.json": "f86b09e94b1c524dec7d1c6353965665ae8891e9517faef6e742d639c5578c99",
|
|
131
131
|
"schemas/v2/credential-reference.schema.json": "ea699ce30f58fb2c32d4279158b3f730f70be7b2dc8c3037693a76e66eb3e001",
|
|
132
132
|
"schemas/v2/debug-bundle.schema.json": "0bfa4b1d91fefbc12c95670dba9d5eacb72b879bb9a5c074e821ca9ed0fee6fd",
|
|
@@ -176,14 +176,14 @@
|
|
|
176
176
|
"schemas/v2/run-orchestrator-decided-event.schema.json": "bb76e15ff72e669f12938684033b55dc567470e1a378e19ffc3fa0e4034ab5a7",
|
|
177
177
|
"schemas/v2/run-snapshot.schema.json": "65a3c82220da2e1a54324665d517dc0e627225f3d37ef75b43bef3fb7e3bac0e",
|
|
178
178
|
"schemas/v2/security-advisory.schema.json": "6a382f737730a738d634266315e9d335e5760020a0cdd9f30727c2f987a01770",
|
|
179
|
-
"schemas/v2/self-hosted-runner-dispatch-frame.schema.json": "
|
|
179
|
+
"schemas/v2/self-hosted-runner-dispatch-frame.schema.json": "9524c3fcf2e195bb10b9561dea34eb2fbe49607b90d010efb0e4c9e36bf87842",
|
|
180
180
|
"schemas/v2/self-hosted-runner-registration.schema.json": "93d60003a668cacb0eeac37699ca2884c6b79332d9494cb9e0a1752ac8f8a16c",
|
|
181
|
-
"schemas/v2/self-hosted-runner-result-frame.schema.json": "
|
|
181
|
+
"schemas/v2/self-hosted-runner-result-frame.schema.json": "505b8e44c93bc4fd3c9fa73bbd7bd5b3d1a1a123f54096761fc0d0b210e5ec8a",
|
|
182
182
|
"schemas/v2/subject-link.schema.json": "1fc941d6c14af886534009f2ffa6611e2035eb07e30b1df918c50d6bafff026c",
|
|
183
183
|
"schemas/v2/subject.schema.json": "094e981fb20052a911393e219ea5f2065d8b8417b65080a70c5ea99fd960a8fe",
|
|
184
184
|
"schemas/v2/suspend-request.schema.json": "4a0dda88a257cdb21c9b57fc9ad898d91de0d75bc4944a693eb950b709f58c91",
|
|
185
185
|
"schemas/v2/tool-descriptor.schema.json": "658b18ef173e0f12d732f92d4866a9b3daeb8f88b493db8b025a577e09dd681c",
|
|
186
|
-
"schemas/v2/trigger-event.schema.json": "
|
|
186
|
+
"schemas/v2/trigger-event.schema.json": "66077c3b3d5b81591270655643ae8957c4c42aaf7cf31c45e8e45cb1b73fde63",
|
|
187
187
|
"schemas/v2/trigger-subscription-registration.schema.json": "04bc314a07c7e8b565577cb8df5821d968ce2af915900dfdccd4a24e928d20db",
|
|
188
188
|
"schemas/v2/trigger-subscription.schema.json": "c887d4b9a12be0133f8c9dfd3a8bafcb518e27ecc2e800447ebc4d1fef111503",
|
|
189
189
|
"schemas/v2/ui-plugin-message.schema.json": "879d65867c01656b3962d64283e66364dcb8f626ad26c5fcf2d2d55a79ae0ce9",
|
|
@@ -200,7 +200,7 @@
|
|
|
200
200
|
"schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
|
|
201
201
|
"spec/v1/alias-detectors.json": "e90916db12d2297e2e560b2d54d2c4702d6cb3bef7179a575844687a7ec17262",
|
|
202
202
|
"spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
|
|
203
|
-
"spec/v1/core-standard-manifest.json": "
|
|
203
|
+
"spec/v1/core-standard-manifest.json": "c1b2629f9d2a374fd9121c1f5ce906e7c4225798c49373cb66f9064050b23bb7",
|
|
204
204
|
"spec/v1/deprecations.json": "c81c8f71460936c8d489d8270477c7661cf4a5d7b7bad86c98899750b5656027",
|
|
205
205
|
"spec/v1/deprecations.schema.json": "18c87e78bedc210431f795ae44c5b5d202f2f3317850d5cf86867d4f1fa1cdfb",
|
|
206
206
|
"spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
|
|
@@ -219,7 +219,7 @@
|
|
|
219
219
|
"spec/v2/core/errors.md": "ad85fc830139e587f20deabf88297b91b1fb6ce31e80d5442624a650246ab16e",
|
|
220
220
|
"spec/v2/core/events.md": "f1bac16419740f3b722febb6f5caaabed8838e092cba3d080fe9184024049dcf",
|
|
221
221
|
"spec/v2/core/form-content-packs.md": "6ffaae85d4550be48dd53fb6e2959af8aceb35bd87c5343b722885084f197713",
|
|
222
|
-
"spec/v2/core/headers.md": "
|
|
222
|
+
"spec/v2/core/headers.md": "cc60370c7443752d462d032478bf23d976ec2efc2e9730cf76542a1d8be4cfc7",
|
|
223
223
|
"spec/v2/core/idempotency.md": "b86c352d964aebad532727f314e26ed2177d49de382076fa64d3162e2f56118d",
|
|
224
224
|
"spec/v2/core/identity.md": "9d2b823eb899463807452fe5721dcf09881837c161be024175957db27870b243",
|
|
225
225
|
"spec/v2/core/interop.md": "d4c404e469e1d9d9be88bc2c939d777e7d7c1d0419b1251c18770f039838a401",
|
|
@@ -271,8 +271,8 @@
|
|
|
271
271
|
"spec/v2/path-manifest.json": "a6f4e652e866889e8bde815874062f62929aee95dc8e58e7152c56adee262163",
|
|
272
272
|
"spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
|
|
273
273
|
"spec/v2/profiles.json": "0636f19fceae625390003a347e70ef4797d84766b5c24ce8a02cea52aadebca4",
|
|
274
|
-
"spec/v2/release.json": "
|
|
274
|
+
"spec/v2/release.json": "6d251f6f5ea4ce8478b97bcb6eb278c5d74539503696f0dc5c27b00cce9374f0",
|
|
275
275
|
"spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624"
|
|
276
276
|
},
|
|
277
|
-
"corpusCommit": "
|
|
277
|
+
"corpusCommit": "61d4b171eb376eb754e27b681d5aaf79fd92e46d"
|
|
278
278
|
}
|
package/src/cli.ts
CHANGED
|
@@ -75,6 +75,10 @@ interface ParsedArgs {
|
|
|
75
75
|
readonly signingKeyPath: string | undefined;
|
|
76
76
|
readonly signingKeyId: string | undefined;
|
|
77
77
|
readonly verifierKeyPath: string | undefined;
|
|
78
|
+
/** `--verify <bundle.json>` — audit a bundle someone else cut, without cloning the corpus. */
|
|
79
|
+
readonly verifyPath: string | undefined;
|
|
80
|
+
/** `--host-key <pem>` — the key `--verify` checks the host signature under. Absent ⇒ the verdict is INCOMPLETE, not clean. */
|
|
81
|
+
readonly verifyHostKeyPath: string | undefined;
|
|
78
82
|
readonly verifierKeyId: string | undefined;
|
|
79
83
|
/**
|
|
80
84
|
* S43 (2026-08-18) — cap on concurrently running scenario FILES, forwarded to
|
|
@@ -100,6 +104,7 @@ function parseArgs(argv: readonly string[]): ParsedArgs {
|
|
|
100
104
|
let bundleVersion: '2' | '3' = '3';
|
|
101
105
|
let hostBuild: ParsedArgs['hostBuild'];
|
|
102
106
|
let evidenceTier: ParsedArgs['evidenceTier'] = 'self';
|
|
107
|
+
let verifyPath: string | undefined, verifyHostKeyPath: string | undefined;
|
|
103
108
|
let signingKeyPath: string | undefined, signingKeyId: string | undefined, verifierKeyPath: string | undefined, verifierKeyId: string | undefined;
|
|
104
109
|
let targetMajor: 1 | 2 | undefined;
|
|
105
110
|
let maxWorkers: number | undefined = parseMaxWorkers(process.env.OPENWOP_MAX_WORKERS, 'OPENWOP_MAX_WORKERS');
|
|
@@ -187,6 +192,12 @@ function parseArgs(argv: readonly string[]): ParsedArgs {
|
|
|
187
192
|
// `OPENWOP_REQUIRE_BEHAVIOR=true` did anything. So a cut that named the
|
|
188
193
|
// flag measured the LOOSE answer and reported the strict one — the exact
|
|
189
194
|
// failure shape the runbook warns about, in the tool that measures it.
|
|
195
|
+
case '--verify':
|
|
196
|
+
verifyPath = nextValue();
|
|
197
|
+
break;
|
|
198
|
+
case '--host-key':
|
|
199
|
+
verifyHostKeyPath = nextValue();
|
|
200
|
+
break;
|
|
190
201
|
case '--require-behavior':
|
|
191
202
|
process.env['OPENWOP_REQUIRE_BEHAVIOR'] = 'true';
|
|
192
203
|
break;
|
|
@@ -223,6 +234,8 @@ function parseArgs(argv: readonly string[]): ParsedArgs {
|
|
|
223
234
|
verifierKeyPath,
|
|
224
235
|
verifierKeyId,
|
|
225
236
|
maxWorkers,
|
|
237
|
+
verifyPath,
|
|
238
|
+
verifyHostKeyPath,
|
|
226
239
|
};
|
|
227
240
|
}
|
|
228
241
|
|
|
@@ -283,6 +296,17 @@ Certification (RFC 0089):
|
|
|
283
296
|
to <out.json>. Requires --base-url (and --api-key as usual).
|
|
284
297
|
|
|
285
298
|
Other:
|
|
299
|
+
--verify <bundle> Audit a certification bundle WITHOUT a host. Exit codes:
|
|
300
|
+
0 verified — no rejections, host signature checked,
|
|
301
|
+
certified list re-derived from the bundle's own
|
|
302
|
+
discovery document
|
|
303
|
+
1 rejected — one or more problems, each printed
|
|
304
|
+
2 coherent but NOT independently verified (no
|
|
305
|
+
--host-key, or the certified list is the emitter's
|
|
306
|
+
word). Do not quote this as "verified"
|
|
307
|
+
3 the file is missing or is not a bundle
|
|
308
|
+
--host-key <pem> The key --verify checks the host signature under. Without
|
|
309
|
+
it the signature is unverified and --verify exits 2.
|
|
286
310
|
--require-behavior Strict mode: an ADVERTISED behaviour that cannot be
|
|
287
311
|
observed FAILS instead of soft-skipping (RFC 0148 §B).
|
|
288
312
|
Equivalent to OPENWOP_REQUIRE_BEHAVIOR=true.
|
|
@@ -861,6 +885,74 @@ async function main(): Promise<never> {
|
|
|
861
885
|
process.exit(0);
|
|
862
886
|
}
|
|
863
887
|
|
|
888
|
+
// `--verify <bundle.json>` — audit a bundle WITHOUT a host and without cloning
|
|
889
|
+
// the corpus. `verifyBundleV3` was exported but only ever called as the
|
|
890
|
+
// emitter's self-check, so auditing a third party's certification claim meant
|
|
891
|
+
// writing a driver script — which means in practice nobody did, and a
|
|
892
|
+
// certification program whose verifier ships only as a library function is
|
|
893
|
+
// asking to be taken on trust.
|
|
894
|
+
//
|
|
895
|
+
// The exit contract has FOUR values on purpose. "Structurally coherent" and
|
|
896
|
+
// "verified" are different facts, and collapsing them is how the vacuous pass
|
|
897
|
+
// gets rebuilt: without exit 2, every CI that runs `--verify` with no key
|
|
898
|
+
// prints green and reports "verified".
|
|
899
|
+
if (args.verifyPath !== undefined) {
|
|
900
|
+
if (!existsSync(args.verifyPath)) {
|
|
901
|
+
process.stderr.write(`openwop-conformance --verify: no such file: ${args.verifyPath}\n`);
|
|
902
|
+
process.exit(3);
|
|
903
|
+
}
|
|
904
|
+
let bundle: BundleV3;
|
|
905
|
+
try {
|
|
906
|
+
bundle = JSON.parse(readFileSync(args.verifyPath, 'utf8')) as BundleV3;
|
|
907
|
+
} catch (e) {
|
|
908
|
+
process.stderr.write(`openwop-conformance --verify: ${args.verifyPath} is not readable JSON — ${(e as Error).message}\n`);
|
|
909
|
+
process.exit(3);
|
|
910
|
+
}
|
|
911
|
+
const hostKey = args.verifyHostKeyPath !== undefined ? readFileSync(args.verifyHostKeyPath, 'utf8') : undefined;
|
|
912
|
+
const verifierKey = args.verifierKeyPath !== undefined ? readFileSync(args.verifierKeyPath, 'utf8') : undefined;
|
|
913
|
+
const verdict = verifyBundleV3(bundle, {
|
|
914
|
+
...(hostKey !== undefined ? { hostPublicKeyPem: hostKey } : {}),
|
|
915
|
+
...(verifierKey !== undefined ? { verifierPublicKeyPem: verifierKey } : {}),
|
|
916
|
+
});
|
|
917
|
+
const t = bundle.results?.totals ?? {};
|
|
918
|
+
const out: string[] = [
|
|
919
|
+
`bundle: ${args.verifyPath}`,
|
|
920
|
+
`host: ${bundle.host?.name ?? '?'} build ${bundle.host?.build?.kind ?? '?'}:${String(bundle.host?.build?.id ?? '?').slice(0, 12)}`,
|
|
921
|
+
`suite: ${bundle.suite?.version ?? '?'} (this CLI is ${suiteVersion()})`,
|
|
922
|
+
`totals: executedPass=${t.executedPass ?? '?'} executedFail=${t.executedFail ?? '?'} blocked=${t.blocked ?? '?'} inapplicable=${t.inapplicable ?? '?'} skipped=${t.skipped ?? '?'}`,
|
|
923
|
+
`certified: ${verdict.certifiedProfiles.length > 0 ? verdict.certifiedProfiles.join(', ') : '(none)'}`,
|
|
924
|
+
'',
|
|
925
|
+
'What this command does NOT do:',
|
|
926
|
+
' · It does not re-run anything. A host that measured itself wrongly, and signed',
|
|
927
|
+
' the result, verifies clean here. This audits an attestation, not a host.',
|
|
928
|
+
' · It does not resolve keyId against the live host. It checks the signature under',
|
|
929
|
+
' the key YOU supply with --host-key; it cannot tell you that key is the host\u2019s.',
|
|
930
|
+
' · It does not judge suite currency. The suite version is reported above as data:',
|
|
931
|
+
' an older bundle measured less, and whose fact that is belongs to its emitter.',
|
|
932
|
+
'',
|
|
933
|
+
];
|
|
934
|
+
if (verdict.rejections.length > 0) {
|
|
935
|
+
out.push(`REJECTED \u2014 ${verdict.rejections.length} problem(s):`);
|
|
936
|
+
for (const r of verdict.rejections) out.push(` [${r.kind}]${r.profile ? ` (${r.profile})` : ''} ${r.detail}`);
|
|
937
|
+
process.stdout.write(out.join('\n') + '\n');
|
|
938
|
+
process.exit(1);
|
|
939
|
+
}
|
|
940
|
+
const gaps: string[] = [];
|
|
941
|
+
if (!verdict.signatureVerified) gaps.push('the host signature was NOT verified (no --host-key supplied)');
|
|
942
|
+
if (!verdict.derivabilityChecked) gaps.push('the certified list was NOT re-derived from the bundle\u2019s discovery document \u2014 it is the emitter\u2019s word');
|
|
943
|
+
if ((bundle.claimedProfiles ?? []).some((p) => p.evidenceTier === 'independent') && !verdict.verifierSignatureVerified) gaps.push('an `independent` tier is claimed but the verifier signature was NOT verified (no --verifier-key)');
|
|
944
|
+
if (gaps.length > 0) {
|
|
945
|
+
out.push('COHERENT, NOT INDEPENDENTLY VERIFIED \u2014 zero rejections, but:');
|
|
946
|
+
for (const g of gaps) out.push(` \u00b7 ${g}`);
|
|
947
|
+
out.push('', 'Do not quote this as "verified". Exit 2 exists to keep that distinction.');
|
|
948
|
+
process.stdout.write(out.join('\n') + '\n');
|
|
949
|
+
process.exit(2);
|
|
950
|
+
}
|
|
951
|
+
out.push('VERIFIED \u2014 zero rejections, host signature checked, certified list re-derived.');
|
|
952
|
+
process.stdout.write(out.join('\n') + '\n');
|
|
953
|
+
process.exit(0);
|
|
954
|
+
}
|
|
955
|
+
|
|
864
956
|
// Env vars OVERRIDE flags only when the flag was unset (consistent
|
|
865
957
|
// with the rest of the harness — env wins on the absence of CLI input).
|
|
866
958
|
const env: NodeJS.ProcessEnv = { ...process.env };
|