@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.
- package/AGENTS.md +1 -1
- package/BASELINE.md +23 -5
- package/CHANGELOG.md +236 -0
- package/DELIVERY.md +59 -6
- package/README.md +40 -9
- package/VERIFY.md +9 -1
- package/docs/acp-backend-rail.md +9 -1
- package/docs/external-mcp-host.md +6 -3
- package/docs/setup-clean-host.md +93 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +3 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +6 -2
- package/package.json +2 -2
- package/pi-extensions/acp-provider.ts +1 -1
- package/pi-extensions/lib/acp/models.ts +3 -3
- package/pi-extensions/lib/acp/overlay.ts +6 -2
- package/pi-extensions/lib/meta-session.ts +25 -0
- package/run.sh +71 -26
- package/scripts/agy-bridge.sh +1 -1
- package/scripts/agy-imprint.sh +10 -7
- package/scripts/check-acp-carrier-augment.ts +6 -5
- package/scripts/check-acp-cortex.ts +41 -0
- package/scripts/check-acp-provider-surface.ts +9 -6
- package/scripts/check-copilot-birth-hook.ts +3 -1
- package/scripts/check-copilot-receive-arm.ts +169 -21
- package/scripts/check-copilot-statusline.ts +3 -1
- package/scripts/check-gate-qualification.ts +10 -9
- package/scripts/check-hook-launch-topology.ts +6 -5
- package/scripts/check-install-surface.ts +2 -1
- package/scripts/check-meta-facts.ts +6 -4
- package/scripts/check-meta-hook-session-switch.ts +4 -3
- package/scripts/check-meta-identity-consumers.ts +8 -7
- package/scripts/check-meta-receiver-marker.ts +2 -1
- package/scripts/check-omp-birth-hook.ts +3 -2
- package/scripts/check-probe-bridge-command.ts +3 -1
- package/scripts/check-setup-qualification.sh +21 -0
- package/scripts/copilot-bridge-doctor.sh +15 -5
- package/scripts/copilot-bridge-install.sh +6 -2
- package/scripts/copilot-launch.sh +16 -2
- package/scripts/copilot-receive-bridge.sh +80 -13
- package/scripts/lib/reclaim-on-exit.ts +86 -0
- package/scripts/meta-bridge-doctor.sh +56 -15
- package/scripts/meta-bridge-install.sh +13 -7
- package/scripts/meta-bridge-uninstall.sh +6 -4
- package/scripts/mutants/acp-cortex.json +26 -2
- package/scripts/mutants/copilot-receive.json +26 -0
- package/scripts/mutants/omp-birth.json +23 -6
- package/scripts/mutants/setup-verdict.json +13 -0
- package/scripts/omp-bridge-doctor.sh +110 -41
- package/scripts/omp-bridge-install.sh +6 -2
- package/scripts/omp-receive-install.sh +7 -2
- package/scripts/raw-async-delivery/README.md +10 -1
- package/scripts/raw-codex-measure/README.md +689 -0
- package/scripts/raw-codex-measure/source-audit.md +243 -0
- package/scripts/raw-macos-measure/README.md +148 -0
- package/scripts/raw-macos-measure/probe.sh +389 -0
- package/scripts/smoke-meta-async-drift.sh +7 -5
- package/scripts/smoke-meta-install-state.sh +70 -19
- package/scripts/smoke-omp-bridge-state.sh +38 -0
- 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).
|