@junghanacs/entwurf 0.15.1 → 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/CHANGELOG.md +229 -0
- package/DELIVERY.md +3 -2
- package/README.md +70 -87
- package/VERIFY.md +4 -1
- package/docs/acp-backend-rail.md +1 -1
- package/docs/external-mcp-host.md +147 -9
- package/docs/setup-clean-host.md +105 -0
- 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 +13 -3
- 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 +296 -4
- package/scripts/check-entwurf-self-address.ts +31 -0
- package/scripts/check-gate-qualification.ts +4 -1
- 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/inventory-verification-surface.ts +1 -1
- package/scripts/meta-bridge-hook-log.sh +9 -1
- package/scripts/mutants/copilot-birth.json +7 -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/self-address.json +34 -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/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/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/tsconfig.json +2 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"lane": "omp-receive",
|
|
4
|
+
"mutants": [
|
|
5
|
+
{
|
|
6
|
+
"claim": "OMP-RECEIVE-SCOPE-FENCE",
|
|
7
|
+
"title": "a receiver that arms on any mode publishes the HOST citizen's doorbell from inside a task subagent — two watchers on one mailbox, and a marker whose owner is not the session it names",
|
|
8
|
+
"subject": "pi-extensions/meta-bridge-receive-omp.ts",
|
|
9
|
+
"find": ["\t\tif (ctx.mode !== HOST_MODE) {"],
|
|
10
|
+
"replace": ["\t\tif (false && ctx.mode !== HOST_MODE) {"],
|
|
11
|
+
"gate": ["bash", "run.sh", "check-omp-receive-arm"],
|
|
12
|
+
"timeoutSeconds": 300,
|
|
13
|
+
"signature": "[QK:OMP-RECEIVE-SCOPE-FENCE]",
|
|
14
|
+
"signatureSource": "scripts/check-omp-receive-arm.ts"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"claim": "OMP-RECEIVE-NEW-RETIRES-OLD",
|
|
18
|
+
"title": "skipping the unarm on a birth edge leaves the PREVIOUS citizen armed after `/new` — same pid, same start key, so the marker reads live forever and dispatch enqueues into a mailbox nobody watches. The one false-deliverability cell the start-key guard structurally cannot catch",
|
|
19
|
+
"subject": "pi-extensions/meta-bridge-receive-omp.ts",
|
|
20
|
+
"find": ["\t\tthis.unarm(`edge=${edge}`);"],
|
|
21
|
+
"replace": ["\t\tif (!this.armed) this.unarm(`edge=${edge}`);"],
|
|
22
|
+
"gate": ["bash", "run.sh", "check-omp-receive-arm"],
|
|
23
|
+
"timeoutSeconds": 300,
|
|
24
|
+
"signature": "[QK:OMP-RECEIVE-NEW-RETIRES-OLD]",
|
|
25
|
+
"signatureSource": "scripts/check-omp-receive-arm.ts"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"claim": "OMP-RECEIVE-NO-WATCH-NO-MARKER",
|
|
29
|
+
"title": "writing the marker BEFORE the watch advertises a doorbell nobody is listening at — the fail-closed rule inverted, and invisible until a reply silently rots",
|
|
30
|
+
"subject": "pi-extensions/meta-bridge-receive-omp.ts",
|
|
31
|
+
"find": ["\t\t\twatcher = fs.watch(signal, () => this.ring(\"signal\"));"],
|
|
32
|
+
"replace": [
|
|
33
|
+
"\t\t\twriteMetaReceiverMarker({\n\t\t\t\tgardenId,\n\t\t\t\tbackend: BACKEND,\n\t\t\t\tnativeSessionId,\n\t\t\t\townerPid: this.deps.pid,\n\t\t\t\townerKind: \"omp-host\",\n\t\t\t\tarmProvenance: \"session-start\",\n\t\t\t\treceiversDir: bundle.receiversDir,\n\t\t\t});\n\t\t\twatcher = fs.watch(signal, () => this.ring(\"signal\"));"
|
|
34
|
+
],
|
|
35
|
+
"gate": ["bash", "run.sh", "check-omp-receive-arm"],
|
|
36
|
+
"timeoutSeconds": 300,
|
|
37
|
+
"signature": "[QK:OMP-RECEIVE-NO-WATCH-NO-MARKER]",
|
|
38
|
+
"signatureSource": "scripts/check-omp-receive-arm.ts"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"claim": "OMP-RECEIVE-ANNOUNCE-NEVER-PUSHES",
|
|
42
|
+
"title": "carrying the mailbox body into the doorbell turns every message into a prompt injection — the body is untrusted by construction, and the announce-only contract is the only thing keeping it out of the model's turn",
|
|
43
|
+
"subject": "pi-extensions/meta-bridge-receive-omp.ts",
|
|
44
|
+
"find": [
|
|
45
|
+
"\t\t\t\t\t`[entwurf inbox] ${unread} unread mailbox ${plural} available for garden ${armed.gardenId}. ` +"
|
|
46
|
+
],
|
|
47
|
+
"replace": [
|
|
48
|
+
"\t\t\t\t\t`[entwurf inbox] ${unread} unread mailbox ${plural} for garden ${armed.gardenId}: ${fs.readFileSync(path.join(armed.mailbox, `${fresh[0]}.delivered`), \"utf8\")} ` +"
|
|
49
|
+
],
|
|
50
|
+
"gate": ["bash", "run.sh", "check-omp-receive-arm"],
|
|
51
|
+
"timeoutSeconds": 300,
|
|
52
|
+
"signature": "[QK:OMP-RECEIVE-ANNOUNCE-NEVER-PUSHES]",
|
|
53
|
+
"signatureSource": "scripts/check-omp-receive-arm.ts"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"claim": "OMP-RECEIVE-NO-UNCANCELLABLE-TIMER",
|
|
57
|
+
"title": "starting the arm retry on a build with no `ctx.clearTimer` leaves a 500ms poll running inside the operator's TUI for the life of the session, with nothing able to stop it — the exact silent no-op measured when `clearInterval` was called through `?.`",
|
|
58
|
+
"subject": "pi-extensions/meta-bridge-receive-omp.ts",
|
|
59
|
+
"find": ["\t\tif (typeof ctx.setInterval !== \"function\" || typeof ctx.clearTimer !== \"function\") {"],
|
|
60
|
+
"replace": ["\t\tif (typeof ctx.setInterval !== \"function\") {"],
|
|
61
|
+
"gate": ["bash", "run.sh", "check-omp-receive-arm"],
|
|
62
|
+
"timeoutSeconds": 300,
|
|
63
|
+
"signature": "[QK:OMP-RECEIVE-NO-UNCANCELLABLE-TIMER]",
|
|
64
|
+
"signatureSource": "scripts/check-omp-receive-arm.ts"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"claim": "OMP-RECEIVE-REFUSES-WITHOUT-WAKE",
|
|
68
|
+
"title": "arming without the vendor's send surface advertises a citizen as addressable while nothing can ever wake it — a marker that promises a wake this process cannot perform",
|
|
69
|
+
"subject": "pi-extensions/meta-bridge-receive-omp.ts",
|
|
70
|
+
"find": ["\t\tif (!this.canRing()) {"],
|
|
71
|
+
"replace": ["\t\tif (false) {"],
|
|
72
|
+
"gate": ["bash", "run.sh", "check-omp-receive-arm"],
|
|
73
|
+
"timeoutSeconds": 300,
|
|
74
|
+
"signature": "[QK:OMP-RECEIVE-REFUSES-WITHOUT-WAKE]",
|
|
75
|
+
"signatureSource": "scripts/check-omp-receive-arm.ts"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"claim": "OMP-RECEIVE-RETRY-ARMS",
|
|
79
|
+
"title": "an arm that never retries leaves every session where this unit loaded before birth permanently unaddressable — and directory-name collation decides which of the two runs first, so the defect is invisible on the host that happened to sort favourably",
|
|
80
|
+
"subject": "pi-extensions/meta-bridge-receive-omp.ts",
|
|
81
|
+
"find": ["\t\tthis.startRetry(edge, ctx);"],
|
|
82
|
+
"replace": ["\t\tif (false) this.startRetry(edge, ctx);"],
|
|
83
|
+
"gate": ["bash", "run.sh", "check-omp-receive-arm"],
|
|
84
|
+
"timeoutSeconds": 300,
|
|
85
|
+
"signature": "[QK:OMP-RECEIVE-RETRY-ARMS]",
|
|
86
|
+
"signatureSource": "scripts/check-omp-receive-arm.ts"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"claim": "OMP-RECEIVE-BIND-LEAKS-NO-LISTENERS",
|
|
90
|
+
"title": "registering the exit guard at BIND time adds one process listener per session — subagents included — so a host that spawns enough of them prints MaxListenersExceededWarning into the operator's TUI",
|
|
91
|
+
"subject": "pi-extensions/meta-bridge-receive-omp.ts",
|
|
92
|
+
"find": ["\tconst receiver = new OmpReceiver(pi, deps);"],
|
|
93
|
+
"replace": [
|
|
94
|
+
"\tconst receiver = new OmpReceiver(pi, deps);\n\tprocess.on(\"exit\", () => receiver.unarm(\"process-exit\"));"
|
|
95
|
+
],
|
|
96
|
+
"gate": ["bash", "run.sh", "check-omp-receive-arm"],
|
|
97
|
+
"timeoutSeconds": 300,
|
|
98
|
+
"signature": "[QK:OMP-RECEIVE-BIND-LEAKS-NO-LISTENERS]",
|
|
99
|
+
"signatureSource": "scripts/check-omp-receive-arm.ts"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"claim": "OMP-RECEIVE-UNARM-IS-IDENTITY-GUARDED",
|
|
103
|
+
"title": "an unguarded teardown deletes a replacement receiver's marker for the same citizen — the live doorbell disappears because a dying instance tidied up after someone else",
|
|
104
|
+
"subject": "pi-extensions/meta-bridge-receive-omp.ts",
|
|
105
|
+
"find": ["\t\t\tif (mine && mine.ownerPid === this.deps.pid && mine.gardenId === armed.gardenId) {"],
|
|
106
|
+
"replace": ["\t\t\tif (mine) {"],
|
|
107
|
+
"gate": ["bash", "run.sh", "check-omp-receive-arm"],
|
|
108
|
+
"timeoutSeconds": 300,
|
|
109
|
+
"signature": "[QK:OMP-RECEIVE-UNARM-IS-IDENTITY-GUARDED]",
|
|
110
|
+
"signatureSource": "scripts/check-omp-receive-arm.ts"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"claim": "OMP-RECEIVE-ID-DRIFT-REFUSED",
|
|
114
|
+
"title": "trusting a STALE sender marker arms the doorbell off an id the vendor has already moved past — the record check does not cover it, because the record can agree with the vendor while the marker does not",
|
|
115
|
+
"subject": "pi-extensions/meta-bridge-receive-omp.ts",
|
|
116
|
+
"find": ["\tif (marker.nativeSessionId !== nativeSessionId) {"],
|
|
117
|
+
"replace": ["\tif (false) {"],
|
|
118
|
+
"gate": ["bash", "run.sh", "check-omp-receive-arm"],
|
|
119
|
+
"timeoutSeconds": 300,
|
|
120
|
+
"signature": "[QK:OMP-RECEIVE-ID-DRIFT-REFUSED]",
|
|
121
|
+
"signatureSource": "scripts/check-omp-receive-arm.ts"
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"claim": "OMP-RECEIVE-EDGE-CANCELS-OLD-RETRY",
|
|
125
|
+
"title": "skipping the stored cancel closure on a birth edge and nulling only the handle ledger leaves the FIRST context's 500ms poll running in the operator's TUI with nothing left able to stop it, and starts a second beside it — one orphan per birth edge that arrives while birth is still slow. The uncancellable-timer failure the arm path refuses to create, reintroduced on the cancel path",
|
|
126
|
+
"subject": "pi-extensions/meta-bridge-receive-omp.ts",
|
|
127
|
+
"find": ["\t\tthis.cancelRetry();\n\t\tthis.unarm(`edge=${edge}`);"],
|
|
128
|
+
"replace": ["\t\tthis.retryHandle = null;\n\t\tthis.retryCancel = null;\n\t\tthis.unarm(`edge=${edge}`);"],
|
|
129
|
+
"gate": ["bash", "run.sh", "check-omp-receive-arm"],
|
|
130
|
+
"timeoutSeconds": 300,
|
|
131
|
+
"signature": "[QK:OMP-RECEIVE-EDGE-CANCELS-OLD-RETRY]",
|
|
132
|
+
"signatureSource": "scripts/check-omp-receive-arm.ts"
|
|
133
|
+
}
|
|
134
|
+
]
|
|
135
|
+
}
|
|
@@ -54,6 +54,40 @@
|
|
|
54
54
|
"timeoutSeconds": 60,
|
|
55
55
|
"signature": "[QK:SELFADDR-RAIL-FACT-LEAK]",
|
|
56
56
|
"signatureSource": "scripts/check-entwurf-self-address.ts"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"claim": "SELFADDR-NO-FALLBACK-SELF-FETCH",
|
|
60
|
+
"title": "buildTrustedMetaSenderEnvelope falls every non-native-push backend back to self-fetch (omp renders a mailboxPath nothing drains)",
|
|
61
|
+
"subject": "mcp/entwurf-bridge/src/index.ts",
|
|
62
|
+
"find": [
|
|
63
|
+
"\tconst metaDeliveryDomain: MetaDeliveryDomain = nativePushSupported(identity.backend)",
|
|
64
|
+
"\t\t? \"native-push\"",
|
|
65
|
+
"\t\t: resolveMailboxWakeModeCapability(identity)",
|
|
66
|
+
"\t\t\t? \"self-fetch\"",
|
|
67
|
+
"\t\t\t: \"none\";"
|
|
68
|
+
],
|
|
69
|
+
"replace": [
|
|
70
|
+
"\tconst metaDeliveryDomain: MetaDeliveryDomain = nativePushSupported(identity.backend) ? \"native-push\" : \"self-fetch\";"
|
|
71
|
+
],
|
|
72
|
+
"gate": ["bash", "run.sh", "check-entwurf-self-address"],
|
|
73
|
+
"timeoutSeconds": 60,
|
|
74
|
+
"signature": "[QK:SELFADDR-NO-FALLBACK-SELF-FETCH]",
|
|
75
|
+
"signatureSource": "scripts/check-entwurf-self-address.ts"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"claim": "SELFADDR-MAILBOX-WAKE-SEAM",
|
|
79
|
+
"title": "self-fetch derivation uses a today-correct backend-name list instead of resolveMailboxWakeModeCapability — drifts when the registry admits another self-fetch backend",
|
|
80
|
+
"subject": "mcp/entwurf-bridge/src/index.ts",
|
|
81
|
+
"find": ["\t\t: resolveMailboxWakeModeCapability(identity)", "\t\t\t? \"self-fetch\"", "\t\t\t: \"none\";"],
|
|
82
|
+
"replace": [
|
|
83
|
+
"\t\t: identity.backend === \"claude-code\" || identity.backend === \"copilot\"",
|
|
84
|
+
"\t\t\t? \"self-fetch\"",
|
|
85
|
+
"\t\t\t: \"none\";"
|
|
86
|
+
],
|
|
87
|
+
"gate": ["bash", "run.sh", "check-entwurf-self-address"],
|
|
88
|
+
"timeoutSeconds": 60,
|
|
89
|
+
"signature": "[QK:SELFADDR-MAILBOX-WAKE-SEAM]",
|
|
90
|
+
"signatureSource": "scripts/check-entwurf-self-address.ts"
|
|
57
91
|
}
|
|
58
92
|
]
|
|
59
93
|
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# omp-bridge-doctor.sh — the fail-loud surface for the OMP BIRTH extension (#87).
|
|
3
|
+
#
|
|
4
|
+
# TWO AXES, REPORTED SEPARATELY, EITHER RED IS RED (AGENTS.md Hard Rule 13):
|
|
5
|
+
# runtime — is there a unit omp will actually import, does it carry the CURRENT
|
|
6
|
+
# writer, and what did it do the last time it ran?
|
|
7
|
+
# ownership — does entwurf own that unit, by a state whose shape and bindings hold?
|
|
8
|
+
# Runtime coverage does not prove ownership, and broken ownership does not erase a
|
|
9
|
+
# visibly working configuration.
|
|
10
|
+
#
|
|
11
|
+
# THE PLACEMENT LAYER, AND WHY THIS DOCTOR CAN READ IT AT ALL. omp offers two vendor
|
|
12
|
+
# surfaces for declaring an extension (ledger M2): a native root scan of
|
|
13
|
+
# `<agent-dir>/extensions` (`discovery/builtin.ts:483` → `discovery/helpers.ts:625-712`)
|
|
14
|
+
# and an `extensions:` array in a settings file. This unit uses the FIRST, so the whole
|
|
15
|
+
# installed artifact is one directory entwurf created and can remove exactly — there is
|
|
16
|
+
# no operator-owned SSOT to preimage, merge into, or accidentally clobber, and this
|
|
17
|
+
# doctor's runtime axis is a structural read rather than a diff against somebody else's
|
|
18
|
+
# config. (`~/.omp/agent/config.yml` carries the operator's model roles, status line and
|
|
19
|
+
# plan settings; nothing here writes to it. The MCP hand is the opposite case and does
|
|
20
|
+
# write a vendor config file, with the preimage/inverse discipline that comes with it —
|
|
21
|
+
# see doctor-omp-mcp.)
|
|
22
|
+
#
|
|
23
|
+
# WHAT IT DELIBERATELY DOES NOT CLAIM. A green runtime axis is not a live citizen: only
|
|
24
|
+
# a real omp TUI session minting a record is that (`adding-a-harness.md` step 3(b)). An
|
|
25
|
+
# installed unit with zero omp records is reported as NOT-YET, never as red.
|
|
26
|
+
set -uo pipefail
|
|
27
|
+
|
|
28
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
29
|
+
REPO="$(cd "$HERE/.." && pwd)"
|
|
30
|
+
UNIT="entwurf-meta-omp"
|
|
31
|
+
SRC="$REPO/pi/meta-bridge-omp"
|
|
32
|
+
ASM="${ENTWURF_OMP_ASM:-${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/meta-bridge-omp/.assembled}"
|
|
33
|
+
STATE_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/omp-bridge"
|
|
34
|
+
STATE_FILE="$STATE_DIR/install-state.json"
|
|
35
|
+
|
|
36
|
+
fail=0
|
|
37
|
+
own_fail=0
|
|
38
|
+
ok() { echo " ok $*"; }
|
|
39
|
+
note() { echo " note $*"; }
|
|
40
|
+
bad() { echo " FAIL $*"; fail=1; }
|
|
41
|
+
own_bad() { echo " FAIL $*"; own_fail=1; }
|
|
42
|
+
|
|
43
|
+
# shellcheck source=scripts/omp-bridge-oracle.sh
|
|
44
|
+
. "$HERE/omp-bridge-oracle.sh"
|
|
45
|
+
|
|
46
|
+
echo "[omp birth extension — runtime axis]"
|
|
47
|
+
|
|
48
|
+
if ! command -v omp >/dev/null 2>&1; then
|
|
49
|
+
note "the 'omp' CLI is not on PATH — this host has no omp to be a citizen of (entwurf never installs one)"
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
AGENT_DIR="$(omp_agent_dir)"
|
|
53
|
+
if [ -z "$AGENT_DIR" ]; then
|
|
54
|
+
bad "the omp agent directory this host reads is AMBIGUOUS (see the reason above) — no verdict can be given about a directory we refuse to guess"
|
|
55
|
+
AGENT_DIR=""
|
|
56
|
+
UNIT_DIR=""
|
|
57
|
+
else
|
|
58
|
+
UNIT_DIR="$AGENT_DIR/extensions/$UNIT"
|
|
59
|
+
ok "effective omp agent dir: $AGENT_DIR (unit path: $UNIT_DIR)"
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
if [ -n "$UNIT_DIR" ]; then
|
|
63
|
+
if [ -L "$UNIT_DIR" ]; then
|
|
64
|
+
bad "$UNIT_DIR is a SYMLINK — this installer never creates one, so what omp imports there is not ours"
|
|
65
|
+
elif [ ! -d "$UNIT_DIR" ]; then
|
|
66
|
+
if [ -f "$STATE_FILE" ]; then
|
|
67
|
+
bad "ownership state exists but the unit is GONE: $UNIT_DIR — no omp session can be born. Re-run ./run.sh install-omp-bridge"
|
|
68
|
+
else
|
|
69
|
+
note "no unit installed at $UNIT_DIR and no ownership state — this host has not run install-omp-bridge (zero state, not a fault)"
|
|
70
|
+
fi
|
|
71
|
+
elif omp_assembly_valid "$(dirname "$UNIT_DIR")" "$UNIT" 2>/tmp/omp-doctor-oracle.$$; then
|
|
72
|
+
ENTRY_NAME="index.ts"; [ -f "$UNIT_DIR/index.ts" ] || ENTRY_NAME="index.js"
|
|
73
|
+
ok "installed unit is structurally complete (entry $ENTRY_NAME + lib + capability registry + type:module)"
|
|
74
|
+
ok "omp discovers it by its own native rule: <agent-dir>/extensions/$UNIT/$ENTRY_NAME (discovery/helpers.ts:700-710)"
|
|
75
|
+
# WRITER PARITY. The unit carries a COPY of the shared V3 writer and of the capability
|
|
76
|
+
# registry, exactly like the Claude and Copilot units. A stale copy is the failure mode
|
|
77
|
+
# `adding-a-harness.md` step 2(c) names: the deployed reader refuses records the new
|
|
78
|
+
# backend list admits, including its own. Same judgement, same prescription.
|
|
79
|
+
LIB_EXT="ts"; [ "$ENTRY_NAME" = "index.js" ] && LIB_EXT="js"
|
|
80
|
+
case "$REPO" in
|
|
81
|
+
*/node_modules/@junghanacs/entwurf) SRC_LIB="$REPO/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js"; SRC_ENTRY="$REPO/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js" ;;
|
|
82
|
+
*) SRC_LIB="$REPO/pi-extensions/lib/meta-session.ts"; SRC_ENTRY="$REPO/pi-extensions/meta-bridge-omp.ts" ;;
|
|
83
|
+
esac
|
|
84
|
+
for pair in "lib/meta-session.$LIB_EXT|$SRC_LIB|writer" "$ENTRY_NAME|$SRC_ENTRY|birth entry" "entwurf-capabilities.json|$REPO/pi/entwurf-capabilities.json|capability registry"; do
|
|
85
|
+
rel="${pair%%|*}"; rest="${pair#*|}"; src="${rest%%|*}"; label="${rest##*|}"
|
|
86
|
+
if [ ! -f "$src" ]; then
|
|
87
|
+
note "$label source is absent in this checkout ($src) — parity not measurable here"
|
|
88
|
+
continue
|
|
89
|
+
fi
|
|
90
|
+
INST_SHA="$(sha256sum "$UNIT_DIR/$rel" 2>/dev/null | cut -c1-12)"
|
|
91
|
+
SRC_SHA="$(sha256sum "$src" 2>/dev/null | cut -c1-12)"
|
|
92
|
+
if [ -n "$INST_SHA" ] && [ "$INST_SHA" = "$SRC_SHA" ]; then
|
|
93
|
+
ok "installed $label matches source ($INST_SHA)"
|
|
94
|
+
else
|
|
95
|
+
bad "installed $label is STALE: installed=${INST_SHA:-missing} vs source=$SRC_SHA ($UNIT_DIR/$rel). The live omp extension is running old bytes — run ./run.sh install-omp-bridge, then restart any open omp session"
|
|
96
|
+
fi
|
|
97
|
+
done
|
|
98
|
+
else
|
|
99
|
+
bad "installed unit at $UNIT_DIR fails the structural oracle: $(cat /tmp/omp-doctor-oracle.$$ 2>/dev/null)"
|
|
100
|
+
fi
|
|
101
|
+
rm -f /tmp/omp-doctor-oracle.$$
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
# ── WHERE THE GARDEN ROOTS ARE, under the SAME policy production uses (#87 B1) ──
|
|
105
|
+
# NOT `PI_CODING_AGENT_DIR ?? ~/.pi/agent`. For backend omp that variable is the VENDOR's
|
|
106
|
+
# agent dir — `setProfile` exports it for every named profile (`oh-my-pi` v18.0.0
|
|
107
|
+
# `utils/src/dirs.ts:452-473`) — so reading it here would point this doctor at a different
|
|
108
|
+
# store than the one the extension and the bridge child actually write to. The OMP policy
|
|
109
|
+
# is: the four `ENTWURF_META_*` overrides, else `<HOME>/.pi/agent/<surface>`, never PI.
|
|
110
|
+
#
|
|
111
|
+
# We do not re-implement that policy in bash. We reach it the same way the OMP bridge child
|
|
112
|
+
# does — drop the foreign variable, then let entwurf's own default resolver answer — and we
|
|
113
|
+
# ask the OWNER for the answer: `meta-facts` (#65) is the read-only projection of the
|
|
114
|
+
# CERTIFIED store, so it reports both the store directory it resolved and the records that
|
|
115
|
+
# passed certification. One call, two facts, no second parser.
|
|
116
|
+
#
|
|
117
|
+
# PI presence itself is NOT a fault and is never reported as one: under `omp --profile work`
|
|
118
|
+
# the vendor sets it deliberately. It is simply not a garden-root input.
|
|
119
|
+
# PREFLIGHT THE SAME GRAMMAR THE POLICY ENFORCES, BEFORE ASKING ANYTHING (#87 A2). The
|
|
120
|
+
# overrides are absolute or `~`/`~/…` only. A relative value would resolve against each
|
|
121
|
+
# process's own working directory, and this doctor does NOT share one with the extension —
|
|
122
|
+
# `run_ts` cd's to the repository. Asking `meta-facts` first would therefore answer from an
|
|
123
|
+
# unrelated empty directory and print NOT-YET (or PASS) about a store that is not the one
|
|
124
|
+
# the extension writes to. A refused environment is RUNTIME RED here, never laundered.
|
|
125
|
+
ROOT_POLICY_REFUSAL=""
|
|
126
|
+
for var in ENTWURF_META_SESSIONS_DIR ENTWURF_META_MAILBOX_DIR ENTWURF_META_SENDERS_DIR ENTWURF_META_RECEIVERS_DIR; do
|
|
127
|
+
val="${!var:-}"
|
|
128
|
+
[ -n "$val" ] || continue
|
|
129
|
+
case "$val" in
|
|
130
|
+
/*|"~"|"~/"*) ;;
|
|
131
|
+
*) ROOT_POLICY_REFUSAL="${ROOT_POLICY_REFUSAL:+$ROOT_POLICY_REFUSAL, }$var=$val" ;;
|
|
132
|
+
esac
|
|
133
|
+
done
|
|
134
|
+
|
|
135
|
+
FACTS_RC=0
|
|
136
|
+
FACTS_JSON=""
|
|
137
|
+
STORE=""
|
|
138
|
+
OMP_RECORDS=0
|
|
139
|
+
STORE_DEFECTS=0
|
|
140
|
+
if [ -n "$ROOT_POLICY_REFUSAL" ]; then
|
|
141
|
+
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 and mints nothing; no store can be named here, so this is RED rather than a NOT-YET verdict about some other directory"
|
|
142
|
+
else
|
|
143
|
+
FACTS_JSON="$(env -u PI_CODING_AGENT_DIR "$REPO/run.sh" meta-facts 2>&1)" || FACTS_RC=$?
|
|
144
|
+
fi
|
|
145
|
+
if [ -z "$ROOT_POLICY_REFUSAL" ] && [ "$FACTS_RC" -eq 0 ]; then
|
|
146
|
+
FACTS_LINE="$(printf '%s' "$FACTS_JSON" | python3 -c '
|
|
147
|
+
import json, sys
|
|
148
|
+
facts = json.load(sys.stdin)
|
|
149
|
+
citizens = facts.get("citizens") or []
|
|
150
|
+
omp = [c for c in citizens if c.get("backend") == "omp"]
|
|
151
|
+
print(facts.get("storeDir", ""), len(omp), len(facts.get("defects") or []))
|
|
152
|
+
' 2>/dev/null)" || FACTS_LINE=""
|
|
153
|
+
if [ -n "$FACTS_LINE" ]; then
|
|
154
|
+
STORE="${FACTS_LINE%% *}"
|
|
155
|
+
REST="${FACTS_LINE#* }"
|
|
156
|
+
OMP_RECORDS="${REST%% *}"
|
|
157
|
+
STORE_DEFECTS="${REST##* }"
|
|
158
|
+
fi
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
if [ -n "$ROOT_POLICY_REFUSAL" ]; then
|
|
162
|
+
AGENT=""
|
|
163
|
+
HOOK_LOG=""
|
|
164
|
+
elif [ -z "$STORE" ]; then
|
|
165
|
+
bad "the certified record store could not be read (meta-facts rc=$FACTS_RC) — no honest statement about omp citizens can be made on this host:"
|
|
166
|
+
printf '%s\n' "$FACTS_JSON" | tail -3 | sed 's/^/ /'
|
|
167
|
+
AGENT=""
|
|
168
|
+
HOOK_LOG=""
|
|
169
|
+
else
|
|
170
|
+
AGENT="$(dirname "$STORE")"
|
|
171
|
+
HOOK_LOG="$AGENT/meta-bridge-hook.log"
|
|
172
|
+
ok "omp garden roots resolve to $AGENT (ENTWURF_META_* overrides, else \$HOME/.pi/agent — PI_CODING_AGENT_DIR is vendor-owned here and ignored)"
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
# ── what the unit DID, read off the shared hook log ──────────────────────────
|
|
176
|
+
# Mint errors and marker errors are judged on SEPARATE axes, and that separation is not
|
|
177
|
+
# stylistic: a failed marker write lands AFTER the successful mint line, so a doctor that
|
|
178
|
+
# folded them together would read "the hook ran and did not mint" about a session whose
|
|
179
|
+
# record is right there (`adding-a-harness.md` step 6, measured on Copilot).
|
|
180
|
+
echo
|
|
181
|
+
echo "[what the extension did — ${HOOK_LOG:-<unresolved>}]"
|
|
182
|
+
if [ -n "$HOOK_LOG" ] && [ -f "$HOOK_LOG" ]; then
|
|
183
|
+
MINT_ERRORS=' ERROR \[omp\] (?!sender-marker-)'
|
|
184
|
+
LAST_ERROR_LINE="$(grep -nP "$MINT_ERRORS" "$HOOK_LOG" 2>/dev/null | tail -1 | cut -d: -f1)"
|
|
185
|
+
LAST_OK_LINE="$(grep -n ' INFO \[omp\] \(create\|attach\) ' "$HOOK_LOG" 2>/dev/null | tail -1 | cut -d: -f1)"
|
|
186
|
+
TOTAL_ERRORS="$(grep -cP "$MINT_ERRORS" "$HOOK_LOG" 2>/dev/null | head -1)"; TOTAL_ERRORS="${TOTAL_ERRORS:-0}"
|
|
187
|
+
if [ -z "$LAST_ERROR_LINE" ]; then
|
|
188
|
+
ok "no omp mint ERROR lines in $HOOK_LOG"
|
|
189
|
+
elif [ -n "$LAST_OK_LINE" ] && [ "$LAST_OK_LINE" -gt "$LAST_ERROR_LINE" ]; then
|
|
190
|
+
note "$TOTAL_ERRORS historical omp mint ERROR line(s), all followed by a successful mint (line $LAST_OK_LINE > $LAST_ERROR_LINE) — recovered, not red"
|
|
191
|
+
else
|
|
192
|
+
bad "the newest omp mint line in $HOOK_LOG is an unrecovered ERROR — the extension RAN and did not mint:"
|
|
193
|
+
grep -P "$MINT_ERRORS" "$HOOK_LOG" | tail -3 | sed 's/^/ /'
|
|
194
|
+
fi
|
|
195
|
+
|
|
196
|
+
MARKER_FAILED="$(grep -c ' ERROR \[omp\] sender-marker-failed ' "$HOOK_LOG" 2>/dev/null | head -1)"
|
|
197
|
+
MARKER_REFUSED="$(grep -c ' WARN \[omp\] sender-marker-refused ' "$HOOK_LOG" 2>/dev/null | head -1)"
|
|
198
|
+
MARKER_OK="$(grep -c ' INFO \[omp\] sender marker ' "$HOOK_LOG" 2>/dev/null | head -1)"
|
|
199
|
+
if [ "${MARKER_FAILED:-0}" -gt 0 ]; then
|
|
200
|
+
bad "${MARKER_FAILED} sender-marker WRITE failure(s) — those citizens exist but cannot send under their own garden id:"
|
|
201
|
+
grep ' ERROR \[omp\] sender-marker-failed ' "$HOOK_LOG" | tail -3 | sed 's/^/ /'
|
|
202
|
+
elif [ "${MARKER_REFUSED:-0}" -gt 0 ]; then
|
|
203
|
+
note "${MARKER_REFUSED} sender-marker refusal(s) and ${MARKER_OK:-0} armed — a refusal is fail-closed, not a fault"
|
|
204
|
+
elif [ "${MARKER_OK:-0}" -gt 0 ]; then
|
|
205
|
+
ok "${MARKER_OK} sender marker(s) armed — those citizens send under their own garden id"
|
|
206
|
+
else
|
|
207
|
+
note "no omp sender-marker lines yet (nothing has fired on this host)"
|
|
208
|
+
fi
|
|
209
|
+
|
|
210
|
+
# The SCOPE FENCE leaves its own receipt, and it is evidence rather than noise: one
|
|
211
|
+
# line per session this unit refused to mint, naming the mode. A host with subagent
|
|
212
|
+
# traffic and zero refusals would mean the fence never ran.
|
|
213
|
+
SCOPE_REFUSED="$(grep -c ' INFO \[omp\] scope-refused ' "$HOOK_LOG" 2>/dev/null | head -1)"
|
|
214
|
+
STATUS_ISSUES="$(grep -c ' WARN \[omp\] status-' "$HOOK_LOG" 2>/dev/null | head -1)"
|
|
215
|
+
note "${SCOPE_REFUSED:-0} non-tui session(s) refused by the §3.5 scope fence — the designed answer for task subagents, rpc/rpc-ui and acp"
|
|
216
|
+
if [ "${STATUS_ISSUES:-0}" -gt 0 ]; then
|
|
217
|
+
note "${STATUS_ISSUES} visible-identity warning(s) — the garden id did not render on the status line for those sessions (statusLine.showHookStatus is default true; check the operator's config.yml)"
|
|
218
|
+
fi
|
|
219
|
+
elif [ -n "$HOOK_LOG" ]; then
|
|
220
|
+
note "no hook log yet at $HOOK_LOG (nothing has fired on this host)"
|
|
221
|
+
fi
|
|
222
|
+
|
|
223
|
+
# ── the records themselves, read through the CERTIFIED surface ───────────────
|
|
224
|
+
# NEVER a text grep (#87 B3). "This host has an omp garden citizen" is a claim on the
|
|
225
|
+
# record-authority axis, and the production writer earns it by certifying the WHOLE active
|
|
226
|
+
# store before writing — regular non-symlink files, live V3 schema, filename↔body
|
|
227
|
+
# agreement, unique nativeSessionId (`meta-session.ts` certifyActiveStore). A grep for the
|
|
228
|
+
# text `"backend": "omp"` matched a file containing nothing else and printed PASS. A doctor
|
|
229
|
+
# that claims admission must not weaken the contract the writer holds, so the count above
|
|
230
|
+
# comes from `meta-facts`' certified citizen list.
|
|
231
|
+
if [ -n "$STORE" ]; then
|
|
232
|
+
if [ "${STORE_DEFECTS:-0}" -gt 0 ]; then
|
|
233
|
+
note "$STORE_DEFECTS uncertifiable entr(ies) in $STORE are excluded from this count — run ./run.sh doctor-meta-bridge, which owns that axis"
|
|
234
|
+
fi
|
|
235
|
+
if [ "${OMP_RECORDS:-0}" -gt 0 ]; then
|
|
236
|
+
ok "$OMP_RECORDS CERTIFIED omp meta-record(s) in $STORE (garden citizen proven on this host)"
|
|
237
|
+
else
|
|
238
|
+
note "NOT-YET: zero certified omp meta-records. An omp TUI session is born when it OPENS (session_start fires after first paint, before the first prompt) — open one and re-run this doctor"
|
|
239
|
+
fi
|
|
240
|
+
fi
|
|
241
|
+
|
|
242
|
+
# ── §6 identity-carrier contamination, DETECTED (never silently preferred) ───
|
|
243
|
+
# `adding-a-harness.md` step 6 and `docs/external-mcp-host.md`: a complete
|
|
244
|
+
# PI_SESSION_ID + PI_AGENT_ID pair WINS over a native sender marker in the bridge's
|
|
245
|
+
# authoritative-self resolution, so an omp session started from a pi citizen's bash — and
|
|
246
|
+
# every internal agent borrowing its MCP manager — would speak under the parent pi garden
|
|
247
|
+
# id. Bundle C's managed fresh launch has no per-window unset form in tmux, so it writes
|
|
248
|
+
# both names EMPTY instead. That is an honest scrub: authoritative readers trim and require
|
|
249
|
+
# truthy values, so an empty or whitespace-only value is nonauthoritative just like absence.
|
|
250
|
+
# This doctor detects only an actual nonblank carrier and reports it on its own axis rather
|
|
251
|
+
# than absorbing it into any other verdict.
|
|
252
|
+
echo
|
|
253
|
+
echo "[inherited pi identity carriers on live omp processes]"
|
|
254
|
+
if [ -d /proc ]; then
|
|
255
|
+
CONTAMINATED=""
|
|
256
|
+
# Candidate set. Production scans every `omp` pid. `ENTWURF_OMP_CARRIER_PIDS` (set, even
|
|
257
|
+
# to empty) narrows it so a hermetic smoke can hand this branch REAL processes it launched
|
|
258
|
+
# and read their real `/proc/<pid>/environ`. It narrows candidates ONLY — the nonblank
|
|
259
|
+
# predicate below is the same production code either way.
|
|
260
|
+
if [ -n "${ENTWURF_OMP_CARRIER_PIDS+x}" ]; then
|
|
261
|
+
CARRIER_CANDIDATES="$ENTWURF_OMP_CARRIER_PIDS"
|
|
262
|
+
else
|
|
263
|
+
CARRIER_CANDIDATES="$(pgrep -x omp 2>/dev/null || true)"
|
|
264
|
+
fi
|
|
265
|
+
for pid in $CARRIER_CANDIDATES; do
|
|
266
|
+
case "$pid" in *[!0-9]*) continue ;; esac
|
|
267
|
+
ENVIRON="/proc/$pid/environ"
|
|
268
|
+
[ -r "$ENVIRON" ] || continue
|
|
269
|
+
if tr '\0' '\n' < "$ENVIRON" 2>/dev/null | grep -qE '^(PI_SESSION_ID|PI_AGENT_ID)=.*[^[:space:]]'; then
|
|
270
|
+
CONTAMINATED="$CONTAMINATED $pid"
|
|
271
|
+
fi
|
|
272
|
+
done
|
|
273
|
+
if [ -n "$CONTAMINATED" ]; then
|
|
274
|
+
bad "live omp process(es)$CONTAMINATED carry a non-empty PI_SESSION_ID/PI_AGENT_ID inherited from a pi citizen's shell. Their MCP children would speak under the PARENT pi garden id, not their own — close them and relaunch omp from a shell without those variables"
|
|
275
|
+
else
|
|
276
|
+
ok "no live omp process carries a non-empty PI_SESSION_ID/PI_AGENT_ID (empty carrier values are the managed tmux scrub and are nonauthoritative; omp mints neither itself — the danger is pure inheritance passthrough, ledger M6)"
|
|
277
|
+
fi
|
|
278
|
+
else
|
|
279
|
+
note "/proc is unavailable, so live omp environments could not be read on this platform"
|
|
280
|
+
fi
|
|
281
|
+
|
|
282
|
+
# ── ownership axis ───────────────────────────────────────────────────────────
|
|
283
|
+
echo
|
|
284
|
+
echo "[ownership axis]"
|
|
285
|
+
if [ -L "$STATE_FILE" ]; then
|
|
286
|
+
own_bad "ownership state $STATE_FILE is a SYMLINK — refusing to trust it"
|
|
287
|
+
elif [ -f "$STATE_FILE" ]; then
|
|
288
|
+
if STATE_FACTS="$(omp_state_read "$STATE_FILE" "${UNIT_DIR:-$STATE_FILE}" "$ASM" 2>&1)"; then
|
|
289
|
+
ok "ownership state is well-formed and bound to this installation (version ${STATE_FACTS%% *}, entry ${STATE_FACTS##* })"
|
|
290
|
+
if [ -d "$ASM/$UNIT" ]; then
|
|
291
|
+
ok "the recorded assembly is present: $ASM/$UNIT"
|
|
292
|
+
else
|
|
293
|
+
own_bad "ownership state names an assembly that is gone: $ASM/$UNIT — the inverse can no longer prove what it placed. Re-run ./run.sh install-omp-bridge"
|
|
294
|
+
fi
|
|
295
|
+
else
|
|
296
|
+
own_bad "ownership state is corrupt or names a different installation: $STATE_FACTS"
|
|
297
|
+
fi
|
|
298
|
+
else
|
|
299
|
+
if [ -n "$UNIT_DIR" ] && [ -d "$UNIT_DIR" ]; then
|
|
300
|
+
own_bad "a unit is installed at $UNIT_DIR but entwurf holds NO ownership state for it — neither the installer nor the inverse will touch it (#87 B2: a shape is not a proof of ownership). Inspect it; if it is a stale copy of ours, remove it by hand and re-run ./run.sh install-omp-bridge"
|
|
301
|
+
else
|
|
302
|
+
note "no ownership state and no installed unit — zero state, which is a SKIP rather than a fault"
|
|
303
|
+
fi
|
|
304
|
+
fi
|
|
305
|
+
|
|
306
|
+
echo
|
|
307
|
+
if [ "$fail" -ne 0 ] || [ "$own_fail" -ne 0 ]; then
|
|
308
|
+
[ "$fail" -ne 0 ] && echo "[omp-bridge-doctor] runtime axis: FAIL" || echo "[omp-bridge-doctor] runtime axis: PASS"
|
|
309
|
+
[ "$own_fail" -ne 0 ] && echo "[omp-bridge-doctor] ownership axis: FAIL" || echo "[omp-bridge-doctor] ownership axis: PASS"
|
|
310
|
+
echo "[omp-bridge-doctor] FAIL"
|
|
311
|
+
exit 1
|
|
312
|
+
fi
|
|
313
|
+
echo "[omp-bridge-doctor] runtime axis: PASS"
|
|
314
|
+
echo "[omp-bridge-doctor] ownership axis: PASS"
|
|
315
|
+
echo "[omp-bridge-doctor] PASS"
|