@junghanacs/entwurf 0.19.0 → 0.20.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 (59) hide show
  1. package/AGENTS.md +1 -1
  2. package/BASELINE.md +23 -5
  3. package/CHANGELOG.md +236 -0
  4. package/DELIVERY.md +59 -6
  5. package/README.md +40 -9
  6. package/VERIFY.md +9 -1
  7. package/docs/acp-backend-rail.md +9 -1
  8. package/docs/external-mcp-host.md +6 -3
  9. package/docs/setup-clean-host.md +93 -7
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +3 -3
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +6 -2
  12. package/package.json +2 -2
  13. package/pi-extensions/acp-provider.ts +1 -1
  14. package/pi-extensions/lib/acp/models.ts +3 -3
  15. package/pi-extensions/lib/acp/overlay.ts +6 -2
  16. package/pi-extensions/lib/meta-session.ts +25 -0
  17. package/run.sh +71 -26
  18. package/scripts/agy-bridge.sh +1 -1
  19. package/scripts/agy-imprint.sh +10 -7
  20. package/scripts/check-acp-carrier-augment.ts +6 -5
  21. package/scripts/check-acp-cortex.ts +41 -0
  22. package/scripts/check-acp-provider-surface.ts +9 -6
  23. package/scripts/check-copilot-birth-hook.ts +3 -1
  24. package/scripts/check-copilot-receive-arm.ts +169 -21
  25. package/scripts/check-copilot-statusline.ts +3 -1
  26. package/scripts/check-gate-qualification.ts +10 -9
  27. package/scripts/check-hook-launch-topology.ts +6 -5
  28. package/scripts/check-install-surface.ts +2 -1
  29. package/scripts/check-meta-facts.ts +6 -4
  30. package/scripts/check-meta-hook-session-switch.ts +4 -3
  31. package/scripts/check-meta-identity-consumers.ts +8 -7
  32. package/scripts/check-meta-receiver-marker.ts +2 -1
  33. package/scripts/check-omp-birth-hook.ts +3 -2
  34. package/scripts/check-probe-bridge-command.ts +3 -1
  35. package/scripts/check-setup-qualification.sh +21 -0
  36. package/scripts/copilot-bridge-doctor.sh +15 -5
  37. package/scripts/copilot-bridge-install.sh +6 -2
  38. package/scripts/copilot-launch.sh +16 -2
  39. package/scripts/copilot-receive-bridge.sh +80 -13
  40. package/scripts/lib/reclaim-on-exit.ts +86 -0
  41. package/scripts/meta-bridge-doctor.sh +56 -15
  42. package/scripts/meta-bridge-install.sh +13 -7
  43. package/scripts/meta-bridge-uninstall.sh +6 -4
  44. package/scripts/mutants/acp-cortex.json +26 -2
  45. package/scripts/mutants/copilot-receive.json +26 -0
  46. package/scripts/mutants/omp-birth.json +23 -6
  47. package/scripts/mutants/setup-verdict.json +13 -0
  48. package/scripts/omp-bridge-doctor.sh +110 -41
  49. package/scripts/omp-bridge-install.sh +6 -2
  50. package/scripts/omp-receive-install.sh +7 -2
  51. package/scripts/raw-async-delivery/README.md +10 -1
  52. package/scripts/raw-codex-measure/README.md +689 -0
  53. package/scripts/raw-codex-measure/source-audit.md +243 -0
  54. package/scripts/raw-macos-measure/README.md +148 -0
  55. package/scripts/raw-macos-measure/probe.sh +389 -0
  56. package/scripts/smoke-meta-async-drift.sh +7 -5
  57. package/scripts/smoke-meta-install-state.sh +70 -19
  58. package/scripts/smoke-omp-bridge-state.sh +38 -0
  59. package/scripts/smoke-setup-verdict.sh +100 -0
