@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
|
@@ -40,96 +40,122 @@ PLUGIN="entwurf-meta-receive-copilot"
|
|
|
40
40
|
QUALIFIED="$PLUGIN@$MKT_NAME"
|
|
41
41
|
STALE_CLAUDE_UNIT="entwurf-meta-receive@meta-bridge-local"
|
|
42
42
|
ASM="${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/meta-bridge-copilot/.assembled"
|
|
43
|
+
STATE_FILE="${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/copilot-bridge/install-state.json"
|
|
43
44
|
AGENT_DIR="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}"
|
|
44
45
|
SESSIONS_DIR="$AGENT_DIR/meta-sessions"
|
|
45
46
|
HOOK_LOG="$AGENT_DIR/meta-bridge-hook.log"
|
|
46
47
|
|
|
48
|
+
# RUNTIME truth and OWNERSHIP truth are separate axes (#86 C3a, same discipline as
|
|
49
|
+
# doctor-pi-package): a visibly working registration does not prove entwurf owns it,
|
|
50
|
+
# and a broken ownership record does not erase working runtime configuration. Either
|
|
51
|
+
# required axis red makes the final verdict red.
|
|
47
52
|
fail=0
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
own_fail=0
|
|
54
|
+
ok() { echo " ok $*"; }
|
|
55
|
+
bad() { echo " FAIL $*"; fail=1; }
|
|
56
|
+
badown(){ echo " FAIL $*"; own_fail=1; }
|
|
57
|
+
note() { echo " note $*"; }
|
|
58
|
+
|
|
59
|
+
# Shared with the installer and the inverse: one structural oracle, one state
|
|
60
|
+
# validator, one exact list-row grammar — three surfaces, zero drifting copies.
|
|
61
|
+
# shellcheck source=copilot-bridge-oracle.sh
|
|
62
|
+
. "$HERE/copilot-bridge-oracle.sh"
|
|
51
63
|
|
|
52
64
|
echo "[copilot-bridge-doctor] toolchain"
|
|
53
65
|
command -v copilot >/dev/null 2>&1 && ok "copilot CLI on PATH" || bad "copilot CLI missing from PATH"
|
|
54
66
|
command -v node >/dev/null 2>&1 && ok "node on PATH" || bad "node missing from PATH"
|
|
55
67
|
|
|
56
68
|
echo "[copilot-bridge-doctor] assembled artifact"
|
|
57
|
-
|
|
58
|
-
|
|
69
|
+
# The SAME structural oracle the installer's adoption preflight and the inverse use
|
|
70
|
+
# (C3a amendment, B defect 1): launcher presence/bake, live baked node and hook
|
|
71
|
+
# entry, capability registry at the plugin root, and the native baked hooks.json form
|
|
72
|
+
# are all ONE parser in copilot-bridge-oracle.sh — the doctor keeps no second copy
|
|
73
|
+
# that could drift from what install/adopt actually accepts. The oracle prints its
|
|
74
|
+
# first failing reason, which is surfaced verbatim.
|
|
75
|
+
if ORACLE_REASON="$(copilot_assembly_valid "$ASM" "$PLUGIN" 2>&1)"; then
|
|
76
|
+
ok "assembly at $ASM passes the shared structural oracle (baked launcher, live node + hook entry, capability registry, native hooks.json)"
|
|
59
77
|
else
|
|
60
|
-
bad "assembly
|
|
78
|
+
bad "assembly fails the shared structural oracle: ${ORACLE_REASON:-no reason printed} — run ./run.sh install-copilot-bridge"
|
|
61
79
|
fi
|
|
62
80
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
81
|
+
echo "[copilot-bridge-doctor] copilot wiring"
|
|
82
|
+
# A failing list is UNKNOWN, never an absence (#86 C3a): reporting "not installed" off
|
|
83
|
+
# a broken/unauthenticated CLI would send the operator to reinstall over a host whose
|
|
84
|
+
# real contents nobody read.
|
|
85
|
+
QUALIFIED_REGISTERED=0
|
|
86
|
+
LISTED_VERSION=""
|
|
87
|
+
if ! PLUGIN_LIST="$(copilot plugin list 2>/dev/null)"; then
|
|
88
|
+
bad "'copilot plugin list' failed — this host's installed plugins are UNKNOWN (not empty); fix the Copilot CLI error and re-run"
|
|
89
|
+
PLUGIN_LIST=""
|
|
90
|
+
else
|
|
91
|
+
# EXACT row read with its version captured: a substring read would accept a longer id
|
|
92
|
+
# that merely contains ours, and a malformed/ambiguous listing is something nobody
|
|
93
|
+
# may act on. WHAT A MATCH DOES NOT PROVE: Copilot exposes no load/execution receipt,
|
|
94
|
+
# so a listing says the plugin is REGISTERED, never that Copilot loaded this unit or
|
|
95
|
+
# will invoke its hook. Only a real first prompt settles that — see birth evidence.
|
|
96
|
+
if LISTED_ROW="$(copilot_exact_row_version "$PLUGIN_LIST" "$QUALIFIED" 2>&1)"; then
|
|
97
|
+
if [ "$LISTED_ROW" = "absent" ]; then
|
|
98
|
+
bad "$QUALIFIED is NOT installed in Copilot — run ./run.sh install-copilot-bridge"
|
|
99
|
+
else
|
|
100
|
+
QUALIFIED_REGISTERED=1
|
|
101
|
+
LISTED_VERSION="${LISTED_ROW#one }"
|
|
102
|
+
ok "$QUALIFIED (v$LISTED_VERSION) is registered in Copilot (registration, not proof it is loaded)"
|
|
103
|
+
fi
|
|
71
104
|
else
|
|
72
|
-
|
|
73
|
-
BAKED_NODE="$(sed -n 's/^NODE_BIN="\(.*\)"$/\1/p' "$LAUNCHER" | head -1)"
|
|
74
|
-
[ -x "$BAKED_NODE" ] && ok "baked node exists: $BAKED_NODE" \
|
|
75
|
-
|| bad "baked node is missing or not executable: ${BAKED_NODE:-(unparsed)} — node moved (NixOS store churn?); reinstall"
|
|
76
|
-
BAKED_ENTRY="$(sed -n 's|^HOOK_ENTRY="\$PLUGIN_ROOT/\(.*\)"$|\1|p' "$LAUNCHER" | head -1)"
|
|
77
|
-
[ -n "$BAKED_ENTRY" ] && [ -f "$ASM/$PLUGIN/$BAKED_ENTRY" ] && ok "hook entry present: $BAKED_ENTRY" \
|
|
78
|
-
|| bad "hook entry missing beside the launcher: ${BAKED_ENTRY:-(unparsed)}"
|
|
105
|
+
bad "the plugin listing for $QUALIFIED is malformed or ambiguous: $LISTED_ROW"
|
|
79
106
|
fi
|
|
80
|
-
|
|
81
|
-
|
|
107
|
+
case "$PLUGIN_LIST" in
|
|
108
|
+
*"$STALE_CLAUDE_UNIT"*)
|
|
109
|
+
bad "the Claude unit '$STALE_CLAUDE_UNIT' is still installed in Copilot — it fires on every prompt and exits 1 before node starts (Copilot's schema has no args key). Re-run the installer without --keep-stale-claude-unit." ;;
|
|
110
|
+
*) ok "the Claude unit is not installed in Copilot" ;;
|
|
111
|
+
esac
|
|
82
112
|
fi
|
|
83
113
|
|
|
84
|
-
[
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if [ -f "$HOOKS" ]; then
|
|
89
|
-
HOOKS_PATH="$HOOKS" ASM_LAUNCHER="$LAUNCHER" python3 - <<'PY'
|
|
90
|
-
import json, os, sys
|
|
91
|
-
from pathlib import Path
|
|
92
|
-
hooks = json.loads(Path(os.environ["HOOKS_PATH"]).read_text(encoding="utf-8"))
|
|
93
|
-
launcher = os.environ["ASM_LAUNCHER"]
|
|
94
|
-
problems = []
|
|
95
|
-
if hooks.get("version") != 1:
|
|
96
|
-
problems.append("hooks.json root `version` must be the literal 1 (Copilot rejects the plugin otherwise)")
|
|
97
|
-
events = hooks.get("hooks") or {}
|
|
98
|
-
if set(events) != {"sessionStart", "userPromptSubmitted"}:
|
|
99
|
-
problems.append(f"hook events drifted: {sorted(events)} — want exactly ['sessionStart', 'userPromptSubmitted']")
|
|
100
|
-
for name, entries in events.items():
|
|
101
|
-
for i, entry in enumerate(entries if isinstance(entries, list) else []):
|
|
102
|
-
exec_value = entry.get("exec")
|
|
103
|
-
if not isinstance(exec_value, str):
|
|
104
|
-
problems.append(f"hooks.{name}[{i}].exec must be a STRING (an array is rejected at plugin load)")
|
|
105
|
-
elif exec_value != launcher:
|
|
106
|
-
problems.append(f"hooks.{name}[{i}].exec does not point at the assembled launcher: {exec_value}")
|
|
107
|
-
if "args" in entry:
|
|
108
|
-
problems.append(f"hooks.{name}[{i}] carries `args`, which Copilot's schema has no key for")
|
|
109
|
-
for p in problems:
|
|
110
|
-
print(f" FAIL {p}")
|
|
111
|
-
sys.exit(1 if problems else 0)
|
|
112
|
-
PY
|
|
113
|
-
[ $? -eq 0 ] && ok "hooks.json is the Copilot native form, baked at the assembled launcher" || fail=1
|
|
114
|
+
echo "[copilot-bridge-doctor] ownership (package-owned state; separate axis from runtime)"
|
|
115
|
+
if ! MKT_LIST="$(copilot plugin marketplace list 2>/dev/null)"; then
|
|
116
|
+
badown "'copilot plugin marketplace list' failed — this host's marketplaces are UNKNOWN (not empty); fix the Copilot CLI error and re-run"
|
|
117
|
+
MKT_LIST=""
|
|
114
118
|
else
|
|
115
|
-
|
|
119
|
+
# The SAME marketplace-row grammar install/inverse refuse on (C3a amendment, B
|
|
120
|
+
# defects 1+2): a malformed, non-Local, or DUPLICATE same-named listing is a red
|
|
121
|
+
# ownership fact here, never "the first row".
|
|
122
|
+
if MKT_ROW="$(copilot_marketplace_local_path "$MKT_LIST" "$MKT_NAME" 2>&1)"; then
|
|
123
|
+
if [ "$MKT_ROW" = "absent" ]; then
|
|
124
|
+
note "marketplace $MKT_NAME is not registered (consistent with an uninstalled or partially installed host)"
|
|
125
|
+
else
|
|
126
|
+
MKT_PATH="${MKT_ROW#one }"
|
|
127
|
+
if [ "$MKT_PATH" = "$ASM" ]; then
|
|
128
|
+
ok "marketplace $MKT_NAME is registered at this package's assembly path"
|
|
129
|
+
else
|
|
130
|
+
badown "marketplace '$MKT_NAME' is registered at '$MKT_PATH', not at $ASM — ownership drift; that registration is not provably ours"
|
|
131
|
+
fi
|
|
132
|
+
fi
|
|
133
|
+
else
|
|
134
|
+
badown "the marketplace listing for '$MKT_NAME' is malformed, non-Local, or duplicated: $MKT_ROW"
|
|
135
|
+
fi
|
|
136
|
+
fi
|
|
137
|
+
if [ -L "$STATE_FILE" ]; then
|
|
138
|
+
badown "ownership state $STATE_FILE is a symlink — not a trustworthy record"
|
|
139
|
+
elif [ -f "$STATE_FILE" ]; then
|
|
140
|
+
# The SAME fail-closed validator the installer and the inverse use (exact keyset,
|
|
141
|
+
# exact types, constants + effective-ASM binding, no flag coercion).
|
|
142
|
+
if STATE_VERDICT="$(copilot_state_read "$STATE_FILE" "$QUALIFIED" "$MKT_NAME" "$ASM" 2>&1)"; then
|
|
143
|
+
ok "ownership state present and bound to this installation (ownedMarketplace/ownedAssembly/pluginVersion: $STATE_VERDICT)"
|
|
144
|
+
# version drift (final amendment): the vendor lists an exact row at a version the
|
|
145
|
+
# ownership record did not install — the inverse would refuse, so the doctor names
|
|
146
|
+
# it now instead of letting the operator discover it there.
|
|
147
|
+
STATE_PLUGIN_VERSION="$(printf '%s' "$STATE_VERDICT" | cut -d' ' -f3)"
|
|
148
|
+
if [ -n "$LISTED_VERSION" ] && [ "$LISTED_VERSION" != "$STATE_PLUGIN_VERSION" ]; then
|
|
149
|
+
badown "version drift — $QUALIFIED is listed at v$LISTED_VERSION but the ownership state recorded v$STATE_PLUGIN_VERSION; repair: './run.sh install-copilot-bridge' re-binds ownership"
|
|
150
|
+
fi
|
|
151
|
+
else
|
|
152
|
+
badown "ownership state is corrupt or names a different installation: $STATE_VERDICT — inspect $STATE_FILE"
|
|
153
|
+
fi
|
|
154
|
+
elif [ "$QUALIFIED_REGISTERED" -eq 1 ]; then
|
|
155
|
+
badown "LEGACY no-state installation: $QUALIFIED is registered but no ownership state exists — repair: './run.sh install-copilot-bridge' (same-host adoption binds the state)"
|
|
156
|
+
else
|
|
157
|
+
note "no ownership state (nothing this package records as installed here)"
|
|
116
158
|
fi
|
|
117
|
-
|
|
118
|
-
echo "[copilot-bridge-doctor] copilot wiring"
|
|
119
|
-
PLUGIN_LIST="$(copilot plugin list 2>/dev/null)"
|
|
120
|
-
case "$PLUGIN_LIST" in
|
|
121
|
-
*"$QUALIFIED"*)
|
|
122
|
-
# WHAT THIS DOES NOT PROVE. Copilot exposes no load/execution receipt, so a listing
|
|
123
|
-
# says the plugin is REGISTERED, never that Copilot loaded this unit or will invoke
|
|
124
|
-
# its hook. Only a real first prompt settles that — see the birth-evidence section.
|
|
125
|
-
ok "$QUALIFIED is registered in Copilot (registration, not proof it is loaded)" ;;
|
|
126
|
-
*) bad "$QUALIFIED is NOT installed in Copilot — run ./run.sh install-copilot-bridge" ;;
|
|
127
|
-
esac
|
|
128
|
-
case "$PLUGIN_LIST" in
|
|
129
|
-
*"$STALE_CLAUDE_UNIT"*)
|
|
130
|
-
bad "the Claude unit '$STALE_CLAUDE_UNIT' is still installed in Copilot — it fires on every prompt and exits 1 before node starts (Copilot's schema has no args key). Re-run the installer without --keep-stale-claude-unit." ;;
|
|
131
|
-
*) ok "the Claude unit is not installed in Copilot" ;;
|
|
132
|
-
esac
|
|
133
159
|
|
|
134
160
|
echo "[copilot-bridge-doctor] birth evidence"
|
|
135
161
|
COPILOT_RECORDS=0
|
|
@@ -202,8 +228,15 @@ else
|
|
|
202
228
|
fi
|
|
203
229
|
|
|
204
230
|
echo
|
|
205
|
-
|
|
231
|
+
# The two axes are reported separately and either red is a red verdict (#86 C3a):
|
|
232
|
+
# runtime coverage does not prove ownership, and broken ownership does not erase
|
|
233
|
+
# visibly working runtime configuration.
|
|
234
|
+
if [ "$fail" -ne 0 ] || [ "$own_fail" -ne 0 ]; then
|
|
235
|
+
[ "$fail" -ne 0 ] && echo "[copilot-bridge-doctor] runtime axis: FAIL" || echo "[copilot-bridge-doctor] runtime axis: PASS"
|
|
236
|
+
[ "$own_fail" -ne 0 ] && echo "[copilot-bridge-doctor] ownership axis: FAIL" || echo "[copilot-bridge-doctor] ownership axis: PASS"
|
|
206
237
|
echo "[copilot-bridge-doctor] FAIL"
|
|
207
238
|
exit 1
|
|
208
239
|
fi
|
|
240
|
+
echo "[copilot-bridge-doctor] runtime axis: PASS"
|
|
241
|
+
echo "[copilot-bridge-doctor] ownership axis: PASS"
|
|
209
242
|
echo "[copilot-bridge-doctor] PASS"
|
|
@@ -45,6 +45,12 @@ ASM="${ENTWURF_COPILOT_ASM:-${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/meta-br
|
|
|
45
45
|
# the unit THIS repo installed (cross-review, terra).
|
|
46
46
|
STALE_CLAUDE_UNIT="entwurf-meta-receive@meta-bridge-local"
|
|
47
47
|
QUALIFIED="$PLUGIN@$MKT_NAME"
|
|
48
|
+
# Package-owned ownership state (#86 C3a): the birth unit gets the same discipline the
|
|
49
|
+
# MCP/receiver/footer units already have. The state names exactly what this installer
|
|
50
|
+
# owns — the qualified plugin id, the marketplace name, and the assembly root — so the
|
|
51
|
+
# inverse can remove precisely that and nothing else. No credential, no preimage.
|
|
52
|
+
STATE_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/copilot-bridge"
|
|
53
|
+
STATE_FILE="$STATE_DIR/install-state.json"
|
|
48
54
|
KEEP_STALE=0
|
|
49
55
|
# --assemble-only stops after the assembly, before the Copilot CLI is touched. It is
|
|
50
56
|
# how the gate reaches the real bake on a host (and in CI) that has no Copilot.
|
|
@@ -77,6 +83,140 @@ NODE_MAJOR="$("$NODE_BIN" -p 'process.versions.node.split(".")[0]' 2>/dev/null |
|
|
|
77
83
|
# inside Copilot, where nothing surfaces it. Refuse here instead.
|
|
78
84
|
[ "$NODE_MAJOR" -ge 24 ] 2>/dev/null || die "node >= 24 is required (package engines); resolved $NODE_BIN reports major '$NODE_MAJOR'."
|
|
79
85
|
|
|
86
|
+
# shellcheck source=copilot-bridge-oracle.sh
|
|
87
|
+
. "$HERE/copilot-bridge-oracle.sh"
|
|
88
|
+
|
|
89
|
+
if [ "$ASSEMBLE_ONLY" -eq 1 ]; then
|
|
90
|
+
# --assemble-only is a GATE-ONLY path that deliberately writes no ownership state, so
|
|
91
|
+
# it must never touch the LIVE assembly: without a state, a rebuilt live assembly
|
|
92
|
+
# would drift from what the state/marketplace record. Require the explicit temp
|
|
93
|
+
# override its callers already pass, BEFORE any assembly mutation (amendment,
|
|
94
|
+
# 2026-08-27).
|
|
95
|
+
DEFAULT_ASM="${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/meta-bridge-copilot/.assembled"
|
|
96
|
+
if [ -z "${ENTWURF_COPILOT_ASM:-}" ]; then
|
|
97
|
+
die "--assemble-only requires an explicit ENTWURF_COPILOT_ASM (a temp dir): this gate-only path writes no ownership state and must not rebuild the live assembly."
|
|
98
|
+
fi
|
|
99
|
+
if [ "$(python3 -c 'import os,sys;print(os.path.abspath(sys.argv[1]))' "$ENTWURF_COPILOT_ASM")" = \
|
|
100
|
+
"$(python3 -c 'import os,sys;print(os.path.abspath(sys.argv[1]))' "$DEFAULT_ASM")" ]; then
|
|
101
|
+
die "--assemble-only refuses the DEFAULT live assembly path ($DEFAULT_ASM): pass a temp ENTWURF_COPILOT_ASM instead."
|
|
102
|
+
fi
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
# The shipped plugin version travels into the ownership state so the post-install list
|
|
106
|
+
# evidence and the doctor can join on it.
|
|
107
|
+
PLUGIN_VERSION="$(python3 -c "import json,sys;print(json.load(open(sys.argv[1]))['version'])" "$SRC/$PLUGIN/.claude-plugin/plugin.json")" \
|
|
108
|
+
|| die "could not read the shipped plugin version from $SRC/$PLUGIN/.claude-plugin/plugin.json"
|
|
109
|
+
# C3a amendment (B defect 4): the version rides space-separated state facts and exact
|
|
110
|
+
# list rows, so a shipped version carrying whitespace/control (or nothing at all)
|
|
111
|
+
# would truncate into a fabricated value downstream. Refuse it at the source.
|
|
112
|
+
case "$PLUGIN_VERSION" in
|
|
113
|
+
""|*[![:graph:]]*) die "shipped plugin version ${PLUGIN_VERSION:-<empty>} is empty or carries whitespace/control characters — it must be one printable token (it travels through space-separated fact transports and exact list rows)." ;;
|
|
114
|
+
esac
|
|
115
|
+
|
|
116
|
+
# Atomic state writer: regular file, temp+rename, exact schema. `ownedMarketplace`
|
|
117
|
+
# flips false→true around the marketplace add so a partial failure leaves an honest
|
|
118
|
+
# record of what is actually owned.
|
|
119
|
+
write_state() { # $1 = ownedMarketplace (true|false)
|
|
120
|
+
mkdir -p "$STATE_DIR"
|
|
121
|
+
STATE_FILE_ENV="$STATE_FILE" QUALIFIED_ENV="$QUALIFIED" MKT_ENV="$MKT_NAME" ASM_ENV="$ASM" \
|
|
122
|
+
PLUGIN_VERSION_ENV="$PLUGIN_VERSION" OWNED_MKT_ENV="$1" python3 - <<'PY' || die "could not write the ownership state at $STATE_FILE"
|
|
123
|
+
import json, os, tempfile
|
|
124
|
+
state_file = os.environ["STATE_FILE_ENV"]
|
|
125
|
+
state = {
|
|
126
|
+
"schemaVersion": 1,
|
|
127
|
+
"qualifiedId": os.environ["QUALIFIED_ENV"],
|
|
128
|
+
"marketplaceName": os.environ["MKT_ENV"],
|
|
129
|
+
"assemblyPath": os.path.abspath(os.environ["ASM_ENV"]),
|
|
130
|
+
"pluginVersion": os.environ["PLUGIN_VERSION_ENV"],
|
|
131
|
+
"ownedMarketplace": os.environ["OWNED_MKT_ENV"] == "true",
|
|
132
|
+
"ownedAssembly": True,
|
|
133
|
+
"installedAt": __import__("datetime").datetime.now(__import__("datetime").timezone.utc).isoformat(),
|
|
134
|
+
}
|
|
135
|
+
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(state_file), prefix=".install-state.")
|
|
136
|
+
with os.fdopen(fd, "w") as fh:
|
|
137
|
+
json.dump(state, fh, indent=2)
|
|
138
|
+
fh.write("\n")
|
|
139
|
+
os.replace(tmp, state_file)
|
|
140
|
+
PY
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# ── 0. vendor ownership preflight (READ-ONLY; #86 C3a) ────────────────────────
|
|
144
|
+
# Every destructive vendor step below is licensed HERE, from `plugin list` +
|
|
145
|
+
# `plugin marketplace list` plus the ownership state — a failing list is UNKNOWN and
|
|
146
|
+
# refuses (an unknown host is not an empty one), a same-named marketplace at another
|
|
147
|
+
# path is someone else's and refuses, and a no-state host adopts an existing exact
|
|
148
|
+
# QUALIFIED only when the listed marketplace path is OUR effective assembly AND that
|
|
149
|
+
# assembly passes the doctor's structural oracle. Refusals here are zero-write: no
|
|
150
|
+
# assembly rebuild, no state, no vendor mutation.
|
|
151
|
+
MKT_PREREGISTERED=0
|
|
152
|
+
QUALIFIED_LISTED=0
|
|
153
|
+
INSTALLED_LIST=""
|
|
154
|
+
if [ "$ASSEMBLE_ONLY" -eq 0 ]; then
|
|
155
|
+
if ! INSTALLED_LIST="$(copilot plugin list 2>/dev/null)"; then
|
|
156
|
+
die "'copilot plugin list' failed, so this host's installed plugins are UNKNOWN — refusing before any write. Fix the Copilot CLI error and re-run."
|
|
157
|
+
fi
|
|
158
|
+
if ! MKT_LIST="$(copilot plugin marketplace list 2>/dev/null)"; then
|
|
159
|
+
die "'copilot plugin marketplace list' failed, so this host's marketplaces are UNKNOWN — refusing before any write. Fix the Copilot CLI error and re-run."
|
|
160
|
+
fi
|
|
161
|
+
# EXACT row membership (amendment): a substring match would let `$QUALIFIED-extra`
|
|
162
|
+
# or `prefix-$QUALIFIED` from someone else authorize the adoption/uninstall branches.
|
|
163
|
+
# ANY exact version is accepted here — a reinstall over an older shipped version is
|
|
164
|
+
# exactly the bounded upgrade path (the version is recorded below and the sequence
|
|
165
|
+
# still runs the loud exact uninstall first) — but a malformed or ambiguous listing
|
|
166
|
+
# is something nobody may act on and refuses.
|
|
167
|
+
LISTED_ROW="$(copilot_exact_row_version "$INSTALLED_LIST" "$QUALIFIED")" \
|
|
168
|
+
|| die "the 'copilot plugin list' rows for $QUALIFIED are malformed or ambiguous (see above) — refusing before any write."
|
|
169
|
+
if [ "$LISTED_ROW" != "absent" ]; then
|
|
170
|
+
QUALIFIED_LISTED=1
|
|
171
|
+
echo "[copilot-bridge-install] $QUALIFIED currently listed at ${LISTED_ROW#one } (shipped: v$PLUGIN_VERSION) — bounded upgrade via loud exact uninstall"
|
|
172
|
+
fi
|
|
173
|
+
# ONE marketplace-row grammar (C3a amendment, B defects 1+2): the shared oracle
|
|
174
|
+
# parses the measured `<name> (Local: <abs path>)` form, refuses a non-Local or
|
|
175
|
+
# garbled row, and refuses DUPLICATE same-named rows instead of silently taking the
|
|
176
|
+
# first — a second marketplace with our name could otherwise steer every path check
|
|
177
|
+
# at the wrong target. The exact registered path is what ownership joins on.
|
|
178
|
+
MKT_ROW="$(copilot_marketplace_local_path "$MKT_LIST" "$MKT_NAME")" \
|
|
179
|
+
|| die "the 'copilot plugin marketplace list' rows for '$MKT_NAME' are malformed, non-Local, or duplicated (see above) — refusing before any write."
|
|
180
|
+
if [ "$MKT_ROW" != "absent" ]; then
|
|
181
|
+
MKT_PREREGISTERED=1
|
|
182
|
+
MKT_PATH="${MKT_ROW#one }"
|
|
183
|
+
if [ "$MKT_PATH" != "$ASM" ]; then
|
|
184
|
+
die "a marketplace named '$MKT_NAME' is registered at '$MKT_PATH', not at this install's assembly ($ASM). That registration is not provably ours — zero writes. Inspect with 'copilot plugin marketplace list' and remove it manually if it is stale."
|
|
185
|
+
fi
|
|
186
|
+
fi
|
|
187
|
+
|
|
188
|
+
if [ -L "$STATE_FILE" ]; then
|
|
189
|
+
die "ownership state $STATE_FILE is a symlink — refusing to trust or overwrite it."
|
|
190
|
+
fi
|
|
191
|
+
if [ -f "$STATE_FILE" ]; then
|
|
192
|
+
# The ONE fail-closed schema/constants/binding validator, shared with the inverse
|
|
193
|
+
# and the doctor (copilot_state_read in the oracle) — exact keyset, exact types,
|
|
194
|
+
# no flag coercion. pluginVersion equality with the shipped version is deliberately
|
|
195
|
+
# NOT required here: a state written by an older shipped version is exactly the
|
|
196
|
+
# upgrade-reinstall case.
|
|
197
|
+
copilot_state_read "$STATE_FILE" "$QUALIFIED" "$MKT_NAME" "$ASM" >/dev/null \
|
|
198
|
+
|| die "ownership state $STATE_FILE is corrupt or names a different installation (see above) — refusing with zero writes. Inspect it, or remove it manually if it is stale."
|
|
199
|
+
echo "[copilot-bridge-install] ownership state matches this installation (reinstall/repair)"
|
|
200
|
+
else
|
|
201
|
+
# No state. Adoption is EXPLICIT and narrow; anything else that already exists and
|
|
202
|
+
# cannot be proven ours refuses with zero writes.
|
|
203
|
+
if [ "$QUALIFIED_LISTED" -eq 1 ]; then
|
|
204
|
+
if [ "$MKT_PREREGISTERED" -eq 1 ] && copilot_assembly_valid "$ASM" "$PLUGIN"; then
|
|
205
|
+
echo "[copilot-bridge-install] adopting the legacy no-state installation ($QUALIFIED at $ASM; structural oracle green)"
|
|
206
|
+
else
|
|
207
|
+
die "'$QUALIFIED' is installed but carries no ownership state and its assembly/marketplace does not prove it ours (marketplace at ASM: $MKT_PREREGISTERED; structural oracle above). Refusing with zero writes — inspect 'copilot plugin list' / '$ASM' manually."
|
|
208
|
+
fi
|
|
209
|
+
elif [ "$MKT_PREREGISTERED" -eq 1 ]; then
|
|
210
|
+
if copilot_assembly_valid "$ASM" "$PLUGIN"; then
|
|
211
|
+
echo "[copilot-bridge-install] adopting the legacy marketplace registration ($MKT_NAME at $ASM, plugin not installed; structural oracle green)"
|
|
212
|
+
else
|
|
213
|
+
die "marketplace '$MKT_NAME' is registered at this install's assembly path but that assembly fails the structural oracle (see above) and no ownership state exists. Refusing with zero writes — remove the marketplace manually ('copilot plugin marketplace remove $MKT_NAME'), then re-run."
|
|
214
|
+
fi
|
|
215
|
+
fi
|
|
216
|
+
# fresh host (both absent): proceed.
|
|
217
|
+
fi
|
|
218
|
+
fi
|
|
219
|
+
|
|
80
220
|
# Same install-shape split as the Claude installer: an installed package lives below
|
|
81
221
|
# node_modules where Node refuses strip-types on `.ts`, so it runs the tsc-emitted
|
|
82
222
|
# closure; a dev clone (and the XDG artifact) runs the `.ts` source.
|
|
@@ -179,22 +319,21 @@ if [ "$ASSEMBLE_ONLY" -eq 1 ]; then
|
|
|
179
319
|
exit 0
|
|
180
320
|
fi
|
|
181
321
|
|
|
322
|
+
# Ownership state BEFORE any vendor mutation (#86 C3a): once the complete assembly is
|
|
323
|
+
# published, the state exists so every partial vendor failure below leaves a repair and
|
|
324
|
+
# inverse authority behind. `ownedMarketplace` starts true only when the preflight
|
|
325
|
+
# matched/adopted the exact registration; otherwise it flips true after the add. The
|
|
326
|
+
# state is never deleted on a later failure — rerunning this installer is the repair.
|
|
327
|
+
if [ "$MKT_PREREGISTERED" -eq 1 ]; then write_state true; else write_state false; fi
|
|
328
|
+
echo "[copilot-bridge-install] ownership state written: $STATE_FILE"
|
|
329
|
+
|
|
182
330
|
# --- 2. retire the stale Claude unit from Copilot ---------------------------
|
|
183
331
|
# It is ours, it fires on every Copilot prompt, and it exits 1 before node starts.
|
|
184
332
|
# Leaving it installed means the operator keeps paying for a hook that cannot work.
|
|
333
|
+
# The installed-plugin list was captured (and its failure refused) by the shared
|
|
334
|
+
# read-only preflight above — an unknown list never reaches this branch.
|
|
335
|
+
# INSTALL-ONLY territory: the inverse never touches this unit.
|
|
185
336
|
if [ "$KEEP_STALE" -eq 0 ]; then
|
|
186
|
-
# ASK FIRST, then act. Treating every nonzero uninstall as "not present" would report
|
|
187
|
-
# a CLI, config or permission failure as an absence and then install on top of a unit
|
|
188
|
-
# that is still firing (cross-review, terra). `copilot plugin list` prints the
|
|
189
|
-
# QUALIFIED id, which is also what distinguishes our unit from a same-named plugin
|
|
190
|
-
# somebody else installed from another marketplace.
|
|
191
|
-
# The LIST failing is not an empty list. `|| true` would hand the "nothing installed"
|
|
192
|
-
# arm a broken, unauthenticated or permission-denied Copilot CLI and then install on
|
|
193
|
-
# top of whatever is really there (cross-review, terra). The assignment IS the
|
|
194
|
-
# condition so `set -e` cannot kill us before we can say why.
|
|
195
|
-
if ! INSTALLED_LIST="$(copilot plugin list 2>/dev/null)"; then
|
|
196
|
-
die "'copilot plugin list' failed, so this host's installed plugins are UNKNOWN. An unknown list is not an empty one — the stale Claude unit may still be firing. Fix the Copilot CLI error, or re-run with --keep-stale-claude-unit to skip this check deliberately."
|
|
197
|
-
fi
|
|
198
337
|
case "$INSTALLED_LIST" in
|
|
199
338
|
*"$STALE_CLAUDE_UNIT"*)
|
|
200
339
|
copilot plugin uninstall "$STALE_CLAUDE_UNIT" >/dev/null \
|
|
@@ -208,17 +347,38 @@ if [ "$KEEP_STALE" -eq 0 ]; then
|
|
|
208
347
|
fi
|
|
209
348
|
|
|
210
349
|
# --- 3. (re)register the marketplace and install ----------------------------
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
350
|
+
# LOUD, in a bounded order, licensed by the preflight (#86 C3a): the old blind
|
|
351
|
+
# `|| true` pair could swallow a real vendor failure and then report a working
|
|
352
|
+
# install. Each step now acts only when the preflight saw its subject, and a failure
|
|
353
|
+
# is an honest partial failure — state and assembly stay for the rerun-repair.
|
|
354
|
+
# `--force` is forbidden: `marketplace remove --force` uninstalls that marketplace's
|
|
355
|
+
# plugins as a side effect (measured 2026-08-27), and no entwurf writer takes that
|
|
356
|
+
# shortcut.
|
|
357
|
+
if [ "$QUALIFIED_LISTED" -eq 1 ]; then
|
|
358
|
+
copilot plugin uninstall "$QUALIFIED" >/dev/null \
|
|
359
|
+
|| die "'copilot plugin uninstall $QUALIFIED' failed — honest partial failure; state and assembly are retained, re-run to repair."
|
|
360
|
+
fi
|
|
361
|
+
if [ "$MKT_PREREGISTERED" -eq 1 ]; then
|
|
362
|
+
copilot plugin marketplace remove "$MKT_NAME" >/dev/null \
|
|
363
|
+
|| die "'copilot plugin marketplace remove $MKT_NAME' failed (never retried with --force) — honest partial failure; state and assembly are retained, re-run to repair."
|
|
364
|
+
fi
|
|
365
|
+
copilot plugin marketplace add "$ASM" >/dev/null || die "'copilot plugin marketplace add $ASM' failed — state and assembly are retained, re-run to repair."
|
|
366
|
+
write_state true
|
|
367
|
+
copilot plugin install "$QUALIFIED" >/dev/null || die "'copilot plugin install $QUALIFIED' failed — state and assembly are retained, re-run to repair."
|
|
215
368
|
|
|
216
369
|
# --- 4. evidence ------------------------------------------------------------
|
|
217
370
|
echo "--- copilot plugin list ---"
|
|
218
|
-
# The qualified id
|
|
219
|
-
#
|
|
220
|
-
|
|
371
|
+
# The qualified id AND the shipped version as an EXACT row: a bare-name or substring
|
|
372
|
+
# grep would be satisfied by a same-named plugin from another marketplace, a stale
|
|
373
|
+
# version this run did not install, or a longer id that merely contains ours (list
|
|
374
|
+
# shape measured on copilot 1.0.80).
|
|
375
|
+
POST_LIST="$(copilot plugin list 2>/dev/null)" || die "post-install: 'copilot plugin list' failed — the install evidence is UNKNOWN."
|
|
376
|
+
POST_ROW="$(copilot_exact_row_version "$POST_LIST" "$QUALIFIED")" \
|
|
377
|
+
|| die "post-install: the 'copilot plugin list' rows for $QUALIFIED are malformed or ambiguous (see above)."
|
|
378
|
+
[ "$POST_ROW" = "one $PLUGIN_VERSION" ] \
|
|
379
|
+
|| die "post-install: want exactly one '$QUALIFIED (v$PLUGIN_VERSION)' row, got '$POST_ROW' (install did not take)."
|
|
380
|
+
echo "$QUALIFIED (v$PLUGIN_VERSION) registered"
|
|
221
381
|
echo
|
|
222
382
|
echo "[copilot-bridge-install] DONE. A Copilot session becomes a citizen on its FIRST PROMPT,"
|
|
223
383
|
echo "not when the window opens — sessionStart is deferred to the first prompt (measured)."
|
|
224
|
-
echo "Verify with: ./run.sh doctor-copilot-bridge"
|
|
384
|
+
echo "Verify with: ./run.sh doctor-copilot-bridge · inverse: ./run.sh uninstall-copilot-bridge"
|