@junghanacs/entwurf 0.20.0 → 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.
Files changed (112) hide show
  1. package/AGENTS.md +85 -190
  2. package/BASELINE.md +6 -5
  3. package/CHANGELOG.md +220 -14
  4. package/CONTRIBUTING.md +1 -1
  5. package/DELIVERY.md +202 -60
  6. package/README.md +54 -26
  7. package/VERIFY.md +37 -7
  8. package/docs/acp-backend-rail.md +31 -15
  9. package/docs/external-mcp-host.md +58 -35
  10. package/docs/fresh-cut-policy.md +3 -2
  11. package/docs/setup-clean-host.md +110 -17
  12. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +101 -109
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +2 -2
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +6 -0
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +326 -0
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +22 -2
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +3 -1
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +6 -7
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-native-push.js +30 -17
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +7 -1
  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 +50 -15
  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/meta-bridge-fresh-cut.js +6 -1
  30. package/mcp/entwurf-bridge/src/index.ts +115 -111
  31. package/mcp/entwurf-bridge/tsconfig.build.json +1 -0
  32. package/package.json +14 -5
  33. package/pi-extensions/entwurf-control.ts +71 -19
  34. package/pi-extensions/lib/acp/acp-client.ts +3 -3
  35. package/pi-extensions/lib/acp/backend-adapter.ts +3 -3
  36. package/pi-extensions/lib/acp/backend.ts +3 -3
  37. package/pi-extensions/lib/acp/event-mapper.ts +4 -4
  38. package/pi-extensions/lib/acp/tool-surface.ts +6 -0
  39. package/pi-extensions/lib/codex-fresh-preflight.ts +363 -0
  40. package/pi-extensions/lib/compaction-send-guard.ts +80 -0
  41. package/pi-extensions/lib/entwurf-fact-provider.ts +29 -3
  42. package/pi-extensions/lib/entwurf-peers-render.ts +4 -1
  43. package/pi-extensions/lib/entwurf-v2-contract.ts +6 -7
  44. package/pi-extensions/lib/entwurf-v2-native-push.ts +35 -18
  45. package/pi-extensions/lib/entwurf-v2-production.ts +10 -3
  46. package/pi-extensions/lib/entwurf-v2-surface.ts +7 -4
  47. package/pi-extensions/lib/meta-mailbox-body.ts +22 -13
  48. package/pi-extensions/lib/meta-sender-identity.ts +305 -0
  49. package/pi-extensions/lib/mux-fresh-call.ts +64 -19
  50. package/pi-extensions/lib/native-push/adapter.ts +21 -24
  51. package/pi-extensions/lib/native-push/codex-ws-client.ts +506 -0
  52. package/pi-extensions/lib/native-push/register.ts +7 -9
  53. package/pi-extensions/meta-bridge-hook-codex.ts +371 -0
  54. package/run.sh +251 -35
  55. package/scripts/check-acp-usage-accounting.ts +9 -9
  56. package/scripts/check-agy-sender-identity.ts +1 -1
  57. package/scripts/check-codex-birth-hook.ts +264 -0
  58. package/scripts/check-codex-bridge-identity.ts +179 -0
  59. package/scripts/check-codex-native-push.ts +386 -0
  60. package/scripts/check-codex-sender-identity.ts +495 -0
  61. package/scripts/check-compaction-send-guard.ts +130 -0
  62. package/scripts/check-copilot-receive-arm.ts +4 -1
  63. package/scripts/check-entwurf-fact-provider.ts +38 -0
  64. package/scripts/check-entwurf-peers-surface.ts +13 -1
  65. package/scripts/check-entwurf-self-address.ts +15 -16
  66. package/scripts/check-entwurf-v2-contract.ts +4 -3
  67. package/scripts/check-entwurf-v2-decider.ts +7 -5
  68. package/scripts/check-entwurf-v2-native-push.ts +35 -7
  69. package/scripts/check-entwurf-v2-production.ts +203 -11
  70. package/scripts/check-entwurf-v2-runner.ts +1 -1
  71. package/scripts/check-entwurf-v2-surface.ts +1 -1
  72. package/scripts/check-gate-qualification.ts +7 -4
  73. package/scripts/check-harness-admission-parity.ts +0 -1
  74. package/scripts/check-install-surface.ts +23 -7
  75. package/scripts/check-mux-launch-tmux.ts +47 -2
  76. package/scripts/check-native-push-adapter.ts +20 -16
  77. package/scripts/check-native-push-register.ts +5 -1
  78. package/scripts/check-release-gate-outcomes.ts +47 -1
  79. package/scripts/check-setup-qualification.sh +3 -1
  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-statusline-config.py +434 -0
  85. package/scripts/codex_toml_io.py +532 -0
  86. package/scripts/lib/codex-fresh-live-protocol.ts +113 -0
  87. package/scripts/lib/codex-fresh-source-receipts.ts +399 -0
  88. package/scripts/lib/launch-receipt-windows.ts +46 -0
  89. package/scripts/lib/tmux-coordinate-row.ts +58 -0
  90. package/scripts/meta-bridge-fresh-cut.ts +6 -1
  91. package/scripts/mutants/codex-native.json +838 -0
  92. package/scripts/mutants/compaction-send-guard.json +103 -0
  93. package/scripts/mutants/entwurf-peers.json +19 -0
  94. package/scripts/mutants/mux-fresh-call.json +19 -8
  95. package/scripts/mutants/omp-fresh.json +6 -4
  96. package/scripts/mutants/pi-package-ownership.json +26 -0
  97. package/scripts/mutants/release-gate.json +13 -0
  98. package/scripts/mutants/v2-surface.json +53 -1
  99. package/scripts/pi_settings_io.py +3 -1
  100. package/scripts/raw-async-delivery/README.md +2 -1
  101. package/scripts/raw-codex-measure/README.md +114 -46
  102. package/scripts/register-pi-package.py +38 -30
  103. package/scripts/register-pi-provider.py +3 -2
  104. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  105. package/scripts/smoke-agy-native-push-live.ts +3 -1
  106. package/scripts/smoke-codex-birth.sh +347 -0
  107. package/scripts/smoke-codex-config-state.sh +511 -0
  108. package/scripts/smoke-codex-fresh-live.ts +1186 -0
  109. package/scripts/smoke-codex-native-push-live.ts +75 -0
  110. package/scripts/smoke-setup-verdict.sh +123 -10
  111. package/scripts/smoke-user-scope-citizen.sh +199 -71
  112. package/scripts/tsconfig.json +1 -0
