@junghanacs/entwurf 0.20.1 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +85 -190
- package/BASELINE.md +6 -3
- package/CHANGELOG.md +473 -14
- package/CONTRIBUTING.md +1 -1
- package/DELIVERY.md +332 -60
- package/README.md +100 -22
- package/VERIFY.md +93 -7
- package/docs/acp-backend-rail.md +0 -1
- package/docs/external-mcp-host.md +64 -33
- package/docs/setup-clean-host.md +151 -17
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +130 -114
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-caller-seat.js +174 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +519 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +22 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +3 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +6 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-native-push.js +30 -17
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +7 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +3 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +8 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +7 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-mailbox-body.js +20 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +227 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +189 -19
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/adapter.js +10 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/codex-ws-client.js +403 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/register.js +4 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook-codex.js +325 -0
- package/mcp/entwurf-bridge/dist/scripts/codex-socket-path.js +30 -0
- package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +6 -1
- package/mcp/entwurf-bridge/src/index.ts +154 -116
- package/mcp/entwurf-bridge/tsconfig.build.json +10 -0
- package/package.json +13 -4
- package/pi-extensions/entwurf-control.ts +71 -19
- package/pi-extensions/lib/codex-caller-seat.ts +204 -0
- package/pi-extensions/lib/codex-fresh-preflight.ts +580 -0
- package/pi-extensions/lib/compaction-send-guard.ts +80 -0
- package/pi-extensions/lib/entwurf-fact-provider.ts +29 -3
- package/pi-extensions/lib/entwurf-peers-render.ts +4 -1
- package/pi-extensions/lib/entwurf-v2-contract.ts +6 -7
- package/pi-extensions/lib/entwurf-v2-native-push.ts +35 -18
- package/pi-extensions/lib/entwurf-v2-production.ts +10 -3
- package/pi-extensions/lib/entwurf-v2-runner.ts +3 -2
- package/pi-extensions/lib/entwurf-v2-send.ts +16 -11
- package/pi-extensions/lib/entwurf-v2-surface.ts +7 -4
- package/pi-extensions/lib/meta-mailbox-body.ts +22 -13
- package/pi-extensions/lib/meta-sender-identity.ts +305 -0
- package/pi-extensions/lib/mux-fresh-call.ts +233 -29
- package/pi-extensions/lib/native-push/adapter.ts +21 -24
- package/pi-extensions/lib/native-push/codex-ws-client.ts +506 -0
- package/pi-extensions/lib/native-push/register.ts +7 -9
- package/pi-extensions/meta-bridge-hook-codex.ts +371 -0
- package/run.sh +342 -28
- package/scripts/check-agy-sender-identity.ts +1 -1
- package/scripts/check-codex-app-server-launch.ts +445 -0
- package/scripts/check-codex-birth-hook.ts +264 -0
- package/scripts/check-codex-bridge-identity.ts +179 -0
- package/scripts/check-codex-native-push.ts +386 -0
- package/scripts/check-codex-sender-identity.ts +495 -0
- package/scripts/check-compaction-send-guard.ts +130 -0
- package/scripts/check-copilot-receive-arm.ts +4 -1
- package/scripts/check-entwurf-fact-provider.ts +38 -0
- package/scripts/check-entwurf-peers-surface.ts +13 -1
- package/scripts/check-entwurf-self-address.ts +15 -16
- package/scripts/check-entwurf-v2-contract.ts +4 -3
- package/scripts/check-entwurf-v2-decider.ts +7 -5
- package/scripts/check-entwurf-v2-native-push.ts +35 -7
- package/scripts/check-entwurf-v2-production.ts +245 -12
- package/scripts/check-entwurf-v2-runner.ts +1 -1
- package/scripts/check-entwurf-v2-send.ts +26 -7
- package/scripts/check-entwurf-v2-surface.ts +1 -1
- package/scripts/check-gate-qualification.ts +8 -3
- package/scripts/check-harness-admission-parity.ts +0 -1
- package/scripts/check-mux-launch-tmux.ts +345 -4
- package/scripts/check-native-push-adapter.ts +20 -16
- package/scripts/check-native-push-register.ts +5 -1
- package/scripts/check-release-gate-outcomes.ts +47 -1
- package/scripts/check-setup-qualification.sh +3 -1
- package/scripts/codex-app-server-launch.sh +275 -0
- package/scripts/codex-birth-doctor.sh +276 -0
- package/scripts/codex-birth-install.sh +414 -0
- package/scripts/codex-birth-uninstall.sh +170 -0
- package/scripts/codex-mcp-config.py +435 -0
- package/scripts/codex-socket-path.ts +33 -0
- package/scripts/codex-statusline-config.py +434 -0
- package/scripts/codex-terminal-title-config.py +500 -0
- package/scripts/codex_toml_io.py +653 -0
- package/scripts/lib/codex-fresh-live-protocol.ts +121 -0
- package/scripts/lib/codex-fresh-source-receipts.ts +426 -0
- package/scripts/lib/launch-receipt-windows.ts +46 -0
- package/scripts/lib/tmux-coordinate-row.ts +58 -0
- package/scripts/meta-bridge-fresh-cut.ts +6 -1
- package/scripts/mutants/codex-app-server-launch.json +157 -0
- package/scripts/mutants/codex-caller-seat.json +336 -0
- package/scripts/mutants/codex-native.json +838 -0
- package/scripts/mutants/compaction-send-guard.json +103 -0
- package/scripts/mutants/entwurf-peers.json +19 -0
- package/scripts/mutants/mux-fresh-call.json +93 -10
- package/scripts/mutants/omp-fresh.json +6 -4
- package/scripts/mutants/release-gate.json +13 -0
- package/scripts/mutants/v2-surface.json +75 -1
- package/scripts/raw-async-delivery/README.md +2 -1
- package/scripts/raw-codex-measure/README.md +114 -46
- package/scripts/smoke-agy-native-push-live.ts +3 -1
- package/scripts/smoke-codex-birth.sh +347 -0
- package/scripts/smoke-codex-config-state.sh +700 -0
- package/scripts/smoke-codex-fresh-live.ts +1426 -0
- package/scripts/smoke-codex-native-push-live.ts +75 -0
- package/scripts/smoke-entwurf-chain-live.ts +50 -0
- package/scripts/smoke-setup-verdict.sh +125 -10
- package/scripts/tsconfig.json +1 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""codex-mcp-config — stateful `[mcp_servers.entwurf-bridge]` adapter; stdlib only.
|
|
3
|
+
|
|
4
|
+
One reversible atom in `$CODEX_HOME/config.toml`: the entwurf-bridge MCP server
|
|
5
|
+
entry in the MEASURED stdio shape (`command` written by `codex mcp add`, M3 in
|
|
6
|
+
scripts/raw-codex-measure/README.md), the vendor-supported `env_vars` allowlist
|
|
7
|
+
that forwards the operator-owned app-server context without storing its values,
|
|
8
|
+
plus the one literal provenance env value
|
|
9
|
+
`ENTWURF_BRIDGE_NATIVE_HOST = "codex"` — a local ownership/drift atom, never
|
|
10
|
+
cryptographic authentication. `codex mcp add` is never called because it has
|
|
11
|
+
no exact inverse; this writer owns exactly the table-family lines it touches
|
|
12
|
+
(codex_toml_io), captures the operator's first preimage once, and restores it
|
|
13
|
+
byte-exact.
|
|
14
|
+
|
|
15
|
+
Refuses, with the offending name in the message: a symlinked config (someone
|
|
16
|
+
else's SSOT), malformed TOML, a foreign/no-state collision it cannot attribute,
|
|
17
|
+
a state retargeted at another config path, and explicit contrary operator
|
|
18
|
+
values by name — an `url` entry (the vendor's streamable_http transport) or an
|
|
19
|
+
`enabled = false` entry are the operator's decisions, not drift to overwrite.
|
|
20
|
+
|
|
21
|
+
Doctor separates the two axes the run.sh wiring composes later: the FIRST
|
|
22
|
+
token is effective runtime validity judged from the config alone, the SECOND
|
|
23
|
+
is ownership judged from the install state; `doctor-invocation` prints the
|
|
24
|
+
exact stdio invocation so the wiring can boot-prove the configured command is
|
|
25
|
+
the stable entwurf-bridge bin.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import json
|
|
31
|
+
import os
|
|
32
|
+
import sys
|
|
33
|
+
sys.dont_write_bytecode = True
|
|
34
|
+
|
|
35
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
36
|
+
from codex_toml_io import ( # noqa: E402
|
|
37
|
+
append_block,
|
|
38
|
+
assert_blast_radius,
|
|
39
|
+
atomic_write,
|
|
40
|
+
atomic_write_state,
|
|
41
|
+
deep_get,
|
|
42
|
+
die,
|
|
43
|
+
find_table_family,
|
|
44
|
+
now,
|
|
45
|
+
parse_toml,
|
|
46
|
+
read_text,
|
|
47
|
+
render_file,
|
|
48
|
+
scan_lines,
|
|
49
|
+
span_text,
|
|
50
|
+
splice,
|
|
51
|
+
toml_string,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
STATE_SCHEMA_VERSION = 1
|
|
55
|
+
ATOM = "codex-mcp"
|
|
56
|
+
SERVER_KEY = "entwurf-bridge"
|
|
57
|
+
SERVER_PATH = ("mcp_servers", SERVER_KEY)
|
|
58
|
+
PROVENANCE_ENV = "ENTWURF_BRIDGE_NATIVE_HOST"
|
|
59
|
+
PROVENANCE_VALUE = "codex"
|
|
60
|
+
CONTEXT_ENV_VARS = (
|
|
61
|
+
"CODEX_HOME",
|
|
62
|
+
"ENTWURF_DIR",
|
|
63
|
+
"PI_CODING_AGENT_DIR",
|
|
64
|
+
"ENTWURF_META_SESSIONS_DIR",
|
|
65
|
+
"ENTWURF_META_MAILBOX_DIR",
|
|
66
|
+
"ENTWURF_META_SENDERS_DIR",
|
|
67
|
+
"ENTWURF_META_RECEIVERS_DIR",
|
|
68
|
+
"TMUX",
|
|
69
|
+
"TMUX_PANE",
|
|
70
|
+
)
|
|
71
|
+
STATE_REQUIRED = (
|
|
72
|
+
"managedConfigPath",
|
|
73
|
+
"atom",
|
|
74
|
+
"serverKey",
|
|
75
|
+
"command",
|
|
76
|
+
"detectMode",
|
|
77
|
+
"configExistedBefore",
|
|
78
|
+
"entryExistedBefore",
|
|
79
|
+
"preimage",
|
|
80
|
+
"installedAt",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def canonical_block(command: str) -> list[str]:
|
|
85
|
+
forwarded = ", ".join(toml_string(name) for name in CONTEXT_ENV_VARS)
|
|
86
|
+
return [
|
|
87
|
+
f"[mcp_servers.{SERVER_KEY}]",
|
|
88
|
+
f"command = {toml_string(command)}",
|
|
89
|
+
f"env_vars = [{forwarded}]",
|
|
90
|
+
f"[mcp_servers.{SERVER_KEY}.env]",
|
|
91
|
+
f"{PROVENANCE_ENV} = {toml_string(PROVENANCE_VALUE)}",
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def canonical_semantics(command: str) -> dict:
|
|
96
|
+
return {
|
|
97
|
+
"command": command,
|
|
98
|
+
"env_vars": list(CONTEXT_ENV_VARS),
|
|
99
|
+
"env": {PROVENANCE_ENV: PROVENANCE_VALUE},
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
def load_state(state_path: str) -> dict | None:
|
|
103
|
+
raw = read_text(state_path, "install-state")
|
|
104
|
+
if raw is None:
|
|
105
|
+
return None
|
|
106
|
+
if not raw.strip():
|
|
107
|
+
die(4, f"codex-mcp: install-state {state_path} is empty — refusing to guess")
|
|
108
|
+
try:
|
|
109
|
+
state = json.loads(raw)
|
|
110
|
+
except ValueError as error:
|
|
111
|
+
die(4, f"codex-mcp: install-state {state_path} is not valid JSON: {error}")
|
|
112
|
+
if not isinstance(state, dict):
|
|
113
|
+
die(4, f"codex-mcp: install-state {state_path} top-level must be a JSON object")
|
|
114
|
+
return state
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def checked_state(state_path: str) -> dict:
|
|
118
|
+
state = load_state(state_path)
|
|
119
|
+
if state is None:
|
|
120
|
+
die(2, f"codex-mcp: no install-state at {state_path} — nothing to undo")
|
|
121
|
+
if state.get("schemaVersion") != STATE_SCHEMA_VERSION:
|
|
122
|
+
die(4, f"codex-mcp: install-state {state_path} is not schemaVersion {STATE_SCHEMA_VERSION} — refusing")
|
|
123
|
+
atom = state.get("atom")
|
|
124
|
+
if atom != ATOM:
|
|
125
|
+
# A foreign state: another atom's receipt sitting on our path. Never
|
|
126
|
+
# silently overwritten, never adopted — attribution first.
|
|
127
|
+
die(4, f"codex-mcp: install-state {state_path} belongs to atom {atom!r}, not {ATOM!r} — refusing to touch it")
|
|
128
|
+
if state.get("serverKey") != SERVER_KEY:
|
|
129
|
+
die(4, f"codex-mcp: install-state {state_path} serverKey is not {SERVER_KEY} — refusing")
|
|
130
|
+
if any(key not in state for key in STATE_REQUIRED):
|
|
131
|
+
die(4, f"codex-mcp: install-state {state_path} has an unsupported shape — refusing")
|
|
132
|
+
path = state.get("managedConfigPath")
|
|
133
|
+
if not isinstance(path, str) or not os.path.isabs(path):
|
|
134
|
+
die(4, f"codex-mcp: install-state {state_path} has no absolute managedConfigPath")
|
|
135
|
+
return state
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def prior_state(state_path: str, config_path: str) -> dict | None:
|
|
139
|
+
state = load_state(state_path)
|
|
140
|
+
if state is None:
|
|
141
|
+
return None
|
|
142
|
+
if state.get("schemaVersion") != STATE_SCHEMA_VERSION or state.get("atom") != ATOM:
|
|
143
|
+
# Route through checked_state for the honest, named refusal.
|
|
144
|
+
checked_state(state_path)
|
|
145
|
+
if state.get("managedConfigPath") != os.path.abspath(config_path):
|
|
146
|
+
die(
|
|
147
|
+
3,
|
|
148
|
+
f"codex-mcp: install-state {state_path} is bound to {state.get('managedConfigPath')}, "
|
|
149
|
+
f"not {os.path.abspath(config_path)} — refusing to retarget an install onto another config",
|
|
150
|
+
)
|
|
151
|
+
checked = checked_state(state_path)
|
|
152
|
+
return checked
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def classify_entry(parsed: dict) -> tuple[str, dict | None]:
|
|
156
|
+
"""The entry's effective verdict and its parsed value (None when absent).
|
|
157
|
+
`refuse:<name>` verdicts are the explicit contrary operator values."""
|
|
158
|
+
entry = deep_get(parsed, SERVER_PATH)
|
|
159
|
+
if entry is None:
|
|
160
|
+
return "absent", None
|
|
161
|
+
if not isinstance(entry, dict):
|
|
162
|
+
return "refuse:not-a-table", entry
|
|
163
|
+
if "url" in entry and "command" not in entry:
|
|
164
|
+
return "refuse:url", entry
|
|
165
|
+
if entry.get("enabled") is False:
|
|
166
|
+
return "refuse:enabled", entry
|
|
167
|
+
if "command" not in entry:
|
|
168
|
+
return "refuse:no-command", entry
|
|
169
|
+
return "stdio", entry
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def install(config_path: str, command: str, state_path: str) -> None:
|
|
173
|
+
if os.path.islink(config_path):
|
|
174
|
+
die(3, f"codex-mcp: refusing to adopt {config_path} — symlink to {os.readlink(config_path)} (someone else's SSOT)")
|
|
175
|
+
if os.path.exists(config_path) and not os.path.isfile(config_path):
|
|
176
|
+
die(4, f"codex-mcp: refusing: {config_path} exists and is not a regular file")
|
|
177
|
+
|
|
178
|
+
text = read_text(config_path, "config")
|
|
179
|
+
parsed = parse_toml(text if text is not None else "", f"config {config_path}") if text is not None else {}
|
|
180
|
+
verdict, entry = classify_entry(parsed)
|
|
181
|
+
if verdict.startswith("refuse:"):
|
|
182
|
+
name = verdict.split(":", 1)[1]
|
|
183
|
+
die(
|
|
184
|
+
3,
|
|
185
|
+
f"codex-mcp: refusing: [mcp_servers.{SERVER_KEY}] in {config_path} explicitly sets "
|
|
186
|
+
f"`{name}` ({'streamable_http transport, not stdio' if name == 'url' else 'the server is explicitly disabled' if name == 'enabled' else 'no stdio command'}). "
|
|
187
|
+
"That is the operator's decision, not drift — entwurf will not overwrite it by name.",
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
lines = (text if text is not None else "").split("\n")
|
|
191
|
+
infos = scan_lines(lines)
|
|
192
|
+
family = find_table_family(lines, infos, SERVER_PATH)
|
|
193
|
+
if entry is not None and family is None:
|
|
194
|
+
die(
|
|
195
|
+
4,
|
|
196
|
+
f"codex-mcp: [mcp_servers.{SERVER_KEY}] in {config_path} exists but not as a literal table block "
|
|
197
|
+
"(inline/dotted form) — this writer owns only line-shaped tables and refuses to touch it",
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
prior = prior_state(state_path, config_path)
|
|
201
|
+
if prior is None:
|
|
202
|
+
state = {
|
|
203
|
+
"schemaVersion": STATE_SCHEMA_VERSION,
|
|
204
|
+
"managedConfigPath": os.path.abspath(config_path),
|
|
205
|
+
"atom": ATOM,
|
|
206
|
+
"serverKey": SERVER_KEY,
|
|
207
|
+
"command": command,
|
|
208
|
+
"detectMode": "adopt-regular-file" if text is not None else "created-new",
|
|
209
|
+
"configExistedBefore": text is not None,
|
|
210
|
+
"entryExistedBefore": family is not None,
|
|
211
|
+
# The FIRST preimage, captured once: the exact bytes of the table
|
|
212
|
+
# family we superseded (or null when we appended to empty space).
|
|
213
|
+
"preimage": span_text(lines, family[0], family[1]) if family is not None else None,
|
|
214
|
+
"installedAt": now(),
|
|
215
|
+
}
|
|
216
|
+
else:
|
|
217
|
+
state = prior
|
|
218
|
+
state["command"] = command # our recorded intent follows a re-run; preimages never do
|
|
219
|
+
|
|
220
|
+
block = canonical_block(command)
|
|
221
|
+
if family is not None:
|
|
222
|
+
candidate_lines = splice(lines, family[0], family[1], block)
|
|
223
|
+
else:
|
|
224
|
+
candidate_lines = append_block(lines, block) if text is not None else list(block)
|
|
225
|
+
candidate = render_file(candidate_lines)
|
|
226
|
+
|
|
227
|
+
_, after = assert_blast_radius(text, candidate, SERVER_PATH, f"config {config_path}")
|
|
228
|
+
if deep_get(after, SERVER_PATH) != canonical_semantics(command):
|
|
229
|
+
die(7, f"codex-mcp: internal refusal: candidate entry is not the canonical stdio shape")
|
|
230
|
+
|
|
231
|
+
# The receipt is durable BEFORE the operator's config is ever touched: it
|
|
232
|
+
# already carries the exact preimage of the bytes about to be replaced
|
|
233
|
+
# (or a prior receipt's preimage, on a re-run). A crash or write failure
|
|
234
|
+
# between here and the config write below leaves the operator's config
|
|
235
|
+
# untouched and the receipt correct — a retry resumes from it instead of
|
|
236
|
+
# mistaking our own canonical block for a fresh operator preimage.
|
|
237
|
+
atomic_write_state(state_path, state)
|
|
238
|
+
# Strongest idempotence is an untouched file: same bytes, same mtime.
|
|
239
|
+
if candidate != (text if text is not None else ""):
|
|
240
|
+
atomic_write(config_path, candidate, prefix=".codex-mcp-")
|
|
241
|
+
sys.stdout.write(f"{state['detectMode']} {os.path.abspath(config_path)}\n")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def uninstall(state_path: str) -> None:
|
|
245
|
+
state = checked_state(state_path)
|
|
246
|
+
config_path = state["managedConfigPath"]
|
|
247
|
+
if os.path.islink(config_path):
|
|
248
|
+
die(3, f"codex-mcp: refusing to uninstall — {config_path} became a symlink (someone else's SSOT)")
|
|
249
|
+
|
|
250
|
+
text = read_text(config_path, "config")
|
|
251
|
+
if text is None:
|
|
252
|
+
os.remove(state_path) # the config is already gone; the receipt outlived its target
|
|
253
|
+
sys.stdout.write(f"uninstalled {config_path} (config already absent)\n")
|
|
254
|
+
return
|
|
255
|
+
parsed = parse_toml(text, f"config {config_path}")
|
|
256
|
+
entry = deep_get(parsed, SERVER_PATH)
|
|
257
|
+
lines = text.split("\n")
|
|
258
|
+
infos = scan_lines(lines)
|
|
259
|
+
family = find_table_family(lines, infos, SERVER_PATH)
|
|
260
|
+
|
|
261
|
+
if family is not None:
|
|
262
|
+
expected = canonical_semantics(state["command"])
|
|
263
|
+
if entry != expected:
|
|
264
|
+
die(
|
|
265
|
+
3,
|
|
266
|
+
f"codex-mcp: refusing to uninstall — [{'.'.join(SERVER_PATH)}] drifted from the "
|
|
267
|
+
"exact post-install table. Removing it would delete operator edits; repair or remove "
|
|
268
|
+
"those edits deliberately, then re-run the inverse",
|
|
269
|
+
)
|
|
270
|
+
preimage = state.get("preimage")
|
|
271
|
+
if preimage is None:
|
|
272
|
+
candidate_lines = splice(lines, family[0], family[1], [])
|
|
273
|
+
else:
|
|
274
|
+
candidate_lines = splice(lines, family[0], family[1], preimage.split("\n"))
|
|
275
|
+
candidate = render_file(candidate_lines)
|
|
276
|
+
_, after = assert_blast_radius(text, candidate, SERVER_PATH, f"config {config_path}")
|
|
277
|
+
if preimage is not None:
|
|
278
|
+
# Restoring must reproduce the superseded entry exactly (it is a
|
|
279
|
+
# byte splice; this guards the scanner, not the operator).
|
|
280
|
+
preimage_doc = parse_toml(preimage, "preimage block")
|
|
281
|
+
if deep_get(preimage_doc, SERVER_PATH) != deep_get(after, SERVER_PATH):
|
|
282
|
+
die(7, "codex-mcp: internal refusal: preimage splice changed the entry's meaning")
|
|
283
|
+
if state.get("detectMode") == "created-new" and not [l for l in candidate_lines if l.strip()]:
|
|
284
|
+
# We created this file; the inverse removes it.
|
|
285
|
+
os.remove(config_path)
|
|
286
|
+
os.remove(state_path)
|
|
287
|
+
sys.stdout.write(f"uninstalled {config_path} (file removed)\n")
|
|
288
|
+
return
|
|
289
|
+
atomic_write(config_path, candidate, prefix=".codex-mcp-")
|
|
290
|
+
elif entry is not None:
|
|
291
|
+
# The table is gone by NAME but the path is still populated — the
|
|
292
|
+
# operator (or the vendor) reshaped it into inline/dotted TOML. This
|
|
293
|
+
# writer cannot span-edit that shape; treating it as absent would
|
|
294
|
+
# silently drop the receipt over an entry we never actually took
|
|
295
|
+
# back. Refuse and keep the receipt so the operator can repair it.
|
|
296
|
+
die(
|
|
297
|
+
3,
|
|
298
|
+
f"codex-mcp: refusing to uninstall — [{'.'.join(SERVER_PATH)}] in {config_path} exists but not as a "
|
|
299
|
+
"literal table block (inline/dotted form) — this writer owns only line-shaped tables and refuses to "
|
|
300
|
+
"guess; the receipt is retained",
|
|
301
|
+
)
|
|
302
|
+
# else: the entry is genuinely gone — never resurrect a removed table.
|
|
303
|
+
|
|
304
|
+
os.remove(state_path) # the receipt is deleted LAST, after the config is honest
|
|
305
|
+
sys.stdout.write(f"uninstalled {config_path}\n")
|
|
306
|
+
|
|
307
|
+
def _ownership(state_path: str, config_path: str) -> tuple[str, dict | None]:
|
|
308
|
+
"""(ownership token, state) — `unowned` when no readable receipt of OUR
|
|
309
|
+
atom binds THIS config path. A foreign or retargeted receipt is not an
|
|
310
|
+
ownership claim, so the doctor reports unowned and install refuses
|
|
311
|
+
separately (checked_state names the collision)."""
|
|
312
|
+
state = load_state(state_path)
|
|
313
|
+
if state is None:
|
|
314
|
+
return "unowned", None
|
|
315
|
+
if (
|
|
316
|
+
state.get("schemaVersion") != STATE_SCHEMA_VERSION
|
|
317
|
+
or state.get("atom") != ATOM
|
|
318
|
+
or state.get("managedConfigPath") != os.path.abspath(config_path)
|
|
319
|
+
):
|
|
320
|
+
return "unowned", None
|
|
321
|
+
return "owned", state
|
|
322
|
+
|
|
323
|
+
def _effective_entry(config_path: str) -> tuple[str, dict | None]:
|
|
324
|
+
"""The EFFECTIVE axis, judged from the config alone — never the state."""
|
|
325
|
+
if os.path.islink(config_path):
|
|
326
|
+
return "symlink", None
|
|
327
|
+
text = read_text(config_path, "config")
|
|
328
|
+
if text is None or not text.strip():
|
|
329
|
+
return "file-absent", None
|
|
330
|
+
try:
|
|
331
|
+
parsed = parse_toml(text, f"config {config_path}")
|
|
332
|
+
except SystemExit:
|
|
333
|
+
return "invalid-toml", None
|
|
334
|
+
verdict, entry = classify_entry(parsed)
|
|
335
|
+
if verdict == "absent":
|
|
336
|
+
return "not-configured", None
|
|
337
|
+
if verdict == "refuse:url":
|
|
338
|
+
return "http-transport", entry
|
|
339
|
+
if verdict == "refuse:enabled":
|
|
340
|
+
return "disabled", entry
|
|
341
|
+
if verdict.startswith("refuse:"):
|
|
342
|
+
return "malformed-entry", None
|
|
343
|
+
return f"configured {entry.get('command')}", entry
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def doctor_static(config_path: str, expected_command: str, state_path: str) -> None:
|
|
347
|
+
"""Two separated tokens on one line: `<effective> <ownership> [drift reason]`.
|
|
348
|
+
The first token never reads the install state; the second never changes the
|
|
349
|
+
first. RED (exit 1) only while a receipt binds here and the entry is not
|
|
350
|
+
what we installed — an unowned-but-working entry is an honest note, not a
|
|
351
|
+
failure (this host's operator ships exactly that shape)."""
|
|
352
|
+
ownership, state = _ownership(state_path, config_path)
|
|
353
|
+
effective, entry = _effective_entry(config_path)
|
|
354
|
+
drift = None
|
|
355
|
+
if ownership == "owned":
|
|
356
|
+
if entry is None:
|
|
357
|
+
drift = "entry-gone"
|
|
358
|
+
elif effective == "http-transport":
|
|
359
|
+
drift = "transport-replaced (url)"
|
|
360
|
+
elif effective == "disabled":
|
|
361
|
+
drift = "disabled (enabled = false)"
|
|
362
|
+
else:
|
|
363
|
+
reason = drift_reason(entry, expected_command)
|
|
364
|
+
if reason is not None:
|
|
365
|
+
drift = reason
|
|
366
|
+
if drift is not None:
|
|
367
|
+
sys.stdout.write(f"{effective} drift {drift}\n")
|
|
368
|
+
raise SystemExit(1)
|
|
369
|
+
sys.stdout.write(f"{effective} {ownership}\n")
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def doctor_invocation(config_path: str) -> None:
|
|
373
|
+
"""The exact stdio invocation the vendor would spawn, as one JSON line —
|
|
374
|
+
the input the run.sh wiring feeds `probe-bridge-command` to boot-prove the
|
|
375
|
+
configured command is the stable entwurf-bridge bin."""
|
|
376
|
+
effective, entry = _effective_entry(config_path)
|
|
377
|
+
if not effective.startswith("configured "):
|
|
378
|
+
sys.stdout.write(f"{effective}\n")
|
|
379
|
+
raise SystemExit(1)
|
|
380
|
+
args = entry.get("args", [])
|
|
381
|
+
env = entry.get("env", {})
|
|
382
|
+
if not isinstance(args, list) or not all(isinstance(item, str) for item in args):
|
|
383
|
+
sys.stdout.write("malformed-entry (args is not an array of strings)\n")
|
|
384
|
+
raise SystemExit(1)
|
|
385
|
+
if not isinstance(env, dict) or not all(isinstance(k, str) and isinstance(v, str) for k, v in env.items()):
|
|
386
|
+
sys.stdout.write("malformed-entry (env is not a table of strings)\n")
|
|
387
|
+
raise SystemExit(1)
|
|
388
|
+
sys.stdout.write(json.dumps({"command": entry["command"], "args": args, "env": env}, separators=(",", ":")) + "\n")
|
|
389
|
+
|
|
390
|
+
def drift_reason(entry: dict, expected_command: str) -> str | None:
|
|
391
|
+
if entry.get("command") != expected_command:
|
|
392
|
+
return f"command-mismatch (configured {entry.get('command')!r}, installed {expected_command!r})"
|
|
393
|
+
env = entry.get("env")
|
|
394
|
+
if not isinstance(env, dict) or PROVENANCE_ENV not in env:
|
|
395
|
+
return f"env-label-missing ({PROVENANCE_ENV})"
|
|
396
|
+
if env.get(PROVENANCE_ENV) != PROVENANCE_VALUE:
|
|
397
|
+
return f"env-label-foreign ({PROVENANCE_ENV}={env.get(PROVENANCE_ENV)!r})"
|
|
398
|
+
env_vars = entry.get("env_vars")
|
|
399
|
+
if not isinstance(env_vars, list) or not all(isinstance(name, str) for name in env_vars):
|
|
400
|
+
return "context-env-vars-missing-or-malformed"
|
|
401
|
+
missing = [name for name in CONTEXT_ENV_VARS if name not in env_vars]
|
|
402
|
+
if missing:
|
|
403
|
+
return f"context-env-vars-missing ({', '.join(missing)})"
|
|
404
|
+
foreign = [name for name in env_vars if name not in CONTEXT_ENV_VARS]
|
|
405
|
+
if foreign:
|
|
406
|
+
return f"context-env-vars-foreign ({', '.join(foreign)})"
|
|
407
|
+
if env_vars != list(CONTEXT_ENV_VARS):
|
|
408
|
+
return "context-env-vars-reordered-or-duplicated"
|
|
409
|
+
foreign_keys = sorted(set(entry) - {"command", "env_vars", "env"})
|
|
410
|
+
if foreign_keys:
|
|
411
|
+
return f"foreign-keys ({', '.join(foreign_keys)})"
|
|
412
|
+
extra_env = sorted(set(env) - {PROVENANCE_ENV})
|
|
413
|
+
if extra_env:
|
|
414
|
+
return f"foreign-env-keys ({', '.join(extra_env)})"
|
|
415
|
+
return None
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def main(argv: list[str]) -> None:
|
|
419
|
+
if len(argv) < 2:
|
|
420
|
+
die(5, "usage: codex-mcp-config.py <install config command state|uninstall state|doctor-static config command state|doctor-invocation config>")
|
|
421
|
+
verb = argv[1]
|
|
422
|
+
if verb == "install" and len(argv) == 5:
|
|
423
|
+
install(argv[2], argv[3], argv[4])
|
|
424
|
+
elif verb == "uninstall" and len(argv) == 3:
|
|
425
|
+
uninstall(argv[2])
|
|
426
|
+
elif verb == "doctor-static" and len(argv) == 5:
|
|
427
|
+
doctor_static(argv[2], argv[3], argv[4])
|
|
428
|
+
elif verb == "doctor-invocation" and len(argv) == 3:
|
|
429
|
+
doctor_invocation(argv[2])
|
|
430
|
+
else:
|
|
431
|
+
die(5, "usage: codex-mcp-config.py <install config command state|uninstall state|doctor-static config command state|doctor-invocation config>")
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
if __name__ == "__main__":
|
|
435
|
+
main(sys.argv)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* codex-socket-path — print the Codex app-server default control-socket path for THIS
|
|
3
|
+
* environment, and nothing else.
|
|
4
|
+
*
|
|
5
|
+
* WHY A LEAF EXISTS FOR ONE LINE. `entwurf codex-app-server` is a bash launcher, and bash
|
|
6
|
+
* cannot import `resolveCodexDefaultSocketPath`. Its first version re-derived the path
|
|
7
|
+
* instead — `${CODEX_HOME:-$HOME/.codex}` plus a POSIX `[:space:]` trim — and a gate compared
|
|
8
|
+
* the two spellings over four ASCII-normal inputs, which they agreed on.
|
|
9
|
+
*
|
|
10
|
+
* They did not agree everywhere. `[측정 2026-09-16]` with `CODEX_HOME=$''` the TS leaf
|
|
11
|
+
* trims (JS `String.prototype.trim` strips U+FEFF) and falls back to `$HOME/.codex`, while the
|
|
12
|
+
* bash trim keeps the byte and yields `<BOM>/app-server-control/app-server-control.sock`. Same
|
|
13
|
+
* for `path.join`'s normalization of a trailing slash or a `..` segment. Every one of those is
|
|
14
|
+
* a managed launch starting a server at an address delivery and preflight never look at — a
|
|
15
|
+
* false success of exactly the kind this repo refuses.
|
|
16
|
+
*
|
|
17
|
+
* So the second spelling is gone rather than widened. There is ONE implementation of this
|
|
18
|
+
* address, this leaf prints it, and the launcher asks. Matching a transcription against its
|
|
19
|
+
* original can only ever test the inputs somebody thought of.
|
|
20
|
+
*
|
|
21
|
+
* Reads `process.env` directly: the resolver's contract is the ambient environment of whoever
|
|
22
|
+
* is about to launch, and an argv seam here would be a way to redirect the address.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { resolveCodexDefaultSocketPath } from "../pi-extensions/lib/native-push/codex-ws-client.ts";
|
|
26
|
+
|
|
27
|
+
if (process.argv.length > 2) {
|
|
28
|
+
console.error("usage: codex-socket-path (no arguments; the environment is the input)");
|
|
29
|
+
process.exit(2);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// No trailing newline: the caller substitutes this straight into an address.
|
|
33
|
+
process.stdout.write(resolveCodexDefaultSocketPath(process.env));
|