@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,221 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# omp-bridge-install.sh — operator-grade global installer for the OMP BIRTH extension
|
|
3
|
+
# (#87). The omp sibling of copilot-bridge-install.sh, deliberately NOT a mode of it.
|
|
4
|
+
#
|
|
5
|
+
# WHAT THIS INSTALLS. ONE extension directory whose managed job is to mint the visible
|
|
6
|
+
# omp TUI host as a garden citizen, name it as this host's sender, and show its garden id
|
|
7
|
+
# on omp's own status line. The MCP hand is a separate surface (`install-omp-mcp`), and
|
|
8
|
+
# receive does not exist yet — three facts, three installers, three failure modes.
|
|
9
|
+
#
|
|
10
|
+
# WHY THERE IS NO LAUNCHER AND NO MANIFEST BAKE. `[source]` an omp "hook" is an
|
|
11
|
+
# in-process EXTENSION: `--hook` aliases `--extension`, a module default-exports
|
|
12
|
+
# `(pi) => void`, and there is no spawned hook command and no stdin envelope (oh-my-pi
|
|
13
|
+
# v18.0.0, `docs/hooks.md`; `extensibility/extensions/types.ts:1592`). The Claude and
|
|
14
|
+
# Copilot installers bake a node path and an entry path into a launcher because their
|
|
15
|
+
# payload is a separate process; here the vendor imports the module itself, so the whole
|
|
16
|
+
# install is: assemble a self-contained unit, then place it where omp discovers it.
|
|
17
|
+
#
|
|
18
|
+
# WHERE IT PLACES IT, AND WHY THERE. `<agent-dir>/extensions/<unit>/index.{ts,js}` is one
|
|
19
|
+
# of omp's three native discovery rules (`discovery/builtin.ts:483` →
|
|
20
|
+
# `discovery/helpers.ts:625-712`), and the only one that needs nothing rewritten per
|
|
21
|
+
# install shape. The agent dir is resolved by the shared oracle, which REFUSES rather
|
|
22
|
+
# than guesses when an inherited `PI_*` knob makes it ambiguous (ledger M6).
|
|
23
|
+
#
|
|
24
|
+
# Platform: Linux only, same fence as the Claude and Copilot installers.
|
|
25
|
+
set -euo pipefail
|
|
26
|
+
|
|
27
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
28
|
+
REPO="$(cd "$HERE/.." && pwd)"
|
|
29
|
+
UNIT="entwurf-meta-omp"
|
|
30
|
+
SRC="$REPO/pi/meta-bridge-omp"
|
|
31
|
+
# `ENTWURF_OMP_ASM` exists for the gate: check-omp-birth-hook drives this script for real,
|
|
32
|
+
# into a temp dir, with --assemble-only. A gate that re-implemented the assembly would be
|
|
33
|
+
# testing its own copy of the logic instead of the shipped one.
|
|
34
|
+
ASM="${ENTWURF_OMP_ASM:-${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/meta-bridge-omp/.assembled}"
|
|
35
|
+
STATE_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/omp-bridge"
|
|
36
|
+
STATE_FILE="$STATE_DIR/install-state.json"
|
|
37
|
+
ASSEMBLE_ONLY=0
|
|
38
|
+
|
|
39
|
+
die() { echo "[omp-bridge-install] $*" >&2; exit 1; }
|
|
40
|
+
|
|
41
|
+
for arg in "$@"; do
|
|
42
|
+
case "$arg" in
|
|
43
|
+
--assemble-only) ASSEMBLE_ONLY=1 ;;
|
|
44
|
+
*) die "unknown argument: $arg (--assemble-only)" ;;
|
|
45
|
+
esac
|
|
46
|
+
done
|
|
47
|
+
|
|
48
|
+
[ "$(uname -s)" = "Linux" ] || die "Linux only; $(uname -s) is not a certified axis for this install."
|
|
49
|
+
command -v python3 >/dev/null 2>&1 || die "python3 is required and is not on PATH."
|
|
50
|
+
|
|
51
|
+
# shellcheck source=scripts/omp-bridge-oracle.sh
|
|
52
|
+
. "$HERE/omp-bridge-oracle.sh"
|
|
53
|
+
|
|
54
|
+
if [ "$ASSEMBLE_ONLY" -eq 1 ]; then
|
|
55
|
+
# A GATE-ONLY path that writes no ownership state, so it must never touch the LIVE
|
|
56
|
+
# assembly: without a state, a rebuilt live assembly would drift from what the state
|
|
57
|
+
# records. Same refusal the Copilot installer carries, for the same reason.
|
|
58
|
+
DEFAULT_ASM="${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/meta-bridge-omp/.assembled"
|
|
59
|
+
[ -n "${ENTWURF_OMP_ASM:-}" ] \
|
|
60
|
+
|| die "--assemble-only requires an explicit ENTWURF_OMP_ASM (a temp dir): this gate-only path writes no ownership state and must not rebuild the live assembly."
|
|
61
|
+
if [ "$(python3 -c 'import os,sys;print(os.path.abspath(sys.argv[1]))' "$ENTWURF_OMP_ASM")" = \
|
|
62
|
+
"$(python3 -c 'import os,sys;print(os.path.abspath(sys.argv[1]))' "$DEFAULT_ASM")" ]; then
|
|
63
|
+
die "--assemble-only refuses the DEFAULT live assembly path ($DEFAULT_ASM): pass a temp ENTWURF_OMP_ASM instead."
|
|
64
|
+
fi
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
AGENT_DIR=""
|
|
68
|
+
UNIT_DIR=""
|
|
69
|
+
if [ "$ASSEMBLE_ONLY" -eq 0 ]; then
|
|
70
|
+
command -v omp >/dev/null 2>&1 || die "the 'omp' CLI is not on PATH. entwurf never installs a harness — install omp first, then re-run."
|
|
71
|
+
AGENT_DIR="$(omp_agent_dir)" || die "refusing: the omp agent directory this host reads is ambiguous (see above)."
|
|
72
|
+
UNIT_DIR="$AGENT_DIR/extensions/$UNIT"
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
UNIT_VERSION="$(python3 -c "import json,sys;print(json.load(open(sys.argv[1]))['version'])" "$SRC/$UNIT/package.json")" \
|
|
76
|
+
|| die "could not read the shipped unit version from $SRC/$UNIT/package.json"
|
|
77
|
+
case "$UNIT_VERSION" in
|
|
78
|
+
""|*[![:graph:]]*) die "shipped unit version ${UNIT_VERSION:-<empty>} is empty or carries whitespace/control characters — it travels through space-separated fact transports." ;;
|
|
79
|
+
esac
|
|
80
|
+
|
|
81
|
+
# Same install-shape split as the Claude and Copilot installers: an installed package
|
|
82
|
+
# lives below node_modules where Node refuses strip-types on `.ts`, so it ships the
|
|
83
|
+
# tsc-emitted closure; a dev clone ships the `.ts` source. omp itself reads both (its
|
|
84
|
+
# discovery prefers index.ts and falls back to index.js, `discovery/helpers.ts:700-710`).
|
|
85
|
+
case "$REPO" in
|
|
86
|
+
*/node_modules/@junghanacs/entwurf)
|
|
87
|
+
ENTRY_NAME="index.js"
|
|
88
|
+
ENTRY_SRC="$REPO/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js"
|
|
89
|
+
LIB_SRC="$REPO/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js"
|
|
90
|
+
LIB_EXT="js" ;;
|
|
91
|
+
*)
|
|
92
|
+
ENTRY_NAME="index.ts"
|
|
93
|
+
ENTRY_SRC="$REPO/pi-extensions/meta-bridge-omp.ts"
|
|
94
|
+
LIB_SRC="$REPO/pi-extensions/lib/meta-session.ts"
|
|
95
|
+
LIB_EXT="ts" ;;
|
|
96
|
+
esac
|
|
97
|
+
|
|
98
|
+
[ -f "$ENTRY_SRC" ] || die "birth entry artifact missing: $ENTRY_SRC (run 'pnpm run build-bridge' in a dev clone, or reinstall the package)."
|
|
99
|
+
[ -f "$LIB_SRC" ] || die "entry lib artifact missing: $LIB_SRC (same build-bridge dist closure)."
|
|
100
|
+
|
|
101
|
+
write_state() { # $1 = ownedUnitDir (true|false)
|
|
102
|
+
mkdir -p "$STATE_DIR"
|
|
103
|
+
STATE_FILE_ENV="$STATE_FILE" UNIT_DIR_ENV="$UNIT_DIR" ASM_ENV="$ASM" \
|
|
104
|
+
UNIT_VERSION_ENV="$UNIT_VERSION" ENTRY_NAME_ENV="$ENTRY_NAME" OWNED_ENV="$1" python3 - <<'PY' \
|
|
105
|
+
|| die "could not write the ownership state at $STATE_FILE"
|
|
106
|
+
import datetime, json, os, tempfile
|
|
107
|
+
state_file = os.environ["STATE_FILE_ENV"]
|
|
108
|
+
state = {
|
|
109
|
+
"schemaVersion": 1,
|
|
110
|
+
"unitDir": os.path.abspath(os.environ["UNIT_DIR_ENV"]),
|
|
111
|
+
"assemblyPath": os.path.abspath(os.environ["ASM_ENV"]),
|
|
112
|
+
"unitVersion": os.environ["UNIT_VERSION_ENV"],
|
|
113
|
+
"entryName": os.environ["ENTRY_NAME_ENV"],
|
|
114
|
+
"ownedUnitDir": os.environ["OWNED_ENV"] == "true",
|
|
115
|
+
"installedAt": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
|
116
|
+
}
|
|
117
|
+
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(state_file), prefix=".install-state.")
|
|
118
|
+
with os.fdopen(fd, "w") as fh:
|
|
119
|
+
json.dump(state, fh, indent=2)
|
|
120
|
+
fh.write("\n")
|
|
121
|
+
os.replace(tmp, state_file)
|
|
122
|
+
PY
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# ── 0. ownership preflight (READ-ONLY) ───────────────────────────────────────
|
|
126
|
+
# Every destructive step below is licensed HERE, and the only licence is ownership STATE.
|
|
127
|
+
# Anything that already exists at our path and cannot be proven ours refuses with zero
|
|
128
|
+
# writes — a structural shape is not a proof of ownership (#87 B2). A SYMLINK at the unit
|
|
129
|
+
# path is always a refusal too: writing through it would mutate somebody else's tree.
|
|
130
|
+
if [ "$ASSEMBLE_ONLY" -eq 0 ]; then
|
|
131
|
+
if [ -L "$STATE_FILE" ]; then
|
|
132
|
+
die "ownership state $STATE_FILE is a symlink — refusing to trust or overwrite it."
|
|
133
|
+
fi
|
|
134
|
+
if [ -L "$UNIT_DIR" ]; then
|
|
135
|
+
die "$UNIT_DIR is a SYMLINK. entwurf never writes through a link into a tree it does not own — remove it by hand if it is stale, then re-run."
|
|
136
|
+
fi
|
|
137
|
+
if [ -f "$STATE_FILE" ]; then
|
|
138
|
+
omp_state_read "$STATE_FILE" "$UNIT_DIR" "$ASM" >/dev/null \
|
|
139
|
+
|| die "ownership state $STATE_FILE is corrupt or names a different installation (see above) — refusing with zero writes."
|
|
140
|
+
echo "[omp-bridge-install] ownership state matches this installation (reinstall/repair)"
|
|
141
|
+
elif [ -e "$UNIT_DIR" ]; then
|
|
142
|
+
# NO SHAPE-BASED ADOPTION (#87 B2). A structural oracle answers "is this a complete
|
|
143
|
+
# unit", never "is this OURS": it compares no bytes, rejects no extra files and
|
|
144
|
+
# establishes no provenance. Adopting on that answer meant moving a stranger's
|
|
145
|
+
# directory aside, publishing over it and DELETING the preimage — and the inverse
|
|
146
|
+
# then `rm -rf`s the path on the same unproven claim. A foreign or hand-made
|
|
147
|
+
# extension with these filenames, or our own unit plus operator files beside it, was
|
|
148
|
+
# destroyed unrecoverably. Ownership state is the only proof of ownership, so a
|
|
149
|
+
# no-state path refuses with zero writes.
|
|
150
|
+
die "$UNIT_DIR already exists and entwurf holds NO ownership state for it. A directory that merely LOOKS like our unit is not proof that it is ours — adopting it would overwrite it and delete the preimage with no inverse. Refusing with zero writes: inspect it, and if it is stale remove it by hand, then re-run."
|
|
151
|
+
fi
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
# ── 1. assemble a self-contained unit ────────────────────────────────────────
|
|
155
|
+
# ATOMIC: assemble beside the live artifact and swap only once it is complete, so a
|
|
156
|
+
# failure never leaves a half-written extension for omp to import on the next launch.
|
|
157
|
+
STAGE="$ASM.staging.$$"
|
|
158
|
+
rm -rf "$STAGE"
|
|
159
|
+
trap 'rm -rf "$STAGE"' EXIT
|
|
160
|
+
mkdir -p "$STAGE/$UNIT/lib"
|
|
161
|
+
cp "$SRC/$UNIT/package.json" "$STAGE/$UNIT/package.json"
|
|
162
|
+
cp "$ENTRY_SRC" "$STAGE/$UNIT/$ENTRY_NAME"
|
|
163
|
+
cp "$LIB_SRC" "$STAGE/$UNIT/lib/meta-session.$LIB_EXT"
|
|
164
|
+
cp "$REPO/pi-extensions/lib/session-id.js" "$STAGE/$UNIT/lib/session-id.js"
|
|
165
|
+
# The capability registry must travel at the unit ROOT: metaCapabilitiesFilePath()
|
|
166
|
+
# resolves it via `../` from lib/ in the bundle layout. Without it every mint throws.
|
|
167
|
+
cp "$REPO/pi/entwurf-capabilities.json" "$STAGE/$UNIT/entwurf-capabilities.json"
|
|
168
|
+
|
|
169
|
+
omp_assembly_valid "$STAGE" "$UNIT" || die "the freshly staged assembly fails the structural oracle (see above) — refusing to publish it."
|
|
170
|
+
|
|
171
|
+
PREV=""
|
|
172
|
+
if [ -d "$ASM" ]; then
|
|
173
|
+
PREV="$ASM.previous.$$"
|
|
174
|
+
mv "$ASM" "$PREV" || die "could not move the previous assembly aside ($ASM)."
|
|
175
|
+
fi
|
|
176
|
+
mkdir -p "$(dirname "$ASM")"
|
|
177
|
+
if ! mv "$STAGE" "$ASM"; then
|
|
178
|
+
[ -n "$PREV" ] && mv "$PREV" "$ASM"
|
|
179
|
+
die "could not publish the staged assembly into $ASM (previous assembly restored)."
|
|
180
|
+
fi
|
|
181
|
+
trap - EXIT
|
|
182
|
+
[ -n "$PREV" ] && rm -rf "$PREV"
|
|
183
|
+
|
|
184
|
+
echo "[omp-bridge-install] assembled $ASM (birth extension only; the MCP hand is install-omp-mcp)"
|
|
185
|
+
|
|
186
|
+
if [ "$ASSEMBLE_ONLY" -eq 1 ]; then
|
|
187
|
+
echo "[omp-bridge-install] --assemble-only: stopping before the omp agent directory is touched"
|
|
188
|
+
exit 0
|
|
189
|
+
fi
|
|
190
|
+
|
|
191
|
+
# ── 2. place it where omp discovers it ───────────────────────────────────────
|
|
192
|
+
# Ownership state BEFORE the vendor-directory mutation: once the complete assembly is
|
|
193
|
+
# published, the state exists so a partial failure below still leaves an inverse
|
|
194
|
+
# authority behind. `ownedUnitDir` flips false→true around the placement.
|
|
195
|
+
write_state false
|
|
196
|
+
echo "[omp-bridge-install] ownership state written: $STATE_FILE"
|
|
197
|
+
|
|
198
|
+
mkdir -p "$(dirname "$UNIT_DIR")"
|
|
199
|
+
PLACE_STAGE="$UNIT_DIR.staging.$$"
|
|
200
|
+
rm -rf "$PLACE_STAGE"
|
|
201
|
+
cp -r "$ASM/$UNIT" "$PLACE_STAGE" || die "could not stage the unit into $(dirname "$UNIT_DIR")."
|
|
202
|
+
PREV_UNIT=""
|
|
203
|
+
if [ -d "$UNIT_DIR" ]; then
|
|
204
|
+
PREV_UNIT="$UNIT_DIR.previous.$$"
|
|
205
|
+
mv "$UNIT_DIR" "$PREV_UNIT" || { rm -rf "$PLACE_STAGE"; die "could not move the previous unit aside ($UNIT_DIR)."; }
|
|
206
|
+
fi
|
|
207
|
+
if ! mv "$PLACE_STAGE" "$UNIT_DIR"; then
|
|
208
|
+
[ -n "$PREV_UNIT" ] && mv "$PREV_UNIT" "$UNIT_DIR"
|
|
209
|
+
rm -rf "$PLACE_STAGE"
|
|
210
|
+
die "could not publish the unit into $UNIT_DIR (previous unit restored)."
|
|
211
|
+
fi
|
|
212
|
+
[ -n "$PREV_UNIT" ] && rm -rf "$PREV_UNIT"
|
|
213
|
+
write_state true
|
|
214
|
+
|
|
215
|
+
omp_assembly_valid "$(dirname "$UNIT_DIR")" "$UNIT" || die "the placed unit fails the structural oracle (see above)."
|
|
216
|
+
|
|
217
|
+
echo "[omp-bridge-install] installed $UNIT_DIR (entry $ENTRY_NAME, version $UNIT_VERSION)"
|
|
218
|
+
echo "[omp-bridge-install] DONE. An omp TUI session becomes a citizen when it OPENS —"
|
|
219
|
+
echo "the birth edge is the extension's session_start, which fires after first paint and"
|
|
220
|
+
echo "before the first prompt; a task subagent of that session is refused by design."
|
|
221
|
+
echo "Verify with: ./run.sh doctor-omp-bridge · inverse: ./run.sh uninstall-omp-bridge"
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# omp-bridge-oracle.sh — the ONE structural oracle for the OMP birth unit (#87).
|
|
3
|
+
# Sourced by the installer, the inverse and the doctor so "valid assembly", "valid
|
|
4
|
+
# ownership state" and "which agent dir does omp actually read" cannot mean three
|
|
5
|
+
# different things in three files.
|
|
6
|
+
#
|
|
7
|
+
# omp_agent_dir
|
|
8
|
+
# Resolve the agent directory omp itself would read, or REFUSE. Prints the absolute
|
|
9
|
+
# path on rc 0; prints the reason on stderr and returns rc 1 when the environment is
|
|
10
|
+
# ambiguous. See the function header for why an inherited `PI_*` knob is a refusal
|
|
11
|
+
# rather than a guess.
|
|
12
|
+
#
|
|
13
|
+
# omp_assembly_valid <asm-root> <unit-name>
|
|
14
|
+
# rc 0 = the assembly is a complete unit (entry + lib + registry + package.json with
|
|
15
|
+
# `type: module`); rc 1 with the first failing reason on stderr.
|
|
16
|
+
#
|
|
17
|
+
# omp_state_read <state_file> <unit_dir> <asm>
|
|
18
|
+
# Fail-closed EXACT-schema reader for the ownership state — one validator for all
|
|
19
|
+
# three surfaces. rc 0 prints `<unitVersion> <entryName>`; any missing/unknown key,
|
|
20
|
+
# wrong type, empty string, relative path or constant drift is rc 1.
|
|
21
|
+
#
|
|
22
|
+
# Deliberately BOUNDED: environment, structure, one state schema. Records, markers and
|
|
23
|
+
# hook-log truth stay on the doctor's own axes.
|
|
24
|
+
|
|
25
|
+
# The vendor's own agent-dir resolution, reproduced only as far as this lane needs.
|
|
26
|
+
#
|
|
27
|
+
# `[source]` omp reads `PI_CONFIG_DIR` (default `.omp`) and `PI_CODING_AGENT_DIR`
|
|
28
|
+
# (`packages/utils/src/dirs.ts:23`, `:210`, `:358`), and selects a profile from
|
|
29
|
+
# `OMP_PROFILE` first, `PI_PROFILE` second (`dirs.ts:57`, `:85-91`), which moves the
|
|
30
|
+
# agent dir to `~/.omp/profiles/<name>/agent`.
|
|
31
|
+
#
|
|
32
|
+
# WHY A `PI_*` KNOB IS A REFUSAL AND NOT A LOOKUP (ledger M6). omp is a pi fork and kept
|
|
33
|
+
# pi's env vocabulary, so those three variables now steer TWO harnesses: entwurf's own pi
|
|
34
|
+
# rail sandboxes `PI_CODING_AGENT_DIR` (AGENTS.md Hard Rule 12) and omp reads the same
|
|
35
|
+
# name. A value in the installing shell therefore does not say WHICH harness it is
|
|
36
|
+
# addressing. Installing into a directory chosen by that guess is how an operator ends up
|
|
37
|
+
# with a unit no live omp reads, or a unit written into the pi rail's sandbox. `OMP_PROFILE`
|
|
38
|
+
# is omp's own name and is honoured; the pi-shaped names refuse, and `ENTWURF_OMP_AGENT_DIR`
|
|
39
|
+
# is the explicit override every gate and smoke passes.
|
|
40
|
+
omp_agent_dir() {
|
|
41
|
+
if [ -n "${ENTWURF_OMP_AGENT_DIR:-}" ]; then
|
|
42
|
+
python3 -c 'import os,sys;print(os.path.abspath(os.path.expanduser(sys.argv[1])))' "$ENTWURF_OMP_AGENT_DIR"
|
|
43
|
+
return 0
|
|
44
|
+
fi
|
|
45
|
+
if [ -n "${PI_CODING_AGENT_DIR:-}" ]; then
|
|
46
|
+
echo "PI_CODING_AGENT_DIR is set ($PI_CODING_AGENT_DIR). omp reads it too (utils/src/dirs.ts:358), so it does not say whether it is addressing omp or entwurf's pi rail — refusing to guess. Unset it, or pass ENTWURF_OMP_AGENT_DIR explicitly." >&2
|
|
47
|
+
return 1
|
|
48
|
+
fi
|
|
49
|
+
if [ -n "${PI_CONFIG_DIR:-}" ]; then
|
|
50
|
+
echo "PI_CONFIG_DIR is set ($PI_CONFIG_DIR). It renames omp's whole config root (utils/src/dirs.ts:23, :210) and is a pi-shaped name — refusing to guess. Unset it, or pass ENTWURF_OMP_AGENT_DIR explicitly." >&2
|
|
51
|
+
return 1
|
|
52
|
+
fi
|
|
53
|
+
if [ -n "${PI_PROFILE:-}" ] && [ -z "${OMP_PROFILE:-}" ]; then
|
|
54
|
+
echo "PI_PROFILE is set ($PI_PROFILE) with no OMP_PROFILE. omp falls back to it (utils/src/dirs.ts:85-91) but so does pi — refusing to guess which harness this profile addresses. Set OMP_PROFILE explicitly, or pass ENTWURF_OMP_AGENT_DIR." >&2
|
|
55
|
+
return 1
|
|
56
|
+
fi
|
|
57
|
+
local root="$HOME/.omp"
|
|
58
|
+
if [ -n "${OMP_PROFILE:-}" ]; then
|
|
59
|
+
case "$OMP_PROFILE" in
|
|
60
|
+
[a-z0-9]*)
|
|
61
|
+
case "$OMP_PROFILE" in
|
|
62
|
+
*[!a-z0-9._-]*)
|
|
63
|
+
echo "OMP_PROFILE '$OMP_PROFILE' is not a valid omp profile name (utils/src/dirs.ts PROFILE_NAME_RE)." >&2
|
|
64
|
+
return 1 ;;
|
|
65
|
+
esac
|
|
66
|
+
root="$HOME/.omp/profiles/$OMP_PROFILE" ;;
|
|
67
|
+
*)
|
|
68
|
+
echo "OMP_PROFILE '$OMP_PROFILE' is not a valid omp profile name (utils/src/dirs.ts PROFILE_NAME_RE)." >&2
|
|
69
|
+
return 1 ;;
|
|
70
|
+
esac
|
|
71
|
+
fi
|
|
72
|
+
printf '%s\n' "$root/agent"
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
omp_assembly_valid() {
|
|
76
|
+
local asm="$1" unit="$2"
|
|
77
|
+
local dir="$asm/$unit"
|
|
78
|
+
if [ ! -d "$dir" ]; then
|
|
79
|
+
echo "assembly has no unit directory: $dir" >&2
|
|
80
|
+
return 1
|
|
81
|
+
fi
|
|
82
|
+
local entry=""
|
|
83
|
+
if [ -f "$dir/index.ts" ]; then entry="index.ts"; elif [ -f "$dir/index.js" ]; then entry="index.js"; fi
|
|
84
|
+
if [ -z "$entry" ]; then
|
|
85
|
+
echo "assembly has no index.ts/index.js entry: $dir (omp discovers a subdirectory extension by that name, discovery/helpers.ts:700-710)" >&2
|
|
86
|
+
return 1
|
|
87
|
+
fi
|
|
88
|
+
local libext="ts"
|
|
89
|
+
[ "$entry" = "index.js" ] && libext="js"
|
|
90
|
+
for required in "package.json" "entwurf-capabilities.json" "lib/meta-session.$libext" "lib/session-id.js"; do
|
|
91
|
+
if [ ! -f "$dir/$required" ]; then
|
|
92
|
+
echo "assembly is missing $required: $dir/$required" >&2
|
|
93
|
+
return 1
|
|
94
|
+
fi
|
|
95
|
+
done
|
|
96
|
+
# `type: module` is not cosmetic on the installed-package shape: an ESM `index.js`
|
|
97
|
+
# under a directory with no nearest package.json type field is read as CommonJS.
|
|
98
|
+
if ! DIR_ENV="$dir" python3 - <<'PY'
|
|
99
|
+
import json, os, sys
|
|
100
|
+
pkg = json.load(open(os.path.join(os.environ["DIR_ENV"], "package.json")))
|
|
101
|
+
if pkg.get("type") != "module":
|
|
102
|
+
print(f"assembly package.json type is {pkg.get('type')!r}, want the literal \"module\"", file=sys.stderr)
|
|
103
|
+
sys.exit(1)
|
|
104
|
+
if not isinstance(pkg.get("version"), str) or not pkg["version"]:
|
|
105
|
+
print("assembly package.json has no non-empty version", file=sys.stderr)
|
|
106
|
+
sys.exit(1)
|
|
107
|
+
PY
|
|
108
|
+
then
|
|
109
|
+
return 1
|
|
110
|
+
fi
|
|
111
|
+
return 0
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
omp_state_read() {
|
|
115
|
+
local state_file="$1" unit_dir="$2" asm="$3"
|
|
116
|
+
STATE_FILE_ENV="$state_file" UNIT_DIR_ENV="$unit_dir" ASM_ENV="$asm" python3 - <<'PY'
|
|
117
|
+
import json, os, sys
|
|
118
|
+
try:
|
|
119
|
+
state = json.load(open(os.environ["STATE_FILE_ENV"]))
|
|
120
|
+
except (OSError, json.JSONDecodeError) as err:
|
|
121
|
+
print(f"state parse failed: {err}", file=sys.stderr)
|
|
122
|
+
sys.exit(1)
|
|
123
|
+
EXACT_KEYS = {"schemaVersion", "unitDir", "assemblyPath", "unitVersion", "entryName", "ownedUnitDir", "installedAt"}
|
|
124
|
+
if not isinstance(state, dict) or set(state) != EXACT_KEYS:
|
|
125
|
+
got = sorted(state) if isinstance(state, dict) else type(state).__name__
|
|
126
|
+
print(f"state keyset drifted: {got} != {sorted(EXACT_KEYS)}", file=sys.stderr)
|
|
127
|
+
sys.exit(1)
|
|
128
|
+
if state["schemaVersion"] != 1:
|
|
129
|
+
print(f"state schemaVersion is {state['schemaVersion']!r}, want the literal 1", file=sys.stderr)
|
|
130
|
+
sys.exit(1)
|
|
131
|
+
for key in ("unitDir", "assemblyPath", "unitVersion", "entryName", "installedAt"):
|
|
132
|
+
if not isinstance(state[key], str) or not state[key]:
|
|
133
|
+
print(f"state {key} must be a nonempty string, got {state[key]!r}", file=sys.stderr)
|
|
134
|
+
sys.exit(1)
|
|
135
|
+
if any(ord(c) < 0x21 or ord(c) == 0x7f for c in state["unitVersion"]):
|
|
136
|
+
print(f"state unitVersion contains whitespace/control characters: {state['unitVersion']!r} — "
|
|
137
|
+
"the space-separated fact transport would truncate it into a fabricated version", file=sys.stderr)
|
|
138
|
+
sys.exit(1)
|
|
139
|
+
if state["entryName"] not in ("index.ts", "index.js"):
|
|
140
|
+
print(f"state entryName is {state['entryName']!r}, want index.ts or index.js", file=sys.stderr)
|
|
141
|
+
sys.exit(1)
|
|
142
|
+
if not isinstance(state["ownedUnitDir"], bool):
|
|
143
|
+
print(f"state ownedUnitDir must be a boolean, got {state['ownedUnitDir']!r} — flags are never coerced", file=sys.stderr)
|
|
144
|
+
sys.exit(1)
|
|
145
|
+
for key, want in (("unitDir", os.environ["UNIT_DIR_ENV"]), ("assemblyPath", os.environ["ASM_ENV"])):
|
|
146
|
+
if not os.path.isabs(state[key]):
|
|
147
|
+
print(f"state {key} is not absolute: {state[key]!r}", file=sys.stderr)
|
|
148
|
+
sys.exit(1)
|
|
149
|
+
if os.path.abspath(state[key]) != os.path.abspath(want):
|
|
150
|
+
print(f"state {key} names {state[key]!r}, not this installation's {want!r}", file=sys.stderr)
|
|
151
|
+
sys.exit(1)
|
|
152
|
+
print(f"{state['unitVersion']} {state['entryName']}")
|
|
153
|
+
PY
|
|
154
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# omp-bridge-uninstall.sh — the honest inverse of install-omp-bridge (#87).
|
|
3
|
+
#
|
|
4
|
+
# HONEST means: remove exactly what the ownership state says this repo installed, and
|
|
5
|
+
# nothing else. No `--force`, no "clean up anything that looks like ours", no removal of
|
|
6
|
+
# a unit directory we cannot prove we placed. A no-state host is NOT an empty host: it is
|
|
7
|
+
# a host whose ownership we cannot establish, so the answer is a named refusal.
|
|
8
|
+
#
|
|
9
|
+
# The state is deleted LAST, so an interrupted inverse leaves the authority to retry.
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
|
|
12
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
13
|
+
UNIT="entwurf-meta-omp"
|
|
14
|
+
ASM="${ENTWURF_OMP_ASM:-${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/meta-bridge-omp/.assembled}"
|
|
15
|
+
STATE_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/omp-bridge"
|
|
16
|
+
STATE_FILE="$STATE_DIR/install-state.json"
|
|
17
|
+
|
|
18
|
+
die() { echo "[omp-bridge-uninstall] $*" >&2; exit 1; }
|
|
19
|
+
|
|
20
|
+
# shellcheck source=scripts/omp-bridge-oracle.sh
|
|
21
|
+
. "$HERE/omp-bridge-oracle.sh"
|
|
22
|
+
|
|
23
|
+
AGENT_DIR="$(omp_agent_dir)" || die "refusing: the omp agent directory this host reads is ambiguous (see above)."
|
|
24
|
+
UNIT_DIR="$AGENT_DIR/extensions/$UNIT"
|
|
25
|
+
|
|
26
|
+
[ -L "$STATE_FILE" ] && die "ownership state $STATE_FILE is a symlink — refusing to trust it."
|
|
27
|
+
if [ ! -f "$STATE_FILE" ]; then
|
|
28
|
+
die "no ownership state at $STATE_FILE, so nothing here is provably ours. Refusing with zero writes — if $UNIT_DIR is a stale hand-copied unit, remove it by hand."
|
|
29
|
+
fi
|
|
30
|
+
STATE_FACTS="$(omp_state_read "$STATE_FILE" "$UNIT_DIR" "$ASM")" \
|
|
31
|
+
|| die "ownership state $STATE_FILE is corrupt or names a different installation (see above) — refusing with zero writes."
|
|
32
|
+
STATE_ENTRY="${STATE_FACTS#* }"
|
|
33
|
+
|
|
34
|
+
if [ -e "$UNIT_DIR" ]; then
|
|
35
|
+
if [ -L "$UNIT_DIR" ]; then
|
|
36
|
+
die "$UNIT_DIR is a SYMLINK — this installer never creates one, so it is not ours. Refusing; remove it by hand."
|
|
37
|
+
fi
|
|
38
|
+
if [ ! -f "$UNIT_DIR/$STATE_ENTRY" ]; then
|
|
39
|
+
die "$UNIT_DIR exists but does not carry the entry this install recorded ($STATE_ENTRY) — it is not provably the unit we placed. Refusing with zero writes."
|
|
40
|
+
fi
|
|
41
|
+
rm -rf "$UNIT_DIR"
|
|
42
|
+
echo "[omp-bridge-uninstall] removed the installed unit: $UNIT_DIR"
|
|
43
|
+
else
|
|
44
|
+
echo "[omp-bridge-uninstall] the installed unit was already absent: $UNIT_DIR"
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
if [ -d "$ASM" ]; then
|
|
48
|
+
rm -rf "$ASM"
|
|
49
|
+
echo "[omp-bridge-uninstall] removed the assembly: $ASM"
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
rm -f "$STATE_FILE"
|
|
53
|
+
rmdir "$STATE_DIR" 2>/dev/null || true
|
|
54
|
+
echo "[omp-bridge-uninstall] ownership state removed: $STATE_FILE"
|
|
55
|
+
echo "[omp-bridge-uninstall] DONE. Existing omp sessions keep the record they already"
|
|
56
|
+
echo "minted — records are preserved, never deleted (AGENTS.md Hard Rule 8); what stops"
|
|
57
|
+
echo "is any FUTURE birth. The MCP hand is a separate inverse: ./run.sh uninstall-omp-mcp"
|