@@ -22,29 +22,36 @@ settings-RELATIVE entry that resolves to repo_dir (see below). A look-alike repo
22
22
  removed. Every non-entwurf package and every other settings key is preserved.
23
23
 
24
24
  CANONICAL IS NOT ONLY THE ABSOLUTE PATH. A packages[] entry is resolved by pi
25
- against the SETTINGS FILE'S OWN DIRECTORY, so this repo's committed
26
- `.pi/settings.json` names itself portably as `".."` the exact form
27
- check-install-surface S7c pins. Comparing entries against the resolved absolute
28
- path ALONE did not recognize that as entwurf, so `setup` appended the absolute
29
- path beside it and rewrote the tracked, biome-governed file in a foreign style:
30
- a dev clone went RED at `pnpm check` step 1, diagnosed as a "formatting" error
31
- (#53 B). Two rules follow, and they are the same rule read forwards and backwards:
25
+ against the SETTINGS FILE'S OWN DIRECTORY, so a settings-relative entry such as
26
+ `".."` can name the very repo being registered. Comparing entries against the
27
+ resolved absolute path ALONE did not recognize that as entwurf, so `setup` appended
28
+ the absolute path beside it and rewrote a tracked, biome-governed file in a foreign
29
+ style: a dev clone went RED at `pnpm check` step 1, diagnosed as a "formatting"
30
+ error (#53 B). Two rules follow, and they are the same rule read forwards and
31
+ backwards:
32
32
  - register: an entry that RESOLVES to repo_dir is already canonical → no-op, and
33
33
  when a rewrite is genuinely needed a settings-relative self-reference is kept
34
34
  as the survivor, so the portable form is never silently absolutized;
35
35
  - remove: register only ever WRITES the absolute form, so a settings-relative
36
- self-reference cannot be install's own output — it was authored by the repo
37
- (this one commits `".."`) or by the operator. Uninstall is install's inverse,
38
- not a settings editor: it leaves that ONE class in place and SAYS SO on stdout,
39
- rather than deleting source bytes install never wrote. The cost is stated
40
- rather than hidden: on a settings file whose only entwurf entry is relative,
41
- `--remove` is a no-op and the package stays registered until a human edits it.
42
- would_remove() asks the same split, so `--dry-run` can never disagree with what
43
- `--remove` does.
36
+ self-reference cannot be install's own output — it was authored by hand.
37
+ Uninstall is install's inverse, not a settings editor: it leaves that ONE class
38
+ in place and SAYS SO on stdout, rather than deleting source bytes install never
39
+ wrote. The cost is stated rather than hidden: on a settings file whose only
40
+ entwurf entry is relative, `--remove` is a no-op and the package stays
41
+ registered until a human edits it. would_remove() asks the same split, so
42
+ `--dry-run` can never disagree with what `--remove` does.
43
+ This repo's own `.pi/settings.json` USED to be the load-bearing instance of that
44
+ shape. It no longer carries a packages[] entry at all: entwurf's checkout
45
+ self-registers through USER scope only, because a project entry naming "whichever
46
+ checkout I am in" plus a user entry naming the installed one are two package
47
+ identities to pi, and a second checkout then loads a rival copy of pi-extensions
48
+ and wins `--entwurf-control` (#110). The predicate stays because the shape is still
49
+ writable by hand, and because resolving-then-comparing is also what keeps an
50
+ unrelated relative package like `../../repos/gh/andenken` from being read as ours.
44
51
  A rewrite also preserves the file's existing indentation instead of forcing 2
45
52
  spaces. That is narrower than it sounds — it keeps the indent UNIT, not a
46
- formatter's line-collapsing decisions — so the byte-identity guarantee this repo's
47
- own settings depend on comes from the no-op path, never from the writer's style.
53
+ formatter's line-collapsing decisions — so a byte-identity guarantee always comes
54
+ from the no-op path, never from the writer's style.
48
55
 
49
56
  This wiring (user scope) dropped when `pi install` was removed from setup
50
57
  (2026-07-03: `--entwurf-control` unknown in a foreign cwd). Extracting it here
@@ -129,11 +136,11 @@ def source_of(item: object) -> object:
129
136
  def is_settings_relative_self(source: str, settings_dir: str, repo_dir: str) -> bool:
130
137
  """True iff this entry is a SETTINGS-RELATIVE path naming repo_dir itself.
131
138
 
132
- This is the repo's own committed portable form (`".."` in
133
- <repo>/.pi/settings.json) functionally identical to the absolute entry,
134
- because pi resolves a relative package source against the settings file's own
135
- directory. It is also the one shape register never writes, which is what lets
136
- remove treat it as source rather than as install state.
139
+ The portable form (`".."` in <repo>/.pi/settings.json) — functionally identical
140
+ to the absolute entry, because pi resolves a relative package source against the
141
+ settings file's own directory. It is also the one shape register never writes,
142
+ which is what lets remove treat it as source rather than as install state. This
143
+ repo stopped shipping it at #110; an operator may still write it by hand.
137
144
 
138
145
  Anything carrying a scheme (npm:/git:/https:) is a spec, not a path; anything
139
146
  absolute (`/`, `~`) is not settings-relative. Everything else is resolved and
@@ -159,8 +166,8 @@ def is_entwurf_source(source: str, repo_dir: str, settings_dir: str | None = Non
159
166
  - the exact resolved repo dir;
160
167
  - an npm install path ending in node_modules/@junghanacs/entwurf;
161
168
  - an explicit npm package source for @junghanacs/entwurf;
162
- - a settings-relative path that RESOLVES to the repo dir (the committed
163
- portable `".."`), when the caller supplies settings_dir;
169
+ - a settings-relative path that RESOLVES to the repo dir (the portable
170
+ `".."` form), when the caller supplies settings_dir;
164
171
  - a local filesystem path whose final directory is literally "entwurf"
165
172
  (dev clone / stale move). Remote URL/git-like strings are NOT treated as
166
173
  local paths merely because their last segment is "entwurf".
@@ -242,7 +249,7 @@ def register(settings_path: Path, repo_dir_arg: str) -> str:
242
249
  # the canonical absolute string or as a settings-relative path resolving there.
243
250
  # Both are the same registration to pi, so both are a no-op: order-insensitive, no
244
251
  # rewrite, mtime stable. Recognizing only the absolute form is what made `setup`
245
- # duplicate this repo's own committed `".."` and restyle the tracked file (#53 B).
252
+ # duplicate a settings-relative `".."` and restyle the tracked file (#53 B).
246
253
  # Both arms require the STRING form, so an object-form entry still collapses to a
247
254
  # canonical string exactly as before.
248
255
  if len(entwurf_entries) == 1 and isinstance(entwurf_entries[0], str) and (
@@ -252,8 +259,9 @@ def register(settings_path: Path, repo_dir_arg: str) -> str:
252
259
  return "noop"
253
260
 
254
261
  # A rewrite keeps the PORTABLE form when the file already had one: absolutizing a
255
- # committed `".."` would repair the duplicate and dirty the tracked bytes in the
256
- # same breath. Otherwise the canonical absolute path is what install writes.
262
+ # hand-written `".."` would repair the duplicate and dirty bytes install never
263
+ # wrote in the same breath. Otherwise the canonical absolute path is what install
264
+ # writes.
257
265
  survivor = next(
258
266
  (
259
267
  source_of(e) for e in entwurf_entries
@@ -277,7 +285,7 @@ def _removable(packages: list, repo_dir: str, settings_dir: str) -> tuple[list,
277
285
 
278
286
  PRESERVED = settings-relative self-references, the one shape register CANNOT
279
287
  have produced (it always writes the resolved absolute path). Such an entry is
280
- the repo's committed portable registration or the operator's own hand edit.
288
+ the operator's own hand edit.
281
289
  Deleting it would make uninstall a source editor, which is the defect #53 B is
282
290
  about, pointed the other way. main() prints what was kept so the incompleteness
283
291
  is loud rather than silent.
@@ -665,7 +673,7 @@ def remove_user(settings_path: Path, repo_dir_arg: str, state_path: Path, orphan
665
673
  else:
666
674
  print(f"remove: no entwurf packages[] entry naming this root ({settings_path})")
667
675
  if preserved_rel:
668
- print(f"remove: kept {len(preserved_rel)} settings-relative entwurf entr{'y' if len(preserved_rel) == 1 else 'ies'} — committed/operator source, not install state")
676
+ print(f"remove: kept {len(preserved_rel)} settings-relative entwurf entr{'y' if len(preserved_rel) == 1 else 'ies'} — operator source, not install state")
669
677
  if foreign_like:
670
678
  print(f"remove: kept {len(foreign_like)} entwurf-shaped entr{'y' if len(foreign_like) == 1 else 'ies'} with no recorded owner "
671
679
  "(not provably this root's) — use './run.sh doctor-pi-package' / takeover-user-scope to resolve ownership")
@@ -829,7 +837,7 @@ def main(argv: list[str]) -> int:
829
837
  if kept:
830
838
  print(
831
839
  f"remove: kept {kept} settings-relative entwurf entr{'y' if kept == 1 else 'ies'} "
832
- f"({settings_path}) — install never writes that form, so it is committed/operator "
840
+ f"({settings_path}) — install never writes that form, so it is operator "
833
841
  "source, not install state; edit the file by hand to drop it"
834
842
  )
835
843
  return 0
@@ -25,8 +25,9 @@ Scope asymmetry (봉인계약 4·6, REASONED — not the unfounded asymmetry dev
25
25
  of our key is tidied; siblings kept.
26
26
  project <repo>/.pi/settings.json — checkout-LOCAL, disposable, re-creatable, and `run.sh
27
27
  remove` already covers it → NO state. install normalizes the command; remove strips
28
- our-managed shapes (the bare bin AND the legacy repo path). project-scope state is a
29
- NAMED FOLLOW-UP (NEXT), deliberately out of this lane.
28
+ our-managed shapes (the bare bin AND the legacy repo path). project scope takes NO
29
+ state, deliberately: it is checkout-local, disposable, and `run.sh remove` is its
30
+ whole inverse.
30
31
 
31
32
  USER-SCOPE OWNER BINDING (#86 C2): the user install-state additionally records
32
33
  `installerRoot` (the repo/package root that installed the key), so an old root's
@@ -3,7 +3,7 @@
3
3
  // LIVE=1 ./run.sh smoke-acp-raw-turn-live
4
4
  //
5
5
  // What this proves (and ONLY this): the pinned Claude ACP adapter
6
- // (@agentclientprotocol/claude-agent-acp@0.75.1) spawns, speaks the ACP wire
6
+ // (@agentclientprotocol/claude-agent-acp@0.76.0) spawns, speaks the ACP wire
7
7
  // protocol over stdio NDJSON, and returns one real model turn. It is the
8
8
  // bytes-flow proof that the S2a dep surface is not just installable but
9
9
  // actually drivable — before any provider/overlay/streamSimple code (S2b+).
@@ -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"