@openwop/openwop-conformance 2.42.7 → 2.42.9

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.
@@ -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.42.7",
5
- "corpusTag": "v2.42.7",
4
+ "version": "2.42.9",
5
+ "corpusTag": "v2.42.9",
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": "69464bc0e1e71ef3b543a73d4fa4f67202343d2603c80af8033f6c3619ef1b30",
11
11
  "api/redocly.yaml": "b0604c89b2ca6d5076ec25725c539dad44a741a811fe524439ee6daef8baa09f",
12
- "api/seams-v2.yaml": "32ff503cc953a0d824789241295f08504db7d8a167bd2a2d9f489bcb7cb015e8",
13
- "api/v2/asyncapi.yaml": "a7a442826ad9641640b2cc6b9ce1de9beef93b9290f19cd17cf2254ea94c932b",
14
- "api/v2/openapi.yaml": "4d631c452aeec8eea96c03ea85f14fc56ea73ca476433bf98cc9c4bf0984e820",
12
+ "api/seams-v2.yaml": "91a84ef4a8d38824e528ee7f59b29df34c167f2e5e695f200ad19b0fd4b47cdd",
13
+ "api/v2/asyncapi.yaml": "b93593055cf033fdf37b8a18fd36a2045b989a97a97b49adda23efb6ff1654e6",
14
+ "api/v2/openapi.yaml": "d698d93dd3ef46c060bcdb68f8950aa983ef8904ad18cf14b87ce96304ae59ed",
15
15
  "api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
16
16
  "schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
17
17
  "schemas/a2a-task-state.schema.json": "75d5049dea8bd873ff0e7546f1c60c8d36c219bec7084264360be54a8be30ae1",
@@ -116,7 +116,7 @@
116
116
  "schemas/v2/artifact.schema.json": "85b9dc284d39b99d2a66fa8cbf47f2dd3d8526d3bd6cc411ad315b372de38975",
117
117
  "schemas/v2/audit-verify-result.schema.json": "2a4bfc42976717f78c10468fdba756ca6b19e49762699e6dffd418ffff08e55d",
118
118
  "schemas/v2/budget-policy.schema.json": "c7449daeb6e1d95e7a047b8c2814d3d54748697c27046972834874867d9b5f4c",
119
- "schemas/v2/capabilities.schema.json": "65bd0b18b4c00708bc582af9fb4399fd3009c4d452401c6c8ad26695b00b7235",
119
+ "schemas/v2/capabilities.schema.json": "959b7a1b8e40aeafb8b9702bd0e4b220c0beb02a6afa3ca64fe9b04b95ae3df5",
120
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",
@@ -142,7 +142,7 @@
142
142
  "schemas/v2/envelopes/schema.request.schema.json": "be2c5f7aa881cdd252ccff47ac02193659e1393633a1541f4297383205d384a2",
143
143
  "schemas/v2/envelopes/schema.response.schema.json": "d049a081267c06c28879c331b147f3ed821b14ccfd93005c67aff64db0a8c68e",
144
144
  "schemas/v2/envelopes/ui.a2ui-surface.schema.json": "e92a2548ba65454c9e0a34f72ea6768d18f59e61fa5da7668d0b2d5baddaddc8",
145
- "schemas/v2/error-envelope.schema.json": "b812a33626efd68d492269ff4a713e5a91a59d7071eb2470db99757f260e5d47",
145
+ "schemas/v2/error-envelope.schema.json": "467d7109c54bff71a6cfd457d3bd65a23ca34b8e5577e742d080692771a78ebf",
146
146
  "schemas/v2/eval-summary.schema.json": "2f384daf05b8f3b4f604688cae504d6d9971ba41b8c771e55fe9a17e09e86b7a",
147
147
  "schemas/v2/export-bundle.schema.json": "bd28905a7729437ab7f2ad8bb33e13a371b2bf5605a7eb2962a0d246de312d69",
148
148
  "schemas/v2/form-content-pack-manifest.schema.json": "a1942a124e250a6e9fbdb6b811c5a125e78fbcbcaa46dfc71d0636f24710a339",
@@ -202,51 +202,53 @@
202
202
  "schemas/workload-identity.schema.json": "64ed93e12cc9f0657e56ff9b8f045e5d68d397df5276d103c07f42c7f34f0579",
203
203
  "schemas/workspace-file-create.schema.json": "70a23b92c2958c0165d7d29aea9020a045dc7816af82c80c5f50d0aac19c4856",
204
204
  "schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
205
- "spec/v1/alias-detectors.json": "2401fcb1c18cdd688c018b3d716ae6bca85c5e356220ee2b793bd9d81872412d",
205
+ "spec/v1/alias-detectors.json": "29f21e0fb2c7a232a92a5c19b4cba3a86bc3b5e16f06780452f0421c66c6ec38",
206
206
  "spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
207
- "spec/v1/core-standard-manifest.json": "5c8757f4745d701dd79b34c63e4ba5ef9623292cc8ae7c661f2976f4a5b142a1",
208
- "spec/v1/deprecations.json": "2d03f4729810280147ea08c630ea65434f2dc5370567d0aac41c3595f8337d40",
207
+ "spec/v1/core-standard-manifest.json": "717b478af97d11a4c7844fceecd536a70783ca1cbb7079ec1434e710a24fc6e5",
208
+ "spec/v1/deprecations.json": "46df807fb4adf0aae24671c20f69975cf3496e3bc3a521cb7e6ea556bdce563c",
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": "456f3453ce453f0149f3d95c1ac26e669ae7d2bf2541a26320860105c4d55566",
213
+ "spec/v1/gaps.json": "2b032d91d614a758db347be4f5e617b8ac3c4622b6f95ffcfa928276c0fb7377",
214
214
  "spec/v1/gaps.schema.json": "8fd83259f556553c9df0f53e7a82ca8c2a4771d471197f69b4e2ae0ceeacfd66",
