@junghanacs/entwurf 0.15.0 → 0.16.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 -0
- package/CHANGELOG.md +299 -0
- package/DELIVERY.md +4 -3
- package/README.md +99 -67
- package/VERIFY.md +13 -7
- package/docs/acp-backend-rail.md +1 -1
- package/docs/external-mcp-host.md +147 -9
- package/docs/setup-clean-host.md +221 -16
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +65 -20
- package/mcp/entwurf-bridge/dist/pi-extensions/entwurf-capabilities.json +1 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +17 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +172 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +152 -17
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/omp-fresh-preflight.js +271 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js +988 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-receive-omp.js +574 -0
- package/mcp/entwurf-bridge/dist/scripts/omp-receive-facts.js +84 -0
- package/mcp/entwurf-bridge/src/index.ts +65 -19
- package/mcp/entwurf-bridge/tsconfig.build.json +19 -3
- package/mcp/tsconfig.json +10 -0
- package/package.json +14 -4
- package/pi/entwurf-capabilities.json +1 -0
- package/pi/meta-bridge-omp/entwurf-meta-omp/package.json +7 -0
- package/pi/omp-receive/entwurf-receive-omp/package.json +7 -0
- package/pi-extensions/entwurf-control.ts +12 -9
- package/pi-extensions/lib/entwurf-self-address.ts +18 -7
- package/pi-extensions/lib/meta-sender-identity.ts +1 -1
- package/pi-extensions/lib/meta-session.ts +219 -5
- package/pi-extensions/lib/mux-fresh-call.ts +171 -17
- package/pi-extensions/lib/omp-fresh-preflight.ts +275 -0
- package/pi-extensions/meta-bridge-omp.ts +1244 -0
- package/pi-extensions/meta-bridge-receive-omp.ts +666 -0
- package/run.sh +1041 -174
- package/scripts/agy-bridge.sh +4 -2
- package/scripts/check-copilot-birth-hook.ts +700 -23
- package/scripts/check-entwurf-self-address.ts +31 -0
- package/scripts/check-fresh-cut-gate.sh +12 -4
- package/scripts/check-gate-qualification.ts +8 -2
- package/scripts/check-harness-admission-parity.ts +143 -0
- package/scripts/check-meta-doctor-oracle.sh +20 -0
- package/scripts/check-omp-birth-hook.ts +1049 -0
- package/scripts/check-omp-fresh-preflight.ts +181 -0
- package/scripts/check-omp-receive-arm.ts +516 -0
- package/scripts/check-setup-qualification.sh +126 -0
- package/scripts/copilot-bridge-doctor.sh +104 -71
- package/scripts/copilot-bridge-install.sh +180 -20
- package/scripts/copilot-bridge-oracle.sh +223 -0
- package/scripts/copilot-bridge-uninstall.sh +137 -0
- package/scripts/dev-bin.sh +50 -9
- package/scripts/fake-copilot-vendor.sh +67 -0
- package/scripts/inventory-verification-surface.ts +1 -1
- package/scripts/meta-bridge-hook-log.sh +9 -1
- package/scripts/mutants/copilot-birth.json +96 -5
- package/scripts/mutants/mux-fresh-call.json +22 -22
- package/scripts/mutants/omp-birth.json +173 -0
- package/scripts/mutants/omp-fresh.json +300 -0
- package/scripts/mutants/omp-receive.json +135 -0
- package/scripts/mutants/pi-package-ownership.json +92 -0
- package/scripts/mutants/self-address.json +34 -0
- package/scripts/mutants/setup-verdict.json +148 -0
- package/scripts/mutants/source-install.json +32 -0
- package/scripts/omp-bridge-doctor.sh +315 -0
- package/scripts/omp-bridge-install.sh +221 -0
- package/scripts/omp-bridge-oracle.sh +154 -0
- package/scripts/omp-bridge-uninstall.sh +57 -0
- package/scripts/omp-mcp-bridge.sh +320 -0
- package/scripts/omp-mcp-config.py +392 -0
- package/scripts/omp-receive-doctor.sh +246 -0
- package/scripts/omp-receive-facts.ts +106 -0
- package/scripts/omp-receive-install.sh +228 -0
- package/scripts/omp-receive-uninstall.sh +60 -0
- package/scripts/omp-tool-surface.py +349 -0
- package/scripts/pi_settings_io.py +22 -0
- package/scripts/raw-omp-measure/README.md +420 -0
- package/scripts/raw-omp-measure/probe-extension.ts +76 -0
- package/scripts/raw-omp-measure/probe-receive-surface.ts +250 -0
- package/scripts/raw-omp-measure/source-audit.md +414 -0
- package/scripts/register-pi-package.py +525 -7
- package/scripts/register-pi-provider.py +162 -12
- package/scripts/smoke-agy-install-state.sh +86 -30
- package/scripts/smoke-agy-statusline-state.sh +9 -6
- package/scripts/smoke-omp-bridge-state.sh +221 -0
- package/scripts/smoke-omp-fresh-live.ts +497 -0
- package/scripts/smoke-omp-mcp-state.sh +327 -0
- package/scripts/smoke-omp-receive-live.ts +340 -0
- package/scripts/smoke-omp-receive-state.sh +196 -0
- package/scripts/smoke-setup-verdict.sh +291 -0
- package/scripts/smoke-user-scope-citizen.sh +388 -1
- package/scripts/tsconfig.json +2 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Stateful OMP-native MCP-config adapter; stdlib only (#87 step 5).
|
|
3
|
+
|
|
4
|
+
Owns ONE server key inside the omp-native user MCP file (``<agent dir>/mcp.json``).
|
|
5
|
+
The file wrapper is always ``mcpServers`` and the stdio entry follows omp's OWN writer
|
|
6
|
+
layer — ``{command, args?, env?}`` with ``type`` omitted (stdio is the default) —
|
|
7
|
+
measured at ``mcp/config-writer.ts:111-143`` + ``config/mcp-schema.json`` on v18.0.0.
|
|
8
|
+
|
|
9
|
+
THE SERVER KEY IS A PINNED LITERAL, AND THAT IS THE WHOLE MECHANISM. omp already
|
|
10
|
+
translates Claude Code's ``~/.claude.json`` as an IMPORT provider, so on a host that ever
|
|
11
|
+
used Claude Code there is already an ``entwurf-bridge`` server — carrying Claude Code's
|
|
12
|
+
external agent id. This writer does not remove it, hide it, or ask the operator to: it
|
|
13
|
+
shadows it, and same-key first-wins is how. Provider priorities are native=100 >
|
|
14
|
+
omp-plugins=90 > claude=80 (``capability/index.ts:84-91``), dedupe is first-wins on
|
|
15
|
+
``key: server => server.name`` (``:183``), and on a key hit ``equivalent()`` is NEVER
|
|
16
|
+
consulted (``:203-207``) — so an entry whose env deliberately differs still suppresses the
|
|
17
|
+
import completely. A DIFFERENT key would load BOTH: two bridge tool families, one of them
|
|
18
|
+
still introducing this session as Claude Code.
|
|
19
|
+
|
|
20
|
+
``disabledServers`` IS NOT THE HIDE-IMPORT TOOL. Suppression is by NAME
|
|
21
|
+
(``mcp/config.ts:123-127``) and a suppressed item still claims the dedupe key
|
|
22
|
+
(``capability/index.ts:191-196``), so denylisting ``entwurf-bridge`` kills the native entry
|
|
23
|
+
and the import together. This adapter never writes that key, and the doctor goes red when
|
|
24
|
+
somebody else has.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import datetime
|
|
28
|
+
import json
|
|
29
|
+
import os
|
|
30
|
+
import sys
|
|
31
|
+
import tempfile
|
|
32
|
+
|
|
33
|
+
STATE_SCHEMA_VERSION = 1
|
|
34
|
+
|
|
35
|
+
# PINNED. The literal `mcpServers` map key the Claude import also uses — measured LIVE on
|
|
36
|
+
# oracle 2026-08-27 (`/mcp list` → "Claude Code (~/.claude.json): entwurf-bridge") and in
|
|
37
|
+
# source (`discovery/claude.ts:88-92` assigns the plain object key with no prefix). Changing
|
|
38
|
+
# this string does not rename our entry; it un-shadows the import.
|
|
39
|
+
SERVER_KEY = "entwurf-bridge"
|
|
40
|
+
|
|
41
|
+
# The provenance label this whole surface exists to fix: an omp session riding the import
|
|
42
|
+
# introduces itself to the bridge as Claude Code.
|
|
43
|
+
EXTERNAL_AGENT_ID = "external-mcp/omp"
|
|
44
|
+
ENV_KEY = "ENTWURF_BRIDGE_EXTERNAL_AGENT_ID"
|
|
45
|
+
|
|
46
|
+
SCHEMA_URL = "https://raw.githubusercontent.com/can1357/oh-my-pi/main/packages/coding-agent/src/config/mcp-schema.json"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def die(code: int, message: str) -> "None":
|
|
50
|
+
sys.stderr.write(message.rstrip("\n") + "\n")
|
|
51
|
+
raise SystemExit(code)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def load_object(path: str, label: str) -> dict:
|
|
55
|
+
try:
|
|
56
|
+
with open(path, "r", encoding="utf-8") as handle:
|
|
57
|
+
raw = handle.read()
|
|
58
|
+
except OSError as error:
|
|
59
|
+
die(4, f"omp-mcp: cannot read {label} {path}: {error}")
|
|
60
|
+
if not raw.strip():
|
|
61
|
+
return {}
|
|
62
|
+
try:
|
|
63
|
+
value = json.loads(raw)
|
|
64
|
+
except json.JSONDecodeError as error:
|
|
65
|
+
die(4, f"omp-mcp: {label} {path} is not valid JSON: {error}")
|
|
66
|
+
if not isinstance(value, dict):
|
|
67
|
+
die(4, f"omp-mcp: {label} {path} top-level must be a JSON object")
|
|
68
|
+
return value
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def atomic_write(path: str, value: dict) -> None:
|
|
72
|
+
directory = os.path.dirname(os.path.abspath(path))
|
|
73
|
+
os.makedirs(directory, exist_ok=True)
|
|
74
|
+
fd, temporary = tempfile.mkstemp(dir=directory, prefix=".omp-mcp-", suffix=".json")
|
|
75
|
+
try:
|
|
76
|
+
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
77
|
+
json.dump(value, handle, indent=2)
|
|
78
|
+
handle.write("\n")
|
|
79
|
+
os.replace(temporary, path)
|
|
80
|
+
except BaseException:
|
|
81
|
+
try:
|
|
82
|
+
os.unlink(temporary)
|
|
83
|
+
except FileNotFoundError:
|
|
84
|
+
pass
|
|
85
|
+
raise
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def now() -> str:
|
|
89
|
+
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def our_entry(command: str, args: list) -> dict:
|
|
93
|
+
# `type` is omitted on purpose: stdio is the default in omp's own schema, and the
|
|
94
|
+
# vendor's writer omits it too. `env` carries the one fact that makes this entry worth
|
|
95
|
+
# writing at all — the omp provenance label.
|
|
96
|
+
return {"command": command, "args": args, "env": {ENV_KEY: EXTERNAL_AGENT_ID}}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def checked_state(state_path: str) -> dict:
|
|
100
|
+
state = load_object(state_path, "install-state")
|
|
101
|
+
required = (
|
|
102
|
+
"managedConfigPath",
|
|
103
|
+
"serverKey",
|
|
104
|
+
"command",
|
|
105
|
+
"args",
|
|
106
|
+
"detectMode",
|
|
107
|
+
"configExistedBefore",
|
|
108
|
+
"preimage",
|
|
109
|
+
)
|
|
110
|
+
if state.get("schemaVersion") != STATE_SCHEMA_VERSION or any(key not in state for key in required):
|
|
111
|
+
die(4, f"omp-mcp: install-state {state_path} has an unsupported shape")
|
|
112
|
+
if state.get("serverKey") != SERVER_KEY:
|
|
113
|
+
die(4, f"omp-mcp: install-state {state_path} serverKey is not {SERVER_KEY}")
|
|
114
|
+
return state
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def prior_state(state_path: str, config_path: str) -> dict | None:
|
|
118
|
+
"""The ownership state for THIS target, or a refusal.
|
|
119
|
+
|
|
120
|
+
A state that names a DIFFERENT config is not "no state" (#87 D1). Treating it as
|
|
121
|
+
absent captured a fresh preimage and overwrote the single state file, stranding the
|
|
122
|
+
previous profile's managed entry with no inverse left to remove it. There is one
|
|
123
|
+
state and one target: changing the target is uninstall-then-install, in that order.
|
|
124
|
+
"""
|
|
125
|
+
if not os.path.exists(state_path):
|
|
126
|
+
return None
|
|
127
|
+
state = checked_state(state_path)
|
|
128
|
+
if state["managedConfigPath"] != os.path.abspath(config_path):
|
|
129
|
+
die(
|
|
130
|
+
3,
|
|
131
|
+
f"omp-mcp: install-state {state_path} already manages {state['managedConfigPath']}, "
|
|
132
|
+
f"but this host now reads {os.path.abspath(config_path)}. One state owns one target: "
|
|
133
|
+
"overwriting it would strand the first entry with no inverse. Run uninstall-omp-mcp with "
|
|
134
|
+
"the ORIGINAL omp agent dir / profile selected, then install here.",
|
|
135
|
+
)
|
|
136
|
+
return state
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def disabled_names(data: dict) -> list:
|
|
140
|
+
value = data.get("disabledServers")
|
|
141
|
+
return [name for name in value if isinstance(name, str)] if isinstance(value, list) else []
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def install(config_path: str, command: str, args_json: str, state_path: str) -> None:
|
|
145
|
+
if os.path.islink(config_path):
|
|
146
|
+
die(3, f"omp-mcp: refusing to adopt {config_path} — symlink to {os.readlink(config_path)} (someone else's SSOT)")
|
|
147
|
+
try:
|
|
148
|
+
args = json.loads(args_json)
|
|
149
|
+
except json.JSONDecodeError as error:
|
|
150
|
+
die(5, f"omp-mcp: args must be a JSON array: {error}")
|
|
151
|
+
if not isinstance(args, list) or not all(isinstance(a, str) for a in args):
|
|
152
|
+
die(5, "omp-mcp: args must be a JSON array of strings")
|
|
153
|
+
|
|
154
|
+
existed = os.path.exists(config_path)
|
|
155
|
+
data = load_object(config_path, "config") if existed else {"$schema": SCHEMA_URL}
|
|
156
|
+
servers = data.get("mcpServers")
|
|
157
|
+
if servers is None:
|
|
158
|
+
servers = {}
|
|
159
|
+
data["mcpServers"] = servers
|
|
160
|
+
if not isinstance(servers, dict):
|
|
161
|
+
die(4, f"omp-mcp: {config_path} mcpServers must be a JSON object")
|
|
162
|
+
|
|
163
|
+
# A pre-existing denylist on OUR key would suppress the entry we are about to write —
|
|
164
|
+
# and the import with it. Refuse rather than write a server nothing will ever load.
|
|
165
|
+
if SERVER_KEY in disabled_names(data):
|
|
166
|
+
die(
|
|
167
|
+
3,
|
|
168
|
+
f"omp-mcp: {config_path} lists {SERVER_KEY!r} in disabledServers. Suppression is by NAME "
|
|
169
|
+
"(mcp/config.ts:123-127) and a suppressed item still claims the dedupe key "
|
|
170
|
+
"(capability/index.ts:191-196), so this would kill BOTH the native entry and the Claude "
|
|
171
|
+
"import. Remove that denylist entry first — it is never the way to hide an import.",
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
prior = prior_state(state_path, config_path)
|
|
175
|
+
if prior is None:
|
|
176
|
+
state = {
|
|
177
|
+
"schemaVersion": STATE_SCHEMA_VERSION,
|
|
178
|
+
"managedConfigPath": os.path.abspath(config_path),
|
|
179
|
+
"serverKey": SERVER_KEY,
|
|
180
|
+
"command": command,
|
|
181
|
+
"args": args,
|
|
182
|
+
"detectMode": "adopt-regular-file" if existed else "created-new",
|
|
183
|
+
"configExistedBefore": existed,
|
|
184
|
+
# THE PREIMAGE IS THE CURRENT VALUE ON DISK, even when it is byte-identical to
|
|
185
|
+
# what we are about to write. Inventing a "there was nothing here before" case
|
|
186
|
+
# for one backend is the special-casing the rail exists to prevent.
|
|
187
|
+
"preimage": servers.get(SERVER_KEY),
|
|
188
|
+
"installedAt": now(),
|
|
189
|
+
}
|
|
190
|
+
else:
|
|
191
|
+
state = prior
|
|
192
|
+
|
|
193
|
+
servers[SERVER_KEY] = our_entry(command, args)
|
|
194
|
+
atomic_write(config_path, data)
|
|
195
|
+
atomic_write(state_path, state)
|
|
196
|
+
sys.stdout.write(f"{state['detectMode']} {os.path.abspath(config_path)}\n")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def uninstall(state_path: str) -> None:
|
|
200
|
+
if not os.path.exists(state_path):
|
|
201
|
+
die(2, f"omp-mcp: no install-state at {state_path} — nothing to undo")
|
|
202
|
+
state = checked_state(state_path)
|
|
203
|
+
config_path = state.get("managedConfigPath")
|
|
204
|
+
if not isinstance(config_path, str) or not os.path.isabs(config_path):
|
|
205
|
+
die(4, f"omp-mcp: install-state {state_path} has no absolute managedConfigPath")
|
|
206
|
+
if os.path.islink(config_path):
|
|
207
|
+
die(3, f"omp-mcp: refusing to uninstall — {config_path} became a symlink (someone else's SSOT)")
|
|
208
|
+
|
|
209
|
+
if os.path.exists(config_path):
|
|
210
|
+
data = load_object(config_path, "config")
|
|
211
|
+
servers = data.get("mcpServers")
|
|
212
|
+
if isinstance(servers, dict):
|
|
213
|
+
preimage = state.get("preimage")
|
|
214
|
+
if preimage is None:
|
|
215
|
+
servers.pop(SERVER_KEY, None)
|
|
216
|
+
else:
|
|
217
|
+
servers[SERVER_KEY] = preimage
|
|
218
|
+
created = state.get("detectMode") == "created-new" and state.get("configExistedBefore") is False
|
|
219
|
+
# We created this file, and nothing but our own two keys is left in it. The
|
|
220
|
+
# honest inverse of "created-new" is an absent file — a stub carrying only a
|
|
221
|
+
# $schema line would be an artifact of ours surviving our own removal.
|
|
222
|
+
only_ours = created and len(servers) == 0 and set(data.keys()) <= {"mcpServers", "$schema"}
|
|
223
|
+
if only_ours:
|
|
224
|
+
os.remove(config_path)
|
|
225
|
+
else:
|
|
226
|
+
atomic_write(config_path, data)
|
|
227
|
+
|
|
228
|
+
os.remove(state_path)
|
|
229
|
+
sys.stdout.write(f"uninstalled {config_path}\n")
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def native_entry_state(servers: dict) -> str:
|
|
233
|
+
"""`absent` | `invalid` | `present` — STRUCTURAL validity of the entry under our key.
|
|
234
|
+
|
|
235
|
+
Deliberately says nothing about ownership, about which command it names, or about
|
|
236
|
+
provenance: those are other axes with other verdicts. This one answers only "would omp
|
|
237
|
+
be able to load this at all", which is what makes an invalid entry red without an
|
|
238
|
+
install-state (#87 B4).
|
|
239
|
+
"""
|
|
240
|
+
if not isinstance(servers, dict) or SERVER_KEY not in servers:
|
|
241
|
+
return "absent"
|
|
242
|
+
server = servers.get(SERVER_KEY)
|
|
243
|
+
if not isinstance(server, dict):
|
|
244
|
+
return "invalid"
|
|
245
|
+
if server.get("type") not in (None, "stdio"):
|
|
246
|
+
return "invalid"
|
|
247
|
+
command = server.get("command")
|
|
248
|
+
if not isinstance(command, str) or not command:
|
|
249
|
+
return "invalid"
|
|
250
|
+
args = server.get("args", [])
|
|
251
|
+
if not isinstance(args, list) or not all(isinstance(arg, str) for arg in args):
|
|
252
|
+
return "invalid"
|
|
253
|
+
env = server.get("env", {})
|
|
254
|
+
if not isinstance(env, dict) or not all(isinstance(k, str) and isinstance(v, str) for k, v in env.items()):
|
|
255
|
+
return "invalid"
|
|
256
|
+
return "present"
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _doctor_invocation(config_path: str, command: str):
|
|
260
|
+
if os.path.islink(config_path):
|
|
261
|
+
return "symlink", None
|
|
262
|
+
if not os.path.exists(config_path):
|
|
263
|
+
return "file-absent", None
|
|
264
|
+
try:
|
|
265
|
+
data = load_object(config_path, "config")
|
|
266
|
+
except SystemExit:
|
|
267
|
+
return "invalid-json", None
|
|
268
|
+
if SERVER_KEY in disabled_names(data):
|
|
269
|
+
return "self-disabled", None
|
|
270
|
+
servers = data.get("mcpServers") if isinstance(data.get("mcpServers"), dict) else {}
|
|
271
|
+
# STRUCTURE first, through the one shared predicate — so "is this loadable at all" has
|
|
272
|
+
# a single answer here and in doctor-shadow, and cannot drift into two.
|
|
273
|
+
state = native_entry_state(servers)
|
|
274
|
+
if state == "absent":
|
|
275
|
+
return "not-ours", None
|
|
276
|
+
if state == "invalid":
|
|
277
|
+
return "invalid-entry", None
|
|
278
|
+
server = servers[SERVER_KEY]
|
|
279
|
+
found = server["command"]
|
|
280
|
+
args = server.get("args", [])
|
|
281
|
+
env = server.get("env", {})
|
|
282
|
+
if found != command:
|
|
283
|
+
return "not-ours", None
|
|
284
|
+
# The provenance label is the reason this entry exists. An entry that boots but
|
|
285
|
+
# introduces the session as some other harness is not our entry.
|
|
286
|
+
if env.get(ENV_KEY) != EXTERNAL_AGENT_ID:
|
|
287
|
+
return "foreign-provenance", None
|
|
288
|
+
return "configured", {"command": found, "args": args, "env": env}
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def doctor_static(config_path: str, command: str) -> None:
|
|
292
|
+
status, invocation = _doctor_invocation(config_path, command)
|
|
293
|
+
if status != "configured":
|
|
294
|
+
sys.stdout.write(f"{status}\n")
|
|
295
|
+
return
|
|
296
|
+
sys.stdout.write(f"configured {invocation['command']}\n")
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def doctor_invocation(config_path: str, command: str) -> None:
|
|
300
|
+
status, invocation = _doctor_invocation(config_path, command)
|
|
301
|
+
if status != "configured":
|
|
302
|
+
die(4, f"omp-mcp: cannot read configured invocation from {config_path}: {status}")
|
|
303
|
+
sys.stdout.write(json.dumps(invocation, separators=(",", ":")) + "\n")
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def doctor_shadow(config_path: str, *import_paths: str) -> None:
|
|
307
|
+
"""Is the EFFECTIVE `entwurf-bridge` this host's omp will load the native entry?
|
|
308
|
+
|
|
309
|
+
This is a CONFIGURATION read, never a runtime receipt: it reports what omp's own
|
|
310
|
+
precedence rules decide about the files on disk. The vendor's own `/mcp list` pane is
|
|
311
|
+
the live oracle, and it is taken once as a LIVE receipt rather than re-derived here.
|
|
312
|
+
|
|
313
|
+
Prints one line per fact, then a verdict line:
|
|
314
|
+
native <present|invalid|absent>
|
|
315
|
+
import <path> <key-present|key-absent|unreadable>
|
|
316
|
+
disabled <yes|no>
|
|
317
|
+
verdict <native-wins|native-invalid|import-wins|both-suppressed|no-entry>
|
|
318
|
+
|
|
319
|
+
RUNTIME VALIDITY AND OWNERSHIP ARE SEPARATE AXES (Hard Rule 13, #87 B4). "The key is
|
|
320
|
+
present" is not "the entry works": a null / non-object / malformed value under our key
|
|
321
|
+
still claims the dedupe slot, so the import is suppressed AND nothing loads. Reporting
|
|
322
|
+
that as `native-wins` turned a broken effective source into a PASS whenever no
|
|
323
|
+
ownership state happened to exist. An invalid entry gets its own verdict, and it is red
|
|
324
|
+
on the runtime axis whether or not entwurf owns anything here.
|
|
325
|
+
"""
|
|
326
|
+
data = {}
|
|
327
|
+
if os.path.exists(config_path) and not os.path.islink(config_path):
|
|
328
|
+
try:
|
|
329
|
+
data = load_object(config_path, "config")
|
|
330
|
+
except SystemExit:
|
|
331
|
+
data = {}
|
|
332
|
+
servers = data.get("mcpServers") if isinstance(data.get("mcpServers"), dict) else {}
|
|
333
|
+
native_state = native_entry_state(servers)
|
|
334
|
+
sys.stdout.write(f"native {native_state}\n")
|
|
335
|
+
|
|
336
|
+
import_hit = False
|
|
337
|
+
for path in import_paths:
|
|
338
|
+
expanded = os.path.expanduser(path)
|
|
339
|
+
if not os.path.exists(expanded):
|
|
340
|
+
continue
|
|
341
|
+
try:
|
|
342
|
+
imported = load_object(expanded, "imported config")
|
|
343
|
+
except SystemExit:
|
|
344
|
+
sys.stdout.write(f"import {expanded} unreadable\n")
|
|
345
|
+
continue
|
|
346
|
+
keys = imported.get("mcpServers")
|
|
347
|
+
present = isinstance(keys, dict) and SERVER_KEY in keys
|
|
348
|
+
import_hit = import_hit or present
|
|
349
|
+
sys.stdout.write(f"import {expanded} {'key-present' if present else 'key-absent'}\n")
|
|
350
|
+
|
|
351
|
+
disabled = SERVER_KEY in disabled_names(data)
|
|
352
|
+
sys.stdout.write(f"disabled {'yes' if disabled else 'no'}\n")
|
|
353
|
+
|
|
354
|
+
if disabled:
|
|
355
|
+
verdict = "both-suppressed"
|
|
356
|
+
elif native_state == "invalid":
|
|
357
|
+
# It still claims the dedupe key, so the import is suppressed too — but nothing
|
|
358
|
+
# loads. Never `native-wins`.
|
|
359
|
+
verdict = "native-invalid"
|
|
360
|
+
elif native_state == "present":
|
|
361
|
+
verdict = "native-wins"
|
|
362
|
+
elif import_hit:
|
|
363
|
+
verdict = "import-wins"
|
|
364
|
+
else:
|
|
365
|
+
verdict = "no-entry"
|
|
366
|
+
sys.stdout.write(f"verdict {verdict}\n")
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def main(argv: list[str]) -> None:
|
|
370
|
+
if len(argv) < 2:
|
|
371
|
+
die(5, "usage: omp-mcp-config.py <install|uninstall|doctor-static|doctor-invocation|doctor-shadow> ...")
|
|
372
|
+
match argv[1]:
|
|
373
|
+
case "install" if len(argv) == 6:
|
|
374
|
+
install(argv[2], argv[3], argv[4], argv[5])
|
|
375
|
+
case "uninstall" if len(argv) == 3:
|
|
376
|
+
uninstall(argv[2])
|
|
377
|
+
case "doctor-static" if len(argv) == 4:
|
|
378
|
+
doctor_static(argv[2], argv[3])
|
|
379
|
+
case "doctor-invocation" if len(argv) == 4:
|
|
380
|
+
doctor_invocation(argv[2], argv[3])
|
|
381
|
+
case "doctor-shadow" if len(argv) >= 3:
|
|
382
|
+
doctor_shadow(argv[2], *argv[3:])
|
|
383
|
+
case _:
|
|
384
|
+
die(
|
|
385
|
+
5,
|
|
386
|
+
"usage: omp-mcp-config.py <install config command args-json state|uninstall state|"
|
|
387
|
+
"doctor-static config command|doctor-invocation config command|doctor-shadow config [import...]>",
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
if __name__ == "__main__":
|
|
392
|
+
main(sys.argv)
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# omp-receive-doctor.sh — the fail-loud surface for the OMP RECEIVER extension
|
|
3
|
+
# (#87 bundle B).
|
|
4
|
+
#
|
|
5
|
+
# TWO AXES, REPORTED SEPARATELY, EITHER RED IS RED (AGENTS.md Hard Rule 13):
|
|
6
|
+
# runtime — is there a receiver unit omp will import, does it carry the CURRENT
|
|
7
|
+
# writer, what did it do the last time it ran, and is a doorbell actually
|
|
8
|
+
# held right now?
|
|
9
|
+
# ownership — does entwurf own that unit, by a state whose shape and bindings hold?
|
|
10
|
+
#
|
|
11
|
+
# WHAT "ARMED" IS ALLOWED TO MEAN HERE, AND WHAT IT IS NOT. A receiver marker records
|
|
12
|
+
# that a LIVE owner reached the watch-arm emit. It is NOT proof that the vendor's file
|
|
13
|
+
# watch is still registered — the Claude unit says the same about its own marker in the
|
|
14
|
+
# same words (`meta-bridge-hook.ts:279-280`), and on this rail the owner is the operator's
|
|
15
|
+
# TUI process itself, so a wedged event loop looks identical to a healthy one from out
|
|
16
|
+
# here. This doctor therefore reports what it can prove (a marker, its owner's liveness,
|
|
17
|
+
# the mailbox behind it) and never upgrades that into "a wake will happen".
|
|
18
|
+
#
|
|
19
|
+
# THE MARKER READ GOES THROUGH THE PRODUCTION READER, never a filename or a grep:
|
|
20
|
+
# `./run.sh omp-receive-facts` projects `readMetaReceiverMarker` for both readings — the
|
|
21
|
+
# live one dispatch gets, and the file as written — so "nothing armed" stays
|
|
22
|
+
# distinguishable from "armed, then the session died".
|
|
23
|
+
#
|
|
24
|
+
# ZERO ARMED RECEIVERS IS NOT-YET, NEVER RED. A host with the unit installed and no omp
|
|
25
|
+
# TUI open has nothing to arm; that is the designed resting state.
|
|
26
|
+
set -uo pipefail
|
|
27
|
+
|
|
28
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
29
|
+
REPO="$(cd "$HERE/.." && pwd)"
|
|
30
|
+
UNIT="entwurf-receive-omp"
|
|
31
|
+
SRC="$REPO/pi/omp-receive"
|
|
32
|
+
ASM="${ENTWURF_OMP_RECEIVE_ASM:-${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/omp-receive/.assembled}"
|
|
33
|
+
STATE_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/omp-receive"
|
|
34
|
+
STATE_FILE="$STATE_DIR/install-state.json"
|
|
35
|
+
BIRTH_STATE="${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/omp-bridge/install-state.json"
|
|
36
|
+
|
|
37
|
+
fail=0
|
|
38
|
+
own_fail=0
|
|
39
|
+
ok() { echo " ok $*"; }
|
|
40
|
+
note() { echo " note $*"; }
|
|
41
|
+
bad() { echo " FAIL $*"; fail=1; }
|
|
42
|
+
own_bad() { echo " FAIL $*"; own_fail=1; }
|
|
43
|
+
|
|
44
|
+
# shellcheck source=scripts/omp-bridge-oracle.sh
|
|
45
|
+
. "$HERE/omp-bridge-oracle.sh"
|
|
46
|
+
|
|
47
|
+
echo "[omp-receive-doctor] unit: $UNIT"
|
|
48
|
+
|
|
49
|
+
# ── root-grammar preflight, BEFORE anything is read ──────────────────────────
|
|
50
|
+
# Same rule and same reason as the birth doctor (#87 A2): the four overrides are absolute
|
|
51
|
+
# or `~`-rooted only. A relative value resolves against each process's own cwd, and this
|
|
52
|
+
# doctor does not share one with the extension, so asking anything first would answer
|
|
53
|
+
# about a different directory. A refused environment is RUNTIME RED, never laundered into
|
|
54
|
+
# a NOT-YET about some other store.
|
|
55
|
+
ROOT_POLICY_REFUSAL=""
|
|
56
|
+
for var in ENTWURF_META_SESSIONS_DIR ENTWURF_META_MAILBOX_DIR ENTWURF_META_SENDERS_DIR ENTWURF_META_RECEIVERS_DIR; do
|
|
57
|
+
val="${!var:-}"
|
|
58
|
+
[ -n "$val" ] || continue
|
|
59
|
+
case "$val" in
|
|
60
|
+
/*|"~"|"~/"*) ;;
|
|
61
|
+
*) ROOT_POLICY_REFUSAL="${ROOT_POLICY_REFUSAL:+$ROOT_POLICY_REFUSAL, }$var=$val" ;;
|
|
62
|
+
esac
|
|
63
|
+
done
|
|
64
|
+
|
|
65
|
+
echo
|
|
66
|
+
echo "[omp-receive-doctor] runtime axis"
|
|
67
|
+
|
|
68
|
+
AGENT_DIR=""
|
|
69
|
+
UNIT_DIR=""
|
|
70
|
+
if AGENT_DIR="$(omp_agent_dir 2>/dev/null)"; then
|
|
71
|
+
UNIT_DIR="$AGENT_DIR/extensions/$UNIT"
|
|
72
|
+
ok "omp agent directory: $AGENT_DIR"
|
|
73
|
+
else
|
|
74
|
+
bad "the omp agent directory this host reads is AMBIGUOUS (inherited PI_CODING_AGENT_DIR / PI_CONFIG_DIR / PI_PROFILE) — no honest statement about the installed unit can be made; pass ENTWURF_OMP_AGENT_DIR if you mean a non-default one"
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
# ── the artifact omp would import ────────────────────────────────────────────
|
|
78
|
+
INSTALLED_ENTRY=""
|
|
79
|
+
if [ -n "$UNIT_DIR" ]; then
|
|
80
|
+
if [ -L "$UNIT_DIR" ]; then
|
|
81
|
+
bad "$UNIT_DIR is a SYMLINK — this installer never creates one, so what omp would import is not ours"
|
|
82
|
+
elif [ -d "$UNIT_DIR" ]; then
|
|
83
|
+
if omp_assembly_valid "$(dirname "$UNIT_DIR")" "$UNIT" 2>/dev/null; then
|
|
84
|
+
[ -f "$UNIT_DIR/index.ts" ] && INSTALLED_ENTRY="index.ts"
|
|
85
|
+
[ -z "$INSTALLED_ENTRY" ] && [ -f "$UNIT_DIR/index.js" ] && INSTALLED_ENTRY="index.js"
|
|
86
|
+
ok "installed unit is structurally complete: $UNIT_DIR (entry $INSTALLED_ENTRY)"
|
|
87
|
+
else
|
|
88
|
+
bad "installed unit at $UNIT_DIR fails the structural oracle — omp may import a half-unit"
|
|
89
|
+
fi
|
|
90
|
+
else
|
|
91
|
+
note "NOT-INSTALLED: no receiver unit at $UNIT_DIR. omp citizens on this host are outbound-only; dispatch to them answers mailbox-undeliverable. Install with ./run.sh install-omp-receive"
|
|
92
|
+
fi
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
# ── writer parity: source vs assembled vs installed ──────────────────────────
|
|
96
|
+
# The same stale-writer axis the birth and Claude doctors carry. A deployed unit that
|
|
97
|
+
# predates the source is the failure that looks like success: everything is green and the
|
|
98
|
+
# behaviour is last week's.
|
|
99
|
+
digest() { [ -f "$1" ] && python3 -c "import hashlib,sys;print(hashlib.sha256(open(sys.argv[1],'rb').read()).hexdigest()[:12])" "$1" || echo "-"; }
|
|
100
|
+
SRC_ENTRY="$REPO/pi-extensions/meta-bridge-receive-omp.ts"
|
|
101
|
+
case "$REPO" in
|
|
102
|
+
*/node_modules/@junghanacs/entwurf) SRC_ENTRY="$REPO/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-receive-omp.js" ;;
|
|
103
|
+
esac
|
|
104
|
+
# TWO artifacts, not one. The ENTRY is this unit's own logic; `lib/meta-session` is the
|
|
105
|
+
# SHARED writer every native unit carries a copy of, and it is the one that goes stale
|
|
106
|
+
# silently — a backend id, a marker field or a root-policy fix lands in the source while
|
|
107
|
+
# three deployed bundles keep last week's copy. Checking only the entry would call such a
|
|
108
|
+
# host green (measured: it did, in this very smoke).
|
|
109
|
+
if [ -n "$INSTALLED_ENTRY" ]; then
|
|
110
|
+
LIB_EXT="ts"; [ "$INSTALLED_ENTRY" = "index.js" ] && LIB_EXT="js"
|
|
111
|
+
SRC_LIB="$REPO/pi-extensions/lib/meta-session.ts"
|
|
112
|
+
[ "$LIB_EXT" = "js" ] && SRC_LIB="$REPO/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js"
|
|
113
|
+
for pair in "entry:$SRC_ENTRY:$UNIT_DIR/$INSTALLED_ENTRY" "writer:$SRC_LIB:$UNIT_DIR/lib/meta-session.$LIB_EXT"; do
|
|
114
|
+
what="${pair%%:*}"; rest="${pair#*:}"; src="${rest%%:*}"; ins="${rest#*:}"
|
|
115
|
+
SRC_D="$(digest "$src")"; INS_D="$(digest "$ins")"
|
|
116
|
+
if [ "$SRC_D" = "$INS_D" ]; then
|
|
117
|
+
ok "$what parity: source == installed ($SRC_D)"
|
|
118
|
+
else
|
|
119
|
+
bad "STALE $what: source=$SRC_D installed=$INS_D ($ins) — the deployed receiver is not this checkout's. Re-run ./run.sh install-omp-receive (and 'pnpm run build-bridge' first in a dev clone if you changed the source)"
|
|
120
|
+
fi
|
|
121
|
+
done
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
# ── what the unit DID, off its own log ───────────────────────────────────────
|
|
125
|
+
# Arm failures and doorbell failures are judged on SEPARATE axes: a citizen can be armed
|
|
126
|
+
# and still fail to ring, and folding the two would print "nothing is armed" about a
|
|
127
|
+
# session whose marker is right there.
|
|
128
|
+
RECEIVE_LOG=""
|
|
129
|
+
if [ -n "$ROOT_POLICY_REFUSAL" ]; then
|
|
130
|
+
bad "the omp meta-root policy REFUSES this environment: $ROOT_POLICY_REFUSAL — a garden root must be absolute or ~-rooted. The extension receives the same refusal, so no store can be named here"
|
|
131
|
+
else
|
|
132
|
+
RECEIVE_LOG="${ENTWURF_META_SESSIONS_DIR:+$(dirname "$ENTWURF_META_SESSIONS_DIR")}"
|
|
133
|
+
[ -n "$RECEIVE_LOG" ] || RECEIVE_LOG="$HOME/.pi/agent"
|
|
134
|
+
RECEIVE_LOG="$RECEIVE_LOG/meta-bridge-receive-omp.log"
|
|
135
|
+
fi
|
|
136
|
+
if [ -n "$RECEIVE_LOG" ] && [ -f "$RECEIVE_LOG" ]; then
|
|
137
|
+
# `grep -c` PRINTS 0 and EXITS 1 on no-match, so the idiomatic `|| echo 0` appends a
|
|
138
|
+
# SECOND zero and every later `[ "$N" -gt 0 ]` dies with "integer expected" — caught
|
|
139
|
+
# here on a real host, where a stale-marker read went through a doctor that had already
|
|
140
|
+
# printed two shell errors. Count without the fallback and normalise once.
|
|
141
|
+
count_in_log() { grep -c -- "$1" "$RECEIVE_LOG" 2>/dev/null | head -1 | tr -cd '0-9'; }
|
|
142
|
+
ARMED_N="$(count_in_log " armed garden=")"; ARMED_N="${ARMED_N:-0}"
|
|
143
|
+
RANG_N="$(count_in_log " rang garden=")"; RANG_N="${RANG_N:-0}"
|
|
144
|
+
GAVEUP_N="$(count_in_log "arm-gave-up")"; GAVEUP_N="${GAVEUP_N:-0}"
|
|
145
|
+
ARMFAIL_N="$(grep -cE "arm-failed|arm-refused" "$RECEIVE_LOG" 2>/dev/null | head -1 | tr -cd '0-9')"; ARMFAIL_N="${ARMFAIL_N:-0}"
|
|
146
|
+
RINGFAIL_N="$(grep -cE "doorbell-failed|watch-error|signal-vanished" "$RECEIVE_LOG" 2>/dev/null | head -1 | tr -cd '0-9')"; RINGFAIL_N="${RINGFAIL_N:-0}"
|
|
147
|
+
ok "receive log: $RECEIVE_LOG (armed=$ARMED_N rang=$RANG_N)"
|
|
148
|
+
[ "$GAVEUP_N" -gt 0 ] && bad "$GAVEUP_N session(s) GAVE UP arming — birth never wrote a sender marker in that process. Those citizens were never addressable; check ./run.sh doctor-omp-bridge"
|
|
149
|
+
[ "$ARMFAIL_N" -gt 0 ] && bad "$ARMFAIL_N arm failure/refusal line(s) — a citizen exists but no doorbell was published for it:" && grep "arm-failed\|arm-refused" "$RECEIVE_LOG" | tail -3 | sed 's/^/ /'
|
|
150
|
+
[ "$RINGFAIL_N" -gt 0 ] && bad "$RINGFAIL_N doorbell/watch failure line(s) — the wake half broke on a live citizen:" && grep "doorbell-failed\|watch-error\|signal-vanished" "$RECEIVE_LOG" | tail -3 | sed 's/^/ /'
|
|
151
|
+
elif [ -n "$RECEIVE_LOG" ]; then
|
|
152
|
+
note "NOT-YET: no receive log at $RECEIVE_LOG — this unit has not run. Open an omp TUI and re-run"
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
# ── who is armed RIGHT NOW, through the production reader ────────────────────
|
|
156
|
+
if [ -z "$ROOT_POLICY_REFUSAL" ]; then
|
|
157
|
+
FACTS_RC=0
|
|
158
|
+
FACTS="$(env -u PI_CODING_AGENT_DIR "$REPO/run.sh" omp-receive-facts 2>&1)" || FACTS_RC=$?
|
|
159
|
+
if [ "$FACTS_RC" -ne 0 ]; then
|
|
160
|
+
bad "omp-receive-facts could not read the receiver markers (rc=$FACTS_RC):"
|
|
161
|
+
printf '%s\n' "$FACTS" | tail -3 | sed 's/^/ /'
|
|
162
|
+
else
|
|
163
|
+
SUMMARY="$(printf '%s' "$FACTS" | python3 -c '
|
|
164
|
+
import json, sys
|
|
165
|
+
d = json.load(sys.stdin)
|
|
166
|
+
rs = d.get("receivers") or []
|
|
167
|
+
live = [r for r in rs if r.get("ownerLive")]
|
|
168
|
+
dead = [r for r in rs if not r.get("ownerLive")]
|
|
169
|
+
print(len(rs), len(live), len(dead), len(d.get("unreadableMarkers") or []))
|
|
170
|
+
for r in live:
|
|
171
|
+
print("LIVE", r["gardenId"], r["ownerPid"], r["ownerKind"], r["unreadDelivered"], r["freshUnannounced"])
|
|
172
|
+
for r in dead:
|
|
173
|
+
print("DEAD", r["gardenId"], r["ownerPid"], r["ownerKind"], r["unreadDelivered"], r["freshUnannounced"])
|
|
174
|
+
' 2>/dev/null)" || SUMMARY=""
|
|
175
|
+
if [ -z "$SUMMARY" ]; then
|
|
176
|
+
bad "omp-receive-facts returned output this doctor could not parse — refusing to guess"
|
|
177
|
+
else
|
|
178
|
+
COUNTS="$(printf '%s' "$SUMMARY" | head -1)"
|
|
179
|
+
TOTAL="$(echo "$COUNTS" | cut -d' ' -f1)"; LIVE="$(echo "$COUNTS" | cut -d' ' -f2)"
|
|
180
|
+
DEAD="$(echo "$COUNTS" | cut -d' ' -f3)"; BADM="$(echo "$COUNTS" | cut -d' ' -f4)"
|
|
181
|
+
if [ "$TOTAL" -eq 0 ]; then
|
|
182
|
+
note "NOT-YET: no omp receiver markers. Nothing is armed, so dispatch to an omp citizen is the honest mailbox-undeliverable refusal. Open an omp TUI with this unit installed and re-run"
|
|
183
|
+
else
|
|
184
|
+
# A stale marker is not a fault: the reader already refuses it and dispatch is
|
|
185
|
+
# fail-closed. It is reported so an operator can tell "nothing armed" from
|
|
186
|
+
# "something armed and then the session died" — two states one count would hide.
|
|
187
|
+
if [ "$LIVE" -gt 0 ]; then
|
|
188
|
+
ok "$LIVE live / $DEAD stale omp receiver marker(s)"
|
|
189
|
+
else
|
|
190
|
+
note "0 live / $DEAD stale omp receiver marker(s) — nothing is armed right now, so dispatch to an omp citizen is the honest mailbox-undeliverable refusal"
|
|
191
|
+
fi
|
|
192
|
+
printf '%s\n' "$SUMMARY" | tail -n +2 | while read -r state gid pid kind unread fresh; do
|
|
193
|
+
if [ "$state" = "LIVE" ]; then
|
|
194
|
+
echo " armed $gid owner=$pid ($kind) unread=$unread not-yet-announced=$fresh"
|
|
195
|
+
[ "${fresh:-0}" -gt 0 ] && echo " note $fresh body/ies are enqueued but no doorbell has announced them — the watch may not be firing"
|
|
196
|
+
else
|
|
197
|
+
echo " stale $gid owner=$pid ($kind) — owner is gone; the reader already refuses this, dispatch is fail-closed"
|
|
198
|
+
fi
|
|
199
|
+
done
|
|
200
|
+
fi
|
|
201
|
+
[ "${BADM:-0}" -gt 0 ] && bad "$BADM receiver marker file(s) the production reader could not parse — inspect them by hand"
|
|
202
|
+
fi
|
|
203
|
+
fi
|
|
204
|
+
fi
|
|
205
|
+
|
|
206
|
+
# ── the two units this one is useless without ────────────────────────────────
|
|
207
|
+
# Not a duplicate of their own doctors — a NOTE, on the one axis this doctor owns:
|
|
208
|
+
# a doorbell with no birth has no citizen to join, and a doorbell with no tool hand rings
|
|
209
|
+
# for a model that has no entwurf_inbox_read to drain with.
|
|
210
|
+
if [ -f "$BIRTH_STATE" ]; then
|
|
211
|
+
ok "birth unit is installed (ownership state present) — this receiver has a citizen to join"
|
|
212
|
+
else
|
|
213
|
+
note "the BIRTH unit is not installed here (no $BIRTH_STATE). Without it nothing mints a citizen, so this receiver will log arm-deferred and then give up. Install with ./run.sh install-omp-bridge"
|
|
214
|
+
fi
|
|
215
|
+
if [ -n "$AGENT_DIR" ] && [ -f "$AGENT_DIR/mcp.json" ] && grep -q "entwurf-bridge" "$AGENT_DIR/mcp.json" 2>/dev/null; then
|
|
216
|
+
ok "an entwurf-bridge MCP entry exists in $AGENT_DIR/mcp.json — the model has a way to reach entwurf_inbox_read (doctor-omp-mcp owns that axis)"
|
|
217
|
+
else
|
|
218
|
+
note "no entwurf-bridge entry found in the omp MCP config. The doorbell announces a tool the model would not have; install with ./run.sh install-omp-mcp and verify with ./run.sh doctor-omp-mcp"
|
|
219
|
+
fi
|
|
220
|
+
|
|
221
|
+
# ── ownership axis ───────────────────────────────────────────────────────────
|
|
222
|
+
echo
|
|
223
|
+
echo "[omp-receive-doctor] ownership axis"
|
|
224
|
+
if [ -L "$STATE_FILE" ]; then
|
|
225
|
+
own_bad "ownership state $STATE_FILE is a SYMLINK — refusing to trust it"
|
|
226
|
+
elif [ -f "$STATE_FILE" ]; then
|
|
227
|
+
if [ -n "$UNIT_DIR" ] && omp_state_read "$STATE_FILE" "$UNIT_DIR" "$ASM" >/dev/null 2>&1; then
|
|
228
|
+
ok "ownership state binds this installation: $STATE_FILE"
|
|
229
|
+
if [ -n "$UNIT_DIR" ] && [ ! -e "$UNIT_DIR" ]; then
|
|
230
|
+
own_bad "ownership state claims $UNIT_DIR but nothing is there — the inverse (./run.sh uninstall-omp-receive) will refuse until this is resolved"
|
|
231
|
+
fi
|
|
232
|
+
else
|
|
233
|
+
own_bad "ownership state $STATE_FILE is corrupt or names a DIFFERENT installation than $UNIT_DIR"
|
|
234
|
+
fi
|
|
235
|
+
elif [ -n "$UNIT_DIR" ] && [ -e "$UNIT_DIR" ]; then
|
|
236
|
+
own_bad "$UNIT_DIR exists with NO ownership state. A directory that looks like our unit is not proof it is ours — the installer refuses to adopt it and the inverse refuses to remove it. Resolve by hand"
|
|
237
|
+
else
|
|
238
|
+
note "no ownership state and no unit — this host has no omp receiver installed (a clean absence, not a fault)"
|
|
239
|
+
fi
|
|
240
|
+
|
|
241
|
+
echo
|
|
242
|
+
if [ "$fail" -eq 0 ]; then echo "[omp-receive-doctor] runtime axis: PASS"; else echo "[omp-receive-doctor] runtime axis: FAIL"; fi
|
|
243
|
+
if [ "$own_fail" -eq 0 ]; then echo "[omp-receive-doctor] ownership axis: PASS"; else echo "[omp-receive-doctor] ownership axis: FAIL"; fi
|
|
244
|
+
if [ "$fail" -eq 0 ] && [ "$own_fail" -eq 0 ]; then echo "[omp-receive-doctor] PASS"; exit 0; fi
|
|
245
|
+
echo "[omp-receive-doctor] FAIL"
|
|
246
|
+
exit 1
|