@openwop/openwop-conformance 2.31.1 → 2.33.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 +26 -0
- package/README.md +5 -4
- package/coverage.md +2 -1
- package/dist/cli.js +7 -4
- package/dist/lib/certification-bundle-v3.js +12 -2
- package/dist/lib/child-env.js +29 -0
- package/dist/lib/v2-profiles.js +66 -0
- package/dist/spec-artifacts.lock.json +2 -2
- package/package.json +2 -2
- package/requirements.json +130 -12
- package/scenario-majors.json +5 -2
- package/schemas/CORPUS-STAMP.json +17 -17
- package/src/cli.ts +7 -4
- package/src/lib/a2a-fake-peer.ts +16 -3
- package/src/lib/certification-bundle-v3.ts +11 -2
- package/src/lib/child-env.ts +29 -0
- package/src/lib/mcp-fake-server.ts +13 -0
- package/src/lib/v2-profiles.ts +66 -0
- package/src/lib/webhook-receiver.ts +24 -5
- package/src/scenarios/a2a-1-0-agent-card.test.ts +1 -1
- package/src/scenarios/a2a-peer-authority.test.ts +1 -1
- package/src/scenarios/a2a-task-roundtrip.test.ts +1 -1
- package/src/scenarios/a2a-version-negotiation.test.ts +3 -3
- package/src/scenarios/mcp-extension-opacity.test.ts +1 -1
- package/src/scenarios/mcp-mrtr-roundtrip.test.ts +1 -1
- package/src/scenarios/mcp-tool-roundtrip.test.ts +1 -1
- package/src/scenarios/mcp-version-negotiation.test.ts +3 -3
- package/src/scenarios/v2-durability-recovery.test.ts +245 -55
- package/src/scenarios/v2-minimum-version-refused.test.ts +2 -2
- package/src/scenarios/v2-mrtr-rounds-ceiling.test.ts +1 -1
- package/src/scenarios/v2-negotiation-authenticated.test.ts +2 -2
- package/src/scenarios/v2-negotiation-decided-emitted.test.ts +3 -3
- package/src/scenarios/v2-relaxation-recorded.test.ts +37 -2
- package/src/scenarios/v2-webhook-egress-refusal.test.ts +115 -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.
|
|
5
|
-
"corpusTag": "v2.
|
|
4
|
+
"version": "2.33.0",
|
|
5
|
+
"corpusTag": "v2.33.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": "975b6cd87fa679857df9fcdc60b6a2bed98510b831e82a5eff5a114e3b263f6e",
|
|
13
|
+
"api/v2/asyncapi.yaml": "96ba13176119bbf54a94bd41d789ebaa919f67c81d529bdc3c66ffb3f419ee36",
|
|
14
|
+
"api/v2/openapi.yaml": "428ab881d0a8d08d388dd89d4b4457f3454cd0433b665674180d3e85821b978d",
|
|
15
15
|
"api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
|
|
16
16
|
"schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
|
|
17
17
|
"schemas/a2a-task-state.schema.json": "c9365918f993f943b4b619d42551eb066a1ed33a08d895d51b395432a5b1f1bc",
|
|
@@ -112,19 +112,19 @@
|
|
|
112
112
|
"schemas/v2/ai-envelope.schema.json": "0dc7dfbf032a37239ffd81fc2d67f999a40782ff5074866a56e414164cdee809",
|
|
113
113
|
"schemas/v2/annotation-create.schema.json": "ec32c22a243b343eeb6956a23be2d76ba5b50c8018ec3ec9d6efa5a1d9b0c792",
|
|
114
114
|
"schemas/v2/annotation.schema.json": "eb4813cdaa8e325412cfcaeeee5c8a28f3751c946dec64e546c7ba0ad3aad8d4",
|
|
115
|
-
"schemas/v2/artifact-type-pack-manifest.schema.json": "
|
|
115
|
+
"schemas/v2/artifact-type-pack-manifest.schema.json": "f77d8dcc3112afec8b2a8e67e56752d5811deb2c2455b0d6d4c6489eb4c4a829",
|
|
116
116
|
"schemas/v2/audit-verify-result.schema.json": "a748dfadb5155adfde8a91045e76b0db6695005fe559035d71670fdd55edb3ba",
|
|
117
117
|
"schemas/v2/budget-policy.schema.json": "c7449daeb6e1d95e7a047b8c2814d3d54748697c27046972834874867d9b5f4c",
|
|
118
118
|
"schemas/v2/capabilities.schema.json": "e62d44f23c21c8f64b273fcdb083636e801c0de976cbd1f93333f99928dd1025",
|
|
119
119
|
"schemas/v2/certification-bundle.schema.json": "ab9204e37f9e0bba79857275cd38be2da98e54d26b4ecb97469829efe6ebea28",
|
|
120
120
|
"schemas/v2/channel-presence-payload.schema.json": "1c20ad810cb311d47aa0b95d928490826b2468fd161f884cd29e867053e6782f",
|
|
121
121
|
"schemas/v2/channel-written-payload.schema.json": "ccecff3c71a3275ad8db035ac0e04db25e6f8d23cf091ddbcd5a60d1be1cbb6b",
|
|
122
|
-
"schemas/v2/chat-card-pack-manifest.schema.json": "
|
|
122
|
+
"schemas/v2/chat-card-pack-manifest.schema.json": "f7cf09b30d3e1d2251620d84ea9f65541438d015ab44aec10d1dcfe68d8bd496",
|
|
123
123
|
"schemas/v2/compact-tool-descriptor.schema.json": "b5c418d72082d5b713bf318212a83397e5be98e56d25ca056302b41b8dd8273b",
|
|
124
124
|
"schemas/v2/compensation-policy.schema.json": "b3c1142637f5bf980f2b0bb67adb47e8cb7abb99a0405a4b6816ccf5d9c089d6",
|
|
125
125
|
"schemas/v2/compensation-projection.schema.json": "f46977c7076347b5473faa8c4bbe73d8321c33b33c98fff2f26ee7cbada4ed98",
|
|
126
126
|
"schemas/v2/configurable.schema.json": "33a6884047e2490f723733bf8b74d3b9164e00374cfea6dc3eac5a70dc368861",
|
|
127
|
-
"schemas/v2/connection-pack-manifest.schema.json": "
|
|
127
|
+
"schemas/v2/connection-pack-manifest.schema.json": "4300e815cf376959f8250ed25ba0545ae8bd525e449c6442326934f451070cc9",
|
|
128
128
|
"schemas/v2/conversation-event.schema.json": "e87b7fb36ab4ca0afe9f77993654065b28b67e5f24b20175907f90e238942025",
|
|
129
129
|
"schemas/v2/conversation-turn.schema.json": "a15b43dc15dbcc6aaf216bfab7424c540b514d7e40e661a732384aad45387006",
|
|
130
130
|
"schemas/v2/credential-provenance.schema.json": "f86b09e94b1c524dec7d1c6353965665ae8891e9517faef6e742d639c5578c99",
|
|
@@ -144,7 +144,7 @@
|
|
|
144
144
|
"schemas/v2/error-envelope.schema.json": "bb3e6c487dafa951a56ff224a7c029c462f0f1b754a7f51f6729609b4f43a438",
|
|
145
145
|
"schemas/v2/eval-summary.schema.json": "9d31025752d0f67ad3b8a32e82a5314972c530ef9f780dd82e484ae8c5131198",
|
|
146
146
|
"schemas/v2/export-bundle.schema.json": "bd28905a7729437ab7f2ad8bb33e13a371b2bf5605a7eb2962a0d246de312d69",
|
|
147
|
-
"schemas/v2/form-content-pack-manifest.schema.json": "
|
|
147
|
+
"schemas/v2/form-content-pack-manifest.schema.json": "8ec157cf51241d4df800398e811d2c35addd076b929c58be3ce3dcd671c67641",
|
|
148
148
|
"schemas/v2/frontend-plugin-manifest.schema.json": "fed6b8ddb3056c53b1bfc91525410a4445a3c37a8bf9eb9f6f9964ffea26d993",
|
|
149
149
|
"schemas/v2/goal.schema.json": "146c4182e36d3538778f947e668a92cdb69ec42ee0820104b00063b035e342d7",
|
|
150
150
|
"schemas/v2/heartbeat-evaluated.schema.json": "4086b1ba1d0547c08b038dfb263ac89416a3e406430db1250156fb2f9b74f8d6",
|
|
@@ -156,12 +156,12 @@
|
|
|
156
156
|
"schemas/v2/localized-content-section.schema.json": "29c7bad0ad9ebc6c45211282e8afb3fdb35a4ca075ce0404b6e2c29f35fa5313",
|
|
157
157
|
"schemas/v2/memory-entry.schema.json": "290576ae84f4fef4cdf975932b87963283b11c50ea3d2654b32cd96aa6537c3d",
|
|
158
158
|
"schemas/v2/memory-list-options.schema.json": "f66703bccb68efb28391c273abb66aa117ee5b5ce475c477846ed700c336afcd",
|
|
159
|
-
"schemas/v2/node-pack-manifest.schema.json": "
|
|
159
|
+
"schemas/v2/node-pack-manifest.schema.json": "47d122f7d4711eaa13464c5487c26ed36103b7c85adf1987004bf2d6842d663b",
|
|
160
160
|
"schemas/v2/orchestrator-decision.schema.json": "2da17538e84cfbe70151eb118f1ccf670cff58b8697148e394074d32ac4af6b4",
|
|
161
161
|
"schemas/v2/org-chart-responsibility-view.schema.json": "8a2565e4cdc6737a3946a70799f93642808b92f9c8d56dcdff3a0782053b89a1",
|
|
162
162
|
"schemas/v2/pack-lockfile.schema.json": "2685417702c105146b780cd99154296a46578fbb10d81c8c713348ee26fea898",
|
|
163
163
|
"schemas/v2/prompt-kind.schema.json": "67251c937a5a91fb97c0422050e808dc0c162a9b8561366814cf87b0a7f62e00",
|
|
164
|
-
"schemas/v2/prompt-pack-manifest.schema.json": "
|
|
164
|
+
"schemas/v2/prompt-pack-manifest.schema.json": "4b1e510341cc6ba167b007ecae38ce0d71d5b89c751671777c5457da926e9744",
|
|
165
165
|
"schemas/v2/prompt-ref.schema.json": "11538fa36a7a652c93100cac4f1857eb4130137b546e3d94890618636d44b729",
|
|
166
166
|
"schemas/v2/prompt-template.schema.json": "494fc83028cb3af5dbb8a943d0e83b2348f7775e9fa02369162bcb58a6f81559",
|
|
167
167
|
"schemas/v2/proposal.schema.json": "cb5ea4fcb54be94fd2a92cf2799357dc73335181a89f5766f432d69e21cc3a80",
|
|
@@ -189,7 +189,7 @@
|
|
|
189
189
|
"schemas/v2/ui-plugin-message.schema.json": "879d65867c01656b3962d64283e66364dcb8f626ad26c5fcf2d2d55a79ae0ce9",
|
|
190
190
|
"schemas/v2/webhook-dead-letter-page.schema.json": "543f5ccc26d8f777684a037172ab7b9fdf70735e5394eeb42e5fa6569503f528",
|
|
191
191
|
"schemas/v2/webhook-delivery.schema.json": "4e8ab3b722ad1731adf0548c1217f0de62da40de14e8abf77533aee4ace4c079",
|
|
192
|
-
"schemas/v2/workflow-chain-pack-manifest.schema.json": "
|
|
192
|
+
"schemas/v2/workflow-chain-pack-manifest.schema.json": "58fa5b67c6e3a320f77aa1eb5d5fa7c6c9b3356afe8b75ad74a7cf082496e04b",
|
|
193
193
|
"schemas/v2/workflow-definition.schema.json": "9aafddec32b84eb6aec3185be024850f1b9444c6193568771b0b636595fef136",
|
|
194
194
|
"schemas/v2/workload-identity.schema.json": "1f589debfac6d70e0908da511550ec69a27b4bcc8314551cd843b6d923605efc",
|
|
195
195
|
"schemas/v2/workspace-file-create.schema.json": "b6b5260857318c3b67de0231ed15e3a746f525880a763e7b36a7854178dd10d0",
|
|
@@ -201,7 +201,7 @@
|
|
|
201
201
|
"schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
|
|
202
202
|
"spec/v1/alias-detectors.json": "40069d5976eeb6ba1384a648e57e5cfd673db3fce36175115fb8293bee9664d4",
|
|
203
203
|
"spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
|
|
204
|
-
"spec/v1/core-standard-manifest.json": "
|
|
204
|
+
"spec/v1/core-standard-manifest.json": "9f5d065a0a84cf4745f53957f395b1e868453f910531e030f2da4039ee2c9140",
|
|
205
205
|
"spec/v1/deprecations.json": "307083ce29c23fd406015951f99a30d78d6187ff061d38dc9732f62191b40f3f",
|
|
206
206
|
"spec/v1/deprecations.schema.json": "18c87e78bedc210431f795ae44c5b5d202f2f3317850d5cf86867d4f1fa1cdfb",
|
|
207
207
|
"spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
|
|
@@ -228,7 +228,7 @@
|
|
|
228
228
|
"spec/v2/core/interop.md": "6bd581556b9ab1a1509c01b9a1ec2bcb9e4a8300e68fb01570704417e70f20b9",
|
|
229
229
|
"spec/v2/core/interrupt.md": "3a926d84b942d4ee7daccc4443c93dc29f6a69f2936ba813182f2c1336db2cbb",
|
|
230
230
|
"spec/v2/core/overview.md": "7dc00ace32d4e4ce929a383651f0029fc4b7091ec87487b1bf26ead1be209e0e",
|
|
231
|
-
"spec/v2/core/packs.md": "
|
|
231
|
+
"spec/v2/core/packs.md": "1c0919059760f9a3940a93264a76b7e30f9a0f6f27ff19753cc88f15753d8088",
|
|
232
232
|
"spec/v2/core/persistence.md": "9e241f84259e38848f4a2ad1c21debb0dffd5f8aafbad26408aa6cc8969bf93b",
|
|
233
233
|
"spec/v2/core/replay.md": "b980a66e2543107240e3dc4f58131b9f4ff52478737e79ade6d5777b26ad1119",
|
|
234
234
|
"spec/v2/core/runs.md": "136319842bacbbb300178980e4b83444917d5b193cc5fdd92e39a4902ba8d3c0",
|
|
@@ -274,13 +274,13 @@
|
|
|
274
274
|
"spec/v2/facets/schemaVersions.schema.json": "890d3416c4b66511d51f329abc68e6f467f4338f16807e489fe682999b2b56b9",
|
|
275
275
|
"spec/v2/facets/supportedEnvelopes.schema.json": "644f22cc2236e30a611b5b7f1ba0458ff1925d68b94c514d9672583224e126bd",
|
|
276
276
|
"spec/v2/facets/webhooks.schema.json": "4bf3f8eb3161a43910efac4c44ccaf9567060c9da52d4cf09bcf3888d1b92192",
|
|
277
|
-
"spec/v2/facets/workflowChainPacks.schema.json": "
|
|
277
|
+
"spec/v2/facets/workflowChainPacks.schema.json": "ea625ce818fbeb746478f00f31f96ee9613d538e4409c258c1bfb517acdeaba9",
|
|
278
278
|
"spec/v2/id-field-bindings.json": "e73c06f3055ad30173507ac817a458b96362c8f26209ba105c53653292cf63d3",
|
|
279
279
|
"spec/v2/path-manifest.json": "c123c9fd77dc1b3f9c2e9346ace80cdae007e6511138894956abda01e61ec1c6",
|
|
280
280
|
"spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
|
|
281
281
|
"spec/v2/profiles.json": "0636f19fceae625390003a347e70ef4797d84766b5c24ce8a02cea52aadebca4",
|
|
282
|
-
"spec/v2/release.json": "
|
|
282
|
+
"spec/v2/release.json": "94a3ea302324d9f7df87ce62a186b0b98936de6ce5ac5ec01a3fb3208c38efc3",
|
|
283
283
|
"spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624"
|
|
284
284
|
},
|
|
285
|
-
"corpusCommit": "
|
|
285
|
+
"corpusCommit": "e9825b0da6ca8e2dbc76353e52aea73a56473393"
|
|
286
286
|
}
|
package/src/cli.ts
CHANGED
|
@@ -54,7 +54,8 @@ import {
|
|
|
54
54
|
PROFILE_FLOOR_SCENARIOS,
|
|
55
55
|
} from './lib/profiles.js';
|
|
56
56
|
import { setV2ProfileFloors, v2ProfileFloorFiles } from './lib/requirement-registry.js';
|
|
57
|
-
import { v2ProfileIds } from './lib/v2-profiles.js';
|
|
57
|
+
import { profilesDeniedByObservedRelaxation, profilesRelaxedBy, v2ProfileIds } from './lib/v2-profiles.js';
|
|
58
|
+
import { childEnv } from './lib/child-env.js';
|
|
58
59
|
|
|
59
60
|
interface ParsedArgs {
|
|
60
61
|
readonly baseUrl: string | undefined;
|
|
@@ -525,7 +526,7 @@ async function runCertify(args: ParsedArgs, baseUrl: string, apiKey: string): Pr
|
|
|
525
526
|
// (and assertion count) here; the runner reads it after the run so bundle v2
|
|
526
527
|
// rows come from what scenarios RECORDED, not from per-file pass/fail/skip.
|
|
527
528
|
const ledgerFile = join(reportDir, 'requirement-ledger.jsonl');
|
|
528
|
-
const env: NodeJS.ProcessEnv =
|
|
529
|
+
const env: NodeJS.ProcessEnv = childEnv(process.env);
|
|
529
530
|
env.OPENWOP_BASE_URL = baseUrl;
|
|
530
531
|
env.OPENWOP_API_KEY = apiKey;
|
|
531
532
|
env.OPENWOP_LEDGER_PATH = ledgerFile;
|
|
@@ -693,7 +694,9 @@ async function runCertify(args: ParsedArgs, baseUrl: string, apiKey: string): Pr
|
|
|
693
694
|
// belongs to. Matched the same way the verifier matches it, so the emitter
|
|
694
695
|
// and `verifyBundleV3` cannot disagree about the same bundle.
|
|
695
696
|
const relaxedObligations = new Set((relaxations ?? []).map((r) => r.obligation));
|
|
696
|
-
const
|
|
697
|
+
const relaxedProfileIds = profilesRelaxedBy([...relaxedObligations], claimedProfiles);
|
|
698
|
+
const observedRelaxed = profilesDeniedByObservedRelaxation(rows3, claimedProfiles).profiles;
|
|
699
|
+
const relaxedProfile = (p: string): boolean => relaxedProfileIds.has(p) || observedRelaxed.has(p);
|
|
697
700
|
const claimed3 = claimedProfiles.filter((p) => !(p in DEPRECATED_PROFILE_ALIASES)).map((p) => ({ id: p, evidenceTier: args.evidenceTier, witnessCount: witnessCountFor(p), certified: !relaxedProfile(p) && (verdictFor(p)?.certifiable ?? false) && !notHeld.has(p) && !rejectedProfiles.some((v) => v.profile === p) && totals3.blocked === 0 }));
|
|
698
701
|
const lockPath = resolvePath(conformanceRoot, 'dist', 'spec-artifacts.lock.json');
|
|
699
702
|
const lock = existsSync(lockPath) ? (JSON.parse(readFileSync(lockPath, 'utf8')) as { version: string; stampSha256: string }) : undefined;
|
|
@@ -973,7 +976,7 @@ async function main(): Promise<never> {
|
|
|
973
976
|
|
|
974
977
|
// Env vars OVERRIDE flags only when the flag was unset (consistent
|
|
975
978
|
// with the rest of the harness — env wins on the absence of CLI input).
|
|
976
|
-
const env: NodeJS.ProcessEnv =
|
|
979
|
+
const env: NodeJS.ProcessEnv = childEnv(process.env);
|
|
977
980
|
if (args.baseUrl) env.OPENWOP_BASE_URL = args.baseUrl;
|
|
978
981
|
if (args.apiKey) env.OPENWOP_API_KEY = args.apiKey;
|
|
979
982
|
if (args.impl) env.OPENWOP_IMPLEMENTATION_NAME = args.impl;
|
package/src/lib/a2a-fake-peer.ts
CHANGED
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
* @see https://a2a-protocol.org/v0.3.0/specification
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
|
+
import { resolvePublicFront } from './webhook-receiver.js';
|
|
41
42
|
import { createServer, type Server } from 'node:http';
|
|
42
43
|
import type { AddressInfo } from 'node:net';
|
|
43
44
|
|
|
@@ -198,6 +199,18 @@ export class A2AFakePeer {
|
|
|
198
199
|
return `http://127.0.0.1:${this._boundPort}`;
|
|
199
200
|
}
|
|
200
201
|
|
|
202
|
+
/**
|
|
203
|
+
* The address to hand THE HOST UNDER TEST — `OPENWOP_A2A_FAKE_PEER_URL` when the operator fronts
|
|
204
|
+
* this peer publicly (https, publicly resolvable; validated loudly, same rule
|
|
205
|
+
* as the webhook receiver), else `endpoint()`. The suite's own requests to its
|
|
206
|
+
* own fake keep using `endpoint()`: they need no tunnel and must not depend on
|
|
207
|
+
* one. Pin the listener with the matching `_PORT` variable so the front has a
|
|
208
|
+
* fixed port to forward to.
|
|
209
|
+
*/
|
|
210
|
+
hostFacingEndpoint(): string {
|
|
211
|
+
return resolvePublicFront('OPENWOP_A2A_FAKE_PEER_URL', this.endpoint()).url;
|
|
212
|
+
}
|
|
213
|
+
|
|
201
214
|
reset(): void {
|
|
202
215
|
this._tasks.clear();
|
|
203
216
|
this._invocations.length = 0;
|
|
@@ -293,7 +306,7 @@ export class A2AFakePeer {
|
|
|
293
306
|
description: 'Synthetic A2A peer for openwop conformance suite (dual-era: 1.0 + 0.3-legacy)',
|
|
294
307
|
version: '1.1.0',
|
|
295
308
|
supportedInterfaces: [...this._protocolVersions].sort((a, b) => (a === '1.0' ? -1 : b === '1.0' ? 1 : 0)).map((v) => ({
|
|
296
|
-
url: `${this.
|
|
309
|
+
url: `${this.hostFacingEndpoint()}/a2a/jsonrpc`,
|
|
297
310
|
protocolBinding: 'JSONRPC',
|
|
298
311
|
protocolVersion: v,
|
|
299
312
|
})),
|
|
@@ -310,13 +323,13 @@ export class A2AFakePeer {
|
|
|
310
323
|
protocolVersion: '0.3.0',
|
|
311
324
|
name: 'openwop-conformance-fake-a2a',
|
|
312
325
|
description: 'Synthetic A2A peer for openwop conformance suite',
|
|
313
|
-
url: `${this.
|
|
326
|
+
url: `${this.hostFacingEndpoint()}/a2a/jsonrpc`,
|
|
314
327
|
version: '1.0.0',
|
|
315
328
|
capabilities: { streaming: false, pushNotifications: false },
|
|
316
329
|
skills: [{ id: 'echo', name: 'echo', description: 'Returns input verbatim', tags: ['echo'] }],
|
|
317
330
|
defaultInputModes: ['text'],
|
|
318
331
|
defaultOutputModes: ['text'],
|
|
319
|
-
additionalInterfaces: [{ url: `${this.
|
|
332
|
+
additionalInterfaces: [{ url: `${this.hostFacingEndpoint()}/a2a/jsonrpc`, transport: 'JSONRPC' }],
|
|
320
333
|
};
|
|
321
334
|
}
|
|
322
335
|
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { createHash, createPrivateKey, createPublicKey, sign as edSign, verify as edVerify, type KeyObject } from 'node:crypto';
|
|
21
21
|
import { profileDerivable, type DiscoveryPayload } from './profiles.js';
|
|
22
|
-
import { v2RegistryAvailable } from './v2-profiles.js';
|
|
22
|
+
import { profilesDeniedByObservedRelaxation, profilesRelaxedBy, v2RegistryAvailable } from './v2-profiles.js';
|
|
23
23
|
|
|
24
24
|
export type BundleV3Result = 'executed-pass' | 'executed-fail' | 'skipped' | 'inapplicable' | 'blocked';
|
|
25
25
|
|
|
@@ -179,9 +179,18 @@ export function verifyBundleV3(bundle: BundleV3, opts: VerifyV3Options = {}): V3
|
|
|
179
179
|
}
|
|
180
180
|
// Relaxations: a relaxed obligation's profile cannot certify (RFC 0173 §A.2).
|
|
181
181
|
const relaxed = new Set((bundle.host?.relaxations ?? []).map((r) => r.obligation.split('.')[0]));
|
|
182
|
+
// Ownership comes from the profile registry, not from the profile's name — see profilesRelaxedBy.
|
|
183
|
+
const relaxedProfiles = profilesRelaxedBy((bundle.host?.relaxations ?? []).map((r) => r.obligation), (bundle.claimedProfiles ?? []).map((p) => p.id));
|
|
184
|
+
// An OBSERVED relaxation nobody declared: the bundle's own results show the
|
|
185
|
+
// host accepting a destination its egress guard MUST refuse. Same denial as a
|
|
186
|
+
// declared one, so declaring nothing is not a way around the rule.
|
|
187
|
+
const observed = profilesDeniedByObservedRelaxation(bundle.results?.requirements ?? [], (bundle.claimedProfiles ?? []).map((p) => p.id));
|
|
188
|
+
for (const p of bundle.claimedProfiles ?? []) {
|
|
189
|
+
if (p.certified && observed.profiles.has(p.id) && !relaxedProfiles.has(p.id)) rejections.push({ kind: 'undeclared-relaxation-observed', profile: p.id, detail: `${p.id} is marked certified, but this bundle's own results show the host running relaxed on ${observed.obligations.join(', ')} with no host.relaxations[] entry recording it (security-defaults.md §Relaxations: every relaxation a host runs under MUST be recorded, and its profile MUST NOT certify)` });
|
|
190
|
+
}
|
|
182
191
|
const certifiedProfiles: string[] = [];
|
|
183
192
|
for (const p of bundle.claimedProfiles ?? []) {
|
|
184
|
-
if (p.certified &&
|
|
193
|
+
if (p.certified && relaxedProfiles.has(p.id)) rejections.push({ kind: 'relaxed-profile-certified', profile: p.id, detail: `${p.id} is marked certified while a relaxation on ${[...relaxed].join(', ')} is recorded (RFC 0173 §A.2)` });
|
|
185
194
|
else if (p.certified) certifiedProfiles.push(p.id);
|
|
186
195
|
}
|
|
187
196
|
if (expected.blocked > 0 && certifiedProfiles.length > 0) rejections.push({ kind: 'blocked-certified', detail: `${expected.blocked} blocked row(s): a bundle with blocked > 0 does not certify (RFC 0168 §E.1)` });
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The environment handed to the vitest child the CLI spawns.
|
|
3
|
+
*
|
|
4
|
+
* `{ ...process.env }` handed the child EVERYTHING the operator exported,
|
|
5
|
+
* including `OPENWOP_BUNDLE_SIGNING_KEY` — the PKCS8 PEM of the key that signs
|
|
6
|
+
* the certification bundle. Nothing in the child reads it: the signer runs in
|
|
7
|
+
* the PARENT, after the run (`cli.ts` `--certify`). It was there only because
|
|
8
|
+
* nothing took it out.
|
|
9
|
+
*
|
|
10
|
+
* Why that is a leak and not a tidiness point. Measured by a host operator
|
|
11
|
+
* during a production cut (2026-09-20): on macOS `npm exec` rewrites
|
|
12
|
+
* `process.title`, and a process listing (`ps`, `pgrep -fl`) then spills into
|
|
13
|
+
* the child's environ — so listing processes mid-cut printed the signing key,
|
|
14
|
+
* in full, into the operator's terminal and transcript. Same-uid or root only,
|
|
15
|
+
* but that is exactly who is looking at the terminal. A scenario, a reporter, a
|
|
16
|
+
* vitest plugin or a crash dump in the child had the same access for no reason.
|
|
17
|
+
*
|
|
18
|
+
* The rule: material only the parent uses never enters the child. The key IDS
|
|
19
|
+
* are not secret and stay (a scenario may legitimately name the key it expects).
|
|
20
|
+
*/
|
|
21
|
+
export const PARENT_ONLY_ENV: readonly string[] = [
|
|
22
|
+
'OPENWOP_BUNDLE_SIGNING_KEY',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export function childEnv(parent: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
26
|
+
const env: NodeJS.ProcessEnv = { ...parent };
|
|
27
|
+
for (const name of PARENT_ONLY_ENV) delete env[name];
|
|
28
|
+
return env;
|
|
29
|
+
}
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
* @see SECURITY/threat-model-prompt-injection.md §"UNTRUSTED marker"
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
|
+
import { resolvePublicFront } from './webhook-receiver.js';
|
|
37
38
|
import { createServer, type Server } from 'node:http';
|
|
38
39
|
import type { AddressInfo } from 'node:net';
|
|
39
40
|
|
|
@@ -149,6 +150,18 @@ export class McpFakeServer {
|
|
|
149
150
|
return `http://127.0.0.1:${this._boundPort}`;
|
|
150
151
|
}
|
|
151
152
|
|
|
153
|
+
/**
|
|
154
|
+
* The address to hand THE HOST UNDER TEST — `OPENWOP_MCP_FAKE_SERVER_URL` when the operator fronts
|
|
155
|
+
* this server publicly (https, publicly resolvable; validated loudly, same rule
|
|
156
|
+
* as the webhook receiver), else `endpoint()`. The suite's own requests to its
|
|
157
|
+
* own fake keep using `endpoint()`: they need no tunnel and must not depend on
|
|
158
|
+
* one. Pin the listener with the matching `_PORT` variable so the front has a
|
|
159
|
+
* fixed port to forward to.
|
|
160
|
+
*/
|
|
161
|
+
hostFacingEndpoint(): string {
|
|
162
|
+
return resolvePublicFront('OPENWOP_MCP_FAKE_SERVER_URL', this.endpoint()).url;
|
|
163
|
+
}
|
|
164
|
+
|
|
152
165
|
invocations(): readonly McpInvocation[] {
|
|
153
166
|
return this._invocations;
|
|
154
167
|
}
|
package/src/lib/v2-profiles.ts
CHANGED
|
@@ -110,3 +110,69 @@ export function v2ProfileIds(doc: DiscoveryPayload): readonly string[] | null {
|
|
|
110
110
|
export function v2ProfileDerivable(doc: DiscoveryPayload, profile: string): boolean {
|
|
111
111
|
return v2ProfileIds(doc)?.includes(profile) ?? false;
|
|
112
112
|
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Which of `profileIds` a relaxed obligation denies (RFC 0173 §A.2;
|
|
116
|
+
* `security-defaults.md` §Relaxations: "a bundle that records a relaxation MUST
|
|
117
|
+
* NOT certify the profile the relaxed obligation belongs to").
|
|
118
|
+
*
|
|
119
|
+
* An obligation is `<family>.<name>`. A profile OWNS a family when its registry
|
|
120
|
+
* predicate lists it — `openwop-core-standard` is built on `interrupt`, `replay`,
|
|
121
|
+
* `webhooks`, `idempotency`, `eventLog`, so a relaxation on any of those denies
|
|
122
|
+
* it. That ownership was always stated in `spec/v2/profiles.json`; nothing read
|
|
123
|
+
* it. Until 2.33.0 the verifier tested whether the profile ID contained the
|
|
124
|
+
* family name, and the emitter tested whether it contained the WHOLE obligation
|
|
125
|
+
* string — two different rules under a comment saying they matched — and since
|
|
126
|
+
* no real v2 profile id contains a family name, a declared relaxation denied
|
|
127
|
+
* nothing on any real host. The one scenario pinning the rule used a fixture
|
|
128
|
+
* profile called `openwop-webhooks`, which is why it stayed green.
|
|
129
|
+
*
|
|
130
|
+
* The id-substring rule is KEPT as a second arm, not replaced: it is how a
|
|
131
|
+
* non-registry profile id (a v1 alias, a fixture) names its family, and
|
|
132
|
+
* dropping it would un-deny something that is denied today. When the registry
|
|
133
|
+
* is unavailable only that arm can run; the caller already records that layout
|
|
134
|
+
* as `derivabilityChecked: false`.
|
|
135
|
+
*
|
|
136
|
+
* ONE function, called by the emitter and the verifier, so the file and the
|
|
137
|
+
* verdict on the file cannot disagree again.
|
|
138
|
+
*/
|
|
139
|
+
export function profilesRelaxedBy(obligations: readonly string[], profileIds: readonly string[]): ReadonlySet<string> {
|
|
140
|
+
const families = new Set(obligations.map((o) => o.split('.')[0] ?? '').filter((f) => f.length > 0));
|
|
141
|
+
const out = new Set<string>();
|
|
142
|
+
if (families.size === 0) return out;
|
|
143
|
+
const registry = readRegistry();
|
|
144
|
+
const owned = new Map<string, readonly string[]>();
|
|
145
|
+
for (const p of registry ?? []) {
|
|
146
|
+
if (typeof p.id === 'string') owned.set(p.id, Array.isArray(p.predicate?.families) ? (p.predicate.families as unknown[]).map(String) : []);
|
|
147
|
+
}
|
|
148
|
+
for (const id of profileIds) {
|
|
149
|
+
const ownsOne = (owned.get(id) ?? []).some((f) => families.has(f));
|
|
150
|
+
const namesOne = [...families].some((f) => id.includes(f));
|
|
151
|
+
if (ownsOne || namesOne) out.add(id);
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The requirement whose FAILURE is an observed, undeclared relaxation.
|
|
158
|
+
*
|
|
159
|
+
* `conformance/README.md` said of `OPENWOP_HOST_RELAXATIONS` that "the suite
|
|
160
|
+
* cannot detect an undeclared one". For the egress guard that was never true:
|
|
161
|
+
* a host that ACCEPTS `https://127.0.0.1/…` as a webhook destination has shown
|
|
162
|
+
* on the wire that its guard is open (`webhooks.md`: "a host MUST reject (400
|
|
163
|
+
* webhook_url_rejected) … RFC 1918 and loopback and link-local ranges").
|
|
164
|
+
* `v2-webhook-egress-refusal` records that as `executed-fail` on this id unless
|
|
165
|
+
* the operator DECLARED the relaxation — in which case {@link profilesRelaxedBy}
|
|
166
|
+
* already denies the profile. Either way a profile built on `webhooks` does not
|
|
167
|
+
* certify on a host whose guard is open; this closes the path where it did,
|
|
168
|
+
* silently, because nobody declared anything.
|
|
169
|
+
*/
|
|
170
|
+
export const UNDECLARED_RELAXATION_WITNESSES: ReadonlyArray<{ readonly requirementId: string; readonly obligation: string }> = [
|
|
171
|
+
{ requirementId: 'openwop.requirement.0171.webhook-egress-refused', obligation: 'webhooks.egress-guard' },
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
/** Profiles that MUST NOT certify because a row above is `executed-fail` in this bundle's own results. */
|
|
175
|
+
export function profilesDeniedByObservedRelaxation(rows: ReadonlyArray<{ readonly id: string; readonly result: string }>, profileIds: readonly string[]): { readonly profiles: ReadonlySet<string>; readonly obligations: readonly string[] } {
|
|
176
|
+
const obligations = UNDECLARED_RELAXATION_WITNESSES.filter((w) => rows.some((r) => r.id === w.requirementId && r.result === 'executed-fail')).map((w) => w.obligation);
|
|
177
|
+
return { profiles: profilesRelaxedBy(obligations, profileIds), obligations };
|
|
178
|
+
}
|
|
@@ -235,7 +235,26 @@ export async function discoverOwnedTenant(
|
|
|
235
235
|
* "the host could not be exercised".
|
|
236
236
|
*/
|
|
237
237
|
export function resolveRegistrationUrl(localUrl: string): { url: string; tunnelled: boolean } {
|
|
238
|
-
|
|
238
|
+
return resolvePublicFront('OPENWOP_WEBHOOK_RECEIVER_URL', localUrl);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* The same rule, for ANY suite fixture the host under test must reach — the
|
|
243
|
+
* webhook receiver, the A2A fake peer, the MCP fake server. `envName` names the
|
|
244
|
+
* operator's public front for that one fixture; unset means the local address.
|
|
245
|
+
*
|
|
246
|
+
* Generalised in 2.33.0. Until then only the webhook receiver had a front, so a
|
|
247
|
+
* host that advertised `a2a` or `mcp` could be measured ONLY with its egress
|
|
248
|
+
* guard relaxed — those fakes advertised `http://127.0.0.1:<port>` and nothing
|
|
249
|
+
* else — and a relaxed guard is a relaxation the bundle must declare and cannot
|
|
250
|
+
* certify under (`security-defaults.md` §Relaxations). The two production hosts
|
|
251
|
+
* cut relaxation-free only because they advertise neither family. The rule made
|
|
252
|
+
* a host that implements MORE of the protocol LESS able to certify; found when
|
|
253
|
+
* the steward's own reference host turned out to be certifying under an
|
|
254
|
+
* undeclared one.
|
|
255
|
+
*/
|
|
256
|
+
export function resolvePublicFront(envName: string, localUrl: string): { url: string; tunnelled: boolean } {
|
|
257
|
+
const raw = process.env[envName]?.trim();
|
|
239
258
|
if (!raw) return { url: localUrl, tunnelled: false };
|
|
240
259
|
|
|
241
260
|
let parsed: URL;
|
|
@@ -243,7 +262,7 @@ export function resolveRegistrationUrl(localUrl: string): { url: string; tunnell
|
|
|
243
262
|
parsed = new URL(raw);
|
|
244
263
|
} catch {
|
|
245
264
|
throw new Error(
|
|
246
|
-
|
|
265
|
+
`${envName} is not a valid URL: ${JSON.stringify(raw)}`,
|
|
247
266
|
);
|
|
248
267
|
}
|
|
249
268
|
|
|
@@ -252,7 +271,7 @@ export function resolveRegistrationUrl(localUrl: string): { url: string; tunnell
|
|
|
252
271
|
// ALLOW_PRIVATE flag insufficient in the first place.
|
|
253
272
|
if (parsed.protocol !== 'https:') {
|
|
254
273
|
throw new Error(
|
|
255
|
-
|
|
274
|
+
`${envName} MUST be https: (got ${parsed.protocol}). ` +
|
|
256
275
|
'A plain-http front cannot clear the scheme gate, so it cannot witness this scenario.',
|
|
257
276
|
);
|
|
258
277
|
}
|
|
@@ -272,13 +291,13 @@ export function resolveRegistrationUrl(localUrl: string): { url: string; tunnell
|
|
|
272
291
|
/^(fc|fd)/.test(host);
|
|
273
292
|
if (isLoopback || isPrivate) {
|
|
274
293
|
throw new Error(
|
|
275
|
-
|
|
294
|
+
`${envName} MUST be a publicly-resolvable host (got ${parsed.hostname}). ` +
|
|
276
295
|
'It is the PUBLIC front for the local receiver — a tunnel or TLS-terminating proxy — ' +
|
|
277
296
|
'not the receiver address itself.',
|
|
278
297
|
);
|
|
279
298
|
}
|
|
280
299
|
|
|
281
|
-
return { url: raw, tunnelled: true };
|
|
300
|
+
return { url: envName === 'OPENWOP_WEBHOOK_RECEIVER_URL' ? raw : raw.replace(/\/+$/, ''), tunnelled: true };
|
|
282
301
|
}
|
|
283
302
|
|
|
284
303
|
/**
|
|
@@ -62,7 +62,7 @@ describe('RFC 0152 — the suite peer speaks A2A 1.0 (dual-era A2AFakePeer)', ()
|
|
|
62
62
|
expect(ifaces.map((i) => i.protocolVersion)).toEqual(['1.0', '0.3']);
|
|
63
63
|
for (const i of ifaces) {
|
|
64
64
|
expect(i.protocolBinding).toBe('JSONRPC');
|
|
65
|
-
expect(i.url.startsWith(peer.
|
|
65
|
+
expect(i.url.startsWith(peer.hostFacingEndpoint())).toBe(true);
|
|
66
66
|
}
|
|
67
67
|
expect(card['url'], req('openwop.it.a2a-1-0-agent-card.the-agent-card-is-1-0-shaped-supportedinterfaces-no-top-level-url-protocolversio', 'RFC 0152 §A/§B/§C/§D', '1.0 removed top-level url')).toBeUndefined();
|
|
68
68
|
expect(card['protocolVersion'], req('openwop.it.a2a-1-0-agent-card.the-agent-card-is-1-0-shaped-supportedinterfaces-no-top-level-url-protocolversio', 'RFC 0152 §A/§B/§C/§D', '1.0 removed top-level protocolVersion (it is per interface)')).toBeUndefined();
|
|
@@ -64,7 +64,7 @@ describe('RFC 0152 §E — a2a-peer-no-authority-escalation (host as A2A client,
|
|
|
64
64
|
peer.reset();
|
|
65
65
|
peer.setNextPeerAssertsAuthority(true);
|
|
66
66
|
const drive = await driver.post('/v1/host/sample/a2a/invoke', {
|
|
67
|
-
peerUrl: peer.
|
|
67
|
+
peerUrl: peer.hostFacingEndpoint(),
|
|
68
68
|
scenario: 'peer-asserts-authority',
|
|
69
69
|
});
|
|
70
70
|
if (drive.status === 404 || drive.status === 403) {
|
|
@@ -66,7 +66,7 @@ function probePeer(): { url: string; isReal: boolean } | null {
|
|
|
66
66
|
const real = process.env.OPENWOP_A2A_REAL_PEER_URL;
|
|
67
67
|
if (real && real.length > 0) return { url: real.replace(/\/$/, ''), isReal: true };
|
|
68
68
|
const fake = getA2AFakePeer();
|
|
69
|
-
if (fake) return { url: fake.
|
|
69
|
+
if (fake) return { url: fake.hostFacingEndpoint(), isReal: false };
|
|
70
70
|
return null;
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -78,7 +78,7 @@ describe('RFC 0152 §B — A2A version negotiation', () => {
|
|
|
78
78
|
const peer = getA2AFakePeer();
|
|
79
79
|
if (peer === null) return softSkip('blocked', 'precondition not met — `peer === null` returned early (no fake peer wired in this run) (seam, prior step, or fixture unavailable)'); // no fake peer wired in this run
|
|
80
80
|
peer.reset();
|
|
81
|
-
const drive = await driver.post('/v1/host/sample/a2a/invoke', { peerUrl: peer.
|
|
81
|
+
const drive = await driver.post('/v1/host/sample/a2a/invoke', { peerUrl: peer.hostFacingEndpoint() });
|
|
82
82
|
if (drive.status === 404 || drive.status === 403) {
|
|
83
83
|
// Seam absent. RFC 0148 §A: unobservable resolves to `blocked`, not a pass.
|
|
84
84
|
expect(
|
|
@@ -114,7 +114,7 @@ describe('RFC 0152 §B — A2A version negotiation', () => {
|
|
|
114
114
|
const preferred = caps?.preferredVersion;
|
|
115
115
|
peer.reset();
|
|
116
116
|
const drive = await driver.post('/v1/host/sample/a2a/invoke', {
|
|
117
|
-
peerUrl: peer.
|
|
117
|
+
peerUrl: peer.hostFacingEndpoint(),
|
|
118
118
|
authenticated: true,
|
|
119
119
|
peerOffersOnly: '0.3',
|
|
120
120
|
});
|
|
@@ -151,7 +151,7 @@ describe('RFC 0152 §B — A2A version negotiation', () => {
|
|
|
151
151
|
const peer = getA2AFakePeer();
|
|
152
152
|
if (peer === null) return softSkip('blocked', 'precondition not met — `peer === null` returned early (seam, prior step, or fixture unavailable)');
|
|
153
153
|
const drive = await driver.post('/v1/host/sample/a2a/invoke', {
|
|
154
|
-
peerUrl: peer.
|
|
154
|
+
peerUrl: peer.hostFacingEndpoint(),
|
|
155
155
|
requestVersion: '99.0',
|
|
156
156
|
});
|
|
157
157
|
if (drive.status === 404 || drive.status === 403) return softSkip('blocked', 'precondition not met — `drive.status === 404 || drive.status === 403` returned early (seam, prior step, or fixture unavailable)');
|
|
@@ -47,7 +47,7 @@ describe.skipIf(!process.env.OPENWOP_BASE_URL)('RFC 0153 §D — mcp-extension-o
|
|
|
47
47
|
if (server === null) return softSkip('blocked', 'the suite MCP fake server is not started in this run — the host-as-client leg cannot be driven');
|
|
48
48
|
server.reset();
|
|
49
49
|
server.setNextResultAssertsAuthority(true);
|
|
50
|
-
const drive = await driver.post('/v1/host/sample/mcp/invoke', { serverUrl: server.
|
|
50
|
+
const drive = await driver.post('/v1/host/sample/mcp/invoke', { serverUrl: server.hostFacingEndpoint(), tool: 'echo', arguments: { text: 'opaque' }, scenario: 'extension-asserts-authority' });
|
|
51
51
|
if (drive.status === 404 || drive.status === 403) {
|
|
52
52
|
// Advertised mcp-2026-07-28 but the invoke seam answered {drive.status}: not observable here.
|
|
53
53
|
// Default mode records `blocked` (RFC 0148 §A); OPENWOP_REQUIRE_BEHAVIOR=true fails
|
|
@@ -58,7 +58,7 @@ describe.skipIf(!process.env.OPENWOP_BASE_URL)('RFC 0153 §C — mcp-mrtr-roundt
|
|
|
58
58
|
if (server === null) return softSkip('blocked', 'the suite MCP fake server is not started in this run — the client half cannot be driven');
|
|
59
59
|
server.reset();
|
|
60
60
|
const drive = await driver.post('/v1/host/sample/mcp/invoke', {
|
|
61
|
-
serverUrl: server.
|
|
61
|
+
serverUrl: server.hostFacingEndpoint(),
|
|
62
62
|
tool: 'needs_input',
|
|
63
63
|
clientCapabilities: { elicitation: {} },
|
|
64
64
|
elicitationAnswer: { name: 'Ada' },
|
|
@@ -179,7 +179,7 @@ function probeEndpoint(): { url: string; isReal: boolean } | null {
|
|
|
179
179
|
const real = process.env.OPENWOP_MCP_REAL_SERVER_URL;
|
|
180
180
|
if (real && real.length > 0) return { url: real.replace(/\/$/, ''), isReal: true };
|
|
181
181
|
const fake = getMcpFakeServer();
|
|
182
|
-
if (fake) return { url: fake.
|
|
182
|
+
if (fake) return { url: fake.hostFacingEndpoint(), isReal: false };
|
|
183
183
|
return null;
|
|
184
184
|
}
|
|
185
185
|
|
|
@@ -86,7 +86,7 @@ describe('RFC 0153 §A/§B — MCP revision negotiation', () => {
|
|
|
86
86
|
const server = getMcpFakeServer();
|
|
87
87
|
if (server === null) return softSkip('blocked', 'precondition not met — `server === null` returned early (seam, prior step, or fixture unavailable)');
|
|
88
88
|
server.reset();
|
|
89
|
-
const drive = await driver.post('/v1/host/sample/mcp/invoke', { serverUrl: server.
|
|
89
|
+
const drive = await driver.post('/v1/host/sample/mcp/invoke', { serverUrl: server.hostFacingEndpoint() });
|
|
90
90
|
if (drive.status === 404 || drive.status === 403) {
|
|
91
91
|
expect(
|
|
92
92
|
drive.status,
|
|
@@ -125,7 +125,7 @@ describe('RFC 0153 §A/§B — MCP revision negotiation', () => {
|
|
|
125
125
|
if (server === null) return softSkip('blocked', 'precondition not met — `server === null` returned early (seam, prior step, or fixture unavailable)');
|
|
126
126
|
const caps = await mcp();
|
|
127
127
|
server.reset();
|
|
128
|
-
const drive = await driver.post('/v1/host/sample/mcp/invoke', { serverUrl: server.
|
|
128
|
+
const drive = await driver.post('/v1/host/sample/mcp/invoke', { serverUrl: server.hostFacingEndpoint() });
|
|
129
129
|
if (drive.status === 404 || drive.status === 403) return seamAbsent(`host advertises mcp version negotiation but the invoke seam /v1/host/sample/mcp/invoke answered ${drive.status} — the host-as-client legs are unobservable (host-sample-test-seams.md)`);
|
|
130
130
|
// it never advertised has made its own discovery document unreliable, which
|
|
131
131
|
// is worse than advertising nothing — a consumer that read it made a
|
|
@@ -148,7 +148,7 @@ describe('RFC 0153 §A/§B — MCP revision negotiation', () => {
|
|
|
148
148
|
const server = getMcpFakeServer();
|
|
149
149
|
if (server === null) return softSkip('blocked', 'precondition not met — `server === null` returned early (seam, prior step, or fixture unavailable)');
|
|
150
150
|
const drive = await driver.post('/v1/host/sample/mcp/invoke', {
|
|
151
|
-
serverUrl: server.
|
|
151
|
+
serverUrl: server.hostFacingEndpoint(),
|
|
152
152
|
requestVersion: '1999-01-01',
|
|
153
153
|
});
|
|
154
154
|
if (drive.status === 404 || drive.status === 403) return seamAbsent(`host advertises mcp version negotiation but the invoke seam /v1/host/sample/mcp/invoke answered ${drive.status} — the host-as-client legs are unobservable (host-sample-test-seams.md)`);
|