@junghanacs/entwurf 0.20.1 → 0.21.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 +85 -190
- package/BASELINE.md +4 -3
- package/CHANGELOG.md +195 -14
- package/CONTRIBUTING.md +1 -1
- package/DELIVERY.md +202 -60
- package/README.md +49 -22
- package/VERIFY.md +37 -7
- package/docs/acp-backend-rail.md +0 -1
- package/docs/external-mcp-host.md +54 -33
- package/docs/setup-clean-host.md +110 -17
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +101 -109
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +326 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +22 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +3 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +6 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-native-push.js +30 -17
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +7 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +7 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-mailbox-body.js +20 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +227 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +50 -15
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/adapter.js +10 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/codex-ws-client.js +403 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/register.js +4 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook-codex.js +325 -0
- package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +6 -1
- package/mcp/entwurf-bridge/src/index.ts +115 -111
- package/mcp/entwurf-bridge/tsconfig.build.json +1 -0
- package/package.json +13 -4
- package/pi-extensions/entwurf-control.ts +71 -19
- package/pi-extensions/lib/codex-fresh-preflight.ts +363 -0
- package/pi-extensions/lib/compaction-send-guard.ts +80 -0
- package/pi-extensions/lib/entwurf-fact-provider.ts +29 -3
- package/pi-extensions/lib/entwurf-peers-render.ts +4 -1
- package/pi-extensions/lib/entwurf-v2-contract.ts +6 -7
- package/pi-extensions/lib/entwurf-v2-native-push.ts +35 -18
- package/pi-extensions/lib/entwurf-v2-production.ts +10 -3
- package/pi-extensions/lib/entwurf-v2-surface.ts +7 -4
- package/pi-extensions/lib/meta-mailbox-body.ts +22 -13
- package/pi-extensions/lib/meta-sender-identity.ts +305 -0
- package/pi-extensions/lib/mux-fresh-call.ts +64 -19
- package/pi-extensions/lib/native-push/adapter.ts +21 -24
- package/pi-extensions/lib/native-push/codex-ws-client.ts +506 -0
- package/pi-extensions/lib/native-push/register.ts +7 -9
- package/pi-extensions/meta-bridge-hook-codex.ts +371 -0
- package/run.sh +218 -28
- package/scripts/check-agy-sender-identity.ts +1 -1
- package/scripts/check-codex-birth-hook.ts +264 -0
- package/scripts/check-codex-bridge-identity.ts +179 -0
- package/scripts/check-codex-native-push.ts +386 -0
- package/scripts/check-codex-sender-identity.ts +495 -0
- package/scripts/check-compaction-send-guard.ts +130 -0
- package/scripts/check-copilot-receive-arm.ts +4 -1
- package/scripts/check-entwurf-fact-provider.ts +38 -0
- package/scripts/check-entwurf-peers-surface.ts +13 -1
- package/scripts/check-entwurf-self-address.ts +15 -16
- package/scripts/check-entwurf-v2-contract.ts +4 -3
- package/scripts/check-entwurf-v2-decider.ts +7 -5
- package/scripts/check-entwurf-v2-native-push.ts +35 -7
- package/scripts/check-entwurf-v2-production.ts +203 -11
- package/scripts/check-entwurf-v2-runner.ts +1 -1
- package/scripts/check-entwurf-v2-surface.ts +1 -1
- package/scripts/check-gate-qualification.ts +6 -3
- package/scripts/check-harness-admission-parity.ts +0 -1
- package/scripts/check-mux-launch-tmux.ts +47 -2
- package/scripts/check-native-push-adapter.ts +20 -16
- package/scripts/check-native-push-register.ts +5 -1
- package/scripts/check-release-gate-outcomes.ts +47 -1
- package/scripts/check-setup-qualification.sh +3 -1
- package/scripts/codex-birth-doctor.sh +276 -0
- package/scripts/codex-birth-install.sh +414 -0
- package/scripts/codex-birth-uninstall.sh +170 -0
- package/scripts/codex-mcp-config.py +435 -0
- package/scripts/codex-statusline-config.py +434 -0
- package/scripts/codex_toml_io.py +532 -0
- package/scripts/lib/codex-fresh-live-protocol.ts +113 -0
- package/scripts/lib/codex-fresh-source-receipts.ts +399 -0
- package/scripts/lib/launch-receipt-windows.ts +46 -0
- package/scripts/lib/tmux-coordinate-row.ts +58 -0
- package/scripts/meta-bridge-fresh-cut.ts +6 -1
- package/scripts/mutants/codex-native.json +838 -0
- package/scripts/mutants/compaction-send-guard.json +103 -0
- package/scripts/mutants/entwurf-peers.json +19 -0
- package/scripts/mutants/mux-fresh-call.json +19 -8
- package/scripts/mutants/omp-fresh.json +6 -4
- package/scripts/mutants/release-gate.json +13 -0
- package/scripts/mutants/v2-surface.json +53 -1
- package/scripts/raw-async-delivery/README.md +2 -1
- package/scripts/raw-codex-measure/README.md +114 -46
- package/scripts/smoke-agy-native-push-live.ts +3 -1
- package/scripts/smoke-codex-birth.sh +347 -0
- package/scripts/smoke-codex-config-state.sh +511 -0
- package/scripts/smoke-codex-fresh-live.ts +1186 -0
- package/scripts/smoke-codex-native-push-live.ts +75 -0
- package/scripts/smoke-setup-verdict.sh +123 -10
- package/scripts/tsconfig.json +1 -0
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* and required to be byte-identical, and `TMUX`/`TMUX_PANE` INHERITED from a fixture pane's
|
|
9
9
|
* own `/proc/<pid>/environ` rather than assembled.
|
|
10
10
|
*
|
|
11
|
-
* The
|
|
11
|
+
* The runtimes under test are FIXTURE scripts, not the operator's real `pi`/`codex`. T1-a's claim is
|
|
12
12
|
* "the window opens in the right place and the named runtime is what starts in it" — a claim a
|
|
13
13
|
* long-lived stand-in proves exactly, without an interactive agent, a model call, or a session
|
|
14
14
|
* record. `resolvePiRuntime` (which finds the official `pi` on PATH) is proven by the
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
* - the #105 seat, through the real `freshCall`: an absent seat refuses and creates nothing,
|
|
28
28
|
* an existing one puts the window in THAT session with a receipt naming the resolved
|
|
29
29
|
* target, and the resulting handle closes through `closeWindow` from outside that session
|
|
30
|
+
* - #95's omitted Codex seat resolves exact existing `codex`, while omitted Pi stays caller-local
|
|
30
31
|
*/
|
|
31
32
|
|
|
32
33
|
import assert from "node:assert/strict";
|
|
@@ -120,6 +121,7 @@ async function main(): Promise<void> {
|
|
|
120
121
|
// process must be the runtime itself for the pane_pid claim to mean anything.
|
|
121
122
|
const runtime = path.join(runtimeDir, "pi");
|
|
122
123
|
fs.writeFileSync(runtime, "#!/bin/sh\nexec sleep 900\n", { mode: 0o755 });
|
|
124
|
+
fs.writeFileSync(path.join(runtimeDir, "codex"), "#!/bin/sh\nexec sleep 900\n", { mode: 0o755 });
|
|
123
125
|
|
|
124
126
|
try {
|
|
125
127
|
assert.equal(fx("-f", "/dev/null", "new-session", "-d", "-s", SESSION).status, 0, "fixture new-session");
|
|
@@ -353,7 +355,7 @@ async function main(): Promise<void> {
|
|
|
353
355
|
fxLines("list-windows", "-t", placement.sessionId, "-F", "#{window_id}").join(" ") === callerWindowsBefore,
|
|
354
356
|
);
|
|
355
357
|
ok(
|
|
356
|
-
"seat: an omitted seat still lands in the caller's own session and the receipt names none",
|
|
358
|
+
"seat: an omitted Pi seat still lands in the caller's own session and the receipt names none",
|
|
357
359
|
(() => {
|
|
358
360
|
const own = call();
|
|
359
361
|
if (!own.ok) return false;
|
|
@@ -370,7 +372,50 @@ async function main(): Promise<void> {
|
|
|
370
372
|
closeWindow(receipt, inherited) === "closed" &&
|
|
371
373
|
!fxLines("list-windows", "-a", "-F", "#{window_id}").includes(receipt.windowId),
|
|
372
374
|
);
|
|
375
|
+
|
|
376
|
+
// (iv) Codex's omitted seat is a product default, not fixture prose. Missing home
|
|
377
|
+
// refuses before mutation; then a private session named exactly `codex` makes the real
|
|
378
|
+
// composition resolve that name while the Pi omitted-seat control stays caller-local.
|
|
379
|
+
const beforeMissingHome = inventory();
|
|
380
|
+
const missingHome = freshCall(
|
|
381
|
+
{
|
|
382
|
+
backend: "codex",
|
|
383
|
+
model: "fixture-model",
|
|
384
|
+
task: "fixture task",
|
|
385
|
+
callerGardenId: "20260101T000000-fixture",
|
|
386
|
+
},
|
|
387
|
+
inherited,
|
|
388
|
+
);
|
|
389
|
+
ok(
|
|
390
|
+
"codex home: omitted placement refuses a missing exact home without mutation",
|
|
391
|
+
!missingHome.ok && missingHome.reason === "tmux-session-missing" && inventory() === beforeMissingHome,
|
|
392
|
+
);
|
|
393
|
+
assert.equal(fx("new-session", "-d", "-s", "codex").status, 0, "fixture Codex home session");
|
|
394
|
+
const codexHomeId = fxLines("list-windows", "-t", "=codex", "-F", "#{session_id}")[0];
|
|
395
|
+
const home = freshCall(
|
|
396
|
+
{
|
|
397
|
+
backend: "codex",
|
|
398
|
+
model: "fixture-model",
|
|
399
|
+
task: "fixture task",
|
|
400
|
+
callerGardenId: "20260101T000000-fixture",
|
|
401
|
+
},
|
|
402
|
+
inherited,
|
|
403
|
+
);
|
|
404
|
+
assert.ok(home.ok, `the omitted-placement Codex call must succeed: ${home.ok ? "" : home.reason}`);
|
|
405
|
+
ok(
|
|
406
|
+
"codex home: omitted placement lands in exact existing `codex`, not the caller session",
|
|
407
|
+
home.receipt.sessionId === codexHomeId &&
|
|
408
|
+
home.receipt.sessionId !== placement.sessionId &&
|
|
409
|
+
home.receipt.tmuxSession === "codex" &&
|
|
410
|
+
home.receipt.tmuxSessionSource === "codex-home",
|
|
411
|
+
);
|
|
412
|
+
ok(
|
|
413
|
+
"codex home: the home receipt closes by stable handle",
|
|
414
|
+
closeWindow(home.receipt, inherited) === "closed" &&
|
|
415
|
+
!fxLines("list-windows", "-a", "-F", "#{window_id}").includes(home.receipt.windowId),
|
|
416
|
+
);
|
|
373
417
|
fx("kill-session", "-t", seatId);
|
|
418
|
+
fx("kill-session", "-t", codexHomeId);
|
|
374
419
|
ok("seat: the fixture is back to one session", sessionCount() === 1);
|
|
375
420
|
}
|
|
376
421
|
} finally {
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
AGY_SEND_TIMEOUT_MS,
|
|
30
30
|
antigravityAdapter,
|
|
31
31
|
createAntigravityAdapter,
|
|
32
|
+
type NativePushRoute,
|
|
32
33
|
type NativePushRunner,
|
|
33
34
|
resolveNativePushAdapter,
|
|
34
35
|
} from "../pi-extensions/lib/native-push/adapter.ts";
|
|
@@ -45,6 +46,11 @@ function eq(label: string, actual: unknown, expected: unknown): void {
|
|
|
45
46
|
passed++;
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
function agyAddress(route: NativePushRoute): string {
|
|
50
|
+
assert.equal(route.backend, "antigravity", "Antigravity adapter returned its own route kind");
|
|
51
|
+
return route.backend === "antigravity" ? route.lsAddress : "(wrong backend)";
|
|
52
|
+
}
|
|
53
|
+
|
|
48
54
|
const FAKE_BINARY = "/fake/bin/agy";
|
|
49
55
|
const CONV = "conv-abc-123";
|
|
50
56
|
|
|
@@ -129,7 +135,7 @@ function makeFakeRunner(config: FakeConfig): { runner: NativePushRunner; calls:
|
|
|
129
135
|
ok("full-scan: probe finds the route on the SECOND pid (not head -1)", result.status === "alive");
|
|
130
136
|
eq(
|
|
131
137
|
"full-scan: route is pid 456's port (5002)",
|
|
132
|
-
result.status === "alive" ? result.route
|
|
138
|
+
result.status === "alive" ? agyAddress(result.route) : "(dead)",
|
|
133
139
|
"127.0.0.1:5002",
|
|
134
140
|
);
|
|
135
141
|
// proof it did NOT stop at pid 123: it probed BOTH 5001 (miss) and 5002 (hit).
|
|
@@ -219,7 +225,11 @@ for (const [code, what] of [
|
|
|
219
225
|
const adapter = createAntigravityAdapter({ runner, binary: FAKE_BINARY });
|
|
220
226
|
|
|
221
227
|
const r1 = await adapter.probe(CONV);
|
|
222
|
-
eq(
|
|
228
|
+
eq(
|
|
229
|
+
"volatile-route: probe #1 route = 5002",
|
|
230
|
+
r1.status === "alive" ? agyAddress(r1.route) : "(dead)",
|
|
231
|
+
"127.0.0.1:5002",
|
|
232
|
+
);
|
|
223
233
|
const pgrepAfter1 = calls.filter((c) => c.argv[0] === "pgrep").length;
|
|
224
234
|
|
|
225
235
|
// the LS port shifts (per-process, volatile) — a fresh probe must RE-DISCOVER it.
|
|
@@ -227,7 +237,7 @@ for (const [code, what] of [
|
|
|
227
237
|
const r2 = await adapter.probe(CONV);
|
|
228
238
|
eq(
|
|
229
239
|
"volatile-route: probe #2 RE-DISCOVERS the changed route = 5003 (no cache)",
|
|
230
|
-
r2.status === "alive" ? r2.route
|
|
240
|
+
r2.status === "alive" ? agyAddress(r2.route) : "(dead)",
|
|
231
241
|
"127.0.0.1:5003",
|
|
232
242
|
);
|
|
233
243
|
ok(
|
|
@@ -241,7 +251,7 @@ for (const [code, what] of [
|
|
|
241
251
|
{
|
|
242
252
|
const { runner, calls } = makeFakeRunner({ pids: [123], ss: ssLine(5002, 123), serving: () => true });
|
|
243
253
|
const adapter = createAntigravityAdapter({ runner, binary: FAKE_BINARY });
|
|
244
|
-
await adapter.send({ lsAddress: "127.0.0.1:5002" }, CONV, "hello world");
|
|
254
|
+
await adapter.send({ backend: "antigravity", lsAddress: "127.0.0.1:5002" }, CONV, "hello world");
|
|
245
255
|
const sendCall = calls.find((c) => c.argv.includes("send-message"));
|
|
246
256
|
eq("send: argv === [binary, agentapi, send-message, conv, content]", sendCall?.argv, [
|
|
247
257
|
FAKE_BINARY,
|
|
@@ -262,7 +272,7 @@ for (const [code, what] of [
|
|
|
262
272
|
const adapter = createAntigravityAdapter({ runner, binary: FAKE_BINARY });
|
|
263
273
|
let threw = false;
|
|
264
274
|
try {
|
|
265
|
-
await adapter.send({ lsAddress: "127.0.0.1:5002" }, CONV, "hi");
|
|
275
|
+
await adapter.send({ backend: "antigravity", lsAddress: "127.0.0.1:5002" }, CONV, "hi");
|
|
266
276
|
} catch (err) {
|
|
267
277
|
threw = true;
|
|
268
278
|
ok("send-fail: error names the non-zero exit", /native-push send failed/.test((err as Error).message));
|
|
@@ -286,7 +296,7 @@ for (const [code, what] of [
|
|
|
286
296
|
const { runner, calls } = makeFakeRunner({ pids: [123], ss: ssLine(5002, 123), serving: () => true });
|
|
287
297
|
const adapter = createAntigravityAdapter({ runner, binary: FAKE_BINARY });
|
|
288
298
|
await adapter.probe(CONV);
|
|
289
|
-
await adapter.send({ lsAddress: "127.0.0.1:5002" }, CONV, "hi");
|
|
299
|
+
await adapter.send({ backend: "antigravity", lsAddress: "127.0.0.1:5002" }, CONV, "hi");
|
|
290
300
|
const metaCall = calls.find((c) => c.argv.includes("get-conversation-metadata"));
|
|
291
301
|
const sendCall = calls.find((c) => c.argv.includes("send-message"));
|
|
292
302
|
eq("timeout: get-conversation-metadata carries the metadata timeout", metaCall?.timeoutMs, AGY_METADATA_TIMEOUT_MS);
|
|
@@ -317,7 +327,7 @@ for (const [code, what] of [
|
|
|
317
327
|
const adapter = createAntigravityAdapter({ runner, binary: FAKE_BINARY });
|
|
318
328
|
let threw = false;
|
|
319
329
|
try {
|
|
320
|
-
await adapter.send({ lsAddress: "127.0.0.1:5002" }, CONV, "hi");
|
|
330
|
+
await adapter.send({ backend: "antigravity", lsAddress: "127.0.0.1:5002" }, CONV, "hi");
|
|
321
331
|
} catch {
|
|
322
332
|
threw = true;
|
|
323
333
|
}
|
|
@@ -327,15 +337,9 @@ for (const [code, what] of [
|
|
|
327
337
|
// ── resolver fail-fast (mirror resolveAcpBackendAdapter) ─────────────────────
|
|
328
338
|
eq("resolve: antigravity → the antigravity adapter", resolveNativePushAdapter("antigravity").id, "antigravity");
|
|
329
339
|
eq("adapter id: antigravityAdapter.id === antigravity", antigravityAdapter.id, "antigravity");
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
resolveNativePushAdapter("codex");
|
|
334
|
-
} catch {
|
|
335
|
-
threw = true;
|
|
336
|
-
}
|
|
337
|
-
ok("resolve: unknown backend (codex) → throw (no silent default)", threw);
|
|
338
|
-
}
|
|
340
|
+
eq("resolve: codex → the Codex adapter", resolveNativePushAdapter("codex").id, "codex");
|
|
341
|
+
eq("retry policy: Antigravity may retry", resolveNativePushAdapter("antigravity").retriable, true);
|
|
342
|
+
eq("retry policy: Codex never retries", resolveNativePushAdapter("codex").retriable, false);
|
|
339
343
|
{
|
|
340
344
|
let threw = false;
|
|
341
345
|
try {
|
|
@@ -39,6 +39,7 @@ const REPO_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."
|
|
|
39
39
|
function fakeAdapter(probe: NativePushProbeResult): NativePushAdapter {
|
|
40
40
|
return {
|
|
41
41
|
id: "antigravity",
|
|
42
|
+
retriable: true,
|
|
42
43
|
async probe() {
|
|
43
44
|
return probe;
|
|
44
45
|
},
|
|
@@ -48,7 +49,10 @@ function fakeAdapter(probe: NativePushProbeResult): NativePushAdapter {
|
|
|
48
49
|
};
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
const ALIVE: NativePushProbeResult = {
|
|
52
|
+
const ALIVE: NativePushProbeResult = {
|
|
53
|
+
status: "alive",
|
|
54
|
+
route: { backend: "antigravity", lsAddress: "127.0.0.1:5599" },
|
|
55
|
+
};
|
|
52
56
|
|
|
53
57
|
async function main(): Promise<void> {
|
|
54
58
|
const dir = mkdtempSync(path.join(os.tmpdir(), "entwurf-register-"));
|
|
@@ -297,6 +297,7 @@ function runSubcommand(sub: string, env: Record<string, string | undefined>): {
|
|
|
297
297
|
"smoke-acp-long-turn-live": ["VERIFY.md", "on-demand, not part of `release-gate`"],
|
|
298
298
|
"smoke-mux-fresh-call-live": ["VERIFY.md", "Fresh-call LIVE is on-demand, not part of `release-gate`"],
|
|
299
299
|
"smoke-agy-native-push-live": ["VERIFY.md", "Aggregate release-gate does not own an agy conversation id"],
|
|
300
|
+
"smoke-codex-native-push-live": ["VERIFY.md", "Aggregate release-gate does not own a loaded Codex thread"],
|
|
300
301
|
"smoke-acp-ordering-probe-live": ["docs/acp-backend-rail.md", "opt-in paired observation"],
|
|
301
302
|
// Cortex needs an external Snowflake connection the HOST owns, so an aggregate
|
|
302
303
|
// that required it would block every cut taken without that account. Excluded —
|
|
@@ -806,6 +807,48 @@ function runSubcommand(sub: string, env: Record<string, string | undefined>): {
|
|
|
806
807
|
);
|
|
807
808
|
}
|
|
808
809
|
|
|
810
|
+
// ===========================================================================
|
|
811
|
+
// Codex first admission is a MUST; the loaded-thread probe remains on-demand
|
|
812
|
+
//
|
|
813
|
+
// The old native-push smoke consumes a thread the operator loaded in an attached
|
|
814
|
+
// TUI, so the aggregate cannot own its prerequisite. The first-admission smoke
|
|
815
|
+
// instead owns all three visible windows in the Pi -> Codex -> Pi chain and takes
|
|
816
|
+
// only an explicit app-server PID as seat authority. The new gate must not silently
|
|
817
|
+
// pull the old loaded-thread probe into release-gate, and keeping the old exclusion
|
|
818
|
+
// must not excuse the new end-to-end acceptance.
|
|
819
|
+
// ===========================================================================
|
|
820
|
+
{
|
|
821
|
+
const runSh = readFileSync(join(REPO_DIR, "run.sh"), "utf8");
|
|
822
|
+
const gateBody = runSh.slice(runSh.indexOf("release_gate() {"), runSh.indexOf("# 5. Summary"));
|
|
823
|
+
const verify = readFileSync(join(REPO_DIR, "VERIFY.md"), "utf8");
|
|
824
|
+
const admissionSteps = gateBody.split("\n").filter((line) => line.includes('"$self" smoke-codex-fresh-live'));
|
|
825
|
+
const loadedThreadSteps = gateBody
|
|
826
|
+
.split("\n")
|
|
827
|
+
.filter((line) => line.includes('"$self" smoke-codex-native-push-live'));
|
|
828
|
+
const gaps: string[] = [];
|
|
829
|
+
if (admissionSteps.length !== 1) {
|
|
830
|
+
gaps.push(`release_gate runs smoke-codex-fresh-live ${admissionSteps.length}x (need exactly one MUST step)`);
|
|
831
|
+
}
|
|
832
|
+
if (admissionSteps.length === 1 && !admissionSteps[0]?.includes("run_live_step")) {
|
|
833
|
+
gaps.push("smoke-codex-fresh-live does not go through run_live_step, so LIVE=1/SKIP cannot reach --cut");
|
|
834
|
+
}
|
|
835
|
+
if (loadedThreadSteps.length !== 0) {
|
|
836
|
+
gaps.push(`release_gate runs the operator-thread smoke ${loadedThreadSteps.length}x (need zero)`);
|
|
837
|
+
}
|
|
838
|
+
if (!verify.includes("Aggregate release-gate does not own a loaded Codex thread")) {
|
|
839
|
+
gaps.push("VERIFY.md no longer states why smoke-codex-native-push-live remains on-demand");
|
|
840
|
+
}
|
|
841
|
+
if (!runSh.includes("ENTWURF_CODEX_APP_SERVER_PID")) {
|
|
842
|
+
gaps.push("run.sh no longer names the explicit operator-owned app-server PID prerequisite");
|
|
843
|
+
}
|
|
844
|
+
assert.ok(
|
|
845
|
+
gaps.length === 0,
|
|
846
|
+
"[QK:CODEX-FIRST-ADMISSION-IS-RELEASE-MUST] the fresh Codex acceptance must run exactly once through " +
|
|
847
|
+
"run_live_step, while the loaded-thread native-push smoke stays excluded for its documented operator-owned " +
|
|
848
|
+
`prerequisite. Broken: ${gaps.join("; ")}`,
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
|
|
809
852
|
// ===========================================================================
|
|
810
853
|
// The operator's CONFIGURED bridge invocation is proven BEFORE the cost-bearing LIVE tier
|
|
811
854
|
//
|
|
@@ -873,5 +916,8 @@ console.log(
|
|
|
873
916
|
"five historical reds; and " +
|
|
874
917
|
"every gate a committed mutant names is itself inside check:full or states its exclusion in prose an operator " +
|
|
875
918
|
"reads; and the CI push trigger is filtered to branch refs, so a release tag creates no duplicate run; and " +
|
|
876
|
-
"
|
|
919
|
+
"Pi -> visible Codex -> visible Pi first-admission callback/addressed/outbound acceptance is a release MUST " +
|
|
920
|
+
"while the operator-loaded-thread Codex native-push probe remains excluded; and the operator's CONFIGURED " +
|
|
921
|
+
"bridge invocation is booted exactly once " +
|
|
922
|
+
"through run_step, before the ACP LIVE tier",
|
|
877
923
|
);
|
|
@@ -73,7 +73,7 @@ run_setup() { # $1=HOME-root $2=project $3=PATH $4=PI_BIN $5=COPILOT_BIN(opt) $6
|
|
|
73
73
|
set +e
|
|
74
74
|
# ONE physical line by contract: check-install-surface S5c is a line-scoped static tripwire,
|
|
75
75
|
# so the sandbox env assignments must ride the same line as the run.sh drive they guard.
|
|
76
|
-
OUT="$(HOME="$1" XDG_DATA_HOME="$1/.local/share" XDG_STATE_HOME="$1/.local/state" XDG_CACHE_HOME="$1/.cache" XDG_CONFIG_HOME="$1/.config" PI_CODING_AGENT_DIR="$1/.pi/agent" PATH="$3" PI_BIN="$4" CLAUDE_BIN="$ABSENT" AGY_BIN="$ABSENT" COPILOT_BIN="${5:-$ABSENT}" OMP_BIN="${6:-$ABSENT}" ENTWURF_OMP_AGENT_DIR="$1/.omp/agent" bash "$PKG/run.sh" setup "$2" 2>&1)"
|
|
76
|
+
OUT="$(HOME="$1" XDG_DATA_HOME="$1/.local/share" XDG_STATE_HOME="$1/.local/state" XDG_CACHE_HOME="$1/.cache" XDG_CONFIG_HOME="$1/.config" PI_CODING_AGENT_DIR="$1/.pi/agent" PATH="$3" PI_BIN="$4" CLAUDE_BIN="$ABSENT" AGY_BIN="$ABSENT" COPILOT_BIN="${5:-$ABSENT}" OMP_BIN="${6:-$ABSENT}" CODEX_BIN="$ABSENT" ENTWURF_OMP_AGENT_DIR="$1/.omp/agent" bash "$PKG/run.sh" setup "$2" 2>&1)"
|
|
77
77
|
RC=$?
|
|
78
78
|
set -e
|
|
79
79
|
}
|
|
@@ -93,6 +93,8 @@ want "A: an absent copilot is one zero-state SKIP — no unit composed, no .copi
|
|
|
93
93
|
"printf '%s' \"\$OUT\" | grep -q 'copilot: SKIP' && [ ! -e '$HOME_A/.copilot' ]"
|
|
94
94
|
want "A: an absent omp is one zero-state SKIP — no unit composed, no .omp written [QK:SETUP-OMP-ABSENT-SKIP]" \
|
|
95
95
|
"printf '%s' \"\$OUT\" | grep -q 'omp: SKIP' && [ ! -e '$HOME_A/.omp' ]"
|
|
96
|
+
want "A: an absent Codex is one zero-state SKIP — no unit composed, no .codex written [QK:CODEX-SETUP-ZERO-STATE]" \
|
|
97
|
+
"printf '%s' \"\$OUT\" | grep -q 'codex: SKIP' && [ ! -e '$HOME_A/.codex' ]"
|
|
96
98
|
want "A control: mode named first, pi/claude/agy SKIP, bins PASS, core FAIL, NON-GREEN summary" \
|
|
97
99
|
"printf '%s' \"\$OUT\" | head -n 1 | grep -q 'mode: installed package' && printf '%s' \"\$OUT\" | grep -q 'pi: SKIP' && printf '%s' \"\$OUT\" | grep -q 'claude: SKIP' && printf '%s' \"\$OUT\" | grep -q 'agy: SKIP' && printf '%s' \"\$OUT\" | grep -q 'bins: PASS' && printf '%s' \"\$OUT\" | grep -q 'core: FAIL' && printf '%s' \"\$OUT\" | grep -q 'NON-GREEN'"
|
|
98
100
|
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# codex-birth-doctor — the Codex birth unit verdict: our bytes, and the vendor receipt.
|
|
3
|
+
#
|
|
4
|
+
# Three axes, reported independently, because they fail for different reasons and an
|
|
5
|
+
# operator repairs them with different hands:
|
|
6
|
+
#
|
|
7
|
+
# RUNTIME is there a node the launcher can exec, and a Codex CLI at all?
|
|
8
|
+
# UNIT are the declaration, launcher and closure present, ours by digest, and
|
|
9
|
+
# owned/permissioned so that nobody else could have written them?
|
|
10
|
+
# TRUST has the VENDOR recorded the operator's one-time decision for this exact
|
|
11
|
+
# declaration identity?
|
|
12
|
+
#
|
|
13
|
+
# WHAT THE TRUST AXIS IS, EXACTLY. It reads `$CODEX_HOME/config.toml` and looks for the
|
|
14
|
+
# vendor's own receipt at the fixed key `<hooks.json>:session_start:0:0`, whose value must
|
|
15
|
+
# be a `trusted_hash` of the shape `sha256:<64 hex>`. That is ALL it claims: a vendor trust
|
|
16
|
+
# receipt is present for the declaration identity this unit publishes. It does NOT compute
|
|
17
|
+
# what that hash should be, does not compare it to anything of ours, and therefore never
|
|
18
|
+
# says the approval is cryptographically valid — the vendor hashes a normalized identity of
|
|
19
|
+
# its own (`[source]` hooks/src/engine/discovery.rs:775-792) and recomputing it here would
|
|
20
|
+
# be entwurf asserting authority over somebody else's security decision.
|
|
21
|
+
#
|
|
22
|
+
# WHAT THIS DOCTOR NEVER DOES. It does not write, pre-seed or repair `[hooks.state]`, and it
|
|
23
|
+
# never launches Codex. A missing receipt is not something to fix from here: the operator
|
|
24
|
+
# opens a visible Codex, answers the vendor's prompt once, and comes back.
|
|
25
|
+
#
|
|
26
|
+
# Trust missing makes the WHOLE doctor red, on purpose. A unit whose bytes are perfect but
|
|
27
|
+
# whose hook the vendor will not run is a citizen nobody will ever be born as — reporting
|
|
28
|
+
# that as green is the exact false success this file exists to refuse. `--unit-only` exists
|
|
29
|
+
# for the sandbox gate, which has no vendor and no operator; it can never make setup or the
|
|
30
|
+
# fresh preflight green, because neither passes that flag.
|
|
31
|
+
set -euo pipefail
|
|
32
|
+
|
|
33
|
+
UNIT_ONLY=0
|
|
34
|
+
for arg in "$@"; do
|
|
35
|
+
case "$arg" in
|
|
36
|
+
# For the hermetic gate only: there is no vendor and no operator inside a sandbox, so the
|
|
37
|
+
# TRUST axis has nothing to read. It is REPORTED as skipped, never as green, and setup and
|
|
38
|
+
# the fresh preflight both call this doctor without the flag.
|
|
39
|
+
--unit-only) UNIT_ONLY=1 ;;
|
|
40
|
+
*) printf '[codex-birth-doctor] unknown argument %s — this doctor takes only --unit-only.\n' "'$arg'" >&2; exit 1 ;;
|
|
41
|
+
esac
|
|
42
|
+
done
|
|
43
|
+
|
|
44
|
+
CODEX_HOME="${CODEX_HOME:-$HOME/.codex}"
|
|
45
|
+
HOOKS_FILE="$CODEX_HOME/hooks.json"
|
|
46
|
+
PACKAGE_STATE_ROOT="${XDG_DATA_HOME:-$HOME/.local/share}/entwurf"
|
|
47
|
+
UNIT_ROOT="$PACKAGE_STATE_ROOT/codex-birth"
|
|
48
|
+
HELPER_DIR="$UNIT_ROOT/helper"
|
|
49
|
+
LAUNCHER_NAME="codex-birth-launch.sh"
|
|
50
|
+
LAUNCHER="$HELPER_DIR/$LAUNCHER_NAME"
|
|
51
|
+
STATE_FILE="$UNIT_ROOT/install-state.json"
|
|
52
|
+
ME="$(id -u)"
|
|
53
|
+
|
|
54
|
+
RED=0
|
|
55
|
+
section() { printf '\n── %s %s\n' "$1" "─────────────────────────────────────────"; }
|
|
56
|
+
bad() { printf ' RED %s\n' "$1"; RED=$((RED + 1)); }
|
|
57
|
+
ok() { printf ' ok %s\n' "$1"; }
|
|
58
|
+
info() { printf ' · %s\n' "$1"; }
|
|
59
|
+
|
|
60
|
+
sha_of() { sha256sum -- "$1" | cut -d' ' -f1; }
|
|
61
|
+
|
|
62
|
+
section "PATHS"
|
|
63
|
+
# The declaration records an ABSOLUTE launcher path and the vendor keys its trust receipt to
|
|
64
|
+
# it, so a relative CODEX_HOME or XDG_DATA_HOME would make this doctor judge a different unit
|
|
65
|
+
# depending on the caller's cwd.
|
|
66
|
+
case "$CODEX_HOME" in
|
|
67
|
+
/*) ok "CODEX_HOME resolves absolute ($CODEX_HOME)" ;;
|
|
68
|
+
*) bad "CODEX_HOME resolves to a relative path ('$CODEX_HOME') — every verdict below would depend on the caller's cwd." ;;
|
|
69
|
+
esac
|
|
70
|
+
case "$UNIT_ROOT" in
|
|
71
|
+
/*) ok "the unit root resolves absolute ($UNIT_ROOT)" ;;
|
|
72
|
+
*) bad "the unit root resolves to a relative path ('$UNIT_ROOT') — set an absolute XDG_DATA_HOME or HOME." ;;
|
|
73
|
+
esac
|
|
74
|
+
|
|
75
|
+
section "RUNTIME"
|
|
76
|
+
if NODE_BIN="$(command -v node 2>/dev/null)"; then
|
|
77
|
+
NODE_MAJOR="$("$NODE_BIN" -p 'process.versions.node.split(".")[0]' 2>/dev/null || echo 0)"
|
|
78
|
+
if [ "$NODE_MAJOR" -ge 24 ] 2>/dev/null; then
|
|
79
|
+
ok "node $("$NODE_BIN" -p 'process.versions.node') at $NODE_BIN (>= 24, so strip-types runs the payload)"
|
|
80
|
+
else
|
|
81
|
+
bad "node at $NODE_BIN reports major '$NODE_MAJOR'; the payload is raw TypeScript and needs >= 24."
|
|
82
|
+
fi
|
|
83
|
+
else
|
|
84
|
+
bad "no node on PATH — the hook payload cannot run."
|
|
85
|
+
NODE_BIN=""
|
|
86
|
+
fi
|
|
87
|
+
if CODEX_BIN="$(command -v "${CODEX_BIN:-codex}" 2>/dev/null)"; then
|
|
88
|
+
info "codex CLI: $CODEX_BIN $("$CODEX_BIN" --version 2>/dev/null | head -1)"
|
|
89
|
+
else
|
|
90
|
+
info "no codex CLI on PATH — the unit's bytes are still judged below; the harness is the operator's."
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
# The directory holding the receipt carries the receipt's authority: anyone who can write it
|
|
94
|
+
# can replace what licenses every removal. Judged before the state is read, and never repaired.
|
|
95
|
+
judge_parent() { # $1 = dir, $2 = label
|
|
96
|
+
[ -e "$1" ] || return 0
|
|
97
|
+
if [ -L "$1" ]; then bad "$2 ($1) is a SYMLINK — ownership authority may not come through a link."; return 0; fi
|
|
98
|
+
[ -d "$1" ] || { bad "$2 ($1) is not a directory."; return 0; }
|
|
99
|
+
local st mode uid
|
|
100
|
+
st="$(stat -c '%a %u' -- "$1")"; mode="$(printf '%s' "$st" | cut -d' ' -f1)"; uid="$(printf '%s' "$st" | cut -d' ' -f2)"
|
|
101
|
+
[ "$uid" = "$ME" ] || { bad "$2 ($1) is owned by uid $uid, not you ($ME) — it holds the receipt that licenses removals."; return 0; }
|
|
102
|
+
[ $((8#$mode & 0022)) -eq 0 ] || bad "$2 ($1) is group/world-writable (mode $mode), so the ownership state inside it is not provably ours."
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
section "UNIT (the bytes this unit publishes, and whether they are still ours)"
|
|
106
|
+
judge_parent "$PACKAGE_STATE_ROOT" "the package state root"
|
|
107
|
+
judge_parent "$UNIT_ROOT" "the unit root"
|
|
108
|
+
if [ ! -e "$STATE_FILE" ] && [ ! -L "$STATE_FILE" ]; then
|
|
109
|
+
if [ -e "$HOOKS_FILE" ] || [ -e "$HELPER_DIR" ]; then
|
|
110
|
+
bad "no ownership state at $STATE_FILE, yet $HOOKS_FILE or $HELPER_DIR exists — those bytes are not provably ours and the inverse will refuse them. Inspect them."
|
|
111
|
+
else
|
|
112
|
+
# NOT an informational note. "Nothing is installed" is the most complete way for this
|
|
113
|
+
# unit to be broken: no declaration, no closure, no record will ever be minted — and a
|
|
114
|
+
# host that has trusted an OLDER declaration still carries a vendor receipt, so the TRUST
|
|
115
|
+
# axis alone can read green over an empty unit. Measured exactly that way before this was
|
|
116
|
+
# red. An absent unit is a red unit, in both modes.
|
|
117
|
+
bad "no Codex birth unit is installed here (no ownership state at $STATE_FILE, no declaration, no closure), so no Codex thread on this host can become a citizen.
|
|
118
|
+
Install: ./run.sh install-codex-birth"
|
|
119
|
+
fi
|
|
120
|
+
elif [ -L "$STATE_FILE" ]; then
|
|
121
|
+
bad "$STATE_FILE is a SYMLINK — removal authority may not come through a link."
|
|
122
|
+
elif [ ! -f "$STATE_FILE" ]; then
|
|
123
|
+
bad "$STATE_FILE is not a regular file."
|
|
124
|
+
elif [ -z "$NODE_BIN" ]; then
|
|
125
|
+
bad "the ownership state cannot be parsed without node."
|
|
126
|
+
else
|
|
127
|
+
STATE_ST="$(stat -c '%a %u' -- "$STATE_FILE")"
|
|
128
|
+
STATE_MODE="$(printf '%s' "$STATE_ST" | cut -d' ' -f1)"
|
|
129
|
+
STATE_UID="$(printf '%s' "$STATE_ST" | cut -d' ' -f2)"
|
|
130
|
+
[ "$STATE_UID" = "$ME" ] || bad "$STATE_FILE is owned by uid $STATE_UID, not you ($ME) — anyone who can rewrite it can license removal of the paths it names."
|
|
131
|
+
[ $((8#$STATE_MODE & 0022)) -eq 0 ] || bad "$STATE_FILE is group/world-writable (mode $STATE_MODE) — the same authority problem."
|
|
132
|
+
|
|
133
|
+
INVENTORY="$("$NODE_BIN" -e '
|
|
134
|
+
const fs = require("node:fs");
|
|
135
|
+
const path = require("node:path");
|
|
136
|
+
const s = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
|
137
|
+
const out = [];
|
|
138
|
+
const hex = (v) => typeof v === "string" && /^[0-9a-f]{64}$/.test(v);
|
|
139
|
+
if (s.schema !== "codex-birth-install-state/v1") out.push("BAD\tforeign state schema " + JSON.stringify(s.schema));
|
|
140
|
+
if (s.status !== "installed") out.push("BAD\tthe state status is " + JSON.stringify(s.status) + ", so a previous install did not finish publishing. Repair: ./run.sh install-codex-birth");
|
|
141
|
+
if (s.hooksFile !== process.argv[2]) out.push("BAD\tthe state is bound to " + s.hooksFile + ", not the fixed path " + process.argv[2]);
|
|
142
|
+
if (s.helperDir !== process.argv[3]) out.push("BAD\tthe state is bound to helper dir " + s.helperDir + ", not the fixed path " + process.argv[3]);
|
|
143
|
+
if (!hex(s.hooksSha256)) out.push("BAD\thooksSha256 is not a digest");
|
|
144
|
+
else out.push("WANT\t" + s.hooksSha256 + "\t" + s.hooksFile + "\tthe declaration");
|
|
145
|
+
const expected = [
|
|
146
|
+
"codex-birth-launch.sh",
|
|
147
|
+
"meta-bridge-hook-codex.ts",
|
|
148
|
+
"lib/meta-session.ts",
|
|
149
|
+
"lib/native-push/codex-ws-client.ts",
|
|
150
|
+
"lib/session-id.js",
|
|
151
|
+
"entwurf-capabilities.json",
|
|
152
|
+
];
|
|
153
|
+
if (!Array.isArray(s.helperFiles) || s.helperFiles.length === 0) out.push("BAD\thelperFiles is empty");
|
|
154
|
+
else {
|
|
155
|
+
// Exactly the closure this unit publishes. A short inventory hides a file the inverse
|
|
156
|
+
// can never reclaim; a long one licenses deleting something we never wrote.
|
|
157
|
+
const named = s.helperFiles.map((f) => (f == null ? "" : f.path));
|
|
158
|
+
const missing = expected.filter((name) => !named.includes(name));
|
|
159
|
+
const extra = named.filter((name) => !expected.includes(name));
|
|
160
|
+
const duplicate = named.filter((name, i) => named.indexOf(name) !== i);
|
|
161
|
+
if (missing.length) out.push("BAD\tthe recorded closure inventory is missing " + missing.join(", ") + " — the doctor cannot judge bytes no state names");
|
|
162
|
+
if (extra.length) out.push("BAD\tthe recorded closure inventory names files this unit never publishes: " + extra.join(", "));
|
|
163
|
+
if (duplicate.length) out.push("BAD\tthe recorded closure inventory repeats " + duplicate.join(", "));
|
|
164
|
+
}
|
|
165
|
+
if (Array.isArray(s.helperFiles)) for (const f of s.helperFiles) {
|
|
166
|
+
if (typeof f?.path !== "string" || f.path.startsWith("/") || f.path.split("/").includes("..")) { out.push("BAD\tunsafe helper path " + JSON.stringify(f?.path)); continue; }
|
|
167
|
+
if (!hex(f.sha256)) { out.push("BAD\tbad helper digest for " + f.path); continue; }
|
|
168
|
+
out.push(["WANT", f.sha256, path.join(s.helperDir, f.path), "helper member " + f.path, f.mode ?? ""].join("\t"));
|
|
169
|
+
}
|
|
170
|
+
out.push("INFO\tunitVersion=" + (s.unitVersion ?? "?") + " nodeBin=" + (s.nodeBin ?? "?"));
|
|
171
|
+
process.stdout.write(out.join("\n") + "\n");
|
|
172
|
+
' "$STATE_FILE" "$HOOKS_FILE" "$HELPER_DIR" 2>&1)" || { bad "the ownership state is unreadable or malformed: $INVENTORY"; INVENTORY=""; }
|
|
173
|
+
|
|
174
|
+
while IFS="$(printf '\t')" read -r kind a b c d; do
|
|
175
|
+
case "$kind" in
|
|
176
|
+
BAD) bad "$a" ;;
|
|
177
|
+
INFO) info "$a" ;;
|
|
178
|
+
WANT)
|
|
179
|
+
if [ -L "$b" ]; then bad "$c is a SYMLINK ($b) — this unit publishes no links."
|
|
180
|
+
elif [ ! -e "$b" ]; then bad "$c is MISSING: $b"
|
|
181
|
+
elif [ ! -f "$b" ]; then bad "$c is not a regular file: $b"
|
|
182
|
+
else
|
|
183
|
+
live="$(sha_of "$b")"
|
|
184
|
+
st="$(stat -c '%a %u' -- "$b")"
|
|
185
|
+
mode="$(printf '%s' "$st" | cut -d' ' -f1)"; uid="$(printf '%s' "$st" | cut -d' ' -f2)"
|
|
186
|
+
if [ "$live" != "$a" ]; then bad "$c was EDITED after install ($b: live $live, recorded $a) — the inverse will refuse to remove it."
|
|
187
|
+
elif [ "$uid" != "$ME" ]; then bad "$c is owned by uid $uid, not you ($ME): $b"
|
|
188
|
+
elif [ $((8#$mode & 0022)) -ne 0 ]; then bad "$c is group/world-writable (mode $mode): $b"
|
|
189
|
+
else ok "$c matches the state ($b)"
|
|
190
|
+
fi
|
|
191
|
+
[ -n "$d" ] && [ "$mode" != "${d#0}" ] && [ "0$mode" != "$d" ] && info "$c mode is $mode, state recorded $d"
|
|
192
|
+
fi
|
|
193
|
+
;;
|
|
194
|
+
esac
|
|
195
|
+
done <<EOF
|
|
196
|
+
$INVENTORY
|
|
197
|
+
EOF
|
|
198
|
+
fi
|
|
199
|
+
|
|
200
|
+
TRUST_REPAIR="Repair: open a visible plain Codex (no flags), answer the vendor's prompt with
|
|
201
|
+
'Trust all and continue', send one first turn, then re-run this doctor."
|
|
202
|
+
|
|
203
|
+
section "TRUST (the vendor's receipt for this exact declaration)"
|
|
204
|
+
if [ "$UNIT_ONLY" = "1" ]; then
|
|
205
|
+
info "--unit-only: the vendor receipt was NOT read. This is a skipped axis, not a green one."
|
|
206
|
+
elif [ ! -e "$CODEX_HOME/config.toml" ]; then
|
|
207
|
+
bad "$CODEX_HOME/config.toml does not exist, so the vendor has recorded no trust decision for $HOOKS_FILE.
|
|
208
|
+
$TRUST_REPAIR"
|
|
209
|
+
elif ! command -v python3 >/dev/null 2>&1; then
|
|
210
|
+
bad "python3 is required to read the vendor config (the same reader the MCP/status-line atoms use) and is not on PATH."
|
|
211
|
+
else
|
|
212
|
+
# The key the vendor writes is `<declaration path>:<event>:<group>:<handler>` — measured on
|
|
213
|
+
# this host after one 'Trust all'. We look for exactly ours: a receipt for a DIFFERENT
|
|
214
|
+
# declaration is not this unit's approval, however valid it is for whoever owns it.
|
|
215
|
+
TRUST_KEY="$HOOKS_FILE:session_start:0:0"
|
|
216
|
+
# python3 + tomllib, the same reader the two config atoms already use — node has no TOML
|
|
217
|
+
# parser and inventing one here would be a second opinion about the vendor's own file.
|
|
218
|
+
TRUST_OUT="$(python3 -c '
|
|
219
|
+
import sys, tomllib
|
|
220
|
+
cfg_path, key = sys.argv[1], sys.argv[2]
|
|
221
|
+
try:
|
|
222
|
+
cfg = tomllib.load(open(cfg_path, "rb"))
|
|
223
|
+
except Exception as err: # noqa: BLE001 - the message is the verdict
|
|
224
|
+
sys.stdout.write("ERROR\t" + str(err)); raise SystemExit(0)
|
|
225
|
+
state = cfg.get("hooks", {}).get("state")
|
|
226
|
+
if not isinstance(state, dict):
|
|
227
|
+
sys.stdout.write("ABSENT\tno [hooks.state] table"); raise SystemExit(0)
|
|
228
|
+
if key not in state:
|
|
229
|
+
others = sorted(state.keys())
|
|
230
|
+
sys.stdout.write("WRONGKEY\t" + (", ".join(others) if others else "(none)")); raise SystemExit(0)
|
|
231
|
+
entry = state[key]
|
|
232
|
+
digest = entry.get("trusted_hash") if isinstance(entry, dict) else None
|
|
233
|
+
if not isinstance(digest, str):
|
|
234
|
+
sys.stdout.write("MALFORMED\tthe receipt carries no trusted_hash string"); raise SystemExit(0)
|
|
235
|
+
import re
|
|
236
|
+
if not re.fullmatch(r"sha256:[0-9a-f]{64}", digest):
|
|
237
|
+
sys.stdout.write("MALFORMED\ttrusted_hash is not sha256:<64 hex>: " + repr(digest)); raise SystemExit(0)
|
|
238
|
+
sys.stdout.write("PRESENT\t" + digest)
|
|
239
|
+
' "$CODEX_HOME/config.toml" "$TRUST_KEY" 2>&1)" || TRUST_OUT="ERROR $TRUST_OUT"
|
|
240
|
+
TRUST_KIND="$(printf '%s' "$TRUST_OUT" | cut -f1)"
|
|
241
|
+
TRUST_DETAIL="$(printf '%s' "$TRUST_OUT" | cut -f2-)"
|
|
242
|
+
case "$TRUST_KIND" in
|
|
243
|
+
PRESENT)
|
|
244
|
+
ok "a vendor trust receipt is present for $TRUST_KEY ($TRUST_DETAIL)"
|
|
245
|
+
info "this reports that the vendor RECORDED a decision for this declaration identity — it is"
|
|
246
|
+
info "not a cryptographic validation, and this doctor never computes what that hash should be."
|
|
247
|
+
;;
|
|
248
|
+
ABSENT)
|
|
249
|
+
bad "no vendor trust receipt for this declaration — $TRUST_DETAIL. Until the operator answers the
|
|
250
|
+
vendor's prompt once, this hook is declared and never runs.
|
|
251
|
+
$TRUST_REPAIR"
|
|
252
|
+
;;
|
|
253
|
+
WRONGKEY)
|
|
254
|
+
bad "the vendor has trust receipts, but NONE at this unit's key $TRUST_KEY (present: $TRUST_DETAIL).
|
|
255
|
+
A receipt for another declaration is somebody else's approval, not this one's.
|
|
256
|
+
$TRUST_REPAIR"
|
|
257
|
+
;;
|
|
258
|
+
MALFORMED)
|
|
259
|
+
bad "the vendor trust receipt at $TRUST_KEY is unreadable: $TRUST_DETAIL.
|
|
260
|
+
$TRUST_REPAIR"
|
|
261
|
+
;;
|
|
262
|
+
*)
|
|
263
|
+
bad "the vendor config at $CODEX_HOME/config.toml could not be read: $TRUST_DETAIL"
|
|
264
|
+
;;
|
|
265
|
+
esac
|
|
266
|
+
fi
|
|
267
|
+
|
|
268
|
+
if [ "$RED" -ne 0 ]; then
|
|
269
|
+
printf '\n[codex-birth-doctor] %d RED — see above.\n' "$RED" >&2
|
|
270
|
+
exit 1
|
|
271
|
+
fi
|
|
272
|
+
if [ "$UNIT_ONLY" = "1" ]; then
|
|
273
|
+
printf '\n[codex-birth-doctor] unit bytes and ownership are intact; the TRUST axis was skipped (--unit-only).\n'
|
|
274
|
+
else
|
|
275
|
+
printf '\n[codex-birth-doctor] unit bytes are ours and the vendor trust receipt is present for this declaration.\n'
|
|
276
|
+
fi
|