@junghanacs/entwurf 0.12.9 → 0.13.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/AGENTS.md +135 -248
- package/BASELINE.md +46 -7
- package/CHANGELOG.md +35 -0
- package/CONTRIBUTING.md +4 -2
- package/DELIVERY.md +5 -4
- package/README.md +46 -18
- package/VERIFY.md +10 -6
- package/demo/demo.sh +7 -1
- package/docs/setup-clean-host.md +10 -4
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +159 -66
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +148 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +16 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +66 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +190 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +12 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-core.js +26 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +10 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +15 -17
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +13 -13
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-resume-args.js +31 -41
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +8 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +32 -25
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +4 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +5 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +9 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn-production.js +3 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +17 -20
- package/mcp/entwurf-bridge/src/index.ts +190 -82
- package/package.json +7 -3
- package/pi/settings.reference.json +1 -1
- package/pi-extensions/acp-provider.ts +20 -10
- package/pi-extensions/entwurf-control.ts +69 -27
- package/pi-extensions/lib/acp/augment.ts +60 -2
- package/pi-extensions/lib/acp/backend-adapter.ts +183 -8
- package/pi-extensions/lib/acp/backend.ts +5 -1
- package/pi-extensions/lib/acp/config.ts +19 -5
- package/pi-extensions/lib/acp/engraving.ts +3 -1
- package/pi-extensions/lib/acp/event-mapper.ts +10 -3
- package/pi-extensions/lib/acp/models.ts +69 -7
- package/pi-extensions/lib/acp/overlay.ts +234 -3
- package/pi-extensions/lib/acp/tool-surface.ts +12 -4
- package/pi-extensions/lib/entwurf-core.ts +26 -9
- package/pi-extensions/lib/entwurf-deliverability.ts +10 -6
- package/pi-extensions/lib/entwurf-fact-provider.ts +15 -17
- package/pi-extensions/lib/entwurf-facts.ts +13 -13
- package/pi-extensions/lib/entwurf-resume-args.ts +40 -46
- package/pi-extensions/lib/entwurf-self-address.ts +8 -9
- package/pi-extensions/lib/entwurf-v2-contract-schema.ts +7 -2
- package/pi-extensions/lib/entwurf-v2-contract.ts +32 -25
- package/pi-extensions/lib/entwurf-v2-decider.ts +8 -8
- package/pi-extensions/lib/entwurf-v2-lock.ts +5 -4
- package/pi-extensions/lib/entwurf-v2-production.ts +14 -11
- package/pi-extensions/lib/entwurf-v2-spawn-production.ts +3 -1
- package/pi-extensions/lib/meta-session.ts +17 -20
- package/run.sh +200 -62
- package/scripts/agy-bridge-config.py +249 -49
- package/scripts/agy-bridge.sh +59 -14
- package/scripts/check-acp-carrier-augment.ts +34 -2
- package/scripts/check-acp-cortex.ts +668 -0
- package/scripts/check-acp-provider-surface.ts +50 -6
- package/scripts/check-acp-sdk-surface.ts +22 -11
- package/scripts/check-acp-session-reuse.ts +64 -1
- package/scripts/check-agy-permission-matrix.py +655 -0
- package/scripts/check-entwurf-deliverability.ts +9 -6
- package/scripts/check-entwurf-fact-provider.ts +7 -7
- package/scripts/check-entwurf-facts.ts +12 -12
- package/scripts/check-entwurf-resume-args.ts +22 -60
- package/scripts/check-entwurf-self-address.ts +187 -4
- package/scripts/check-entwurf-v2-contract.ts +2 -2
- package/scripts/check-entwurf-v2-production.ts +5 -5
- package/scripts/check-entwurf-v2-spawn-production.ts +6 -2
- package/scripts/check-entwurf-v2-surface.ts +327 -14
- package/scripts/check-gate-qualification.ts +787 -0
- package/scripts/check-meta-identity-consumers.ts +1 -1
- package/scripts/check-probe-cli-shim.ts +879 -0
- package/scripts/check-probe-ordering.ts +2450 -0
- package/scripts/check-shell-quote.ts +5 -4
- package/scripts/fixtures/probe-cli-shim +20 -0
- package/scripts/fixtures/probe-mcp-server.ts +168 -12
- package/scripts/lib/mutation-qualify.ts +794 -0
- package/scripts/lib/probe-acp-turn.ts +207 -0
- package/scripts/lib/probe-cli-shim.ts +464 -0
- package/scripts/lib/probe-cli-target.ts +165 -0
- package/scripts/lib/probe-event-log.ts +383 -0
- package/scripts/lib/probe-verdict.ts +1213 -0
- package/scripts/mutants/acp-augment.json +30 -0
- package/scripts/mutants/acp-cortex.json +196 -0
- package/scripts/mutants/agy-permission.json +144 -0
- package/scripts/mutants/meta-identity.json +17 -0
- package/scripts/mutants/probe-ordering.json +1032 -0
- package/scripts/mutants/self-address.json +59 -0
- package/scripts/mutants/v2-surface.json +87 -0
- package/scripts/smoke-acp-cortex-live.ts +392 -0
- package/scripts/smoke-acp-ordering-probe-live.ts +848 -0
- package/scripts/smoke-acp-raw-turn-live.ts +1 -1
- package/scripts/smoke-acp-v2-send-live.ts +33 -18
- package/scripts/smoke-agy-install-state.sh +205 -20
- package/scripts/smoke-entwurf-v2-matrix-live.ts +3 -2
- package/pi-extensions/lib/entwurf-mailbox-guard.ts +0 -100
- package/scripts/check-entwurf-mailbox-guard.ts +0 -266
|
@@ -30,6 +30,17 @@ import {
|
|
|
30
30
|
toDispatchInput,
|
|
31
31
|
} from "../pi-extensions/lib/entwurf-v2-surface.ts";
|
|
32
32
|
|
|
33
|
+
/** Cut ONE model-facing string out of a registration block so two descriptions in the same
|
|
34
|
+
* block cannot satisfy an assertion for each other (round-4 masking, 2026-07-27). Returns ""
|
|
35
|
+
* when either marker is missing, which the callers' `length > 120` guard turns into a loud
|
|
36
|
+
* failure rather than a silent pass. */
|
|
37
|
+
function sliceDescription(block: string, startMarker: string, endMarker: string): string {
|
|
38
|
+
const s = block.indexOf(startMarker);
|
|
39
|
+
if (s === -1) return "";
|
|
40
|
+
const e = block.indexOf(endMarker, s + startMarker.length);
|
|
41
|
+
return e === -1 ? "" : block.slice(s, e);
|
|
42
|
+
}
|
|
43
|
+
|
|
33
44
|
let passed = 0;
|
|
34
45
|
function ok(label: string, cond: boolean): void {
|
|
35
46
|
assert.ok(cond, label);
|
|
@@ -37,6 +48,126 @@ function ok(label: string, cond: boolean): void {
|
|
|
37
48
|
passed++;
|
|
38
49
|
}
|
|
39
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Collapse a model-facing string's SOURCE form into the text the model actually reads:
|
|
53
|
+
* drop TS string-concat glue between adjacent literals and normalize whitespace.
|
|
54
|
+
*/
|
|
55
|
+
function modelText(slice: string): string {
|
|
56
|
+
return slice
|
|
57
|
+
.replace(/["']\s*\+\s*["']/g, "")
|
|
58
|
+
.replace(/\s+/g, " ")
|
|
59
|
+
.trim();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* F-2 (2026-07-27): the two model-facing surfaces described the lock, `mode`, and the
|
|
64
|
+
* unsupported-citizen outcome set in ways the code does not support. Measured against
|
|
65
|
+
* source, not prose:
|
|
66
|
+
* - LOCK: `entwurf-v2-decider.ts` returns a held claim ONLY on the control-socket-domain
|
|
67
|
+
* branch — and that branch covers the live send AND the dormant cell's spawn-bg resume.
|
|
68
|
+
* The mailbox and native-push branches both carry `lock: null`. "socket paths take a
|
|
69
|
+
* per-target lock" read as "the socket transport locks", which got spawn-bg (a SEPARATE
|
|
70
|
+
* relaunch transport that nonetheless locks) and native-push (a live send that does NOT)
|
|
71
|
+
* exactly backwards.
|
|
72
|
+
* - MODE: `mode` exists on exactly ONE ExecutionPlan variant, `control-socket`. The
|
|
73
|
+
* meta-mailbox and native-push plans have no such field and spawn-bg says so in a comment.
|
|
74
|
+
* "mode applies to a live send" is false for native-push, which IS a live send.
|
|
75
|
+
* - THIRD RESULT: `resolveDispatch` downgrades the unsupported fire-and-forget cell to
|
|
76
|
+
* `mailbox-undeliverable` when the separate deliverability fact is false, so an
|
|
77
|
+
* `unsupported` citizen has THREE outcomes (mailbox / native-push / reject), not two.
|
|
78
|
+
*
|
|
79
|
+
* Applied to BOTH surfaces from one predicate so they cannot drift apart again.
|
|
80
|
+
*/
|
|
81
|
+
function assertRailSemantics(tag: string, longDescRaw: string, intentDescRaw: string, modeDescRaw: string): void {
|
|
82
|
+
// Compare the text the MODEL reads, not the shape the author happened to type. pi-native
|
|
83
|
+
// wraps inside one template literal; the MCP surface wraps by `+`-concatenating literals.
|
|
84
|
+
// Without this normalization the same sentence matches on one surface and not the other,
|
|
85
|
+
// and the assertion silently measures line-wrapping instead of meaning.
|
|
86
|
+
const longDesc = modelText(longDescRaw);
|
|
87
|
+
const intentDesc = modelText(intentDescRaw);
|
|
88
|
+
const modeDesc = modelText(modeDescRaw);
|
|
89
|
+
ok(
|
|
90
|
+
`${tag} — long description scopes the per-target lock to the control-socket DOMAIN [QK:V2SURF-MCP-LOCK-DOMAIN]`,
|
|
91
|
+
/control-socket-DOMAIN\s+dispatch/.test(longDesc) && /lock-free/.test(longDesc),
|
|
92
|
+
);
|
|
93
|
+
ok(
|
|
94
|
+
`${tag} — long description says spawn-bg ALSO runs under that domain's lock`,
|
|
95
|
+
/spawn-bg/.test(longDesc) && /still runs under that domain's lock/.test(longDesc),
|
|
96
|
+
);
|
|
97
|
+
ok(
|
|
98
|
+
`${tag} — long description says the mailbox AND native-push rails are lock-free`,
|
|
99
|
+
/mailbox and native-push rails are lock-free/.test(longDesc),
|
|
100
|
+
);
|
|
101
|
+
// NEGATIVE (exact false sentence): the shipped wording, whitespace-tolerant because the
|
|
102
|
+
// pi-native template literal wrapped it across a newline.
|
|
103
|
+
ok(
|
|
104
|
+
`${tag} — long description no longer says "socket paths … per-target lock"`,
|
|
105
|
+
!/socket\s+paths\s+(take a )?per-target lock/.test(longDesc),
|
|
106
|
+
);
|
|
107
|
+
ok(`${tag} — long description scopes mode to a CONTROL-SOCKET send`, /CONTROL-SOCKET send/.test(longDesc));
|
|
108
|
+
// NEGATIVE (exact false sentence), both shipped spellings.
|
|
109
|
+
ok(
|
|
110
|
+
`${tag} — long description no longer applies mode to "a live send"`,
|
|
111
|
+
!/\bfor a live send\b/.test(longDesc) && !/mode\/wants_reply apply to a live send/.test(longDesc),
|
|
112
|
+
);
|
|
113
|
+
ok(
|
|
114
|
+
`${tag} — long description names the THIRD result for an unsupported citizen`,
|
|
115
|
+
/mailbox-undeliverable/.test(longDesc) && /THIRD RESULT/.test(longDesc),
|
|
116
|
+
);
|
|
117
|
+
ok(
|
|
118
|
+
`${tag} — intent param names a rail's reject, not only its allow cell`,
|
|
119
|
+
/mailbox-undeliverable/.test(intentDesc) && /native-push-target-dead/.test(intentDesc),
|
|
120
|
+
);
|
|
121
|
+
// The native-push probe is THREE-valued (NATIVE_PUSH_DISPATCH_TABLE): alive / dead /
|
|
122
|
+
// indeterminate. Both surfaces collapsed it to two and labelled indeterminate as dead —
|
|
123
|
+
// reporting an unestablished probe as a measured death. Presence of both reason names is
|
|
124
|
+
// not enough: they must sit next to the liveness they belong to, or a text can list both
|
|
125
|
+
// names while still mapping them the wrong way round. So require ADJACENCY — the liveness
|
|
126
|
+
// word appears in the run of text immediately preceding its reason.
|
|
127
|
+
const adjacent = (text: string, liveness: string, reason: string): boolean => {
|
|
128
|
+
const at = text.indexOf(reason);
|
|
129
|
+
if (at < 0) return false;
|
|
130
|
+
return new RegExp(`\\b${liveness}\\b`, "i").test(text.slice(Math.max(0, at - 90), at));
|
|
131
|
+
};
|
|
132
|
+
for (const [which, text] of [
|
|
133
|
+
["long description", longDesc],
|
|
134
|
+
["intent param", intentDesc],
|
|
135
|
+
] as const) {
|
|
136
|
+
ok(
|
|
137
|
+
`${tag} — ${which} keeps the native-push probe 3-valued (dead ≠ indeterminate)`,
|
|
138
|
+
/native-push-target-dead/.test(text) && /native-push-probe-indeterminate/.test(text),
|
|
139
|
+
);
|
|
140
|
+
ok(
|
|
141
|
+
`${tag} — ${which} maps dead→native-push-target-dead and indeterminate→native-push-probe-indeterminate (adjacency, not just presence)`,
|
|
142
|
+
adjacent(text, "dead", "native-push-target-dead") &&
|
|
143
|
+
adjacent(text, "indeterminate", "native-push-probe-indeterminate"),
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
ok(`${tag} — mode param is isolated (non-vacuous)`, modeDesc.length > 60);
|
|
147
|
+
ok(
|
|
148
|
+
`${tag} — mode param scopes itself to a CONTROL-SOCKET send and denies the other rails [QK:V2SURF-PI-MODE-SCOPE]`,
|
|
149
|
+
/CONTROL-SOCKET send/.test(modeDesc) && /no mode/.test(modeDesc),
|
|
150
|
+
);
|
|
151
|
+
ok(
|
|
152
|
+
`${tag} — mode param no longer calls itself "Delivery mode for a live send"`,
|
|
153
|
+
!/Delivery mode for a live send/.test(modeDesc) && !/\bfor a live send\b/.test(modeDesc),
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Containment-disjointness is NOT enough to prove a description slice is tight. Round 5
|
|
159
|
+
* (2026-07-27) shipped a "long description" slice of 4,468 chars that SPANNED the parameter
|
|
160
|
+
* entries because a bare `description:` marker matched the `target` param first — and two
|
|
161
|
+
* over-wide slices can still be mutually non-containing. So pin the ABSENCE of every
|
|
162
|
+
* parameter marker in the long-description slice: if the slice ever swallows the schema
|
|
163
|
+
* again, this fails instead of silently satisfying the schema's own text.
|
|
164
|
+
*/
|
|
165
|
+
function assertLongDescExcludesParams(tag: string, longDesc: string, markers: readonly string[]): void {
|
|
166
|
+
for (const marker of markers) {
|
|
167
|
+
ok(`${tag} — long description slice excludes the \`${marker}\` parameter marker`, !longDesc.includes(marker));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
40
171
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
41
172
|
const REPO = path.join(HERE, "..");
|
|
42
173
|
const SURFACE_SRC = path.join(REPO, "pi-extensions/lib/entwurf-v2-surface.ts");
|
|
@@ -339,17 +470,87 @@ async function main(): Promise<void> {
|
|
|
339
470
|
"4: pi-native — NO static import of the self-address fence (TS5097 stays closed)",
|
|
340
471
|
!/import[^;]*from\s*"\.\/lib\/entwurf-self-address\.(js|ts)"/.test(code),
|
|
341
472
|
);
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
//
|
|
473
|
+
// Every SHIPPED rail must appear in the model-facing text. Measured 2026-07-27: both
|
|
474
|
+
// surfaces described only control-socket/spawn-bg/mailbox and told the model that an
|
|
475
|
+
// `unsupported` citizen is reached "→ mailbox" — false for Antigravity, whose native-push
|
|
476
|
+
// rail is intercepted BEFORE the mailbox mini-table and has no mailbox at all. A model
|
|
477
|
+
// reading that would pick mailbox semantics for a citizen that has none.
|
|
478
|
+
//
|
|
479
|
+
// Scope this to the registration block, NOT the whole file: the first version of this
|
|
480
|
+
// assertion swept `src`, so deleting the sentence from the description still passed on an
|
|
481
|
+
// unrelated `native-push` string elsewhere in the module. A rail-completeness claim that
|
|
482
|
+
// any import line can satisfy is not a claim.
|
|
483
|
+
const piV2Start = src.indexOf("function registerEntwurfV2Tool");
|
|
484
|
+
const piV2End = src.indexOf("\nfunction ", piV2Start + 1);
|
|
485
|
+
// Require a REAL end marker. `piV2End === -1 ? undefined : …` would silently widen the
|
|
486
|
+
// block to end-of-file and keep passing, which is the opposite of the fail-loud this
|
|
487
|
+
// scope exists for.
|
|
488
|
+
ok(
|
|
489
|
+
"4: pi-native — registration block is isolated (non-vacuous scope for the description checks)",
|
|
490
|
+
piV2Start !== -1 && piV2End > piV2Start,
|
|
491
|
+
);
|
|
492
|
+
const piV2Block = src.slice(piV2Start, piV2End);
|
|
493
|
+
ok("4: pi-native — registration block is substantial", piV2Block.length > 500);
|
|
494
|
+
// BLOCK SCOPE IS NOT ENOUGH — the two model-facing strings mask each other. Measured
|
|
495
|
+
// 2026-07-27 (round 4): the `intent` param description was corrected to split the two
|
|
496
|
+
// reject reasons while the LONG tool description still advertised the merged, false one,
|
|
497
|
+
// and this gate stayed green at 51 checks because both live in the same block and only
|
|
498
|
+
// ONE had to carry each literal. So slice them apart and require EACH to be true.
|
|
499
|
+
// The start marker MUST be unique to the long description. Measured 2026-07-27 (round 5):
|
|
500
|
+
// a bare `description:` matched the `target` PARAM first, so this "long description"
|
|
501
|
+
// slice was 4,468 chars spanning target + intent + the long text — the very masking the
|
|
502
|
+
// split was added to remove. An over-wide slice is a silent pass, so pin the exact
|
|
503
|
+
// backtick opening instead, and assert the two slices are actually disjoint.
|
|
504
|
+
const piLongDesc = sliceDescription(piV2Block, "description: `CANONICAL", "\n\t\tparameters:");
|
|
505
|
+
const piIntentDesc = sliceDescription(piV2Block, "intent: StringEnum(", "message:");
|
|
345
506
|
ok(
|
|
346
|
-
"4: pi-native —
|
|
347
|
-
|
|
507
|
+
"4: pi-native — long/intent slices are disjoint (neither can satisfy the other's claim)",
|
|
508
|
+
piLongDesc.length > 0 &&
|
|
509
|
+
piIntentDesc.length > 0 &&
|
|
510
|
+
!piLongDesc.includes(piIntentDesc) &&
|
|
511
|
+
!piIntentDesc.includes(piLongDesc),
|
|
348
512
|
);
|
|
513
|
+
for (const [what, text] of [
|
|
514
|
+
["long tool description", piLongDesc],
|
|
515
|
+
["intent param description", piIntentDesc],
|
|
516
|
+
] as const) {
|
|
517
|
+
ok(`4: pi-native — ${what} is isolated (non-vacuous)`, text.length > 120);
|
|
518
|
+
ok(`4: pi-native — ${what} names the native-push rail`, /native-push/.test(text));
|
|
519
|
+
// The two no-resume-authority rejects are DIFFERENT reasons: a native-push backend IS
|
|
520
|
+
// probe-measured, so calling its reject `backend-liveness-unsupported` is the exact lie
|
|
521
|
+
// `entwurf-v2-contract.ts:143` warns about in so many words.
|
|
522
|
+
ok(
|
|
523
|
+
`4: pi-native — ${what} separates the self-fetch and native-push owned rejects`,
|
|
524
|
+
/backend-liveness-unsupported/.test(text) && /native-push-no-resume-authority/.test(text),
|
|
525
|
+
);
|
|
526
|
+
// Direct tripwire for the exact sentence that shipped: merging the two backends under
|
|
527
|
+
// one reason. Presence-only pins cannot catch this — the merged claim can sit right
|
|
528
|
+
// beside the correct literals.
|
|
529
|
+
ok(
|
|
530
|
+
`4: pi-native — ${what} never merges the two backends under one reject reason`,
|
|
531
|
+
!/self-fetch and native-push alike/.test(text),
|
|
532
|
+
);
|
|
533
|
+
// Caller-intent steer (live-peer owned-outcome bug): owned-outcome is dormant-only and
|
|
534
|
+
// NEVER auto-converted. Pinned PER STRING — the file-wide version of this check could
|
|
535
|
+
// be satisfied by whichever description still carried it.
|
|
536
|
+
ok(
|
|
537
|
+
`4: pi-native — ${what} says owned-outcome is dormant-only + never auto-converted`,
|
|
538
|
+
/DORMANT socket-domain citizen/.test(text) && /auto-converted/.test(text),
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
ok("4: pi-native — long description denies native-push a mailbox", /NO mailbox/.test(piLongDesc));
|
|
349
542
|
ok(
|
|
350
|
-
"4: pi-native — description
|
|
351
|
-
/
|
|
543
|
+
"4: pi-native — long description steers live/alive peer → fire-and-forget",
|
|
544
|
+
/liveness=alive/.test(piLongDesc) && /fire-and-forget/.test(piLongDesc),
|
|
352
545
|
);
|
|
546
|
+
assertLongDescExcludesParams("4: pi-native", piLongDesc, [
|
|
547
|
+
"target: Type.String(",
|
|
548
|
+
"intent: StringEnum(",
|
|
549
|
+
"mode: Type.Optional(",
|
|
550
|
+
"wants_reply: Type.Optional(",
|
|
551
|
+
]);
|
|
552
|
+
const piModeDesc = sliceDescription(piV2Block, "mode: Type.Optional(", "wants_reply:");
|
|
553
|
+
assertRailSemantics("4: pi-native", piLongDesc, piIntentDesc, piModeDesc);
|
|
353
554
|
}
|
|
354
555
|
|
|
355
556
|
// ── 5: MCP bridge wiring guard ────────────────────────────────────────────
|
|
@@ -369,7 +570,16 @@ async function main(): Promise<void> {
|
|
|
369
570
|
// adapter, and must NOT itself reach for a legacy transport (the decider routes).
|
|
370
571
|
const v2Start = src.indexOf('"entwurf_v2"');
|
|
371
572
|
const after = src.indexOf("server.tool(", v2Start + 1);
|
|
372
|
-
|
|
573
|
+
// F-6: require a REAL end boundary, the same fail-loud rule block 4 already applies.
|
|
574
|
+
// `after === -1 ? undefined : after` meant that the moment the NEXT `server.tool(` was
|
|
575
|
+
// renamed, reordered, or removed, this block silently widened to end-of-file — and every
|
|
576
|
+
// "must NOT appear in the v2 block" assertion below would then be reading other tools'
|
|
577
|
+
// text. A boundary that disappears has to fail, not grow.
|
|
578
|
+
ok(
|
|
579
|
+
"5: MCP — v2 registration block has a REAL end boundary (next server.tool present)",
|
|
580
|
+
v2Start !== -1 && after > v2Start,
|
|
581
|
+
);
|
|
582
|
+
const v2Block = src.slice(v2Start, after);
|
|
373
583
|
ok("5: MCP — v2 handler builds buildSendSenderEnvelope()", /buildSendSenderEnvelope\(\)/.test(v2Block));
|
|
374
584
|
ok("5: MCP — v2 handler passes senderProvider: () => sender", /senderProvider:\s*\(\)\s*=>\s*sender/.test(v2Block));
|
|
375
585
|
ok(
|
|
@@ -380,15 +590,118 @@ async function main(): Promise<void> {
|
|
|
380
590
|
"5: MCP — v2 handler routes through the runner, NOT legacy rpcCall/enqueueMetaMessage",
|
|
381
591
|
!/\brpcCall\(/.test(v2Block) && !/\benqueueMetaMessage\(/.test(v2Block),
|
|
382
592
|
);
|
|
383
|
-
//
|
|
384
|
-
//
|
|
593
|
+
// Same shipped-rail completeness rule as the pi-native surface (4), and split the SAME
|
|
594
|
+
// two ways: the long tool description and the `intent` param description must EACH be
|
|
595
|
+
// true on their own (see the round-4 masking note in block 4).
|
|
596
|
+
// Same fail-loud rule as block 4: a missing marker would make `slice(0, -1)` return
|
|
597
|
+
// nearly the whole block and sail past the length guard, so assert the boundary first.
|
|
598
|
+
const mcpSchemaStart = v2Block.indexOf("\t{");
|
|
599
|
+
ok("5: MCP — schema boundary marker is present (slice cannot silently widen)", mcpSchemaStart > 0);
|
|
600
|
+
const mcpLongDesc = v2Block.slice(0, mcpSchemaStart);
|
|
601
|
+
const mcpIntentDesc = sliceDescription(v2Block, "intent: z", "message: z");
|
|
602
|
+
ok(
|
|
603
|
+
"5: MCP — long/intent slices are disjoint (neither can satisfy the other's claim)",
|
|
604
|
+
mcpLongDesc.length > 0 &&
|
|
605
|
+
mcpIntentDesc.length > 0 &&
|
|
606
|
+
!mcpLongDesc.includes(mcpIntentDesc) &&
|
|
607
|
+
!mcpIntentDesc.includes(mcpLongDesc),
|
|
608
|
+
);
|
|
609
|
+
for (const [what, text] of [
|
|
610
|
+
["long tool description", mcpLongDesc],
|
|
611
|
+
["intent param description", mcpIntentDesc],
|
|
612
|
+
] as const) {
|
|
613
|
+
ok(`5: MCP — ${what} is isolated (non-vacuous)`, text.length > 120);
|
|
614
|
+
ok(`5: MCP — ${what} names the native-push rail`, /native-push/.test(text));
|
|
615
|
+
ok(
|
|
616
|
+
`5: MCP — ${what} separates the self-fetch and native-push owned rejects [QK:V2SURF-MERGED-REJECT]`,
|
|
617
|
+
/backend-liveness-unsupported/.test(text) && /native-push-no-resume-authority/.test(text),
|
|
618
|
+
);
|
|
619
|
+
ok(
|
|
620
|
+
`5: MCP — ${what} never merges the two backends under one reject reason`,
|
|
621
|
+
!/self-fetch and native-push alike/.test(text),
|
|
622
|
+
);
|
|
623
|
+
// Same per-string caller-intent pin as block 4 (a sibling reaching in over MCP reads
|
|
624
|
+
// THIS description, not the pi-native one).
|
|
625
|
+
ok(
|
|
626
|
+
`5: MCP — ${what} says owned-outcome is dormant-only + never auto-converted`,
|
|
627
|
+
/DORMANT socket-domain citizen/.test(text) && /auto-converted/.test(text),
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
ok("5: MCP — long description denies native-push a mailbox", /NO mailbox/.test(mcpLongDesc));
|
|
631
|
+
ok(
|
|
632
|
+
"5: MCP — long description steers live/alive peer → fire-and-forget",
|
|
633
|
+
/liveness=alive/.test(mcpLongDesc) && /fire-and-forget/.test(mcpLongDesc),
|
|
634
|
+
);
|
|
635
|
+
assertLongDescExcludesParams("5: MCP", mcpLongDesc, ["target: z", "intent: z", "mode: z", "wants_reply: z"]);
|
|
636
|
+
const mcpModeDesc = sliceDescription(v2Block, "mode: z", "wants_reply: z");
|
|
637
|
+
assertRailSemantics("5: MCP", mcpLongDesc, mcpIntentDesc, mcpModeDesc);
|
|
638
|
+
|
|
639
|
+
// entwurf_peers is a FACT surface, but it still told the model what `unsupported` means —
|
|
640
|
+
// and "does NOT mean unreachable" is false for a record whose backend has no adapter on
|
|
641
|
+
// this lane (codex, or a self-fetch citizen that has exited): `resolveDispatch` rejects it
|
|
642
|
+
// as mailbox-undeliverable. Sliced with the same real-boundary rule as the v2 block.
|
|
643
|
+
const peersStart = src.indexOf('"entwurf_peers"');
|
|
644
|
+
const peersAfter = src.indexOf("server.tool(", peersStart + 1);
|
|
645
|
+
ok(
|
|
646
|
+
"5: MCP — entwurf_peers block has a REAL end boundary (next server.tool present)",
|
|
647
|
+
peersStart !== -1 && peersAfter > peersStart,
|
|
648
|
+
);
|
|
649
|
+
const peersBlock = src.slice(peersStart, peersAfter);
|
|
650
|
+
ok("5: MCP — entwurf_peers block is isolated (non-vacuous)", peersBlock.length > 200);
|
|
651
|
+
ok(
|
|
652
|
+
"5: MCP — entwurf_peers names the THIRD answer (a reject), not just the two allow rails",
|
|
653
|
+
/mailbox-undeliverable/.test(peersBlock) && /native-push-target-dead/.test(peersBlock),
|
|
654
|
+
);
|
|
655
|
+
ok(
|
|
656
|
+
"5: MCP — entwurf_peers no longer claims `unsupported` does NOT mean unreachable",
|
|
657
|
+
!/does NOT mean unreachable/.test(peersBlock),
|
|
658
|
+
);
|
|
659
|
+
|
|
660
|
+
// F-7: entwurf_inbox_read described itself as draining "your own" inbox while the handler
|
|
661
|
+
// passes the CALLER-SUPPLIED gardenId straight to readMetaInbox with no comparison against
|
|
662
|
+
// an authoritative self identity. README/setup-clean-host deliberately keep that surface open
|
|
663
|
+
// (a plain external host with no garden record may call the read tools), so the honest repair
|
|
664
|
+
// is the description, not a new guard — but a false access claim must not survive either.
|
|
665
|
+
const inboxStart = src.indexOf('"entwurf_inbox_read"');
|
|
666
|
+
const inboxAfter = src.indexOf("server.tool(", inboxStart + 1);
|
|
667
|
+
ok(
|
|
668
|
+
"5: MCP — entwurf_inbox_read block has a REAL end boundary (next server.tool present)",
|
|
669
|
+
inboxStart !== -1 && inboxAfter > inboxStart,
|
|
670
|
+
);
|
|
671
|
+
const inboxBlock = src.slice(inboxStart, inboxAfter);
|
|
672
|
+
const inboxText = modelText(inboxBlock);
|
|
673
|
+
ok("5: MCP — entwurf_inbox_read block is isolated (non-vacuous)", inboxText.length > 200);
|
|
674
|
+
ok(
|
|
675
|
+
"5: MCP — entwurf_inbox_read states the garden id is CALLER-SUPPLIED and unverified [QK:V2SURF-INBOX-SCOPE-HONESTY]",
|
|
676
|
+
/CALLER-SUPPLIED/.test(inboxText) && /NOT verified against your own identity/.test(inboxText),
|
|
677
|
+
);
|
|
678
|
+
ok(
|
|
679
|
+
"5: MCP — entwurf_inbox_read warns that a foreign id drains THEIR mail and stamps THEIR receipt",
|
|
680
|
+
/drains THEIR mail and stamps THEIR receipt/.test(inboxText),
|
|
681
|
+
);
|
|
682
|
+
// NEGATIVE (exact false sentence): the shipped "your own inbox" access claim.
|
|
683
|
+
ok(
|
|
684
|
+
"5: MCP — entwurf_inbox_read no longer claims it drains `your own` inbox",
|
|
685
|
+
!/your own meta-bridge inbox/.test(inboxText) && !/receipt on your meta-record/.test(inboxText),
|
|
686
|
+
);
|
|
687
|
+
// The description↔handler agreement has to be pinned on the HANDLER's side too, and
|
|
688
|
+
// `readMetaInbox({ gardenId })` alone does not do it: a handler could resolve its
|
|
689
|
+
// authoritative self and enforce equality FIRST and still end in that same call, leaving
|
|
690
|
+
// this row green while the description's "not verified" sentence became false. So require
|
|
691
|
+
// the pass-through POSITIVELY *and* require the absence of any identity-equality guard.
|
|
692
|
+
// If a future change decides to enforce own-inbox (a behaviour change — see NEXT §QUEUE 1),
|
|
693
|
+
// this goes red and forces the description to change with it.
|
|
385
694
|
ok(
|
|
386
|
-
"5: MCP —
|
|
387
|
-
/
|
|
695
|
+
"5: MCP — entwurf_inbox_read passes the caller's gardenId straight to readMetaInbox",
|
|
696
|
+
/readMetaInbox\(\{ gardenId \}\)/.test(inboxBlock),
|
|
388
697
|
);
|
|
698
|
+
const inboxIdentityGuard =
|
|
699
|
+
/buildAuthoritativeSelfEnvelope|buildTrustedMetaSenderEnvelope|resolveTrustedMetaSenderIdentity/.test(
|
|
700
|
+
inboxBlock,
|
|
701
|
+
) || /sender\.sessionId|self\.envelope|envelope\.sessionId/.test(inboxBlock);
|
|
389
702
|
ok(
|
|
390
|
-
"5: MCP —
|
|
391
|
-
|
|
703
|
+
"5: MCP — entwurf_inbox_read resolves NO authoritative self identity (so 'caller-supplied, not verified' is true)",
|
|
704
|
+
!inboxIdentityGuard,
|
|
392
705
|
);
|
|
393
706
|
}
|
|
394
707
|
|