@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.
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +299 -0
- package/DELIVERY.md +4 -3
- package/README.md +99 -67
- package/VERIFY.md +13 -7
- package/docs/acp-backend-rail.md +1 -1
- package/docs/external-mcp-host.md +147 -9
- package/docs/setup-clean-host.md +221 -16
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +65 -20
- package/mcp/entwurf-bridge/dist/pi-extensions/entwurf-capabilities.json +1 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +17 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +172 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +152 -17
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/omp-fresh-preflight.js +271 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js +988 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-receive-omp.js +574 -0
- package/mcp/entwurf-bridge/dist/scripts/omp-receive-facts.js +84 -0
- package/mcp/entwurf-bridge/src/index.ts +65 -19
- package/mcp/entwurf-bridge/tsconfig.build.json +19 -3
- package/mcp/tsconfig.json +10 -0
- package/package.json +14 -4
- package/pi/entwurf-capabilities.json +1 -0
- package/pi/meta-bridge-omp/entwurf-meta-omp/package.json +7 -0
- package/pi/omp-receive/entwurf-receive-omp/package.json +7 -0
- package/pi-extensions/entwurf-control.ts +12 -9
- package/pi-extensions/lib/entwurf-self-address.ts +18 -7
- package/pi-extensions/lib/meta-sender-identity.ts +1 -1
- package/pi-extensions/lib/meta-session.ts +219 -5
- package/pi-extensions/lib/mux-fresh-call.ts +171 -17
- package/pi-extensions/lib/omp-fresh-preflight.ts +275 -0
- package/pi-extensions/meta-bridge-omp.ts +1244 -0
- package/pi-extensions/meta-bridge-receive-omp.ts +666 -0
- package/run.sh +1041 -174
- package/scripts/agy-bridge.sh +4 -2
- package/scripts/check-copilot-birth-hook.ts +700 -23
- package/scripts/check-entwurf-self-address.ts +31 -0
- package/scripts/check-fresh-cut-gate.sh +12 -4
- package/scripts/check-gate-qualification.ts +8 -2
- package/scripts/check-harness-admission-parity.ts +143 -0
- package/scripts/check-meta-doctor-oracle.sh +20 -0
- package/scripts/check-omp-birth-hook.ts +1049 -0
- package/scripts/check-omp-fresh-preflight.ts +181 -0
- package/scripts/check-omp-receive-arm.ts +516 -0
- package/scripts/check-setup-qualification.sh +126 -0
- package/scripts/copilot-bridge-doctor.sh +104 -71
- package/scripts/copilot-bridge-install.sh +180 -20
- package/scripts/copilot-bridge-oracle.sh +223 -0
- package/scripts/copilot-bridge-uninstall.sh +137 -0
- package/scripts/dev-bin.sh +50 -9
- package/scripts/fake-copilot-vendor.sh +67 -0
- package/scripts/inventory-verification-surface.ts +1 -1
- package/scripts/meta-bridge-hook-log.sh +9 -1
- package/scripts/mutants/copilot-birth.json +96 -5
- package/scripts/mutants/mux-fresh-call.json +22 -22
- package/scripts/mutants/omp-birth.json +173 -0
- package/scripts/mutants/omp-fresh.json +300 -0
- package/scripts/mutants/omp-receive.json +135 -0
- package/scripts/mutants/pi-package-ownership.json +92 -0
- package/scripts/mutants/self-address.json +34 -0
- package/scripts/mutants/setup-verdict.json +148 -0
- package/scripts/mutants/source-install.json +32 -0
- package/scripts/omp-bridge-doctor.sh +315 -0
- package/scripts/omp-bridge-install.sh +221 -0
- package/scripts/omp-bridge-oracle.sh +154 -0
- package/scripts/omp-bridge-uninstall.sh +57 -0
- package/scripts/omp-mcp-bridge.sh +320 -0
- package/scripts/omp-mcp-config.py +392 -0
- package/scripts/omp-receive-doctor.sh +246 -0
- package/scripts/omp-receive-facts.ts +106 -0
- package/scripts/omp-receive-install.sh +228 -0
- package/scripts/omp-receive-uninstall.sh +60 -0
- package/scripts/omp-tool-surface.py +349 -0
- package/scripts/pi_settings_io.py +22 -0
- package/scripts/raw-omp-measure/README.md +420 -0
- package/scripts/raw-omp-measure/probe-extension.ts +76 -0
- package/scripts/raw-omp-measure/probe-receive-surface.ts +250 -0
- package/scripts/raw-omp-measure/source-audit.md +414 -0
- package/scripts/register-pi-package.py +525 -7
- package/scripts/register-pi-provider.py +162 -12
- package/scripts/smoke-agy-install-state.sh +86 -30
- package/scripts/smoke-agy-statusline-state.sh +9 -6
- package/scripts/smoke-omp-bridge-state.sh +221 -0
- package/scripts/smoke-omp-fresh-live.ts +497 -0
- package/scripts/smoke-omp-mcp-state.sh +327 -0
- package/scripts/smoke-omp-receive-live.ts +340 -0
- package/scripts/smoke-omp-receive-state.sh +196 -0
- package/scripts/smoke-setup-verdict.sh +291 -0
- package/scripts/smoke-user-scope-citizen.sh +388 -1
- package/scripts/tsconfig.json +2 -0
|
@@ -0,0 +1,1049 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-omp-birth-hook — #87 gate: the OMP BIRTH path, proven without omp.
|
|
3
|
+
*
|
|
4
|
+
* WHAT IT BINDS. The chain from the shipped unit to a real garden citizen:
|
|
5
|
+
*
|
|
6
|
+
* omp-bridge-install.sh --assemble-only (the REAL assembler, into a temp dir)
|
|
7
|
+
* -> the assembled unit's own index.ts, imported the way omp imports it
|
|
8
|
+
* -> the factory bound to a MOCK ExtensionAPI, fired with a MOCK ExtensionContext
|
|
9
|
+
* -> mode "tui" : a v3 record with backend "omp" + a sender marker + a status line
|
|
10
|
+
* -> every other : nothing at all, and a log line saying so
|
|
11
|
+
*
|
|
12
|
+
* WHY A MOCK HOST RATHER THAN A SPAWNED LAUNCHER. The Claude and Copilot gates fire a
|
|
13
|
+
* shipped shell launcher because those vendors EXEC a hook process. omp does not: it
|
|
14
|
+
* imports the module into its own process and calls the exported factory
|
|
15
|
+
* (`extensibility/extensions/types.ts:1592`; `--hook` aliases `--extension`). The
|
|
16
|
+
* faithful hermetic stand-in for that is therefore a process that imports the assembled
|
|
17
|
+
* entry and calls it — which is also what makes the pid assertions below meaningful.
|
|
18
|
+
*
|
|
19
|
+
* THE PID ORACLE IS INDEPENDENT OF THE WRITER. The child process IS the "omp host": the
|
|
20
|
+
* gate knows its pid from `spawnSync` before reading any marker, and the gate's own pid is
|
|
21
|
+
* the child's PARENT. So "the marker is keyed to the host's own pid, not to its parent"
|
|
22
|
+
* is checked against two numbers this gate holds for reasons that have nothing to do with
|
|
23
|
+
* the payload. That distinction is the whole step-6 difference between this backend and
|
|
24
|
+
* the exec'd ones, and getting it backwards would key every omp citizen's marker to the
|
|
25
|
+
* shell that launched omp.
|
|
26
|
+
*
|
|
27
|
+
* WHAT IT DOES NOT PROVE, and must not be read as proving. It proves the MECHANISM, not
|
|
28
|
+
* the ADMISSION. §3.5 acceptance is a real omp TUI session minting exactly one record
|
|
29
|
+
* while one real task subagent mints none (`adding-a-harness.md` step 3(b), 3.5(b)); a
|
|
30
|
+
* mock context is gate evidence, never a live citizen.
|
|
31
|
+
*
|
|
32
|
+
* Hermetic: temp dirs only, no network, no omp, no model turn.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import assert from "node:assert/strict";
|
|
36
|
+
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
37
|
+
import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
38
|
+
import { tmpdir } from "node:os";
|
|
39
|
+
import * as path from "node:path";
|
|
40
|
+
import { fileURLToPath } from "node:url";
|
|
41
|
+
import { resolveFactList } from "../pi-extensions/lib/entwurf-facts.ts";
|
|
42
|
+
import { nativePushSupported } from "../pi-extensions/lib/entwurf-v2-contract.ts";
|
|
43
|
+
import { META_SENDER_BACKENDS, resolveTrustedMetaSenderIdentity } from "../pi-extensions/lib/meta-sender-identity.ts";
|
|
44
|
+
import {
|
|
45
|
+
applyOmpBridgeChildRootPolicy,
|
|
46
|
+
listAllMetaIdentitiesDir,
|
|
47
|
+
type MetaRootBundle,
|
|
48
|
+
MetaRootPolicyError,
|
|
49
|
+
OMP_BRIDGE_PROVENANCE_LABEL,
|
|
50
|
+
ompMetaRoots,
|
|
51
|
+
processStartKey,
|
|
52
|
+
readMetaReceiverMarker,
|
|
53
|
+
} from "../pi-extensions/lib/meta-session.ts";
|
|
54
|
+
|
|
55
|
+
const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
56
|
+
const UNIT = "entwurf-meta-omp";
|
|
57
|
+
|
|
58
|
+
let passed = 0;
|
|
59
|
+
function ok(label: string, cond: boolean): void {
|
|
60
|
+
assert.ok(cond, label);
|
|
61
|
+
console.log(` ok ${label}`);
|
|
62
|
+
passed++;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const root = mkdtempSync(path.join(tmpdir(), "entwurf-omp-birth."));
|
|
66
|
+
const asm = path.join(root, "asm");
|
|
67
|
+
|
|
68
|
+
// ── 1. the real assembler ────────────────────────────────────────────────────
|
|
69
|
+
// Through `run.sh`, not straight at the script: the verb dispatch is part of the install
|
|
70
|
+
// surface (check-pack-install once caught a strict parser refusing its own verb).
|
|
71
|
+
execFileSync("bash", [path.join(REPO, "run.sh"), "install-omp-bridge", "--assemble-only"], {
|
|
72
|
+
env: {
|
|
73
|
+
...process.env,
|
|
74
|
+
HOME: path.join(root, "assembler-home"),
|
|
75
|
+
XDG_DATA_HOME: path.join(root, "assembler-xdg"),
|
|
76
|
+
ENTWURF_OMP_ASM: asm,
|
|
77
|
+
},
|
|
78
|
+
stdio: "pipe",
|
|
79
|
+
});
|
|
80
|
+
const unit = path.join(asm, UNIT);
|
|
81
|
+
const entry = path.join(unit, "index.ts");
|
|
82
|
+
ok(
|
|
83
|
+
"assembler produced the unit, its entry, the shared writer and the capability registry",
|
|
84
|
+
[
|
|
85
|
+
entry,
|
|
86
|
+
path.join(unit, "package.json"),
|
|
87
|
+
path.join(unit, "lib", "meta-session.ts"),
|
|
88
|
+
path.join(unit, "lib", "session-id.js"),
|
|
89
|
+
path.join(unit, "entwurf-capabilities.json"),
|
|
90
|
+
].every((p) => existsSync(p)),
|
|
91
|
+
);
|
|
92
|
+
// The name is not decoration: omp discovers a subdirectory extension by `index.{ts,js}`
|
|
93
|
+
// (`discovery/helpers.ts:700-710`). Any other filename here is a unit the vendor never
|
|
94
|
+
// loads, and nothing else in this gate would notice.
|
|
95
|
+
ok("the entry is named index.ts — the name omp's subdirectory discovery rule looks for", existsSync(entry));
|
|
96
|
+
const pkg = JSON.parse(readFileSync(path.join(unit, "package.json"), "utf8")) as Record<string, unknown>;
|
|
97
|
+
// An ESM index.js under a directory with no nearest package.json `type` is read as
|
|
98
|
+
// CommonJS, which is the installed-package shape's silent failure.
|
|
99
|
+
ok('the unit package.json declares "type": "module"', pkg.type === "module");
|
|
100
|
+
ok(
|
|
101
|
+
"the unit carries no `omp.extensions` declaration — discovery is the index rule, so nothing needs baking",
|
|
102
|
+
pkg.omp === undefined,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// ── 2. the mock omp host ─────────────────────────────────────────────────────
|
|
106
|
+
// One child process per scenario, each with its own store. The child imports the
|
|
107
|
+
// ASSEMBLED entry (not the repo source), binds it to a mock ExtensionAPI, and fires the
|
|
108
|
+
// requested edges with mock ExtensionContexts.
|
|
109
|
+
const harness = path.join(root, "omp-host.mjs");
|
|
110
|
+
writeFileSync(
|
|
111
|
+
harness,
|
|
112
|
+
`
|
|
113
|
+
import { pathToFileURL } from "node:url";
|
|
114
|
+
const spec = JSON.parse(process.argv[2]);
|
|
115
|
+
const mod = await import(pathToFileURL(spec.entry).href);
|
|
116
|
+
const handlers = new Map();
|
|
117
|
+
const flags = [];
|
|
118
|
+
const sent = [];
|
|
119
|
+
// The mock vendor API. \`getFlag\` answers undefined unless the scenario names a payload —
|
|
120
|
+
// which is the manual-omp shape, and the shape every birth cell here uses: a bootstrap must
|
|
121
|
+
// never arm, never poll and never send for a session the launcher did not open.
|
|
122
|
+
const pi = {
|
|
123
|
+
on: (event, handler) => { const list = handlers.get(event) ?? []; list.push(handler); handlers.set(event, list); },
|
|
124
|
+
registerFlag: (name, options) => { flags.push([name, options?.type]); },
|
|
125
|
+
getFlag: name => (spec.bootstrapFlag && name === "entwurf-bootstrap" ? spec.bootstrapFlag : undefined),
|
|
126
|
+
getAllTools: () => spec.tools ?? [],
|
|
127
|
+
getActiveTools: () => (spec.tools ?? []).map(t => t.name),
|
|
128
|
+
sendUserMessage: (content, options) => { sent.push([content, options?.deliverAs ?? null]); },
|
|
129
|
+
};
|
|
130
|
+
(mod.default ?? mod)(pi);
|
|
131
|
+
const statuses = [];
|
|
132
|
+
for (const edge of spec.edges) {
|
|
133
|
+
const ctx = {
|
|
134
|
+
mode: edge.mode,
|
|
135
|
+
cwd: edge.cwd,
|
|
136
|
+
hasUI: edge.hasUI ?? true,
|
|
137
|
+
ui: edge.noUi ? {} : { setStatus: (key, text) => statuses.push([key, text]) },
|
|
138
|
+
sessionManager: edge.noManager ? undefined : {
|
|
139
|
+
getSessionId: () => edge.sessionId,
|
|
140
|
+
getCwd: () => edge.managerCwd,
|
|
141
|
+
getSessionFile: () => edge.sessionFile,
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
for (const handler of handlers.get(edge.event) ?? []) {
|
|
145
|
+
await handler({ type: edge.event, reason: edge.reason }, ctx);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const result = JSON.stringify({ pid: process.pid, boundEvents: [...handlers.keys()].sort(), statuses, flags, sent });
|
|
149
|
+
process.stdout.write(result + "\\n");
|
|
150
|
+
if (spec.resultFile) {
|
|
151
|
+
const { writeFileSync, renameSync } = await import("node:fs");
|
|
152
|
+
writeFileSync(spec.resultFile + ".tmp", result + "\\n");
|
|
153
|
+
renameSync(spec.resultFile + ".tmp", spec.resultFile);
|
|
154
|
+
}
|
|
155
|
+
// HOLD: the sender-marker guards are about a LIVE owner (pid + start-key), so the one
|
|
156
|
+
// scenario that runs the production resolver has to be resolved while this process still
|
|
157
|
+
// exists — a marker whose owner has exited names nobody, by design.
|
|
158
|
+
if (spec.hold) setInterval(() => {}, 1 << 30);
|
|
159
|
+
`,
|
|
160
|
+
"utf8",
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
interface HostResult {
|
|
164
|
+
pid: number;
|
|
165
|
+
boundEvents: string[];
|
|
166
|
+
statuses: Array<[string, string]>;
|
|
167
|
+
/** `[name, type]` for every flag the factory registered, in registration order. */
|
|
168
|
+
flags: Array<[string, string]>;
|
|
169
|
+
/** `[content, deliverAs]` for every `sendUserMessage` the unit made. A birth-only
|
|
170
|
+
* scenario must leave this EMPTY: no flag means no bootstrap means no model turn. */
|
|
171
|
+
sent: Array<[string, string | null]>;
|
|
172
|
+
status: number | null;
|
|
173
|
+
stderr: string;
|
|
174
|
+
}
|
|
175
|
+
interface Edge {
|
|
176
|
+
event: "session_start" | "session_switch";
|
|
177
|
+
mode: string;
|
|
178
|
+
cwd?: string;
|
|
179
|
+
managerCwd?: string;
|
|
180
|
+
sessionId?: string;
|
|
181
|
+
sessionFile?: string;
|
|
182
|
+
reason?: string;
|
|
183
|
+
noManager?: boolean;
|
|
184
|
+
noUi?: boolean;
|
|
185
|
+
hasUI?: boolean;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* ONE SANDBOX HOME PER SCENARIO, AND THE DOUBLE-DUTY KNOB IS POISON IN ALL OF THEM.
|
|
190
|
+
*
|
|
191
|
+
* Hard Rule 12 wants HOME, `PI_CODING_AGENT_DIR` and every writable XDG root fenced, and
|
|
192
|
+
* the first version of this gate fenced neither HOME nor XDG — it relocated the store with
|
|
193
|
+
* `PI_CODING_AGENT_DIR` instead, which after #87 B1 is exactly the variable an OMP surface
|
|
194
|
+
* must ignore. So the relocation moved to HOME, and PI became a POISON path every scenario
|
|
195
|
+
* carries: a single artifact under it means some OMP surface still derives a garden root
|
|
196
|
+
* from the vendor's own agent-dir variable. `<home>/omp-profile-agent` is not a decorative
|
|
197
|
+
* name — that is the shape `omp --profile work` actually produces
|
|
198
|
+
* (`oh-my-pi` v18.0.0 `utils/src/dirs.ts:452-473`).
|
|
199
|
+
*
|
|
200
|
+
* The four `ENTWURF_META_*` overrides are stripped by default (so the DEFAULT policy is
|
|
201
|
+
* what every scenario exercises) and passed explicitly only by the override cell.
|
|
202
|
+
*/
|
|
203
|
+
function piPoison(home: string): string {
|
|
204
|
+
return path.join(home, "omp-profile-agent");
|
|
205
|
+
}
|
|
206
|
+
function agentDir(home: string): string {
|
|
207
|
+
return path.join(home, ".pi", "agent");
|
|
208
|
+
}
|
|
209
|
+
function isolatedEnv(home: string, overrides?: MetaRootBundle): NodeJS.ProcessEnv {
|
|
210
|
+
const env: NodeJS.ProcessEnv = { ...process.env };
|
|
211
|
+
env.HOME = home;
|
|
212
|
+
env.PI_CODING_AGENT_DIR = piPoison(home);
|
|
213
|
+
env.XDG_DATA_HOME = path.join(home, ".local", "share");
|
|
214
|
+
env.XDG_CONFIG_HOME = path.join(home, ".config");
|
|
215
|
+
env.XDG_STATE_HOME = path.join(home, ".local", "state");
|
|
216
|
+
env.XDG_CACHE_HOME = path.join(home, ".cache");
|
|
217
|
+
env.XDG_RUNTIME_DIR = path.join(home, ".run");
|
|
218
|
+
delete env.ENTWURF_META_SESSIONS_DIR;
|
|
219
|
+
delete env.ENTWURF_META_MAILBOX_DIR;
|
|
220
|
+
delete env.ENTWURF_META_SENDERS_DIR;
|
|
221
|
+
delete env.ENTWURF_META_RECEIVERS_DIR;
|
|
222
|
+
if (overrides) {
|
|
223
|
+
env.ENTWURF_META_SESSIONS_DIR = overrides.sessionsDir;
|
|
224
|
+
env.ENTWURF_META_MAILBOX_DIR = overrides.mailboxDir;
|
|
225
|
+
env.ENTWURF_META_SENDERS_DIR = overrides.sendersDir;
|
|
226
|
+
env.ENTWURF_META_RECEIVERS_DIR = overrides.receiversDir;
|
|
227
|
+
}
|
|
228
|
+
return env;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function runHost(env: NodeJS.ProcessEnv, edges: Edge[]): HostResult {
|
|
232
|
+
const res = spawnSync(
|
|
233
|
+
process.execPath,
|
|
234
|
+
["--experimental-strip-types", "--disable-warning=ExperimentalWarning", harness, JSON.stringify({ entry, edges })],
|
|
235
|
+
{ env, encoding: "utf8" },
|
|
236
|
+
);
|
|
237
|
+
const parsed = res.stdout?.trim()
|
|
238
|
+
? (JSON.parse(res.stdout.trim().split("\n").pop() as string) as Omit<HostResult, "status" | "stderr">)
|
|
239
|
+
: { pid: 0, boundEvents: [], statuses: [], flags: [], sent: [] };
|
|
240
|
+
return { ...parsed, status: res.status, stderr: res.stderr ?? "" };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function host(home: string, edges: Edge[], overrides?: MetaRootBundle): HostResult {
|
|
244
|
+
mkdirSync(home, { recursive: true });
|
|
245
|
+
return runHost(isolatedEnv(home, overrides), edges);
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Same mock host, but LEFT RUNNING so the production sender resolver can be exercised the
|
|
249
|
+
* way it runs in production: against a live owner. `readMetaSenderMarker`'s second guard is
|
|
250
|
+
* pid + start-key — the owner must still be the very process that wrote the marker — so a
|
|
251
|
+
* resolver assertion taken after the child exited would be asserting that a dead session
|
|
252
|
+
* names nobody, which is a different (and already covered) fact.
|
|
253
|
+
*/
|
|
254
|
+
function hostHeld(home: string, edges: Edge[]): { pid: number; kill: () => void } {
|
|
255
|
+
mkdirSync(home, { recursive: true });
|
|
256
|
+
const resultFile = path.join(home, "host-result.json");
|
|
257
|
+
const child = spawn(
|
|
258
|
+
process.execPath,
|
|
259
|
+
[
|
|
260
|
+
"--experimental-strip-types",
|
|
261
|
+
"--disable-warning=ExperimentalWarning",
|
|
262
|
+
harness,
|
|
263
|
+
JSON.stringify({ entry, edges, resultFile, hold: true }),
|
|
264
|
+
],
|
|
265
|
+
{ env: isolatedEnv(home), stdio: "ignore" },
|
|
266
|
+
);
|
|
267
|
+
const waiter = new Int32Array(new SharedArrayBuffer(4));
|
|
268
|
+
for (let i = 0; i < 200 && !existsSync(resultFile); i++) Atomics.wait(waiter, 0, 0, 50);
|
|
269
|
+
if (!existsSync(resultFile)) {
|
|
270
|
+
child.kill("SIGKILL");
|
|
271
|
+
throw new Error("the held mock omp host never reported a result");
|
|
272
|
+
}
|
|
273
|
+
const parsed = JSON.parse(readFileSync(resultFile, "utf8")) as { pid: number };
|
|
274
|
+
return { pid: parsed.pid, kill: () => child.kill("SIGKILL") };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function records(home: string): ReturnType<typeof listAllMetaIdentitiesDir>["identities"] {
|
|
278
|
+
const dir = path.join(agentDir(home), "meta-sessions");
|
|
279
|
+
if (!existsSync(dir)) return [];
|
|
280
|
+
return listAllMetaIdentitiesDir(dir).identities;
|
|
281
|
+
}
|
|
282
|
+
function hookLog(home: string): string {
|
|
283
|
+
const file = path.join(agentDir(home), "meta-bridge-hook.log");
|
|
284
|
+
return existsSync(file) ? readFileSync(file, "utf8") : "";
|
|
285
|
+
}
|
|
286
|
+
/** What the cell's garden root actually contains — "" when nothing ever wrote there. */
|
|
287
|
+
function agentEntries(home: string): string[] {
|
|
288
|
+
const dir = agentDir(home);
|
|
289
|
+
return existsSync(dir) ? readdirSync(dir) : [];
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const CWD = "/home/junghan/repos/gh/entwurf";
|
|
293
|
+
const HOST_ID = "01a042da-537a-7770-a275-7b8162eecca4";
|
|
294
|
+
const HOST_FILE =
|
|
295
|
+
"/home/junghan/.omp/agent/sessions/-tmp/2026-08-27T10-53-19-610Z_01a042da-537a-7770-a275-7b8162eecca4.jsonl";
|
|
296
|
+
|
|
297
|
+
// ── 2.5 the OMP four-root joint binding, part 1 — the policy itself ────────
|
|
298
|
+
//
|
|
299
|
+
// THE DEFECT THIS REPLANTS. `PI_CODING_AGENT_DIR` means "pi's persistence root" to entwurf
|
|
300
|
+
// and "my agent dir" to the OMP vendor, and `setProfile` exports it for every named profile
|
|
301
|
+
// (`oh-my-pi` v18.0.0 `utils/src/dirs.ts:452-473`). A birth path that resolved through the
|
|
302
|
+
// shared default therefore minted an `omp --profile work` session's record and marker into
|
|
303
|
+
// a DIFFERENT garden store — a pi sandbox, if that is where the value came from.
|
|
304
|
+
//
|
|
305
|
+
// ALL FOUR ROOTS, NOT THE TWO BIRTH WRITES TO. Splitting mailbox from receivers is the
|
|
306
|
+
// dangerous cell: dispatch can trust an armed receiver marker in one root and enqueue into
|
|
307
|
+
// the mailbox of the other while the real watcher drains the first — false deliverability.
|
|
308
|
+
// Bundle A creates no receive artifact, so mailbox and receivers are checked as RETURNED
|
|
309
|
+
// VALUES here; bundle B will consume the same two members.
|
|
310
|
+
//
|
|
311
|
+
// THE ORACLE IS NOT THE PRODUCTION RESOLVER, and that is the whole design of this cell.
|
|
312
|
+
// Comparing "what the extension computed" against "what the production resolver computes"
|
|
313
|
+
// is COMMON MODE: if both halves shared the same wrong fallback the equation would still
|
|
314
|
+
// balance and the mutant would survive semantically green. So the expected paths are
|
|
315
|
+
// literals built HERE, from the sandbox HOME this gate chose, and each subject is compared
|
|
316
|
+
// against those literals independently.
|
|
317
|
+
//
|
|
318
|
+
// WHY IT RUNS BEFORE THE BIRTH SECTIONS. A root-policy defect moves EVERY artifact this
|
|
319
|
+
// gate asserts on, so left at the end it would trip the plain tui-mint claim first and
|
|
320
|
+
// its own mutant would be killed for the wrong reason. The pure half — which no other
|
|
321
|
+
// mutant can touch — is asked first; the "did the extension really write there" half is
|
|
322
|
+
// asked at the end, after the claims it would otherwise pre-empt.
|
|
323
|
+
|
|
324
|
+
/** The REAL MCP writer's entry env, driven into a temp file. If the managed entry ever
|
|
325
|
+
* carried a HOME or a meta-root key, the vendor's `{...parent, ...entry.env}` composition
|
|
326
|
+
* could make the child resolve differently from the host no matter how correct the shared
|
|
327
|
+
* leaf is — so this keyset is read from the writer's own bytes, not assumed. */
|
|
328
|
+
const managedEntryEnv: Record<string, string> = (() => {
|
|
329
|
+
const cfgDir = mkdtempSync(path.join(root, "mcp-entry."));
|
|
330
|
+
const cfg = path.join(cfgDir, "mcp.json");
|
|
331
|
+
execFileSync(
|
|
332
|
+
"python3",
|
|
333
|
+
[
|
|
334
|
+
path.join(REPO, "scripts", "omp-mcp-config.py"),
|
|
335
|
+
"install",
|
|
336
|
+
cfg,
|
|
337
|
+
"entwurf-bridge",
|
|
338
|
+
"[]",
|
|
339
|
+
path.join(cfgDir, "state.json"),
|
|
340
|
+
],
|
|
341
|
+
{ stdio: "pipe" },
|
|
342
|
+
);
|
|
343
|
+
const written = JSON.parse(readFileSync(cfg, "utf8")) as {
|
|
344
|
+
mcpServers: Record<string, { env?: Record<string, string> }>;
|
|
345
|
+
};
|
|
346
|
+
return written.mcpServers["entwurf-bridge"].env ?? {};
|
|
347
|
+
})();
|
|
348
|
+
ok(
|
|
349
|
+
"the managed MCP entry's env is PROVENANCE-ONLY — it cannot hand the bridge child a HOME or a meta root",
|
|
350
|
+
JSON.stringify(Object.keys(managedEntryEnv).sort()) === JSON.stringify(["ENTWURF_BRIDGE_EXTERNAL_AGENT_ID"]) &&
|
|
351
|
+
managedEntryEnv.ENTWURF_BRIDGE_EXTERNAL_AGENT_ID === OMP_BRIDGE_PROVENANCE_LABEL,
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
/** The vendor's own child-env composition: parent env plus the managed entry's env
|
|
355
|
+
* (`oh-my-pi` v18.0.0 `mcp/transports/stdio.ts:575-607`). Never a hand-picked subset. */
|
|
356
|
+
function composeChildEnv(hostEnv: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
357
|
+
return { ...hostEnv, ...managedEntryEnv };
|
|
358
|
+
}
|
|
359
|
+
function bundleEquals(a: MetaRootBundle, b: MetaRootBundle): boolean {
|
|
360
|
+
return (
|
|
361
|
+
a.sessionsDir === b.sessionsDir &&
|
|
362
|
+
a.mailboxDir === b.mailboxDir &&
|
|
363
|
+
a.sendersDir === b.sendersDir &&
|
|
364
|
+
a.receiversDir === b.receiversDir
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
interface RootCell {
|
|
369
|
+
label: string;
|
|
370
|
+
home: string;
|
|
371
|
+
want: MetaRootBundle;
|
|
372
|
+
/** Set only by the override cell; the default cell must exercise the DEFAULT policy. */
|
|
373
|
+
overrides?: MetaRootBundle;
|
|
374
|
+
}
|
|
375
|
+
// CELL 1 — the DEFAULT policy under a poisoned, profile-shaped PI value. This is the cell
|
|
376
|
+
// the exact-once mutant has to fail: restore the legacy PI fallback inside the shared leaf
|
|
377
|
+
// and the whole bundle moves under `<home>/omp-profile-agent`.
|
|
378
|
+
const defaultCellHome = path.join(root, "roots-default");
|
|
379
|
+
// CELL 2 — all four `ENTWURF_META_*` overrides, at four DISTINCT paths. Hard Rule 12
|
|
380
|
+
// sandboxing has to keep working, and each override has to be honoured on its own axis:
|
|
381
|
+
// one shared prefix would let a single wrong join satisfy all four assertions.
|
|
382
|
+
const overrideCellHome = path.join(root, "roots-override");
|
|
383
|
+
const overrideCellDirs: MetaRootBundle = {
|
|
384
|
+
sessionsDir: path.join(overrideCellHome, "ov-a", "records"),
|
|
385
|
+
mailboxDir: path.join(overrideCellHome, "ov-b", "post"),
|
|
386
|
+
sendersDir: path.join(overrideCellHome, "ov-c", "who-sent"),
|
|
387
|
+
receiversDir: path.join(overrideCellHome, "ov-d", "watchers"),
|
|
388
|
+
};
|
|
389
|
+
const ROOT_CELLS: RootCell[] = [
|
|
390
|
+
{
|
|
391
|
+
label: "default roots",
|
|
392
|
+
home: defaultCellHome,
|
|
393
|
+
want: {
|
|
394
|
+
sessionsDir: path.join(defaultCellHome, ".pi", "agent", "meta-sessions"),
|
|
395
|
+
mailboxDir: path.join(defaultCellHome, ".pi", "agent", "meta-mailbox"),
|
|
396
|
+
sendersDir: path.join(defaultCellHome, ".pi", "agent", "meta-senders"),
|
|
397
|
+
receiversDir: path.join(defaultCellHome, ".pi", "agent", "meta-receivers"),
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
label: "four distinct overrides",
|
|
402
|
+
home: overrideCellHome,
|
|
403
|
+
want: overrideCellDirs,
|
|
404
|
+
overrides: overrideCellDirs,
|
|
405
|
+
},
|
|
406
|
+
];
|
|
407
|
+
|
|
408
|
+
for (const cell of ROOT_CELLS) {
|
|
409
|
+
const hostEnv = isolatedEnv(cell.home, cell.overrides);
|
|
410
|
+
ok(
|
|
411
|
+
`[QK:OMP-META-ROOT-JOINT-BINDING] ${cell.label}: the shared OMP leaf resolves all four roots to the expected paths`,
|
|
412
|
+
bundleEquals(ompMetaRoots(hostEnv, cell.home), cell.want),
|
|
413
|
+
);
|
|
414
|
+
const childEnv = composeChildEnv(hostEnv);
|
|
415
|
+
const applied = applyOmpBridgeChildRootPolicy(childEnv, cell.home);
|
|
416
|
+
ok(
|
|
417
|
+
`${cell.label}: the OMP-labeled bridge child resolves the SAME four roots`,
|
|
418
|
+
applied.applied && applied.roots !== null && bundleEquals(applied.roots, cell.want),
|
|
419
|
+
);
|
|
420
|
+
ok(
|
|
421
|
+
`${cell.label}: the child dropped the foreign PI_CODING_AGENT_DIR from its OWN env`,
|
|
422
|
+
childEnv.PI_CODING_AGENT_DIR === undefined,
|
|
423
|
+
);
|
|
424
|
+
ok(
|
|
425
|
+
`${cell.label}: every default-root consumer in that child now reads the expected bundle`,
|
|
426
|
+
childEnv.ENTWURF_META_SESSIONS_DIR === cell.want.sessionsDir &&
|
|
427
|
+
childEnv.ENTWURF_META_MAILBOX_DIR === cell.want.mailboxDir &&
|
|
428
|
+
childEnv.ENTWURF_META_SENDERS_DIR === cell.want.sendersDir &&
|
|
429
|
+
childEnv.ENTWURF_META_RECEIVERS_DIR === cell.want.receiversDir,
|
|
430
|
+
);
|
|
431
|
+
// ADDITIVE, not a global rewrite: a child that carries another harness's label keeps
|
|
432
|
+
// its environment exactly as the vendor handed it over.
|
|
433
|
+
const foreignChild = composeChildEnv(hostEnv);
|
|
434
|
+
foreignChild.ENTWURF_BRIDGE_EXTERNAL_AGENT_ID = "external-mcp/claude-code";
|
|
435
|
+
const foreignApplied = applyOmpBridgeChildRootPolicy(foreignChild, cell.home);
|
|
436
|
+
ok(
|
|
437
|
+
`${cell.label}: a child carrying another harness's label keeps PI and gets no OMP policy`,
|
|
438
|
+
!foreignApplied.applied && foreignChild.PI_CODING_AGENT_DIR === piPoison(cell.home),
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ── 2.6 the OMP meta-root override grammar ─────────────────────────────────
|
|
443
|
+
//
|
|
444
|
+
// A garden root has to mean the same directory in two processes that do NOT share a
|
|
445
|
+
// working directory: the extension's cwd is wherever the operator launched omp, and the
|
|
446
|
+
// doctor's is the repository (`run_ts` cd's there). `path.resolve` on a relative value
|
|
447
|
+
// therefore makes cwd an authority, and the two halves silently address different stores —
|
|
448
|
+
// measured with one `ENTWURF_META_SESSIONS_DIR=relative-records` landing in two places
|
|
449
|
+
// (#87 A2, Terra review). So the grammar is absolute or `~`/`~/…`, and everything else is a
|
|
450
|
+
// NAMED refusal both halves receive from the same leaf. `~user` is refused too: it is not
|
|
451
|
+
// HOME, and nothing here resolves another account's home.
|
|
452
|
+
{
|
|
453
|
+
const relHome = path.join(root, "roots-relative");
|
|
454
|
+
for (const rejected of ["relative-records", "./relative-records", "../sideways", "~other/records"]) {
|
|
455
|
+
const hostEnv = isolatedEnv(relHome);
|
|
456
|
+
hostEnv.ENTWURF_META_SESSIONS_DIR = rejected;
|
|
457
|
+
let leafRefusal: unknown;
|
|
458
|
+
try {
|
|
459
|
+
ompMetaRoots(hostEnv, relHome);
|
|
460
|
+
} catch (err) {
|
|
461
|
+
leafRefusal = err;
|
|
462
|
+
}
|
|
463
|
+
ok(
|
|
464
|
+
`[QK:OMP-META-ROOT-ABSOLUTE-ONLY] the shared leaf REFUSES ${JSON.stringify(rejected)} instead of resolving it against a cwd`,
|
|
465
|
+
leafRefusal instanceof MetaRootPolicyError && leafRefusal.message.includes("ENTWURF_META_SESSIONS_DIR"),
|
|
466
|
+
);
|
|
467
|
+
const childEnv = composeChildEnv(hostEnv);
|
|
468
|
+
let childRefusal: unknown;
|
|
469
|
+
try {
|
|
470
|
+
applyOmpBridgeChildRootPolicy(childEnv, relHome);
|
|
471
|
+
} catch (err) {
|
|
472
|
+
childRefusal = err;
|
|
473
|
+
}
|
|
474
|
+
ok(
|
|
475
|
+
`the OMP-labeled bridge child gets the SAME refusal for ${JSON.stringify(rejected)}`,
|
|
476
|
+
childRefusal instanceof MetaRootPolicyError,
|
|
477
|
+
);
|
|
478
|
+
// The refusal happens BEFORE any mutation — the policy resolves first and only then
|
|
479
|
+
// touches the env — so a refused child is left exactly as the vendor handed it over.
|
|
480
|
+
ok(
|
|
481
|
+
`${JSON.stringify(rejected)}: the refused child mutated NOTHING (PI kept, no override pinned)`,
|
|
482
|
+
childEnv.PI_CODING_AGENT_DIR === piPoison(relHome) &&
|
|
483
|
+
childEnv.ENTWURF_META_SESSIONS_DIR === rejected &&
|
|
484
|
+
childEnv.ENTWURF_META_MAILBOX_DIR === undefined &&
|
|
485
|
+
childEnv.ENTWURF_META_SENDERS_DIR === undefined &&
|
|
486
|
+
childEnv.ENTWURF_META_RECEIVERS_DIR === undefined,
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
// The ACCEPTED half of the same grammar, so the refusal above is a boundary and not a ban.
|
|
490
|
+
for (const [accepted, want] of [
|
|
491
|
+
["~", path.join(relHome, "meta-sessions")],
|
|
492
|
+
["~/garden/records", path.join(relHome, "garden", "records")],
|
|
493
|
+
] as Array<[string, string]>) {
|
|
494
|
+
const hostEnv = isolatedEnv(relHome);
|
|
495
|
+
hostEnv.ENTWURF_META_SESSIONS_DIR = accepted;
|
|
496
|
+
ok(
|
|
497
|
+
`${JSON.stringify(accepted)} is accepted and expands from HOME, which both halves share`,
|
|
498
|
+
// `~` alone names the root itself, so the leaf appends nothing — assert the exact
|
|
499
|
+
// literal each form must produce rather than a prefix.
|
|
500
|
+
ompMetaRoots(hostEnv, relHome).sessionsDir === (accepted === "~" ? relHome : want),
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// ── 2.7 the child policy selects on the EXACT raw provenance label ──────────
|
|
506
|
+
// The writer emits the literal and `doctor-omp-mcp` compares the literal, so a
|
|
507
|
+
// whitespace-drifted entry is FOREIGN to both. Trimming here made it foreign to the doctor
|
|
508
|
+
// and ours to the child — an entry the doctor calls red would still mutate that child's
|
|
509
|
+
// garden roots (#87 A3, Terra review).
|
|
510
|
+
{
|
|
511
|
+
const labelHome = path.join(root, "label-drift");
|
|
512
|
+
for (const drifted of [
|
|
513
|
+
" external-mcp/omp",
|
|
514
|
+
"external-mcp/omp ",
|
|
515
|
+
"\texternal-mcp/omp",
|
|
516
|
+
"external-mcp/omp\t",
|
|
517
|
+
"external-mcp/omp\n",
|
|
518
|
+
"external-mcp/OMP",
|
|
519
|
+
]) {
|
|
520
|
+
const hostEnv = isolatedEnv(labelHome);
|
|
521
|
+
const childEnv = composeChildEnv(hostEnv);
|
|
522
|
+
childEnv.ENTWURF_BRIDGE_EXTERNAL_AGENT_ID = drifted;
|
|
523
|
+
const applied = applyOmpBridgeChildRootPolicy(childEnv, labelHome);
|
|
524
|
+
ok(
|
|
525
|
+
`a drifted provenance label ${JSON.stringify(drifted)} does NOT select the OMP root policy`,
|
|
526
|
+
!applied.applied && applied.roots === null,
|
|
527
|
+
);
|
|
528
|
+
ok(
|
|
529
|
+
`${JSON.stringify(drifted)}: PI and all four overrides are untouched in that child`,
|
|
530
|
+
childEnv.PI_CODING_AGENT_DIR === piPoison(labelHome) &&
|
|
531
|
+
childEnv.ENTWURF_META_SESSIONS_DIR === undefined &&
|
|
532
|
+
childEnv.ENTWURF_META_MAILBOX_DIR === undefined &&
|
|
533
|
+
childEnv.ENTWURF_META_SENDERS_DIR === undefined &&
|
|
534
|
+
childEnv.ENTWURF_META_RECEIVERS_DIR === undefined,
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// ── 3. the visible tui host is born ──────────────────────────────────────────
|
|
540
|
+
const store = path.join(root, "store");
|
|
541
|
+
const born = host(store, [
|
|
542
|
+
{ event: "session_start", mode: "tui", cwd: CWD, managerCwd: CWD, sessionId: HOST_ID, sessionFile: HOST_FILE },
|
|
543
|
+
]);
|
|
544
|
+
ok("the mock host exited 0 — best-effort, never breaks the operator's turn", born.status === 0);
|
|
545
|
+
// BOTH edges, because the name does not tell you when it fires: `/new`, fork and in-TUI
|
|
546
|
+
// resume re-fire as session_switch, NOT session_start (`agent-session.ts:6910-8074`, audit
|
|
547
|
+
// C2). A unit bound to session_start alone leaves every post-`/new` session unminted while
|
|
548
|
+
// its status line still shows the previous citizen's id.
|
|
549
|
+
// Three more edges joined them in #87 Bundle C. The fresh bootstrap releases the caller's task
|
|
550
|
+
// only after it has SEEN the exact callback tool succeed — `tool_call`/`tool_result` are the
|
|
551
|
+
// only public surface that says so (`extensions/types.ts:1271-1278`) — and it DELIVERS that
|
|
552
|
+
// task at the next `turn_end`, because `[LIVE 2026-08-30]` a send from inside the tool_result
|
|
553
|
+
// handler never reached the session at all. All three are bound unconditionally because the
|
|
554
|
+
// flag's VALUE is not readable at factory time (see the factory's own note), so this assertion
|
|
555
|
+
// is also the record that a manual omp session pays those null checks, deliberately.
|
|
556
|
+
ok(
|
|
557
|
+
"[QK:OMP-BIRTH-BINDS-BOTH-EDGES] the factory binds exactly the two birth edges, the two bootstrap tool edges and the turn_end boundary the task is delivered on",
|
|
558
|
+
JSON.stringify(born.boundEvents) ===
|
|
559
|
+
JSON.stringify(["session_start", "session_switch", "tool_call", "tool_result", "turn_end"]),
|
|
560
|
+
);
|
|
561
|
+
ok(
|
|
562
|
+
"[QK:OMP-BIRTH-REGISTERS-BOOTSTRAP-FLAG] the factory registers the one-purpose string flag BEFORE argv is reparsed — a flag registered later is never filled (`main.ts:1799-1810`)",
|
|
563
|
+
JSON.stringify(born.flags) === JSON.stringify([["entwurf-bootstrap", "string"]]),
|
|
564
|
+
);
|
|
565
|
+
// THE MANUAL HOST IS THE COMMON CASE AND IT MUST NOTICE NOTHING. Every scenario in this gate
|
|
566
|
+
// is a birth without a bootstrap flag — an operator's own `omp`. A unit that started a turn
|
|
567
|
+
// here would be typing into the operator's session uninvited, which is a strictly worse
|
|
568
|
+
// failure than never bootstrapping at all.
|
|
569
|
+
ok(
|
|
570
|
+
"[QK:OMP-BIRTH-NO-FLAG-NO-TURN] a birth with no --entwurf-bootstrap flag sends NOTHING — no callback prompt, no task, no model turn",
|
|
571
|
+
born.sent.length === 0,
|
|
572
|
+
);
|
|
573
|
+
let live = records(store);
|
|
574
|
+
ok("[QK:OMP-BIRTH-TUI-MINTS] a tui session_start minted exactly one record", live.length === 1);
|
|
575
|
+
ok("the record's backend is omp", live[0]?.backend === "omp");
|
|
576
|
+
ok("the record joins on the vendor's own session id", live[0]?.nativeSessionId === HOST_ID);
|
|
577
|
+
ok("the record carries the session's cwd", live[0]?.cwd === CWD);
|
|
578
|
+
ok(
|
|
579
|
+
"the record carries the vendor's transcript path and does not guess a model",
|
|
580
|
+
live[0]?.transcriptPath === HOST_FILE && live[0]?.model === null,
|
|
581
|
+
);
|
|
582
|
+
const bornGardenId = live[0]?.gardenId ?? "";
|
|
583
|
+
|
|
584
|
+
// ── 4. visible identity (step 4) ─────────────────────────────────────────────
|
|
585
|
+
// `setStatus` is the ONLY surface that renders extension-owned text on a v18 TUI
|
|
586
|
+
// (`setFooter`/`setHeader` are literal no-ops, `extension-ui-controller.ts:139`; the
|
|
587
|
+
// built-in segment set is a closed enum). A citizen whose address is only visible by
|
|
588
|
+
// scraping records is not lifecycle parity (`adding-a-harness.md` step 4).
|
|
589
|
+
ok(
|
|
590
|
+
"[QK:OMP-BIRTH-SHOWS-GARDEN-ID] the host's garden id was written to the status line",
|
|
591
|
+
born.statuses.length === 1 && born.statuses[0][1].includes(bornGardenId),
|
|
592
|
+
);
|
|
593
|
+
ok("it is written under one owned key, not over the operator's line", born.statuses[0][0] === "entwurf");
|
|
594
|
+
|
|
595
|
+
// ── 5. the §3.5 scope fence ──────────────────────────────────────────────────
|
|
596
|
+
// EVERY task subagent re-emits session_start against a fresh copy of this factory and
|
|
597
|
+
// inherits the parent's MCP proxy tools by default (`task/executor.ts:3305`, `:3021-3024`),
|
|
598
|
+
// so this is the only thing standing between one visible sibling and a garden id per
|
|
599
|
+
// internal agent. `hasUI` is deliberately TRUE in the rpc rows below: it is true under
|
|
600
|
+
// rpc/rpc-ui/ACP in the real vendor too (`runner.ts:879-881`), so a fence that consulted
|
|
601
|
+
// it would admit exactly the sessions this refuses.
|
|
602
|
+
// THE DECISIVE ROW FIRST, with the claim's literal signature on it:
|
|
603
|
+
// a real task subagent runs `mode:"print"` with `hasUI:false` on the SAME OS pid as its
|
|
604
|
+
// host (`task/executor.ts:3115`, `:3252`, `:3305`), and it must leave the store exactly
|
|
605
|
+
// as it found it — the LIVE receipt for this is 519 records before and 519 after
|
|
606
|
+
// (`scripts/raw-omp-measure/README.md` §3.5).
|
|
607
|
+
{
|
|
608
|
+
const hostStore = path.join(root, "host-then-subagent");
|
|
609
|
+
host(hostStore, [
|
|
610
|
+
{ event: "session_start", mode: "tui", cwd: CWD, managerCwd: CWD, sessionId: HOST_ID, sessionFile: HOST_FILE },
|
|
611
|
+
]);
|
|
612
|
+
const before = records(hostStore).length;
|
|
613
|
+
host(hostStore, [
|
|
614
|
+
{
|
|
615
|
+
event: "session_start",
|
|
616
|
+
mode: "print",
|
|
617
|
+
hasUI: false,
|
|
618
|
+
cwd: CWD,
|
|
619
|
+
managerCwd: CWD,
|
|
620
|
+
sessionId: "01a042dc-9540-7643-b3ef-99d223b459a9",
|
|
621
|
+
sessionFile: `${HOST_FILE}/Greeting.jsonl`,
|
|
622
|
+
},
|
|
623
|
+
]);
|
|
624
|
+
ok(
|
|
625
|
+
"[QK:OMP-BIRTH-SCOPE-FENCE] a task subagent's own session_start mints NOTHING — one visible host, one garden id",
|
|
626
|
+
before === 1 && records(hostStore).length === 1,
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
for (const [label, mode, hasUI] of [
|
|
631
|
+
["a task subagent (print)", "print", false],
|
|
632
|
+
["rpc", "rpc", true],
|
|
633
|
+
["rpc-ui / acp (mode is rpc, hasUI is TRUE)", "rpc", true],
|
|
634
|
+
["--mode json", "json", false],
|
|
635
|
+
] as Array<[string, string, boolean]>) {
|
|
636
|
+
const negStore = path.join(root, `neg-${mode}-${hasUI}`);
|
|
637
|
+
const res = host(negStore, [
|
|
638
|
+
{
|
|
639
|
+
event: "session_start",
|
|
640
|
+
mode,
|
|
641
|
+
hasUI,
|
|
642
|
+
cwd: CWD,
|
|
643
|
+
managerCwd: CWD,
|
|
644
|
+
sessionId: `sub-${mode}-${hasUI}`,
|
|
645
|
+
sessionFile: HOST_FILE,
|
|
646
|
+
},
|
|
647
|
+
]);
|
|
648
|
+
ok(`${label}: exits 0`, res.status === 0);
|
|
649
|
+
ok(`${label}: mints NO record — it is not a citizen`, records(negStore).length === 0);
|
|
650
|
+
ok(`${label}: writes no sender marker either`, !agentEntries(negStore).includes("meta-senders"));
|
|
651
|
+
ok(`${label}: touches no status line`, res.statuses.length === 0);
|
|
652
|
+
ok(
|
|
653
|
+
`${label}: leaves the refusal in the log the doctor reads`,
|
|
654
|
+
hookLog(negStore).includes(`scope-refused edge=session_start mode=${mode}`),
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// ── 6. the re-birth edge ─────────────────────────────────────────────────────
|
|
659
|
+
// `/new`, fork and in-TUI resume fire session_switch, NOT session_start
|
|
660
|
+
// (`agent-session.ts:6910-8074`, audit C2). A unit wired to session_start alone leaves
|
|
661
|
+
// every post-`/new` session unminted while its status line still shows the previous
|
|
662
|
+
// citizen's id.
|
|
663
|
+
const sw = host(store, [
|
|
664
|
+
{
|
|
665
|
+
event: "session_switch",
|
|
666
|
+
mode: "tui",
|
|
667
|
+
reason: "resume",
|
|
668
|
+
cwd: CWD,
|
|
669
|
+
managerCwd: CWD,
|
|
670
|
+
sessionId: HOST_ID,
|
|
671
|
+
sessionFile: HOST_FILE,
|
|
672
|
+
},
|
|
673
|
+
]);
|
|
674
|
+
live = records(store);
|
|
675
|
+
ok(
|
|
676
|
+
"a session_switch on the SAME native id attaches — still one record, same garden id",
|
|
677
|
+
live.length === 1 && live[0]?.gardenId === bornGardenId && sw.status === 0,
|
|
678
|
+
);
|
|
679
|
+
ok(
|
|
680
|
+
"and it re-renders the same garden id on the status line",
|
|
681
|
+
sw.statuses.length === 1 && sw.statuses[0][1].includes(bornGardenId),
|
|
682
|
+
);
|
|
683
|
+
|
|
684
|
+
const NEW_ID = "01a042dc-9540-7643-b3ef-99d223b459a9";
|
|
685
|
+
const swNew = host(store, [
|
|
686
|
+
{
|
|
687
|
+
event: "session_switch",
|
|
688
|
+
mode: "tui",
|
|
689
|
+
reason: "new",
|
|
690
|
+
cwd: CWD,
|
|
691
|
+
managerCwd: CWD,
|
|
692
|
+
sessionId: NEW_ID,
|
|
693
|
+
sessionFile: HOST_FILE,
|
|
694
|
+
},
|
|
695
|
+
]);
|
|
696
|
+
live = records(store);
|
|
697
|
+
ok(
|
|
698
|
+
"a session_switch to a NEW native id mints the replacement's own record",
|
|
699
|
+
live.length === 2 && live.some((r) => r.nativeSessionId === NEW_ID) && swNew.status === 0,
|
|
700
|
+
);
|
|
701
|
+
ok("the replacement is its own citizen, not a rewrite of the first", new Set(live.map((r) => r.gardenId)).size === 2);
|
|
702
|
+
|
|
703
|
+
// ── 7. WHO-SENT: the ONE-PROCESS join ────────────────────────────────────────
|
|
704
|
+
// This is the step-6 fact that differs from every exec'd backend. The bridge child's
|
|
705
|
+
// parent is the omp host itself (`mcp/transports/stdio.ts:41-57`: Linux `detached` is
|
|
706
|
+
// setsid only, so the child keeps omp as its parent), and the extension runs INSIDE that
|
|
707
|
+
// host — so the marker must be keyed to the host's OWN pid. Keyed to `process.ppid` it
|
|
708
|
+
// would name the shell that launched omp, and every marker would be looked up under a pid
|
|
709
|
+
// no bridge child is a child of.
|
|
710
|
+
const sendersDir = path.join(agentDir(store), "meta-senders");
|
|
711
|
+
const ownMarker = path.join(sendersDir, "omp", `${swNew.pid}.json`);
|
|
712
|
+
ok("[QK:OMP-SENDER-MARKER-OWN-PID] the marker is keyed to the HOST process's own pid", existsSync(ownMarker));
|
|
713
|
+
ok(
|
|
714
|
+
"and NOT to its parent — the gate is that parent, and a marker there would name the launching shell",
|
|
715
|
+
!existsSync(path.join(sendersDir, "omp", `${process.pid}.json`)),
|
|
716
|
+
);
|
|
717
|
+
const marker = JSON.parse(readFileSync(ownMarker, "utf8")) as {
|
|
718
|
+
backend?: string;
|
|
719
|
+
gardenId?: string;
|
|
720
|
+
nativeSessionId?: string;
|
|
721
|
+
ownerPid?: number;
|
|
722
|
+
ownerStartKey?: string;
|
|
723
|
+
};
|
|
724
|
+
ok(
|
|
725
|
+
"the marker names the citizen minted on that edge",
|
|
726
|
+
marker.gardenId === live.find((r) => r.nativeSessionId === NEW_ID)?.gardenId,
|
|
727
|
+
);
|
|
728
|
+
ok(
|
|
729
|
+
"the marker agrees with the record on backend and native id",
|
|
730
|
+
marker.backend === "omp" && marker.nativeSessionId === NEW_ID,
|
|
731
|
+
);
|
|
732
|
+
ok("the marker's ownerPid IS the host process's pid", marker.ownerPid === swNew.pid);
|
|
733
|
+
ok(
|
|
734
|
+
"the marker carries the owner's start-key, so a reused pid cannot inherit this citizen",
|
|
735
|
+
typeof marker.ownerStartKey === "string" && marker.ownerStartKey.length > 0,
|
|
736
|
+
);
|
|
737
|
+
ok(
|
|
738
|
+
"the gate's own start-key is a different value — the two pids are genuinely different processes",
|
|
739
|
+
marker.ownerStartKey !== processStartKey(process.pid) || swNew.pid === process.pid,
|
|
740
|
+
);
|
|
741
|
+
|
|
742
|
+
// The READ half. A marker nobody looks for is invisible — that asymmetry is exactly how
|
|
743
|
+
// #46 made an agy citizen send as an anonymous external host.
|
|
744
|
+
ok(
|
|
745
|
+
"[QK:OMP-SENDER-READER-OPEN] omp is one of the backends the resolver scans — writer and reader open together or not at all",
|
|
746
|
+
META_SENDER_BACKENDS.includes("omp"),
|
|
747
|
+
);
|
|
748
|
+
function withSessionsDir<T>(dir: string, fn: () => T): T {
|
|
749
|
+
const prev = process.env.ENTWURF_META_SESSIONS_DIR;
|
|
750
|
+
process.env.ENTWURF_META_SESSIONS_DIR = dir;
|
|
751
|
+
try {
|
|
752
|
+
return fn();
|
|
753
|
+
} finally {
|
|
754
|
+
if (prev === undefined) delete process.env.ENTWURF_META_SESSIONS_DIR;
|
|
755
|
+
else process.env.ENTWURF_META_SESSIONS_DIR = prev;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
// Against a LIVE host, because that is the only state the resolver is allowed to trust.
|
|
759
|
+
{
|
|
760
|
+
const liveStore = path.join(root, "live-owner");
|
|
761
|
+
const held = hostHeld(liveStore, [
|
|
762
|
+
{
|
|
763
|
+
event: "session_start",
|
|
764
|
+
mode: "tui",
|
|
765
|
+
cwd: CWD,
|
|
766
|
+
managerCwd: CWD,
|
|
767
|
+
sessionId: "omp-live-0001",
|
|
768
|
+
sessionFile: HOST_FILE,
|
|
769
|
+
},
|
|
770
|
+
]);
|
|
771
|
+
try {
|
|
772
|
+
const liveSenders = path.join(agentDir(liveStore), "meta-senders");
|
|
773
|
+
const trusted = withSessionsDir(path.join(agentDir(liveStore), "meta-sessions"), () =>
|
|
774
|
+
resolveTrustedMetaSenderIdentity({ ownerPids: [held.pid], sendersDir: liveSenders }),
|
|
775
|
+
);
|
|
776
|
+
ok(
|
|
777
|
+
"the bridge resolver joins that marker to exactly ONE identity — the live citizen that wrote it",
|
|
778
|
+
trusted?.identity.nativeSessionId === "omp-live-0001" && trusted?.identity.backend === "omp",
|
|
779
|
+
);
|
|
780
|
+
// The pid a real bridge child would present is its PARENT — the omp host — which is
|
|
781
|
+
// exactly the pid this marker is keyed to. Reading it under the host's parent (this
|
|
782
|
+
// gate) must find nothing: that is the difference between the one-process join and
|
|
783
|
+
// the exec'd backends' parent join, checked from the reader's side too.
|
|
784
|
+
ok(
|
|
785
|
+
"and the host's PARENT resolves to nobody — the join key is the host itself",
|
|
786
|
+
withSessionsDir(path.join(agentDir(liveStore), "meta-sessions"), () =>
|
|
787
|
+
resolveTrustedMetaSenderIdentity({ ownerPids: [process.pid], sendersDir: liveSenders }),
|
|
788
|
+
) === null,
|
|
789
|
+
);
|
|
790
|
+
} finally {
|
|
791
|
+
held.kill();
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// A marker whose owner has EXITED names nobody: the pid+start-key guard is what stops a
|
|
796
|
+
// dead session's reused pid from inheriting its garden id. `swNew.pid` is exactly that —
|
|
797
|
+
// the host that minted the replacement above and then exited.
|
|
798
|
+
ok(
|
|
799
|
+
"a marker whose owner process is gone resolves to NOBODY — liveness is a guard, not a formality",
|
|
800
|
+
withSessionsDir(path.join(agentDir(store), "meta-sessions"), () =>
|
|
801
|
+
resolveTrustedMetaSenderIdentity({ ownerPids: [swNew.pid], sendersDir }),
|
|
802
|
+
) === null,
|
|
803
|
+
);
|
|
804
|
+
|
|
805
|
+
// The record store is the authority; the marker is only a hint it must agree with.
|
|
806
|
+
{
|
|
807
|
+
const orphanStore = path.join(root, "orphan-marker");
|
|
808
|
+
const orphan = hostHeld(orphanStore, [
|
|
809
|
+
{
|
|
810
|
+
event: "session_start",
|
|
811
|
+
mode: "tui",
|
|
812
|
+
cwd: CWD,
|
|
813
|
+
managerCwd: CWD,
|
|
814
|
+
sessionId: "omp-orphan-0001",
|
|
815
|
+
sessionFile: HOST_FILE,
|
|
816
|
+
},
|
|
817
|
+
]);
|
|
818
|
+
try {
|
|
819
|
+
const orphanSenders = path.join(agentDir(orphanStore), "meta-senders");
|
|
820
|
+
ok(
|
|
821
|
+
"precondition: that store has its own marker too",
|
|
822
|
+
existsSync(path.join(orphanSenders, "omp", `${orphan.pid}.json`)),
|
|
823
|
+
);
|
|
824
|
+
for (const f of readdirSync(path.join(agentDir(orphanStore), "meta-sessions")))
|
|
825
|
+
rmSync(path.join(agentDir(orphanStore), "meta-sessions", f));
|
|
826
|
+
// The owner is still ALIVE here, so this isolates the record-authority guard alone:
|
|
827
|
+
// the marker is only a pid->garden hint, and the store is what must vouch for it.
|
|
828
|
+
ok(
|
|
829
|
+
"a marker whose record is gone resolves to NOBODY even while its owner lives — a hint is not an identity",
|
|
830
|
+
withSessionsDir(path.join(agentDir(orphanStore), "meta-sessions"), () =>
|
|
831
|
+
resolveTrustedMetaSenderIdentity({ ownerPids: [orphan.pid], sendersDir: orphanSenders }),
|
|
832
|
+
) === null,
|
|
833
|
+
);
|
|
834
|
+
} finally {
|
|
835
|
+
orphan.kill();
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// ── 8. birth does NOT arm a receiver ─────────────────────────────────────────
|
|
840
|
+
// omp HAS a documented wake surface (`pi.sendUserMessage`, "idle starts a turn"), and
|
|
841
|
+
// that is exactly why this assertion matters: a receiver marker claims that a LIVE
|
|
842
|
+
// process holds a watch. Nothing this unit installs holds one, so a marker written here
|
|
843
|
+
// would make the citizen read deliverable, wired to nothing, for as long as the TUI
|
|
844
|
+
// stayed open. Receive is bundle B's separate admission.
|
|
845
|
+
//
|
|
846
|
+
// ASKED THROUGH THE CERTIFIED READER, NOT THROUGH `readdir` (#87 D3). "No `meta-receivers`
|
|
847
|
+
// directory" is a weaker claim than it looks: an empty directory arms nothing and changes
|
|
848
|
+
// no dispatch fact, so a mutant that merely created one was killed for the wrong reason
|
|
849
|
+
// while a real record-bound marker would have sailed past. The question dispatch actually
|
|
850
|
+
// asks is `readMetaReceiverMarker(gardenId)`, and that is the question asked here — for
|
|
851
|
+
// EVERY citizen this store minted, with the liveness guard off so a dead owner cannot
|
|
852
|
+
// hide a marker that exists.
|
|
853
|
+
{
|
|
854
|
+
const receiversDir = path.join(agentDir(store), "meta-receivers");
|
|
855
|
+
const armed = live.filter(
|
|
856
|
+
(record) => readMetaReceiverMarker({ gardenId: record.gardenId, receiversDir, verifyOwner: false }) !== null,
|
|
857
|
+
);
|
|
858
|
+
ok(
|
|
859
|
+
"[QK:OMP-BIRTH-DOES-NOT-ARM-RECEIVER] the certified receiver reader finds NO marker for ANY citizen birth minted",
|
|
860
|
+
live.length >= 2 && armed.length === 0,
|
|
861
|
+
);
|
|
862
|
+
ok("and birth opens no mailbox root either", !agentEntries(store).includes("meta-mailbox"));
|
|
863
|
+
}
|
|
864
|
+
ok("omp is NOT native-push — a sender marker buys who-sent, never replyability", nativePushSupported("omp") === false);
|
|
865
|
+
|
|
866
|
+
// ── 9. the citizen is a PEER, and an honest one ─────────────────────────────
|
|
867
|
+
const facts = resolveFactList(live, []);
|
|
868
|
+
const peer = facts.peers.find((p) => p.gardenId === bornGardenId);
|
|
869
|
+
ok("the minted citizen appears in the peer fact list", peer !== undefined);
|
|
870
|
+
ok("its liveness is `unsupported` — no control-socket probe exists for this backend", peer?.liveness === "unsupported");
|
|
871
|
+
|
|
872
|
+
// ── 10. negatives: every refusal is a REFUSAL, not a guessed record ─────────
|
|
873
|
+
function refuses(label: string, edge: Edge, expectInLog: string): void {
|
|
874
|
+
const negStore = path.join(root, `neg-${label.replace(/[^a-z0-9]+/gi, "-").slice(0, 40)}`);
|
|
875
|
+
const res = host(negStore, [edge]);
|
|
876
|
+
ok(`${label}: exits 0 (best-effort, never breaks the operator's turn)`, res.status === 0);
|
|
877
|
+
ok(`${label}: writes NO record`, records(negStore).length === 0);
|
|
878
|
+
ok(`${label}: logs an ERROR the doctor can read (${expectInLog})`, hookLog(negStore).includes(expectInLog));
|
|
879
|
+
}
|
|
880
|
+
// Two vendor-authoritative cwds that disagree is the case a `a ?? b` would swallow: the
|
|
881
|
+
// context we were handed is not trustworthy, so neither value may be minted.
|
|
882
|
+
refuses(
|
|
883
|
+
"[QK:OMP-BIRTH-CWD-DISAGREEMENT] disagreeing ctx.cwd and sessionManager.getCwd()",
|
|
884
|
+
{ event: "session_start", mode: "tui", cwd: CWD, managerCwd: "/tmp", sessionId: HOST_ID, sessionFile: HOST_FILE },
|
|
885
|
+
"disagree",
|
|
886
|
+
);
|
|
887
|
+
refuses(
|
|
888
|
+
"an empty session id",
|
|
889
|
+
{ event: "session_start", mode: "tui", cwd: CWD, managerCwd: CWD, sessionId: "", sessionFile: HOST_FILE },
|
|
890
|
+
"getSessionId() is not a non-empty string",
|
|
891
|
+
);
|
|
892
|
+
refuses(
|
|
893
|
+
"no cwd from either source",
|
|
894
|
+
{ event: "session_start", mode: "tui", sessionId: HOST_ID, sessionFile: HOST_FILE },
|
|
895
|
+
"neither ctx.cwd nor sessionManager.getCwd()",
|
|
896
|
+
);
|
|
897
|
+
refuses(
|
|
898
|
+
"no session manager at all",
|
|
899
|
+
{ event: "session_start", mode: "tui", cwd: CWD, sessionId: HOST_ID, noManager: true },
|
|
900
|
+
"ctx.sessionManager is missing",
|
|
901
|
+
);
|
|
902
|
+
|
|
903
|
+
// An absent transcript path is NOT a refusal: `getSessionFile()` is lazy and may
|
|
904
|
+
// legitimately name nothing yet (`session-manager.ts:1950-1952`).
|
|
905
|
+
{
|
|
906
|
+
const lazyStore = path.join(root, "lazy-file");
|
|
907
|
+
host(lazyStore, [{ event: "session_start", mode: "tui", cwd: CWD, managerCwd: CWD, sessionId: "omp-lazy-0001" }]);
|
|
908
|
+
const lazy = records(lazyStore);
|
|
909
|
+
ok(
|
|
910
|
+
"a session whose transcript file does not exist YET is still born, with a null transcriptPath",
|
|
911
|
+
lazy.length === 1 && lazy[0]?.transcriptPath === null,
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// A TUI with no usable status surface still becomes a citizen: rendering is not birth.
|
|
916
|
+
{
|
|
917
|
+
const noUiStore = path.join(root, "no-ui");
|
|
918
|
+
const res = host(noUiStore, [
|
|
919
|
+
{
|
|
920
|
+
event: "session_start",
|
|
921
|
+
mode: "tui",
|
|
922
|
+
cwd: CWD,
|
|
923
|
+
managerCwd: CWD,
|
|
924
|
+
sessionId: "omp-noui-0001",
|
|
925
|
+
sessionFile: HOST_FILE,
|
|
926
|
+
noUi: true,
|
|
927
|
+
},
|
|
928
|
+
]);
|
|
929
|
+
ok(
|
|
930
|
+
"a tui host with no setStatus is still minted — a missing render is not a missing citizen",
|
|
931
|
+
records(noUiStore).length === 1 && res.status === 0,
|
|
932
|
+
);
|
|
933
|
+
ok("and the doctor is told the id did not render", hookLog(noUiStore).includes("status-refused"));
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// ── 11. the shipped skeleton ────────────────────────────────────────────────
|
|
937
|
+
const shippedPkg = JSON.parse(
|
|
938
|
+
readFileSync(path.join(REPO, "pi", "meta-bridge-omp", UNIT, "package.json"), "utf8"),
|
|
939
|
+
) as Record<string, unknown>;
|
|
940
|
+
ok(
|
|
941
|
+
"the committed unit skeleton declares type:module and a non-empty version",
|
|
942
|
+
shippedPkg.type === "module" && typeof shippedPkg.version === "string" && (shippedPkg.version as string).length > 0,
|
|
943
|
+
);
|
|
944
|
+
ok(
|
|
945
|
+
"the committed skeleton ships no entry of its own — the entry is assembled from pi-extensions/",
|
|
946
|
+
!existsSync(path.join(REPO, "pi", "meta-bridge-omp", UNIT, "index.ts")),
|
|
947
|
+
);
|
|
948
|
+
|
|
949
|
+
// ── 12. the four-root joint binding, part 2 — the extension really writes there ──
|
|
950
|
+
// Part 1 proved the two halves of the policy AGREE. This proves the birth extension is
|
|
951
|
+
// actually BOUND to it: the assembled unit, run for real, must leave its record, its sender
|
|
952
|
+
// marker and its hook log at the same literal paths — and nothing at all below the poisoned
|
|
953
|
+
// vendor knob. Deliberately last: a root defect moves every artifact in this file, so an
|
|
954
|
+
// earlier position here would pre-empt the claims above and kill their mutants for the
|
|
955
|
+
// wrong reason.
|
|
956
|
+
for (const cell of ROOT_CELLS) {
|
|
957
|
+
const hostEnv = isolatedEnv(cell.home, cell.overrides);
|
|
958
|
+
mkdirSync(cell.home, { recursive: true });
|
|
959
|
+
const bornHere = runHost(hostEnv, [
|
|
960
|
+
{
|
|
961
|
+
event: "session_start",
|
|
962
|
+
mode: "tui",
|
|
963
|
+
cwd: CWD,
|
|
964
|
+
managerCwd: CWD,
|
|
965
|
+
sessionId: `omp-roots-${cell.label.replace(/[^a-z0-9]+/gi, "-")}`,
|
|
966
|
+
sessionFile: HOST_FILE,
|
|
967
|
+
},
|
|
968
|
+
]);
|
|
969
|
+
const minted = existsSync(cell.want.sessionsDir) ? listAllMetaIdentitiesDir(cell.want.sessionsDir).identities : [];
|
|
970
|
+
ok(
|
|
971
|
+
`${cell.label}: the extension minted its record under the expected sessions root`,
|
|
972
|
+
bornHere.status === 0 && minted.length === 1 && minted[0]?.backend === "omp",
|
|
973
|
+
);
|
|
974
|
+
ok(
|
|
975
|
+
`${cell.label}: its sender marker landed under the expected senders root`,
|
|
976
|
+
existsSync(path.join(cell.want.sendersDir, "omp", `${bornHere.pid}.json`)),
|
|
977
|
+
);
|
|
978
|
+
ok(
|
|
979
|
+
`${cell.label}: the hook log followed the same sessions-root policy`,
|
|
980
|
+
existsSync(path.join(path.dirname(cell.want.sessionsDir), "meta-bridge-hook.log")),
|
|
981
|
+
);
|
|
982
|
+
ok(`${cell.label}: NOTHING was written below the poisoned PI_CODING_AGENT_DIR`, !existsSync(piPoison(cell.home)));
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// ── 13. the meta-root refusal, end to end ──────────────────────────────────
|
|
986
|
+
// Part 2.6 proved the two halves refuse the same value. This proves what the refusal is
|
|
987
|
+
// WORTH: the real extension mints nothing anywhere, the refusal is still visible in the log
|
|
988
|
+
// (a policy that silenced its own diagnostic would be worse than the split), and the doctor
|
|
989
|
+
// goes RUNTIME RED instead of laundering an unrelated empty directory into NOT-YET.
|
|
990
|
+
// Deliberately last, for the same attribution reason as section 12.
|
|
991
|
+
{
|
|
992
|
+
const relHome = path.join(root, "relative-live");
|
|
993
|
+
mkdirSync(relHome, { recursive: true });
|
|
994
|
+
const hostEnv = isolatedEnv(relHome);
|
|
995
|
+
hostEnv.ENTWURF_META_SESSIONS_DIR = "relative-records";
|
|
996
|
+
const refused = runHost(hostEnv, [
|
|
997
|
+
{
|
|
998
|
+
event: "session_start",
|
|
999
|
+
mode: "tui",
|
|
1000
|
+
cwd: CWD,
|
|
1001
|
+
managerCwd: CWD,
|
|
1002
|
+
sessionId: "omp-relative-override",
|
|
1003
|
+
sessionFile: HOST_FILE,
|
|
1004
|
+
},
|
|
1005
|
+
]);
|
|
1006
|
+
ok(
|
|
1007
|
+
"a refused override mints NOTHING — not under HOME, and not against anyone's cwd",
|
|
1008
|
+
refused.status === 0 &&
|
|
1009
|
+
records(relHome).length === 0 &&
|
|
1010
|
+
!existsSync(path.join(relHome, "relative-records")) &&
|
|
1011
|
+
!existsSync(path.join(REPO, "relative-records")),
|
|
1012
|
+
);
|
|
1013
|
+
ok(
|
|
1014
|
+
"and the refusal is still LOGGED — the diagnostic falls back to the policy's own base, naming the variable",
|
|
1015
|
+
hookLog(relHome).includes("ENTWURF_META_SESSIONS_DIR"),
|
|
1016
|
+
);
|
|
1017
|
+
|
|
1018
|
+
// The doctor half of the same claim. Its own preflight must refuse BEFORE it asks
|
|
1019
|
+
// `meta-facts`, whose `run_ts` would answer from the repository's cwd instead.
|
|
1020
|
+
const docHome = path.join(root, "relative-doctor");
|
|
1021
|
+
const docAgent = path.join(docHome, ".omp", "agent");
|
|
1022
|
+
mkdirSync(docAgent, { recursive: true });
|
|
1023
|
+
const doctorEnv: NodeJS.ProcessEnv = {
|
|
1024
|
+
...isolatedEnv(docHome),
|
|
1025
|
+
ENTWURF_OMP_AGENT_DIR: docAgent,
|
|
1026
|
+
ENTWURF_META_SESSIONS_DIR: "relative-records",
|
|
1027
|
+
};
|
|
1028
|
+
const doctored = spawnSync("bash", [path.join(REPO, "run.sh"), "doctor-omp-bridge"], {
|
|
1029
|
+
env: doctorEnv,
|
|
1030
|
+
encoding: "utf8",
|
|
1031
|
+
});
|
|
1032
|
+
const doctorOut = `${doctored.stdout ?? ""}${doctored.stderr ?? ""}`;
|
|
1033
|
+
ok(
|
|
1034
|
+
"the doctor is RUNTIME RED on a refused override, not NOT-YET about some other directory",
|
|
1035
|
+
doctored.status !== 0 && doctorOut.includes("REFUSES") && doctorOut.includes("runtime axis: FAIL"),
|
|
1036
|
+
);
|
|
1037
|
+
// Matched on the VERDICT lines, not on the word "NOT-YET" — the refusal message names
|
|
1038
|
+
// that verdict in order to say it is refusing to give it, and an assertion that matched
|
|
1039
|
+
// its own explanation would pass for the wrong reason.
|
|
1040
|
+
ok(
|
|
1041
|
+
"and it never claimed a store or a citizen count from the refused environment",
|
|
1042
|
+
!doctorOut.includes("garden citizen proven on this host") &&
|
|
1043
|
+
!doctorOut.includes("NOT-YET: zero certified") &&
|
|
1044
|
+
!doctorOut.includes("omp garden roots resolve to"),
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
rmSync(root, { recursive: true, force: true });
|
|
1049
|
+
console.log(`[check-omp-birth-hook] ${passed} assertions ok`);
|