@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.
Files changed (90) hide show
  1. package/AGENTS.md +1 -0
  2. package/CHANGELOG.md +299 -0
  3. package/DELIVERY.md +4 -3
  4. package/README.md +99 -67
  5. package/VERIFY.md +13 -7
  6. package/docs/acp-backend-rail.md +1 -1
  7. package/docs/external-mcp-host.md +147 -9
  8. package/docs/setup-clean-host.md +221 -16
  9. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +65 -20
  10. package/mcp/entwurf-bridge/dist/pi-extensions/entwurf-capabilities.json +1 -0
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +17 -6
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +1 -1
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +172 -5
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +152 -17
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/omp-fresh-preflight.js +271 -0
  16. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js +988 -0
  17. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-receive-omp.js +574 -0
  18. package/mcp/entwurf-bridge/dist/scripts/omp-receive-facts.js +84 -0
  19. package/mcp/entwurf-bridge/src/index.ts +65 -19
  20. package/mcp/entwurf-bridge/tsconfig.build.json +19 -3
  21. package/mcp/tsconfig.json +10 -0
  22. package/package.json +14 -4
  23. package/pi/entwurf-capabilities.json +1 -0
  24. package/pi/meta-bridge-omp/entwurf-meta-omp/package.json +7 -0
  25. package/pi/omp-receive/entwurf-receive-omp/package.json +7 -0
  26. package/pi-extensions/entwurf-control.ts +12 -9
  27. package/pi-extensions/lib/entwurf-self-address.ts +18 -7
  28. package/pi-extensions/lib/meta-sender-identity.ts +1 -1
  29. package/pi-extensions/lib/meta-session.ts +219 -5
  30. package/pi-extensions/lib/mux-fresh-call.ts +171 -17
  31. package/pi-extensions/lib/omp-fresh-preflight.ts +275 -0
  32. package/pi-extensions/meta-bridge-omp.ts +1244 -0
  33. package/pi-extensions/meta-bridge-receive-omp.ts +666 -0
  34. package/run.sh +1041 -174
  35. package/scripts/agy-bridge.sh +4 -2
  36. package/scripts/check-copilot-birth-hook.ts +700 -23
  37. package/scripts/check-entwurf-self-address.ts +31 -0
  38. package/scripts/check-fresh-cut-gate.sh +12 -4
  39. package/scripts/check-gate-qualification.ts +8 -2
  40. package/scripts/check-harness-admission-parity.ts +143 -0
  41. package/scripts/check-meta-doctor-oracle.sh +20 -0
  42. package/scripts/check-omp-birth-hook.ts +1049 -0
  43. package/scripts/check-omp-fresh-preflight.ts +181 -0
  44. package/scripts/check-omp-receive-arm.ts +516 -0
  45. package/scripts/check-setup-qualification.sh +126 -0
  46. package/scripts/copilot-bridge-doctor.sh +104 -71
  47. package/scripts/copilot-bridge-install.sh +180 -20
  48. package/scripts/copilot-bridge-oracle.sh +223 -0
  49. package/scripts/copilot-bridge-uninstall.sh +137 -0
  50. package/scripts/dev-bin.sh +50 -9
  51. package/scripts/fake-copilot-vendor.sh +67 -0
  52. package/scripts/inventory-verification-surface.ts +1 -1
  53. package/scripts/meta-bridge-hook-log.sh +9 -1
  54. package/scripts/mutants/copilot-birth.json +96 -5
  55. package/scripts/mutants/mux-fresh-call.json +22 -22
  56. package/scripts/mutants/omp-birth.json +173 -0
  57. package/scripts/mutants/omp-fresh.json +300 -0
  58. package/scripts/mutants/omp-receive.json +135 -0
  59. package/scripts/mutants/pi-package-ownership.json +92 -0
  60. package/scripts/mutants/self-address.json +34 -0
  61. package/scripts/mutants/setup-verdict.json +148 -0
  62. package/scripts/mutants/source-install.json +32 -0
  63. package/scripts/omp-bridge-doctor.sh +315 -0
  64. package/scripts/omp-bridge-install.sh +221 -0
  65. package/scripts/omp-bridge-oracle.sh +154 -0
  66. package/scripts/omp-bridge-uninstall.sh +57 -0
  67. package/scripts/omp-mcp-bridge.sh +320 -0
  68. package/scripts/omp-mcp-config.py +392 -0
  69. package/scripts/omp-receive-doctor.sh +246 -0
  70. package/scripts/omp-receive-facts.ts +106 -0
  71. package/scripts/omp-receive-install.sh +228 -0
  72. package/scripts/omp-receive-uninstall.sh +60 -0
  73. package/scripts/omp-tool-surface.py +349 -0
  74. package/scripts/pi_settings_io.py +22 -0
  75. package/scripts/raw-omp-measure/README.md +420 -0
  76. package/scripts/raw-omp-measure/probe-extension.ts +76 -0
  77. package/scripts/raw-omp-measure/probe-receive-surface.ts +250 -0
  78. package/scripts/raw-omp-measure/source-audit.md +414 -0
  79. package/scripts/register-pi-package.py +525 -7
  80. package/scripts/register-pi-provider.py +162 -12
  81. package/scripts/smoke-agy-install-state.sh +86 -30
  82. package/scripts/smoke-agy-statusline-state.sh +9 -6
  83. package/scripts/smoke-omp-bridge-state.sh +221 -0
  84. package/scripts/smoke-omp-fresh-live.ts +497 -0
  85. package/scripts/smoke-omp-mcp-state.sh +327 -0
  86. package/scripts/smoke-omp-receive-live.ts +340 -0
  87. package/scripts/smoke-omp-receive-state.sh +196 -0
  88. package/scripts/smoke-setup-verdict.sh +291 -0
  89. package/scripts/smoke-user-scope-citizen.sh +388 -1
  90. package/scripts/tsconfig.json +2 -0