215
- "spec/v1/migrations.json": "8839497d6a4830b53fa7867014a8336f0d725ef2346352e54fb580b7947897af",
215
+ "spec/v1/migrations.json": "2efcd9064b2bc65a84a3b3d281aa86a91efd002690c1f4f14e8335c4a18194d1",
216
216
  "spec/v1/migrations.schema.json": "886779aa6c22e646db097f5df210adb018a4dd14a7b815465a18c8a7056c8f72",
217
217
  "spec/v1/operation-path-manifest.json": "fada0342815fb129c1ae61f1829bd6e34417717408ce86fcc54280f373c19e74",
218
218
  "spec/v1/spec-gaps.json": "ea9612af55bea9dae3e82223d4f714243f8f590b0193f1188257b13491a38afc",
219
- "spec/v2/README.md": "4cb63710360e9322a1c7c04aacbad5ce890f79c2da24e7da5ac3bf1fc90bebd7",
219
+ "spec/v2/README.md": "4fa067d377580495e60baf08a4eafc3214b8ca48d8fb270bf91fc5066fad7186",
220
220
  "spec/v2/core/capabilities.md": "225269a257421ebe677070dd13b05f6f297a2a343916e440b7428b839e2da9a4",
221
221
  "spec/v2/core/conformance.md": "e7bd7d54062f28801071165a1b92edbeee3774bb3710f31eed75817139b66d95",
222
222
  "spec/v2/core/connection-packs.md": "3168d2c8f1a59417a7256a678f4203570be434f64dead6b11fdec86b352686df",
223
223
  "spec/v2/core/conversation.md": "6db43d8160bc45d3944aa1151816a6f0fd1669d59d7f822267a2d6cc9db22990",
224
- "spec/v2/core/errors.md": "7ba57f0922673038a5f8479b47da88810bf15145ed14a0cd1ef43214e8eb27b5",
224
+ "spec/v2/core/errors.md": "d14649ee60f8260ec163464163409ad7f50cb86fee7b846e0190f66070b3774b",
225
225
  "spec/v2/core/events.md": "234338deb97be648731e8ab2126a90b7b8ad0a7070d98ffc85ee4689ff059791",
226
- "spec/v2/core/form-content-packs.md": "347b1c649864360bf2731e6e047a3e26b0783530201185827e85359e90f0b233",
227
- "spec/v2/core/headers.md": "89839a4e1ad793e565afe672c8e003ab3852ab7be89145a2510860ac5ca4de5c",
226
+ "spec/v2/core/form-content-packs.md": "41d40afe0a3760bcd792a4c406e16f28b0ab0aa7b0925323110d3fa2730d5400",
227
+ "spec/v2/core/headers.md": "5cb1624fa63acc81992b6a6d842ec392cf7271aaf2f65d1f83c8c4fa286e6730",
228
228
  "spec/v2/core/host-services.md": "15923cceb95cba148a32e0944f6a2d1d25e9cc36b0c169b67ee42d483bcac800",
229
+ "spec/v2/core/i18n.md": "846c6d2878fc8454c2ae0a1576f6b6e6c8958d47187a6c96cc5826ca3c3e2e91",
229
230
  "spec/v2/core/idempotency.md": "21830b7d23dd03e9e8ba4b0de253dd001fc960f166310d6b6b60b1c33cf60b30",
230
- "spec/v2/core/identity.md": "fb6606a322a9167b8ab91402cf7214b6ced3e439eb4b6f9240a0739d9a3e58f1",
231
+ "spec/v2/core/identity.md": "acb7f739729c69a1f40dcaa5b1d873ba9f32993c17bfd297f5fa56351cf47c69",
231
232
  "spec/v2/core/interop.md": "3cfa2af70ae6ba1f4b4268447d46d1d2f5fb65f7917f15bad159d9779d25b1a6",
232
233
  "spec/v2/core/interrupt.md": "b31b3f3be7888043ef38c82367a8d743a775c2b6bd499f7da44355ff00452435",
233
234
  "spec/v2/core/node-pack-runtimes.md": "d52966319c53aaa03575025915b73b470a42832c039c41d73aff6931758630cb",
234
235
  "spec/v2/core/oauth.md": "57f167d899993b6959c243e4102819f4014b365299b9b2b253decd2781e6b8b7",
235
- "spec/v2/core/overview.md": "bb6b088d5ffd3ea3f336bba37ef2ca45e7b0bee2c7f25ecb2d78d3f25a3fcee1",
236
+ "spec/v2/core/overview.md": "3dbc8c0cf7474b31816b1a12412c1edf5597066c38edcdbca4dd2e465880f65b",
236
237
  "spec/v2/core/packs.md": "26b0fc3b81f98adbdf70796482db89fa81a6ce3da6f830cd5e8b3d71541c497f",
237
238
  "spec/v2/core/persistence.md": "bba672ed3bf6d83af6e4e65c0f4f480fc21fe12207811e321306af2108d9010d",
