@junghanacs/entwurf 0.35.1 → 0.36.1
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 +2 -2
- package/CHANGELOG.md +403 -0
- package/CONTRIBUTING.md +1 -1
- package/DELIVERY.md +17 -0
- package/FAQ.md +3 -3
- package/README.md +30 -16
- package/VERIFY.md +12 -10
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +69 -5
- package/docs/setup-clean-host.md +30 -22
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +4 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/codex-ws-client.js +99 -18
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +3 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +59 -14
- package/mcp/entwurf-bridge/dist/scripts/codex-socket-classify.js +44 -0
- package/mcp/entwurf-bridge/tsconfig.build.json +5 -0
- package/package.json +21 -16
- package/pi/pi-durable/bootstrap.mjs +12 -8
- package/pi/pi-durable/carrier/runtime.js +52 -2
- package/pi/pi-durable/carrier/subagent.js +6 -1
- package/pi/pi-durable/carrier/tui.js +31 -4
- package/pi/pi-durable/carrier-relocation.mjs +2 -2
- package/pi/pi-durable/overlay/upstream-pin.json +79 -36
- package/pi-extensions/lib/codex-fresh-preflight.ts +4 -3
- package/pi-extensions/lib/native-push/codex-ws-client.ts +129 -18
- package/pi-extensions/lib/pi-durable-runtime.ts +4 -4
- package/pi-extensions/meta-bridge-pi-durable.ts +77 -21
- package/run.sh +116 -39
- package/scripts/check-acp-prompt-lifecycle.ts +4 -2
- package/scripts/check-acp-session-reuse.ts +4 -2
- package/scripts/check-acp-stop-reason.ts +4 -2
- package/scripts/check-acp-stream-hooks.ts +4 -2
- package/scripts/check-acp-usage-accounting.ts +4 -2
- package/scripts/check-codex-app-server-launch.ts +55 -12
- package/scripts/check-codex-native-push.ts +231 -12
- package/scripts/check-gate-qualification.ts +4 -4
- package/scripts/check-herdr-supply.ts +32 -7
- package/scripts/codex-app-server-launch.sh +54 -29
- package/scripts/codex-socket-classify.ts +46 -0
- package/scripts/emit-pi-durable-carrier.ts +159 -168
- package/scripts/fixtures/herdr-supply.json +10 -9
- package/scripts/lib/acp-gate-home.ts +75 -0
- package/scripts/lib/codex-socket-listener.ts +429 -0
- package/scripts/mutants/acp-stop-reason.json +11 -0
- package/scripts/mutants/codex-app-server-launch.json +39 -12
- package/scripts/mutants/codex-native.json +67 -0
- package/scripts/mutants/control-send-receipt.json +1 -1
- package/scripts/mutants/herdr-supply.json +6 -6
- package/scripts/mutants/pack-install.json +3 -3
- package/scripts/mutants/pi-durable.json +165 -16
- package/scripts/mutants/pi-floor.json +5 -5
- package/scripts/pi-durable-contact-host.mjs +1 -1
- package/scripts/smoke-codex-fresh-live.ts +31 -7
- package/scripts/smoke-pi-durable-fresh-live.ts +22 -3
- package/pi/pi-durable/overlay/runtime-contacts.patch +0 -283
|
@@ -181,18 +181,219 @@ assert.equal(
|
|
|
181
181
|
"[QK:CODEX-NATIVE-FIXED-SOCKET] CODEX_HOME owns the vendor default socket root",
|
|
182
182
|
);
|
|
183
183
|
|
|
184
|
-
|
|
184
|
+
// ── the control endpoint: a plain socket, or Codex's EXACT rendezvous alias (#141) ──────────
|
|
185
|
+
// From Codex 0.162.1 the advertised path is a symlink to a 0600 socket the vendor binds under
|
|
186
|
+
// `realpath(/tmp)/codex-daemon-<euid>/`, named by the sha256 hex of the canonical advertised
|
|
187
|
+
// parent + "/" + basename (`[source]` rust-v0.162.1 `app-server-transport/src/transport/
|
|
188
|
+
// unix_socket.rs:290-304`, `uds/src/daemon_directory.rs:13-38`). The expected target below is
|
|
189
|
+
// derived HERE from that upstream rule, not read back from the product, and every cell builds
|
|
190
|
+
// its own sandbox socket directory: `daemonRoot`/`euid` point the product at it, so the real
|
|
191
|
+
// `/tmp/codex-daemon-<euid>` is never touched. Each refusal cell keeps every OTHER hop valid,
|
|
192
|
+
// so it fails on its own rule and not on a neighbour's. A real other-uid owner needs root and
|
|
193
|
+
// stays unmeasured; so does macOS, where `/tmp` canonicalizes to `/private/tmp`.
|
|
194
|
+
//
|
|
195
|
+
// The root and the socket directory names are SHORT on purpose: a bound socket path must fit
|
|
196
|
+
// sun_path (108 bytes on Linux), and a 64-hex name under a long sandbox root does not — listen
|
|
197
|
+
// fails with EINVAL. That limit is why the vendor keeps its own root at `/tmp/codex-daemon-<euid>`.
|
|
198
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-cnp-"));
|
|
199
|
+
const euid = process.geteuid?.() ?? -1;
|
|
200
|
+
const sandboxServers: net.Server[] = [];
|
|
201
|
+
// A tree whose canonical path is longer than PATH_MAX can only be removed through the short
|
|
202
|
+
// symlinked path that reaches it, so it is removed first, before the root.
|
|
203
|
+
const overlongTrees: string[] = [];
|
|
185
204
|
try {
|
|
186
205
|
const missing = checkCodexSocketFile(path.join(temp, "missing.sock"));
|
|
187
|
-
assert.deepEqual(
|
|
206
|
+
assert.deepEqual(
|
|
207
|
+
missing.ok ? null : [missing.status, missing.status === "dead" ? missing.layout : null],
|
|
208
|
+
["dead", "absent"],
|
|
209
|
+
"missing socket is a dead app-server, and says it is the absent layout",
|
|
210
|
+
);
|
|
188
211
|
const regular = path.join(temp, "regular");
|
|
189
212
|
fs.writeFileSync(regular, "not a socket");
|
|
190
213
|
const wrongType = checkCodexSocketFile(regular);
|
|
191
214
|
assert.deepEqual(wrongType.ok ? null : wrongType.status, "indeterminate", "wrong path type never reads dead");
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
215
|
+
|
|
216
|
+
const listen = async (socketPath: string, mode: number): Promise<void> => {
|
|
217
|
+
const server = net.createServer();
|
|
218
|
+
sandboxServers.push(server);
|
|
219
|
+
await new Promise<void>((resolve, reject) => {
|
|
220
|
+
server.once("error", reject);
|
|
221
|
+
server.listen(socketPath, () => resolve());
|
|
222
|
+
});
|
|
223
|
+
fs.chmodSync(socketPath, mode);
|
|
224
|
+
};
|
|
225
|
+
const layout = (name: string): { caseRoot: string; advertised: string; daemonRoot: string; expected: string } => {
|
|
226
|
+
const caseRoot = path.join(temp, name);
|
|
227
|
+
const parent = path.join(caseRoot, "home", "app-server-control");
|
|
228
|
+
fs.mkdirSync(parent, { recursive: true });
|
|
229
|
+
const daemonRoot = path.join(caseRoot, "d");
|
|
230
|
+
fs.mkdirSync(daemonRoot);
|
|
231
|
+
fs.chmodSync(daemonRoot, 0o700);
|
|
232
|
+
const hash = crypto
|
|
233
|
+
.createHash("sha256")
|
|
234
|
+
.update(`${fs.realpathSync(parent)}/app-server-control.sock`)
|
|
235
|
+
.digest("hex");
|
|
236
|
+
return {
|
|
237
|
+
caseRoot,
|
|
238
|
+
advertised: path.join(parent, "app-server-control.sock"),
|
|
239
|
+
daemonRoot,
|
|
240
|
+
expected: path.join(daemonRoot, hash),
|
|
241
|
+
};
|
|
242
|
+
};
|
|
243
|
+
const classify = (advertised: string, daemonRoot: string, over: { euid?: number } = {}) =>
|
|
244
|
+
checkCodexSocketFile(advertised, { daemonRoot, euid, ...over });
|
|
245
|
+
const statusOf = (check: ReturnType<typeof checkCodexSocketFile>): string => (check.ok ? "ok" : check.status);
|
|
246
|
+
|
|
247
|
+
const plainDir = path.join(temp, "plain");
|
|
248
|
+
fs.mkdirSync(plainDir);
|
|
249
|
+
const plain = path.join(plainDir, "app-server-control.sock");
|
|
250
|
+
await listen(plain, 0o600);
|
|
251
|
+
assert.deepEqual(
|
|
252
|
+
classify(plain, path.join(temp, "no-daemon-dir")),
|
|
253
|
+
{ ok: true, connectPath: plain },
|
|
254
|
+
"(A) a plain socket owned by the effective uid is the endpoint itself — the <=0.153.4 layout is unchanged",
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
const exact = layout("exact");
|
|
258
|
+
await listen(exact.expected, 0o600);
|
|
259
|
+
fs.symlinkSync(exact.expected, exact.advertised);
|
|
260
|
+
assert.deepEqual(
|
|
261
|
+
classify(exact.advertised, exact.daemonRoot),
|
|
262
|
+
{ ok: true, connectPath: exact.expected },
|
|
263
|
+
"(B) Codex's exact rendezvous alias is accepted, and the endpoint is the verified physical socket, not the alias",
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
const wrongHash = layout("wrong-hash");
|
|
267
|
+
const elsewhere = path.join(wrongHash.daemonRoot, "0".repeat(64));
|
|
268
|
+
await listen(elsewhere, 0o600);
|
|
269
|
+
fs.symlinkSync(elsewhere, wrongHash.advertised);
|
|
270
|
+
const wrongHashCheck = classify(wrongHash.advertised, wrongHash.daemonRoot);
|
|
271
|
+
assert.equal(
|
|
272
|
+
statusOf(wrongHashCheck),
|
|
273
|
+
"indeterminate",
|
|
274
|
+
"[QK:CODEX-SOCKET-ALIAS-EXACT] an alias into Codex's own socket directory whose name is not the hash of THIS advertised path is refused — a valid 0600 socket in the right 0700 directory is not enough",
|
|
275
|
+
);
|
|
276
|
+
assert.match(JSON.stringify(wrongHashCheck), /not Codex's rendezvous alias/, "that refusal says why, by name");
|
|
277
|
+
|
|
278
|
+
const relative = layout("relative");
|
|
279
|
+
await listen(relative.expected, 0o600);
|
|
280
|
+
fs.symlinkSync(path.relative(path.dirname(relative.advertised), relative.expected), relative.advertised);
|
|
281
|
+
assert.equal(
|
|
282
|
+
statusOf(classify(relative.advertised, relative.daemonRoot)),
|
|
283
|
+
"indeterminate",
|
|
284
|
+
"a relative alias target is refused even when it resolves to the expected socket",
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
const chain = layout("chain");
|
|
288
|
+
await listen(chain.expected, 0o600);
|
|
289
|
+
const hop = path.join(chain.caseRoot, "hop.sock");
|
|
290
|
+
fs.symlinkSync(chain.expected, hop);
|
|
291
|
+
fs.symlinkSync(hop, chain.advertised);
|
|
292
|
+
assert.equal(
|
|
293
|
+
statusOf(classify(chain.advertised, chain.daemonRoot)),
|
|
294
|
+
"indeterminate",
|
|
295
|
+
"an alias that reaches the expected socket through a second link is refused; only the vendor's one hop is accepted",
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
const dangling = layout("dangling");
|
|
299
|
+
fs.symlinkSync(dangling.expected, dangling.advertised);
|
|
300
|
+
const danglingCheck = classify(dangling.advertised, dangling.daemonRoot);
|
|
301
|
+
assert.deepEqual(
|
|
302
|
+
danglingCheck.ok ? null : [danglingCheck.status, danglingCheck.status === "dead" ? danglingCheck.layout : null],
|
|
303
|
+
["dead", "dangling-alias"],
|
|
304
|
+
"the exact alias with its socket gone is dead, named as the dangling alias the vendor itself removes on start",
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
const openDir = layout("dir-0755");
|
|
308
|
+
await listen(openDir.expected, 0o600);
|
|
309
|
+
fs.symlinkSync(openDir.expected, openDir.advertised);
|
|
310
|
+
fs.chmodSync(openDir.daemonRoot, 0o755);
|
|
311
|
+
assert.equal(
|
|
312
|
+
statusOf(classify(openDir.advertised, openDir.daemonRoot)),
|
|
313
|
+
"indeterminate",
|
|
314
|
+
"a socket directory other users can enter is refused — the vendor's own rule is mode & 0o777 == 0o700",
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
const stickyDir = layout("dir-1700");
|
|
318
|
+
await listen(stickyDir.expected, 0o600);
|
|
319
|
+
fs.symlinkSync(stickyDir.expected, stickyDir.advertised);
|
|
320
|
+
fs.chmodSync(stickyDir.daemonRoot, 0o1700);
|
|
321
|
+
assert.deepEqual(
|
|
322
|
+
classify(stickyDir.advertised, stickyDir.daemonRoot),
|
|
323
|
+
{ ok: true, connectPath: stickyDir.expected },
|
|
324
|
+
"a sticky 01700 socket directory is accepted: the vendor masks with 0o777, so refusing it would reject a directory Codex itself uses",
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
const linkedDir = layout("dir-symlink");
|
|
328
|
+
await listen(linkedDir.expected, 0o600);
|
|
329
|
+
fs.symlinkSync(linkedDir.expected, linkedDir.advertised);
|
|
330
|
+
const realDir = path.join(linkedDir.caseRoot, "real-daemon-dir");
|
|
331
|
+
fs.renameSync(linkedDir.daemonRoot, realDir);
|
|
332
|
+
fs.symlinkSync(realDir, linkedDir.daemonRoot);
|
|
333
|
+
assert.equal(
|
|
334
|
+
statusOf(classify(linkedDir.advertised, linkedDir.daemonRoot)),
|
|
335
|
+
"indeterminate",
|
|
336
|
+
"a socket directory that is itself a symlink is refused, as the vendor refuses it",
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
const fileTarget = layout("physical-file");
|
|
340
|
+
fs.writeFileSync(fileTarget.expected, "", { mode: 0o600 });
|
|
341
|
+
fs.symlinkSync(fileTarget.expected, fileTarget.advertised);
|
|
342
|
+
assert.equal(
|
|
343
|
+
statusOf(classify(fileTarget.advertised, fileTarget.daemonRoot)),
|
|
344
|
+
"indeterminate",
|
|
345
|
+
"an exact alias whose target is a regular file is refused, never connected to",
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
const readable = layout("physical-0644");
|
|
349
|
+
await listen(readable.expected, 0o644);
|
|
350
|
+
fs.symlinkSync(readable.expected, readable.advertised);
|
|
351
|
+
assert.equal(
|
|
352
|
+
statusOf(classify(readable.advertised, readable.daemonRoot)),
|
|
353
|
+
"indeterminate",
|
|
354
|
+
"an exact alias whose socket group or other users can open is refused",
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
assert.deepEqual(
|
|
358
|
+
[
|
|
359
|
+
statusOf(classify(plain, path.join(temp, "no-daemon-dir"), { euid: euid + 1 })),
|
|
360
|
+
statusOf(classify(exact.advertised, exact.daemonRoot, { euid: euid + 1 })),
|
|
361
|
+
],
|
|
362
|
+
["indeterminate", "indeterminate"],
|
|
363
|
+
"ownership is compared with the EFFECTIVE uid (the vendor's geteuid): under another euid both layouts are refused",
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
// canonicalize(parent) failure is the alias state lstat cannot see coming. The advertised
|
|
367
|
+
// path is short, but it reaches its parent through a symlink, and the parent's canonical
|
|
368
|
+
// path is longer than PATH_MAX — the vendor's own `canonicalize` fails on it the same way.
|
|
369
|
+
// It must be indeterminate: never dead, and never a throw the launcher would read as an
|
|
370
|
+
// unrecognised status.
|
|
371
|
+
const canonicalCase = path.join(temp, "canonical");
|
|
372
|
+
let far = canonicalCase;
|
|
373
|
+
while (far.length < 3600) far = path.join(far, "d".repeat(200));
|
|
374
|
+
fs.mkdirSync(far, { recursive: true });
|
|
375
|
+
const via = path.join(canonicalCase, "via");
|
|
376
|
+
fs.symlinkSync(far, via);
|
|
377
|
+
overlongTrees.push(path.join(via, "e".repeat(200)));
|
|
378
|
+
let overlongParent = via;
|
|
379
|
+
for (let i = 0; i < 4; i += 1) overlongParent = path.join(overlongParent, "e".repeat(200));
|
|
380
|
+
fs.mkdirSync(overlongParent, { recursive: true });
|
|
381
|
+
const overlongAdvertised = path.join(overlongParent, "app-server-control.sock");
|
|
382
|
+
fs.symlinkSync(exact.expected, overlongAdvertised);
|
|
383
|
+
const overlongCheck = classify(overlongAdvertised, exact.daemonRoot);
|
|
384
|
+
assert.equal(
|
|
385
|
+
statusOf(overlongCheck),
|
|
386
|
+
"indeterminate",
|
|
387
|
+
"an alias whose parent cannot be canonicalized is indeterminate",
|
|
388
|
+
);
|
|
389
|
+
assert.match(
|
|
390
|
+
JSON.stringify(overlongCheck),
|
|
391
|
+
/could not verify/,
|
|
392
|
+
"that refusal names the verification it could not do",
|
|
393
|
+
);
|
|
195
394
|
} finally {
|
|
395
|
+
for (const server of sandboxServers) server.close();
|
|
396
|
+
for (const tree of overlongTrees) fs.rmSync(tree, { recursive: true, force: true });
|
|
196
397
|
fs.rmSync(temp, { recursive: true, force: true });
|
|
197
398
|
}
|
|
198
399
|
|
|
@@ -201,15 +402,33 @@ const firstPage = new FakeProtocol([
|
|
|
201
402
|
{ data: ["other"], nextCursor: "next" },
|
|
202
403
|
{ data: ["target"], nextCursor: null },
|
|
203
404
|
]);
|
|
405
|
+
// The check is asked about the advertised default path, and everything after it — the probe's
|
|
406
|
+
// connect AND the route the queue send uses — goes to the endpoint the check returned. Here
|
|
407
|
+
// the two are different strings, so a probe that went back to the advertised path goes red.
|
|
408
|
+
let checkedPath = "";
|
|
409
|
+
let openedPath = "";
|
|
204
410
|
const alive = await probeCodexThread("target", {
|
|
205
411
|
env: { CODEX_HOME: "/fixed" },
|
|
206
|
-
checkSocket: () =>
|
|
207
|
-
|
|
412
|
+
checkSocket: (advertised) => {
|
|
413
|
+
checkedPath = advertised;
|
|
414
|
+
return { ok: true, connectPath: "/verified/physical.sock" };
|
|
415
|
+
},
|
|
416
|
+
openProtocol: {
|
|
417
|
+
open: async (socketPath) => {
|
|
418
|
+
openedPath = socketPath;
|
|
419
|
+
return firstPage;
|
|
420
|
+
},
|
|
421
|
+
},
|
|
208
422
|
});
|
|
209
423
|
assert.deepEqual(
|
|
210
424
|
alive,
|
|
211
|
-
{ status: "alive", route: { backend: "codex", socketPath: "/
|
|
212
|
-
"[QK:CODEX-NATIVE-LOADED-PROBE] full loaded-thread pagination proves the volatile route",
|
|
425
|
+
{ status: "alive", route: { backend: "codex", socketPath: "/verified/physical.sock" } },
|
|
426
|
+
"[QK:CODEX-NATIVE-LOADED-PROBE] full loaded-thread pagination proves the volatile route, on the endpoint the socket check verified",
|
|
427
|
+
);
|
|
428
|
+
assert.deepEqual(
|
|
429
|
+
[checkedPath, openedPath],
|
|
430
|
+
["/fixed/app-server-control/app-server-control.sock", "/verified/physical.sock"],
|
|
431
|
+
"the socket check reads the advertised default path; the probe connects to the endpoint it verified, never the alias again",
|
|
213
432
|
);
|
|
214
433
|
assert.equal(firstPage.closed, true, "successful probe closes its connection");
|
|
215
434
|
assert.deepEqual(
|
|
@@ -221,7 +440,7 @@ assert.deepEqual(firstPage.calls[3]?.params, { cursor: "next" }, "pagination car
|
|
|
221
440
|
const absentProtocol = new FakeProtocol([{}, { data: [], nextCursor: null }]);
|
|
222
441
|
const absent = await probeCodexThread("target", {
|
|
223
442
|
env: { CODEX_HOME: "/fixed" },
|
|
224
|
-
checkSocket: () => ({ ok: true }),
|
|
443
|
+
checkSocket: (advertised) => ({ ok: true, connectPath: advertised }),
|
|
225
444
|
openProtocol: opener(absentProtocol),
|
|
226
445
|
});
|
|
227
446
|
assert.equal(absent.status, "dead", "a clean loaded inventory without the target is not delivery-capable");
|
|
@@ -230,7 +449,7 @@ assert.equal(absentProtocol.closed, true, "negative probe closes its connection"
|
|
|
230
449
|
const malformedProtocol = new FakeProtocol([{}, { data: [{ id: "wrong-shape" }], nextCursor: null }]);
|
|
231
450
|
const malformed = await probeCodexThread("target", {
|
|
232
451
|
env: { CODEX_HOME: "/fixed" },
|
|
233
|
-
checkSocket: () => ({ ok: true }),
|
|
452
|
+
checkSocket: (advertised) => ({ ok: true, connectPath: advertised }),
|
|
234
453
|
openProtocol: opener(malformedProtocol),
|
|
235
454
|
});
|
|
236
455
|
assert.equal(malformed.status, "indeterminate", "a malformed inventory is unknown, never dead");
|
|
@@ -374,7 +593,7 @@ assert.deepEqual(
|
|
|
374
593
|
|
|
375
594
|
const adapter = createCodexNativePushAdapter({
|
|
376
595
|
env: { CODEX_HOME: "/fixed" },
|
|
377
|
-
checkSocket: () => ({ ok: true }),
|
|
596
|
+
checkSocket: (advertised) => ({ ok: true, connectPath: advertised }),
|
|
378
597
|
openProtocol: opener(new FakeProtocol([{}, { data: ["target"], nextCursor: null }])),
|
|
379
598
|
runner: queueRunner,
|
|
380
599
|
});
|
|
@@ -3442,7 +3442,7 @@ let manifestCount: number;
|
|
|
3442
3442
|
"acp-overlay": 1,
|
|
3443
3443
|
"acp-prompt-lifecycle": 15,
|
|
3444
3444
|
"acp-set-model-readback": 2,
|
|
3445
|
-
"acp-stop-reason":
|
|
3445
|
+
"acp-stop-reason": 7,
|
|
3446
3446
|
"acp-stream-hooks": 10,
|
|
3447
3447
|
"acp-usage-accounting": 12,
|
|
3448
3448
|
"acp-vendor-rejection": 4,
|
|
@@ -3450,10 +3450,10 @@ let manifestCount: number;
|
|
|
3450
3450
|
"bridge-boot-resume": 5,
|
|
3451
3451
|
"bridge-command-boot": 12,
|
|
3452
3452
|
"capability-cache": 3,
|
|
3453
|
-
"codex-app-server-launch":
|
|
3453
|
+
"codex-app-server-launch": 10,
|
|
3454
3454
|
"codex-caller-seat": 28,
|
|
3455
3455
|
"control-send-receipt": 19,
|
|
3456
|
-
"codex-native":
|
|
3456
|
+
"codex-native": 87,
|
|
3457
3457
|
"compaction-send-guard": 7,
|
|
3458
3458
|
"control-socket-disconnect": 4,
|
|
3459
3459
|
"demo-reproducibility": 12,
|
|
@@ -3464,7 +3464,7 @@ let manifestCount: number;
|
|
|
3464
3464
|
"pi-bridge-sender": 4,
|
|
3465
3465
|
"pi-mcp-bridge": 2,
|
|
3466
3466
|
"pi-mcp-register": 3,
|
|
3467
|
-
"pi-durable":
|
|
3467
|
+
"pi-durable": 78,
|
|
3468
3468
|
"copilot-receive": 20,
|
|
3469
3469
|
"entwurf-peers": 1,
|
|
3470
3470
|
"fresh-call-dispatch": 12,
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
* HSUP-CI-REQUIRES-HERDR that floor runs with absence turned red
|
|
33
33
|
* HSUP-CI-LINUX-ONLY only the Linux check job takes this; the other jobs are untouched
|
|
34
34
|
* HSUP-SETUP-NEVER-INSTALLS the product's own setup/package never installs herdr (Rule 17)
|
|
35
|
-
* HSUP-
|
|
35
|
+
* HSUP-EXECUTION-RECEIPT-BOUND an asset recorded as executed declares a receipt bound to this
|
|
36
|
+
* pin's version and its own full digest; an unexecuted one has none
|
|
36
37
|
*/
|
|
37
38
|
|
|
38
39
|
import assert from "node:assert/strict";
|
|
@@ -62,7 +63,34 @@ interface Supply {
|
|
|
62
63
|
measuredOn: string;
|
|
63
64
|
measuredArch: string;
|
|
64
65
|
releaseUrlPrefix: string;
|
|
65
|
-
assets: Record<
|
|
66
|
+
assets: Record<
|
|
67
|
+
string,
|
|
68
|
+
{ name: string; sha256: string; executed: boolean; executedNote: string; executedReceipt?: string }
|
|
69
|
+
>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** An asset may be RECORDED as executed only with a declared receipt bound to THIS pin: the
|
|
73
|
+
* receipt BEGINS with the exact token `herdr <supply.version> ` (a substring match would let
|
|
74
|
+
* `herdr 0.9.40` stand for 0.9.4) and names the asset's FULL sha256. That checks the
|
|
75
|
+
* declaration's binding and nothing more — it is not a cryptographic receipt and cannot prove the
|
|
76
|
+
* binary ran. An unexecuted asset carries no receipt, both states keep a note, and the
|
|
77
|
+
* architecture the manifest says it was measured on must itself be recorded as executed. The
|
|
78
|
+
* architecture is never hardcoded: a future x86_64 `true` is admitted the same way, with its own. */
|
|
79
|
+
function executionReceiptsBound(supply: Supply): boolean {
|
|
80
|
+
const assets = Object.values(supply.assets);
|
|
81
|
+
const noted = (note: unknown): boolean => typeof note === "string" && note.trim().length > 0;
|
|
82
|
+
return (
|
|
83
|
+
assets.length > 0 &&
|
|
84
|
+
assets.every((a) =>
|
|
85
|
+
a.executed === true
|
|
86
|
+
? typeof a.executedReceipt === "string" &&
|
|
87
|
+
a.executedReceipt.startsWith(`herdr ${supply.version} `) &&
|
|
88
|
+
a.executedReceipt.includes(a.sha256) &&
|
|
89
|
+
noted(a.executedNote)
|
|
90
|
+
: a.executed === false && a.executedReceipt === undefined && noted(a.executedNote),
|
|
91
|
+
) &&
|
|
92
|
+
supply.assets[supply.measuredArch]?.executed === true
|
|
93
|
+
);
|
|
66
94
|
}
|
|
67
95
|
|
|
68
96
|
function main(): void {
|
|
@@ -171,11 +199,8 @@ function main(): void {
|
|
|
171
199
|
|
|
172
200
|
// ── honesty about what has actually run ──────────────────────────────────────────────
|
|
173
201
|
ok(
|
|
174
|
-
"[QK:HSUP-
|
|
175
|
-
supply
|
|
176
|
-
supply.assets.aarch64?.executed === true &&
|
|
177
|
-
supply.measuredArch === "aarch64" &&
|
|
178
|
-
(supply.assets.x86_64?.executedNote ?? "").length > 0,
|
|
202
|
+
"[QK:HSUP-EXECUTION-RECEIPT-BOUND] an asset is recorded as executed only with a DECLARED receipt bound to this pin's version and its own full digest, and an unexecuted one carries none — a pinned digest is never evidence that its cell ran, and a receipt from another pin or another asset is not this one's. This checks the declaration's binding, not that the binary ran",
|
|
203
|
+
executionReceiptsBound(supply),
|
|
179
204
|
);
|
|
180
205
|
|
|
181
206
|
console.log(`\n[check-herdr-supply] ${passed} assertions ok`);
|
|
@@ -156,38 +156,53 @@ done
|
|
|
156
156
|
# --- is somebody already listening there? ----------------------------------
|
|
157
157
|
# Three outcomes, and they are NOT the same repair. A live socket means a second server
|
|
158
158
|
# would either lose the bind race or silently replace the endpoint every citizen's record
|
|
159
|
-
# already points at — refuse and name the holder. An indeterminate path (symlink
|
|
160
|
-
# socket, owned by another uid) is
|
|
161
|
-
#
|
|
162
|
-
#
|
|
163
|
-
#
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
159
|
+
# already points at — refuse and name the holder. An indeterminate path (a symlink that is not
|
|
160
|
+
# Codex's own alias, not a socket, owned by another uid) is refused, because a launcher that
|
|
161
|
+
# guessed past it would be clobbering something it cannot identify. A dead socket file is the
|
|
162
|
+
# ordinary leftover of a Ctrl-C, and the vendor replaces it — that one is a fact line, not a
|
|
163
|
+
# refusal. So is a dangling rendezvous alias: from Codex 0.162.1 the advertised path is a
|
|
164
|
+
# symlink into the vendor's own socket directory, and the vendor removes exactly that alias
|
|
165
|
+
# when it starts.
|
|
166
|
+
#
|
|
167
|
+
# WHAT A LAUNCH IS ALLOWED TO TRUST is decided ONCE, by the classifier delivery and preflight
|
|
168
|
+
# use (`checkCodexSocketFile`). This launcher keeps no copy of it: telling Codex's exact alias
|
|
169
|
+
# from any other symlink takes a hash and an exact readlink comparison, and a second
|
|
170
|
+
# implementation here would be the transcription `codex-socket-path` already measured drifting.
|
|
171
|
+
# It runs that classifier's COMPILED twin directly with `node`, in a checkout and in an installed
|
|
172
|
+
# package alike: `build-bridge` emits it (`pnpm install` runs it through `prepare`, and the package
|
|
173
|
+
# ships it through `prepack`), so a missing file is an incomplete installation, refused by name —
|
|
174
|
+
# never a reason to fall back to the TypeScript source. The Python below only CONNECTS to the
|
|
175
|
+
# endpoint the classifier verified, to tell a live server from a stale socket file. An answer the
|
|
176
|
+
# classifier never gives is refused right here, before it can be read as one of the probe's own
|
|
177
|
+
# verdicts.
|
|
178
|
+
#
|
|
179
|
+
# The gate's limit, stated rather than hidden: it executes this launcher on non-alias layouts
|
|
180
|
+
# and on a non-exact symlink, so it proves the launcher accepts no more than the classifier.
|
|
181
|
+
# The exact-alias accept path is proven on `checkCodexSocketFile` itself, with a sandbox socket
|
|
182
|
+
# directory. This launcher's own alias branch is NOT exercised by an existing-server LIVE
|
|
183
|
+
# diagnostic; that launch/refusal path needs its own explicitly authorised LIVE observation.
|
|
184
|
+
CLASSIFIER="$REPO_DIR/mcp/entwurf-bridge/dist/scripts/codex-socket-classify.js"
|
|
185
|
+
[ -f "$CLASSIFIER" ] \
|
|
186
|
+
|| fail "codex-app-server-classifier-missing: the compiled socket classifier is not at
|
|
187
|
+
$CLASSIFIER
|
|
188
|
+
This launcher runs it directly and keeps no other copy of it. Reinstall @junghanacs/entwurf (the
|
|
189
|
+
package ships it through prepack build-bridge), or run 'pnpm run build-bridge' in a source checkout."
|
|
190
|
+
if ! classified="$(node "$CLASSIFIER")"; then
|
|
191
|
+
fail "could not classify the control socket at $SOCK.
|
|
192
|
+
'node $CLASSIFIER' failed; it is the only classification of this socket,
|
|
193
|
+
and this launcher will not guess past it. Its output above says why."
|
|
194
|
+
fi
|
|
195
|
+
classified_word="${classified%% *}"
|
|
196
|
+
classified_detail="${classified#* }"
|
|
197
|
+
case "$classified_word" in
|
|
198
|
+
socket)
|
|
199
|
+
probe="$(python3 - "$classified_detail" <<'PY'
|
|
200
|
+
import socket, sys
|
|
186
201
|
|
|
187
202
|
probe = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
188
203
|
probe.settimeout(2)
|
|
189
204
|
try:
|
|
190
|
-
probe.connect(
|
|
205
|
+
probe.connect(sys.argv[1])
|
|
191
206
|
except (ConnectionRefusedError, FileNotFoundError):
|
|
192
207
|
print("stale")
|
|
193
208
|
except OSError as exc:
|
|
@@ -197,7 +212,17 @@ else:
|
|
|
197
212
|
finally:
|
|
198
213
|
probe.close()
|
|
199
214
|
PY
|
|
200
|
-
)" || fail "could not
|
|
215
|
+
)" || fail "could not probe the control socket at $classified_detail (python3 is required by this launcher)."
|
|
216
|
+
;;
|
|
217
|
+
absent) probe="absent" ;;
|
|
218
|
+
dangling) probe="stale $classified_detail" ;;
|
|
219
|
+
indeterminate) probe="indeterminate $classified_detail" ;;
|
|
220
|
+
*)
|
|
221
|
+
fail "codex-app-server-socket-probe-unrecognised: the socket classifier answered
|
|
222
|
+
'$classified_word', which is not one of socket/absent/dangling/indeterminate. Refusing to launch on a
|
|
223
|
+
reading nothing understands."
|
|
224
|
+
;;
|
|
225
|
+
esac
|
|
201
226
|
|
|
202
227
|
probe_status="${probe%% *}"
|
|
203
228
|
probe_reason="${probe#* }"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* codex-socket-classify — classify the Codex app-server default control socket for THIS
|
|
3
|
+
* environment, through the one product classifier, and print the answer for a shell.
|
|
4
|
+
*
|
|
5
|
+
* WHY A LEAF. `entwurf codex-app-server` is a bash launcher, and it has to decide whether a
|
|
6
|
+
* launch would clobber something. From Codex 0.162.1 the advertised socket path is a
|
|
7
|
+
* rendezvous alias into a vendor-owned directory, and telling the vendor's exact alias from any
|
|
8
|
+
* other symlink takes a hash, an exact readlink comparison and ownership/mode checks. Doing that
|
|
9
|
+
* in the launcher's Python would be a second implementation of a security classification, and
|
|
10
|
+
* `codex-socket-path.ts` records what a second spelling of this socket did once (it diverged).
|
|
11
|
+
* So the launcher asks `checkCodexSocketFile`, the classifier delivery and preflight use, and
|
|
12
|
+
* only connects to what it was told.
|
|
13
|
+
*
|
|
14
|
+
* HOW IT IS REACHED. The launcher runs this file's COMPILED twin,
|
|
15
|
+
* `mcp/entwurf-bridge/dist/scripts/codex-socket-classify.js`, directly with `node` — in a checkout
|
|
16
|
+
* and in an installed package alike. There is no `run.sh` verb and no `run_ts` crossing for it:
|
|
17
|
+
* `build-bridge` emits it (`pnpm install` runs that through `prepare`, packing through `prepack`),
|
|
18
|
+
* and a missing twin is an incomplete installation the launcher refuses by name.
|
|
19
|
+
*
|
|
20
|
+
* One line on stdout, a word then a tab-separated detail:
|
|
21
|
+
* socket <connectPath> a verified socket; the launcher connects there to tell live from stale
|
|
22
|
+
* absent nothing at the advertised path
|
|
23
|
+
* dangling <reason> Codex's own alias whose socket is gone; the vendor removes it on start
|
|
24
|
+
* indeterminate <reason> anything else; the launcher refuses
|
|
25
|
+
*
|
|
26
|
+
* Reads `process.env` and takes no argument, for the reason `codex-socket-path.ts` gives: an argv
|
|
27
|
+
* seam would be a way to redirect the address. It has no daemon-root input either; the gate's
|
|
28
|
+
* exact-alias cells drive `checkCodexSocketFile` directly with a sandbox root.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import {
|
|
32
|
+
checkCodexSocketFile,
|
|
33
|
+
resolveCodexDefaultSocketPath,
|
|
34
|
+
} from "../pi-extensions/lib/native-push/codex-ws-client.ts";
|
|
35
|
+
|
|
36
|
+
if (process.argv.length > 2) {
|
|
37
|
+
console.error("usage: codex-socket-classify (no arguments; the environment is the input)");
|
|
38
|
+
process.exit(2);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const oneLine = (text: string): string => text.replace(/[\t\r\n]+/g, " ");
|
|
42
|
+
const check = checkCodexSocketFile(resolveCodexDefaultSocketPath(process.env));
|
|
43
|
+
if (check.ok) process.stdout.write(`socket\t${check.connectPath}`);
|
|
44
|
+
else if (check.status === "dead" && check.layout === "absent") process.stdout.write("absent");
|
|
45
|
+
else if (check.status === "dead") process.stdout.write(`dangling\t${oneLine(check.reason)}`);
|
|
46
|
+
else process.stdout.write(`indeterminate\t${oneLine(check.reason)}`);
|