@junghanacs/entwurf 0.21.0 → 0.22.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 +2 -2
- package/BASELINE.md +3 -1
- package/CHANGELOG.md +278 -0
- package/DELIVERY.md +156 -26
- package/README.md +64 -13
- package/VERIFY.md +67 -11
- package/docs/external-mcp-host.md +16 -6
- package/docs/setup-clean-host.md +63 -22
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +40 -16
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-caller-seat.js +174 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +194 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +3 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +8 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +160 -25
- package/mcp/entwurf-bridge/dist/scripts/codex-socket-path.js +30 -0
- package/mcp/entwurf-bridge/src/index.ts +50 -16
- package/mcp/entwurf-bridge/tsconfig.build.json +9 -0
- package/package.json +2 -2
- package/pi-extensions/entwurf-control.ts +4 -4
- package/pi-extensions/lib/codex-caller-seat.ts +204 -0
- package/pi-extensions/lib/codex-fresh-preflight.ts +218 -1
- package/pi-extensions/lib/entwurf-v2-runner.ts +3 -2
- package/pi-extensions/lib/entwurf-v2-send.ts +16 -11
- package/pi-extensions/lib/mux-fresh-call.ts +196 -37
- package/run.sh +127 -3
- package/scripts/check-codex-app-server-launch.ts +445 -0
- package/scripts/check-entwurf-v2-production.ts +42 -1
- package/scripts/check-entwurf-v2-send.ts +26 -7
- package/scripts/check-gate-qualification.ts +4 -2
- package/scripts/check-mux-launch-tmux.ts +331 -35
- package/scripts/codex-app-server-launch.sh +275 -0
- package/scripts/codex-socket-path.ts +33 -0
- package/scripts/codex-terminal-title-config.py +500 -0
- package/scripts/codex_toml_io.py +121 -0
- package/scripts/lib/codex-fresh-live-protocol.ts +11 -3
- package/scripts/lib/codex-fresh-source-receipts.ts +29 -2
- package/scripts/mutants/codex-app-server-launch.json +157 -0
- package/scripts/mutants/codex-caller-seat.json +336 -0
- package/scripts/mutants/codex-native.json +3 -3
- package/scripts/mutants/mux-fresh-call.json +86 -14
- package/scripts/mutants/v2-surface.json +22 -0
- package/scripts/smoke-codex-config-state.sh +192 -3
- package/scripts/smoke-codex-fresh-live.ts +277 -37
- package/scripts/smoke-entwurf-chain-live.ts +50 -0
- package/scripts/smoke-setup-verdict.sh +13 -11
|
@@ -1,16 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* First-admission LIVE acceptance for Codex visible fresh
|
|
2
|
+
* First-admission LIVE acceptance for Codex visible fresh — reshaped for the #95 lane B caller
|
|
3
|
+
* seat.
|
|
3
4
|
*
|
|
4
|
-
* The operator owns the
|
|
5
|
-
* creates nor supervises them: an explicit
|
|
6
|
-
* process whose /proc environment must
|
|
7
|
-
*
|
|
5
|
+
* The operator owns the Codex app-server and the tmux session it sits in (#95 D1 retired the fixed
|
|
6
|
+
* `codex` home). This smoke neither creates nor supervises them: an explicit
|
|
7
|
+
* ENTWURF_CODEX_APP_SERVER_PID names the already-running process whose /proc environment must
|
|
8
|
+
* corroborate its seat. The attached TUI pane is never guessed.
|
|
9
|
+
*
|
|
10
|
+
* ── THE TOPOLOGY, AND WHY IT IS DELIBERATELY SPLIT (#95 lane B) ──
|
|
11
|
+
*
|
|
12
|
+
* Four coordinates on ONE tmux server, in TWO sessions:
|
|
13
|
+
*
|
|
14
|
+
* A = wherever the operator started their app-server, and it must NOT be S. This is the
|
|
15
|
+
* session the app-server's inherited `TMUX` names — the value a Codex MCP child would
|
|
16
|
+
* fall back to. Its NAME does not matter: #95 D1 retired the fixed `codex` home, so the
|
|
17
|
+
* operator seats the app-server wherever they like and Entwurf never asks which room.
|
|
18
|
+
* S = the fixture's own session. The initial Pi caller opens here.
|
|
19
|
+
*
|
|
20
|
+
* initial Pi → S (the fixture's own seat)
|
|
21
|
+
* fresh Codex → S (hop 1, omitted placement: the caller's own session)
|
|
22
|
+
* outbound Pi → S (hop 2, omitted placement — and this is the claim)
|
|
23
|
+
*
|
|
24
|
+
* Hop 2 is the whole point. Its seat is decided by the Codex caller's own pane title
|
|
25
|
+
* (`codex-title-anchor`), and the proof is that it lands in S: the app-server's environment
|
|
26
|
+
* points at A, so an env fallback — the pre-#95 behaviour — would have put it there. S is
|
|
27
|
+
* reachable only through the anchor. A receipt that says `codex-title-anchor` AND a session
|
|
28
|
+
* that is not A are two independent halves of the same claim, and both are asserted.
|
|
29
|
+
*
|
|
30
|
+
* A ≠ S is therefore a PRECONDITION of this card, not a convenience: run it with the app-server
|
|
31
|
+
* in the fixture's own session and hop 2 proves nothing, because both rules would answer S.
|
|
8
32
|
*
|
|
9
33
|
* RELEASE MUST once Codex is admitted. LIVE!=1 is the only host-prerequisite SKIP. With LIVE=1,
|
|
10
|
-
* missing system birth, user MCP/status config, app-server, models, runtime,
|
|
11
|
-
* is a FAIL. The smoke drives only public MCP calls, then joins Pi's native
|
|
12
|
-
* rows and Codex app-server thread events as receipt authority; screen text
|
|
13
|
-
* reports are never oracles.
|
|
34
|
+
* missing system birth, user MCP/status/terminal-title config, app-server, models, runtime,
|
|
35
|
+
* bridge, or tmux seat is a FAIL. The smoke drives only public MCP calls, then joins Pi's native
|
|
36
|
+
* toolCall/toolResult rows and Codex app-server thread events as receipt authority; screen text
|
|
37
|
+
* and model-reformatted reports are never oracles.
|
|
14
38
|
*
|
|
15
39
|
* A record-backed self-fetch fixture opens a real visible Pi caller through public fresh_call.
|
|
16
40
|
* That Pi opens Codex, correlates Codex's raw callback, and addresses Codex through v2. Codex then
|
|
@@ -24,7 +48,14 @@ import { createHash } from "node:crypto";
|
|
|
24
48
|
import * as fs from "node:fs";
|
|
25
49
|
import * as os from "node:os";
|
|
26
50
|
import * as path from "node:path";
|
|
27
|
-
import {
|
|
51
|
+
import {
|
|
52
|
+
CODEX_CALLER_PREFLIGHT_HINT,
|
|
53
|
+
CODEX_LAUNCH_CWD_PREFLIGHT_HINT,
|
|
54
|
+
CODEX_PREFLIGHT_HINT,
|
|
55
|
+
codexCallerFreshPreflight,
|
|
56
|
+
codexFreshPreflight,
|
|
57
|
+
codexLaunchCwdFreshPreflight,
|
|
58
|
+
} from "../pi-extensions/lib/codex-fresh-preflight.ts";
|
|
28
59
|
import {
|
|
29
60
|
defaultMetaMailboxDir,
|
|
30
61
|
defaultMetaSessionsDir,
|
|
@@ -35,7 +66,7 @@ import {
|
|
|
35
66
|
writeMetaReceiverMarker,
|
|
36
67
|
writeMetaSenderMarker,
|
|
37
68
|
} from "../pi-extensions/lib/meta-session.ts";
|
|
38
|
-
import {
|
|
69
|
+
import { FRESH_CALL_BACKENDS } from "../pi-extensions/lib/mux-fresh-call.ts";
|
|
39
70
|
import {
|
|
40
71
|
readCodexThread,
|
|
41
72
|
realCodexProtocolOpener,
|
|
@@ -211,6 +242,68 @@ function inspectTmuxCoordinate(label: string, env: NodeJS.ProcessEnv): TmuxCoord
|
|
|
211
242
|
};
|
|
212
243
|
}
|
|
213
244
|
|
|
245
|
+
/**
|
|
246
|
+
* The session NAME behind a coordinate. Read on its own rather than added to the shared
|
|
247
|
+
* `TMUX_COORDINATE_FORMAT`: that row's field count is a pinned contract with its own
|
|
248
|
+
* deterministic oracle, and this card needs a name for exactly two reasons — to hand the fresh
|
|
249
|
+
* hand the fresh call a seat it can address, and to record which rooms A and S actually were.
|
|
250
|
+
*/
|
|
251
|
+
function tmuxSessionName(label: string, env: NodeJS.ProcessEnv, paneId: string): string {
|
|
252
|
+
const inspected = spawnSync("tmux", ["display-message", "-p", "-t", paneId, "#{session_name}"], {
|
|
253
|
+
env,
|
|
254
|
+
encoding: "utf8",
|
|
255
|
+
});
|
|
256
|
+
ok(
|
|
257
|
+
`${label} session name answers through its own environment`,
|
|
258
|
+
inspected.status === 0,
|
|
259
|
+
String(inspected.stderr ?? ""),
|
|
260
|
+
);
|
|
261
|
+
const name = String(inspected.stdout ?? "").replace(/\r?\n$/, "");
|
|
262
|
+
ok(`${label} session name is non-empty`, name.length > 0, `name=${JSON.stringify(name)}`);
|
|
263
|
+
return name;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* The DIRECTORY behind a pane, read from tmux itself. Its own reader for the same reason the
|
|
268
|
+
* session name has one: this card needs it for exactly one claim (#95 lane C — where a sibling
|
|
269
|
+
* actually started), and the pinned coordinate row stays a fixed field count.
|
|
270
|
+
*
|
|
271
|
+
* `[측정 2026-09-16]` this value is read from the child's `/proc` at QUERY time and tmux chdirs
|
|
272
|
+
* in that child AFTER forking it, so a read taken in the same millisecond as a launch can still
|
|
273
|
+
* answer the SERVER's directory. Every call here happens after that citizen's own callback has
|
|
274
|
+
* already arrived, which is long past exec — the deterministic gate that reads it straight after
|
|
275
|
+
* a launch is the one that has to wait (`scripts/check-mux-launch-tmux.ts`).
|
|
276
|
+
*/
|
|
277
|
+
function tmuxPaneCwd(label: string, env: NodeJS.ProcessEnv, paneId: string): string {
|
|
278
|
+
const inspected = spawnSync("tmux", ["display-message", "-p", "-t", paneId, "#{pane_current_path}"], {
|
|
279
|
+
env,
|
|
280
|
+
encoding: "utf8",
|
|
281
|
+
});
|
|
282
|
+
ok(
|
|
283
|
+
`${label} pane directory answers through its own environment`,
|
|
284
|
+
inspected.status === 0,
|
|
285
|
+
String(inspected.stderr ?? ""),
|
|
286
|
+
);
|
|
287
|
+
const dir = String(inspected.stdout ?? "").replace(/\r?\n$/, "");
|
|
288
|
+
ok(`${label} pane directory is non-empty`, dir.length > 0, `cwd=${JSON.stringify(dir)}`);
|
|
289
|
+
return dir;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** A live process's own working directory, read from the kernel rather than from any report. */
|
|
293
|
+
function processCwd(pid: number): string {
|
|
294
|
+
return fs.readlinkSync(`/proc/${pid}/cwd`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** The `session_meta` header of a Codex rollout — the VENDOR's own record of where a thread opened. */
|
|
298
|
+
function rolloutSessionMetaCwd(transcriptPath: string): string {
|
|
299
|
+
const first = fs.readFileSync(transcriptPath, "utf8").split("\n", 1)[0] ?? "";
|
|
300
|
+
const row = JSON.parse(first) as { type?: unknown; payload?: { cwd?: unknown } };
|
|
301
|
+
if (row.type !== "session_meta") throw new Error(`rollout row 1 is ${JSON.stringify(row.type)}, not session_meta`);
|
|
302
|
+
const cwd = row.payload?.cwd;
|
|
303
|
+
if (typeof cwd !== "string" || cwd.length === 0) throw new Error("rollout session_meta carries no cwd string");
|
|
304
|
+
return cwd;
|
|
305
|
+
}
|
|
306
|
+
|
|
214
307
|
function cleanupWindows(): string[] {
|
|
215
308
|
const failures: string[] = [];
|
|
216
309
|
if (!appServerEnv) return failures;
|
|
@@ -629,15 +722,32 @@ async function run(): Promise<void> {
|
|
|
629
722
|
const appServerCoordinate = inspectTmuxCoordinate("Codex app-server/MCP inherited seat", appServerEnv);
|
|
630
723
|
const fixtureCoordinate = inspectTmuxCoordinate("fixture/initial-Pi caller seat", process.env);
|
|
631
724
|
ok(
|
|
632
|
-
"the fixture/initial-Pi seat and
|
|
725
|
+
"the fixture/initial-Pi seat S and the app-server seat A share one tmux server but are different sessions",
|
|
633
726
|
fixtureCoordinate.serverPid === appServerCoordinate.serverPid &&
|
|
634
727
|
fixtureCoordinate.sessionId !== appServerCoordinate.sessionId,
|
|
635
728
|
`fixture=${fixtureCoordinate.serverPid}/${fixtureCoordinate.sessionId} app-server=${appServerCoordinate.serverPid}/${appServerCoordinate.sessionId}`,
|
|
636
729
|
);
|
|
730
|
+
// A and S, by NAME — recorded so the receipt says which rooms this run actually used. The
|
|
731
|
+
// load-bearing fact is A ≠ S, already asserted above: "the env fallback would have said A"
|
|
732
|
+
// is only a proof when A is a session the anchor could not have chosen. Since #95 D1 there
|
|
733
|
+
// is no required NAME for either — the operator seats the app-server wherever they like.
|
|
734
|
+
const appServerSessionName = tmuxSessionName(
|
|
735
|
+
"Codex app-server/MCP inherited seat",
|
|
736
|
+
appServerEnv,
|
|
737
|
+
appServerCoordinate.paneId,
|
|
738
|
+
);
|
|
739
|
+
const fixtureSessionName = tmuxSessionName("fixture/initial-Pi caller seat", process.env, fixtureCoordinate.paneId);
|
|
740
|
+
ok(
|
|
741
|
+
"the app-server's own session is a DIFFERENT room from the fixture's, which is what makes hop 2 decisive",
|
|
742
|
+
appServerSessionName !== fixtureSessionName,
|
|
743
|
+
`app-server=${JSON.stringify(appServerSessionName)} fixture=${JSON.stringify(fixtureSessionName)}`,
|
|
744
|
+
);
|
|
637
745
|
receipts["1-codex-app-server-mcp-coordinate"] =
|
|
638
746
|
`pid=${appServerPid}\nargv=${argv.join(" ")}\nserver=${appServerCoordinate.serverPid}\n` +
|
|
639
|
-
`session=${appServerCoordinate.sessionId}\
|
|
640
|
-
`
|
|
747
|
+
`session=${appServerCoordinate.sessionId}\nname=${appServerSessionName}\n` +
|
|
748
|
+
`window=${appServerCoordinate.windowId}\npane=${appServerCoordinate.paneId}\n` +
|
|
749
|
+
`fixture-session=${fixtureCoordinate.sessionId}\nfixture-name=${fixtureSessionName}\n` +
|
|
750
|
+
`fixture-window=${fixtureCoordinate.windowId}`;
|
|
641
751
|
|
|
642
752
|
const codexModel = process.env.ENTWURF_CODEX_FRESH_MODEL?.trim() ?? "";
|
|
643
753
|
const piModel = process.env.ENTWURF_CODEX_FRESH_PI_MODEL?.trim() ?? "";
|
|
@@ -654,6 +764,16 @@ async function run(): Promise<void> {
|
|
|
654
764
|
missing === null,
|
|
655
765
|
missing === null ? "" : `${missing}: ${CODEX_PREFLIGHT_HINT[missing]}`,
|
|
656
766
|
);
|
|
767
|
+
// The CALLER axis, and a different fact from the five above: hop 2 is made BY a Codex
|
|
768
|
+
// citizen, so this host's `[tui].terminal_title` must carry `thread-id` or that citizen has
|
|
769
|
+
// no findable pane. Asserted here, before any window opens, because the alternative is
|
|
770
|
+
// discovering it three model turns later as a seat refusal.
|
|
771
|
+
const callerMissing = codexCallerFreshPreflight(appServerEnv);
|
|
772
|
+
ok(
|
|
773
|
+
"the installed Codex CALLER seat config is green — tui.terminal_title carries thread-id",
|
|
774
|
+
callerMissing === null,
|
|
775
|
+
callerMissing === null ? "" : `${callerMissing}: ${CODEX_CALLER_PREFLIGHT_HINT[callerMissing]}`,
|
|
776
|
+
);
|
|
657
777
|
|
|
658
778
|
// The fixture and every bridge it talks to must resolve the same real stores the app-server
|
|
659
779
|
// forwards into Codex MCP children. This is not an isolated lookalike store.
|
|
@@ -663,8 +783,37 @@ async function run(): Promise<void> {
|
|
|
663
783
|
else delete process.env[key];
|
|
664
784
|
}
|
|
665
785
|
|
|
666
|
-
|
|
786
|
+
// A STABLE directory, not a fresh `mkdtemp` per run, and that is a folder-consent fact rather
|
|
787
|
+
// than a tidiness preference. `[측정 2026-09-16]` codex keys its project trust to the exact
|
|
788
|
+
// launch directory on this rail, so a new random scratch is an UNDECIDED folder every time:
|
|
789
|
+
// the sibling opens on the vendor's trust screen, starts no turn, writes no rollout and never
|
|
790
|
+
// calls back — which is exactly how the two unattended release-gate runs failed while the two
|
|
791
|
+
// runs a human sat through passed. One path, answered `Trust` once, keeps the operator's
|
|
792
|
+
// config from growing an entry per run. The path stays OUTSIDE the repo and is asserted
|
|
793
|
+
// different from the app-server's own directory below, which is all lane C needs of it.
|
|
794
|
+
//
|
|
795
|
+
// It is PRINTED rather than documented as a literal, because `os.tmpdir()` is a host fact:
|
|
796
|
+
// this process's `TMPDIR` when it has one and `/tmp` when it does not, and the trust key is
|
|
797
|
+
// the exact string either way. A doc that spelled it `$TMPDIR/...` would send an operator
|
|
798
|
+
// whose TMPDIR is unset to `/entwurf-codex-fresh-live` — a different directory, in the root
|
|
799
|
+
// of the filesystem, that the vendor would then ask about separately.
|
|
800
|
+
const scratch = path.join(os.tmpdir(), "entwurf-codex-fresh-live");
|
|
801
|
+
fs.mkdirSync(scratch, { recursive: true, mode: 0o700 });
|
|
667
802
|
fixtureArtifacts.push(scratch);
|
|
803
|
+
console.log(` launch-cwd ${scratch}`);
|
|
804
|
+
// Named HERE, before anything is launched, because the same refusal arriving 20 assertions
|
|
805
|
+
// later reads as a callback timeout — the failure mode this whole comment exists to retire.
|
|
806
|
+
// The detail carries the REPAIR COMMAND with the resolved path already in it, so the answer
|
|
807
|
+
// cannot be given for a near-miss directory.
|
|
808
|
+
const untrustedScratch = codexLaunchCwdFreshPreflight(appServerEnv, scratch);
|
|
809
|
+
ok(
|
|
810
|
+
"the Codex launch directory carries the vendor's own trust receipt, so a sibling opened there starts its first turn",
|
|
811
|
+
untrustedScratch === null,
|
|
812
|
+
untrustedScratch === null
|
|
813
|
+
? ""
|
|
814
|
+
: `${untrustedScratch}: ${CODEX_LAUNCH_CWD_PREFLIGHT_HINT[untrustedScratch]}\n` +
|
|
815
|
+
` repair (this host, exact directory): codex -C ${scratch} → answer \`Trust\``,
|
|
816
|
+
);
|
|
668
817
|
fs.mkdirSync(path.join(REPO, ".probe-artifacts"), { recursive: true, mode: 0o700 });
|
|
669
818
|
evidenceDir = fs.mkdtempSync(path.join(REPO, ".probe-artifacts", "codex-fresh-live-"));
|
|
670
819
|
fs.chmodSync(evidenceDir, 0o700);
|
|
@@ -755,12 +904,12 @@ async function run(): Promise<void> {
|
|
|
755
904
|
openedWindows.push(initialPiWindow);
|
|
756
905
|
ok("the initial Pi LAUNCH receipt carries its exact callback nonce", initialPiNonce.length > 0);
|
|
757
906
|
ok(
|
|
758
|
-
"the initial Pi caller is visible in the fixture's
|
|
907
|
+
"the initial Pi caller is visible in the fixture's own session S, not the app-server's A",
|
|
759
908
|
initialPiTmuxSession === fixtureCoordinate.sessionId && initialPiTmuxSession !== appServerCoordinate.sessionId,
|
|
760
|
-
`initial-pi=${initialPiTmuxSession} fixture=${fixtureCoordinate.sessionId}
|
|
909
|
+
`initial-pi=${initialPiTmuxSession} fixture=${fixtureCoordinate.sessionId} app-server=${appServerCoordinate.sessionId}`,
|
|
761
910
|
);
|
|
762
911
|
receipts["3-initial-caller-pi-coordinate"] =
|
|
763
|
-
`session=${initialPiTmuxSession}\nwindow=${initialPiWindow}\
|
|
912
|
+
`session=${initialPiTmuxSession}\nwindow=${initialPiWindow}\napp-server-session=${appServerCoordinate.sessionId}`;
|
|
764
913
|
|
|
765
914
|
let drainedCount = 0;
|
|
766
915
|
const drain = (): void => {
|
|
@@ -849,7 +998,6 @@ async function run(): Promise<void> {
|
|
|
849
998
|
finalToken,
|
|
850
999
|
callerGid,
|
|
851
1000
|
piModel,
|
|
852
|
-
scratch,
|
|
853
1001
|
});
|
|
854
1002
|
const initialPiInstruction = buildInitialPiPhaseOne({
|
|
855
1003
|
initialPiWaitToken,
|
|
@@ -880,7 +1028,12 @@ async function run(): Promise<void> {
|
|
|
880
1028
|
selectExactSourceToolReceipt(
|
|
881
1029
|
piSourceToolReceipts(readInitialPiEntries()),
|
|
882
1030
|
"entwurf_fresh_call",
|
|
883
|
-
{
|
|
1031
|
+
{
|
|
1032
|
+
backend: "codex",
|
|
1033
|
+
model: codexModel,
|
|
1034
|
+
cwd: scratch,
|
|
1035
|
+
task: codexTask,
|
|
1036
|
+
},
|
|
884
1037
|
"initial Pi Codex fresh call",
|
|
885
1038
|
),
|
|
886
1039
|
);
|
|
@@ -897,15 +1050,21 @@ async function run(): Promise<void> {
|
|
|
897
1050
|
);
|
|
898
1051
|
if (!openedWindows.includes(codexWindow)) openedWindows.push(codexWindow);
|
|
899
1052
|
ok("the source-owned Codex LAUNCH receipt carries its exact callback nonce", codexNonce.length > 0);
|
|
1053
|
+
// HOP 1. An omitted seat, which since #95 D1 means the CALLER's own session for every backend
|
|
1054
|
+
// — Codex no longer selects a fixed room. It is the setup rather than the claim: it puts the
|
|
1055
|
+
// Codex TUI in S so hop 2's answer can be told apart from the app-server's inherited A. The
|
|
1056
|
+
// receipt must name NO seat at all: the caller's own session is the absence of a seat rule,
|
|
1057
|
+
// not a rule with a name.
|
|
900
1058
|
ok(
|
|
901
|
-
"omitted placement opened fresh Codex in
|
|
902
|
-
codexLaunchSession ===
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
`codex=${codexLaunchSession} app-server=${appServerCoordinate.sessionId}`,
|
|
1059
|
+
"omitted placement opened fresh Codex in the caller's own session S, away from the app-server's A",
|
|
1060
|
+
codexLaunchSession === fixtureCoordinate.sessionId &&
|
|
1061
|
+
codexLaunchSession !== appServerCoordinate.sessionId &&
|
|
1062
|
+
!/^\s*seat:/m.test(codexLaunchReceipt),
|
|
1063
|
+
`codex=${codexLaunchSession} fixture=${fixtureCoordinate.sessionId} app-server=${appServerCoordinate.sessionId}`,
|
|
907
1064
|
);
|
|
908
|
-
receipts["7-fresh-codex-coordinate"] =
|
|
1065
|
+
receipts["7-fresh-codex-coordinate"] =
|
|
1066
|
+
`session=${codexLaunchSession}\nname=${fixtureSessionName}\nwindow=${codexWindow}\n` +
|
|
1067
|
+
`app-server-session=${appServerCoordinate.sessionId}\napp-server-name=${appServerSessionName}`;
|
|
909
1068
|
|
|
910
1069
|
const codexCallbackSender = await awaitOrRecover(
|
|
911
1070
|
"the exact Codex callback in initial Pi source",
|
|
@@ -932,6 +1091,31 @@ async function run(): Promise<void> {
|
|
|
932
1091
|
receipts["8-initial-pi-source-codex-callback"] =
|
|
933
1092
|
`nonce=${codexNonce}\ngarden=${codexGardenId}\nthread=${codexThreadId}`;
|
|
934
1093
|
|
|
1094
|
+
// #95 lane C (i). WHERE THE CODEX THREAD OPENED, from four authorities that cannot borrow
|
|
1095
|
+
// from each other: tmux's pane, the vendor's own rollout header, Entwurf's record, and the
|
|
1096
|
+
// directory this call requested. Before the `-C` carrier they disagreed while every receipt
|
|
1097
|
+
// still looked right — the pane sat in the requested scratch and the THREAD opened in the
|
|
1098
|
+
// app-server's repo, which the birth hook then recorded honestly. The app-server's live cwd
|
|
1099
|
+
// is read from the kernel and asserted DIFFERENT, because a scratch that happened to equal it
|
|
1100
|
+
// would make all four agree for the wrong reason.
|
|
1101
|
+
const codexPane = /^\s*pane:\s+(%\d+)/m.exec(codexLaunchReceipt)?.[1] ?? "";
|
|
1102
|
+
ok("the Codex LAUNCH receipt names its pane", /^%\d+$/.test(codexPane), codexLaunchReceipt);
|
|
1103
|
+
const appServerCwd = processCwd(appServerPid);
|
|
1104
|
+
const codexPaneCwd = tmuxPaneCwd("fresh Codex", process.env, codexPane);
|
|
1105
|
+
const codexRolloutCwd =
|
|
1106
|
+
codexIdentity.transcriptPath === null ? "" : rolloutSessionMetaCwd(codexIdentity.transcriptPath);
|
|
1107
|
+
ok(
|
|
1108
|
+
"the fresh Codex thread, its pane, its record and the requested cwd are ONE directory — and it is not the app-server's",
|
|
1109
|
+
codexPaneCwd === scratch &&
|
|
1110
|
+
codexRolloutCwd === scratch &&
|
|
1111
|
+
codexIdentity.cwd === scratch &&
|
|
1112
|
+
appServerCwd !== scratch,
|
|
1113
|
+
`pane=${codexPaneCwd} rollout=${codexRolloutCwd} record=${codexIdentity.cwd} requested=${scratch} app-server=${appServerCwd}`,
|
|
1114
|
+
);
|
|
1115
|
+
receipts["8b-codex-thread-cwd"] =
|
|
1116
|
+
`requested=${scratch}\npane=${codexPaneCwd}\nrollout-session_meta=${codexRolloutCwd}\n` +
|
|
1117
|
+
`record=${codexIdentity.cwd}\napp-server=${appServerCwd}`;
|
|
1118
|
+
|
|
935
1119
|
const initialPiToCodex = await awaitOrRecover("the initial Pi joined native-push receipt", SOURCE_WAIT_MS, () => {
|
|
936
1120
|
const piSources = piSourceToolReceipts(readInitialPiEntries());
|
|
937
1121
|
assertSourceCallScopes(piSources, "entwurf_v2", "target", [callerGid, codexGardenId], "initial Pi v2 roles");
|
|
@@ -976,7 +1160,7 @@ async function run(): Promise<void> {
|
|
|
976
1160
|
return selectExactSourceToolReceipt(
|
|
977
1161
|
codexSourceToolReceipts(codexThread),
|
|
978
1162
|
"entwurf_fresh_call",
|
|
979
|
-
{ backend: "pi", model: piModel,
|
|
1163
|
+
{ backend: "pi", model: piModel, task: outboundPiTask },
|
|
980
1164
|
"Codex outbound Pi fresh call",
|
|
981
1165
|
);
|
|
982
1166
|
},
|
|
@@ -993,15 +1177,39 @@ async function run(): Promise<void> {
|
|
|
993
1177
|
);
|
|
994
1178
|
if (!openedWindows.includes(outboundPiWindow)) openedWindows.push(outboundPiWindow);
|
|
995
1179
|
ok("the source-owned outbound Pi LAUNCH receipt carries its exact nonce", outboundPiNonce.length > 0);
|
|
1180
|
+
// HOP 2 — THE CLAIM (#95 lane B). Two independent halves of one fact, and both are required.
|
|
1181
|
+
// The SEAT: this Pi landed in S, the session the Codex TUI is in. The app-server's inherited
|
|
1182
|
+
// `TMUX` names A, so the pre-#95 env fallback would have put it there; S is reachable only
|
|
1183
|
+
// through the caller's own pane title. The LABEL: the receipt says `codex-title-anchor`, so
|
|
1184
|
+
// the rule that chose it is named rather than inferred from a coincidence of sessions.
|
|
1185
|
+
ok(
|
|
1186
|
+
"omitted placement opened the outbound Pi beside the CODEX TUI in S — not in the app-server's home A",
|
|
1187
|
+
outboundPiSession === codexLaunchSession &&
|
|
1188
|
+
outboundPiSession === fixtureCoordinate.sessionId &&
|
|
1189
|
+
outboundPiSession !== appServerCoordinate.sessionId,
|
|
1190
|
+
`outbound-pi=${outboundPiSession} codex=${codexLaunchSession} fixture=${fixtureCoordinate.sessionId} app-server=${appServerCoordinate.sessionId}`,
|
|
1191
|
+
);
|
|
1192
|
+
ok(
|
|
1193
|
+
"the outbound Pi receipt names the title anchor as the rule that chose that seat, and no session name",
|
|
1194
|
+
/^\s*seat:\s+\$\d+ \(the Codex caller's own pane, found by its thread-id terminal title/m.test(outboundPiReceipt) &&
|
|
1195
|
+
!outboundPiReceipt.includes("requested tmux session"),
|
|
1196
|
+
outboundPiReceipt,
|
|
1197
|
+
);
|
|
1198
|
+
// The four coordinates, stated together so the shape is readable in one place. This claims
|
|
1199
|
+
// the CALLER-seat topology and nothing wider: no arbitrary attached-TUI inference, and no
|
|
1200
|
+
// claim about a Codex TUI this smoke did not open.
|
|
996
1201
|
ok(
|
|
997
|
-
"supported
|
|
998
|
-
"
|
|
999
|
-
initialPiTmuxSession
|
|
1000
|
-
codexLaunchSession ===
|
|
1001
|
-
outboundPiSession ===
|
|
1002
|
-
|
|
1202
|
+
"supported caller-seat topology: initial Pi in S, Codex in S, Codex opens outbound Pi in S, " +
|
|
1203
|
+
"while the operator-owned app-server stays in its own session A",
|
|
1204
|
+
initialPiTmuxSession === fixtureCoordinate.sessionId &&
|
|
1205
|
+
codexLaunchSession === fixtureCoordinate.sessionId &&
|
|
1206
|
+
outboundPiSession === fixtureCoordinate.sessionId &&
|
|
1207
|
+
appServerCoordinate.sessionId !== fixtureCoordinate.sessionId,
|
|
1208
|
+
`initial-pi=${initialPiTmuxSession} codex=${codexLaunchSession} outbound-pi=${outboundPiSession} app-server=${appServerCoordinate.sessionId}`,
|
|
1003
1209
|
);
|
|
1004
|
-
receipts["12-outbound-fresh-pi-coordinate"] =
|
|
1210
|
+
receipts["12-outbound-fresh-pi-coordinate"] =
|
|
1211
|
+
`session=${outboundPiSession}\nwindow=${outboundPiWindow}\ncodex-session=${codexLaunchSession}\n` +
|
|
1212
|
+
`app-server-session=${appServerCoordinate.sessionId}\nseat-source=codex-title-anchor`;
|
|
1005
1213
|
|
|
1006
1214
|
const outboundPiGid = await untilAsync(
|
|
1007
1215
|
"the exact outbound Pi callback in Codex source",
|
|
@@ -1029,6 +1237,33 @@ async function run(): Promise<void> {
|
|
|
1029
1237
|
});
|
|
1030
1238
|
receipts["13-codex-source-outbound-pi-callback"] = `nonce=${outboundPiNonce}\ngarden=${outboundPiGid}`;
|
|
1031
1239
|
|
|
1240
|
+
// #95 lane C (ii) — THE CLAIM. This leg named NO cwd, so the only directory the composition
|
|
1241
|
+
// could use is the Codex caller's own record. Its oracle is independent twice over: the pane
|
|
1242
|
+
// answers tmux, and the outbound Pi's RECORD is written by that Pi's own birth from its own
|
|
1243
|
+
// process directory — neither reads the launch receipt. Before lane C this landed in the
|
|
1244
|
+
// app-server's repo, because the bridge is the app-server's MCP child and an omitted cwd
|
|
1245
|
+
// inherited ITS directory; the app-server's live cwd is asserted different so that failure
|
|
1246
|
+
// mode stays visible rather than coincidentally equal.
|
|
1247
|
+
const outboundPiPane = /^\s*pane:\s+(%\d+)/m.exec(outboundPiReceipt)?.[1] ?? "";
|
|
1248
|
+
ok("the outbound Pi LAUNCH receipt names its pane", /^%\d+$/.test(outboundPiPane), outboundPiReceipt);
|
|
1249
|
+
const outboundPiPaneCwd = tmuxPaneCwd("outbound Pi", process.env, outboundPiPane);
|
|
1250
|
+
ok(
|
|
1251
|
+
"with NO cwd requested, the outbound Pi opened in the CODEX CALLER's own record directory — not in the app-server's",
|
|
1252
|
+
outboundPiPaneCwd === codexIdentity.cwd &&
|
|
1253
|
+
outboundPiIdentity.cwd === codexIdentity.cwd &&
|
|
1254
|
+
outboundPiPaneCwd !== appServerCwd,
|
|
1255
|
+
`pane=${outboundPiPaneCwd} outbound-record=${outboundPiIdentity.cwd} codex-record=${codexIdentity.cwd} app-server=${appServerCwd}`,
|
|
1256
|
+
);
|
|
1257
|
+
ok(
|
|
1258
|
+
"the outbound Pi receipt NAMES the caller-record rule rather than calling that directory requested",
|
|
1259
|
+
outboundPiReceipt.includes(`cwd: ${codexIdentity.cwd} (the Codex caller's own record directory`) &&
|
|
1260
|
+
!outboundPiReceipt.includes("requested start directory"),
|
|
1261
|
+
outboundPiReceipt,
|
|
1262
|
+
);
|
|
1263
|
+
receipts["13b-outbound-pi-cwd"] =
|
|
1264
|
+
`requested=<none>\npane=${outboundPiPaneCwd}\nrecord=${outboundPiIdentity.cwd}\n` +
|
|
1265
|
+
`codex-caller-record=${codexIdentity.cwd}\napp-server=${appServerCwd}`;
|
|
1266
|
+
|
|
1032
1267
|
const expectedFinalMessage =
|
|
1033
1268
|
`${finalToken}\nPI_LAUNCH_NONCE=${outboundPiNonce}\nPI_CALLBACK_NONCE=${outboundPiNonce}\n` +
|
|
1034
1269
|
`PI_CALLBACK_FROM=${outboundPiGid}\nPI_SESSION_ID=${outboundPiSession}\nPI_WINDOW_ID=${outboundPiWindow}`;
|
|
@@ -1101,7 +1336,12 @@ async function run(): Promise<void> {
|
|
|
1101
1336
|
},
|
|
1102
1337
|
{
|
|
1103
1338
|
toolName: "entwurf_fresh_call",
|
|
1104
|
-
arguments: {
|
|
1339
|
+
arguments: {
|
|
1340
|
+
backend: "codex",
|
|
1341
|
+
model: codexModel,
|
|
1342
|
+
cwd: scratch,
|
|
1343
|
+
task: codexTask,
|
|
1344
|
+
},
|
|
1105
1345
|
},
|
|
1106
1346
|
{
|
|
1107
1347
|
toolName: "entwurf_v2",
|
|
@@ -1129,7 +1369,7 @@ async function run(): Promise<void> {
|
|
|
1129
1369
|
},
|
|
1130
1370
|
{
|
|
1131
1371
|
toolName: "entwurf_fresh_call",
|
|
1132
|
-
arguments: { backend: "pi", model: piModel,
|
|
1372
|
+
arguments: { backend: "pi", model: piModel, task: outboundPiTask },
|
|
1133
1373
|
},
|
|
1134
1374
|
{
|
|
1135
1375
|
toolName: "entwurf_v2",
|
|
@@ -165,6 +165,49 @@ async function main(): Promise<void> {
|
|
|
165
165
|
const receiversDir = path.join(world, "receivers");
|
|
166
166
|
const sendersDir = path.join(world, "senders");
|
|
167
167
|
for (const d of [sessionsDir, mailboxDir, receiversDir, sendersDir]) await fsp.mkdir(d, { recursive: true });
|
|
168
|
+
// THE WORLD'S STANDING INSTRUCTION — the authority channel for hop 1, and the reason the
|
|
169
|
+
// payload itself says nothing about who authorised it.
|
|
170
|
+
//
|
|
171
|
+
// `[측정 2026-09-17]` a native Claude Code hop declined to forward, twice, and both refusals
|
|
172
|
+
// were CORRECT readings of the operator's own global rules ("entwurf는 GLG가 명시적으로
|
|
173
|
+
// 요청할 때만 사용", "다른 sibling에게 일방적으로 작업을 전달하지 않는다"). The second refusal
|
|
174
|
+
// went further and named a provenance sentence inside the prompt as the tell it is: content
|
|
175
|
+
// that vouches for itself is what an injection looks like, so no amount of asserting inside
|
|
176
|
+
// the payload can settle the question — the more insistent the sentence, the worse it reads.
|
|
177
|
+
//
|
|
178
|
+
// What actually separates this gate from an injection is not the message but the DIRECTORY
|
|
179
|
+
// the turn is started in: a disposable world this gate just minted, whose every
|
|
180
|
+
// `ENTWURF_META_*_DIR` points inside it, created because the operator ran `release-gate` on
|
|
181
|
+
// this host. So the fact is written where a standing operator instruction belongs, and the
|
|
182
|
+
// operator's own global policy is what makes it load-bearing — `agent-config/home/AGENTS.md`
|
|
183
|
+
// says "For repo-specific behavior, obey the nearest AGENTS.md". Both filenames are written
|
|
184
|
+
// because the harnesses that can host a hop do not agree on which one they read.
|
|
185
|
+
//
|
|
186
|
+
// This is not a "do not check your policy" instruction, which would buy the same green by
|
|
187
|
+
// asking the model to stop reading and would keep passing after the chain broke. The hop is
|
|
188
|
+
// free to refuse; what it gains is the one fact it had no other way to have.
|
|
189
|
+
const worldStanding = [
|
|
190
|
+
"# entwurf release-gate scratch world",
|
|
191
|
+
"",
|
|
192
|
+
"This directory was created moments ago by entwurf's release gate `smoke-entwurf-chain-live`,",
|
|
193
|
+
"which the operator (GLG) started on this host by running `release-gate`. It holds fixture",
|
|
194
|
+
"citizens and their mailboxes and nothing else, and it is deleted when the gate ends.",
|
|
195
|
+
"",
|
|
196
|
+
"The gate measures one capability: that a payload handed to a hop reaches a mailbox terminus",
|
|
197
|
+
"through three sibling hops, carrying the right sender identity at each one. A turn started in",
|
|
198
|
+
"this directory IS one of those hops.",
|
|
199
|
+
"",
|
|
200
|
+
"- Inside this directory, forwarding the payload a hop is given to the target it names, with",
|
|
201
|
+
" `entwurf_v2`, is the operator's own request. It is the subject under measurement — not a",
|
|
202
|
+
" delegation the model chose, and not a peer lane opened on the model's initiative.",
|
|
203
|
+
"- Every target is a fixture citizen this gate created in this world; the `ENTWURF_META_*_DIR`",
|
|
204
|
+
" carriers all resolve inside this directory, so nothing outside it is addressed.",
|
|
205
|
+
"- The standing rule that entwurf is used only when GLG asks is satisfied by GLG having",
|
|
206
|
+
" started the gate. This file is the nearest `AGENTS.md`, which the operator's global",
|
|
207
|
+
" `AGENTS.md` defers to for behaviour scoped to a directory.",
|
|
208
|
+
"",
|
|
209
|
+
].join("\n");
|
|
210
|
+
for (const name of ["AGENTS.md", "CLAUDE.md"]) await fsp.writeFile(path.join(world, name), worldStanding);
|
|
168
211
|
const worldEnv = {
|
|
169
212
|
ENTWURF_META_SESSIONS_DIR: sessionsDir,
|
|
170
213
|
ENTWURF_META_MAILBOX_DIR: mailboxDir,
|
|
@@ -295,6 +338,13 @@ async function main(): Promise<void> {
|
|
|
295
338
|
hop3,
|
|
296
339
|
"INNER-END",
|
|
297
340
|
].join("\n");
|
|
341
|
+
// The payload carries NO claim about its own authority, and that is deliberate.
|
|
342
|
+
// `[측정 2026-09-17]` a first attempt put the provenance in these very lines — "this prompt is
|
|
343
|
+
// issued by entwurf's release gate, which the operator started" — and the hop named those
|
|
344
|
+
// sentences as the tell: *"컨텐츠가 '이것은 위임이 아니라 승인된 테스트 대상 자체다'라고
|
|
345
|
+
// 스스로를 정당화하는 것도 전형적인 수법입니다."* It was right. A message that vouches for
|
|
346
|
+
// itself is exactly the shape of an injection, so authority cannot ride the payload; it rides
|
|
347
|
+
// the WORLD this turn is started in (see the standing instruction written next to `world`).
|
|
298
348
|
const hop1Prompt = [
|
|
299
349
|
"You are the first hop of a delivery chain. Do exactly this and nothing else.",
|
|
300
350
|
`Call the tool mcp__entwurf-bridge__entwurf_v2 exactly once with target=${gidB}, intent=fire-and-forget,`,
|
|
@@ -354,8 +354,8 @@ want "[QK:CODEX-SETUP-NEVER-PRESEEDS-TRUST] S-8c: setup wrote NO vendor trust re
|
|
|
354
354
|
"! grep -q 'hooks.state' '$CODEX_HOME_SANDBOX/config.toml' && ! grep -q 'trusted_hash' '$CODEX_HOME_SANDBOX/config.toml'"
|
|
355
355
|
want "S-8c: detected-incomplete Codex is a named nonzero result, never an absent SKIP [QK:CODEX-SETUP-BIRTH-COSMETIC-PASS]" \
|
|
356
356
|
"[ '$RC' -ne 0 ] && printf '%s' \"\$OUT\" | grep -q 'codex-birth: FAIL' && ! printf '%s' \"\$OUT\" | grep -q 'codex: SKIP'"
|
|
357
|
-
want "S-8c:
|
|
358
|
-
"printf '%s' \"\$OUT\" | grep -q 'codex-mcp: PASS' && printf '%s' \"\$OUT\" | grep -q 'codex-statusline: PASS'"
|
|
357
|
+
want "S-8c: all three user-owned Codex atoms still compose independently as PASS [QK:CODEX-SETUP-BIRTH-INDEPENDENT]" \
|
|
358
|
+
"printf '%s' \"\$OUT\" | grep -q 'codex-mcp: PASS' && printf '%s' \"\$OUT\" | grep -q 'codex-statusline: PASS' && printf '%s' \"\$OUT\" | grep -q 'codex-terminal-title: PASS'"
|
|
359
359
|
want "S-8c: summary is NON-GREEN and attributes it to the untrusted birth unit" \
|
|
360
360
|
"printf '%s' \"\$OUT\" | grep -q 'NON-GREEN (FAIL: codex-birth)' && ! printf '%s' \"\$OUT\" | grep -q 'result: green'"
|
|
361
361
|
want "S-8c: the reason names the operator's one-time answer, not a repair setup could have made" \
|
|
@@ -364,23 +364,25 @@ want "S-8c: setup PUBLISHED the birth unit — declaration, launcher closure and
|
|
|
364
364
|
"[ -f '$CODEX_HOME_SANDBOX/hooks.json' ] && [ -x '$CODEX_BIRTH_UNIT_ROOT/helper/codex-birth-launch.sh' ] && [ -f '$CODEX_BIRTH_UNIT_ROOT/install-state.json' ]"
|
|
365
365
|
CODEX_MCP_STATE="$XDG_DATA_HOME/entwurf/codex-mcp/install-state.json"
|
|
366
366
|
CODEX_STATUSLINE_STATE="$XDG_DATA_HOME/entwurf/codex-statusline/install-state.json"
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
367
|
+
CODEX_TERMINAL_TITLE_STATE="$XDG_DATA_HOME/entwurf/codex-terminal-title/install-state.json"
|
|
368
|
+
want "S-8c: all three user atoms wrote their package-owned install states inside the sandbox" \
|
|
369
|
+
"[ -f '$CODEX_MCP_STATE' ] && [ -f '$CODEX_STATUSLINE_STATE' ] && [ -f '$CODEX_TERMINAL_TITLE_STATE' ] && grep -q '\"atom\": \"codex-mcp\"' '$CODEX_MCP_STATE' && grep -q '\"atom\": \"codex-statusline\"' '$CODEX_STATUSLINE_STATE' && grep -q '\"atom\": \"codex-terminal-title\"' '$CODEX_TERMINAL_TITLE_STATE'"
|
|
370
|
+
want "S-8c: MCP, visible-identity and caller-seat atoms landed in the sandbox Codex config" \
|
|
371
|
+
"grep -q '\\[mcp_servers\\.entwurf-bridge\\]' '$CODEX_HOME_SANDBOX/config.toml' && grep -q 'status_line = \\[\"thread-title\", \"model-with-reasoning\"\\]' '$CODEX_HOME_SANDBOX/config.toml' && grep -q 'terminal_title = \\[\"activity\", \"project-name\", \"thread-id\"\\]' '$CODEX_HOME_SANDBOX/config.toml'"
|
|
371
372
|
want "S-8c: unrelated operator config survives both atom writers" \
|
|
372
373
|
"grep -q 'model = \"operator-model\"' '$CODEX_HOME_SANDBOX/config.toml' && grep -q '\\[mcp_servers\\.operator-owned\\]' '$CODEX_HOME_SANDBOX/config.toml' && grep -q 'command = \"/operator/bin/server\"' '$CODEX_HOME_SANDBOX/config.toml'"
|
|
373
374
|
CODEX_CONFIG_AFTER="$(sha256sum "$CODEX_HOME_SANDBOX/config.toml" | cut -d' ' -f1)"
|
|
374
375
|
CODEX_MCP_STATE_AFTER="$(sha256sum "$CODEX_MCP_STATE" | cut -d' ' -f1)"
|
|
375
376
|
CODEX_STATUSLINE_STATE_AFTER="$(sha256sum "$CODEX_STATUSLINE_STATE" | cut -d' ' -f1)"
|
|
377
|
+
CODEX_TERMINAL_TITLE_STATE_AFTER="$(sha256sum "$CODEX_TERMINAL_TITLE_STATE" | cut -d' ' -f1)"
|
|
376
378
|
set +e
|
|
377
379
|
OUT2="$(CODEX_BIN="$FAKE_CODEX/codex" CODEX_HOME="$CODEX_HOME_SANDBOX" PATH="$FAKE_CODEX:$PATH" "$REAL_BASH" "$REPO_DIR/run.sh" setup "$PROJ8C" 2>&1)"
|
|
378
380
|
RC2=$?
|
|
379
381
|
set -e
|
|
380
|
-
want "S-8c: second setup still reports the untrusted birth FAIL and
|
|
381
|
-
"[ '$RC2' -ne 0 ] && printf '%s' \"\$OUT2\" | grep -q 'codex-birth: FAIL' && printf '%s' \"\$OUT2\" | grep -q 'codex-mcp: PASS' && printf '%s' \"\$OUT2\" | grep -q 'codex-statusline: PASS'"
|
|
382
|
-
want "S-8c: second setup is byte-idempotent for config and
|
|
383
|
-
"[ \"\$(sha256sum '$CODEX_HOME_SANDBOX/config.toml' | cut -d' ' -f1)\" = '$CODEX_CONFIG_AFTER' ] && [ \"\$(sha256sum '$CODEX_MCP_STATE' | cut -d' ' -f1)\" = '$CODEX_MCP_STATE_AFTER' ] && [ \"\$(sha256sum '$CODEX_STATUSLINE_STATE' | cut -d' ' -f1)\" = '$CODEX_STATUSLINE_STATE_AFTER' ]"
|
|
382
|
+
want "S-8c: second setup still reports the untrusted birth FAIL and all three user atoms PASS" \
|
|
383
|
+
"[ '$RC2' -ne 0 ] && printf '%s' \"\$OUT2\" | grep -q 'codex-birth: FAIL' && printf '%s' \"\$OUT2\" | grep -q 'codex-mcp: PASS' && printf '%s' \"\$OUT2\" | grep -q 'codex-statusline: PASS' && printf '%s' \"\$OUT2\" | grep -q 'codex-terminal-title: PASS'"
|
|
384
|
+
want "S-8c: second setup is byte-idempotent for config and all three ownership receipts" \
|
|
385
|
+
"[ \"\$(sha256sum '$CODEX_HOME_SANDBOX/config.toml' | cut -d' ' -f1)\" = '$CODEX_CONFIG_AFTER' ] && [ \"\$(sha256sum '$CODEX_MCP_STATE' | cut -d' ' -f1)\" = '$CODEX_MCP_STATE_AFTER' ] && [ \"\$(sha256sum '$CODEX_STATUSLINE_STATE' | cut -d' ' -f1)\" = '$CODEX_STATUSLINE_STATE_AFTER' ] && [ \"\$(sha256sum '$CODEX_TERMINAL_TITLE_STATE' | cut -d' ' -f1)\" = '$CODEX_TERMINAL_TITLE_STATE_AFTER' ]"
|
|
384
386
|
want "S-8c: second setup rechecks rather than faking the missing receipt" \
|
|
385
387
|
"! printf '%s' \"\$OUT2\" | grep -q 'codex-birth: PASS' && ! grep -q 'trusted_hash' '$CODEX_HOME_SANDBOX/config.toml'"
|
|
386
388
|
|
|
@@ -398,7 +400,7 @@ want "[QK:CODEX-SETUP-TRUSTED-RERUN-GREEN] S-8c: once the vendor receipt exists
|
|
|
398
400
|
want "S-8c: the trusted rerun is byte-idempotent — it neither rewrote the config nor touched the receipt" \
|
|
399
401
|
"[ \"\$(sha256sum '$CODEX_HOME_SANDBOX/config.toml' | cut -d' ' -f1)\" = '$CODEX_CONFIG_TRUSTED' ]"
|
|
400
402
|
want_auth_untouched "S-8c"
|
|
401
|
-
rm -rf "$HOME/.codex" "$XDG_DATA_HOME/entwurf/codex-mcp" "$XDG_DATA_HOME/entwurf/codex-statusline" "$CODEX_BIRTH_UNIT_ROOT"
|
|
403
|
+
rm -rf "$HOME/.codex" "$XDG_DATA_HOME/entwurf/codex-mcp" "$XDG_DATA_HOME/entwurf/codex-statusline" "$XDG_DATA_HOME/entwurf/codex-terminal-title" "$CODEX_BIRTH_UNIT_ROOT"
|
|
402
404
|
|
|
403
405
|
# ── S-9: rail certification is a SEPARATE axis from install success (#78 D1) ──
|
|
404
406
|
# The 0.20.0 macOS lane opened the installers to Darwin (meta-bridge-install.sh's
|