@@ -0,0 +1,106 @@
1
+ /**
2
+ * omp-receive-facts — the read-only projection of the OMP RECEIVE rail (#87 bundle B).
3
+ *
4
+ * WHY THIS EXISTS AS ITS OWN SURFACE. `meta-facts` projects the certified RECORD store
5
+ * and says nothing about receiver markers (measured: its keys are storeDir / citizens /
6
+ * defects). The receive doctor, the install-state smoke and the LIVE acceptance all need
7
+ * the same question answered — "which omp citizens have a LIVE doorbell right now?" — and
8
+ * a doctor that answered it with `ls` or a grep would be claiming an active receiver from
9
+ * a filename. So the answer comes from the PRODUCTION reader, `readMetaReceiverMarker`,
10
+ * exactly as the record count comes from the production certifier.
11
+ *
12
+ * IT REPORTS BOTH READINGS, AND THAT IS THE POINT. `verifyOwner:true` is the live answer
13
+ * dispatch will get; `verifyOwner:false` is the file as written. Reporting only the first
14
+ * would make a crashed session's leftover marker indistinguishable from no marker at all,
15
+ * and the operator needs to tell "nothing armed" from "something armed and then died".
16
+ *
17
+ * READ-ONLY: it opens no session, writes no marker, and never repairs what it finds.
18
+ */
19
+ import * as fs from "node:fs";
20
+ import * as path from "node:path";
21
+ import { type MetaReceiverMarker, ompMetaRoots, readMetaReceiverMarker } from "../pi-extensions/lib/meta-session.ts";
22
+
23
+ interface ReceiverFact {
24
+ gardenId: string;
25
+ backend: string;
26
+ nativeSessionId: string;
27
+ ownerPid: number;
28
+ ownerKind: string;
29
+ armProvenance: string;
30
+ updatedAt: string;
31
+ /** The reading dispatch gets: the owner pid is still the same live process. */
32
+ ownerLive: boolean;
33
+ /** Mailbox bodies the doorbell has already announced and the model has not drained. */
34
+ unreadDelivered: number;
35
+ /** Bodies enqueued but not yet announced — a doorbell that has not fired yet. */
36
+ freshUnannounced: number;
37
+ mailboxDir: string;
38
+ }
39
+
40
+ function countSuffix(dir: string, suffix: string): number {
41
+ try {
42
+ return fs.readdirSync(dir).filter((f) => f.endsWith(suffix)).length;
43
+ } catch {
44
+ return 0;
45
+ }
46
+ }
47
+
48
+ function main(): void {
49
+ const roots = ompMetaRoots();
50
+ const facts: ReceiverFact[] = [];
51
+ const unreadable: string[] = [];
52
+ let entries: string[] = [];
53
+ try {
54
+ entries = fs.readdirSync(roots.receiversDir).filter((f) => f.endsWith(".json"));
55
+ } catch {
56
+ entries = [];
57
+ }
58
+ for (const entry of entries.sort()) {
59
+ const gardenId = entry.slice(0, -".json".length);
60
+ let written: MetaReceiverMarker | null = null;
61
+ try {
62
+ written = readMetaReceiverMarker({
63
+ gardenId,
64
+ receiversDir: roots.receiversDir,
65
+ verifyOwner: false,
66
+ });
67
+ } catch {
68
+ written = null;
69
+ }
70
+ if (!written) {
71
+ unreadable.push(entry);
72
+ continue;
73
+ }
74
+ if (written.backend !== "omp") continue;
75
+ const live = readMetaReceiverMarker({ gardenId, receiversDir: roots.receiversDir, verifyOwner: true }) !== null;
76
+ const mailboxDir = path.join(roots.mailboxDir, gardenId);
77
+ facts.push({
78
+ gardenId: written.gardenId,
79
+ backend: written.backend,
80
+ nativeSessionId: written.nativeSessionId,
81
+ ownerPid: written.ownerPid,
82
+ ownerKind: written.ownerKind,
83
+ armProvenance: written.armProvenance,
84
+ updatedAt: written.updatedAt,
85
+ ownerLive: live,
86
+ unreadDelivered: countSuffix(mailboxDir, ".msg.delivered"),
87
+ freshUnannounced: countSuffix(mailboxDir, ".msg"),
88
+ mailboxDir,
89
+ });
90
+ }
91
+ process.stdout.write(
92
+ `${JSON.stringify(
93
+ {
94
+ schemaVersion: 1,
95
+ receiversDir: roots.receiversDir,
96
+ mailboxDir: roots.mailboxDir,
97
+ receivers: facts,
98
+ unreadableMarkers: unreadable,
99
+ },
100
+ null,
101
+ 2,
102
+ )}\n`,
103
+ );
104
+ }
105
+
106
+ main();
@@ -0,0 +1,228 @@
1
+ #!/usr/bin/env bash
2
+ # omp-receive-install.sh — operator-grade global installer for the OMP RECEIVER
3
+ # extension (#87 bundle B). The fourth thing an omp citizen needs, and deliberately its
4
+ # own installer: birth, the MCP hand and receive are three independent atoms with three
5
+ # independent failure modes, and one installer owning all of them would let a broken
6
+ # birth withhold a doorbell (or worse, imply one).
7
+ #
8
+ # WHY A SECOND UNIT AND NOT A BRANCH INSIDE THE BIRTH UNIT. There is no second PROCESS
9
+ # here — an omp extension runs in-process, so both units live in the operator's TUI — but
10
+ # there are two independent CAPABILITIES, and the gate that proves birth mints no
11
+ # receiver (`check-omp-receive-arm` / `[QK:OMP-BIRTH-DOES-NOT-ARM-RECEIVER]`) stays true
12
+ # only while they are separable. An operator may install birth and decline receive; the
13
+ # inverse below is what makes that an honest offer rather than a slogan.
14
+ #
15
+ # THE UNIT DOES NOT ASSUME IT LOADS SECOND. `[LIVE 2026-08-30]` extension handlers run in
16
+ # directory-name collation order, and a discovered unit sorting before the birth unit saw
17
+ # no sender marker at its own `session_start` (20ms early). `entwurf-receive-omp` happens
18
+ # to sort after `entwurf-meta-omp`, so today it would arm on the first try — by accident.
19
+ # The unit therefore carries a bounded vendor-owned retry instead of relying on that, and
20
+ # this installer must never be "fixed" by renaming a directory.
21
+ #
22
+ # WHERE IT PLACES IT. `<agent-dir>/extensions/<unit>/index.{ts,js}`, the same native
23
+ # discovery rule the birth unit uses (`discovery/helpers.ts:625-712`). The agent dir is
24
+ # resolved by the shared oracle, which REFUSES rather than guesses when an inherited
25
+ # `PI_*` knob makes it ambiguous (ledger M6).
26
+ #
27
+ # Platform: Linux only, same fence as the Claude, Copilot and omp-birth installers.
28
+ set -euo pipefail
29
+
30
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
31
+ REPO="$(cd "$HERE/.." && pwd)"
32
+ UNIT="entwurf-receive-omp"
33
+ SRC="$REPO/pi/omp-receive"
34
+ # `ENTWURF_OMP_RECEIVE_ASM` exists for the gate: check-omp-receive-arm drives this script for real,
35
+ # into a temp dir, with --assemble-only. A gate that re-implemented the assembly would be
36
+ # testing its own copy of the logic instead of the shipped one.
37
+ ASM="${ENTWURF_OMP_RECEIVE_ASM:-${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/omp-receive/.assembled}"
38
+ STATE_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/omp-receive"
39
+ STATE_FILE="$STATE_DIR/install-state.json"
40
+ ASSEMBLE_ONLY=0
41
+
42
+ die() { echo "[omp-receive-install] $*" >&2; exit 1; }
43
+
44
+ for arg in "$@"; do
45
+ case "$arg" in
46
+ --assemble-only) ASSEMBLE_ONLY=1 ;;
47
+ *) die "unknown argument: $arg (--assemble-only)" ;;
48
+ esac
49
+ done
50
+
51
+ [ "$(uname -s)" = "Linux" ] || die "Linux only; $(uname -s) is not a certified axis for this install."
52
+ command -v python3 >/dev/null 2>&1 || die "python3 is required and is not on PATH."
53
+
54
+ # shellcheck source=scripts/omp-bridge-oracle.sh
55
+ . "$HERE/omp-bridge-oracle.sh"
56
+
57
+ if [ "$ASSEMBLE_ONLY" -eq 1 ]; then
58
+ # A GATE-ONLY path that writes no ownership state, so it must never touch the LIVE
59
+ # assembly: without a state, a rebuilt live assembly would drift from what the state
60
+ # records. Same refusal the Copilot installer carries, for the same reason.
61
+ DEFAULT_ASM="${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/omp-receive/.assembled"
62
+ [ -n "${ENTWURF_OMP_RECEIVE_ASM:-}" ] \
63
+ || die "--assemble-only requires an explicit ENTWURF_OMP_RECEIVE_ASM (a temp dir): this gate-only path writes no ownership state and must not rebuild the live assembly."
64
+ if [ "$(python3 -c 'import os,sys;print(os.path.abspath(sys.argv[1]))' "$ENTWURF_OMP_RECEIVE_ASM")" = \
65
+ "$(python3 -c 'import os,sys;print(os.path.abspath(sys.argv[1]))' "$DEFAULT_ASM")" ]; then
66
+ die "--assemble-only refuses the DEFAULT live assembly path ($DEFAULT_ASM): pass a temp ENTWURF_OMP_RECEIVE_ASM instead."
67
+ fi
68
+ fi
69
+
70
+ AGENT_DIR=""
71
+ UNIT_DIR=""
72
+ if [ "$ASSEMBLE_ONLY" -eq 0 ]; then
73
+ 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."
74
+ AGENT_DIR="$(omp_agent_dir)" || die "refusing: the omp agent directory this host reads is ambiguous (see above)."
75
+ UNIT_DIR="$AGENT_DIR/extensions/$UNIT"
76
+ fi
77
+
78
+ UNIT_VERSION="$(python3 -c "import json,sys;print(json.load(open(sys.argv[1]))['version'])" "$SRC/$UNIT/package.json")" \
79
+ || die "could not read the shipped unit version from $SRC/$UNIT/package.json"
80
+ case "$UNIT_VERSION" in
81
+ ""|*[![:graph:]]*) die "shipped unit version ${UNIT_VERSION:-<empty>} is empty or carries whitespace/control characters — it travels through space-separated fact transports." ;;
82
+ esac
83
+
84
+ # Same install-shape split as the Claude and Copilot installers: an installed package
85
+ # lives below node_modules where Node refuses strip-types on `.ts`, so it ships the
86
+ # tsc-emitted closure; a dev clone ships the `.ts` source. omp itself reads both (its
87
+ # discovery prefers index.ts and falls back to index.js, `discovery/helpers.ts:700-710`).
88
+ case "$REPO" in
89
+ */node_modules/@junghanacs/entwurf)
90
+ ENTRY_NAME="index.js"
91
+ ENTRY_SRC="$REPO/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-receive-omp.js"
92
+ LIB_SRC="$REPO/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js"
93
+ LIB_EXT="js" ;;
94
+ *)
95
+ ENTRY_NAME="index.ts"
96
+ ENTRY_SRC="$REPO/pi-extensions/meta-bridge-receive-omp.ts"
97
+ LIB_SRC="$REPO/pi-extensions/lib/meta-session.ts"
98
+ LIB_EXT="ts" ;;
99
+ esac
100
+
101
+ [ -f "$ENTRY_SRC" ] || die "receive entry artifact missing: $ENTRY_SRC (run 'pnpm run build-bridge' in a dev clone, or reinstall the package)."
102
+ [ -f "$LIB_SRC" ] || die "entry lib artifact missing: $LIB_SRC (same build-bridge dist closure)."
103
+
104
+ write_state() { # $1 = ownedUnitDir (true|false)
105
+ mkdir -p "$STATE_DIR"
106
+ STATE_FILE_ENV="$STATE_FILE" UNIT_DIR_ENV="$UNIT_DIR" ASM_ENV="$ASM" \
107
+ UNIT_VERSION_ENV="$UNIT_VERSION" ENTRY_NAME_ENV="$ENTRY_NAME" OWNED_ENV="$1" python3 - <<'PY' \
108
+ || die "could not write the ownership state at $STATE_FILE"
109
+ import datetime, json, os, tempfile
110
+ state_file = os.environ["STATE_FILE_ENV"]
111
+ state = {
112
+ "schemaVersion": 1,
113
+ "unitDir": os.path.abspath(os.environ["UNIT_DIR_ENV"]),
114
+ "assemblyPath": os.path.abspath(os.environ["ASM_ENV"]),
115
+ "unitVersion": os.environ["UNIT_VERSION_ENV"],
116
+ "entryName": os.environ["ENTRY_NAME_ENV"],
117
+ "ownedUnitDir": os.environ["OWNED_ENV"] == "true",
118
+ "installedAt": datetime.datetime.now(datetime.timezone.utc).isoformat(),
119
+ }
120
+ fd, tmp = tempfile.mkstemp(dir=os.path.dirname(state_file), prefix=".install-state.")
121
+ with os.fdopen(fd, "w") as fh:
122
+ json.dump(state, fh, indent=2)
123
+ fh.write("\n")
124
+ os.replace(tmp, state_file)
125
+ PY
126
+ }
127
+
128
+ # ── 0. ownership preflight (READ-ONLY) ───────────────────────────────────────
129
+ # Every destructive step below is licensed HERE, and the only licence is ownership STATE.
130
+ # Anything that already exists at our path and cannot be proven ours refuses with zero
131
+ # writes — a structural shape is not a proof of ownership (#87 B2). A SYMLINK at the unit
132
+ # path is always a refusal too: writing through it would mutate somebody else's tree.
133
+ if [ "$ASSEMBLE_ONLY" -eq 0 ]; then
134
+ if [ -L "$STATE_FILE" ]; then
135
+ die "ownership state $STATE_FILE is a symlink — refusing to trust or overwrite it."
136
+ fi
137
+ if [ -L "$UNIT_DIR" ]; then
138
+ 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."
139
+ fi
140
+ if [ -f "$STATE_FILE" ]; then
141
+ omp_state_read "$STATE_FILE" "$UNIT_DIR" "$ASM" >/dev/null \
142
+ || die "ownership state $STATE_FILE is corrupt or names a different installation (see above) — refusing with zero writes."
143
+ echo "[omp-receive-install] ownership state matches this installation (reinstall/repair)"
144
+ elif [ -e "$UNIT_DIR" ]; then
145
+ # NO SHAPE-BASED ADOPTION (#87 B2). A structural oracle answers "is this a complete
146
+ # unit", never "is this OURS": it compares no bytes, rejects no extra files and
147
+ # establishes no provenance. Adopting on that answer meant moving a stranger's
148
+ # directory aside, publishing over it and DELETING the preimage — and the inverse
149
+ # then `rm -rf`s the path on the same unproven claim. A foreign or hand-made
150
+ # extension with these filenames, or our own unit plus operator files beside it, was
151
+ # destroyed unrecoverably. Ownership state is the only proof of ownership, so a
152
+ # no-state path refuses with zero writes.
153
+ 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."
154
+ fi
155
+ fi
156
+
157
+ # ── 1. assemble a self-contained unit ────────────────────────────────────────
158
+ # ATOMIC: assemble beside the live artifact and swap only once it is complete, so a
159
+ # failure never leaves a half-written extension for omp to import on the next launch.
160
+ STAGE="$ASM.staging.$$"
161
+ rm -rf "$STAGE"
162
+ trap 'rm -rf "$STAGE"' EXIT
163
+ mkdir -p "$STAGE/$UNIT/lib"
164
+ cp "$SRC/$UNIT/package.json" "$STAGE/$UNIT/package.json"
165
+ cp "$ENTRY_SRC" "$STAGE/$UNIT/$ENTRY_NAME"
166
+ cp "$LIB_SRC" "$STAGE/$UNIT/lib/meta-session.$LIB_EXT"
167
+ cp "$REPO/pi-extensions/lib/session-id.js" "$STAGE/$UNIT/lib/session-id.js"
168
+ # The capability registry must travel at the unit ROOT: metaCapabilitiesFilePath()
169
+ # resolves it via `../` from lib/ in the bundle layout. Without it every mint throws.
170
+ cp "$REPO/pi/entwurf-capabilities.json" "$STAGE/$UNIT/entwurf-capabilities.json"
171
+
172
+ omp_assembly_valid "$STAGE" "$UNIT" || die "the freshly staged assembly fails the structural oracle (see above) — refusing to publish it."
173
+
174
+ PREV=""
175
+ if [ -d "$ASM" ]; then
176
+ PREV="$ASM.previous.$$"
177
+ mv "$ASM" "$PREV" || die "could not move the previous assembly aside ($ASM)."
178
+ fi
179
+ mkdir -p "$(dirname "$ASM")"
180
+ if ! mv "$STAGE" "$ASM"; then
181
+ [ -n "$PREV" ] && mv "$PREV" "$ASM"
182
+ die "could not publish the staged assembly into $ASM (previous assembly restored)."
183
+ fi
184
+ trap - EXIT
185
+ [ -n "$PREV" ] && rm -rf "$PREV"
186
+
187
+ echo "[omp-receive-install] assembled $ASM (receiver extension only; birth is install-omp-bridge, the MCP hand is install-omp-mcp)"
188
+
189
+ if [ "$ASSEMBLE_ONLY" -eq 1 ]; then
190
+ echo "[omp-receive-install] --assemble-only: stopping before the omp agent directory is touched"
191
+ exit 0
192
+ fi
193
+
194
+ # ── 2. place it where omp discovers it ───────────────────────────────────────
195
+ # Ownership state BEFORE the vendor-directory mutation: once the complete assembly is
196
+ # published, the state exists so a partial failure below still leaves an inverse
197
+ # authority behind. `ownedUnitDir` flips false→true around the placement.
198
+ write_state false
199
+ echo "[omp-receive-install] ownership state written: $STATE_FILE"
200
+
201
+ mkdir -p "$(dirname "$UNIT_DIR")"
202
+ PLACE_STAGE="$UNIT_DIR.staging.$$"
203
+ rm -rf "$PLACE_STAGE"
204
+ cp -r "$ASM/$UNIT" "$PLACE_STAGE" || die "could not stage the unit into $(dirname "$UNIT_DIR")."
205
+ PREV_UNIT=""
206
+ if [ -d "$UNIT_DIR" ]; then
207
+ PREV_UNIT="$UNIT_DIR.previous.$$"
208
+ mv "$UNIT_DIR" "$PREV_UNIT" || { rm -rf "$PLACE_STAGE"; die "could not move the previous unit aside ($UNIT_DIR)."; }
209
+ fi
210
+ if ! mv "$PLACE_STAGE" "$UNIT_DIR"; then
211
+ [ -n "$PREV_UNIT" ] && mv "$PREV_UNIT" "$UNIT_DIR"
212
+ rm -rf "$PLACE_STAGE"
213
+ die "could not publish the unit into $UNIT_DIR (previous unit restored)."
214
+ fi
215
+ [ -n "$PREV_UNIT" ] && rm -rf "$PREV_UNIT"
216
+ write_state true
217
+
218
+ omp_assembly_valid "$(dirname "$UNIT_DIR")" "$UNIT" || die "the placed unit fails the structural oracle (see above)."
219
+
220
+ echo "[omp-receive-install] installed $UNIT_DIR (entry $ENTRY_NAME, version $UNIT_VERSION)"
221
+ echo "[omp-receive-install] DONE. An omp TUI session becomes ADDRESSABLE once this unit"
222
+ echo "arms: it joins the citizen birth minted in the same process, holds a watch on that"
223
+ echo "citizen's mailbox signal, and rings a doorbell the model drains with"
224
+ echo "entwurf_inbox_read. Until it arms, dispatch to that garden id is still the honest"
225
+ echo "mailbox-undeliverable refusal, which is what an unarmed receiver SHOULD look like."
226
+ echo "Requires the birth unit (install-omp-bridge) and the tool hand (install-omp-mcp):"
227
+ echo "without the hand the model has no entwurf_inbox_read to drain with."
228
+ echo "Verify with: ./run.sh doctor-omp-receive · inverse: ./run.sh uninstall-omp-receive"
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env bash
2
+ # omp-receive-uninstall.sh — the honest inverse of install-omp-receive (#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-receive-omp"
14
+ ASM="${ENTWURF_OMP_RECEIVE_ASM:-${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/omp-receive/.assembled}"
15
+ STATE_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/omp-receive"
16
+ STATE_FILE="$STATE_DIR/install-state.json"
17
+
18
+ die() { echo "[omp-receive-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-receive-uninstall] removed the installed unit: $UNIT_DIR"
43
+ else
44
+ echo "[omp-receive-uninstall] the installed unit was already absent: $UNIT_DIR"
45
+ fi
46
+
47
+ if [ -d "$ASM" ]; then
48
+ rm -rf "$ASM"
49
+ echo "[omp-receive-uninstall] removed the assembly: $ASM"
50
+ fi
51
+
52
+ rm -f "$STATE_FILE"
53
+ rmdir "$STATE_DIR" 2>/dev/null || true
54
+ echo "[omp-receive-uninstall] ownership state removed: $STATE_FILE"
55
+ echo "[omp-receive-uninstall] DONE. Records and sender identity are untouched — this"
56
+ echo "inverse removes only the DOORBELL. A live omp session that already armed keeps its"
57
+ echo "receiver marker until its process exits (the start-key guard retires it then), so"
58
+ echo "an omp TUI still open right now may remain addressable until you close it."
59
+ echo "Birth and the MCP hand are separate inverses: ./run.sh uninstall-omp-bridge ·"
60
+ echo "./run.sh uninstall-omp-mcp"