@junghanacs/entwurf 0.20.1 → 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.
Files changed (111) hide show
  1. package/AGENTS.md +85 -190
  2. package/BASELINE.md +6 -3
  3. package/CHANGELOG.md +473 -14
  4. package/CONTRIBUTING.md +1 -1
  5. package/DELIVERY.md +332 -60
  6. package/README.md +100 -22
  7. package/VERIFY.md +93 -7
  8. package/docs/acp-backend-rail.md +0 -1
  9. package/docs/external-mcp-host.md +64 -33
  10. package/docs/setup-clean-host.md +151 -17
  11. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +130 -114
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-caller-seat.js +174 -0
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +519 -0
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +22 -2
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +3 -1
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +6 -7
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-native-push.js +30 -17
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +7 -1
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +3 -2
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +8 -4
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +7 -4
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-mailbox-body.js +20 -11
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +227 -1
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +189 -19
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/adapter.js +10 -7
  26. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/codex-ws-client.js +403 -0
  27. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/register.js +4 -4
  28. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook-codex.js +325 -0
  29. package/mcp/entwurf-bridge/dist/scripts/codex-socket-path.js +30 -0
  30. package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +6 -1
  31. package/mcp/entwurf-bridge/src/index.ts +154 -116
  32. package/mcp/entwurf-bridge/tsconfig.build.json +10 -0
  33. package/package.json +13 -4
  34. package/pi-extensions/entwurf-control.ts +71 -19
  35. package/pi-extensions/lib/codex-caller-seat.ts +204 -0
  36. package/pi-extensions/lib/codex-fresh-preflight.ts +580 -0
  37. package/pi-extensions/lib/compaction-send-guard.ts +80 -0
  38. package/pi-extensions/lib/entwurf-fact-provider.ts +29 -3
  39. package/pi-extensions/lib/entwurf-peers-render.ts +4 -1
  40. package/pi-extensions/lib/entwurf-v2-contract.ts +6 -7
  41. package/pi-extensions/lib/entwurf-v2-native-push.ts +35 -18
  42. package/pi-extensions/lib/entwurf-v2-production.ts +10 -3
  43. package/pi-extensions/lib/entwurf-v2-runner.ts +3 -2
  44. package/pi-extensions/lib/entwurf-v2-send.ts +16 -11
  45. package/pi-extensions/lib/entwurf-v2-surface.ts +7 -4
  46. package/pi-extensions/lib/meta-mailbox-body.ts +22 -13
  47. package/pi-extensions/lib/meta-sender-identity.ts +305 -0
  48. package/pi-extensions/lib/mux-fresh-call.ts +233 -29
  49. package/pi-extensions/lib/native-push/adapter.ts +21 -24
  50. package/pi-extensions/lib/native-push/codex-ws-client.ts +506 -0
  51. package/pi-extensions/lib/native-push/register.ts +7 -9
  52. package/pi-extensions/meta-bridge-hook-codex.ts +371 -0
  53. package/run.sh +342 -28
  54. package/scripts/check-agy-sender-identity.ts +1 -1
  55. package/scripts/check-codex-app-server-launch.ts +445 -0
  56. package/scripts/check-codex-birth-hook.ts +264 -0
  57. package/scripts/check-codex-bridge-identity.ts +179 -0
  58. package/scripts/check-codex-native-push.ts +386 -0
  59. package/scripts/check-codex-sender-identity.ts +495 -0
  60. package/scripts/check-compaction-send-guard.ts +130 -0
  61. package/scripts/check-copilot-receive-arm.ts +4 -1
  62. package/scripts/check-entwurf-fact-provider.ts +38 -0
  63. package/scripts/check-entwurf-peers-surface.ts +13 -1
  64. package/scripts/check-entwurf-self-address.ts +15 -16
  65. package/scripts/check-entwurf-v2-contract.ts +4 -3
  66. package/scripts/check-entwurf-v2-decider.ts +7 -5
  67. package/scripts/check-entwurf-v2-native-push.ts +35 -7
  68. package/scripts/check-entwurf-v2-production.ts +245 -12
  69. package/scripts/check-entwurf-v2-runner.ts +1 -1
  70. package/scripts/check-entwurf-v2-send.ts +26 -7
  71. package/scripts/check-entwurf-v2-surface.ts +1 -1
  72. package/scripts/check-gate-qualification.ts +8 -3
  73. package/scripts/check-harness-admission-parity.ts +0 -1
  74. package/scripts/check-mux-launch-tmux.ts +345 -4
  75. package/scripts/check-native-push-adapter.ts +20 -16
  76. package/scripts/check-native-push-register.ts +5 -1
  77. package/scripts/check-release-gate-outcomes.ts +47 -1
  78. package/scripts/check-setup-qualification.sh +3 -1
  79. package/scripts/codex-app-server-launch.sh +275 -0
  80. package/scripts/codex-birth-doctor.sh +276 -0
  81. package/scripts/codex-birth-install.sh +414 -0
  82. package/scripts/codex-birth-uninstall.sh +170 -0
  83. package/scripts/codex-mcp-config.py +435 -0
  84. package/scripts/codex-socket-path.ts +33 -0
  85. package/scripts/codex-statusline-config.py +434 -0
  86. package/scripts/codex-terminal-title-config.py +500 -0
  87. package/scripts/codex_toml_io.py +653 -0
  88. package/scripts/lib/codex-fresh-live-protocol.ts +121 -0
  89. package/scripts/lib/codex-fresh-source-receipts.ts +426 -0
  90. package/scripts/lib/launch-receipt-windows.ts +46 -0
  91. package/scripts/lib/tmux-coordinate-row.ts +58 -0
  92. package/scripts/meta-bridge-fresh-cut.ts +6 -1
  93. package/scripts/mutants/codex-app-server-launch.json +157 -0
  94. package/scripts/mutants/codex-caller-seat.json +336 -0
  95. package/scripts/mutants/codex-native.json +838 -0
  96. package/scripts/mutants/compaction-send-guard.json +103 -0
  97. package/scripts/mutants/entwurf-peers.json +19 -0
  98. package/scripts/mutants/mux-fresh-call.json +93 -10
  99. package/scripts/mutants/omp-fresh.json +6 -4
  100. package/scripts/mutants/release-gate.json +13 -0
  101. package/scripts/mutants/v2-surface.json +75 -1
  102. package/scripts/raw-async-delivery/README.md +2 -1
  103. package/scripts/raw-codex-measure/README.md +114 -46
  104. package/scripts/smoke-agy-native-push-live.ts +3 -1
  105. package/scripts/smoke-codex-birth.sh +347 -0
  106. package/scripts/smoke-codex-config-state.sh +700 -0
  107. package/scripts/smoke-codex-fresh-live.ts +1426 -0
  108. package/scripts/smoke-codex-native-push-live.ts +75 -0
  109. package/scripts/smoke-entwurf-chain-live.ts +50 -0
  110. package/scripts/smoke-setup-verdict.sh +125 -10
  111. package/scripts/tsconfig.json +1 -0