239
+ "spec/v2/core/portability.md": "a35fbe66b8b5ccf63cf0216157d4b61d4c47f6dc8e757c830569ddf244e21e8a",
238
240
  "spec/v2/core/replay.md": "a5be4e05242f25f8b5a8a4fb31bd359d5abb4bf34fa8e536403a8ee5779ab876",
239
- "spec/v2/core/runs.md": "26b459d522901ccf752470c6c9c902af435d7e9690755438ddb24c150424341d",
241
+ "spec/v2/core/runs.md": "72a94c27d884036d84290e9b108702e98b0c6b2fb598213bbda44fba9c9f860d",
240
242
  "spec/v2/core/security-defaults.md": "179fdd2efcc1454453243f8ab6473335dfd4389e70cbbc2669f5b83714092973",
241
243
  "spec/v2/core/tool-catalog.md": "6b54b8ce221107de19c718f374a720123c6a5f55db4054e6625f73d06e0a442f",
242
- "spec/v2/core/versioning.md": "000fa2db433039fc4f2b0ad7dc2cbf7aadc10eae60322018727da6f99eda4dc3",
243
- "spec/v2/core/webhooks.md": "0398db27b2ddd97bc2a9a9b1236b5139451765462789dee9c3a8cc6b7b81e666",
244
+ "spec/v2/core/versioning.md": "2bfe16a72a909c46b4585fab39dc9e94c97506ed95df6b1b527f97b4cd3684ef",
245
+ "spec/v2/core/webhooks.md": "78fe97ab5bdee20218183efe32abf5ba1996705d757cb37c8d6a4d808c44199f",
244
246
  "spec/v2/core/workflow-chain-packs.md": "35a957f1dd22016febd76bf2c4dfb79cda58b95d8efd8893b5e5babda9f79cf3",
245
247
  "spec/v2/corrections.json": "b19f8a80fce7e6473012a20a216e358dd38ae7411e6a348f8031e325ed1a0a01",
246
248
  "spec/v2/corrections.schema.json": "46bbffb954d13bd9995aebc912ceeb2544f7064331abf25fd6ab85c81a5cec94",
247
- "spec/v2/declaration.json": "6ed50f204eaac5885188237378feb0f555c4290455861cad133141b8d1296eff",
249
+ "spec/v2/declaration.json": "1253a5590ec2d9ba511d15e8a9d9da5e6926c4daa981a26b73b29bc915477013",
248
250
  "spec/v2/declaration.schema.json": "ef951633e4070899f64828f5cf1b5f4870270a0360f81dbb49b5b4d6cd407550",
249
- "spec/v2/errors.json": "034d5c77088c594e104de783ce7ca55c8174923b4fd177593da231f3bfcacaaa",
251
+ "spec/v2/errors.json": "b89664e9e0e7603253712263ce0843ce607e276c67d0acaf4a680c39bb2bd682",
250
252
  "spec/v2/errors.schema.json": "47404ad53e6147cf9017b813483712466573e419fb33e6c4b6c12d3e7deba1bc",
251
253
  "spec/v2/event-codemap.json": "37869a03cdfd8758b57e6f350641a6ebbd383231cebda51f5473ad85f94ab74c",
252
254
  "spec/v2/event-codemap.schema.json": "b173f2a9bcc0bf9b62a474ce3b91e1431d597e64fd1560fc58662cd8606eaf9c",
@@ -263,7 +265,7 @@
263
265
  "spec/v2/ext/knowledge/README.md": "67be1dc97e5cdc9527d56f4836dc96841548a9c2f8f964777825d56fdf9f0e53",
264
266
  "spec/v2/ext/launchStudio/README.md": "c732bb855252c31ee1e971cbc2936f2ca29b234c111d548c648095634507babb",
265
267
  "spec/v2/ext/messaging/README.md": "32c222c0bce515e75b62a5a44b9f09d0e23c1b40f424e1ab8309deefd296c4b6",
266
- "spec/v2/ext/portability/README.md": "0df93357a90ca6317f16a2bdd45b5073965a3d52a020daf900a74f5fcd709bf8",
268
+ "spec/v2/ext/portability/README.md": "c59423ee446638b748a911b9e97e803c4786157be6120f656f0a060586bc7b82",
267
269
  "spec/v2/ext/provider-idempotency/README.md": "9d1bccef0ec16a19a3370ae79c2f5a1726ce0d1d25a51352907f288a2ccb15d7",
268
270
  "spec/v2/ext/provider-idempotency/registry.json": "5b5fecad604fb4ab38da39f5abe697a4e57b05406c398f8afeb377331b2bd965",
269
271
  "spec/v2/ext/restTransport/README.md": "d485417310616b2f1da97106d970293f3f5712f6cadb6b840f5ac84ee28bdda6",
@@ -295,9 +297,9 @@
295
297
  "spec/v2/path-manifest.json": "17298a245c5da9a70aa7ab0b7f2db4277e06209d6fcdd7187a8b4cea179c08f3",
296
298
  "spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
297
299
  "spec/v2/profiles.json": "180beb9ef2d161766fa23b2987e7e829851e3d316273147cf6a4a3f324befd6b",
298
- "spec/v2/release.json": "0b9bc03ec7efd4cfacfe6ca72b57655787ee9cc5473e9a9b7af21bb1f25350a7",
300
+ "spec/v2/release.json": "1ac31e3dd517036b357bfa5743b760e21d28b8cda7f8550e65d8444e964751c6",
299
301
  "spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624",
