@junghanacs/entwurf 0.15.0 → 0.16.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.
Files changed (90) hide show
  1. package/AGENTS.md +1 -0
  2. package/CHANGELOG.md +299 -0
  3. package/DELIVERY.md +4 -3
  4. package/README.md +99 -67
  5. package/VERIFY.md +13 -7
  6. package/docs/acp-backend-rail.md +1 -1
  7. package/docs/external-mcp-host.md +147 -9
  8. package/docs/setup-clean-host.md +221 -16
  9. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +65 -20
  10. package/mcp/entwurf-bridge/dist/pi-extensions/entwurf-capabilities.json +1 -0
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +17 -6
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +1 -1
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +172 -5
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +152 -17
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/omp-fresh-preflight.js +271 -0
  16. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js +988 -0
  17. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-receive-omp.js +574 -0
  18. package/mcp/entwurf-bridge/dist/scripts/omp-receive-facts.js +84 -0
  19. package/mcp/entwurf-bridge/src/index.ts +65 -19
  20. package/mcp/entwurf-bridge/tsconfig.build.json +19 -3
  21. package/mcp/tsconfig.json +10 -0
  22. package/package.json +14 -4
  23. package/pi/entwurf-capabilities.json +1 -0
  24. package/pi/meta-bridge-omp/entwurf-meta-omp/package.json +7 -0
  25. package/pi/omp-receive/entwurf-receive-omp/package.json +7 -0
  26. package/pi-extensions/entwurf-control.ts +12 -9
  27. package/pi-extensions/lib/entwurf-self-address.ts +18 -7
  28. package/pi-extensions/lib/meta-sender-identity.ts +1 -1
  29. package/pi-extensions/lib/meta-session.ts +219 -5
  30. package/pi-extensions/lib/mux-fresh-call.ts +171 -17
  31. package/pi-extensions/lib/omp-fresh-preflight.ts +275 -0
  32. package/pi-extensions/meta-bridge-omp.ts +1244 -0
  33. package/pi-extensions/meta-bridge-receive-omp.ts +666 -0
  34. package/run.sh +1041 -174
  35. package/scripts/agy-bridge.sh +4 -2
  36. package/scripts/check-copilot-birth-hook.ts +700 -23
  37. package/scripts/check-entwurf-self-address.ts +31 -0
  38. package/scripts/check-fresh-cut-gate.sh +12 -4
  39. package/scripts/check-gate-qualification.ts +8 -2
  40. package/scripts/check-harness-admission-parity.ts +143 -0
  41. package/scripts/check-meta-doctor-oracle.sh +20 -0
  42. package/scripts/check-omp-birth-hook.ts +1049 -0
  43. package/scripts/check-omp-fresh-preflight.ts +181 -0
  44. package/scripts/check-omp-receive-arm.ts +516 -0
  45. package/scripts/check-setup-qualification.sh +126 -0
  46. package/scripts/copilot-bridge-doctor.sh +104 -71
  47. package/scripts/copilot-bridge-install.sh +180 -20
  48. package/scripts/copilot-bridge-oracle.sh +223 -0
  49. package/scripts/copilot-bridge-uninstall.sh +137 -0
  50. package/scripts/dev-bin.sh +50 -9
  51. package/scripts/fake-copilot-vendor.sh +67 -0
  52. package/scripts/inventory-verification-surface.ts +1 -1
  53. package/scripts/meta-bridge-hook-log.sh +9 -1
  54. package/scripts/mutants/copilot-birth.json +96 -5
  55. package/scripts/mutants/mux-fresh-call.json +22 -22
  56. package/scripts/mutants/omp-birth.json +173 -0
  57. package/scripts/mutants/omp-fresh.json +300 -0
  58. package/scripts/mutants/omp-receive.json +135 -0
  59. package/scripts/mutants/pi-package-ownership.json +92 -0
  60. package/scripts/mutants/self-address.json +34 -0
  61. package/scripts/mutants/setup-verdict.json +148 -0
  62. package/scripts/mutants/source-install.json +32 -0
  63. package/scripts/omp-bridge-doctor.sh +315 -0
  64. package/scripts/omp-bridge-install.sh +221 -0
  65. package/scripts/omp-bridge-oracle.sh +154 -0
  66. package/scripts/omp-bridge-uninstall.sh +57 -0
  67. package/scripts/omp-mcp-bridge.sh +320 -0
  68. package/scripts/omp-mcp-config.py +392 -0
  69. package/scripts/omp-receive-doctor.sh +246 -0
  70. package/scripts/omp-receive-facts.ts +106 -0
  71. package/scripts/omp-receive-install.sh +228 -0
  72. package/scripts/omp-receive-uninstall.sh +60 -0
  73. package/scripts/omp-tool-surface.py +349 -0
  74. package/scripts/pi_settings_io.py +22 -0
  75. package/scripts/raw-omp-measure/README.md +420 -0
  76. package/scripts/raw-omp-measure/probe-extension.ts +76 -0
  77. package/scripts/raw-omp-measure/probe-receive-surface.ts +250 -0
  78. package/scripts/raw-omp-measure/source-audit.md +414 -0
  79. package/scripts/register-pi-package.py +525 -7
  80. package/scripts/register-pi-provider.py +162 -12
  81. package/scripts/smoke-agy-install-state.sh +86 -30
  82. package/scripts/smoke-agy-statusline-state.sh +9 -6
  83. package/scripts/smoke-omp-bridge-state.sh +221 -0
  84. package/scripts/smoke-omp-fresh-live.ts +497 -0
  85. package/scripts/smoke-omp-mcp-state.sh +327 -0
  86. package/scripts/smoke-omp-receive-live.ts +340 -0
  87. package/scripts/smoke-omp-receive-state.sh +196 -0
  88. package/scripts/smoke-setup-verdict.sh +291 -0
  89. package/scripts/smoke-user-scope-citizen.sh +388 -1
  90. package/scripts/tsconfig.json +2 -0