@@ -4,10 +4,10 @@ Lane: `docs/adding-a-harness.md` step 1 (six measurements) + §3.5 citizen scope
4
4
  tool-name dialect + step 6's join. The oracle is the vendor artifact or the vendor process,
5
5
  never our assembler.
6
6
 
7
- This file is a MEASUREMENT-ONLY ledger. Nothing here was written by an entwurf installer,
8
- no meta-record was minted, and no operator config was edited. The one entwurf-owned artifact
9
- touched at all is the pre-existing `[mcp_servers.entwurf-bridge]` entry in the operator's own
10
- `~/.codex/config.toml`, which was READ, not written.
7
+ This file is a MEASUREMENT-ONLY ledger of the **2026-09-08 step-1 state**. At that
8
+ measurement date, nothing here had been written by an Entwurf Codex installer, no
9
+ Codex meta-record had been minted, and no operator config had been edited. Later #95
10
+ candidate work exists; this dated ledger is evidence for it, not current product status.
11
11
 
12
12
  Evidence-state vocabulary: **[source]** = read at `file:line` in the vendor checkout;
13
13
  **[host]** = measured on this host (`oracle`, 2026-09-08 KST) with the receipt named;
@@ -207,11 +207,11 @@ it carried is what S1b-B was run to settle.)*
207
207
  (`thread/queue/add|start|list|…`) that did not exist at 0.136. No hook or extension wake
208
208
  surface was added (see M1). The measured route is unchanged and green: **M-B** below.
209
209
 
210
- ## M5 — Parent process topology (the step-6 join)
210
+ ## M5 — Parent process topology (measured refutation of a universal join)
211
211
 
212
- The join `docs/adding-a-harness.md` step 6 requires is `hook.ppid == mcp.ppid == the harness
213
- host pid`. **It holds on Codex but WHICH process is the host depends on the launch mode, and
214
- that difference is the finding.**
212
+ The then-current `docs/adding-a-harness.md` step 6 asked whether
213
+ `hook.ppid == mcp.ppid == harness host pid`. The measurement below preserves the
214
+ answer and explains why that equality is not a universal sender-identity contract.
215
215
 
216
216
  **Embedded mode** (a standalone `codex` TUI with its own `CODEX_HOME`, no app-server):
217
217
 
@@ -240,23 +240,20 @@ pid=1733 (entwurf-bridge) ppid=1393 # 1393 = codex app-server --listen
240
240
  pid=1616 # = the plain codex TUI. Not the parent.