300
- "spec/v2/surface-baseline.json": "549832c2de97b7c7aed07934c1c73c2709ac226dc0cb71fc8c141010bc4928f1"
302
+ "spec/v2/surface-baseline.json": "5cab3b39eaeb344361ac7d9038d15be5cf6a5620f1912fdd0c184cef95bae857"
301
303
  },
302
- "corpusCommit": "f6209c3b2095005b844b44d5046873e4170d9736"
304
+ "corpusCommit": "67e27a37bbd22f24ca2b6a2bce8f0a0f0971e3c8"
303
305
  }
package/src/cli.ts CHANGED
@@ -104,9 +104,8 @@ function parseArgs(argv: readonly string[]): ParsedArgs {
104
104
  let impl: string | undefined;
105
105
  let implVersion: string | undefined;
106
106
  let certify: string | undefined;
107
- // Suite 1.152.0: bundle v2 (RFC 0148) is the default. v1 stays reachable via
108
- // `--bundle-version 1` through the RFC 0148 migration window (ends
109
- // 2026-11-10) and is removed at v2.0 (spec/v1/deprecations.json).
107
+ // Suite 2.0.0-rc.1: bundle v3 (RFC 0168 §E) is the default. v2 stays reachable
108
+ // via `--bundle-version 2`, deprecated (RFC 0168 §E.3); v1 is gone.
110
109
  let bundleVersion: '2' | '3' = '3';
111
110
  let hostBuild: ParsedArgs['hostBuild'];
112
111
  let evidenceTier: ParsedArgs['evidenceTier'] = 'self';
@@ -294,7 +293,7 @@ Certification (RFC 0089):
294
293
  --signing-key <pem> v3: Ed25519 private key (PKCS8 PEM) that signs the bundle, or OPENWOP_BUNDLE_SIGNING_KEY.
295
294
  --signing-key-id <id> v3: the keyId the host publishes for that key, or OPENWOP_BUNDLE_SIGNING_KEY_ID.
296
295
  --evidence-tier <t> v3: self | steward | independent (independent needs --verifier-key/--verifier-key-id).
297
- --bundle-version <2|3> Certification bundle format. Default 2 (corrected 2026-09-03, RFC 0168 §E.4; the text said 1 while the code set 2). Version 2 (RFC 0148
296
+ --bundle-version <2|3> Certification bundle format. Default 3 (since suite 2.0.0-rc.1; 2 is DEPRECATED, RFC 0168 §E.3). Version 2 (RFC 0148
298
297
  §C) records per-requirement DISPOSITIONS instead of pass/fail/skip
299
298
  file lists, so "we could not check" stops being indistinguishable
300
299
  from "checked and it holds". See the note it prints.
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Per-attempt dispatch budgets — `GET /v1/host/sample/test/mock-ai/dispatch-budgets`
3
+ * (host-sample-test-seams.md §28; v2 `api/seams-v2.yaml` `getMockDispatchBudgets`).
4
+ *
5
+ * The companion `last-dispatch-budget` seam reports only the most recent call's
6
+ * `maxTokens`, so a retry's budget cannot be compared with the attempt before
7
+ * it. This seam returns every call's budget in call order. OPTIONAL: an
8
+ * unserved seam (404/405/501) is `unserved`, so the caller falls back to its
9
+ * pre-seam partial witness; a served seam that answers anything else is
10
+ * `broken`, which the caller records as `blocked`.
11
+ */
12
+
13
+ import { driver } from './driver.js';
14
+
15
+ export type DispatchBudgets =
16
+ | { ok: true; attempts: Array<number | null> }
17
+ | { ok: false; unserved: true; reason: string }
18
+ | { ok: false; unserved: false; reason: string };
19
+
20
+ const UNSERVED = new Set([404, 405, 501]);
21
+
22
+ export async function readDispatchBudgets(nodeId: string): Promise<DispatchBudgets> {
23
+ const res = await driver.get(`/v1/host/sample/test/mock-ai/dispatch-budgets?nodeId=${encodeURIComponent(nodeId)}`);
24
+ if (UNSERVED.has(res.status)) {
25
+ return { ok: false, unserved: true, reason: `the dispatch-budgets seam is not served (${res.status})` };
26
+ }
27
+ const attempts = (res.json as { attempts?: unknown } | undefined)?.attempts;
28
+ if (res.status !== 200 || !Array.isArray(attempts)) {
29
+ return { ok: false, unserved: false, reason: `the dispatch-budgets seam answered ${res.status} without an attempts[] array (host-sample-test-seams.md §28)` };
30
+ }
31
+ const out: Array<number | null> = [];
32
+ for (const a of attempts) {
33
+ const m = (a as { maxTokens?: unknown } | null)?.maxTokens;
34
+ if (m !== null && !(typeof m === 'number' && Number.isInteger(m))) {
35
+ return { ok: false, unserved: false, reason: `the dispatch-budgets seam returned an entry whose maxTokens is ${JSON.stringify(m)}, not an integer or null (host-sample-test-seams.md §28)` };
36
+ }
37
+ out.push(m);
38
+ }
39
+ return { ok: true, attempts: out };
40
+ }
41
+
42
+ /**
43
+ * RFC 0033 §B: a host MAY retry a truncated emission, and the new output budget
44
+ * SHOULD be greater than the previous one. A SHOULD is recorded, not failed: the
45
+ * caller asserts only on `met`, and records `unmet` as `inapplicable` with the
46
+ * observed budgets. The comparison is between the attempts the provider actually
47
+ * received (the dispatch-budgets seam), never against the fixture's configured value.
48
+ */
49
+ export type TruncationBudgetVerdict =
50
+ | { kind: 'met'; first: number; retry: number }
51
+ | { kind: 'unmet'; first: number; retry: number }
52
+ | { kind: 'skip'; skip: 'inapplicable' | 'blocked'; reason: string };
53
+
54
+ export async function truncationBudgetVerdict(nodeId: string): Promise<TruncationBudgetVerdict> {
55
+ const budgets = await readDispatchBudgets(nodeId);
56
+ if (!budgets.ok && budgets.unserved) {
57
+ return { kind: 'skip', skip: 'inapplicable', reason: `partial witness — the §B budget SHOULD is not observed: ${budgets.reason}, so the retry's budget cannot be compared with the first attempt's` };
58
+ }
59
+ if (!budgets.ok) return { kind: 'skip', skip: 'blocked', reason: budgets.reason };
60
+ const [first, retry] = budgets.attempts;
61
+ if (budgets.attempts.length < 2) return { kind: 'skip', skip: 'inapplicable', reason: `the host made no truncation retry (RFC 0033 §B: MAY retry); per-attempt budgets ${JSON.stringify(budgets.attempts)}` };
62
+ if (typeof first !== 'number' || typeof retry !== 'number') {
63
+ return { kind: 'skip', skip: 'inapplicable', reason: `partial witness — a call carried no output budget (per-attempt budgets ${JSON.stringify(budgets.attempts)}), so the §B SHOULD cannot be compared` };
64
+ }
65
+ return retry > first ? { kind: 'met', first, retry } : { kind: 'unmet', first, retry };
66
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Cross-file isolation for the mock-AI program seam.
3
+ *
4
+ * `POST /v1/host/sample/test/mock-ai/program` is keyed by `nodeId` alone
5
+ * (host-sample-test-seams.md §5), and a node id belongs to a fixture. When two
6
+ * scenario files drive the same fixture, vitest runs them in parallel workers
7
+ * by default and each can overwrite the other's program between seeding it and
8
+ * reading the run: a flaky result that says nothing about the host.
9
+ *
10
+ * `holdMockNodes(...ids)` holds a lock per node id for the whole file, taken in
11
+ * `beforeAll` and released in `afterAll`, so files sharing a node run one after
12
+ * another. The lock is an atomic `mkdir` in the OS temp dir, keyed by the target
13
+ * base URL and the node id, so it is shared by every worker process on the
14
+ * machine and separate hosts never contend. Ids are taken in sorted order (no
15
+ * deadlock between files holding overlapping sets); a lock older than
16
+ * STALE_MS is presumed left by a killed run and is broken.
17
+ */
18
+
19
+ import { beforeAll, afterAll } from 'vitest';
20
+ import { mkdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
21
+ import { createHash } from 'node:crypto';
22
+ import { tmpdir } from 'node:os';
23
+ import { join } from 'node:path';
24
+
25
+ const POLL_MS = 200;
26
+ const WAIT_MS = 300_000;
27
+ const STALE_MS = 600_000;
28
+
29
+ function lockDir(nodeId: string): string {
30
+ const key = createHash('sha256').update(`${process.env.OPENWOP_BASE_URL ?? ''}\u0000${nodeId}`).digest('hex').slice(0, 24);
31
+ return join(tmpdir(), `openwop-mock-node-${key}`);
32
+ }
33
+
34
+ async function acquire(dir: string, nodeId: string): Promise<void> {
35
+ const deadline = Date.now() + WAIT_MS;
36
+ for (;;) {
37
+ try {
38
+ mkdirSync(dir);
39
+ writeFileSync(join(dir, 'owner'), `${process.pid} ${nodeId} ${new Date().toISOString()}\n`);
40
+ return;
41
+ } catch (e) {
42
+ if ((e as NodeJS.ErrnoException).code !== 'EEXIST') throw e;
43
+ try {
44
+ if (Date.now() - statSync(dir).mtimeMs > STALE_MS) { rmSync(dir, { recursive: true, force: true }); continue; }
45
+ } catch { continue; }
46
+ if (Date.now() > deadline) throw new Error(`mock-node lock for ${nodeId} not acquired within ${WAIT_MS / 1000}s (${dir}) — another scenario file still holds it`);
47
+ await new Promise((r) => setTimeout(r, POLL_MS));
48
+ }
49
+ }
50
+ }
51
+
52
+ /** Hold the mock-AI program slot for these node ids for the duration of the calling file. */
53
+ export function holdMockNodes(...nodeIds: string[]): void {
54
+ const held: string[] = [];
55
+ beforeAll(async () => {
56
+ if (!process.env.OPENWOP_BASE_URL) return; // server-free run: nothing to isolate
57
+ for (const id of [...new Set(nodeIds)].sort()) {
58
+ const dir = lockDir(id);
59
+ await acquire(dir, id);
60
+ held.push(dir);
61
+ }
62
+ }, WAIT_MS + 10_000);
63
+ afterAll(() => {
64
+ for (const dir of held.splice(0).reverse()) rmSync(dir, { recursive: true, force: true });
65
+ });
66
+ }
@@ -27,23 +27,14 @@ import { join } from 'node:path';
27
27
  import { SCHEMAS_DIR } from '../lib/paths.js';
28
28
 
29
29
  /**
30
- * audit-verify-result.schema.json with every `additionalProperties: false`
31
- * relaxed: the REQUIRED members and their types are enforced, extra members
32
- * are not (the reference postgres host emits `checkpointsValid` and a
33
- * per-checkpoint `verified` bit; convicting extras is a separate decision).
30
+ * audit-verify-result.schema.json, CLOSED: every `additionalProperties: false`
31
+ * is enforced. (Until 2026-09-27 the suite stripped them because the reference
32
+ * postgres/sqlite hosts emitted a per-checkpoint `verified` bit the schema does
33
+ * not name; the hosts now keep that bit host-internal. The aggregate verdict is
34
+ * the schema's OPTIONAL `checkpointsValid`.)
34
35
  */
35
- function relaxAdditional(node: unknown): unknown {
36
- if (Array.isArray(node)) return node.map(relaxAdditional);
37
- if (node === null || typeof node !== 'object') return node;
38
- const out: Record<string, unknown> = {};
39
- for (const [k, v] of Object.entries(node as Record<string, unknown>)) {
40
- if (k === 'additionalProperties' && v === false) continue;
41
- out[k] = relaxAdditional(v);
42
- }
43
- return out;
44
- }
45
- const VERIFY_RESULT_SCHEMA = relaxAdditional(
46
- JSON.parse(readFileSync(join(SCHEMAS_DIR, 'audit-verify-result.schema.json'), 'utf8')),
36
+ const VERIFY_RESULT_SCHEMA = JSON.parse(
37
+ readFileSync(join(SCHEMAS_DIR, 'audit-verify-result.schema.json'), 'utf8'),
47
38
  ) as Record<string, unknown>;
48
39
 
49
40
  interface AuditIntegrityCaps {
@@ -33,7 +33,12 @@ import { pollUntilTerminal } from '../lib/polling.js';
33
33
  import { isFixtureAdvertised } from '../lib/fixtures.js';
34
34
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
35
35
  import { req } from '../lib/requirement-ids.js';
36
- import { softSkip } from '../lib/soft-skip.js';
36
+ import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
37
+ import { readDispatchBudgets, truncationBudgetVerdict } from '../lib/mock-ai-budgets.js';
38
+ import { holdMockNodes } from '../lib/mock-node-lock.js';
39
+
40
+ // The mock-AI program seam is keyed by node id; files sharing a fixture's node run one at a time.
41
+ holdMockNodes('truncated-structured-call', 'retry-attempted-structured-call');
37
42
 
38
43
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
39
44
  /**
@@ -158,16 +163,21 @@ describe.skipIf(HTTP_SKIP)('envelope-completion-distinguishes-truncation: trunca
158
163
  ]);
159
164
  if (seed.status === 404) return softSkip('blocked', 'precondition not met — `seed.status === 404` returned early (seam, prior step, or fixture unavailable)');
160
165
 
161
- await startRunAndRead(TRUNCATED_FIXTURE);
162
- const budget = await lastBudget(TRUNCATED_FIXTURE);
163
- if (budget === null) return softSkip('blocked', 'precondition not met — `budget === null` returned early (seam, prior step, or fixture unavailable)');
166
+ const run = await startRunAndRead(TRUNCATED_FIXTURE);
167
+ if (run === null) return softSkip('blocked', 'precondition not met — the run create or event-log read failed');
168
+ // RFC 0033 §B: MAY retry, and the retry budget SHOULD exceed the first
169
+ // attempt's. Compared through the dispatch-budgets seam; an unmet SHOULD is
170
+ // recorded, not failed (suite 2.42.9; this leg asserted `> 50` as a MUST).
171
+ const v = await truncationBudgetVerdict(NODE_OF[TRUNCATED_FIXTURE] as string);
172
+ if (v.kind === 'skip') return softSkip(v.skip, v.reason);
173
+ if (v.kind === 'unmet') return softSkip('inapplicable', `RFC 0033 §B SHOULD not met — the retry carried maxTokens ${v.retry} after the first attempt carried ${v.first}; recorded, not failed`);
164
174
  expect(
165
- budget,
166
- req('openwop.it.envelope-completion-distinguishes-truncation.truncation-retry-budget-strictly-greater-than-initial-rfc-0033-b-truncationbudge',
175
+ v.retry,
176
+ req('openwop.it.envelope-completion-distinguishes-truncation.truncation-retry-budget-strictly-greater-than-initial-rfc-0033-b-truncationbudge',
167
177
  'RFCS/0033-envelope-completion-contract.md §B',
168
- 'truncation retry MUST multiply maxTokens by truncationBudgetMultiplier — final budget > initial 50 fixture value',
178
+ `the truncation retry's budget SHOULD exceed the first attempt's (first ${v.first}, retry ${v.retry})`,
169
179
  ),
170
- ).toBeGreaterThan(50);
180
+ ).toBeGreaterThan(v.first);
171
181
  });
172
182
  });
173
183
 
@@ -210,26 +220,61 @@ describe.skipIf(HTTP_SKIP)('envelope-completion-distinguishes-truncation: schema
210
220
  ]);
211
221
  if (seed.status === 404) return softSkip('blocked', 'precondition not met — `seed.status === 404` returned early (seam, prior step, or fixture unavailable)');
212
222
 
213
- await startRunAndRead(SCHEMA_VIOLATION_FIXTURE);
214
- const budget = await lastBudget(SCHEMA_VIOLATION_FIXTURE);
215
- if (budget === null) return softSkip('blocked', 'precondition not met — `budget === null` returned early (seam, prior step, or fixture unavailable)');
216
- // The schema-violation fixture doesn't set maxTokens explicitly →
217
- // budget snapshots whatever the host's default is on each call.
218
- // The KEY invariant: the retry call's budget MUST NOT be multiplied
219
- // (the truncation path doubles; this path keeps the same). The
220
- // budget on the last call equals the budget on the first call.
221
- // Without a per-call history hook, we can't strictly compare; we
222
- // assert the budget didn't grow into the truncation-path range
223
- // (which would be ≥2× the default — typically 8000 for the
224
- // sample's structuredOutput dispatch path).
225
- if (budget !== null) {
223
+ const result = await startRunAndRead(SCHEMA_VIOLATION_FIXTURE);
224
+ const REQ_ID = 'openwop.it.envelope-completion-distinguishes-truncation.schema-violation-retry-budget-unchanged-from-initial-no-budget-multiplication-on';
225
+
226
+ // Suite 2.42.7: the per-attempt witness. `last-dispatch-budget` reports only
227
+ // the most recent call, so the retry's budget could not be compared with the
228
+ // attempt before it — a host that doubled 256 → 512 on a schema-violation
229
+ // retry passed the `< 20_000` range check below. `dispatch-budgets`
230
+ // (host-sample-test-seams.md §28) returns every call's budget in call order,
231
+ // and the fixture now sets `maxTokens: 256` so attempt 1 carries one. A served
232
+ // seam that breaks its contract is `blocked`; an UNSERVED one falls through to
233
+ // the pre-seam partial witness below (the seam is newer than this leg).
234
+ const budgets = await readDispatchBudgets(NODE_OF[SCHEMA_VIOLATION_FIXTURE] as string);
235
+ if (!budgets.ok && !budgets.unserved) return blockedDespiteAssertions(budgets.reason);
236
+ if (!budgets.ok) {
237
+ const budget = await lastBudget(SCHEMA_VIOLATION_FIXTURE);
238
+ if (budget === null) return softSkip('blocked', 'precondition not met — `budget === null` returned early (seam, prior step, or fixture unavailable)');
239
+ // Pre-seam path: only the most recent call's budget is visible, so it is
240
+ // range-checked against the truncation path (≥2× the default — typically
241
+ // 8000 for the sample's structuredOutput dispatch path), not compared.
226
242
  expect(
227
243
  budget,
228
- req('openwop.it.envelope-completion-distinguishes-truncation.schema-violation-retry-budget-unchanged-from-initial-no-budget-multiplication-on',
244
+ req(REQ_ID,
229
245
  'RFCS/0033-envelope-completion-contract.md §C',
230
246
  'schema-violation retry MUST NOT multiply maxTokens — budget stays at the original value (host default)',
231
247
  ),
232
248
  ).toBeLessThan(20_000);
249
+ // partial-witness-ok: the host does not serve the optional dispatch-budgets
250
+ // seam (host-sample-test-seams.md §28), so the retry's budget is range-checked,
251
+ // not compared with the attempt before it. The acceptance predicate refuses a
252
+ // partial-witness row.
253
+ return softSkip('inapplicable', `partial witness — the §C budget rule is only range-checked: ${budgets.reason}; the most recent call's maxTokens (${budget}) is below the truncation-path range, but a retry that grew the budget within that range is not detected`);
254
+ }
255
+ if (result === null) return blockedDespiteAssertions('the run create or event-log read failed — the retry count the budgets are checked against was never observed');
256
+ const retries = result.events.filter((e) => e.type === 'envelope.retry.attempted').length;
257
+ // RFC 0033 §C: a host MAY retry. No retry means no retry budget to compare;
258
+ // the sibling leg above convicts a host that should have retried and did not.
259
+ // partial-witness-ok: no retry happened, so there is no retry budget to observe.
260
+ if (retries === 0) return softSkip('inapplicable', 'the host made no schema-violation retry (RFC 0033 §C: MAY retry) — there is no retry budget to compare');
261
+ if (budgets.attempts.length < retries + 1) {
262
+ return blockedDespiteAssertions(`the dispatch-budgets seam recorded ${budgets.attempts.length} call(s) but the run emitted ${retries} envelope.retry.attempted event(s), i.e. ${retries + 1} calls — the seam does not report every attempt (host-sample-test-seams.md §28)`);
263
+ }
264
+ const attempts = budgets.attempts;
265
+ if (!attempts.every((m): m is number => m !== null)) {
266
+ // partial-witness-ok: the seam is served but at least one call carried no
267
+ // budget (maxTokens null), so no two attempts' budgets can be compared.
268
+ return softSkip('inapplicable', `partial witness — the §C budget rule is UNWITNESSED: the dispatch-budgets seam shows a call with no output budget (per-attempt budgets: ${JSON.stringify(attempts)}), so no retry's budget can be compared with the attempt before it`);
269
+ }
270
+ for (let i = 1; i < attempts.length; i++) {
271
+ expect(
272
+ attempts[i]! <= attempts[i - 1]!,
273
+ req(REQ_ID,
274
+ 'RFCS/0033-envelope-completion-contract.md §C',
275
+ `schema-violation retries SHALL NOT include an increased output budget — attempt ${i + 1} carried maxTokens ${attempts[i]} after attempt ${i} carried ${attempts[i - 1]} (per-attempt budgets: ${JSON.stringify(attempts)})`,
276
+ ),
277
+ ).toBe(true);
233
278
  }
234
279
  });
235
280
  });
@@ -117,6 +117,10 @@ import { isFixtureAdvertised } from '../lib/fixtures.js';
117
117
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
118
118
  import { req } from '../lib/requirement-ids.js';
119
119
  import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
120
+ import { holdMockNodes } from '../lib/mock-node-lock.js';
121
+
122
+ // The mock-AI program seam is keyed by node id; files sharing a fixture's node run one at a time.
123
+ holdMockNodes('retry-attempted-structured-call');
120
124
 
121
125
  const FIXTURE = 'conformance-envelope-retry-attempted';
122
126
  const NODE_ID = 'retry-attempted-structured-call';
@@ -22,6 +22,11 @@ import { pollUntilTerminal } from '../lib/polling.js';
22
22
  import { isFixtureAdvertised } from '../lib/fixtures.js';
23
23
  import { req } from '../lib/requirement-ids.js';
24
24
  import { softSkip, blockedDespiteAssertions } from '../lib/soft-skip.js';
25
+ import { holdMockNodes } from '../lib/mock-node-lock.js';
26
+ import { truncationBudgetVerdict } from '../lib/mock-ai-budgets.js';
27
+
28
+ // The mock-AI program seam is keyed by node id; files sharing a fixture's node run one at a time.
29
+ holdMockNodes('truncated-structured-call');
25
30
 
26
31
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
27
32
  const FIXTURE = 'conformance-envelope-truncated';
@@ -50,12 +55,6 @@ async function startRunAndRead(): Promise<{ events: RunEvent[]; terminal: unknow
50
55
  return { events, terminal };
51
56
  }
52
57
 
53
- async function lastBudget(): Promise<number | null> {
54
- const res = await driver.get(`/v1/host/sample/test/mock-ai/last-dispatch-budget?nodeId=${encodeURIComponent(NODE_ID)}`);
55
- if (res.status !== 200) return null;
56
- return (res.json as { maxTokens?: number | null }).maxTokens ?? null;
57
- }
58
-
59
58
  describe.skipIf(HTTP_SKIP)('envelope-truncated: runtime behavior (RFC 0032 §B.4 + RFC 0033 §B)', () => {
60
59
  it('when mock returns stopReason: max_tokens on attempt 1, exactly one envelope.truncated event fires with stopReason: max_tokens', async () => {
61
60
  if (!isFixtureAdvertised(FIXTURE)) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!isFixtureAdvertised(FIXTURE)` returned early');
@@ -112,19 +111,20 @@ describe.skipIf(HTTP_SKIP)('envelope-truncated: runtime behavior (RFC 0032 §B.4
112
111
 
113
112
  const result = await startRunAndRead();
114
113
  if (result === null) return softSkip('blocked', 'precondition not met — `result === null` returned early (seam, prior step, or fixture unavailable)');
115
- // After the run, the mock's most-recent budget is the SECOND (retry)
116
- // attempt's maxTokens. Per RFC 0033 §B, this MUST exceed the fixture's
117
- // initial maxTokens (50). The host's default multiplier is 2 — so the
118
- // retry should see 100.
119
- const budget = await lastBudget();
120
- if (budget === null) return softSkip('blocked', 'precondition not met — `budget === null` returned early (host doesn\'t expose the seam) (seam, prior step, or fixture unavailable)'); // host doesn't expose the seam
114
+ // RFC 0033 §B: the retry budget SHOULD exceed the first attempt's (a MAY retry,
115
+ // a SHOULD increase). Suite 2.42.9 compares the budgets the provider actually
116
+ // received, and records an unmet SHOULD rather than failing it; until then this
117
+ // leg asserted `> 50` (the fixture value) as a MUST.
118
+ const v = await truncationBudgetVerdict(NODE_ID);
119
+ if (v.kind === 'skip') return softSkip(v.skip, v.reason);
120
+ if (v.kind === 'unmet') return softSkip('inapplicable', `RFC 0033 §B SHOULD not met — the retry carried maxTokens ${v.retry} after the first attempt carried ${v.first}; recorded, not failed`);
121
121
  expect(
122
- budget,
123
- req('openwop.it.envelope-truncated.retry-attempt-receives-a-maxtokens-value-strictly-greater-than-the-previous-atte',
122
+ v.retry,
123
+ req('openwop.it.envelope-truncated.retry-attempt-receives-a-maxtokens-value-strictly-greater-than-the-previous-atte',
124
124
  'RFCS/0033-envelope-completion-contract.md §B',
125
- 'truncation retry MUST issue with a strictly-increased maxTokens budget (host multiplies by capabilities.envelopes.reliability.completion.truncationBudgetMultiplier)',
125
+ `the truncation retry's budget SHOULD exceed the first attempt's (first ${v.first}, retry ${v.retry})`,
126
126
  ),
127
- ).toBeGreaterThan(50);
127
+ ).toBeGreaterThan(v.first);
128
128
  });
129
129
 
130
130
  it('run terminates `completed` after the second attempt succeeds', async () => {