@@ -0,0 +1,340 @@
1
+ /**
2
+ * smoke-omp-receive-live — the LIVE acceptance for #87 bundle B: another harness wakes an
3
+ * ALREADY-OPEN, IDLE omp citizen, which reads its own inbox in that same native session
4
+ * and answers there.
5
+ *
6
+ * IT DECIDES ITS OWN OUTCOME FROM THE CAPABILITY REGISTRY, never from a hardcoded verdict:
7
+ *
8
+ * registry says omp has no drainable mailbox → protocol SKIP (97). Honest: there is no
9
+ * receiver rail to accept, so no addressed receive can be attempted. An unattended
10
+ * `release-gate` reports it; `--cut` reads it as RED, which is what makes "no cut
11
+ * while the garden is one-way for omp" executable rather than prose.
12
+ * registry claims the rail and LIVE≠1 → protocol SKIP, naming LIVE=1. This step
13
+ * opens a real omp TUI and spends a real model turn.
14
+ * registry claims the rail and LIVE=1 → run the real acceptance below. Any
15
+ * missing prerequisite here is a FAIL, not a skip: a registry that promises a wake
16
+ * has already made the claim this step exists to check.
17
+ *
18
+ * WHY IT OPENS THE TUI ITSELF, AND WHY THAT IS NOT BUNDLE C. omp cannot be opened by
19
+ * `entwurf_fresh_call` — visible fresh is step 9 and a separate admission. But the thing
20
+ * under test is receive, not launch, so this step drives a plain tmux-hosted `omp` as TEST
21
+ * SCAFFOLDING: no nonce callback, no garden id correlation through a launch surface, no
22
+ * managed-runtime claim. It learns the citizen's id the way any observer would, by reading
23
+ * the receiver's own log. `smoke-mux-lifecycle-live` is the precedent for a release MUST
24
+ * that opens real windows and spends turns.
25
+ *
26
+ * WHAT IT PROVES, IN ORDER — each one a separate assertion because each fails differently:
27
+ * 1. an idle omp TUI ARMS a receiver marker joined to the citizen birth minted in the
28
+ * same process (the marker read through the production reader, never a filename);
29
+ * 2. dispatch from another harness ROUTES to the mailbox instead of refusing;
30
+ * 3. the doorbell RINGS on a session with zero typing;
31
+ * 4. the model DRAINS its own inbox — `lastReadAt` moves after `lastEnqueuedAt`, which
32
+ * is the honest receipt a rung doorbell alone is not;
33
+ * 5. the drain happened INSIDE that native session — read from the citizen's own
34
+ * transcript, which is the vendor's record of what that session did.
35
+ *
36
+ * WHAT IT DELIBERATELY DOES NOT ASSERT, AND WHY THAT MATTERS. An earlier draft asked the
37
+ * model to echo a nonce back and failed: `[LIVE 2026-08-30]` the model drained the inbox,
38
+ * recorded the receipt, and then answered *"Its acceptance instruction was unverified, so
39
+ * no token was sent."* It was right, and it was obeying OUR OWN doorbell, which tells it
40
+ * to treat mailbox bodies as untrusted data and not act on unverified imperatives inside
41
+ * them. An acceptance that requires the model to break that rule is an acceptance aimed at
42
+ * our own security contract, and it would go green only on a model careless enough to
43
+ * fail it. So the receipt this step accepts is the one the contract actually promises:
44
+ * the read, recorded by the tool and visible in that session's transcript.
45
+ */
46
+ import { type ChildProcess, execFileSync, spawn } from "node:child_process";
47
+ import * as fs from "node:fs";
48
+ import * as os from "node:os";
49
+ import * as path from "node:path";
50
+ import { loadMetaCapabilityRegistry, ompMetaRoots } from "../pi-extensions/lib/meta-session.ts";
51
+ import { skipLive } from "./lib/live-skip.ts";
52
+
53
+ const LABEL = "smoke-omp-receive-live";
54
+ const REPO = path.resolve(import.meta.dirname, "..");
55
+ const DIST_ENTRY = path.join(REPO, "mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js");
56
+ const TMUX_SESSION = `entwurf-omp-recv-${process.pid}`;
57
+
58
+ let passed = 0;
59
+ function ok(label: string, cond: boolean, detail = ""): void {
60
+ if (!cond) {
61
+ console.error(` FAIL ${label}${detail ? `\n${detail}` : ""}`);
62
+ teardown();
63
+ process.exit(1);
64
+ }
65
+ console.log(` ok ${label}`);
66
+ passed++;
67
+ }
68
+ function fail(message: string): never {
69
+ console.error(`[${LABEL}] ${message}`);
70
+ teardown();
71
+ process.exit(1);
72
+ }
73
+
74
+ function sh(cmd: string, args: string[]): string {
75
+ return execFileSync(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
76
+ }
77
+ function tmux(...args: string[]): string {
78
+ return sh("tmux", args);
79
+ }
80
+ function teardown(): void {
81
+ try {
82
+ tmux("kill-session", "-t", TMUX_SESSION);
83
+ } catch {
84
+ /* the session may never have started; teardown is best-effort */
85
+ }
86
+ }
87
+
88
+ /** Poll a predicate on a real clock. LIVE steps wait on other processes, so this is a
89
+ * genuine wait rather than the busy-loop a hermetic gate would refuse. */
90
+ async function until<T>(what: string, timeoutMs: number, probe: () => T | null): Promise<T> {
91
+ const deadline = Date.now() + timeoutMs;
92
+ for (;;) {
93
+ const got = probe();
94
+ if (got !== null && got !== undefined) return got;
95
+ if (Date.now() > deadline) fail(`timed out after ${timeoutMs}ms waiting for ${what}`);
96
+ await new Promise((r) => setTimeout(r, 250));
97
+ }
98
+ }
99
+
100
+ // ── 1. the registry decides whether this step has anything to accept ─────────
101
+ const wakeMode = (() => {
102
+ try {
103
+ return loadMetaCapabilityRegistry().backends.omp?.wakeMode ?? "ABSENT";
104
+ } catch (err) {
105
+ fail(`capability registry unreadable: ${String(err)}`);
106
+ }
107
+ })();
108
+
109
+ if (wakeMode !== "self-fetch") {
110
+ skipLive(
111
+ LABEL,
112
+ `omp declares wakeMode=${wakeMode}, so it has no drainable mailbox and no receiver rail exists to accept (#87 bundle B). ` +
113
+ "OMP is outbound-only under that label: it sends under its own garden id and NOTHING can reply, which dispatch reports as " +
114
+ "mailbox-undeliverable. That is a designed boundary, not a defect — and it is why a cut taken now would ship a one-way harness. " +
115
+ "Close it by landing the receiver unit and moving wakeMode, which makes this step demand the real roundtrip receipt below.",
116
+ );
117
+ }
118
+ if (process.env.LIVE !== "1") {
119
+ skipLive(
120
+ LABEL,
121
+ "set LIVE=1 to run — this step opens a real omp TUI in tmux and spends one model turn on the operator's configured omp model.",
122
+ );
123
+ }
124
+
125
+ console.log(`[${LABEL}] registry declares omp wakeMode=self-fetch — the receive rail must now prove itself`);
126
+
127
+ // ── 2. preflight: every prerequisite the registry's claim depends on ─────────
128
+ // FAIL, not skip. Once the registry says a wake happens, a missing unit is a broken
129
+ // promise rather than an absent prerequisite.
130
+ try {
131
+ sh("bash", ["-c", "command -v omp"]);
132
+ } catch {
133
+ fail(
134
+ "the 'omp' CLI is not on PATH, but the registry claims omp has a receive rail. entwurf never installs a harness — install omp, or move wakeMode back.",
135
+ );
136
+ }
137
+ try {
138
+ execFileSync("bash", [path.join(REPO, "scripts/omp-receive-doctor.sh")], { stdio: "pipe" });
139
+ console.log(` ok doctor-omp-receive is green (unit installed, ownership bound, writer current)`);
140
+ } catch (err) {
141
+ fail(
142
+ `doctor-omp-receive is RED, so the receive rail the registry promises is not actually deployed here:\n${String((err as { stdout?: Buffer }).stdout ?? err)}`,
143
+ );
144
+ }
145
+ // `tools.xdev` defaults ON and wraps MCP tools as `xd://` devices whose schemas never
146
+ // reach the prompt — a model so configured cannot call `entwurf_inbox_read`, so the
147
+ // doorbell would announce a tool that does not exist for it and this step would fail for
148
+ // a reason that is not the receive rail's fault. Name it up front (measured, #87 2026-08-28).
149
+ const ompConfig = path.join(os.homedir(), ".omp", "agent", "config.yml");
150
+ const xdevOff =
151
+ fs.existsSync(ompConfig) && /(^|\n)tools:\s*\n(\s+.*\n)*?\s+xdev:\s*false/.test(fs.readFileSync(ompConfig, "utf8"));
152
+ ok(
153
+ "operator config sets tools.xdev: false (MCP tools stay top-level, so the model can actually call entwurf_inbox_read)",
154
+ xdevOff,
155
+ ` ${ompConfig} — see docs/setup-clean-host.md §4b`,
156
+ );
157
+ if (!fs.existsSync(DIST_ENTRY)) fail(`the built bridge is missing at ${DIST_ENTRY} — run 'pnpm run build-bridge'.`);
158
+
159
+ // ── 3. open a real, idle omp TUI ─────────────────────────────────────────────
160
+ const roots = ompMetaRoots();
161
+ const receiveLog = path.join(path.dirname(roots.sessionsDir), "meta-bridge-receive-omp.log");
162
+ const logBefore = fs.existsSync(receiveLog) ? fs.readFileSync(receiveLog, "utf8").length : 0;
163
+ const workdir = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-omp-recv-"));
164
+ const nonce = `OMP-RECV-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`.toUpperCase();
165
+
166
+ process.on("exit", teardown);
167
+ tmux("new-session", "-d", "-s", TMUX_SESSION, "-x", "200", "-y", "50", "-c", workdir, `omp --cwd ${workdir}`);
168
+ console.log(`[${LABEL}] opened an omp TUI in tmux session ${TMUX_SESSION} (cwd ${workdir})`);
169
+
170
+ // ── 4. it arms, and the marker is real ───────────────────────────────────────
171
+ const gardenId = await until("the receiver to arm (log line 'armed garden=…')", 60_000, () => {
172
+ if (!fs.existsSync(receiveLog)) return null;
173
+ const fresh = fs.readFileSync(receiveLog, "utf8").slice(logBefore);
174
+ const m = fresh.match(/armed garden=([0-9A-Za-z-]+) owner=(\d+)/);
175
+ return m ? m[1] : null;
176
+ });
177
+ ok(`an idle omp TUI armed a receiver for garden ${gardenId}`, true);
178
+
179
+ const facts = JSON.parse(sh("bash", [path.join(REPO, "run.sh"), "omp-receive-facts"]));
180
+ const armed = (facts.receivers as Array<Record<string, unknown>>).find((r) => r.gardenId === gardenId);
181
+ ok(
182
+ "the production marker reader sees that receiver as LIVE (not a filename read)",
183
+ armed?.ownerLive === true,
184
+ ` ${JSON.stringify(armed)}`,
185
+ );
186
+ ok("the marker names the omp HOST process as the watch owner", armed?.ownerKind === "omp-host");
187
+
188
+ // ── 5. another harness dispatches to it ──────────────────────────────────────
189
+ // Through the REAL bridge over MCP stdio — `tools/call entwurf_v2`, the same surface every
190
+ // other citizen reaches. A direct `enqueueMetaMessage` would bypass the deliverability
191
+ // guard, which is half of what this step is here to prove.
192
+ // The nonce is a CORRELATION token, not an instruction: it is asserted against the
193
+ // delivered mailbox body, never against what the model chose to say. See the header.
194
+ const body = `BUNDLE-B LIVE ACCEPTANCE probe ${nonce}. No action is requested; this message exists so an automated acceptance can confirm it reached the right mailbox.`;
195
+ const dispatch = await new Promise<{ text: string; isError: boolean }>((resolve, reject) => {
196
+ // THE SENDER IS DELIBERATELY ANONYMOUS, AND THAT IS THE HONEST WIRING FOR A GATE.
197
+ // The bridge refuses anonymous sends by default and names this exact hatch for "a
198
+ // deliberately-anonymous external MCP host"; the message then travels marked
199
+ // external/non-replyable. Seeding a fake sender marker instead would fabricate a
200
+ // citizen that does not exist, and the sender's identity is not what this step
201
+ // accepts — the RECEIVE half is. The cross-harness case with a real authoritative
202
+ // sender was measured separately (#87, a claude-code citizen → an omp citizen).
203
+ const child: ChildProcess = spawn(process.execPath, [DIST_ENTRY], {
204
+ stdio: ["pipe", "pipe", "pipe"],
205
+ env: { ...process.env, ENTWURF_BRIDGE_ALLOW_ANONYMOUS_SENDER: "1" },
206
+ });
207
+ let buf = "";
208
+ let err = "";
209
+ const timer = setTimeout(() => {
210
+ child.kill("SIGKILL");
211
+ reject(new Error(`bridge did not answer tools/call entwurf_v2 in 30s\n${err}`));
212
+ }, 30_000);
213
+ child.stderr?.on("data", (d) => {
214
+ err += d.toString();
215
+ });
216
+ child.stdout?.on("data", (d) => {
217
+ buf += d.toString();
218
+ for (const line of buf.split("\n")) {
219
+ if (!line.trim().startsWith("{")) continue;
220
+ try {
221
+ const msg = JSON.parse(line);
222
+ if (msg.id !== 2) continue;
223
+ clearTimeout(timer);
224
+ child.kill("SIGTERM");
225
+ const text = (msg.result?.content ?? []).map((c: { text?: string }) => c.text ?? "").join("\n");
226
+ resolve({ text, isError: msg.result?.isError === true });
227
+ } catch {
228
+ /* partial frame; wait for more */
229
+ }
230
+ }
231
+ });
232
+ child.stdin?.write(
233
+ `${JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: LABEL, version: "0" } } })}\n`,
234
+ );
235
+ child.stdin?.write(`${JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })}\n`);
236
+ child.stdin?.write(
237
+ `${JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/call", params: { name: "entwurf_v2", arguments: { target: gardenId, intent: "fire-and-forget", message: body } } })}\n`,
238
+ );
239
+ });
240
+ ok("tools/call entwurf_v2 to the omp citizen was NOT refused", !dispatch.isError, ` ${dispatch.text}`);
241
+ ok(
242
+ "dispatch chose the MAILBOX rail (self-fetch), not a refusal",
243
+ /mailbox|enqueued/i.test(dispatch.text),
244
+ ` ${dispatch.text}`,
245
+ );
246
+
247
+ // ── 6. the doorbell rings on a session nobody typed into ─────────────────────
248
+ await until("the doorbell to ring", 30_000, () => {
249
+ const fresh = fs.readFileSync(receiveLog, "utf8").slice(logBefore);
250
+ return fresh.includes(`rang garden=${gardenId}`) ? true : null;
251
+ });
252
+ ok(`the doorbell rang for ${gardenId} with zero typing (idle wake)`, true);
253
+
254
+ // ── 7. the MODEL drains its own inbox — the honest receipt ───────────────────
255
+ const statePath = path.join(roots.mailboxDir, gardenId, "state.json");
256
+ const state = await until("the model to record a read receipt (lastReadAt)", 120_000, () => {
257
+ if (!fs.existsSync(statePath)) return null;
258
+ try {
259
+ const s = JSON.parse(fs.readFileSync(statePath, "utf8"));
260
+ return s.lastReadAt ? s : null;
261
+ } catch {
262
+ return null;
263
+ }
264
+ });
265
+ ok(
266
+ "lastReadAt moved AFTER lastEnqueuedAt — a rung doorbell is only a wake attempt; THIS is the receipt",
267
+ Date.parse(state.lastReadAt) >= Date.parse(state.lastEnqueuedAt),
268
+ ` ${JSON.stringify(state)}`,
269
+ );
270
+
271
+ // ── 8. the drain happened INSIDE that session, and it carried OUR body ───────
272
+ // One oracle, two facts, and it has to be the transcript rather than the mailbox:
273
+ // `entwurf_inbox_read` ARCHIVES each body as it returns it, so a `.msg.delivered` check
274
+ // after the drain finds an empty directory — measured here as a failing assertion on a
275
+ // run whose read receipt had already landed. The vendor's own session log keeps both
276
+ // halves permanently: the toolCall (this session asked, for its own garden id) and the
277
+ // toolResult (what came back, including our nonce).
278
+ //
279
+ // This closes a real gap rather than a cosmetic one. `entwurf_inbox_read` takes a
280
+ // CALLER-SUPPLIED garden id and does not verify it against the caller's identity, so
281
+ // `lastReadAt` alone proves a read happened — not that THIS session performed it.
282
+ const record = JSON.parse(sh("bash", [path.join(REPO, "run.sh"), "meta-facts"]));
283
+ const citizen = (record.citizens as Array<Record<string, unknown>>).find((c) => c.gardenId === gardenId);
284
+ ok("the citizen's V3 record names a transcript for this session", typeof citizen?.transcriptPath === "string");
285
+ const transcriptPath = citizen?.transcriptPath as string;
286
+
287
+ interface TranscriptPart {
288
+ type?: string;
289
+ name?: string;
290
+ toolName?: string;
291
+ text?: string;
292
+ arguments?: { gardenId?: string };
293
+ }
294
+ function scanTranscript(): { called: string | null; resultCarriedNonce: boolean } {
295
+ let called: string | null = null;
296
+ let resultCarriedNonce = false;
297
+ if (!fs.existsSync(transcriptPath)) return { called, resultCarriedNonce };
298
+ for (const line of fs.readFileSync(transcriptPath, "utf8").split("\n")) {
299
+ if (!line.includes("entwurf_inbox_read")) continue;
300
+ let entry: { message?: { role?: string; toolName?: string; content?: TranscriptPart[] } };
301
+ try {
302
+ entry = JSON.parse(line);
303
+ } catch {
304
+ continue;
305
+ }
306
+ for (const part of entry.message?.content ?? []) {
307
+ if (
308
+ part.type === "toolCall" &&
309
+ part.name?.includes("entwurf_inbox_read") &&
310
+ part.arguments?.gardenId === gardenId
311
+ ) {
312
+ called = part.name;
313
+ }
314
+ if (entry.message?.role === "toolResult" && entry.message.toolName?.includes("entwurf_inbox_read")) {
315
+ if ((part.text ?? "").includes(nonce)) resultCarriedNonce = true;
316
+ }
317
+ }
318
+ }
319
+ return { called, resultCarriedNonce };
320
+ }
321
+
322
+ const scan = await until("this session's own inbox_read call and its result in the transcript", 60_000, () => {
323
+ const got = scanTranscript();
324
+ return got.called && got.resultCarriedNonce ? got : null;
325
+ });
326
+ ok(
327
+ `the SAME native session called ${scan.called} for its OWN garden id — the drain is joined to the citizen, not merely to the clock`,
328
+ true,
329
+ );
330
+ ok("the drained result carried this run's nonce — the right body reached the right citizen", scan.resultCarriedNonce);
331
+
332
+ teardown();
333
+ try {
334
+ fs.rmSync(workdir, { recursive: true, force: true });
335
+ } catch {
336
+ /* scratch cleanup is best-effort */
337
+ }
338
+ console.log(
339
+ `[${LABEL}] ${passed} assertions ok — garden ${gardenId}, lastEnqueuedAt ${state.lastEnqueuedAt}, lastReadAt ${state.lastReadAt}`,
340
+ );
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env bash
2
+ # smoke-omp-receive-state — hermetic install/doctor/inverse contract for the OMP RECEIVER
3
+ # extension (#87 bundle B). No omp process, no model turn, no writes outside the sandbox.
4
+ #
5
+ # Same four properties the birth unit's smoke holds, plus the two this surface adds:
6
+ # - it installs where the vendor actually looks, and the doctor agrees;
7
+ # - the inverse is honest — it removes exactly what the state records, and a host whose
8
+ # ownership cannot be established REFUSES rather than tidying up;
9
+ # - a foreign or symlinked artifact at our path is refused, never written through;
10
+ # - a stale deployed copy of the shared writer is named;
11
+ # - installing a DOORBELL arms nothing by itself: a fresh install must leave zero
12
+ # receiver markers, because a marker is a claim that a LIVE process holds a watch and
13
+ # an installer holds none;
14
+ # - the doctor stays green while the BIRTH unit is absent, reporting the dependency as a
15
+ # note rather than a fault — receive without birth is a real, legible operator state,
16
+ # not a broken install.
17
+ set -euo pipefail
18
+
19
+ REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
20
+ RUN="$REPO_DIR/run.sh"
21
+ pass=0
22
+ ok() { printf ' ok %s\n' "$1"; pass=$((pass + 1)); }
23
+ die() { printf 'FAIL: %s\n' "$1" >&2; exit 1; }
24
+ want() { eval "$2" && ok "$1" || die "$1"; }
25
+
26
+ REPO_BEFORE="$(cd "$REPO_DIR" && git status --porcelain)"
27
+ SB="$(mktemp -d)"
28
+ trap 'rm -rf "$SB"' EXIT
29
+ # HARD RULE 12, in full. Not HOME + XDG_DATA_HOME alone: an inherited XDG_CONFIG_HOME or a
30
+ # PI_CODING_AGENT_DIR from the operator's shell would leave a writable real root reachable
31
+ # from inside this smoke. PI is set to a POISON path on purpose — after #87 B1 no OMP garden
32
+ # artifact may resolve through it, and the last check in this file asserts that tree stayed
33
+ # empty. (`ENTWURF_OMP_AGENT_DIR` still wins for the VENDOR agent dir, which is what keeps
34
+ # the installer from refusing on the ambiguity below.)
35
+ export HOME="$SB/home"
36
+ export XDG_DATA_HOME="$SB/xdg"
37
+ export XDG_CONFIG_HOME="$SB/xdg-config"
38
+ export XDG_STATE_HOME="$SB/xdg-state"
39
+ export XDG_CACHE_HOME="$SB/xdg-cache"
40
+ export XDG_RUNTIME_DIR="$SB/xdg-runtime"
41
+ PI_POISON="$SB/pi-poison-agent"
42
+ export PI_CODING_AGENT_DIR="$PI_POISON"
43
+ export ENTWURF_OMP_AGENT_DIR="$SB/home/.omp/agent"
44
+ mkdir -p "$ENTWURF_OMP_AGENT_DIR" "$SB/bin" "$XDG_CONFIG_HOME" "$XDG_STATE_HOME" "$XDG_CACHE_HOME" "$XDG_RUNTIME_DIR"
45
+ # The installer requires the vendor on PATH — entwurf never installs a harness, so an
46
+ # absent omp is a refusal rather than a silent no-op. A fake binary is enough: nothing in
47
+ # this lane executes it (the unit is a module omp imports, not a process we launch).
48
+ printf '#!/usr/bin/env bash\nexit 0\n' > "$SB/bin/omp"
49
+ chmod +x "$SB/bin/omp"
50
+ export PATH="$SB/bin:$PATH"
51
+
52
+ UNIT_DIR="$ENTWURF_OMP_AGENT_DIR/extensions/entwurf-receive-omp"
53
+ STATE="$XDG_DATA_HOME/entwurf/omp-receive/install-state.json"
54
+ ASM="$XDG_DATA_HOME/entwurf/omp-receive/.assembled"
55
+
56
+ # ── 1. install ───────────────────────────────────────────────────────────────
57
+ "$RUN" install-omp-receive >/dev/null || die "install-omp-receive failed"
58
+ want "the unit lands where omp's native discovery looks" "[ -f '$UNIT_DIR/index.ts' ]"
59
+ want "it carries the shared V3 writer" "[ -f '$UNIT_DIR/lib/meta-session.ts' ]"
60
+ want "it carries the capability registry the writer resolves via ../" "[ -f '$UNIT_DIR/entwurf-capabilities.json' ]"
61
+ want "ownership state was written" "[ -f '$STATE' ]"
62
+ want "the state records the unit dir it placed" "grep -q '\"unitDir\"' '$STATE' && grep -q 'entwurf-receive-omp' '$STATE'"
63
+ want "config.yml was NOT created or touched — this unit owns no operator SSOT" "[ ! -e '$ENTWURF_OMP_AGENT_DIR/config.yml' ]"
64
+ "$RUN" doctor-omp-receive >/dev/null || die "doctor red right after a clean install"
65
+ ok "doctor is green right after install"
66
+
67
+ # ── 2. reinstall is idempotent ───────────────────────────────────────────────
68
+ "$RUN" install-omp-receive >/dev/null || die "reinstall over our own state failed"
69
+ "$RUN" doctor-omp-receive >/dev/null || die "doctor red after a reinstall"
70
+ ok "reinstall over our own state is idempotent and stays green"
71
+
72
+ # ── 3. a STALE deployed writer is named ──────────────────────────────────────
73
+ printf '\n// drifted\n' >> "$UNIT_DIR/lib/meta-session.ts"
74
+ if OUT="$("$RUN" doctor-omp-receive 2>&1)"; then
75
+ die "doctor stayed green with a stale deployed writer"
76
+ fi
77
+ printf '%s\n' "$OUT" | grep -q "STALE" || die "doctor went red without naming the staleness"
78
+ ok "a drifted copy of the writer is reported as STALE, with the redeploy prescription"
79
+ "$RUN" install-omp-receive >/dev/null
80
+ "$RUN" doctor-omp-receive >/dev/null || die "reinstall did not repair the stale writer"
81
+ ok "reinstall repairs it"
82
+
83
+ # A byte-valid copy of our OWN unit, kept for the no-state cell below: it passes every
84
+ # structural check there is, which is exactly why a shape must not license adoption.
85
+ cp -r "$UNIT_DIR" "$SB/valid-unit"
86
+
87
+ # ── 4. the inverse is honest ─────────────────────────────────────────────────
88
+ "$RUN" uninstall-omp-receive >/dev/null || die "uninstall failed"
89
+ want "the unit is gone" "[ ! -e '$UNIT_DIR' ]"
90
+ want "the assembly is gone" "[ ! -e '$ASM' ]"
91
+ want "the ownership state is gone" "[ ! -e '$STATE' ]"
92
+ want "the agent dir itself survives — we only ever owned our own subdirectory" "[ -d '$ENTWURF_OMP_AGENT_DIR' ]"
93
+ "$RUN" doctor-omp-receive >/dev/null || die "doctor red on a clean uninstalled host (zero state is a SKIP, not a fault)"
94
+ ok "doctor reads a clean uninstalled host as zero-state, not as a fault"
95
+
96
+ if "$RUN" uninstall-omp-receive >/dev/null 2>&1; then
97
+ die "uninstall on a no-state host reported success"
98
+ fi
99
+ ok "a second uninstall REFUSES — a no-state host is unproven ownership, not an empty one"
100
+
101
+ # ── 5. NOTHING at our path is adopted without ownership STATE (#87 B2) ───────
102
+ # The structurally VALID case first, because it is the one that used to pass: the old
103
+ # installer ran a shape oracle, called green "ours", moved the directory aside, published
104
+ # over it and DELETED the preimage — and the inverse then rm -rf'd the path on the same
105
+ # unproven claim. A hand-copied unit, or ours plus operator files beside it, was destroyed
106
+ # with no way back. A shape is not a proof of ownership.
107
+ cp -r "$SB/valid-unit" "$UNIT_DIR"
108
+ printf 'operator note\n' > "$UNIT_DIR/NOTES.md"
109
+ if "$RUN" install-omp-receive >/dev/null 2>&1; then
110
+ die "install ADOPTED a structurally valid no-state unit — a shape is not ownership"
111
+ fi
112
+ want "the stranger's file beside it survives the refusal" "[ -f '$UNIT_DIR/NOTES.md' ]"
113
+ want "the unit's own entry is untouched" "[ -f '$UNIT_DIR/index.ts' ]"
114
+ want "no ownership state was invented by the refusal" "[ ! -e '$STATE' ]"
115
+ ok "a structurally VALID no-state unit is refused with zero writes — ownership state is the only licence"
116
+ rm -rf "$UNIT_DIR"
117
+
118
+ # ── 5b. a foreign unit at our path is refused, never written through ─────────
119
+ mkdir -p "$UNIT_DIR"
120
+ printf 'someone else\n' > "$UNIT_DIR/index.ts"
121
+ if "$RUN" install-omp-receive >/dev/null 2>&1; then
122
+ die "install adopted a foreign directory that fails the structural oracle"
123
+ fi
124
+ want "the foreign file is byte-identical after the refusal" "grep -q 'someone else' '$UNIT_DIR/index.ts'"
125
+ want "no ownership state was written by the refusal" "[ ! -e '$STATE' ]"
126
+ ok "a foreign no-state unit is refused with zero writes"
127
+ rm -rf "$UNIT_DIR"
128
+
129
+ mkdir -p "$SB/elsewhere"
130
+ ln -s "$SB/elsewhere" "$UNIT_DIR"
131
+ if "$RUN" install-omp-receive >/dev/null 2>&1; then
132
+ die "install wrote through a symlinked unit path"
133
+ fi
134
+ want "the symlink target is still empty — nothing was written through the link" "[ -z \"\$(ls -A '$SB/elsewhere')\" ]"
135
+ ok "a SYMLINK at the unit path is refused"
136
+ rm -f "$UNIT_DIR"
137
+
138
+ # ── 6. an ambiguous agent dir refuses instead of guessing (ledger M6) ────────
139
+ # omp is a pi fork and reads pi's env vocabulary, so an inherited PI_* knob does not say
140
+ # WHICH harness it is addressing. Installing into a directory chosen by that guess is how
141
+ # an operator ends up with a unit no live omp reads.
142
+ # Each variable is tested ALONE — the sandbox exports a poisoned PI_CODING_AGENT_DIR
143
+ # globally, and leaving it set would make every row below refuse on that one branch and
144
+ # prove nothing about the others.
145
+ for var in PI_CODING_AGENT_DIR PI_CONFIG_DIR; do
146
+ if env -u ENTWURF_OMP_AGENT_DIR -u PI_CODING_AGENT_DIR -u PI_CONFIG_DIR "$var=$SB/ambiguous" \
147
+ "$RUN" install-omp-receive >/dev/null 2>&1; then
148
+ die "install guessed an agent dir while $var was set"
149
+ fi
150
+ ok "$var set (with no explicit override) REFUSES rather than guessing which harness it addresses"
151
+ done
152
+ if env -u ENTWURF_OMP_AGENT_DIR -u PI_CODING_AGENT_DIR PI_PROFILE=work "$RUN" install-omp-receive >/dev/null 2>&1; then
153
+ die "install guessed an agent dir from PI_PROFILE alone"
154
+ fi
155
+ ok "PI_PROFILE without OMP_PROFILE REFUSES — pi and omp both read it"
156
+
157
+ # ── 7. --assemble-only never touches the live assembly ──────────────────────
158
+ if ENTWURF_OMP_RECEIVE_ASM="$ASM" "$RUN" install-omp-receive --assemble-only >/dev/null 2>&1; then
159
+ die "--assemble-only rebuilt the DEFAULT live assembly"
160
+ fi
161
+ ok "--assemble-only refuses the live assembly path (it writes no ownership state)"
162
+
163
+ # ── 8. nothing resolved through the double-duty PI knob ─────────────────────
164
+ # The whole run had PI_CODING_AGENT_DIR pointing at a poison path. After #87 B1 that
165
+ # variable is the VENDOR's agent dir for backend omp and is never a garden root, so a
166
+ # single artifact under it would mean some surface still derives its roots from it.
167
+ want "the poisoned PI_CODING_AGENT_DIR tree was never created" "[ ! -e '$PI_POISON' ]"
168
+
169
+ # ── 8b. installing a doorbell ARMS NOTHING ──────────────────────────────────
170
+ # A receiver marker claims a LIVE process is holding a watch. An installer is not that
171
+ # process, so a fresh install that produced one would be advertising a doorbell nobody is
172
+ # listening at — the exact false-deliverability this unit exists to avoid.
173
+ "$RUN" install-omp-receive >/dev/null || die "reinstall for the arm check failed"
174
+ want "installing the receiver creates NO receiver marker" "[ ! -d '$HOME/.pi/agent/meta-receivers' ] || [ -z \"\$(ls -A '$HOME/.pi/agent/meta-receivers' 2>/dev/null)\" ]"
175
+ want "installing the receiver creates no mailbox either" "[ ! -d '$HOME/.pi/agent/meta-mailbox' ] || [ -z \"\$(ls -A '$HOME/.pi/agent/meta-mailbox' 2>/dev/null)\" ]"
176
+ ok "an installed doorbell is not an armed one — only a live tui host arms"
177
+
178
+ # ── 8c. receive without birth is a NOTE, not a fault ────────────────────────
179
+ # An operator may install the receiver first, or uninstall birth and leave this behind.
180
+ # That host is legibly incomplete, not broken: the doctor must say so on its own axis
181
+ # without going red, because a red here would send the operator hunting a defect that is
182
+ # really a missing sibling unit.
183
+ if ! OUT="$("$RUN" doctor-omp-receive 2>&1)"; then
184
+ die "doctor-omp-receive went RED merely because the birth unit is not installed"
185
+ fi
186
+ printf '%s\n' "$OUT" | grep -q "BIRTH unit is not installed" \
187
+ || die "doctor stayed green but never NAMED the missing birth unit"
188
+ ok "with no birth unit the doctor is green and names the dependency as a note"
189
+ "$RUN" uninstall-omp-receive >/dev/null || die "final uninstall failed"
190
+
191
+ # ── 9. the repo itself was never written ────────────────────────────────────
192
+ REPO_AFTER="$(cd "$REPO_DIR" && git status --porcelain)"
193
+ [ "$REPO_BEFORE" = "$REPO_AFTER" ] || die "the smoke mutated the repo working tree"
194
+ ok "the checkout is byte-identical to before the smoke"
195
+
196
+ printf '[smoke-omp-receive-state] %d assertions ok\n' "$pass"