@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.
- package/CHANGELOG.md +21 -0
- package/README.md +3 -3
- package/dist/cli.js +3 -4
- package/dist/spec-artifacts.lock.json +2 -2
- package/fixtures/conformance-envelope-retry-attempted.json +2 -1
- package/package.json +2 -2
- package/requirements.json +181 -31
- package/scenario-majors.json +8 -2
- package/schemas/CORPUS-STAMP.json +29 -27
- package/src/cli.ts +3 -4
- package/src/lib/mock-ai-budgets.ts +66 -0
- package/src/lib/mock-node-lock.ts +66 -0
- package/src/scenarios/audit-log-integrity.test.ts +7 -16
- package/src/scenarios/envelope-completion-distinguishes-truncation.test.ts +67 -22
- package/src/scenarios/envelope-retry-attempted.test.ts +4 -0
- package/src/scenarios/envelope-truncated.test.ts +16 -16
- package/src/scenarios/v2-client-version-header.test.ts +124 -0
- package/src/scenarios/v2-id-grammar.test.ts +17 -5
- package/src/scenarios/v2-min-client-version.test.ts +18 -14
- package/src/scenarios/v2-preferred-version-default.test.ts +19 -1
- package/src/scenarios/v2-webhook-delivery-shape.test.ts +5 -0
- package/src/scenarios/v2-webhook-generated-secret.test.ts +76 -0
|
@@ -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.
|
|
5
|
-
"corpusTag": "v2.42.
|
|
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": "
|
|
13
|
-
"api/v2/asyncapi.yaml": "
|
|
14
|
-
"api/v2/openapi.yaml": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
205
|
+
"spec/v1/alias-detectors.json": "29f21e0fb2c7a232a92a5c19b4cba3a86bc3b5e16f06780452f0421c66c6ec38",
|
|
206
206
|
"spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
|
|
207
|
-
"spec/v1/core-standard-manifest.json": "
|
|
208
|
-
"spec/v1/deprecations.json": "
|
|
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": "
|
|
213
|
+
"spec/v1/gaps.json": "2b032d91d614a758db347be4f5e617b8ac3c4622b6f95ffcfa928276c0fb7377",
|
|
214
214
|
"spec/v1/gaps.schema.json": "8fd83259f556553c9df0f53e7a82ca8c2a4771d471197f69b4e2ae0ceeacfd66",
|
|
215
|
-
"spec/v1/migrations.json": "
|
|
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": "
|
|
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": "
|
|
224
|
+
"spec/v2/core/errors.md": "d14649ee60f8260ec163464163409ad7f50cb86fee7b846e0190f66070b3774b",
|
|
225
225
|
"spec/v2/core/events.md": "234338deb97be648731e8ab2126a90b7b8ad0a7070d98ffc85ee4689ff059791",
|
|
226
|
-
"spec/v2/core/form-content-packs.md": "
|
|
227
|
-
"spec/v2/core/headers.md": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
243
|
-
"spec/v2/core/webhooks.md": "
|
|
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": "
|
|
249
|
+
"spec/v2/declaration.json": "1253a5590ec2d9ba511d15e8a9d9da5e6926c4daa981a26b73b29bc915477013",
|
|
248
250
|
"spec/v2/declaration.schema.json": "ef951633e4070899f64828f5cf1b5f4870270a0360f81dbb49b5b4d6cd407550",
|
|
249
|
-
"spec/v2/errors.json": "
|
|
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": "
|
|
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": "
|
|
300
|
+
"spec/v2/release.json": "1ac31e3dd517036b357bfa5743b760e21d28b8cda7f8550e65d8444e964751c6",
|
|
299
301
|
"spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624",
|
|
300
|
-
"spec/v2/surface-baseline.json": "
|
|
302
|
+
"spec/v2/surface-baseline.json": "5cab3b39eaeb344361ac7d9038d15be5cf6a5620f1912fdd0c184cef95bae857"
|
|
301
303
|
},
|
|
302
|
-
"corpusCommit": "
|
|
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
|
|
108
|
-
// `--bundle-version
|
|
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
|
|
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
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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
|
-
|
|
178
|
+
`the truncation retry's budget SHOULD exceed the first attempt's (first ${v.first}, retry ${v.retry})`,
|
|
169
179
|
),
|
|
170
|
-
).toBeGreaterThan(
|
|
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
|
|
215
|
-
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
if (
|
|
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(
|
|
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
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
const
|
|
120
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
125
|
+
`the truncation retry's budget SHOULD exceed the first attempt's (first ${v.first}, retry ${v.retry})`,
|
|
126
126
|
),
|
|
127
|
-
).toBeGreaterThan(
|
|
127
|
+
).toBeGreaterThan(v.first);
|
|
128
128
|
});
|
|
129
129
|
|
|
130
130
|
it('run terminates `completed` after the second attempt succeeds', async () => {
|