@@ -0,0 +1,689 @@
1
+ # raw-codex-measure — Codex CLI 0.153.4 vendor measurement (issue #95)
2
+
3
+ Lane: `docs/adding-a-harness.md` step 1 (six measurements) + §3.5 citizen scope + step 5's
4
+ tool-name dialect + step 6's join. The oracle is the vendor artifact or the vendor process,
5
+ never our assembler.
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.
11
+
12
+ Evidence-state vocabulary: **[source]** = read at `file:line` in the vendor checkout;
13
+ **[host]** = measured on this host (`oracle`, 2026-09-08 KST) with the receipt named;
14
+ **[hypothesis]** = a structure→behaviour inference not yet confirmed by a run.
15
+
16
+ Source-layer claims here were independently audited on 2026-09-08 (terra, against commit
17
+ `87ac7ad`): 20/20 quotations CONFIRMED, 0 corrected. Its three prose defects are folded in and
18
+ marked `Corrected 2026-09-08 after independent audit`. `source-audit.md` holds the per-claim
19
+ receipts a reviewer opens instead of re-deriving. The **S1b-*** sections were measured after
20
+ that audit and have not been through it.
21
+
22
+ **Section ids.** `M1`–`M8` are the step-1 measurements and `M-B` is the raw delivery probe —
23
+ those ids are cited by the first commit and by the independent audit, so they do not move.
24
+ The follow-up round is `S1b-A`–`S1b-D`, on its own axis, because the first draft named one of
25
+ them `M-B` too and two sections cannot share an id.
26
+
27
+ ## Vendor identity
28
+
29
+ - Installed: `codex-cli 0.153.4` **[host]** (`codex --version`), launcher
30
+ `/home/junghan/.local/share/pnpm/bin/codex` → `bin/codex.js` → the Rust binary
31
+ `@openai+codex@0.153.4-linux-arm64/.../vendor/aarch64-unknown-linux-musl/bin/codex`
32
+ (observed in every `/proc/<pid>/cmdline` below).
33
+ - Source checkout for `[source]` receipts: `~/repos/3rd/codex` at tag `rust-v0.153.4`
34
+ (`3d2ee51`), the exact tag `gh release list -R openai/codex` names for `0.153.4` **[host]**.
35
+ All paths below are relative to `codex-rs/` in that checkout.
36
+ - Operator state root: `~/.codex/` — `config.toml` is a symlink into
37
+ `~/repos/gh/agent-config/codex/config.toml`, plus `sessions/YYYY/MM/DD/rollout-*.jsonl`,
38
+ `state_5.sqlite`, `skills`, `plugins`, `shell_snapshots` **[host]**.
39
+ - Distance from the archived probe: `0.136.0` → `0.153.4` is **17 minor lines**. The delivery
40
+ probe survived unchanged (M-B below); the hook surface did not (M1).
41
+
42
+ ## M1 — Hook vocabulary and firing time
43
+
44
+ - **[source]** 12 hook events, not 0.136's 9: `PreToolUse, PermissionRequest, PostToolUse,
45
+ PreCompact, PostCompact, SessionStart, SessionEnd, UserPromptSubmit, SubagentStart,
46
+ SubagentStop, Stop, Interrupt` (`hooks/src/lib.rs:23`). New since the 0.136 note in
47
+ `scripts/raw-async-delivery/README.md`: `PermissionRequest`, `SessionEnd`, `Interrupt`.
48
+ - **[source]** `SessionStart` carries a 4-valued `source` matcher: `startup | resume | clear |
49
+ compact` (`hooks/src/events/session_start.rs:25-38`); the matcher input for `SessionStart`
50
+ is that source string (`:69-74`).
51
+ - **[source]** **Firing is deferred to the first turn, not to window open.** Session
52
+ construction only QUEUES the source (`core/src/session/session.rs:1623`,
53
+ `queue_pending_session_start_source`); the hook runs from
54
+ `run_pending_session_start_hooks` (`core/src/hook_runtime.rs:124`), whose only production
55
+ call sites are inside turn execution (`core/src/session/turn.rs:264` and `:504`).
56
+ - **[host]** Confirmed on a live TUI, and the gap is 49 seconds wide:
57
+ ```
58
+ LAUNCH_AT=2026-09-08T13:49:03.030Z # tmux window opened, TUI drew, model loaded
59
+ PROMPT_SENT_AT=2026-09-08T13:49:50.758Z # first user prompt
60
+ === 2026-09-08T13:49:52.380Z label=SessionStart
61
+ === 2026-09-08T13:49:52.433Z label=UserPromptSubmit
62
+ ```
63
+ The TUI sat fully open and idle for ~47s with the hook log EMPTY. **A Codex citizen is born
64
+ when it is first spoken to, not when its window opens** — the same shape as Copilot, and the
65
+ same trap `docs/adding-a-harness.md` §1(c) names ("a lifecycle event's NAME does not tell you
66
+ when it fires").
67
+ - **[host]** There is a **hook trust gate** in front of all of this, and it is interactive.
68
+ A first launch with three new hook entries stopped at a full-screen prompt:
69
+ ```
70
+ Hooks need review
71
+ 3 hooks are new or changed.
72
+ Hooks can run outside the sandbox after you trust them.
73
+ › 1. Review hooks
74
+ 2. Trust all and continue
75
+ 3. Continue without trusting (hooks won't run)
76
+ ```
77
+ Choosing (2) wrote per-handler trust state back into the same `config.toml`:
78
+ ```
79
+ [hooks.state."<CODEX_HOME>/config.toml:session_start:0:0"]
80
+ trusted_hash = "sha256:679495a037c2aaa8bbbe9b304c5cdeb256a3b56a30575867ca8b60bb766257d1"
81
+ ```
82
+ Until an operator answers that prompt, **a USER-level birth hook does not run**. This has no
83
+ analogue on any shipped backend.
84
+ `[source]` the trust decision itself: `hook_trust_status` returns `Managed` for a managed
85
+ handler and only consults `trusted_hash` for a non-managed one
86
+ (`hooks/src/engine/discovery.rs:794-819`); `hook_enabled` is
87
+ `is_builtin || is_managed || …` (`:813-815`); managed requirement handlers enter with
88
+ `is_managed: true` (`:208-240`), and the enabled/trust state is resolved per handler at
89
+ `:676-733`. The CLI bypass and its warning are at `core/src/config/mod.rs:3253-3260`.
90
+ **Corrected 2026-09-08 after independent audit (terra): the earlier draft cited
91
+ `discovery.rs:84-114` / `:339-343`, which are the policy struct and the `hooks.json` path —
92
+ they do not carry the claim.**
93
+
94
+ **[host] The escape is not a hypothesis any more — it was run, and it works.** See
95
+ **S1b-A** below. A hook declared in the managed `/etc/codex/config.toml` layer runs with **no
96
+ trust prompt at all**, in both the embedded and the app-server-attached mode. The price is
97
+ named there and it is the real constraint: that path is `/etc`, i.e. **root**, not something
98
+ an operator-level `entwurf setup` can write.
99
+ - **[source]** There is **no filesystem-wake vocabulary**: `watchPaths`, `FileChanged` and
100
+ `asyncRewake` appear nowhere in the Rust workspace except in a Claude-hooks IMPORTER, which
101
+ SKIPS any hook carrying them (`external-agent-migration/src/hooks_cla.rs:158-161`). Codex
102
+ does not have Claude's doorbell and does not pretend to.
103
+ - **[source]** `Stop` output is `{decision?: "block", reason?}` (`hooks/src/schema.rs:455-464`).
104
+ That is a turn-boundary continuation, exactly like Copilot's `agentStop`, and — per
105
+ `docs/adding-a-harness.md` step 7(c) — **must not be used as an idle-wake substitute**.
106
+
107
+ ## M2 — Launch form and stdin envelope
108
+
109
+ - **[source]** Hooks are declared in TOML under a `[hooks]` table keyed by the event's
110
+ PascalCase name, each entry a matcher group holding handlers
111
+ (`config/src/hook_config.rs:36-61`, `:153-159`). A `hooks.json` beside the config folder is
112
+ the second accepted source (`hooks/src/engine/discovery.rs:339-343`); both converge on one
113
+ trust identity (`:767`).
114
+ - **[source]** Handler kinds are a tagged enum: `command`, **`mcp_tool`**, `prompt`, `agent`
115
+ (`config/src/hook_config.rs:161-200`). The `mcp_tool` kind takes `{server, tool, input,
116
+ timeout, statusMessage}` — a hook can call an MCP tool DIRECTLY, with no spawned process.
117
+ - **[host]** The declaration that actually loaded:
118
+ ```toml
119
+ [hooks]
120
+ [[hooks.SessionStart]]
121
+ [[hooks.SessionStart.hooks]]
122
+ type = "command"
123
+ command = "<scratch>/hook-probe.sh SessionStart"
124
+ timeout = 30
125
+ ```
126
+ - **[source]** `command` is a SHELL STRING, never an exec-form argv — and that, not the
127
+ particular shell, is the load-bearing fact. `HookHandlerConfig::Command` carries one
128
+ `command: String` (`config/src/hook_config.rs:161-184`) and no argv variant exists in the
129
+ enum. How it is run depends on the configured shell: `build_command`
130
+ (`hooks/src/engine/command_runner.rs:396-418`) uses `shell.program` + `shell.args` when a
131
+ program is configured, and only falls back to `default_shell_command` — `$SHELL`, fallback
132
+ `/bin/sh`, argument `-lc` (`:428-453`) — when `shell.program.is_empty()`. The turn's shell
133
+ can supply that program (`core/src/session/mod.rs:4666-4674`). **Corrected 2026-09-08 after
134
+ independent audit (terra): the earlier draft claimed `$SHELL -lc` unconditionally. It is the
135
+ DEFAULT path, not the only one.** The conclusion is unchanged and does not depend on the
136
+ correction: **codex offers no exec-form hook launcher at all**, so entwurf's Hard Rule 14
137
+ ("no shell-form fallback", derived from Claude's exec manifest) has no counterpart to bind
138
+ to here; a codex unit's provenance story has to be built on something else.
139
+ - **[host]** SessionStart stdin envelope, verbatim (secrets redacted nowhere — there are none
140
+ in it):
141
+ ```json
142
+ {"session_id":"01a08147-fbad-78d2-b266-e058f322125e",
143
+ "transcript_path":"<CODEX_HOME>/sessions/2026/09/08/rollout-2026-09-08T22-49-33-01a08147-fbad-78d2-b266-e058f322125e.jsonl",
144
+ "cwd":"/home/junghan/repos/gh/entwurf",
145
+ "hook_event_name":"SessionStart",
146
+ "model":"gpt-6-astra",
147
+ "permission_mode":"default",
148
+ "source":"startup"}
149
+ ```
150
+ **The thread id IS on the wire** (`session_id`), and so is an absolute transcript path — the
151
+ two fields a birth payload needs, with nothing to guess. `[source]` the wire struct is
152
+ `hooks/src/schema.rs:499-524`.
153
+ - **[host]** `UserPromptSubmit` adds `turn_id` and the raw `prompt`; `SubagentStart` adds
154
+ `agent_id` + `agent_type` (see M8).
155
+
156
+ ## M3 — Config writer
157
+
158
+ - **[host]** `codex mcp add <NAME> -- <COMMAND...>` writes `$CODEX_HOME/config.toml`:
159
+ ```
160
+ $ CODEX_HOME=<scratch> codex mcp add entwurf-bridge -- <scratch>/mcp-probe.sh
161
+ Added global MCP server 'entwurf-bridge'.
162
+ $ cat <scratch>/config.toml
163
+ [mcp_servers.entwurf-bridge]
164
+ command = "<scratch>/mcp-probe.sh"
165
+ ```
166
+ - **[host]** The writer is **table-preserving**: an existing `[hooks]` table with three matcher
167
+ groups survived the add byte-intact, and the new table was appended. So the file is a single
168
+ shared TOML surface — hooks, MCP servers, trust state and project trust all live in it. An
169
+ entwurf writer here owns keys inside a file the operator also hand-edits, and cannot use the
170
+ "own a whole file" shape Copilot's `mcp-config.json` allowed.
171
+ - **[host]** One refusal worth knowing before writing a gate: with `CODEX_HOME` under `/tmp`,
172
+ codex prints `WARNING: proceeding, even though we could not create PATH aliases: Refusing to
173
+ create helper binaries under temporary dir "/tmp"`. All measurement below therefore used a
174
+ scratch `CODEX_HOME` under `$HOME/.cache/`, deleted afterwards.
175
+ - **[host]** Other writer surfaces: `-c key=value` CLI overrides, and `--enable/--disable
176
+ <FEATURE>` as sugar for `-c features.<name>=true|false` (`codex mcp --help`). **`-c` is not
177
+ free** — see M-B gotcha 2.
178
+
179
+ ## M4 — Statusline / visible identity, and the receive surface
180
+
181
+ **Statusline.** `[source]` Codex's status line is a CLOSED ENUM of items
182
+ (`tui/src/bottom_pane/status_line_setup.rs:56-155`), configured by the `/statusline` slash
183
+ command (`tui/src/slash_command.rs:59`, `:118`); default is
184
+ `["model-with-reasoning","current-dir"]` (`tui/src/chatwidget.rs:502`). **There is no
185
+ `statusLine.command` and no custom-text segment** — Copilot's "is the command executable"
186
+ predicate does not exist here, exactly as it did not for omp.
187
+
188
+ Two enum members are candidate carriers, and both are weak:
189
+
190
+ - `thread-id` (alias `session-id`) renders the full thread UUID — the CODEX id, not a garden id.
191
+ - `thread-title` renders `thread_name`, falling back to the thread id when unset
192
+ (`tui/src/chatwidget/status_surfaces.rs:776-784`). `[source]` the name is settable over the
193
+ app-server as `thread/name/set` (`app-server-protocol/src/protocol/common.rs:566`).
194
+ **[host]** but the vendor AUTO-TITLES: after the first turn the TUI printed
195
+ `Session renamed to List every tool you have whose name`. A garden id parked in the thread
196
+ name is therefore contested by the vendor's own titler. **[hypothesis]** it would be
197
+ overwritten on the next auto-title; not yet measured.
198
+
199
+ *(The paragraph above is the pre-Step-1b reading and its `[hypothesis]` was **wrong**. The
200
+ auto-titler does NOT overwrite an explicitly set name: it is guarded on the thread having no
201
+ name at all, and an explicit set wins in both orderings — measured in **S1b-B**. Clause 4 is
202
+ CLOSED for codex, with the two costs named there. The paragraph is kept because the hypothesis
203
+ it carried is what S1b-B was run to settle.)*
204
+
205
+ **Receive.** `[source]` the app-server JSON-RPC surface still carries `turn/start`,
206
+ `turn/steer`, `thread/inject_items`, `thread/status/changed` — and now also a queue family
207
+ (`thread/queue/add|start|list|…`) that did not exist at 0.136. No hook or extension wake
208
+ surface was added (see M1). The measured route is unchanged and green: **M-B** below.
209
+
210
+ ## M5 — Parent process topology (the step-6 join)
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.**
215
+
216
+ **Embedded mode** (a standalone `codex` TUI with its own `CODEX_HOME`, no app-server):
217
+
218
+ ```
219
+ # hook
220
+ self_pid=4583 ppid=3570
221
+ pid=3570 comm=(codex) cmdline=.../vendor/aarch64-unknown-linux-musl/bin/codex
222
+ # MCP child (the same run)
223
+ pid=4167 ppid=3570 cmd=node ... mcp/entwurf-bridge/src/index.ts
224
+ ```
225
+ `hook.ppid == mcp.ppid == 3570 == the TUI process.` The join key exists and is per-window.
226
+
227
+ **App-server-attached mode** (a plain `codex` auto-attached to `codex app-server --listen` —
228
+ i.e. the ONLY mode in which the measured delivery rail works):
229
+
230
+ ```
231
+ # hook, on a thread woken through turn/start
232
+ self_pid=11749 ppid=10664
233
+ pid=10664 comm=(codex) cmdline=.../bin/codex app-server --listen unix://<CODEX_HOME>/app-server-control/app-server-control.sock
234
+ pid=10649 node .../bin/codex.js app-server --listen ...
235
+ # the visible TUI for that same thread was pid 10833 — NOT in this chain
236
+ ```
237
+ and, on the operator's real home with its pre-existing `entwurf-bridge` entry:
238
+ ```
239
+ pid=1733 (entwurf-bridge) ppid=1393 # 1393 = codex app-server --listen
240
+ pid=1616 # = the plain codex TUI. Not the parent.
241
+ ```
242
+
243
+ So in the delivery-capable mode the join still holds — `hook.ppid == mcp.ppid` — but both
244
+ resolve to the app-server.
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`.
254
+
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.
260
+
261
+ **[host]** A second consequence, measured on the same two runs: the hook's environment is the
262
+ HOST process's snapshot, so it changes with the mode.
263
+ - Embedded: the TUI was launched with a synthetic `PI_SESSION_ID=SYNTH-PI-GID`, and the hook
264
+ saw `env_pi=PI_AGENT_ID=SYNTH-PI-AGENT ... PI_SESSION_ID=SYNTH-PI-GID`.
265
+ - App-server-attached: the same TUI env was set, and the hook saw **no `PI_SESSION_ID` at
266
+ all** — only the app-server's own environment.
267
+
268
+ Two things follow. (a) The foreign-identity-carrier hazard of step 6 is REAL on codex in
269
+ embedded mode: a codex opened from a pi citizen's shell hands `PI_SESSION_ID`/`PI_AGENT_ID`
270
+ straight to its hook, and the MCP bridge reads that pair before trying a native marker.
271
+ `[source]` codex does not strip them — `NON_INHERITABLE_ENV_VARS` is five OpenAI/Node token
272
+ names (`protocol/src/shell_environment.rs:14-21`), and the default shell-env policy is
273
+ `inherit: All` with `ignore_default_excludes: true`
274
+ (`protocol/src/config_types.rs:213-214`, `:264-265`). (b) In app-server mode **no launch-time
275
+ env of the visible window reaches the hook at all**, so any design that plants a provenance
276
+ token in the launcher's environment is dead there before it starts.
277
+
278
+ ## M6 — Environment vocabulary
279
+
280
+ - **[source]** Codex's own names: `CODEX_HOME`, `CODEX_THREAD_ID`, `CODEX_SESSION_ID`,
281
+ `CODEX_API_KEY`, `CODEX_ACCESS_TOKEN`, `CODEX_SANDBOX*`, `CODEX_PERMISSION_PROFILE`,
282
+ `CODEX_EXEC_SERVER_*`, `CODEX_APPS_*`, `CODEX_SQLITE_HOME`, … (241 `CODEX_HOME` references
283
+ alone; `protocol/src/shell_environment.rs:6-11` for the identity three).
284
+ - **[source]** **No collision with any name entwurf owns.** `grep -rn '"PI_SESSION_ID"|
285
+ "PI_AGENT_ID"|PI_CODING_AGENT_DIR|ENTWURF' --include=*.rs` over the whole workspace returns
286
+ ZERO hits. Codex is not a pi fork, so the #87 B1 fork-collision failure mode does not apply.
287
+ - **[host]** What a hook actually sees: `CODEX_HOME`, `CODEX_MANAGED_BY_PNPM=1`,
288
+ `CODEX_MANAGED_PACKAGE_ROOT=<pnpm global>/@openai/codex`. Note that **`CODEX_THREAD_ID` is
289
+ NOT in the hook environment** — `[source]` it is inserted only by `create_env`/`populate_env`
290
+ for shell-tool commands (`protocol/src/shell_environment.rs:150-152`). The hook learns the
291
+ thread from stdin (M2), not from env.
292
+
293
+ ## M7 — MCP tool-name dialect (a fourth spelling)
294
+
295
+ - **[source]** The model-facing name is `mcp__` + sanitized server name + `__` + sanitized tool
296
+ name. Prefix constant `LEGACY_MCP_TOOL_NAME_PREFIX = "mcp__"` and delimiter
297
+ `MCP_TOOL_NAME_DELIMITER = "__"` (`core/src/tools/handlers/mcp.rs:46-47`,
298
+ `codex-mcp/src/tools.rs:22`, `:225`); the namespace IS the server name
299
+ (`codex-mcp/src/rmcp_client.rs:821`); prefixing is on unless the `NonPrefixedMcpToolNames`
300
+ feature is enabled (`core/src/config/mod.rs:1793-1796`).
301
+ - **[source]** The sanitizer maps every char outside `[A-Za-z0-9_]` to `_` and preserves case
302
+ and DIGITS (`codex-mcp/src/mcp/mod.rs:544-559`). So `entwurf-bridge` → `entwurf_bridge`,
303
+ while `entwurf_v2` survives intact — unlike omp, whose `[a-z_]` charset eats the `2`.
304
+ - **[host]** LIVE oracle, one turn in a real TUI with the bridge registered. Asked for the
305
+ verbatim names, the model printed:
306
+ ```
307
+ mcp__entwurf_bridge__entwurf_fresh_call
308
+ mcp__entwurf_bridge__entwurf_inbox_read
309
+ mcp__entwurf_bridge__entwurf_peers
310
+ mcp__entwurf_bridge__entwurf_register_native
311
+ mcp__entwurf_bridge__entwurf_resume_call
312
+ mcp__entwurf_bridge__entwurf_self
313
+ mcp__entwurf_bridge__entwurf_v2
314
+ ```
315
+ **The callback spelling for codex is `mcp__entwurf_bridge__entwurf_v2`** — hyphen to
316
+ underscore, digit kept. Four harnesses, four spellings; source and live agree exactly.
317
+ - **[host]** Invocation form is the ordinary function-call surface — the model listed and could
318
+ call these as tools, with no `xd://`-style virtual-device layer to disarm. No non-default
319
+ setting was needed for the tools to be visible.
320
+
321
+ ## M8 — §3.5 citizen scope
322
+
323
+ **Answered, and the predicate is free.** `[source]` a spawned agent does not raise
324
+ `SessionStart` at all — it raises a DIFFERENT event, `SubagentStart`, chosen by
325
+ `StartHookTarget` on `SessionSource::SubAgent(SubAgentSource::ThreadSpawn{..})`
326
+ (`core/src/hook_runtime.rs:129-147`, `hooks/src/events/session_start.rs:52-74`); every other
327
+ subagent source returns before dispatch (`core/src/hook_runtime.rs:146`).
328
+
329
+ **[host]** One live session, one `spawn_agent` call, both hooks armed:
330
+ ```
331
+ === 2026-09-08T13:49:52.380Z label=SessionStart # 1 (one) for the whole session
332
+ === 2026-09-08T13:49:52.433Z label=UserPromptSubmit
333
+ === 2026-09-08T13:51:14.531Z label=UserPromptSubmit
334
+ === 2026-09-08T13:51:20.275Z label=SubagentStart
335
+ $ grep -c "label=SessionStart" measure.log
336
+ 1
337
+ ```
338
+ and the `SubagentStart` envelope keeps the PARENT's thread id while naming the child
339
+ separately:
340
+ ```json
341
+ {"session_id":"01a08147-fbad-78d2-b266-e058f322125e", // parent
342
+ "agent_id":"01a08149-959a-7113-9d35-1845ab19a17f", // child
343
+ "agent_type":"default", "hook_event_name":"SubagentStart", ...}
344
+ ```
345
+ So the top-level predicate is **"the event name is `SessionStart`"** — vendor-authoritative,
346
+ requiring no `mode === "tui"`-style heuristic and no cwd/pid inference. This is the cheapest
347
+ §3.5 answer of any harness measured so far.
348
+
349
+ ## M-B — raw delivery probe, re-run at 0.153.4
350
+
351
+ The 0.136.0 result reproduces **unchanged**. `scripts/raw-async-delivery/codex-local-appserver.sh`
352
+ and `raw-codex-ws-turn-start.py` were run as shipped; neither script needed an edit.
353
+
354
+ ```
355
+ $ codex app-server --listen unix://$HOME/.codex/app-server-control/app-server-control.sock &
356
+ $ codex # plain launch, no -c: auto-attached
357
+ 2026-09-08T13:47:54.633Z
358
+ $ ./raw-codex-ws-turn-start.py "$HOME/.codex/app-server-control/app-server-control.sock" \
359
+ 01a08145-ce6d-77d0-b7a4-cfb57c608282 \
360
+ "raw-probe 0.153.4: reply with exactly the token CODEX-RAW-0153-OK and nothing else"
361
+ {"ok": true, "threadId": "01a08145-ce6d-77d0-b7a4-cfb57c608282",
362
+ "turnId": "01a08146-7c06-7b83-9e65-32e3814ee490",
363
+ "status_seen": ["active", "active", "idle"]}
364
+ EXIT=0
365
+ 2026-09-08T13:47:58.595Z
366
+ ```
367
+ and the visible TUI, with zero typing into it:
368
+ ```
369
+ › raw-probe 0.153.4: reply with exactly the token CODEX-RAW-0153-OK
370
+ and nothing else
371
+
372
+ • CODEX-RAW-0153-OK
373
+ ```
374
+ Full message injection (not a doorbell), model reply in the same native session, `active` →
375
+ `idle` completion observation on the same socket. D6 with D7 observation, reproduced.
376
+
377
+ Reproduced a second time on a scratch `CODEX_HOME` with its own app-server, twice
378
+ (`TOPO-OK`, `TOPO2-OK`), which is where the M5 app-server topology was taken.
379
+
380
+ **One script improvement is owed, and it is not a protocol change.** The shipped README tells
381
+ you to read the threadId from the newest `rollout-*.jsonl`. At 0.153.4 the rollout is
382
+ MATERIALIZED LAZILY — for the first ~40 seconds after launch the newest rollout on this host
383
+ was a file from 2026-08-06, and the live thread had no file at all. The app-server answers the
384
+ question directly:
385
+ ```
386
+ $ <ws-client> thread/loaded/list {}
387
+ {"id":2,"result":{"data":["01a08145-ce6d-77d0-b7a4-cfb57c608282"],"nextCursor":null}}
388
+ ```
389
+ `thread/loaded/list` is the correct threadId source; rollout scraping is a fallback that races
390
+ the first turn. Recorded here rather than changed in the shipped script, because that script is
391
+ the archived 0.136 probe and B was asked to re-run it as shipped.
392
+
393
+ ### Gotchas — the 0.136 four, re-checked
394
+
395
+ 1. **Socket dir 0700, owner-owned** — unchanged; `~/.codex/app-server-control/` is created 0700
396
+ by codex itself **[host]**.
397
+ 2. **`-c` overrides disable auto-attach** — not re-measured at 0.153.4. **[hypothesis]** still
398
+ true; the plain launch used here carried no `-c` precisely because of it.
399
+ 3. **Per-folder trust is orthogonal to addressability** — **[host]** a first launch in an
400
+ untrusted cwd shows `Do you trust the contents of this directory?` and, separately, the hook
401
+ trust prompt of M1. Neither blocked `turn/start` addressability.
402
+ 4. **WS transport, no auth on the UDS** — **[host]** unchanged: the shipped python client's
403
+ handshake returned `HTTP/1.1 101 Switching Protocols` with no token.
404
+
405
+ ## S1b-A — the hook-trust escape, RUN (2026-09-08)
406
+
407
+ M1 left "is there a non-interactive path?" open. It is now closed, in the affirmative, with a
408
+ constraint that matters more than the answer.
409
+
410
+ **Method note first, because it is the part that must not be copied carelessly.** `/etc` was
411
+ never written. Each run used a user namespace carrying an overlayfs whose `lowerdir` is the
412
+ real `/etc` and whose `upperdir` supplied only `codex/config.toml`:
413
+
414
+ ```
415
+ unshare --map-root-user --mount bash -c \
416
+ 'mount -t overlay overlay -o lowerdir=/etc,upperdir=<scratch>/etc-upper,workdir=<scratch>/etc-work /etc \
417
+ && exec env CODEX_HOME=<scratch>/home codex'
418
+ ```
419
+ Before and after every run, `ls /etc/codex` on the host is `No such file or directory`.
420
+
421
+ **A1 — a managed hook runs with NO trust prompt.** `[host]` The hook was declared in the
422
+ overlay's `/etc/codex/config.toml`, which is the `System` config layer and therefore
423
+ `is_managed: true` (`hooks/src/engine/discovery.rs:826`). The TUI showed the ordinary
424
+ directory-trust question and **never showed the "Hooks need review" screen**. After one turn:
425
+
426
+ ```
427
+ === 2026-09-08T14:17:32.016Z MANAGED-HOOK-FIRED label=SessionStart-MANAGED pid=42169 ppid=41459
428
+ ```
429
+
430
+ **A2 — `--dangerously-bypass-hook-trust` also works, and it costs the delivery rail.** `[host]`
431
+ A USER-level hook, no trust prompt, a banner instead:
432
+
433
+ ```
434
+ ⚠ `--dangerously-bypass-hook-trust` is enabled. Enabled hooks may run without review for this invocation.
435
+ === 2026-09-08T14:18:46.837Z USER-HOOK-FIRED label=SessionStart-USER pid=43308 ppid=42714
436
+ ```
437
+ But an app-server was listening on that `CODEX_HOME`'s default control socket for the whole
438
+ run, and both before and after the turn:
439
+ ```
440
+ thread/loaded/list -> {"id": 2, "result": {"data": [], "nextCursor": null}}
441
+ ```
442
+ **The flag is a loader override, so auto-attach never happens and the session is unaddressable.**
443
+ This is the archived 0.136 gotcha 2 measured at 0.153.4 on the flag that matters: for entwurf,
444
+ `--dangerously-bypass-hook-trust` and the delivery rail are mutually exclusive.
445
+
446
+ **A3 — the managed path does NOT have that problem.** `[host]` Same overlay, plus an app-server
447
+ on the scratch `CODEX_HOME`'s default control socket. A plain `codex` (no flags) attached, and
448
+ a raw `turn/start` woke it:
449
+ ```
450
+ thread/loaded/list -> {"data": ["01a08163-5040-7830-bf24-dcba2e3b47c4"]}
451
+ {"ok": true, "threadId": "01a08163-5040-…", "turnId": "01a08163-a403-7752-9a96-31484a5bfca5",
452
+ "status_seen": ["active","active","idle"]}
453
+ • A3-OK # visible TUI, zero typing
454
+ === 2026-09-08T14:19:46.479Z MANAGED-HOOK-FIRED label=SessionStart-MANAGED pid=44298 ppid=43784
455
+ ```
456
+ Managed hook + auto-attach + raw wake, all three at once.
457
+
458
+ **What this closes and what it does not.** It closes "can a codex birth hook run without an
459
+ operator answering a prompt" — yes, through the managed layer, with the delivery rail intact.
460
+ It does NOT make that an installer capability: `/etc/codex/` is **root-owned**, so
461
+ `entwurf setup` running as the operator cannot write it, and Hard Rule 17 ("setup composes what
462
+ the operator already chose") points the same way. The honest shape is a **root-level operator
463
+ step**, comparable in kind to a system service install — not a per-user unit. Nothing here was
464
+ tested with `/etc/codex` genuinely present on a host; the overlay proves codex's behaviour, not
465
+ an installer's.
466
+
467
+ ## S1b-B — clause 4's carrier, RUN (2026-09-08)
468
+
469
+ M4 left `thread-title` as a candidate the vendor might auto-title over. **It survives, in both
470
+ orderings, and the source says why.**
471
+
472
+ `[source]` The automatic titler is guarded on the thread having no name at all:
473
+ `tui/src/app/thread_routing.rs:1841` — `if self.chat_widget.thread_name().is_none() && …`. A
474
+ named thread never enters the branch that generates a title. The explicit setter
475
+ (`thread/name/set` → `thread_set_name`, `app-server/src/request_processors/thread_processor.rs:639-658`)
476
+ carries no such guard.
477
+
478
+ `[host]` With `[tui] status_line = ["thread-title", "model", "current-dir"]`:
479
+
480
+ ```
481
+ # unset -> falls back to the thread UUID, as source predicts
482
+ 01a0815b-d30d-7b63-a257-9011682f0e3a · gpt-6-astra · ~/repos/gh/ent…
483
+
484
+ # thread/name/set {"threadId":"01a0815b-…","name":"entwurf 20260908T224329-2680e2"} -> {"result":{}}
485
+ entwurf 20260908T224329-2680e2 · gpt-6-astra · ~/repos/gh/entwurf ·…
486
+
487
+ # first turn (TITLE-PROBE-OK) -> name UNCHANGED
488
+ # second turn (TITLE-PROBE-2) -> name UNCHANGED
489
+ entwurf 20260908T224329-2680e2 · gpt-6-astra · ~/repos/gh/entwurf ·…
490
+ ```
491
+
492
+ And the ordering race, which is the question a birth-at-first-turn hook actually raises — the
493
+ auto-titler wins first, then entwurf sets the name:
494
+
495
+ ```
496
+ # fresh thread, no name, one turn (RACE-1): the vendor claims the slot
497
+ Reply with RACE-1 · gpt-6-astra · ~/repos/gh/entwurf · ← for agents
498
+ # thread/name/set {"name":"entwurf 20260908T999999-race02"} -> {"result":{}}
499
+ entwurf 20260908T999999-race02 · gpt-6-astra · ~/repos/gh/entwurf ·…
500
+ # one more turn (RACE-2) -> still ours
501
+ entwurf 20260908T999999-race02 · gpt-6-astra · ~/repos/gh/entwurf ·…
502
+ ```
503
+
504
+ **So clause 4 has a working carrier and the ordering does not matter** — a birth payload may set
505
+ the name after the first turn and still win. Two costs to carry into step 4/10, neither
506
+ hypothetical: the carrier is reached over the **app-server**, not from the hook process, so it
507
+ is only available in the auto-attach mode; and `[tui] status_line` is an operator config key,
508
+ so it needs a writer that owns exactly that key (the `scripts/omp-config-xdev.py` shape).
509
+
510
+ Not established: whether any OTHER persistent operator-visible surface exists. `thread-title`
511
+ is the only enum member that can hold arbitrary text (`status_line_setup.rs:56-155`), and no
512
+ non-statusline persistent surface was found — but absence of a find is not a proof of absence.
513
+
514
+ ## S1b-C — two citizens, one app-server (2026-09-08)
515
+
516
+ The M5 sharing claim, measured instead of inferred. Two plain `codex` TUIs auto-attached to one
517
+ `codex app-server --listen`, each with its own thread and its own visible window, each woken by
518
+ its own `turn/start`. `[host]`:
519
+
520
+ ```
521
+ app-server pid = 39226
522
+ visible TUI pids = 39410, 39956 # two separate windows
523
+
524
+ === label=MCP-child self_pid=39797 ppid=39226
525
+ === label=MCP-child self_pid=40074 ppid=39226
526
+ === label=SessionStart self_pid=40507 ppid=39226 session_id=01a0815f-c28e-7080-894c-05200f91ef6b
527
+ === label=UserPromptSubmit self_pid=40530 ppid=39226 session_id=01a0815f-c28e-7080-894c-05200f91ef6b
528
+ === label=SessionStart self_pid=40632 ppid=39226 session_id=01a0815f-e2d1-7f30-930f-da4c1728906d
529
+ === label=UserPromptSubmit self_pid=40655 ppid=39226 session_id=01a0815f-e2d1-7f30-930f-da4c1728906d
530
+ ```
531
+
532
+ Two DIFFERENT `session_id`s, two different windows, **one `ppid` for every hook and every MCP
533
+ child of both**, and neither TUI pid appears anywhere in the chain. The parent-pid join key
534
+ cannot distinguish these two citizens. `[host]`, not `[source]`, and not an inference from one.
535
+
536
+ (Timing incidentally re-confirms M1: the MCP children start at attach — 14:15:31 / 14:15:40 —
537
+ while `SessionStart` waits for the first turn — 14:16:01 / 14:16:05.)
538
+
539
+ ## S1b-D — the join key, found on the wire (2026-09-08)
540
+
541
+ S1b-C proved the parent-pid join cannot separate two codex citizens. It did not prove there is
542
+ no key at all — and there is one. Three reads, no new unit, no marker written.
543
+
544
+ **D1 — nothing in the MCP child's process image distinguishes the thread. `[host]`** Two live
545
+ threads on one app-server, each with its OWN bridge child (`51517` and `51767` — separate pids,
546
+ so the children are not shared):
547
+
548
+ ```
549
+ cmdline: node --experimental-strip-types … mcp/entwurf-bridge/src/index.ts # both, identical
550
+ cwd: /home/junghan/repos/gh/entwurf # both, identical
551
+ environ: diff -> no output # BYTE-IDENTICAL
552
+ env var names (10, values withheld):
553
+ HOME LANG LC_ALL LOGNAME PATH PWD SHELL SHLVL TERM USER
554
+ ```
555
+ **[host]** What is measured: there is **no `CODEX_THREAD_ID`, no `CODEX_SESSION_ID`, no `CODEX_*`
556
+ at all** in an MCP child, and no `PI_*` either — even though this host's hook processes DID see a
557
+ planted `PI_SESSION_ID` (M5). The child env is 10 names wide against a hook env that carried the
558
+ launching shell's whole environment. So on the evidence, **the foreign-identity-carrier hazard
559
+ of M5 does not reach the bridge on codex.**
560
+
561
+ **[hypothesis]** that this is `inherit: Core` rather than `All`. It is only *consistent* with
562
+ `UNIX_CORE_ENV_VARS` (`protocol/src/shell_environment.rs:162-165`) and does not match it: the
563
+ child has `PWD`, `SHLVL` and `TERM`, which that list does not contain (a `bash` wrapper sat in
564
+ this probe's chain and could add all three), and it lacks `TMPDIR`/`TEMP`/`TMP`/`LC_CTYPE`, which
565
+ may simply be unset in the parent. **Nobody read the MCP child's env-composition site in source.**
566
+ The negative above stands on the observed absence; the mechanism behind it does not.
567
+
568
+ **D2 — the identity rides the CALL. `[host]`** One real `tools/call`, captured on the child's
569
+ own stdin:
570
+
571
+ ```json
572
+ {"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"_meta":{
573
+ "callId":"exec-1ef58158-c495-4225-85cc-e7e15075ff78",
574
+ "x-codex-turn-metadata":{
575
+ "session_id":"01a0816b-976f-7061-96e4-0d7f9ae53838",
576
+ "thread_id":"01a0816b-976f-7061-96e4-0d7f9ae53838",
577
+ "turn_id":"01a0816c-f799-7431-a41a-a95e94a9b7f9",
578
+ "turn_started_at_unix_ms":1788877797276,
579
+ "workspaces":{"/home/junghan/repos/gh/entwurf":{…,"latest_git_commit_hash":"e2711ae…"}},
580
+ "thread_source":"user","sandbox":"seccomp","sandbox_mode":"workspace-write",
581
+ "model":"gpt-6-astra","reasoning_effort":"low", …},
582
+ "threadId":"01a0816b-976f-7061-96e4-0d7f9ae53838",
583
+ "itemId":"ctc_0f30971853352535016aa01bea708487d0ad36b74408dc1e98",
584
+ "progressToken":1},
585
+ "name":"entwurf_peers","arguments":{}}}
586
+ ```
587
+ `[source]` Every key above is a member of `_meta` (none is a top-level `params` member), and
588
+ three of the four are traced:
589
+ - `callId` and `x-codex-turn-metadata` — `core/src/mcp_tool_call.rs:1238-1263`
590
+ (`build_mcp_tool_call_request_meta`), the metadata object itself built by
591
+ `core/src/turn_metadata.rs:230-…` (`current_meta_value_for_mcp_request`).
592
+ - `threadId` and `itemId` — `core/src/mcp_tool_call.rs:1328-1349`
593
+ (`with_mcp_tool_call_ids_meta` inserts `MCP_TOOL_THREAD_ID_META_KEY` unconditionally and
594
+ `MCP_TOOL_ITEM_ID_META_KEY` only when an originating item exists; the two key constants are
595
+ the literals `"threadId"` / `"itemId"` at `:1183-1184`), called for the MCP tool call at
596
+ `:506-516`. **Corrected 2026-09-08 after independent audit (terra).** The first draft said
597
+ these were capture-only; that was wrong, and the reason it was wrong is worth keeping: the
598
+ keys are inserted through CONSTANTS, so a grep for the string `"threadId"` in the call path
599
+ finds nothing. A literal search is not a source search.
600
+
601
+ **Honest limit, now one key instead of three:** `progressToken` has no located insertion site.
602
+ The vendor tree carries the constant `MCP_PROGRESS_TOKEN_META_KEY`
603
+ (`rmcp-client/src/elicitation_client_service.rs:32`) and downstream readers, but nothing that
604
+ puts it on an outgoing `tools/call`. That key rests on the wire capture alone.
605
+
606
+ The vendor also tests it: `core/src/mcp_tool_call_tests.rs:1753-1791`
607
+ (`mcp_tool_call_ids_are_added_to_request_meta`) calls `with_mcp_tool_call_ids_meta` directly and
608
+ pins three cases — a stale `threadId`/`itemId` is OVERWRITTEN with the live pair, a `None` prior
609
+ meta becomes `{"threadId": …}` with no `itemId`, and a non-object meta passes through unchanged.
610
+
611
+ *(Finding that test took two passes, and the miss is the reusable part. `:1753-1791` was first
612
+ looked for inside `mcp_tool_call.rs`, where that line is unrelated `_meta` UI-resource
613
+ extraction — so the search concluded the test did not exist. It does, one file over. The cause is
614
+ ordinary and therefore general: **a line range cited without its file path is ambiguous across
615
+ any multi-file source**, and the two files here happen to both have content at 1753
616
+ (`mcp_tool_call.rs:1753` is `meta.and_then(|meta| {`; `mcp_tool_call_tests.rs:1753` is
617
+ `fn mcp_tool_call_ids_are_added_to_request_meta() {`). It is NOT that they share a line-number
618
+ space — the `#[cfg(test)] #[path = "mcp_tool_call_tests.rs"] mod tests;` at
619
+ `core/src/mcp_tool_call.rs:2392-2394` includes the sibling as a module and nothing more. Read
620
+ here, 2026-09-08, after that mechanism was itself asserted wrongly once. Sibling of the constants
621
+ trap two paragraphs up: counting occurrences in the file you happen to have open is not counting
622
+ them in the crate.)*
623
+
624
+ By contrast `initialize` carries no identity at all and is byte-identical between the two
625
+ children — `clientInfo` is the fixed literal `Implementation::new("codex-mcp-client",
626
+ CARGO_PKG_VERSION).with_title("Codex")` (`codex-mcp/src/rmcp_client.rs:1035-1039`):
627
+ ```json
628
+ {"method":"initialize","params":{"protocolVersion":"2025-06-18",
629
+ "capabilities":{"elicitation":{"form":{},"url":{}}},
630
+ "clientInfo":{"name":"codex-mcp-client","title":"Codex","version":"0.153.4"}}}
631
+ ```
632
+ So a codex bridge child cannot know who it serves at startup, and does not need to: **it learns
633
+ per call, which is exactly when it needs to know.**
634
+
635
+ **D3 — the hook's `session_id` and the app-server's `threadId` are the same string. `[host]`**
636
+ Two threads, hook stdin beside `thread/loaded/list`:
637
+
638
+ ```
639
+ HOOK 14:29:57.725 label=SessionStart session_id=01a0816b-976f-7061-96e4-0d7f9ae53838
640
+ HOOK 14:30:02.910 label=SessionStart session_id=01a0816b-bb92-7c81-abde-90820345d58e
641
+ thread/loaded/list -> ["01a0816b-976f-7061-96e4-0d7f9ae53838",
642
+ "01a0816b-bb92-7c81-abde-90820345d58e"]
643
+ ```
644
+ Byte-identical, both of them. So **birth, delivery and the tool-call `_meta` all name the same
645
+ identifier**, and `record.nativeSessionId = threadId` needs no mapping layer. The pre-existing
646
+ `nativeIdLabel: "threadId"` in `pi/entwurf-capabilities.json` is, on this axis, correct — which
647
+ is worth saying precisely, because its `deliveryLevel: "D6"` on the same row still is not.
648
+
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.**
654
+
655
+ One vendor fact this run also surfaced, recorded because it belongs to step 9 clause 2: the
656
+ first `tools/call` raised an interactive approval prompt —
657
+ `Allow the entwurf-bridge MCP server to run tool "entwurf_peers"?` — under the default approval
658
+ mode, and the permission surface spells the tool `entwurf-bridge.entwurf_peers`, a THIRD dialect
659
+ beside the model-facing `mcp__entwurf_bridge__entwurf_peers`. Copilot's "one tool, two dialects"
660
+ lesson repeats here. `entwurf_peers` itself answered from that session, which is the plain
661
+ external-MCP-host row of `docs/external-mcp-host.md` behaving as designed.
662
+
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.
681
+ - *(Two entries lived here until 2026-09-08 and are now **closed by measurement**, not moved:
682
+ "clause 4 has no working candidate" was retired by **S1b-B**, and "the hook trust gate has no
683
+ measured non-interactive path" by **S1b-A**. What each one costs is stated in its section.)*
684
+ - Of the MCP tool-call `_meta` observed in **S1b-D**, `callId`, `x-codex-turn-metadata`,
685
+ `threadId` and `itemId` are traced to vendor insertion sites there. **`progressToken` alone**
686
+ has no located construction site and rests on the wire capture.
687
+ - Whether `/new`, resume or fork returns `thread_name` to `None` — and therefore whether a birth
688
+ payload must re-arm the visible id — is **not measured**. Observation, not a claim (terra,
689
+ 2026-09-08).