@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,243 @@
|
|
|
1
|
+
Author: claude-opus-5 (entwurf fresh sibling, 2026-09-08) — source-layer receipts for #95 step 1.
|
|
2
|
+
These rows were written by the same reader who took the measurements in `README.md`, so they were
|
|
3
|
+
NOT self-certifying. They have since been independently audited: **terra (openai-codex), against
|
|
4
|
+
commit `87ac7ad`, 2026-09-08 — 20/20 quotations CONFIRMED, 0 CORRECTED, 0 UNVERIFIABLE**, with the
|
|
5
|
+
tag identity re-derived independently (`rust-v0.153.4^{}` =
|
|
6
|
+
`3d2ee51ca2d5db578f328aa75e20aa22c0197c9a`). The three defects that audit raised were prose
|
|
7
|
+
precision in `README.md`, not wrong vendor facts; they are folded in there and marked. Rows added
|
|
8
|
+
to this file BY that amendment are marked `[post-audit]` and carry no second reader.
|
|
9
|
+
|
|
10
|
+
Vendor: `~/repos/3rd/codex` at tag `rust-v0.153.4` (`3d2ee51`). Paths are relative to
|
|
11
|
+
`codex-rs/`. Code is the oracle.
|
|
12
|
+
|
|
13
|
+
# Item ledger
|
|
14
|
+
|
|
15
|
+
## H. Hooks — vocabulary, firing, declaration, execution
|
|
16
|
+
|
|
17
|
+
### H1. Event set is 12, not 0.136's 9
|
|
18
|
+
`hooks/src/lib.rs:23-36` — `HOOK_EVENT_NAMES: [&str; 12]`.
|
|
19
|
+
`hooks/src/lib.rs:43-52` — `HOOK_EVENT_NAMES_WITH_MATCHERS: [&str; 9]` (matcher-meaningful subset).
|
|
20
|
+
`config/src/hook_config.rs:36-61` — the same 12 as TOML keys, PascalCase via `serde(rename)`.
|
|
21
|
+
Added since the archived 0.136 note: `PermissionRequest`, `SessionEnd`, `Interrupt`.
|
|
22
|
+
|
|
23
|
+
### H2. SessionStart source is a 4-valued matcher
|
|
24
|
+
`hooks/src/events/session_start.rs:25-38` — `SessionStartSource::{Startup,Resume,Clear,Compact}`
|
|
25
|
+
with `as_str()` → `startup|resume|clear|compact`.
|
|
26
|
+
`hooks/src/events/session_start.rs:69-74` — `matcher_input()` returns that string for
|
|
27
|
+
`SessionStart`, and `agent_type` for `SubagentStart`.
|
|
28
|
+
`core/src/session/session.rs:1600-1606` — the mapping from `InitialHistory`:
|
|
29
|
+
`Resumed→Resume`, `New|Forked→Startup`, `Cleared→Clear`.
|
|
30
|
+
`core/src/session/mod.rs:3812` — compaction queues `Compact`.
|
|
31
|
+
|
|
32
|
+
### H3. Firing is turn-time, not construction-time — the decisive chain
|
|
33
|
+
1. `core/src/session/session.rs:1623` — construction only calls
|
|
34
|
+
`state.queue_pending_session_start_source(session_start_source)`.
|
|
35
|
+
2. `core/src/state/session.rs:55` — the queue is a `VecDeque<SessionStartSource>`;
|
|
36
|
+
`:348-352` push, `:355-358` pop.
|
|
37
|
+
3. `core/src/hook_runtime.rs:124-128` — `run_pending_session_start_hooks` DRAINS that queue
|
|
38
|
+
(`while let Some(..) = sess.take_pending_session_start_source().await`).
|
|
39
|
+
4. `core/src/session/turn.rs:264` and `:504` — the only two production call sites, both inside
|
|
40
|
+
turn execution. `grep -rn 'run_pending_session_start_hooks' core/src` returns no other
|
|
41
|
+
non-test caller.
|
|
42
|
+
Nothing in the construction path invokes the hook engine. `[host]` receipt in README M1.
|
|
43
|
+
|
|
44
|
+
### H4. Handler kinds and TOML shape
|
|
45
|
+
`config/src/hook_config.rs:12-17` — `HooksFile { description?, hooks: HookEventsToml }`
|
|
46
|
+
(the `hooks.json` shape).
|
|
47
|
+
`config/src/hook_config.rs:19-25` — `HooksToml { #[serde(flatten)] events, state }`
|
|
48
|
+
(the `[hooks]` in-config shape, carrying `[hooks.state]`).
|
|
49
|
+
`config/src/hook_config.rs:27-33` — `HookStateToml { enabled?, trusted_hash? }`.
|
|
50
|
+
`config/src/hook_config.rs:153-159` — `MatcherGroup { matcher?, hooks: Vec<HookHandlerConfig> }`.
|
|
51
|
+
`config/src/hook_config.rs:161-200` — `#[serde(tag="type")] HookHandlerConfig`:
|
|
52
|
+
- `command { command, commandWindows?, timeout?, async, statusMessage?, additionalContextLimit? }`
|
|
53
|
+
- `mcp_tool { server, tool, input, timeout?, statusMessage? }`
|
|
54
|
+
- `prompt {}`, `agent {}`
|
|
55
|
+
The `mcp_tool` kind is executed through `HookMcpExecutor` (`hooks/src/mcp.rs:8-24`), whose
|
|
56
|
+
result text is "interpreted using ordinary command-hook output semantics".
|
|
57
|
+
|
|
58
|
+
### H5. Command execution is shell-form, with an env REPLAY
|
|
59
|
+
`hooks/src/engine/command_runner.rs:391-411` — `build_command`: when the configured shell
|
|
60
|
+
program is empty, `default_shell_command(environment)`, then the command line as ONE arg.
|
|
61
|
+
`hooks/src/engine/command_runner.rs:428-453` — non-Windows: program from `SHELL` (fallback
|
|
62
|
+
`/bin/sh`), argument `-lc`. A LOGIN shell: the operator's profile runs first.
|
|
63
|
+
`hooks/src/engine/command_runner.rs:419-423` — `command.env_clear()` then
|
|
64
|
+
`command.envs(environment)` then the per-handler `env` then `scrub_non_inheritable_env_vars`.
|
|
65
|
+
"Replay the session snapshot instead of inheriting the live process environment" is the
|
|
66
|
+
in-source comment; the snapshot is the HOST process's, which is what makes M5's mode split
|
|
67
|
+
visible in the hook's own env.
|
|
68
|
+
There is no exec-form (`argv[]`) handler kind anywhere in `HookHandlerConfig`.
|
|
69
|
+
|
|
70
|
+
### H6. Discovery sources and the trust/managed policy
|
|
71
|
+
`hooks/src/engine/discovery.rs:146-166` — two sources merged per config layer:
|
|
72
|
+
`load_hooks_json(layer.hooks_config_folder())` and the layer's own `hooks` TOML key
|
|
73
|
+
(`:387` reads `layer.config.get("hooks")`).
|
|
74
|
+
`hooks/src/engine/discovery.rs:339-343` — the JSON path is `<config folder>/hooks.json`.
|
|
75
|
+
`config/src/state.rs:239-243` — `hooks_config_folder()` = override else `config_folder()`.
|
|
76
|
+
`hooks/src/engine/discovery.rs:81-88` — `HookDiscoveryPolicy { allow_managed_hooks_only,
|
|
77
|
+
bypass_hook_trust }`; `:105-114` reads `allow_managed_hooks_only` from layer requirements.
|
|
78
|
+
`hooks/src/engine/discovery.rs:767` — the comment binding config-TOML and hooks.json hooks to
|
|
79
|
+
one trust identity.
|
|
80
|
+
`config/src/hook_config.rs:218-250` — `ManagedHooksRequirementsToml { managed_dir,
|
|
81
|
+
windows_managed_dir, #[serde(flatten)] hooks }` + `managed_dir_for_current_platform()`.
|
|
82
|
+
|
|
83
|
+
`[post-audit]` **The coordinates that actually carry the trust claim** (terra's correction; the
|
|
84
|
+
four lines above are the policy struct and the JSON path, which do not):
|
|
85
|
+
`hooks/src/engine/discovery.rs:794-811` — `hook_trust_status`: `is_builtin → Trusted`,
|
|
86
|
+
`is_managed → Managed`, otherwise compare `trusted_hash` against the current hash
|
|
87
|
+
(`Some(eq) → Trusted`, `Some(_) → Modified`, `None → Untrusted`).
|
|
88
|
+
`hooks/src/engine/discovery.rs:813-815` — `hook_enabled(is_managed, is_builtin, state)` is
|
|
89
|
+
`is_builtin || is_managed || state.enabled != Some(false)`.
|
|
90
|
+
`hooks/src/engine/discovery.rs:817-821` — `hook_trusted_hash` reads state ONLY when `!is_managed`.
|
|
91
|
+
`hooks/src/engine/discovery.rs:676-679` — the per-handler site where those three are combined.
|
|
92
|
+
`hooks/src/engine/discovery.rs:208-240` — `append_managed_requirement_handlers` enters with
|
|
93
|
+
`is_managed: true`.
|
|
94
|
+
`core/src/config/mod.rs:3253-3260` — the CLI bypass path and its operator warning.
|
|
95
|
+
|
|
96
|
+
`[post-audit]` **Which layers are managed**: `hooks/src/engine/discovery.rs:823-839` —
|
|
97
|
+
`System` (= `/etc/codex/config.toml`, `config/src/loader/mod.rs:66`), `Mdm`,
|
|
98
|
+
`EnterpriseManaged`, and both `LegacyManagedConfigToml*` are `is_managed: true`. `User`,
|
|
99
|
+
`Project`, `PackagedDefaults` and `SessionFlags` are not. `config/src/loader/mod.rs:746-748`
|
|
100
|
+
puts the requirements file at `/etc/codex/requirements.toml`;
|
|
101
|
+
`config/src/loader/layer_io.rs:22` puts managed config at `/etc/codex/managed_config.toml`.
|
|
102
|
+
`config/src/state.rs:43-62` — `LoaderOverrides` can redirect all of those, but it is internal
|
|
103
|
+
(no CLI flag reaches it outside `cli/src/debug_sandbox.rs`).
|
|
104
|
+
**Exercised on 2026-09-08 — see `README.md` §M-A.** A hook in the managed layer ran with no
|
|
105
|
+
trust prompt; `--dangerously-bypass-hook-trust` also ran the hook but killed auto-attach.
|
|
106
|
+
|
|
107
|
+
### H7. Stop is a turn boundary, not a wake
|
|
108
|
+
`hooks/src/events/stop.rs:97-104` — `StopOutcome { should_stop, stop_reason, should_block,
|
|
109
|
+
block_reason, continuation_fragments }`.
|
|
110
|
+
`hooks/src/schema.rs:455-464` — `StopCommandOutputWire { universal, decision?, reason? }`
|
|
111
|
+
with the in-source note that Claude requires `reason` when `decision == block`.
|
|
112
|
+
Same class as Copilot's `agentStop`; `docs/adding-a-harness.md` step 7(c) forbids using it as
|
|
113
|
+
an idle-wake substitute.
|
|
114
|
+
|
|
115
|
+
### H8. No filesystem-wake vocabulary
|
|
116
|
+
`grep -rn 'watchPaths|FileChanged|asyncRewake' --include=*.rs codex-rs/` returns exactly one
|
|
117
|
+
hit: `external-agent-migration/src/hooks_cla.rs:158-161`, inside a CLAUDE-HOOKS IMPORTER, in a
|
|
118
|
+
`continue` branch that SKIPS any imported hook object carrying `asyncRewake`, `shell` or
|
|
119
|
+
`once`. Codex reads Claude's doorbell vocabulary only to refuse it.
|
|
120
|
+
|
|
121
|
+
## E. Environment
|
|
122
|
+
|
|
123
|
+
### E1. Codex's non-inheritable set is five names, none of them ours
|
|
124
|
+
`protocol/src/shell_environment.rs:14-21` — `NON_INHERITABLE_ENV_VARS` =
|
|
125
|
+
`CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN`, `NODE_REPL_AUTH_TOKEN`, `OPENAI_FEDERATION_RULE_ID`,
|
|
126
|
+
`OPENAI_IDENTITY_TOKEN_FILE`, `OPENAI_WORKLOAD_IDENTITY_CONTEXT`.
|
|
127
|
+
`:33-50` — `scrub_non_inheritable_env_vars` removes those and only those.
|
|
128
|
+
|
|
129
|
+
### E2. Default shell-env policy inherits everything
|
|
130
|
+
`protocol/src/config_types.rs:213-214` — `ShellEnvironmentPolicyInherit::All` is `#[default]`.
|
|
131
|
+
`protocol/src/config_types.rs:264-265` — the `Default` impl sets
|
|
132
|
+
`inherit: All, ignore_default_excludes: true`, so the `*KEY*`/`*SECRET*`/`*TOKEN*` filters at
|
|
133
|
+
`protocol/src/shell_environment.rs:124-131` are OFF by default.
|
|
134
|
+
Consequence: `PI_SESSION_ID` / `PI_AGENT_ID` pass through untouched. `[host]` confirmed in
|
|
135
|
+
README M5 (embedded mode).
|
|
136
|
+
|
|
137
|
+
### E3. CODEX_THREAD_ID is a shell-tool carrier, not a hook carrier
|
|
138
|
+
`protocol/src/shell_environment.rs:7` — the constant.
|
|
139
|
+
`protocol/src/shell_environment.rs:149-152` — inserted by `populate_env` only when a
|
|
140
|
+
`thread_id` argument is supplied, i.e. on the shell-environment construction path.
|
|
141
|
+
`[host]` a hook's `env | grep ^CODEX_` showed `CODEX_HOME`, `CODEX_MANAGED_BY_PNPM`,
|
|
142
|
+
`CODEX_MANAGED_PACKAGE_ROOT` and nothing else.
|
|
143
|
+
|
|
144
|
+
### E4. No entwurf/pi name collision
|
|
145
|
+
`grep -rn '"PI_SESSION_ID"|"PI_AGENT_ID"|PI_CODING_AGENT_DIR|ENTWURF' --include=*.rs codex-rs/`
|
|
146
|
+
→ 0 hits. Codex has no pi lineage, so #87's fork-collision class (step 1(6)) is inapplicable.
|
|
147
|
+
|
|
148
|
+
## T. MCP tool-name dialect
|
|
149
|
+
|
|
150
|
+
### T1. Prefix + delimiter constants
|
|
151
|
+
`core/src/tools/handlers/mcp.rs:46` — `LEGACY_MCP_TOOL_NAME_PREFIX = "mcp__"`.
|
|
152
|
+
`core/src/tools/handlers/mcp.rs:47` and `codex-mcp/src/tools.rs:225` — `MCP_TOOL_NAME_DELIMITER
|
|
153
|
+
= "__"`. (`codex-mcp/src/tools.rs:22` carries the prefix constant for the same crate.)
|
|
154
|
+
|
|
155
|
+
### T2. Namespace is the server name, prefixed unless a feature disables it
|
|
156
|
+
`codex-mcp/src/rmcp_client.rs:821` — `callable_namespace: server_name.to_string()`.
|
|
157
|
+
`codex-mcp/src/tools.rs:139-142` — `callable_namespace_with_prefix(sanitize(...), prefix &&
|
|
158
|
+
!non_prefixed_servers.contains(server))`.
|
|
159
|
+
`codex-mcp/src/tools.rs:228-234` — the prefixing function itself.
|
|
160
|
+
`core/src/config/mod.rs:1793-1796` — `prefix_mcp_tool_names()` is
|
|
161
|
+
`!features.enabled(NonPrefixedMcpToolNames) || non_prefixed_mcp_tool_servers.is_some()`.
|
|
162
|
+
So prefixing is the DEFAULT and a feature flag is required to lose it.
|
|
163
|
+
|
|
164
|
+
### T3. The sanitizer keeps digits and case
|
|
165
|
+
`codex-mcp/src/mcp/mod.rs:544-559` — every char not in `[A-Za-z0-9_]` becomes `_`; empty →
|
|
166
|
+
`"_"`. Contrast omp, whose `[a-z_]` charset deletes the `2` in `entwurf_v2`.
|
|
167
|
+
Derivation for this repo's server: `entwurf-bridge` → `entwurf_bridge`, `entwurf_v2` → itself,
|
|
168
|
+
giving `mcp__entwurf_bridge__entwurf_v2`. `[host]` the live tool list in README M7 matches this
|
|
169
|
+
derivation on all seven bridge tools.
|
|
170
|
+
|
|
171
|
+
### T4. Collision hardening exists and can mutate a name
|
|
172
|
+
`codex-mcp/src/tools.rs:152-193` — a namespace or tool name shared by two distinct raw
|
|
173
|
+
identities gets a 12-hex sha1 suffix (`callable_name_hash_suffix`, `:241-245`).
|
|
174
|
+
`codex-mcp/src/tools.rs:226` — `MAX_TOOL_NAME_LENGTH = 128`, with truncation-plus-hash at
|
|
175
|
+
`:263-285`. So the derived spelling is stable only while no second server collides on
|
|
176
|
+
`entwurf_bridge`. Not exercised.
|
|
177
|
+
|
|
178
|
+
### T5. Hook-facing tool names differ from model-facing ones
|
|
179
|
+
`core/src/tools/handlers/mcp.rs:95-97` — `hook_tool_name()` = `ensure_mcp_prefix(join_tool_name(
|
|
180
|
+
canonical))`, i.e. what `PreToolUse`/`PostToolUse` matchers compare against.
|
|
181
|
+
`core/src/tools/handlers/mcp.rs:100-109` — `join_tool_name` trims a trailing `_` from the
|
|
182
|
+
namespace and a leading `_` from the name before joining.
|
|
183
|
+
The Copilot lesson generalises: measure the string each argv/config position actually wants.
|
|
184
|
+
|
|
185
|
+
## S. §3.5 scope, statusline, app-server
|
|
186
|
+
|
|
187
|
+
### S1. Subagents raise a different event
|
|
188
|
+
`core/src/hook_runtime.rs:129-147` — the target selection. A `SubAgentSource::ThreadSpawn` with
|
|
189
|
+
source `Startup` becomes `StartHookTarget::SubagentStart{turn_id, agent_id, agent_type}`; any
|
|
190
|
+
other `SessionSource::SubAgent(_)` returns `false` WITHOUT dispatching (`:146`); everything else
|
|
191
|
+
is `StartHookTarget::SessionStart`.
|
|
192
|
+
`hooks/src/events/session_start.rs:52-74` — `event_name()` maps those to
|
|
193
|
+
`HookEventName::SubagentStart` / `::SessionStart`.
|
|
194
|
+
`hooks/src/events/session_start.rs:156-167` — the `SubagentStartCommandInput` carries
|
|
195
|
+
`session_id` (parent), `turn_id`, `agent_id`, `agent_type`.
|
|
196
|
+
`hooks/src/events/session_start.rs:271-274` — `continue:false` is honoured for `SessionStart`
|
|
197
|
+
ONLY; `SubagentStart` is context-injection-only.
|
|
198
|
+
`core/src/tools/handlers/multi_agents_spec.rs:85`, `:124` — the model-facing spawner is
|
|
199
|
+
`spawn_agent` (siblings: `send_input`, `wait_agent`, `list_agents`, `close_agent`, …).
|
|
200
|
+
`[host]` one `spawn_agent` call produced exactly one `SubagentStart` and zero additional
|
|
201
|
+
`SessionStart` (README M8).
|
|
202
|
+
|
|
203
|
+
### S2. Statusline is a closed enum with no command segment
|
|
204
|
+
`tui/src/bottom_pane/status_line_setup.rs:56-155` — `StatusLineItem`, ~28 variants, all
|
|
205
|
+
built-in facts. Nothing accepts operator text or a command.
|
|
206
|
+
`tui/src/chatwidget.rs:502` — `DEFAULT_STATUS_LINE_ITEMS = ["model-with-reasoning","current-dir"]`.
|
|
207
|
+
`tui/src/slash_command.rs:59`, `:118` — `/statusline` "configure which items appear in the
|
|
208
|
+
status line".
|
|
209
|
+
`tui/src/bottom_pane/status_line_setup.rs:137-139` — `SessionId` with
|
|
210
|
+
`strum(to_string="thread-id", serialize="session-id")`.
|
|
211
|
+
`tui/src/chatwidget/status_surfaces.rs:776-784` — `ThreadTitle` renders `thread_name`, else the
|
|
212
|
+
thread id.
|
|
213
|
+
`app-server-protocol/src/protocol/common.rs:566` — `ThreadSetName => "thread/name/set"`;
|
|
214
|
+
params `{thread_id, name}` (`app-server-protocol/src/protocol/v2/thread.rs:753-756`, camelCase
|
|
215
|
+
on the wire).
|
|
216
|
+
`[post-audit]` **The auto-titler is guarded on the thread being unnamed**:
|
|
217
|
+
`tui/src/app/thread_routing.rs:1841` — `let automatic_title_user_message = if
|
|
218
|
+
self.chat_widget.thread_name().is_none() && …`. Only inside that branch does the TUI call
|
|
219
|
+
`thread_set_name` with a provisional title (`:1877-1888`) and then generate a model title
|
|
220
|
+
(`tui/src/app/event_dispatch.rs:2579-2592`, which additionally re-checks the current name).
|
|
221
|
+
`app-server/src/request_processors/thread_processor.rs:639-658` — the explicit setter carries
|
|
222
|
+
no such guard.
|
|
223
|
+
**Exercised on 2026-09-08 — see `README.md` §M-B.** An explicitly set name survives turns in
|
|
224
|
+
both orderings.
|
|
225
|
+
|
|
226
|
+
### S3. App-server method surface at 0.153.4
|
|
227
|
+
`app-server-protocol/src/protocol/` — the measured route survives: `turn/start`, `turn/steer`,
|
|
228
|
+
`turn/interrupt`, `thread/inject_items`, `thread/status/changed`, `thread/resume`,
|
|
229
|
+
`thread/loaded/list`, `thread/list`.
|
|
230
|
+
New family since the archived 0.136 note: `thread/queue/{add,start,list,update,reorder,delete,
|
|
231
|
+
changed}`. Not exercised; a candidate to compare against `turn/start` in a later step, not a
|
|
232
|
+
claim about it now.
|
|
233
|
+
|
|
234
|
+
# Open at the source layer
|
|
235
|
+
|
|
236
|
+
- Rows added under `[post-audit]` have no second reader yet.
|
|
237
|
+
- T4: name-collision hardening read, never triggered.
|
|
238
|
+
- S3: the queue family read, never called.
|
|
239
|
+
- Gotcha 2 of the archived probe (`-c` disables auto-attach) is now half-measured:
|
|
240
|
+
`tui/src/lib.rs:919-930` — `can_reuse_implicit_local_daemon` requires empty
|
|
241
|
+
`cli_kv_overrides`, default loader overrides, `!strict_config` and no non-replayable launch
|
|
242
|
+
overrides. `[host]` `--dangerously-bypass-hook-trust` was measured to defeat it (README
|
|
243
|
+
§M-A2); plain `-c` was not separately re-run at 0.153.4.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# raw-macos-measure — Darwin host measurement for the macOS parity lane (#78)
|
|
2
|
+
|
|
3
|
+
Lane: `#78` macOS row, opened by the first green `macos-install-surface` CI run
|
|
4
|
+
([`34303884286`](https://github.com/junghan0611/entwurf/actions/runs/34303884286) @ `70eda03`,
|
|
5
|
+
2026-09-09, `macos-latest`). That run certifies the **Entwurf-only install path** and nothing
|
|
6
|
+
else — it is CERTIFIED (CI), which in this repo is deliberately weaker than a physical-host
|
|
7
|
+
doctor green. This ledger is where the *rest* of the axis gets measured before any code claims it.
|
|
8
|
+
|
|
9
|
+
This file is a MEASUREMENT-ONLY ledger, on the `raw-codex-measure` precedent. Nothing here is
|
|
10
|
+
written by an entwurf installer, no meta-record is minted, and no operator config is edited.
|
|
11
|
+
|
|
12
|
+
**Evidence-state vocabulary:** **[host-linux]** = measured on the Linux oracle (control run —
|
|
13
|
+
tells us what the probe reports where we already know the answer); **[host-darwin]** = measured on
|
|
14
|
+
the borrowed Mac; **[source]** = read at `file:line`; **[hypothesis]** = inference not yet
|
|
15
|
+
confirmed by a run.
|
|
16
|
+
|
|
17
|
+
## Why a probe script and not a prose ledger
|
|
18
|
+
|
|
19
|
+
The other `raw-*-measure` lanes were written on the host that owns the vendor. This one is not:
|
|
20
|
+
the Darwin host is **borrowed**, so its measurement window is short and probably single-shot. The
|
|
21
|
+
executable half (`probe.sh`) exists so that window costs one command instead of a live debugging
|
|
22
|
+
session, and so a mistake in the probe is found on Linux rather than on the Mac.
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
./scripts/raw-macos-measure/probe.sh # ~1.2s, writes nothing outside its own mktemp dir
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
It is `/bin/sh`, not bash, deliberately: macOS ships bash 3.2 and *what a shell may assume here*
|
|
29
|
+
is itself one of the measurements (cell M2). Every cell prints a verdict token — `PRESENT` /
|
|
30
|
+
`ABSENT` / `OK` / `FAIL` / `DIFFERS` / `UNKNOWN` — so no reader has to interpret raw tool output.
|
|
31
|
+
|
|
32
|
+
## Cells
|
|
33
|
+
|
|
34
|
+
| Cell | Question it answers | Consumer of the answer |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| **M1** | Which Darwin, which arch, which machine | BASELINE host record |
|
|
37
|
+
| **M2** | bash version at `/bin/bash` and via `env bash`; are bash-4 features (`declare -A`, `mapfile`) available | the two gates that use bash 4 (`check-install-container.sh`, `check-meta-doctor-oracle.sh`) |
|
|
38
|
+
| **M3** | node / npm / pnpm / python3 / git / tmux presence and versions, **and whether `python3` is the CommandLineTools stub** | `docs/setup-clean-host.md` requirements table; a stub python3 is a total failure of the install+doctor surface, not a degraded cell |
|
|
39
|
+
| **M4** | GNU-vs-BSD tool matrix, measured **behaviourally** (presence is not compatibility) | the P1 substitution surface — `sha256sum` (10 files), `stat -c` (19), `xargs -0` (13), `sort -z` (12), `realpath` (11), `readlink -f` (5), `grep -P` (2), `date -d` (2), `timeout(1)` (5 files) |
|
|
40
|
+
| **M5** | **The identity rail.** Which start-key source exists, at what resolution, and whether a sub-second one is reachable | `processStartKey()` `pi-extensions/lib/meta-session.ts:1554` — the defense that stops a stale marker granting the wrong garden identity |
|
|
41
|
+
| **M6** | Can a reader see another process's argv/environment — measured as a **contrast**, not a single reading | the bridge/receiver discovery paths that read `/proc/<pid>/{environ,cmdline}` (`scripts/omp-bridge-doctor.sh:258-267`, `scripts/copilot-receive-bridge.sh:264-289`) |
|
|
42
|
+
| **M7** | How reachable is a same-second pid reuse (`kern.maxproc`, PID_MAX) | the severity grade of the M5 finding |
|
|
43
|
+
| **M8** | Case-sensitivity, HOME/XDG shape, symlink support, `~/.local/bin` on PATH | installers and the `~/.local/bin/entwurf` symlink ownership |
|
|
44
|
+
| **M9** | Does a **rename-over** raise an `fs.watch` event on the watched path | the mailbox doorbell — the receiver arms a watch, the sender delivers by staging then renaming into place |
|
|
45
|
+
|
|
46
|
+
### Why M6 is a contrast and not a reading
|
|
47
|
+
|
|
48
|
+
A first draft of this probe asked `ps -E` about a `/bin/sleep` child. That would have produced a
|
|
49
|
+
**false negative on Darwin**: Apple-signed, SIP-protected binaries refuse environment reads *by
|
|
50
|
+
construction*, so `REJECTED` there says nothing about our actual case — an unrestricted
|
|
51
|
+
Homebrew/nvm `node` (the entwurf MCP child) and the Copilot CLI. The probe therefore starts a
|
|
52
|
+
`node` child carrying `ENTWURF_PROBE_MARKER=hello-entwurf` and asks whether that exact string is
|
|
53
|
+
visible, **and** keeps the `/bin/sleep` control beside it. `VISIBLE` on the node row next to
|
|
54
|
+
`REJECTED` on the sleep row is the evidence; either row alone is not.
|
|
55
|
+
|
|
56
|
+
`[measured by the audit lane, 2026-09-09]` The Darwin conditions under which `ps -E` can read a
|
|
57
|
+
foreign environment are: same process, SIP disabled, a development kernel, target not restricted,
|
|
58
|
+
or a dedicated entitlement. Only "target not restricted" is satisfiable on a normal host, which is
|
|
59
|
+
exactly why the target choice decides the answer.
|
|
60
|
+
|
|
61
|
+
## M5 — the finding that already exists, measured on Linux
|
|
62
|
+
|
|
63
|
+
The identity core is dual-scheme **[source, `meta-session.ts:1554-1576`]**: `/proc/<pid>/stat`
|
|
64
|
+
field 22 → `linux:<ticks>`, else `ps -o lstart=` → `ps:<lstart>`, else `""` (fail-closed).
|
|
65
|
+
`startKeyScheme()` **[source, `:1630-1633`]** accepts both. **Darwin has no `/proc`, so it always
|
|
66
|
+
takes the `ps:` branch.**
|
|
67
|
+
|
|
68
|
+
**[host-linux, oracle, 2026-09-09 12:32]** The two schemes do not have the same resolution, and
|
|
69
|
+
the gap is measurable without a Mac:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
/proc present? YES
|
|
73
|
+
ps -o lstart= self: Wed Sep 9 12:32:26 2026 <- 1-second granular
|
|
74
|
+
linux ticks 65958142 <- 10ms granular (100 ticks/s)
|
|
75
|
+
child A (556824): Wed Sep 9 12:32:27 2026
|
|
76
|
+
child B (556825): Wed Sep 9 12:32:27 2026
|
|
77
|
+
RESOLUTION 1-SECOND (two back-to-back starts share one key string)
|
|
78
|
+
widened key A Wed Sep 9 12:32:27 2026 sleep 4
|
|
79
|
+
widened key B Wed Sep 9 12:32:27 2026 sleep 4
|
|
80
|
+
WIDENING INSUFFICIENT ALONE (identical argv collides even widened)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Two consequences, stated at the size of the evidence:
|
|
84
|
+
|
|
85
|
+
1. **The `ps:` scheme's pid-reuse window is 1 second, not 10ms.** The key is compared per-pid, so
|
|
86
|
+
two *different* pids sharing an `lstart` string is harmless. The reachable failure is the
|
|
87
|
+
specific sequence the source comment names **[source, `:1538-1544`]**: a process exits, its pid
|
|
88
|
+
is reused **within the same second**, and the new process's `lstart` matches the recorded key —
|
|
89
|
+
so a stale marker validates against the wrong process and grants it the wrong garden identity.
|
|
90
|
+
2. **Widening the key with argv does not close it.** Both children ran identical argv on purpose,
|
|
91
|
+
because that is the worst case and the worst case is what a fail-closed rail must survive — and
|
|
92
|
+
a harness restarting under the same command line is the *normal* shape, not an exotic one.
|
|
93
|
+
|
|
94
|
+
Whether that window is reachable in practice is an M7 question (pid space vs spawn rate), and the
|
|
95
|
+
grade of the finding — blocker or observation — follows from it. `probe.sh` also tests whether a
|
|
96
|
+
microsecond source is reachable on Darwin (`libproc` `proc_pidinfo(PROC_PIDTBSDINFO)` →
|
|
97
|
+
`pbi_start_tvsec`/`pbi_start_tvusec` through python3 ctypes); on Linux that cell degrades with a
|
|
98
|
+
named reason rather than a silent skip:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
libproc UNAVAILABLE (/usr/lib/libproc.dylib: cannot open shared object file: ...)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## If the Mac is single-shot: what this one command already buys
|
|
105
|
+
|
|
106
|
+
The portability audit (`.agent-reports/macos-portability-audit-20260909.md` §8) enumerates a
|
|
107
|
+
nine-item acceptance checklist. **Items 1–6 need no entwurf install and no fence change**, so they
|
|
108
|
+
can all be taken on first contact — and running `probe.sh` once covers every one of them:
|
|
109
|
+
|
|
110
|
+
| Checklist item | Closes | Covered by |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| 1 · run the probe | the host facts | the whole run |
|
|
113
|
+
| 2 · `ps -Eww` on a marker-carrying `node` child | **P2-2 · P2-3 · P2-4** | **M6**, with the SIP-protected `/bin/sleep` control beside it |
|
|
114
|
+
| 3 · `ps -o lstart=` and the two-child cell | **P2-5 · P0-8** | **M5** |
|
|
115
|
+
| 4 · `proc_pidinfo` microsecond cell | **P2-5 Plan B** | **M5** |
|
|
116
|
+
| 5 · expected-failure battery (`readlink -f`, `shasum`, `grep -P`, `timeout`) | **P1-1 · P1-3 · P1-4** | **M4** |
|
|
117
|
+
| 6 · `python3 -V` without the CommandLineTools dialog | **P3-3** | **M3** |
|
|
118
|
+
| 7–9 · installs, `doctor-meta-bridge`, receive round trip | **P3-1 · P3-2 · P2-1 · P3-4 · P3-5** | NOT the probe — needs the 0.20.0 code landed and a real logged-in harness |
|
|
119
|
+
|
|
120
|
+
So the ordering for a scarce window is: **run the probe, keep the output, leave.** Items 7–9 can
|
|
121
|
+
be attempted with whatever time is left, but items 1–6 are the ones that convert open design
|
|
122
|
+
questions into facts, and none of them can be answered from Linux.
|
|
123
|
+
|
|
124
|
+
A cell that comes back the *opposite* of what we expect is worth as much as one that confirms —
|
|
125
|
+
item 5 in particular is a battery of EXPECTED FAILURES, and a surprise success there changes a
|
|
126
|
+
substitution's justification rather than validating it.
|
|
127
|
+
|
|
128
|
+
## Control run — Linux oracle
|
|
129
|
+
|
|
130
|
+
**[host-linux, oracle, 2026-09-09]** The probe was run on Linux before being handed to a Darwin
|
|
131
|
+
host, to prove the probe itself is not the thing under test. Selected verdicts: `stat -c %s` OK ·
|
|
132
|
+
`readlink -f` OK · all three sha256 producers agree on the known digest of `abc` · `sort -z` /
|
|
133
|
+
`xargs -0` / `xargs -0r` / `grep -P` / `date -d` all OK · `ps -E` **REJECTED** even on Linux (so
|
|
134
|
+
the environment-visibility question in M6 is not Darwin-specific) · case-sensitive volume · total
|
|
135
|
+
runtime 1.2s.
|
|
136
|
+
|
|
137
|
+
That control matters for one reason: every `OK` above is a **GNU** answer. The same probe on
|
|
138
|
+
Darwin is expected to turn several of them into `FAIL`, and the list of which ones is exactly the
|
|
139
|
+
P1 substitution work.
|
|
140
|
+
|
|
141
|
+
## Darwin cells — NOT YET MEASURED
|
|
142
|
+
|
|
143
|
+
Nothing below the line has been run on a Mac. GLG is borrowing a company host; when it lands, run
|
|
144
|
+
`probe.sh` once and paste the whole output here under a `[host-darwin]` heading with the date,
|
|
145
|
+
the Darwin version, and who ran it.
|
|
146
|
+
|
|
147
|
+
Do not fill these in from documentation, from a CI runner image spec, or from another project's
|
|
148
|
+
notes. This lane exists because `#78` says *claim only what physical evidence proves*.
|