@junghanacs/entwurf 0.12.9 → 0.12.10
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 +134 -248
- package/BASELINE.md +1 -1
- package/CHANGELOG.md +19 -0
- package/DELIVERY.md +4 -3
- package/README.md +26 -13
- package/VERIFY.md +3 -2
- 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/overlay.js +5 -3
- 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 +5 -3
- package/pi-extensions/entwurf-control.ts +69 -27
- package/pi-extensions/lib/acp/overlay.ts +5 -3
- 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 +48 -41
- 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-sdk-surface.ts +22 -11
- 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 +785 -0
- package/scripts/check-meta-identity-consumers.ts +1 -1
- package/scripts/check-shell-quote.ts +2 -1
- package/scripts/lib/mutation-qualify.ts +794 -0
- package/scripts/mutants/acp-augment.json +30 -0
- package/scripts/mutants/agy-permission.json +144 -0
- package/scripts/mutants/meta-identity.json +17 -0
- package/scripts/mutants/self-address.json +59 -0
- package/scripts/mutants/v2-surface.json +87 -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
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* check-entwurf-deliverability — deterministic gate for the conversational-mailbox
|
|
3
|
-
* deliverability predicate (SE-1/SE-2 slice 2c). This is the predicate
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* (SE-
|
|
3
|
+
* deliverability predicate (SE-1/SE-2 slice 2c). This is the predicate an enqueue site
|
|
4
|
+
* must consult in slice 2d before writing a .msg, so a reply never rots in a dead
|
|
5
|
+
* session's mailbox (SE-2) and is never enqueued for a backend that has no mailbox drain
|
|
6
|
+
* (SE-1, pi). The SHIPPED sites are the v2 decider and its send-fallback re-resolve; the
|
|
7
|
+
* v1 fallback / MCP v1 / pi-native v1 sites were removed in the 0.12 cutover.
|
|
7
8
|
*
|
|
8
9
|
* Proves:
|
|
9
10
|
* - computeMetaReceiverActive: the shared atom — active ⟺ recordBacked ∧ ownerAlive ∧
|
|
@@ -124,8 +125,10 @@ ok(
|
|
|
124
125
|
|
|
125
126
|
// ── receiverMarkerMatchesIdentity: the marker ↔ identity SSOT (SE-2 2d-3) ───
|
|
126
127
|
// A present marker only raises a receiver to active when it agrees with the record on
|
|
127
|
-
// garden/backend/native id. The
|
|
128
|
-
// both route through this one helper, so presence-only false-positives are
|
|
128
|
+
// garden/backend/native id. The v2 production deliverability seam and the MCP bridge's
|
|
129
|
+
// entwurf_self both route through this one helper, so presence-only false-positives are
|
|
130
|
+
// closed on every shipped path — and there is no second implementation to drift from since
|
|
131
|
+
// entwurf-mailbox-guard.ts (production importers: 0) was deleted 2026-07-27.
|
|
129
132
|
const ID = { gardenId: "20260612T100000-aaaaaa", backend: "claude-code", nativeSessionId: "n-1" };
|
|
130
133
|
ok(
|
|
131
134
|
"marker matches identity on all three axes → true",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* into ONE `record-less-socket` diagnostic (#50 C4: a diagnostic subject,
|
|
9
9
|
* never a listing section) whose message names the cause + fix (fresh-cut),
|
|
10
10
|
* - corrupt record → meta-record-read-error diagnostic, listing NOT blinded,
|
|
11
|
-
* - gardenId↔socket collision (
|
|
11
|
+
* - gardenId↔socket collision (out-of-socket-domain citizen + same-gid socket) → BOTH sides
|
|
12
12
|
* quarantined (gid in neither peers nor socketOnly) + one
|
|
13
13
|
* garden-id-socket-conflict diagnostic, and listEntwurfFacts does NOT throw
|
|
14
14
|
* (expected external-state corruption → diagnostics, not a crash),
|
|
@@ -214,16 +214,16 @@ async function main(): Promise<void> {
|
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
// ──
|
|
218
|
-
//
|
|
217
|
+
// ── out-of-socket-domain citizen + symlinked socket (closed gap) ──────────
|
|
218
|
+
// Such a record shares its gid with a *symlinked* `*.sock`. The symlink is
|
|
219
219
|
// never probed, so its gid is absent from socketGids — the old socketGids-only
|
|
220
|
-
// check missed this and the
|
|
220
|
+
// check missed this and the out-of-domain citizen survived as a clean PeerFact while
|
|
221
221
|
// the legacy send path still followed the symlink to a forged receiver. The
|
|
222
|
-
// shared
|
|
222
|
+
// shared isOutOfSocketDomainGardenIdConflict predicate unions socketGids with the
|
|
223
223
|
// symlinked gids, so the citizen is now quarantined (gid in NEITHER peers nor
|
|
224
224
|
// socketOnly) and the conflict diagnostic is raised — alongside the existing
|
|
225
225
|
// socket-symlink-rejected one (both are honest: symlink-rejected = "not probed",
|
|
226
|
-
// conflict = "
|
|
226
|
+
// conflict = "out-of-socket-domain address split").
|
|
227
227
|
{
|
|
228
228
|
const r = await listEntwurfFacts(
|
|
229
229
|
deps(
|
|
@@ -233,7 +233,7 @@ async function main(): Promise<void> {
|
|
|
233
233
|
),
|
|
234
234
|
);
|
|
235
235
|
ok(
|
|
236
|
-
"symlink-collision:
|
|
236
|
+
"symlink-collision: out-of-socket-domain citizen quarantined (NOT in peers)",
|
|
237
237
|
!r.facts.peers.some((p) => p.gardenId === GID_CONFLICT),
|
|
238
238
|
);
|
|
239
239
|
ok(
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
import assert from "node:assert/strict";
|
|
24
24
|
import {
|
|
25
25
|
type FactList,
|
|
26
|
-
|
|
26
|
+
isOutOfSocketDomainGardenIdConflict,
|
|
27
27
|
type PeerFact,
|
|
28
28
|
type RecordLessSocketFact,
|
|
29
29
|
resolveFactList,
|
|
@@ -247,7 +247,7 @@ const GID_SOCKET_ONLY = "20260611T135517-5f0d25";
|
|
|
247
247
|
} catch {
|
|
248
248
|
threw = true;
|
|
249
249
|
}
|
|
250
|
-
ok("dedup/authority:
|
|
250
|
+
ok("dedup/authority: out-of-socket-domain citizen + same-gid socket → fail-loud", threw);
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
// ── dedup (동결3): pi citizen + its socket → PeerFact only, never both ────────
|
|
@@ -333,11 +333,11 @@ const GID_SOCKET_ONLY = "20260611T135517-5f0d25";
|
|
|
333
333
|
);
|
|
334
334
|
}
|
|
335
335
|
|
|
336
|
-
// ──
|
|
336
|
+
// ── isOutOfSocketDomainGardenIdConflict: shared record-side predicate ──────
|
|
337
337
|
// (fact-provider listing + v2 decider dispatch consume this same fn — 4c 재유도
|
|
338
338
|
// 금지 동형). The union over socketGids ∪ symlinkedGardenIds is the fact-provider:125
|
|
339
339
|
// gap closure: a symlinked socket is never probed, so its gid is absent from
|
|
340
|
-
// socketGids; the predicate must STILL flag
|
|
340
|
+
// socketGids; the predicate must STILL flag an out-of-domain citizen owning it.
|
|
341
341
|
{
|
|
342
342
|
const G = "20260611T444444-dddddd";
|
|
343
343
|
const realSockets = new Set([G]);
|
|
@@ -345,21 +345,21 @@ const GID_SOCKET_ONLY = "20260611T135517-5f0d25";
|
|
|
345
345
|
const empty = new Set<string>();
|
|
346
346
|
for (const piBackend of IN_DOMAIN) {
|
|
347
347
|
ok(
|
|
348
|
-
`conflict-predicate:
|
|
349
|
-
!
|
|
348
|
+
`conflict-predicate: socket-domain (${piBackend}) is not a record-side conflict`,
|
|
349
|
+
!isOutOfSocketDomainGardenIdConflict(piBackend, G, realSockets, symlinked),
|
|
350
350
|
);
|
|
351
351
|
}
|
|
352
352
|
ok(
|
|
353
|
-
"conflict-predicate:
|
|
354
|
-
|
|
353
|
+
"conflict-predicate: out-of-socket-domain + real socket → conflict",
|
|
354
|
+
isOutOfSocketDomainGardenIdConflict("claude-code", G, realSockets, empty),
|
|
355
355
|
);
|
|
356
356
|
ok(
|
|
357
|
-
"conflict-predicate:
|
|
358
|
-
|
|
357
|
+
"conflict-predicate: out-of-socket-domain + symlinked socket → conflict",
|
|
358
|
+
isOutOfSocketDomainGardenIdConflict("claude-code", G, empty, symlinked),
|
|
359
359
|
);
|
|
360
360
|
ok(
|
|
361
|
-
"conflict-predicate:
|
|
362
|
-
!
|
|
361
|
+
"conflict-predicate: out-of-socket-domain + no socket → no conflict",
|
|
362
|
+
!isOutOfSocketDomainGardenIdConflict("claude-code", G, empty, empty),
|
|
363
363
|
);
|
|
364
364
|
}
|
|
365
365
|
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* check-entwurf-resume-args — deterministic gate for the 5c-3b resume-argv SSOT
|
|
3
|
-
* (`buildResumePiArgs`). It pins the
|
|
4
|
-
*
|
|
3
|
+
* (`buildResumePiArgs`). It pins the RESIDENT citizen launch shape so it cannot drift.
|
|
4
|
+
* This gate once carried a second `legacy` one-shot variant as a contrast; that launcher
|
|
5
|
+
* and its enum member were removed 2026-07-27, and the half of A1 that still ships is
|
|
6
|
+
* asserted directly instead — a resumed citizen stands its control socket up and never
|
|
7
|
+
* emits the one-shot `--no-extensions`:
|
|
5
8
|
*
|
|
6
|
-
* 1. legacy carries `--no-extensions` and NO `--entwurf-control`.
|
|
7
9
|
* 2. v2-control carries `--entwurf-control` and NO `--no-extensions`.
|
|
8
|
-
* 3.
|
|
9
|
-
*
|
|
10
|
-
* 4. `explicitExtensionArgs` is preserved verbatim, exactly once
|
|
10
|
+
* 3. The headless prefix `--mode json -p` stays and the prompt is the final positional
|
|
11
|
+
* (the prompt-as-turn authority is unchanged in v2 — `-p` is NOT dropped).
|
|
12
|
+
* 4. `explicitExtensionArgs` is preserved verbatim, exactly once
|
|
11
13
|
* (load-bearing for a recorded `provider=entwurf` resume; #29 footgun).
|
|
12
|
-
* 5.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* 7. no cross-contamination: the legacy-only and v2-only flags never leak into the other.
|
|
14
|
+
* 5. `plan.launchArgs` (`--approve` / empty) ride as flags BEFORE the prompt.
|
|
15
|
+
* 6. a null/undefined provider emits NO `--provider` flag; `--model <m>` and `<prompt>`
|
|
16
|
+
* are the last three tokens.
|
|
17
|
+
* 7. the resident posture is exact: `--entwurf-control` present, `--no-extensions` never.
|
|
17
18
|
*
|
|
18
19
|
* Pure string assembly — no IO, no spawn.
|
|
19
20
|
*/
|
|
@@ -45,34 +46,6 @@ function valueAfter(args: readonly string[], flag: string): string | undefined {
|
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
function main(): void {
|
|
48
|
-
// ── legacy variant ──────────────────────────────────────────────────────────────
|
|
49
|
-
{
|
|
50
|
-
const args = buildResumePiArgs({
|
|
51
|
-
variant: "legacy",
|
|
52
|
-
sessionFile: SESSION_FILE,
|
|
53
|
-
explicitExtensionArgs: EXT,
|
|
54
|
-
provider: "entwurf",
|
|
55
|
-
model: "claude-opus-5",
|
|
56
|
-
prompt: "continue the task",
|
|
57
|
-
});
|
|
58
|
-
ok("1 legacy has --no-extensions", args.includes("--no-extensions"));
|
|
59
|
-
ok("1 legacy has NO --entwurf-control", !args.includes("--entwurf-control"));
|
|
60
|
-
ok("3 legacy headless prefix --mode json -p", args[0] === "--mode" && args[1] === "json" && args[2] === "-p");
|
|
61
|
-
ok("3 legacy prompt is the final positional", args[args.length - 1] === "continue the task");
|
|
62
|
-
ok("4 legacy keeps ext args exactly once", args.filter((a) => a === "-e").length === 1);
|
|
63
|
-
ok("6 legacy provider laid out", valueAfter(args, "--provider") === "entwurf");
|
|
64
|
-
ok("6 legacy model laid out", valueAfter(args, "--model") === "claude-opus-5");
|
|
65
|
-
ok("6 legacy resumes by exact FILE (--session <abs path>)", valueAfter(args, "--session") === SESSION_FILE);
|
|
66
|
-
ok("6 legacy carries NO --session-id (the id is pi's own now)", !args.includes("--session-id"));
|
|
67
|
-
// model + prompt are the last three tokens: --model <m> <prompt>
|
|
68
|
-
ok(
|
|
69
|
-
"6 legacy --model <m> <prompt> tail",
|
|
70
|
-
args[args.length - 3] === "--model" &&
|
|
71
|
-
args[args.length - 2] === "claude-opus-5" &&
|
|
72
|
-
args[args.length - 1] === "continue the task",
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
49
|
// ── v2-control variant ──────────────────────────────────────────────────────────
|
|
77
50
|
{
|
|
78
51
|
const args = buildResumePiArgs({
|
|
@@ -107,20 +80,6 @@ function main(): void {
|
|
|
107
80
|
);
|
|
108
81
|
}
|
|
109
82
|
|
|
110
|
-
// ── 5. legacy IGNORES launchArgs (no --approve leaks in) ──────────────────────────
|
|
111
|
-
{
|
|
112
|
-
const args = buildResumePiArgs({
|
|
113
|
-
variant: "legacy",
|
|
114
|
-
sessionFile: SESSION_FILE,
|
|
115
|
-
explicitExtensionArgs: [],
|
|
116
|
-
provider: null,
|
|
117
|
-
model: "m",
|
|
118
|
-
prompt: "p",
|
|
119
|
-
launchArgs: ["--approve"], // present in input but legacy must ignore it
|
|
120
|
-
});
|
|
121
|
-
ok("5 legacy ignores launchArgs (no --approve)", !args.includes("--approve"));
|
|
122
|
-
}
|
|
123
|
-
|
|
124
83
|
// ── 6. null/undefined provider emits NO --provider flag ───────────────────────────
|
|
125
84
|
for (const provider of [null, undefined] as const) {
|
|
126
85
|
const args = buildResumePiArgs({
|
|
@@ -136,20 +95,23 @@ function main(): void {
|
|
|
136
95
|
ok(`6 provider=${provider}: --model <m> <prompt> still tail`, args.slice(-3).join(" ") === "--model m p");
|
|
137
96
|
}
|
|
138
97
|
|
|
139
|
-
// ── 7.
|
|
98
|
+
// ── 7. the resident posture is exact: the one-shot flag never reappears ───────────
|
|
99
|
+
// The `legacy` variant was removed 2026-07-27, so the old cross-contamination pair is
|
|
100
|
+
// gone. What must NOT rot is the A1 half that still ships: a resumed citizen stands its
|
|
101
|
+
// control socket up, which means `--no-extensions` (the one-shot flag that let `pi -p`
|
|
102
|
+
// exit) can never be emitted here again.
|
|
140
103
|
{
|
|
141
|
-
const
|
|
104
|
+
const v2 = buildResumePiArgs({
|
|
142
105
|
sessionFile: SESSION_FILE,
|
|
143
106
|
explicitExtensionArgs: EXT,
|
|
144
107
|
provider: "entwurf",
|
|
145
108
|
model: "m",
|
|
146
109
|
prompt: "p",
|
|
147
110
|
launchArgs: ["--approve"],
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
ok("7
|
|
152
|
-
ok("7 v2-only flag absent from legacy", !legacy.includes("--entwurf-control") && !legacy.includes("--approve"));
|
|
111
|
+
variant: "v2-control",
|
|
112
|
+
});
|
|
113
|
+
ok("7 resident posture never emits the one-shot --no-extensions", !v2.includes("--no-extensions"));
|
|
114
|
+
ok("7 resident posture stands the control socket up", v2.includes("--entwurf-control"));
|
|
153
115
|
}
|
|
154
116
|
|
|
155
117
|
console.log(`\ncheck-entwurf-resume-args: ${passed} checks passed`);
|
|
@@ -7,8 +7,11 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Proves:
|
|
9
9
|
* - PURE truth table (computeSelfAddressability, facts injected): pi replyable ⟺
|
|
10
|
-
* socketAlive;
|
|
11
|
-
*
|
|
10
|
+
* socketAlive; external never replyable; and meta replyability splits by RAIL —
|
|
11
|
+
* self-fetch ⟺ recordBacked ∧ ownerAlive ∧ watchArmed, native-push ⟺ recordBacked ∧
|
|
12
|
+
* probeAlive (a separate axis: an agy citizen never arms a mailbox watch, so the
|
|
13
|
+
* self-fetch atom would make it un-replyable forever), and an unsupplied rail is
|
|
14
|
+
* fail-closed. socketState alive/expected/none is its own assertable field.
|
|
12
15
|
* - The two REGRESSION-PROOF rows the lock requires (record-present, not all-absent):
|
|
13
16
|
* (b) meta record present + owner-dead (start-key mismatch) → false
|
|
14
17
|
* (c) meta record present + watch-unarmed → false
|
|
@@ -147,7 +150,7 @@ function row(facts: SelfAddressabilityFacts): { replyable: boolean; socketState:
|
|
|
147
150
|
|
|
148
151
|
// A native-push citizen must NOT be able to buy replyability with mailbox facts.
|
|
149
152
|
ok(
|
|
150
|
-
"meta/native-push + owner-alive + watch-armed but probe-dead → NOT replyable (mailbox facts cannot rescue it)",
|
|
153
|
+
"meta/native-push + owner-alive + watch-armed but probe-dead → NOT replyable (mailbox facts cannot rescue it) [QK:SELFADDR-RAIL-FACT-LEAK]",
|
|
151
154
|
row(push({ recordBacked: true, ownerAlive: true, watchArmed: true, probeAlive: false })).replyable === false,
|
|
152
155
|
);
|
|
153
156
|
}
|
|
@@ -223,7 +226,10 @@ const piBody = functionBody("buildStrictPiSenderEnvelope");
|
|
|
223
226
|
ok("buildStrictPiSenderEnvelope calls computeSelfAddressability", /computeSelfAddressability\s*\(/.test(piBody));
|
|
224
227
|
// Scoped to THIS function body (not a broad grep): the pi-session envelope must NOT
|
|
225
228
|
// hardcode `replyable: true`; it must derive from the predicate result.
|
|
226
|
-
ok(
|
|
229
|
+
ok(
|
|
230
|
+
"buildStrictPiSenderEnvelope no longer hardcodes `replyable: true` [QK:SELFADDR-NO-HARDCODED-REPLYABLE]",
|
|
231
|
+
!/replyable:\s*true/.test(piBody),
|
|
232
|
+
);
|
|
227
233
|
// existsSync alone is too loose — pin that it probes the CANONICAL socket path,
|
|
228
234
|
// not some other file, so the honesty signal cannot drift to a path that does not
|
|
229
235
|
// represent this session's socket.
|
|
@@ -246,6 +252,183 @@ ok(
|
|
|
246
252
|
/existsSync\s*\(/.test(selfRegion),
|
|
247
253
|
);
|
|
248
254
|
|
|
255
|
+
// ── SLICE INTEGRITY (before any claim is read off the slice) ─────────────────
|
|
256
|
+
// A source-slice assertion is only as good as its boundaries. `toolRegion` counts
|
|
257
|
+
// parens, so an unbalanced paren inside a string literal would silently widen the
|
|
258
|
+
// slice to a LATER tool and let a neighbour's text satisfy an assertion about this
|
|
259
|
+
// one. Pin both ends explicitly instead of trusting the counter: the slice must
|
|
260
|
+
// contain THIS tool's own error label and must NOT reach the next `server.tool(`.
|
|
261
|
+
ok("entwurf_self slice contains its own handler tail (start boundary real)", /entwurf_self error:/.test(selfRegion));
|
|
262
|
+
ok(
|
|
263
|
+
"entwurf_self slice stops before the next tool (end boundary real, no silent widen)",
|
|
264
|
+
!/entwurf_peers/.test(selfRegion) && !/entwurf_inbox_read/.test(selfRegion),
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
// ── F-1: entwurf_self must render the RAIL, never a universal mailbox ────────
|
|
268
|
+
// The defect: `origin === "meta-session"` unconditionally synthesized
|
|
269
|
+
// `<mailboxDir>/<gardenId>`. `origin` is sender PROVENANCE; the rail is the second
|
|
270
|
+
// axis. A native-push citizen (antigravity) has NO mailbox at all (AGENTS Hard Rule
|
|
271
|
+
// 10, VERIFY "No mailbox/receiver-marker evidence counts on this rail"), so that
|
|
272
|
+
// branch printed a path that will never exist and taught the model mailbox semantics
|
|
273
|
+
// its own rail does not have. Pinned PER RAIL, as one reusable predicate so the same
|
|
274
|
+
// judgement can be driven over negative controls below.
|
|
275
|
+
/** Brace-balanced body of the `if (<guard>) { … }` that follows `guard`, or null. */
|
|
276
|
+
function branchBody(region: string, guard: string): string | null {
|
|
277
|
+
const at = region.indexOf(guard);
|
|
278
|
+
if (at < 0) return null;
|
|
279
|
+
const open = region.indexOf("{", at);
|
|
280
|
+
if (open < 0) return null;
|
|
281
|
+
let depth = 0;
|
|
282
|
+
for (let i = open; i < region.length; i++) {
|
|
283
|
+
const c = region[i];
|
|
284
|
+
if (c === "{") depth++;
|
|
285
|
+
else if (c === "}") {
|
|
286
|
+
depth--;
|
|
287
|
+
if (depth === 0) return region.slice(open, i + 1);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function metaRailRenderIsHonest(region: string): { honest: boolean; reason: string } {
|
|
294
|
+
// The mailbox path may be built at most once in the whole render branch…
|
|
295
|
+
const mailboxCalls = region.split("defaultMetaMailboxDir(").length - 1;
|
|
296
|
+
if (mailboxCalls !== 1) {
|
|
297
|
+
return { honest: false, reason: `expected exactly 1 defaultMetaMailboxDir() call, found ${mailboxCalls}` };
|
|
298
|
+
}
|
|
299
|
+
// …and that one call must sit INSIDE the self-fetch branch body. Ordering alone is not
|
|
300
|
+
// containment: an `if (rail === "self-fetch") {}` with an EMPTY body followed by an
|
|
301
|
+
// unconditional mailbox build satisfies "guard appears before mailbox" while still
|
|
302
|
+
// synthesizing a mailbox for every rail — the exact defect this predicate exists to
|
|
303
|
+
// reject. Extract the body by brace-counting and require the call to be in it.
|
|
304
|
+
const selfFetchBody = branchBody(region, 'rail === "self-fetch"');
|
|
305
|
+
if (selfFetchBody === null) {
|
|
306
|
+
return { honest: false, reason: 'no brace-balanced `rail === "self-fetch"` branch body' };
|
|
307
|
+
}
|
|
308
|
+
const inBranch = selfFetchBody.split("defaultMetaMailboxDir(").length - 1;
|
|
309
|
+
if (inBranch !== 1) {
|
|
310
|
+
return {
|
|
311
|
+
honest: false,
|
|
312
|
+
reason: `the self-fetch branch BODY must build the mailbox path exactly once, found ${inBranch} (ordering is not containment)`,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
const pushBody = branchBody(region, 'rail === "native-push"');
|
|
316
|
+
if (pushBody === null) {
|
|
317
|
+
return { honest: false, reason: 'no brace-balanced `rail === "native-push"` branch body' };
|
|
318
|
+
}
|
|
319
|
+
// The native-push branch must deny an inbox AND keep the direct-inject claim conditional
|
|
320
|
+
// on the probe: a dead probe has no live conversation to inject into, so an unconditional
|
|
321
|
+
// "injects a reply into this live conversation" invents the very rail F-1 is about.
|
|
322
|
+
if (!/no inbox/.test(pushBody)) {
|
|
323
|
+
return { honest: false, reason: "the native-push branch does not state that there is no inbox" };
|
|
324
|
+
}
|
|
325
|
+
if (!/while the adapter probe is alive/.test(pushBody)) {
|
|
326
|
+
return {
|
|
327
|
+
honest: false,
|
|
328
|
+
reason: "the native-push branch states direct injection unconditionally (a dead probe has no live conversation)",
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
honest: true,
|
|
333
|
+
reason:
|
|
334
|
+
"the sole mailbox build is INSIDE the self-fetch branch body; native-push denies an inbox and gates injection on the probe",
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
{
|
|
339
|
+
const verdict = metaRailRenderIsHonest(selfRegion);
|
|
340
|
+
ok(`entwurf_self renders the meta rail honestly [QK:SELFADDR-RAIL-RENDER] (${verdict.reason})`, verdict.honest);
|
|
341
|
+
ok(
|
|
342
|
+
"entwurf_self reads the rail from the builder (not re-derived at the render site)",
|
|
343
|
+
/self\.metaDeliveryDomain/.test(selfRegion),
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
// NEGATIVE CONTROLS — two KINDS, because a gate that only rejects the exact old
|
|
347
|
+
// sentence would pass a version where the branch is simply gone (and vice versa).
|
|
348
|
+
// Driving the same predicate over synthetic sources is what proves this assertion
|
|
349
|
+
// BLOCKS, not merely that it exists.
|
|
350
|
+
const retiredUnconditional = [
|
|
351
|
+
'} else if (sender.origin === "meta-session") {',
|
|
352
|
+
"\tconst mailboxPath = path.join(defaultMetaMailboxDir(), sender.sessionId);",
|
|
353
|
+
"\textra.mailboxPath = mailboxPath;",
|
|
354
|
+
"}",
|
|
355
|
+
].join("\n");
|
|
356
|
+
ok(
|
|
357
|
+
"NEGATIVE 1/3 (exact false sentence): the retired unconditional meta-session mailbox is REJECTED",
|
|
358
|
+
metaRailRenderIsHonest(retiredUnconditional).honest === false,
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
const selfFetchOnlyNoPushBranch = [
|
|
362
|
+
"const rail = self.metaDeliveryDomain;",
|
|
363
|
+
'if (rail === "self-fetch") {',
|
|
364
|
+
"\tconst mailboxPath = path.join(defaultMetaMailboxDir(), sender.sessionId);",
|
|
365
|
+
"\textra.mailboxPath = mailboxPath;",
|
|
366
|
+
"}",
|
|
367
|
+
].join("\n");
|
|
368
|
+
ok(
|
|
369
|
+
"NEGATIVE 2/3 (omission): a self-fetch-guarded render with the native-push branch DELETED is REJECTED",
|
|
370
|
+
metaRailRenderIsHonest(selfFetchOnlyNoPushBranch).honest === false,
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
// NEGATIVE 3/3 — the form the FIRST version of this predicate wrongly accepted (caught in
|
|
374
|
+
// cross-review, 2026-07-27): every marker present, guard textually BEFORE the mailbox, yet
|
|
375
|
+
// the guard's body is empty and the mailbox is built unconditionally. Ordering satisfied,
|
|
376
|
+
// containment violated. This row is why the predicate brace-counts the branch body.
|
|
377
|
+
const guardedInNameOnly = [
|
|
378
|
+
"const rail = self.metaDeliveryDomain;",
|
|
379
|
+
'if (rail === "self-fetch") { }',
|
|
380
|
+
"const mailboxPath = path.join(defaultMetaMailboxDir(), sender.sessionId);",
|
|
381
|
+
"extra.mailboxPath = mailboxPath;",
|
|
382
|
+
'if (rail === "native-push") {',
|
|
383
|
+
'\tlines.push("mailbox: none — no inbox; direct-inject only while the adapter probe is alive");',
|
|
384
|
+
"}",
|
|
385
|
+
].join("\n");
|
|
386
|
+
ok(
|
|
387
|
+
"NEGATIVE 3/3 (ordering-but-not-containment): an EMPTY self-fetch guard followed by an unconditional mailbox build is REJECTED",
|
|
388
|
+
metaRailRenderIsHonest(guardedInNameOnly).honest === false,
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ── F-1: the identity-wiring errors must not teach one backend's hook as THE hook ──
|
|
393
|
+
// Both native backends mint a garden id from their OWN hook (Claude Code SessionStart,
|
|
394
|
+
// Antigravity PreInvocation). Naming only SessionStart/Claude sent an agy operator
|
|
395
|
+
// looking for a hook its backend never runs.
|
|
396
|
+
{
|
|
397
|
+
/** Extract a `class NAME extends Error { ... }` body by brace-counting. */
|
|
398
|
+
const classBody = (name: string): string => {
|
|
399
|
+
const sig = `class ${name} extends Error {`;
|
|
400
|
+
const at = src.indexOf(sig);
|
|
401
|
+
assert.ok(at >= 0, `${name} present in MCP source`);
|
|
402
|
+
const open = src.indexOf("{", at);
|
|
403
|
+
let depth = 0;
|
|
404
|
+
for (let i = open; i < src.length; i++) {
|
|
405
|
+
const c = src[i];
|
|
406
|
+
if (c === "{") depth++;
|
|
407
|
+
else if (c === "}") {
|
|
408
|
+
depth--;
|
|
409
|
+
if (depth === 0) return src.slice(open, i + 1);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
throw new Error(`class ${name} body never closed`);
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
// Anchored per class (not a repo-wide grep) so a mention somewhere else in the file
|
|
416
|
+
// cannot make either row vacuously green.
|
|
417
|
+
for (const cls of ["EntwurfEnvelopeWiringError", "EntwurfSenderIdentityError"]) {
|
|
418
|
+
const body = classBody(cls);
|
|
419
|
+
ok(
|
|
420
|
+
`${cls} names BOTH native hooks (SessionStart AND PreInvocation)`,
|
|
421
|
+
/SessionStart/.test(body) && /PreInvocation/.test(body),
|
|
422
|
+
);
|
|
423
|
+
ok(
|
|
424
|
+
`${cls} no longer presents SessionStart/Claude as THE marker writer`,
|
|
425
|
+
!/The native SessionStart hook writes that marker/.test(body) &&
|
|
426
|
+
!/whose SessionStart hook wrote a live/.test(body) &&
|
|
427
|
+
!/keyed by the\s+Claude Code parent pid/.test(body),
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
249
432
|
// ── SE-2 2e-b: meta-session sender replyability from the receiver presence marker ──
|
|
250
433
|
// Identity stays trusted (record-backed), but `replyable` is now derived from whether
|
|
251
434
|
// THIS session's own receiver inbox can actually wake (slice-2 presence marker), not a
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* by prose. Pure; no backend, no socket, no API.
|
|
9
9
|
*
|
|
10
10
|
* Proves:
|
|
11
|
-
* - R1 domain guard:
|
|
11
|
+
* - R1 domain guard: the control-socket domain currently contains pi; claude-code / codex /
|
|
12
12
|
* antigravity are `unsupported` — NEVER folded into dead/indeterminate.
|
|
13
13
|
* - the 6-cell table is exhaustive and every cell is a SINGLE verdict (Q2):
|
|
14
14
|
* exactly two allow cells (ff+live=send, owned+dormant=resume), four reject.
|
|
@@ -92,7 +92,7 @@ function enumValues(schema: unknown): string[] {
|
|
|
92
92
|
|
|
93
93
|
const SOCKET_LIVENESSES = ["alive", "dead", "indeterminate"] as const;
|
|
94
94
|
|
|
95
|
-
// ── R1:
|
|
95
|
+
// ── R1: control-socket capability domain — currently pi ────────────────────
|
|
96
96
|
eq("domain: pi is supported", isLivenessSupported("pi"), true);
|
|
97
97
|
eq("domain: claude-code unsupported (self-fetch, no socket)", isLivenessSupported("claude-code"), false);
|
|
98
98
|
eq("domain: codex unsupported (no probe surface yet)", isLivenessSupported("codex"), false);
|
|
@@ -271,15 +271,15 @@ async function main(): Promise<void> {
|
|
|
271
271
|
ok("A: B1 — a pi target does NO pre-lock lstat (inspectPath unused)", spies.inspectPath.length === 0);
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
// ── A2:
|
|
274
|
+
// ── A2: out-of-socket-domain target checks record-side conflict pre-lock ──
|
|
275
275
|
{
|
|
276
276
|
// claude-code (unsupported) + a symlink at the canonical path → record-side conflict →
|
|
277
277
|
// the decider rejects target-address-conflict BEFORE acquiring (no lock for a quarantined
|
|
278
278
|
// address). The lstat-only inspectPath ran exactly once; acquire never did.
|
|
279
279
|
const { deps, spies } = makeSpiedFactory({ backend: "claude-code", inspectKind: "address-conflict" });
|
|
280
280
|
const decision = await deps.decide({ target: GID, intent: "fire-and-forget", message: "m" });
|
|
281
|
-
ok("A2:
|
|
282
|
-
ok("A2:
|
|
281
|
+
ok("A2: out-of-socket-domain conflict → reject", decision.kind === "reject");
|
|
282
|
+
ok("A2: out-of-domain target lstat'd the conflict exactly once", spies.inspectPath.length === 1);
|
|
283
283
|
ok("A2: a quarantined target is never lock-acquired", spies.acquire.length === 0);
|
|
284
284
|
}
|
|
285
285
|
|
|
@@ -316,7 +316,7 @@ async function main(): Promise<void> {
|
|
|
316
316
|
ok("A3b: no send attempted on a dead target", spies.nativePushSend.length === 0);
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
-
// ── A3:
|
|
319
|
+
// ── A3: out-of-domain indeterminate lstat fails loud ─────────────────────
|
|
320
320
|
{
|
|
321
321
|
const { deps } = makeSpiedFactory({ backend: "claude-code", inspectKind: "indeterminate" });
|
|
322
322
|
let threw = false;
|
|
@@ -325,7 +325,7 @@ async function main(): Promise<void> {
|
|
|
325
325
|
} catch {
|
|
326
326
|
threw = true;
|
|
327
327
|
}
|
|
328
|
-
ok("A3:
|
|
328
|
+
ok("A3: out-of-domain indeterminate lstat → decide throws (QB2 fail-loud)", threw);
|
|
329
329
|
}
|
|
330
330
|
|
|
331
331
|
// ── B: control sendOverSocket builds RpcSendCommand + maps + lockDir release ─
|
|
@@ -499,9 +499,13 @@ async function main(): Promise<void> {
|
|
|
499
499
|
// missing record → not a garden citizen (readMetaIdentityByGardenId fail-fast).
|
|
500
500
|
rejects("20260101T000000-facade", "not a garden citizen", "9 recordless gid → not a citizen");
|
|
501
501
|
|
|
502
|
-
//
|
|
502
|
+
// A citizen outside the spawn-bg capability domain cannot use that rail.
|
|
503
503
|
const gidClaude = mintRecord("claude-native-1", null, "claude-code");
|
|
504
|
-
rejects(
|
|
504
|
+
rejects(
|
|
505
|
+
gidClaude,
|
|
506
|
+
"spawn-bg resume is a host-adapter capability",
|
|
507
|
+
"9 out-of-domain citizen → refused (spawn-bg rail)",
|
|
508
|
+
);
|
|
505
509
|
|
|
506
510
|
// pi citizen with no recorded transcript (no turn yet) → nothing to resume.
|
|
507
511
|
const gidNoFile = mintRecord("0199dddd-1111-4222-8333-444455556666", null);
|