241
241
  ```
242
242
 
243
- So in the delivery-capable mode the join still holds — `hook.ppid == mcp.ppid` but both
244
- resolve to the app-server.
243
+ In the delivery-capable mode, `hook.ppid == mcp.ppid`, but both resolve to the
244
+ app-server rather than to an attached TUI.
245
245
 
246
- **That the app-server is SHARED across citizens was an inference in the first draft, and it
247
- was published without a label.** `[source]` supports it
248
- (`app-server/src/request_processors/initialize_processor.rs:63-68` is a multi-client
249
- shared-thread surface), but the run above shows the parent of ONE attached TUI; reading N from
250
- one is not a measurement. Independent audit (terra, 2026-09-08) caught the missing label.
251
- **It has since been measured directly — see S1b-C below — and the answer is the one the
252
- inference guessed:** two live threads on one app-server, two separate visible TUIs, and every
253
- hook and every MCP child of BOTH threads reports the same `ppid`.
246
+ The first draft inferred that this app-server was shared across citizens. Independent
247
+ audit caught the missing evidence label; **S1b-C** then measured it directly: two live
248
+ threads, two visible TUIs, and every hook/MCP child of both threads reported the same
249
+ parent pid.
254
250
 
255
- **[host]** So this is now a measured fact rather than a structural read: **a sender marker
256
- keyed by parent pid would be one marker for N citizens**, which is precisely the uniqueness the
257
- V3 store contract (Hard Rule 7, `nativeSessionId` ownership) forbids. **This is the
258
- load-bearing result of the whole step**, and it is a design input for step 6, not a defect to
259
- fix here.
251
+ That is the result: a parent-pid marker would be one marker for N Codex citizens. The
252
+ topology is useful negative evidence, not the Codex join. **S1b-D** found the
253
+ vendor-authoritative alternative on each MCP request, and the current candidate strictly
254
+ joins that request metadata to `record.nativeSessionId`. Other measured harnesses still
255
+ use pid/start-key markers where their vendor topology makes that carrier authoritative;
256
+ the general doctrine is the vendor-authoritative record join.
260
257
 
261
258
  **[host]** A second consequence, measured on the same two runs: the hook's environment is the
262
259
  HOST process's snapshot, so it changes with the mode.
@@ -646,11 +643,11 @@ identifier**, and `record.nativeSessionId = threadId` needs no mapping layer. Th
646
643
  `nativeIdLabel: "threadId"` in `pi/entwurf-capabilities.json` is, on this axis, correct — which
647
644
  is worth saying precisely, because its `deliveryLevel: "D6"` on the same row still is not.
648
645
 
649
- **What this changes.** The step-6 question "who sent this?" does not need a parent-pid marker on
650
- codex, and the answer S1b-C ruled out was the wrong shape rather than a dead end: the caller is
651
- named on every call. `[source]` today's bridge reads a `PI_SESSION_ID`/`PI_AGENT_ID` pair or a
652
- native marker looked up by parent pid, and neither exists here so consuming `_meta` would be
653
- new bridge code. **That is step 6, and nothing here implements it.**
646
+ **What this changed on 2026-09-08.** The step-6 question did not need a parent-pid
647
+ marker on Codex: every call named the thread. At that measurement date the bridge only
648
+ read a complete pi env pair or a native pid marker, so consuming `_meta` still required
649
+ new code. The later #95 candidate implements a strict request-metadata join; that later
650
+ implementation is not evidence supplied by this ledger.
654
651
 
655
652
  One vendor fact this run also surfaced, recorded because it belongs to step 9 clause 2: the
656
653
  first `tools/call` raised an interactive approval prompt —
@@ -660,24 +657,95 @@ beside the model-facing `mcp__entwurf_bridge__entwurf_peers`. Copilot's "one too
660
657
  lesson repeats here. `entwurf_peers` itself answered from that session, which is the plain
661
658
  external-MCP-host row of `docs/external-mcp-host.md` behaving as designed.
662
659
 
663
- ## What this measurement does NOT establish
664
-
665
- - The source layer WAS independently audited on 2026-09-08 (terra, on commit `87ac7ad`):
666
- **20/20 quotations CONFIRMED, 0 corrected, 0 unverifiable**, tag identity re-derived
667
- independently (`rust-v0.153.4^{}` = `3d2ee51ca2d5db578f328aa75e20aa22c0197c9a`). Three prose
668
- defects it raised are folded in above and marked as corrections; none was a wrong vendor
669
- fact. The S1b-* sections were added AFTER that audit and carry no second reader yet.
670
- - Evidence level L4 at best: one Linux host (`oracle`, aarch64, NixOS), one CLI version, one
671
- run per claim except M-B (three runs).
672
- - **No entwurf unit was built or installed.** No record, no hook, no marker, no doctor.
673
- Steps 3–10 are untouched by design; this ledger is step 1 only.
674
- - **Step 2 is a pre-existing surprise, not this lane's work, and it is wrong.** `codex` is
675
- ALREADY in `META_BACKENDS` (`pi-extensions/lib/meta-session.ts:84`) and
676
- `META_CITIZEN_BACKENDS` (`:308`), and `pi/entwurf-capabilities.json` grades it
677
- `wakeMode: "direct-inject"`, `deliveryLevel: "D6"`, `nativeIdLabel: "threadId"` — a grade
678
- with no channel behind it, the exact failure `docs/adding-a-harness.md` step 8(c) names.
679
- It is also the declared pre-#82 legacy exception `check-harness-admission-parity` reads out
680
- of `DELIVERY.md`. Nothing here changes it; a reader should not take that D6 as evidence.
660
+ ## S1c vendor queue and visible-fresh argv (2026-09-10)
661
+
662
+ Codex 0.153.4 has the narrow delivery primitive this lane was missing:
663
+
664
+ ```sh
665
+ codex queue \
666
+ --remote unix://$SOCKET \
667
+ --thread 01a08bb0-999a-71b2-be36-357eedc26542 \
668
+ --message 'Reply exactly CODEX-QUEUE-Q1'
669
+ ```
670
+
671
+ The command exited 0 and printed one explicit acceptance receipt:
672
+
673
+ ```text
674
+ Queued message 01a08bb2-c70f-75f3-88c6-48cdffa4d64d for thread 01a08bb0-999a-71b2-be36-357eedc26542.
675
+ ```
676
+
677
+ The attached TUI produced `CODEX-QUEUE-Q1`. A second run queued a turn that executed
678
+ `sleep 12`; a third message was queued immediately while that turn was active. The CLI returned
679
+ two different message ids, and the TUI completed `CODEX-QUEUE-Q2` before
680
+ `CODEX-QUEUE-Q3`. **The vendor owns queue serialization and message identity.** Entwurf should
681
+ exec this command once and validate its receipt; it should not recreate `turn/start`, a second
682
+ queue, or an idempotency ledger. A nonexistent thread exited 1 with
683
+ `thread/queue/add failed: failed to read thread: invalid thread-store request: no rollout found`
684
+ and no acceptance receipt.
685
+
686
+ The ordinary pnpm-installed binary also owns the usable app-server shape:
687
+
688
+ ```sh
689
+ codex app-server --listen unix://$SOCKET
690
+ ```
691
+
692
+ It created a mode-0600 UDS and served WebSocket JSON-RPC. By contrast,
693
+ `codex app-server daemon start` refused this installation because its managed-daemon path
694
+ requires `~/.codex/packages/standalone/current/codex`. Entwurf must not install that separate
695
+ vendor runtime or hide this distinction behind a wrapper: the lightweight contract is to
696
+ detect one already-running ordinary app-server at the fixed vendor socket.
697
+
698
+ The complete fresh argv was then exercised against that app-server:
699
+
700
+ ```sh
701
+ codex \
702
+ --remote unix://$SOCKET \
703
+ --model gpt-5.6-sol \
704
+ --dangerously-bypass-approvals-and-sandbox \
705
+ --no-alt-screen \
706
+ 'Call mcp__entwurf_bridge__entwurf_peers as your first action …'
707
+ ```
708
+
709
+ The visible TUI reported `gpt-5.6-sol high` and `YOLO`; MCP startup completed; the first turn
710
+ called `entwurf-bridge.entwurf_peers` successfully. `--no-alt-screen` was only an observation
711
+ aid and is not part of the launch contract. The fixed launch facts are `--remote`, explicit
712
+ `--model`, the explicit task-wide permission token, and one positional first prompt. Unlike the
713
+ OMP race, the Codex MCP tool was callable on that first turn.
714
+
715
+ The same socket accepted
716
+ `thread/name/set {threadId,name:"20260910T235959-codex"}` with `{}` and the attached TUI
717
+ immediately rendered `Session renamed to 20260910T235959-codex`; a second call restored the
718
+ captured preimage `Entwirf raw queue probe Q1`. This closes the naming primitive only. The
719
+ current footer did not include `thread-title`, so the separate user-config atom remains necessary
720
+ before a garden id is continuously visible.
721
+
722
+ One lifecycle caveat is now bounded rather than hidden. Closing a TUI client does not
723
+ immediately remove its thread from `thread/loaded/list`; the vendor keeps a loaded thread after
724
+ the last subscriber disconnects and unloads it after 30 minutes without subscribers or thread
725
+ activity (`app-server/README.md`, `thread/unsubscribe`). During that vendor-owned loaded lifetime
726
+ `codex queue` remains the acceptance boundary. This is not a pane/process liveness claim and must
727
+ not be re-described as one.
728
+
729
+ These observations make 0.153.4 the first measured coordinate, **not a hard product floor**.
730
+ No silent older-version failure has been measured, so Entwurf should report drift and fail a
731
+ missing capability, not invent a lower-bound promise.
732
+
733
+ ## What this dated measurement did NOT establish
734
+
735
+ - The source layer was independently audited on 2026-09-08 (terra, commit `87ac7ad`):
736
+ **20/20 quotations CONFIRMED**. The later S1b sections had no second reader then.
737
+ - Evidence was L4 at best: one Linux host (`oracle`, aarch64, NixOS), Codex 0.153.4,
738
+ one run per claim except M-B.
739
+ - **As of the 2026-09-08 measurement**, no Entwurf Codex unit had been built or
740
+ installed, no product record/doctor existed, and steps 3–10 were untouched by that
741
+ lane. These are historical scope statements, not claims about the later candidate.
742
+ - Step 2 was already present but unearned at that date: registry/capability rows named
743
+ Codex before a production channel existed. Later #95 code supplies the candidate
744
+ record/native-push/fresh path; its parser, env-boundary, setup, preflight, and clause-7
745
+ amendments plus deep/installed-host acceptance are outside this raw ledger.
746
+ - The hook-trust and visible-name questions were closed by S1b-A/B. The remaining
747
+ release evidence is owned by the current candidate's gates and actual host, not by
748
+ reinterpreting these measurements.
681
749
  - *(Two entries lived here until 2026-09-08 and are now **closed by measurement**, not moved:
682
750
  "clause 4 has no working candidate" was retired by **S1b-B**, and "the hook trust gate has no
683
751
  measured non-interactive path" by **S1b-A**. What each one costs is stated in its section.)*
@@ -128,9 +128,11 @@ async function main(): Promise<void> {
128
128
  try {
129
129
  // ── 1. probe the REAL conversation directly (route diagnostics; proves the live host) ──
130
130
  const probe = await antigravityAdapter.probe(conversationId);
131
- if (probe.status === "alive") {
131
+ if (probe.status === "alive" && probe.route.backend === "antigravity") {
132
132
  artifacts["route"] = probe.route.lsAddress;
133
133
  console.log(` .. probe: alive via ${probe.route.lsAddress}`);
134
+ } else if (probe.status === "alive") {
135
+ artifacts["probe"] = `alive with unexpected ${probe.route.backend} route`;
134
136
  } else {
135
137
  artifacts["probe"] = `${probe.status}: ${probe.reason}`;
136
138
  }
@@ -0,0 +1,347 @@
1
+ #!/usr/bin/env bash
2
+ # smoke-codex-birth — hermetic install/doctor/inverse contract for the Codex
3
+ # birth unit. No Codex process, no model turn, no host path: HOME, XDG_DATA_HOME and
4
+ # CODEX_HOME are all sandboxed, so every byte this gate judges was written inside the fixture.
5
+ #
6
+ # The five things it exists to prove, none of which a reading of the script can establish:
7
+ # 1. the published declaration is the EXACT trust identity (matcher-less, type/command/
8
+ # timeout, quoted fixed launcher path) — because that identity is what an operator
9
+ # approves once, and any drift in it costs a second approval;
10
+ # 2. config.toml is neither read nor written, so the vendor's [hooks.state] and entwurf's
11
+ # own two user atoms survive install AND inverse byte-for-byte;
12
+ # 3. a foreign or edited hooks.json is a ZERO-WRITE refusal, never an adoption — this unit
13
+ # owns the file whole, so adopting one would delete somebody else's hooks;
14
+ # 4. the inverse removes exactly what the state vouches for, and REFUSES a drifted path
15
+ # instead of deleting an edit nobody can read back;
16
+ # 5. the doctor never claims the vendor's trust decision as its own green.
17
+ set -euo pipefail
18
+
19
+ REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
20
+ INSTALL="$REPO_DIR/scripts/codex-birth-install.sh"
21
+ UNINSTALL="$REPO_DIR/scripts/codex-birth-uninstall.sh"
22
+ DOCTOR="$REPO_DIR/scripts/codex-birth-doctor.sh"
23
+ pass=0
24
+ ok() { printf ' ok %s\n' "$1"; pass=$((pass + 1)); }
25
+ die() { printf 'FAIL: %s\n' "$1" >&2; exit 1; }
26
+ safe_umask_fail() { die "[QK:CODEX-BIRTH-INSTALL-SAFE-UMASK] $1"; }
27
+ want() { eval "$2" && ok "$1" || die "$1"; }
28
+ refuses() { # <label> <expected-substring-on-stderr> <cmd...>
29
+ local label="$1" needle="$2"; shift 2
30
+ local err rc=0
31
+ err="$("$@" 2>&1 >/dev/null)" || rc=$?
32
+ [ "$rc" -ne 0 ] && printf '%s' "$err" | grep -q "$needle" || die "$label (rc=$rc, stderr: $err)"
33
+ ok "$label"
34
+ }
35
+
36
+ REPO_BEFORE="$(cd "$REPO_DIR" && git status --porcelain)"
37
+ SB="$(mktemp -d)"
38
+ trap 'rm -rf "$SB"' EXIT
39
+ export HOME="$SB/home"
40
+ export XDG_DATA_HOME="$SB/xdg"
41
+ export CODEX_HOME="$HOME/.codex"
42
+ export PYTHONDONTWRITEBYTECODE=1
43
+ mkdir -p "$CODEX_HOME" "$XDG_DATA_HOME"
44
+
45
+ HOOKS="$CODEX_HOME/hooks.json"
46
+ CFG="$CODEX_HOME/config.toml"
47
+ UNIT_ROOT="$XDG_DATA_HOME/entwurf/codex-birth"
48
+ HELPER="$UNIT_ROOT/helper"
49
+ LAUNCHER="$HELPER/codex-birth-launch.sh"
50
+ STATE="$UNIT_ROOT/install-state.json"
51
+
52
+ seed_config() { # an operator config that ALREADY carries entwurf's two user atoms and the
53
+ # vendor's own hook-trust state — the exact shape this unit must never touch.
54
+ cat > "$CFG" <<'EOF'
55
+ model = "gpt-5.6-luna"
56
+ approval_policy = "never"
57
+ [tui]
58
+ status_line = ["thread-title", "model-with-reasoning", "current-dir"]
59
+ theme = "zenburn"
60
+ [hooks.state."/sandbox/.codex/hooks.json:session_start:0:0"]
61
+ trusted_hash = "sha256:0000000000000000000000000000000000000000000000000000000000000000"
62
+ [mcp_servers.entwurf-bridge]
63
+ command = "entwurf-bridge"
64
+ EOF
65
+ }
66
+ seed_config
67
+ cp "$CFG" "$SB/config.before.toml"
68
+
69
+ ###############################################################################
70
+ # install — the declaration, the closure, and the state
71
+ ###############################################################################
72
+ "$INSTALL" >"$SB/out" 2>&1 || die "install failed: $(cat "$SB/out")"
73
+ want "install reports success" "grep -q '^\[codex-birth-install\] installed$' '$SB/out'"
74
+ want "the declaration exists as a regular file" "[ -f '$HOOKS' ] && [ ! -L '$HOOKS' ]"
75
+ want "the launcher is published and executable" "[ -f '$LAUNCHER' ] && [ -x '$LAUNCHER' ]"
76
+ want "the closure is published whole" "[ -f '$HELPER/meta-bridge-hook-codex.ts' ] && [ -f '$HELPER/lib/meta-session.ts' ] && [ -f '$HELPER/lib/native-push/codex-ws-client.ts' ] && [ -f '$HELPER/lib/session-id.js' ] && [ -f '$HELPER/entwurf-capabilities.json' ]"
77
+ want "the state is a regular non-symlink file" "[ -f '$STATE' ] && [ ! -L '$STATE' ]"
78
+ want "the state is bound to the fixed paths and finished publishing" \
79
+ "node -e 'const s=require(process.argv[1]); if(s.schema!==\"codex-birth-install-state/v1\")throw 0; if(s.status!==\"installed\")throw 0; if(s.hooksFile!==process.argv[2])throw 0; if(s.helperDir!==process.argv[3])throw 0;' '$STATE' '$HOOKS' '$HELPER'"
80
+ want "the state records a digest for every published byte" \
81
+ "node -e 'const s=require(process.argv[1]); const hex=v=>/^[0-9a-f]{64}\$/.test(v); if(!hex(s.hooksSha256))throw 0; if(s.helperFiles.length!==6)throw 0; for(const f of s.helperFiles){if(!hex(f.sha256))throw 0;}' '$STATE'"
82
+
83
+ # The trust identity, asserted field by field. This is the cell that costs a second operator
84
+ # approval if it ever drifts, so it is checked literally rather than by shape.
85
+ want "the declaration is the EXACT trust identity (matcher-less, type/command/timeout, quoted fixed launcher)" \
86
+ "node -e '
87
+ const f=require(process.argv[1]);
88
+ if(Object.keys(f).sort().join(\",\")!==\"description,hooks\")throw new Error(\"top level\");
89
+ if(Object.keys(f.hooks).join(\",\")!==\"SessionStart\")throw new Error(\"events\");
90
+ const g=f.hooks.SessionStart; if(g.length!==1)throw new Error(\"groups\");
91
+ if(\"matcher\" in g[0])throw new Error(\"matcher present\");
92
+ if(Object.keys(g[0]).join(\",\")!==\"hooks\")throw new Error(\"group keys\");
93
+ if(g[0].hooks.length!==1)throw new Error(\"handlers\");
94
+ const h=g[0].hooks[0];
95
+ if(Object.keys(h).sort().join(\",\")!==\"command,timeout,type\")throw new Error(\"handler keys\");
96
+ if(h.type!==\"command\"||h.timeout!==30)throw new Error(\"type/timeout\");
97
+ if(h.command!==\"'\"'\"'\"+process.argv[2]+\"'\"'\"'\")throw new Error(\"command \"+h.command);
98
+ ' '$HOOKS' '$LAUNCHER'"
99
+ want "the declaration carries no trust key of any kind" "! grep -qi 'trusted_hash\|hooks.state' '$HOOKS'"
100
+ want "the launcher bakes a resolved absolute node and execs the published payload" \
101
+ "grep -qE '^NODE_BIN=\"/' '$LAUNCHER' && grep -q 'PAYLOAD=\"$HELPER/meta-bridge-hook-codex.ts\"' '$LAUNCHER' && grep -q '^exec \"\$NODE_BIN\" --experimental-strip-types' '$LAUNCHER'"
102
+
103
+ # config.toml — the file the vendor keeps [hooks.state] in, and the one entwurf's other two
104
+ # user atoms own. This unit must not have opened it for writing at all.
105
+ cmp -s "$SB/config.before.toml" "$CFG" && ok "install left config.toml byte-identical (vendor trust state and entwurf atoms intact)" || die "install modified config.toml"
106
+
107
+ ###############################################################################
108
+ # TRUST — the vendor's receipt, read and never written
109
+ #
110
+ # The bytes being ours is half the verdict. The other half belongs to the operator: until
111
+ # the vendor records their one-time "Trust all" for THIS declaration identity, the hook is
112
+ # declared and never runs, and a doctor that called that green would sell a birth nobody
113
+ # will ever have. The sandbox has no vendor, so the receipt is forged here as a FIXTURE —
114
+ # what is under test is the reading, never the deciding.
115
+ ###############################################################################
116
+ # The seed already carries a receipt for SOMEBODY ELSE's declaration — the honest starting
117
+ # shape on a host whose operator has trusted other hooks before. It is not ours.
118
+ "$DOCTOR" >"$SB/out" 2>&1 && die "[QK:CODEX-BIRTH-DOCTOR-REQUIRES-TRUST] the doctor was green with no vendor trust receipt for THIS declaration"
119
+ want "a receipt at a foreign key is refused by name" "grep -q \"NONE at this unit's key\" '$SB/out'"
120
+ want "the red carries the operator instruction, not a repair this unit could do" \
121
+ "grep -q \"open a visible plain Codex\" '$SB/out' && grep -q \"Trust all and continue\" '$SB/out'"
122
+ # ...and with no [hooks.state] at all the refusal names that instead of guessing.
123
+ grep -v '^\[hooks.state' "$SB/config.before.toml" | grep -v '^trusted_hash' > "$CFG"
124
+ "$DOCTOR" >"$SB/out" 2>&1 && die "the doctor was green with no [hooks.state] table at all"
125
+ want "trust-absent is red and says which table is missing" "grep -q 'no vendor trust receipt for this declaration' '$SB/out' && grep -q 'no \[hooks.state\] table' '$SB/out'"
126
+ cp "$SB/config.before.toml" "$CFG"
127
+ "$DOCTOR" --unit-only >"$SB/out" 2>&1 || safe_umask_fail "--unit-only should be green on intact bytes: $(cat "$SB/out")"
128
+ want "--unit-only reports the trust axis as SKIPPED, never as green" \
129
+ "grep -q 'the TRUST axis was skipped' '$SB/out' && grep -q 'This is a skipped axis, not a green one' '$SB/out'"
130
+
131
+ trust_receipt() { # $1 = the key to write a well-formed receipt under
132
+ cp "$SB/config.before.toml" "$CFG"
133
+ printf '\n[hooks.state."%s"]\ntrusted_hash = "sha256:%s"\n' "$1" "4647c53b6948cd38f2283a3fdf63e40e83b7ff29ecf14650b7e61eec9315e1cc" >> "$CFG"
134
+ }
135
+ trust_receipt "$HOOKS:session_start:0:0"
136
+ sed -i 's/^trusted_hash = .*/trusted_hash = "not-a-digest"/' "$CFG"
137
+ "$DOCTOR" >"$SB/out" 2>&1 && die "a malformed trusted_hash was accepted"
138
+ want "a malformed trusted_hash is refused by shape" "grep -q 'trusted_hash is not sha256:<64 hex>' '$SB/out'"
139
+ trust_receipt "$HOOKS:session_start:0:0"
140
+ cp "$CFG" "$SB/config-trusted.toml"
141
+ "$DOCTOR" >"$SB/out" 2>&1 || die "the doctor should be green once the vendor receipt is present: $(cat "$SB/out")"
142
+ want "the full doctor is green with unit bytes AND the vendor receipt" \
143
+ "grep -q 'the vendor trust receipt is present for this declaration' '$SB/out'"
144
+ want "the green is worded as a vendor RECEIPT, never as a validated hash" \
145
+ "grep -q 'not a cryptographic validation' '$SB/out' && ! grep -qi 'valid trust\|verified hash' '$SB/out'"
146
+ cmp -s "$SB/config-trusted.toml" "$CFG" && ok "reading the receipt wrote nothing back to the vendor config" || die "the doctor modified config.toml"
147
+ cp "$SB/config.before.toml" "$CFG"
148
+
149
+ ###############################################################################
150
+ # idempotence — the strongest form is an untouched file
151
+ ###############################################################################
152
+ MT1="$(stat -c %Y "$HOOKS")"; sleep 1.1
153
+ "$INSTALL" >"$SB/out" 2>&1 || die "reinstall failed: $(cat "$SB/out")"
154
+ MT2="$(stat -c %Y "$HOOKS")"
155
+ want "reinstall does not rewrite the declaration" "[ '$MT1' = '$MT2' ]"
156
+ want "reinstall adopts its own published declaration by digest" "grep -q 'adopting the hooks.json this unit published' '$SB/out'"
157
+ cmp -s "$SB/config.before.toml" "$CFG" && ok "reinstall still left config.toml byte-identical" || die "reinstall modified config.toml"
158
+
159
+ ###############################################################################
160
+ # reinstall over an edited closure — a REFUSAL, never a silent repair
161
+ #
162
+ # The declaration is licensed by digest; the closure must be too, and for a sharper
163
+ # reason: publishing a new generation copies over these exact paths, so a member that
164
+ # drifted is somebody's edit that a `cp` would destroy with no trace. The unit repairs
165
+ # only what it can prove it wrote, so present-and-different stops the whole publish and
166
+ # every byte — hooks.json, helper members, state — must come out unchanged.
167
+ ###############################################################################
168
+ cp "$HOOKS" "$SB/pre-drift-hooks.json"
169
+ cp "$STATE" "$SB/pre-drift-state.json"
170
+ cp "$LAUNCHER" "$SB/pre-drift-launcher.sh"
171
+ printf '# operator edit\n' >> "$LAUNCHER"
172
+ cp "$LAUNCHER" "$SB/drifted-launcher.sh"
173
+ DRIFT_ERR="$("$INSTALL" 2>&1 >/dev/null)" && die "[QK:CODEX-BIRTH-REINSTALL-REFUSES-CLOSURE-DRIFT] reinstall over an edited launcher was ACCEPTED instead of refused"
174
+ ok "reinstall over an edited launcher is refused, not silently repaired"
175
+ printf '%s' "$DRIFT_ERR" | grep -qE "$LAUNCHER was edited after install \(live [0-9a-f]{64}, recorded [0-9a-f]{64}\)" \
176
+ && ok "the refusal names the exact drifted path and both digests" \
177
+ || die "the refusal did not name the path and both digests: $DRIFT_ERR"
178
+ cmp -s "$SB/drifted-launcher.sh" "$LAUNCHER" && ok "the edited launcher survives the refused reinstall byte-for-byte" || die "the refused reinstall overwrote the edited launcher"
179
+ cmp -s "$SB/pre-drift-hooks.json" "$HOOKS" && ok "the refused reinstall left the declaration byte-identical" || die "the refused reinstall rewrote hooks.json"
180
+ cmp -s "$SB/pre-drift-state.json" "$STATE" && ok "the refused reinstall left the ownership state byte-identical" || die "the refused reinstall rewrote the state"
181
+ cmp -s "$SB/config.before.toml" "$CFG" && ok "the refused reinstall left config.toml byte-identical" || die "the refused reinstall modified config.toml"
182
+
183
+ # A drifted PAYLOAD member is the same refusal — the rule is the inventory, not the launcher.
184
+ cp "$SB/pre-drift-launcher.sh" "$LAUNCHER"
185
+ printf '// operator edit\n' >> "$HELPER/meta-bridge-hook-codex.ts"
186
+ refuses "reinstall over an edited closure member is refused too" "meta-bridge-hook-codex.ts was edited after install" "$INSTALL"
187
+ want "the drifted payload is still there" "[ -f '$HELPER/meta-bridge-hook-codex.ts' ]"
188
+
189
+ # ...and an ABSENT member is not drift: a publish interrupted after the state write must
190
+ # stay repairable by the same command, exactly as the inverse calls an absent path done.
191
+ rm -f "$HELPER/meta-bridge-hook-codex.ts"
192
+ "$INSTALL" >"$SB/out" 2>&1 || die "reinstall should REPAIR an absent closure member: $(cat "$SB/out")"
193
+ want "an absent closure member is republished, not refused" "grep -q 'closure member absent, will be republished' '$SB/out' && [ -f '$HELPER/meta-bridge-hook-codex.ts' ]"
194
+ "$DOCTOR" --unit-only >/dev/null 2>&1 || die "the unit axis should be green again after the repair"
195
+ ok "the unit axis is green again after the absent member was republished"
196
+
197
+ ###############################################################################
198
+ # refusals — a foreign file, an edited file, a symlink; each ZERO-WRITE
199
+ ###############################################################################
200
+ "$UNINSTALL" >/dev/null 2>&1 || die "clean uninstall before the refusal cells failed"
201
+ printf '{"hooks":{"SessionStart":[{"hooks":[{"type":"command","command":"/opt/operator/own-hook.sh","timeout":10}]}]}}\n' > "$HOOKS"
202
+ cp "$HOOKS" "$SB/foreign-hooks.json"
203
+ refuses "[QK:CODEX-BIRTH-FOREIGN-HOOKS-REFUSED] a foreign hooks.json is refused, not adopted" "already exists and this host has NO entwurf ownership state" "$INSTALL"
204
+ cmp -s "$SB/foreign-hooks.json" "$HOOKS" && ok "the foreign declaration survives the refusal byte-for-byte" || die "the foreign hooks.json was overwritten"
205
+ want "the foreign refusal wrote no state and no helper" "[ ! -e '$STATE' ] && [ ! -e '$HELPER' ]"
206
+ rm -f "$HOOKS"
207
+
208
+ ln -s "$SB/foreign-hooks.json" "$HOOKS"
209
+ refuses "a symlinked hooks.json is refused" "is a SYMLINK" "$INSTALL"
210
+ want "the symlink refusal wrote no state" "[ ! -e '$STATE' ] && [ -L '$HOOKS' ]"
211
+ rm -f "$HOOKS"
212
+
213
+ "$INSTALL" >/dev/null 2>&1 || die "install before the edited-declaration cell failed"
214
+ printf '\n' >> "$HOOKS"
215
+ cp "$HOOKS" "$SB/edited-hooks.json"
216
+ refuses "an edited declaration is refused instead of republished" "was edited after install" "$INSTALL"
217
+ cmp -s "$SB/edited-hooks.json" "$HOOKS" && ok "the edited declaration survives the refusal byte-for-byte" || die "the edited hooks.json was overwritten"
218
+ "$DOCTOR" --unit-only >"$SB/out" 2>&1 && die "the unit axis should be red while the declaration is edited"
219
+ want "the doctor names the edited declaration" "grep -q 'was EDITED after install' '$SB/out'"
220
+
221
+ ###############################################################################
222
+ # inverse — exact removal, and a refusal that leaves drift in place
223
+ ###############################################################################
224
+ "$UNINSTALL" >"$SB/out" 2>&1 && die "the inverse should refuse while the declaration is drifted"
225
+ want "the inverse refuses the drifted declaration" "grep -q 'DRIFT: the declaration was edited after install' '$SB/out'"
226
+ want "the drifted declaration is LEFT IN PLACE" "[ -f '$HOOKS' ]"
227
+ want "the inverse keeps the state so a later run can still license the path" "[ -f '$STATE' ]"
228
+ cmp -s "$SB/edited-hooks.json" "$HOOKS" && ok "the drifted declaration is byte-identical after the refused inverse" || die "the refused inverse changed the drifted file"
229
+
230
+ cp "$SB/edited-hooks.json" "$SB/keep.json"
231
+ rm -f "$HOOKS"
232
+ "$INSTALL" >/dev/null 2>&1 || die "reinstall after clearing the drift failed"
233
+ printf '# operator edit\n' >> "$LAUNCHER"
234
+ "$UNINSTALL" >"$SB/out" 2>&1 && die "the inverse should refuse while the launcher is drifted"
235
+ want "the inverse refuses a drifted helper member by digest" "grep -q 'DRIFT: helper member was edited after install' '$SB/out'"
236
+ want "the drifted launcher is LEFT IN PLACE" "[ -f '$LAUNCHER' ]"
237
+ want "the declaration it already removed is gone, and the state remains" "[ ! -e '$HOOKS' ] && [ -f '$STATE' ]"
238
+
239
+ rm -f "$LAUNCHER"
240
+ "$INSTALL" >/dev/null 2>&1 || die "reinstall before the exact-inverse cell failed"
241
+ "$UNINSTALL" >"$SB/out" 2>&1 || die "the exact inverse failed: $(cat "$SB/out")"
242
+ want "the inverse removed the declaration" "[ ! -e '$HOOKS' ]"
243
+ want "the inverse removed the helper closure and its directory" "[ ! -e '$HELPER' ]"
244
+ want "the inverse removed its own state" "[ ! -e '$STATE' ]"
245
+ want "the inverse says config.toml was never touched" "grep -q 'config.toml was neither read nor written' '$SB/out'"
246
+ cmp -s "$SB/config.before.toml" "$CFG" && ok "the full install/inverse cycle left config.toml byte-identical" || die "the cycle modified config.toml"
247
+ # An absent unit is RED, not a note. A host that trusted an OLDER declaration still carries a
248
+ # vendor receipt, so a doctor that shrugged at "nothing installed" could report green over an
249
+ # empty unit on the strength of somebody else's approval — measured exactly that way once.
250
+ "$DOCTOR" --unit-only >"$SB/out" 2>&1 && die "[QK:CODEX-BIRTH-DOCTOR-ABSENT-IS-RED] the unit axis was green with NOTHING installed"
251
+ want "an absent unit is named as red, not as an informational note" "grep -q 'no Codex birth unit is installed here' '$SB/out'"
252
+ trust_receipt "$HOOKS:session_start:0:0"
253
+ "$DOCTOR" >"$SB/out" 2>&1 && die "a stale vendor receipt made an ABSENT unit green"
254
+ want "a stale receipt cannot carry an absent unit" "grep -q 'no Codex birth unit is installed here' '$SB/out'"
255
+ cp "$SB/config.before.toml" "$CFG"
256
+
257
+ ###############################################################################
258
+ # unsafe paths — ownership the unit cannot vouch for
259
+ ###############################################################################
260
+ "$INSTALL" >/dev/null 2>&1 || die "install before the unsafe-path cell failed"
261
+ chmod 0777 "$HELPER"
262
+ refuses "a group/world-writable helper directory is refused" "group/world-writable" "$INSTALL"
263
+ chmod 0755 "$HELPER"
264
+ chmod 0666 "$STATE"
265
+ refuses "a group/world-writable ownership state cannot license anything" "group/world-writable" "$INSTALL"
266
+ refuses "the inverse refuses the same unsafe state" "group/world-writable" "$UNINSTALL"
267
+ chmod 0644 "$STATE"
268
+ "$UNINSTALL" >/dev/null 2>&1 || die "final cleanup uninstall failed"
269
+
270
+ ###############################################################################
271
+ # a hostile umask, a short inventory, and a relative root
272
+ ###############################################################################
273
+ # `umask 000` is the operator's shell, not our decision. A directory published under it that
274
+ # anyone can rewrite would let a third party swap the launcher between the digest we record
275
+ # and the exec codex performs — the closure would still match its state and still be somebody
276
+ # else's code.
277
+ rm -rf "$CODEX_HOME/hooks.json" "$UNIT_ROOT"
278
+ ( umask 000; "$INSTALL" >/dev/null 2>&1 ) || die "install under umask 000 failed"
279
+ UNSAFE_DIRS="$(find "$XDG_DATA_HOME" "$CODEX_HOME" -type d -perm /022 2>/dev/null || true)"
280
+ [ -z "$UNSAFE_DIRS" ] && ok "every directory published under umask 000 is non-group/world-writable" \
281
+ || safe_umask_fail "umask 000 published writable directories: $UNSAFE_DIRS"
282
+ want "the published files are not group/world-writable either" \
283
+ "[ -z \"\$(find '$UNIT_ROOT' -type f -perm /022 2>/dev/null)\" ] && [ ! -w /dev/null -o -z \"\$(find '$HOOKS' -perm /022 2>/dev/null)\" ]"
284
+
285
+ # The inventory IS the authority. A state naming only the launcher would leave the payload
286
+ # unlisted: nothing could reclaim it, and nothing would judge it.
287
+ python3 - "$STATE" <<'PY_INV'
288
+ import json, sys
289
+ s = json.load(open(sys.argv[1]))
290
+ s["helperFiles"] = [f for f in s["helperFiles"] if f["path"] == "codex-birth-launch.sh"]
291
+ open(sys.argv[1], "w").write(json.dumps(s))
292
+ PY_INV
293
+ printf '// unlisted\n' >> "$HELPER/meta-bridge-hook-codex.ts"
294
+ refuses "[QK:CODEX-BIRTH-EXACT-CLOSURE-INVENTORY] a short closure inventory is refused by the installer" "exact six members" "$INSTALL"
295
+ want "the unlisted payload survives that refusal" "grep -q '^// unlisted$' '$HELPER/meta-bridge-hook-codex.ts'"
296
+ "$DOCTOR" --unit-only >"$SB/out" 2>&1 && die "the doctor accepted a short closure inventory"
297
+ want "the doctor names the missing inventory members" "grep -q 'recorded closure inventory is missing' '$SB/out'"
298
+ "$UNINSTALL" >"$SB/out" 2>&1 && die "the inverse accepted a short closure inventory"
299
+ want "the inverse refuses and removes NOTHING" \
300
+ "grep -q 'exact six members' '$SB/out' && [ -f '$HOOKS' ] && [ -f '$HELPER/meta-bridge-hook-codex.ts' ] && [ -f '$STATE' ]"
301
+ rm -rf "$CODEX_HOME/hooks.json" "$UNIT_ROOT"
302
+
303
+ # An unknown status is not a receipt, however well its digests match. `publishing` is the one
304
+ # interrupted generation the installer knows how to finish; anything else is a state this unit
305
+ # never wrote, and adopting it would let a stranger's file license our next publish.
306
+ "$INSTALL" >/dev/null 2>&1 || die "install before the status cell failed"
307
+ python3 -c 'import json,sys; s=json.load(open(sys.argv[1])); s["status"]="garbage"; open(sys.argv[1],"w").write(json.dumps(s))' "$STATE"
308
+ cp "$STATE" "$SB/garbage-state.json"; cp "$HOOKS" "$SB/garbage-hooks.json"
309
+ refuses "[QK:CODEX-BIRTH-INSTALL-REFUSES-UNKNOWN-STATUS] a state in an unknown status is refused even with exact digests" "unknown state status" "$INSTALL"
310
+ want "that refusal wrote nothing" \
311
+ "cmp -s '$SB/garbage-state.json' '$STATE' && cmp -s '$SB/garbage-hooks.json' '$HOOKS'"
312
+ # ...while the interrupted generation the installer DOES define is still repairable.
313
+ python3 -c 'import json,sys; s=json.load(open(sys.argv[1])); s["status"]="publishing"; open(sys.argv[1],"w").write(json.dumps(s))' "$STATE"
314
+ "$INSTALL" >/dev/null 2>&1 || die "an interrupted `publishing` state should be finishable, not refused"
315
+ want "the finished publish records installed" "grep -q '\"status\": \"installed\"' '$STATE'"
316
+
317
+ # The directory holding the receipt carries the receipt's authority. Children untouched: what
318
+ # changed is only who may replace the inventory every digest is compared against.
319
+ chmod 0777 "$UNIT_ROOT"
320
+ refuses "[QK:CODEX-BIRTH-STATE-PARENT-AUTHORITY] reinstall refuses a unit root anyone can rewrite" "group/world-writable" "$INSTALL"
321
+ want "the refused reinstall neither repaired nor published over the unsafe root" \
322
+ "[ \"\$(stat -c %a '$UNIT_ROOT')\" = '777' ]"
323
+ "$DOCTOR" --unit-only >"$SB/out" 2>&1 && die "the doctor accepted a unit root anyone can rewrite"
324
+ want "the doctor names the writable unit root" "grep -q 'unit root.*group/world-writable' '$SB/out'"
325
+ "$UNINSTALL" >"$SB/out" 2>&1 && die "the inverse removed things on the word of a state anyone can replace"
326
+ want "the inverse refuses and removes NOTHING from an unsafe root" \
327
+ "grep -q 'group/world-writable' '$SB/out' && [ -f '$STATE' ] && [ -f '$HOOKS' ] && [ -f '$LAUNCHER' ]"
328
+ chmod 0755 "$UNIT_ROOT"
329
+ "$DOCTOR" --unit-only >/dev/null 2>&1 || die "the unit axis should be green once the root is safe again"
330
+ ok "the unit axis is green again once the root is no longer writable"
331
+ "$UNINSTALL" >/dev/null 2>&1 || die "cleanup uninstall after the parent-authority cell failed"
332
+
333
+ # A relative root would make "the fixed launcher path" depend on the caller's cwd — and that
334
+ # path is exactly what the vendor keys its trust receipt to.
335
+ ( cd "$SB" && CODEX_HOME="relative/.codex" "$INSTALL" >"$SB/out" 2>&1 ) && die "[QK:CODEX-BIRTH-REFUSES-RELATIVE-ROOT] a relative CODEX_HOME was accepted"
336
+ want "a relative CODEX_HOME is refused zero-write" \
337
+ "grep -q 'CODEX_HOME resolves to a relative path' '$SB/out' && [ ! -e '$SB/relative' ]"
338
+ ( cd "$SB" && XDG_DATA_HOME="relative/share" "$INSTALL" >"$SB/out" 2>&1 ) && die "a relative XDG_DATA_HOME was accepted"
339
+ want "a relative XDG_DATA_HOME is refused zero-write" \
340
+ "grep -q 'unit root resolves to a relative path' '$SB/out' && [ ! -e '$SB/relative' ]"
341
+ ( cd "$SB" && CODEX_HOME="relative/.codex" "$DOCTOR" --unit-only >"$SB/out" 2>&1 ) && die "the doctor judged a relative CODEX_HOME"
342
+ want "the doctor refuses a relative CODEX_HOME before judging anything" "grep -q \"CODEX_HOME resolves to a relative path\" '$SB/out'"
343
+
344
+ REPO_AFTER="$(cd "$REPO_DIR" && git status --porcelain)"
345
+ [ "$REPO_BEFORE" = "$REPO_AFTER" ] && ok "the checkout is untouched (every byte lived in the sandbox)" || die "this gate modified the repository"
346
+
347
+ printf '\nsmoke-codex-birth: %d checks passed\n' "$pass"