@junghanacs/entwurf 0.12.6 → 0.12.7
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 +31 -17
- package/BASELINE.md +42 -8
- package/CHANGELOG.md +22 -0
- package/DELIVERY.md +71 -14
- package/README.md +77 -45
- package/VERIFY.md +18 -13
- package/docs/setup-clean-host.md +80 -22
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +90 -66
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +54 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +153 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +436 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/context.js +157 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/engraving.js +105 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +90 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +194 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +153 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +42 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +49 -13
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +104 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +30 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-native-push.js +57 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +10 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-release.js +9 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +21 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +5 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +17 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +125 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/adapter.js +158 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/register.js +61 -0
- package/mcp/entwurf-bridge/dist/scripts/agy-imprint.js +166 -0
- package/mcp/entwurf-bridge/dist/scripts/doctor-pi-provider.js +130 -0
- package/mcp/entwurf-bridge/dist/scripts/meta-bridge-prune.js +178 -0
- package/mcp/entwurf-bridge/dist/scripts/new-session-id.js +24 -0
- package/mcp/entwurf-bridge/src/index.ts +101 -67
- package/mcp/entwurf-bridge/test.sh +1 -1
- package/mcp/entwurf-bridge/tsconfig.build.json +23 -3
- package/package.json +10 -5
- package/pi-extensions/lib/entwurf-deliverability.ts +62 -9
- package/pi-extensions/lib/entwurf-self-address.ts +58 -15
- package/pi-extensions/lib/entwurf-v2-contract.ts +120 -12
- package/pi-extensions/lib/entwurf-v2-decider.ts +60 -0
- package/pi-extensions/lib/entwurf-v2-native-push.ts +86 -0
- package/pi-extensions/lib/entwurf-v2-production.ts +20 -0
- package/pi-extensions/lib/entwurf-v2-release.ts +9 -0
- package/pi-extensions/lib/entwurf-v2-runner.ts +29 -1
- package/pi-extensions/lib/entwurf-v2-send.ts +7 -0
- package/pi-extensions/lib/entwurf-v2-surface.ts +17 -0
- package/pi-extensions/lib/meta-sender-identity.ts +154 -0
- package/pi-extensions/lib/native-push/adapter.ts +255 -0
- package/pi-extensions/lib/native-push/register.ts +99 -0
- package/run.sh +756 -214
- package/scripts/agy-bridge-config.py +446 -0
- package/scripts/agy-bridge.sh +359 -0
- package/scripts/agy-hooks-bridge.sh +193 -0
- package/scripts/agy-hooks-config.py +257 -0
- package/scripts/agy-imprint.sh +28 -0
- package/scripts/agy-imprint.ts +193 -0
- package/scripts/agy-statusline-bridge.sh +176 -0
- package/scripts/agy-statusline-config.py +213 -0
- package/scripts/agy-statusline.sh +256 -0
- package/scripts/build-bridge.sh +20 -0
- package/scripts/check-agy-sender-identity.ts +364 -0
- package/scripts/check-entwurf-bridge-boot.ts +8 -2
- package/scripts/check-entwurf-deliverability.ts +34 -0
- package/scripts/check-entwurf-self-address.ts +78 -11
- package/scripts/check-entwurf-v2-contract.ts +136 -1
- package/scripts/check-entwurf-v2-decider.ts +95 -1
- package/scripts/check-entwurf-v2-matrix.ts +14 -3
- package/scripts/check-entwurf-v2-native-push.ts +193 -0
- package/scripts/check-entwurf-v2-production.ts +68 -1
- package/scripts/check-entwurf-v2-runner.ts +58 -0
- package/scripts/check-entwurf-v2-surface.ts +35 -0
- package/scripts/check-install-surface.ts +357 -0
- package/scripts/check-native-push-adapter.ts +319 -0
- package/scripts/check-native-push-register.ts +130 -0
- package/scripts/dev-bin.sh +195 -0
- package/scripts/doctor-pi-provider.ts +140 -0
- package/scripts/meta-bridge-doctor.sh +36 -2
- package/scripts/register-pi-package.py +37 -3
- package/scripts/register-pi-provider.py +287 -0
- package/scripts/smoke-agy-hooks-state.sh +172 -0
- package/scripts/smoke-agy-install-state.sh +660 -0
- package/scripts/smoke-agy-native-push-live.ts +243 -0
- package/scripts/smoke-agy-statusline-state.sh +300 -0
- package/scripts/smoke-meta-async-drift.sh +9 -2
- package/scripts/smoke-meta-install-state.sh +20 -0
- package/scripts/smoke-pi-provider-state.sh +182 -0
- package/scripts/smoke-user-scope-citizen.sh +62 -0
- package/scripts/with-dist-lock.sh +81 -0
- package/scripts/__pycache__/meta-bridge-state.cpython-312.pyc +0 -0
- package/scripts/__pycache__/meta-bridge-state.cpython-313.pyc +0 -0
- package/scripts/__pycache__/register-pi-package.cpython-313.pyc +0 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# agy-bridge — the agy (Antigravity) MCP install ADAPTER (봉인 7). Separate from the Claude
|
|
3
|
+
# marketplace install (no generalization); only the runner/reporting is shared, here.
|
|
4
|
+
#
|
|
5
|
+
# install register ONE entwurf-bridge server entry in the agy mcp_config (adopt a regular
|
|
6
|
+
# file / create a new one / REFUSE a symlink — someone else's SSOT), recording an
|
|
7
|
+
# install-state for an honest inverse. The command written is a STABLE bin
|
|
8
|
+
# (`entwurf-bridge`), NEVER a repo/git-hash path (the oracle dangling lesson).
|
|
9
|
+
# uninstall honest inverse from the install-state (restore preimage / remove our key).
|
|
10
|
+
# doctor 2-tier: STATIC proves both candidate configs (documented + observed) resolve,
|
|
11
|
+
# parse, and carry a resolvable command; LIVE proves runtime-effectiveness only
|
|
12
|
+
# when an agy process exists, else an honest SKIP (never a PASS in disguise).
|
|
13
|
+
#
|
|
14
|
+
# GLOBAL ROOT (the one file that matters): live agy reads its global MCP config from
|
|
15
|
+
# ~/.gemini/config/mcp_config.json (agy's own builtin doc: mcp_servers.md — "Global Configuration:
|
|
16
|
+
# ~/.gemini/config/mcp_config.json, applies to all sessions"). The ~/.gemini/antigravity-cli copy
|
|
17
|
+
# is a stale mis-wiring agy does NOT read as global — install now targets config/ and CLEANS the
|
|
18
|
+
# antigravity-cli entry (one-way migration). This is the "뭐가 글로벌인지" that had been confusing.
|
|
19
|
+
#
|
|
20
|
+
# Paths (all overridable for the isolated smoke):
|
|
21
|
+
# AGY_MCP_CONFIG install target (global) (default: ~/.gemini/config/mcp_config.json)
|
|
22
|
+
# AGY_MCP_CONFIG_ALT legacy root to clean (default: ~/.gemini/antigravity-cli/mcp_config.json)
|
|
23
|
+
# AGY_BRIDGE_COMMAND command to register (default: entwurf-bridge — a stable bin)
|
|
24
|
+
# XDG_DATA_HOME install-state root (state at $XDG_DATA_HOME/entwurf/agy-bridge/)
|
|
25
|
+
set -euo pipefail
|
|
26
|
+
|
|
27
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
28
|
+
CONFIG_PY="$HERE/agy-bridge-config.py"
|
|
29
|
+
|
|
30
|
+
GLOBAL_CONFIG="${AGY_MCP_CONFIG:-$HOME/.gemini/config/mcp_config.json}"
|
|
31
|
+
LEGACY_CONFIG="${AGY_MCP_CONFIG_ALT:-$HOME/.gemini/antigravity-cli/mcp_config.json}"
|
|
32
|
+
# agy's MCP tool-schema cache root (appDataDir/mcp). agy NEVER prunes an orphaned server's cache
|
|
33
|
+
# itself, so a cut-over-FROM key (pi-tools-bridge → entwurf-bridge rename) lingers forever as
|
|
34
|
+
# config garbage. install prunes ONLY these exact legacy keys — never a scan-and-delete that could
|
|
35
|
+
# touch another harness's LIVE MCP cache. Cache, not config: agy re-fetches if ever reconfigured.
|
|
36
|
+
AGY_MCP_CACHE_DIR="${AGY_MCP_CACHE_DIR:-$HOME/.gemini/antigravity-cli/mcp}"
|
|
37
|
+
LEGACY_CACHE_KEYS="pi-tools-bridge"
|
|
38
|
+
COMMAND="${AGY_BRIDGE_COMMAND:-entwurf-bridge}"
|
|
39
|
+
STATE_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/agy-bridge"
|
|
40
|
+
STATE_FILE="$STATE_DIR/install-state.json"
|
|
41
|
+
# agy's settings.json — the SAME file the statusline adapter owns, but a DIFFERENT element: it owns
|
|
42
|
+
# the `statusLine` subtree, we own one string in `permissions.allow`. Element-level ownership on
|
|
43
|
+
# both sides is what lets two adapters share a file without either clobbering the other (or the
|
|
44
|
+
# operator's own rules). Tracked in its own state file so each half has an honest inverse.
|
|
45
|
+
SETTINGS_FILE="${AGY_SETTINGS_CONFIG:-$HOME/.gemini/antigravity-cli/settings.json}"
|
|
46
|
+
PERMISSION_STATE_FILE="$STATE_DIR/permission-state.json"
|
|
47
|
+
ALLOW_RULE="mcp(entwurf-bridge/entwurf_v2)"
|
|
48
|
+
|
|
49
|
+
log() { printf '%s\n' "$*"; }
|
|
50
|
+
fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; }
|
|
51
|
+
|
|
52
|
+
# Is COMMAND runnable? A bare name is looked up on PATH; an absolute/relative path must be an
|
|
53
|
+
# executable file. A "dangling" command (the oracle lesson) fails here.
|
|
54
|
+
command_resolvable() {
|
|
55
|
+
local cmd="$1"
|
|
56
|
+
case "$cmd" in
|
|
57
|
+
*/*) [ -x "$cmd" ] ;; # a path → must be an executable file
|
|
58
|
+
*) command -v "$cmd" >/dev/null 2>&1 ;; # a bare name → must be on PATH
|
|
59
|
+
esac
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Prune the agy MCP tool-schema cache for the KNOWN-legacy server keys (LEGACY_CACHE_KEYS). Removes
|
|
63
|
+
# ONLY the exact-named dirs — a symlink is left intact (not ours), and any OTHER server's cache is
|
|
64
|
+
# never touched. Idempotent (absent = no-op). One-shot cutover hygiene, not honest-inverse tracked.
|
|
65
|
+
prune_legacy_cache() {
|
|
66
|
+
local key dir
|
|
67
|
+
for key in $LEGACY_CACHE_KEYS; do
|
|
68
|
+
dir="$AGY_MCP_CACHE_DIR/$key"
|
|
69
|
+
[ -e "$dir" ] || continue
|
|
70
|
+
if [ -L "$dir" ]; then
|
|
71
|
+
log " cache: legacy '$key' is a symlink — left intact (not ours to remove)"
|
|
72
|
+
continue
|
|
73
|
+
fi
|
|
74
|
+
rm -rf "$dir" && log " cache: pruned stale legacy MCP tool cache '$key'"
|
|
75
|
+
done
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
do_install() {
|
|
79
|
+
log "[agy-bridge install]"
|
|
80
|
+
log " target: $GLOBAL_CONFIG"
|
|
81
|
+
log " command: $COMMAND"
|
|
82
|
+
log " state: $STATE_FILE"
|
|
83
|
+
if ! command_resolvable "$COMMAND"; then
|
|
84
|
+
log " note: '$COMMAND' is not currently resolvable on this host — install still records it"
|
|
85
|
+
log " (a stable bin is installed by the npm package; doctor will FAIL until it is on PATH)."
|
|
86
|
+
fi
|
|
87
|
+
local out rc
|
|
88
|
+
set +e
|
|
89
|
+
out="$(python3 "$CONFIG_PY" install "$GLOBAL_CONFIG" "$COMMAND" "$STATE_FILE" 2>&1)"
|
|
90
|
+
rc=$?
|
|
91
|
+
set -e
|
|
92
|
+
case "$rc" in
|
|
93
|
+
0) log " ok: ${out}" ;;
|
|
94
|
+
3) fail "refused (symlink) — ${out}" ;;
|
|
95
|
+
4) fail "invalid JSON — ${out}" ;;
|
|
96
|
+
*) fail "install error (rc=$rc) — ${out}" ;;
|
|
97
|
+
esac
|
|
98
|
+
# One-way migration: drop the stale entwurf-bridge entry from the LEGACY root (agy does not read
|
|
99
|
+
# it as global MCP config). Non-fatal — a corrupt/symlinked legacy must not brick the install.
|
|
100
|
+
local lout lrc
|
|
101
|
+
set +e
|
|
102
|
+
lout="$(python3 "$CONFIG_PY" clean-legacy "$LEGACY_CONFIG" 2>&1)"
|
|
103
|
+
lrc=$?
|
|
104
|
+
set -e
|
|
105
|
+
case "$lout" in
|
|
106
|
+
cleaned-*) log " legacy: removed stale entwurf-bridge from $LEGACY_CONFIG" ;;
|
|
107
|
+
skip-symlink*) log " legacy: $LEGACY_CONFIG is a symlink (someone else's SSOT) — left intact" ;;
|
|
108
|
+
absent*|not-present*) : ;; # nothing to clean
|
|
109
|
+
*) [ "$lrc" -ne 0 ] && log " legacy: WARN could not clean $LEGACY_CONFIG — ${lout}" ;;
|
|
110
|
+
esac
|
|
111
|
+
prune_legacy_cache
|
|
112
|
+
install_permission
|
|
113
|
+
log " installed. Verify with: ./run.sh doctor-agy-bridge"
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# The other half of a usable bridge: agy defaults every `mcp` action to Ask, so a registered-but-
|
|
117
|
+
# ungranted server stops for a y/n on EVERY entwurf_v2 call. We grant exactly our own tool —
|
|
118
|
+
# never mcp(*), never the operator's command/file rules; those are their trust decision, not the
|
|
119
|
+
# installer's.
|
|
120
|
+
#
|
|
121
|
+
# FAIL LOUD on an explicit install. A grant we could not write is a HALF-installed bridge: the
|
|
122
|
+
# server is registered, every call prompts, and reporting "installed" over that is the exact shape
|
|
123
|
+
# this repo exists to remove (a layer saying yes while the surface says no). The tolerance lives one
|
|
124
|
+
# level up, where it belongs: setup's wire_agy_bridge catches this nonzero and degrades to a
|
|
125
|
+
# reason-specific WARN, because agy is optional and must never brick a pi/Claude host.
|
|
126
|
+
install_permission() {
|
|
127
|
+
local out rc
|
|
128
|
+
set +e
|
|
129
|
+
out="$(python3 "$CONFIG_PY" permission-install "$SETTINGS_FILE" "$PERMISSION_STATE_FILE" 2>&1)"
|
|
130
|
+
rc=$?
|
|
131
|
+
set -e
|
|
132
|
+
case "$rc" in
|
|
133
|
+
0) log " permission: ${out} in $SETTINGS_FILE" ;;
|
|
134
|
+
3) fail "permission refused (symlink) — $SETTINGS_FILE is someone else's SSOT; left intact. The MCP server is registered but NOT granted, so agy would prompt on every entwurf_v2 call. Add '$ALLOW_RULE' to permissions.allow there, or replace the symlink with a regular file and re-run." ;;
|
|
135
|
+
4) fail "permission invalid JSON — $SETTINGS_FILE could not be parsed; left intact. The MCP server is registered but NOT granted, so agy would prompt on every entwurf_v2 call. Repair that file, then re-run." ;;
|
|
136
|
+
*) fail "permission could not be granted (rc=$rc) — ${out}" ;;
|
|
137
|
+
esac
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
do_uninstall() {
|
|
141
|
+
log "[agy-bridge uninstall]"
|
|
142
|
+
log " state: $STATE_FILE"
|
|
143
|
+
local out rc
|
|
144
|
+
set +e
|
|
145
|
+
out="$(python3 "$CONFIG_PY" uninstall "$STATE_FILE" 2>&1)"
|
|
146
|
+
rc=$?
|
|
147
|
+
set -e
|
|
148
|
+
case "$rc" in
|
|
149
|
+
0) log " ok: ${out}" ;;
|
|
150
|
+
2) log " note: ${out}" ;; # no state → nothing to undo (idempotent)
|
|
151
|
+
3) fail "refused (symlink) — ${out}" ;;
|
|
152
|
+
*) fail "uninstall error (rc=$rc) — ${out}" ;;
|
|
153
|
+
esac
|
|
154
|
+
# Honest inverse of the grant. If the operator ALREADY had the rule before we installed, the
|
|
155
|
+
# state records that and the rule stays — it was never ours to take away. A revoke we cannot
|
|
156
|
+
# perform is a FAILED inverse, not a footnote: uninstall would otherwise report success while
|
|
157
|
+
# leaving our rule in the operator's settings.
|
|
158
|
+
set +e
|
|
159
|
+
out="$(python3 "$CONFIG_PY" permission-uninstall "$PERMISSION_STATE_FILE" 2>&1)"
|
|
160
|
+
rc=$?
|
|
161
|
+
set -e
|
|
162
|
+
case "$rc" in
|
|
163
|
+
0) log " permission: ${out}" ;;
|
|
164
|
+
2) : ;; # never granted → nothing to undo (idempotent)
|
|
165
|
+
3) fail "permission refused (symlink) — $SETTINGS_FILE became a symlink since install; our rule is STILL THERE. Remove '$ALLOW_RULE' from permissions.allow by hand at the link target." ;;
|
|
166
|
+
*) fail "permission could not be revoked (rc=$rc) — ${out}" ;;
|
|
167
|
+
esac
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
# Static-check ONE candidate config. Prints a status line; returns 1 on a hard failure
|
|
171
|
+
# (invalid JSON / configured-but-dangling command), 0 otherwise (absent / not-configured /
|
|
172
|
+
# configured+resolvable are not doctor failures — a candidate may legitimately be unused).
|
|
173
|
+
doctor_static_one() {
|
|
174
|
+
local label="$1" candidate="$2"
|
|
175
|
+
local link_note=""
|
|
176
|
+
if [ -L "$candidate" ]; then
|
|
177
|
+
link_note=" [symlink → $(readlink "$candidate")]"
|
|
178
|
+
fi
|
|
179
|
+
local status
|
|
180
|
+
status="$(python3 "$CONFIG_PY" doctor-static "$candidate")"
|
|
181
|
+
case "$status" in
|
|
182
|
+
absent) log " $label: absent$link_note"; return 0 ;;
|
|
183
|
+
not-configured) log " $label: present but entwurf-bridge NOT configured$link_note"; return 0 ;;
|
|
184
|
+
invalid-json) log " $label: INVALID JSON$link_note"; return 1 ;;
|
|
185
|
+
configured\ *)
|
|
186
|
+
local cmd="${status#configured }"
|
|
187
|
+
if command_resolvable "$cmd"; then
|
|
188
|
+
log " $label: configured → '$cmd' (resolvable)$link_note"
|
|
189
|
+
return 0
|
|
190
|
+
fi
|
|
191
|
+
log " $label: configured → '$cmd' DANGLING (not on PATH / not executable)$link_note"
|
|
192
|
+
return 1 ;;
|
|
193
|
+
*) log " $label: unexpected status '$status'$link_note"; return 1 ;;
|
|
194
|
+
esac
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
# Is our tool actually callable without a prompt? A registered server that stops for a y/n on every
|
|
198
|
+
# call is a half-installed bridge, and "why does agy ask me every time?" must not be a mystery — so
|
|
199
|
+
# the permission is doctor evidence, not silent state. The shadow case is the subtle one: agy
|
|
200
|
+
# evaluates Deny > Ask > Allow, so an operator rule like mcp(*) in their ask/deny list overrides our
|
|
201
|
+
# allow while our install-state still looks green. Name it rather than report a false pass.
|
|
202
|
+
doctor_permission() {
|
|
203
|
+
local status installed=0
|
|
204
|
+
# "Installed" = we have state for either half. A missing grant is only a NOTE on a host that never
|
|
205
|
+
# installed the bridge; on an installed one it is DRIFT — the server is registered and every call
|
|
206
|
+
# prompts, which is precisely the half-installed surface a green doctor must never bless. Same
|
|
207
|
+
# distinction the mcp-key N1 check draws between "never installed" and "installed then loosened".
|
|
208
|
+
if [ -f "$STATE_FILE" ] || [ -f "$PERMISSION_STATE_FILE" ]; then installed=1; fi
|
|
209
|
+
status="$(python3 "$CONFIG_PY" permission-doctor "$SETTINGS_FILE")"
|
|
210
|
+
case "$status" in
|
|
211
|
+
configured)
|
|
212
|
+
log " permission ($SETTINGS_FILE): allow → '$ALLOW_RULE' (agy calls entwurf_v2 without prompting)"; return 0 ;;
|
|
213
|
+
covered-by-allow\ *)
|
|
214
|
+
# The operator's own broader rule already grants our tool, so entwurf_v2 does NOT prompt —
|
|
215
|
+
# calling this DRIFT would be a false red about a working surface. But it is THEIR rule, not
|
|
216
|
+
# ours: narrowing that wildcard silently takes our grant with it. Note, never a silent pass.
|
|
217
|
+
local covering="${status#covered-by-allow }"
|
|
218
|
+
# Ownership axis first (hard rule 12): if OUR permission-state says WE added the exact rule
|
|
219
|
+
# to THIS settings file and it is now gone, that is drift of an owned element — the operator's
|
|
220
|
+
# broad rule keeps calls working (say so), but the verdict stays red. A whole-file settings
|
|
221
|
+
# relink (agent-config ensure_link) produces exactly this shape. A state bound to a DIFFERENT
|
|
222
|
+
# file (foreign) or unreadable (corrupt) is the independent state check's verdict, not this one.
|
|
223
|
+
if [ -f "$PERMISSION_STATE_FILE" ] && python3 -c '
|
|
224
|
+
import json, os, sys
|
|
225
|
+
try:
|
|
226
|
+
s = json.load(open(sys.argv[1]))
|
|
227
|
+
except Exception:
|
|
228
|
+
sys.exit(1)
|
|
229
|
+
p = s.get("managedSettingsPath")
|
|
230
|
+
same = isinstance(p, str) and os.path.isabs(p) and os.path.abspath(p) == os.path.abspath(sys.argv[2])
|
|
231
|
+
sys.exit(0 if same and s.get("ruleExistedBefore") is False else 1)' "$PERMISSION_STATE_FILE" "$SETTINGS_FILE"; then
|
|
232
|
+
log " permission ($SETTINGS_FILE): DRIFT — entwurf installed '$ALLOW_RULE' here and it is now GONE. Your '$covering' still covers the tool, so agy does not prompt today — but the grant entwurf owns (and repairs) no longer exists. Fix: ./run.sh install-agy-bridge"
|
|
233
|
+
return 1
|
|
234
|
+
fi
|
|
235
|
+
log " permission ($SETTINGS_FILE): NOTE — we own no rule here, but your '$covering' in permissions.allow already matches $ALLOW_RULE, so agy calls entwurf_v2 without prompting. That grant is YOURS: narrow '$covering' and entwurf_v2 starts prompting again. Run ./run.sh install-agy-bridge if you want the narrow rule owned (and repaired) by entwurf."
|
|
236
|
+
return 0 ;;
|
|
237
|
+
not-configured|absent)
|
|
238
|
+
local what="'$ALLOW_RULE' NOT granted"
|
|
239
|
+
[ "$status" = absent ] && what="settings file absent, so no grant"
|
|
240
|
+
if [ "$installed" -eq 1 ]; then
|
|
241
|
+
log " permission ($SETTINGS_FILE): DRIFT — $what, but the bridge IS installed. agy defaults mcp to Ask, so EVERY entwurf_v2 call prompts: the server is registered and unusable without a y/n. Fix: ./run.sh install-agy-bridge"
|
|
242
|
+
return 1
|
|
243
|
+
fi
|
|
244
|
+
log " permission ($SETTINGS_FILE): $what (bridge not installed here — nothing to grant yet)"; return 0 ;;
|
|
245
|
+
invalid-json)
|
|
246
|
+
log " permission ($SETTINGS_FILE): INVALID JSON — cannot read the permission engine's config"; return 1 ;;
|
|
247
|
+
shadowed-by-*)
|
|
248
|
+
local list rule
|
|
249
|
+
list="${status%% *}"; list="${list#shadowed-by-}"
|
|
250
|
+
rule="${status#* }"
|
|
251
|
+
log " permission ($SETTINGS_FILE): SHADOWED — your '$list' list carries '$rule', and agy evaluates Deny > Ask > Allow, so it OVERRIDES our allow of '$ALLOW_RULE'. agy will keep prompting (or blocking) on every entwurf_v2 call until that rule is narrowed."; return 1 ;;
|
|
252
|
+
*) log " permission ($SETTINGS_FILE): unexpected status '$status'"; return 1 ;;
|
|
253
|
+
esac
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
do_doctor() {
|
|
257
|
+
log "[agy-bridge doctor]"
|
|
258
|
+
local hard_fail=0 configured_any=0 resolvable_any=0
|
|
259
|
+
|
|
260
|
+
log "── static (configured candidates)"
|
|
261
|
+
doctor_static_one "global ($GLOBAL_CONFIG)" "$GLOBAL_CONFIG" || hard_fail=1
|
|
262
|
+
doctor_static_one "legacy ($LEGACY_CONFIG)" "$LEGACY_CONFIG" || hard_fail=1
|
|
263
|
+
doctor_permission || hard_fail=1
|
|
264
|
+
# Did EITHER candidate carry a configured + resolvable entwurf-bridge? Keep this runtime fact
|
|
265
|
+
# separate from ownership-state failures below: a FOREIGN TARGET makes the doctor red, but it
|
|
266
|
+
# does not make a visibly configured command disappear.
|
|
267
|
+
local c candidate_status candidate_cmd
|
|
268
|
+
for c in "$GLOBAL_CONFIG" "$LEGACY_CONFIG"; do
|
|
269
|
+
candidate_status="$(python3 "$CONFIG_PY" doctor-static "$c")"
|
|
270
|
+
case "$candidate_status" in
|
|
271
|
+
configured\ *)
|
|
272
|
+
configured_any=1
|
|
273
|
+
candidate_cmd="${candidate_status#configured }"
|
|
274
|
+
command_resolvable "$candidate_cmd" && resolvable_any=1
|
|
275
|
+
;;
|
|
276
|
+
esac
|
|
277
|
+
done
|
|
278
|
+
|
|
279
|
+
# N1 (state-evidence, mirrors the meta-bridge doctor): if we HAVE an install-state, the
|
|
280
|
+
# managed config it recorded MUST still configure entwurf-bridge. state ∧ missing-key = DRIFT
|
|
281
|
+
# (installed, then someone removed it — the actual "wiring came loose / ?" case) → FAIL. This
|
|
282
|
+
# is the honest distinction from "never installed" (a note, below), which no state backs.
|
|
283
|
+
if [ -f "$STATE_FILE" ]; then
|
|
284
|
+
local managed expected_global
|
|
285
|
+
expected_global="$(python3 -c 'import os,sys; print(os.path.abspath(sys.argv[1]))' "$GLOBAL_CONFIG")"
|
|
286
|
+
if ! managed="$(python3 -c 'import json,os,sys; v=json.load(open(sys.argv[1])).get("managedConfigPath"); assert isinstance(v,str) and os.path.isabs(v); print(os.path.abspath(v))' "$STATE_FILE" 2>/dev/null)"; then
|
|
287
|
+
# isabs: install always records an absolute path. A relative one is tampered/corrupt state —
|
|
288
|
+
# normalizing it against OUR cwd could bless whatever directory the doctor happens to run from.
|
|
289
|
+
log " state: CORRUPT — install-state is unreadable or its managedConfigPath is missing/non-absolute: $STATE_FILE"
|
|
290
|
+
hard_fail=1
|
|
291
|
+
elif [ "$managed" != "$expected_global" ]; then
|
|
292
|
+
# The state describes a DIFFERENT file than the one agy reads here. Checking the recorded
|
|
293
|
+
# file and calling that green blesses a host we do not own: uninstall would not touch the
|
|
294
|
+
# live config, and the live config's provenance is gone. A verification run that isolated
|
|
295
|
+
# HOME but SHARED XDG_DATA_HOME writes exactly this — sandbox settings, real state.
|
|
296
|
+
log " state: FOREIGN TARGET — install-state manages '$managed', but agy reads '$expected_global' on this host. Nothing here is package-owned. Re-run install-agy-bridge against this host (and check whether an isolated test leaked its state)."
|
|
297
|
+
hard_fail=1
|
|
298
|
+
else
|
|
299
|
+
local managed_status
|
|
300
|
+
managed_status="$(python3 "$CONFIG_PY" doctor-static "$managed")"
|
|
301
|
+
case "$managed_status" in
|
|
302
|
+
configured\ *) log " state: install-state present; its managed config still configures entwurf-bridge." ;;
|
|
303
|
+
absent)
|
|
304
|
+
log " state: ORPHANED — install-state records $managed but the file is absent (HOME reset). Auto-cleaning stale state."
|
|
305
|
+
rm -f "$STATE_FILE"
|
|
306
|
+
;;
|
|
307
|
+
*) log " state: DRIFT — install-state records $managed but it no longer configures entwurf-bridge (removed since install)."; hard_fail=1 ;;
|
|
308
|
+
esac
|
|
309
|
+
fi
|
|
310
|
+
elif [ "$configured_any" -eq 0 ]; then
|
|
311
|
+
log " note: no install-state and neither candidate configures entwurf-bridge (never installed — this is the '?' the operator sees; run install-agy-bridge)."
|
|
312
|
+
fi
|
|
313
|
+
|
|
314
|
+
# The permission half carries its own target and can exist/drift independently of the MCP config
|
|
315
|
+
# state. Never nest this check under STATE_FILE: a foreign permission-state must fail even when
|
|
316
|
+
# the config state is absent.
|
|
317
|
+
if [ -f "$PERMISSION_STATE_FILE" ]; then
|
|
318
|
+
local pmanaged expected_settings
|
|
319
|
+
expected_settings="$(python3 -c 'import os,sys; print(os.path.abspath(sys.argv[1]))' "$SETTINGS_FILE")"
|
|
320
|
+
if ! pmanaged="$(python3 -c 'import json,os,sys; v=json.load(open(sys.argv[1])).get("managedSettingsPath"); assert isinstance(v,str) and os.path.isabs(v); print(os.path.abspath(v))' "$PERMISSION_STATE_FILE" 2>/dev/null)"; then
|
|
321
|
+
log " state: CORRUPT (permission) — permission-state is unreadable or its managedSettingsPath is missing/non-absolute: $PERMISSION_STATE_FILE"
|
|
322
|
+
hard_fail=1
|
|
323
|
+
elif [ "$pmanaged" != "$expected_settings" ]; then
|
|
324
|
+
log " state: FOREIGN TARGET (permission) — permission-state manages '$pmanaged', but agy reads '$expected_settings'. The grant recorded there is not the grant on this host."
|
|
325
|
+
hard_fail=1
|
|
326
|
+
fi
|
|
327
|
+
fi
|
|
328
|
+
|
|
329
|
+
log "── live (runtime wiring)"
|
|
330
|
+
if command -v pgrep >/dev/null 2>&1 && pgrep -x agy >/dev/null 2>&1; then
|
|
331
|
+
if [ "$resolvable_any" -eq 1 ]; then
|
|
332
|
+
# HONEST label (N2): a running agy + a resolvable configured candidate is CONSISTENT with
|
|
333
|
+
# runtime wiring, but it does NOT prove agy actually read that config — that needs MCP
|
|
334
|
+
# tool-listing-grade evidence (deferred). Ownership/state failures remain red independently.
|
|
335
|
+
if [ "$hard_fail" -eq 0 ]; then
|
|
336
|
+
log " live: agy is running AND a configured candidate has a resolvable command — consistent with runtime wiring (config-read NOT proven; MCP-tool-listing evidence deferred)."
|
|
337
|
+
else
|
|
338
|
+
log " live: agy is running and a configured candidate resolves, but ownership/state errors above keep this doctor red (config-read NOT proven)."
|
|
339
|
+
fi
|
|
340
|
+
else
|
|
341
|
+
log " live: agy is running but no resolvable configured candidate — runtime wiring is broken."
|
|
342
|
+
hard_fail=1
|
|
343
|
+
fi
|
|
344
|
+
else
|
|
345
|
+
log " live: SKIP — no agy process (cannot check runtime wiring; NOT a pass)."
|
|
346
|
+
fi
|
|
347
|
+
|
|
348
|
+
if [ "$hard_fail" -ne 0 ]; then
|
|
349
|
+
fail "doctor found a broken candidate (invalid JSON / dangling command / broken live wiring)."
|
|
350
|
+
fi
|
|
351
|
+
log "doctor: ok (static candidates clean)."
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
case "${1:-}" in
|
|
355
|
+
install) do_install ;;
|
|
356
|
+
uninstall) do_uninstall ;;
|
|
357
|
+
doctor) do_doctor ;;
|
|
358
|
+
*) echo "usage: agy-bridge.sh <install|uninstall|doctor>" >&2; exit 2 ;;
|
|
359
|
+
esac
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# agy-hooks-bridge — install/uninstall/doctor for Antigravity PreInvocation birth imprint.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
+
CONFIG_PY="$HERE/agy-hooks-config.py"
|
|
7
|
+
HOOKS="${AGY_HOOKS_CONFIG:-$HOME/.gemini/config/plugins/entwurf-agy-imprint/hooks.json}"
|
|
8
|
+
LEGACY_HOOKS="${AGY_LEGACY_HOOKS_CONFIG:-$HOME/.gemini/antigravity-cli/hooks.json}"
|
|
9
|
+
LEGACY_PLUGIN_HOOKS="${AGY_LEGACY_PLUGIN_HOOKS_CONFIG:-$HOME/.gemini/config/plugins/entwurf-probe/hooks.json}"
|
|
10
|
+
PLUGIN_JSON="$(dirname "$HOOKS")/plugin.json"
|
|
11
|
+
COMMAND="${AGY_IMPRINT_COMMAND:-entwurf-agy-imprint}"
|
|
12
|
+
STATE_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/agy-hooks"
|
|
13
|
+
STATE_FILE="$STATE_DIR/install-state.json"
|
|
14
|
+
|
|
15
|
+
log() { printf '%s\n' "$*"; }
|
|
16
|
+
fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; }
|
|
17
|
+
|
|
18
|
+
command_resolvable() {
|
|
19
|
+
local cmd="$1"
|
|
20
|
+
case "$cmd" in
|
|
21
|
+
*/*) [ -x "$cmd" ] ;;
|
|
22
|
+
*) command -v "$cmd" >/dev/null 2>&1 ;;
|
|
23
|
+
esac
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
do_install() {
|
|
27
|
+
log "[agy-hooks install]"
|
|
28
|
+
log " target: $HOOKS"
|
|
29
|
+
log " command: $COMMAND"
|
|
30
|
+
log " state: $STATE_FILE"
|
|
31
|
+
if ! command_resolvable "$COMMAND"; then
|
|
32
|
+
log " note: '$COMMAND' is not currently resolvable on this host — install still records it"
|
|
33
|
+
log " (expose-dev-bin/npm bin-link provides it; doctor will FAIL until it is on PATH)."
|
|
34
|
+
fi
|
|
35
|
+
mkdir -p "$(dirname "$PLUGIN_JSON")"
|
|
36
|
+
if [ ! -e "$PLUGIN_JSON" ]; then
|
|
37
|
+
printf '{\n "name": "entwurf-agy-imprint",\n "version": "1.0.0",\n "description": "Entwurf Antigravity PreInvocation imprint hook"\n}\n' > "$PLUGIN_JSON"
|
|
38
|
+
fi
|
|
39
|
+
local out rc
|
|
40
|
+
set +e
|
|
41
|
+
out="$(python3 "$CONFIG_PY" install "$HOOKS" "$COMMAND" "$STATE_FILE" 2>&1)"
|
|
42
|
+
rc=$?
|
|
43
|
+
set -e
|
|
44
|
+
case "$rc" in
|
|
45
|
+
0) log " ok: ${out}" ;;
|
|
46
|
+
3) fail "refused (symlink) — ${out}" ;;
|
|
47
|
+
4) fail "invalid JSON — ${out}" ;;
|
|
48
|
+
*) fail "install error (rc=$rc) — ${out}" ;;
|
|
49
|
+
esac
|
|
50
|
+
|
|
51
|
+
# Hard-cut SSOT: agy's live hook surface is the plugin hooks.json above. A previous probe pass
|
|
52
|
+
# wrote the same birth hook into ~/.gemini/antigravity-cli/hooks.json, which creates ambiguity
|
|
53
|
+
# (or double PreInvocation if a future agy reads both). Remove ONLY our known legacy keys there;
|
|
54
|
+
# preserve unrelated user hooks; refuse symlinks/corrupt JSON rather than guessing.
|
|
55
|
+
local legacy_out legacy_rc legacy_plugin_out legacy_plugin_rc
|
|
56
|
+
set +e
|
|
57
|
+
legacy_out="$(python3 "$CONFIG_PY" cleanup-legacy "$LEGACY_HOOKS" "$HOOKS" 2>&1)"
|
|
58
|
+
legacy_rc=$?
|
|
59
|
+
set -e
|
|
60
|
+
case "$legacy_rc" in
|
|
61
|
+
0) log " legacy top-level: ${legacy_out}" ;;
|
|
62
|
+
3) fail "legacy top-level cleanup refused (symlink) — ${legacy_out}" ;;
|
|
63
|
+
4) fail "legacy top-level cleanup invalid JSON — ${legacy_out}" ;;
|
|
64
|
+
*) fail "legacy top-level cleanup error (rc=$legacy_rc) — ${legacy_out}" ;;
|
|
65
|
+
esac
|
|
66
|
+
set +e
|
|
67
|
+
legacy_plugin_out="$(python3 "$CONFIG_PY" cleanup-legacy "$LEGACY_PLUGIN_HOOKS" "$HOOKS" 2>&1)"
|
|
68
|
+
legacy_plugin_rc=$?
|
|
69
|
+
set -e
|
|
70
|
+
case "$legacy_plugin_rc" in
|
|
71
|
+
0)
|
|
72
|
+
log " legacy plugin(entwurf-probe): ${legacy_plugin_out}"
|
|
73
|
+
if [ ! -e "$LEGACY_PLUGIN_HOOKS" ]; then
|
|
74
|
+
rm -f "$(dirname "$LEGACY_PLUGIN_HOOKS")/plugin.json"
|
|
75
|
+
rmdir "$(dirname "$LEGACY_PLUGIN_HOOKS")" 2>/dev/null || true
|
|
76
|
+
fi
|
|
77
|
+
;;
|
|
78
|
+
3) fail "legacy plugin cleanup refused (symlink) — ${legacy_plugin_out}" ;;
|
|
79
|
+
4) fail "legacy plugin cleanup invalid JSON — ${legacy_plugin_out}" ;;
|
|
80
|
+
*) fail "legacy plugin cleanup error (rc=$legacy_plugin_rc) — ${legacy_plugin_out}" ;;
|
|
81
|
+
esac
|
|
82
|
+
log " installed. Verify with: ./run.sh doctor-agy-hooks"
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
do_uninstall() {
|
|
86
|
+
log "[agy-hooks uninstall]"
|
|
87
|
+
log " state: $STATE_FILE"
|
|
88
|
+
local out rc
|
|
89
|
+
set +e
|
|
90
|
+
out="$(python3 "$CONFIG_PY" uninstall "$STATE_FILE" 2>&1)"
|
|
91
|
+
rc=$?
|
|
92
|
+
set -e
|
|
93
|
+
case "$rc" in
|
|
94
|
+
0) log " ok: ${out}" ;;
|
|
95
|
+
2) log " note: ${out}" ;;
|
|
96
|
+
3) fail "refused (symlink) — ${out}" ;;
|
|
97
|
+
*) fail "uninstall error (rc=$rc) — ${out}" ;;
|
|
98
|
+
esac
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
do_doctor() {
|
|
102
|
+
log "[agy-hooks doctor]"
|
|
103
|
+
local hard_fail=0 link_note=""
|
|
104
|
+
if [ -L "$HOOKS" ]; then link_note=" [symlink → $(readlink "$HOOKS")]"; fi
|
|
105
|
+
|
|
106
|
+
log "── static (hooks root: Antigravity plugin PreInvocation birth imprint)"
|
|
107
|
+
local status
|
|
108
|
+
status="$(python3 "$CONFIG_PY" doctor-static "$HOOKS")"
|
|
109
|
+
case "$status" in
|
|
110
|
+
file-absent) log " hooks: file absent$link_note" ;;
|
|
111
|
+
not-configured) log " hooks: entwurf-agy-imprint hook absent$link_note" ;;
|
|
112
|
+
invalid-json) log " hooks: INVALID JSON$link_note"; hard_fail=1 ;;
|
|
113
|
+
not-ours) log " hooks: entwurf-agy-imprint key present but NOT ours$link_note" ;;
|
|
114
|
+
configured\ *)
|
|
115
|
+
local cmd="${status#configured }"
|
|
116
|
+
if command_resolvable "$cmd"; then
|
|
117
|
+
log " hooks: configured → '$cmd' (resolvable)$link_note"
|
|
118
|
+
else
|
|
119
|
+
log " hooks: configured → '$cmd' DANGLING (not on PATH / not executable)$link_note"
|
|
120
|
+
hard_fail=1
|
|
121
|
+
fi ;;
|
|
122
|
+
*) log " hooks: unexpected status '$status'$link_note"; hard_fail=1 ;;
|
|
123
|
+
esac
|
|
124
|
+
|
|
125
|
+
log "── legacy hook guards (must not carry entwurf/probe keys)"
|
|
126
|
+
local legacy_status legacy_plugin_status
|
|
127
|
+
legacy_status="$(python3 "$CONFIG_PY" doctor-legacy "$LEGACY_HOOKS" "$HOOKS")"
|
|
128
|
+
case "$legacy_status" in
|
|
129
|
+
absent) log " legacy top-level: absent (ok)" ;;
|
|
130
|
+
same-path) log " legacy top-level: same as active target (ok under override)" ;;
|
|
131
|
+
clean) log " legacy top-level: present but carries no entwurf/probe hook keys (ok)" ;;
|
|
132
|
+
symlink\ *) log " legacy top-level: SYMLINK ${legacy_status#symlink } — cannot prove single-hook ownership"; hard_fail=1 ;;
|
|
133
|
+
invalid-json) log " legacy top-level: INVALID JSON — cannot prove single-hook ownership"; hard_fail=1 ;;
|
|
134
|
+
owned\ *) log " legacy top-level: DRIFT — still carries ${legacy_status#owned } (run install-agy-hooks to clean)"; hard_fail=1 ;;
|
|
135
|
+
*) log " legacy top-level: unexpected status '$legacy_status'"; hard_fail=1 ;;
|
|
136
|
+
esac
|
|
137
|
+
legacy_plugin_status="$(python3 "$CONFIG_PY" doctor-legacy "$LEGACY_PLUGIN_HOOKS" "$HOOKS")"
|
|
138
|
+
case "$legacy_plugin_status" in
|
|
139
|
+
absent) log " legacy plugin(entwurf-probe): absent (ok)" ;;
|
|
140
|
+
same-path) log " legacy plugin(entwurf-probe): same as active target (ok under override)" ;;
|
|
141
|
+
clean) log " legacy plugin(entwurf-probe): present but carries no entwurf/probe hook keys (ok)" ;;
|
|
142
|
+
symlink\ *) log " legacy plugin(entwurf-probe): SYMLINK ${legacy_plugin_status#symlink } — cannot prove single-hook ownership"; hard_fail=1 ;;
|
|
143
|
+
invalid-json) log " legacy plugin(entwurf-probe): INVALID JSON — cannot prove single-hook ownership"; hard_fail=1 ;;
|
|
144
|
+
owned\ *) log " legacy plugin(entwurf-probe): DRIFT — still carries ${legacy_plugin_status#owned } (run install-agy-hooks to clean)"; hard_fail=1 ;;
|
|
145
|
+
*) log " legacy plugin(entwurf-probe): unexpected status '$legacy_plugin_status'"; hard_fail=1 ;;
|
|
146
|
+
esac
|
|
147
|
+
|
|
148
|
+
if [ -f "$STATE_FILE" ]; then
|
|
149
|
+
local managed expected_hooks
|
|
150
|
+
expected_hooks="$(python3 -c 'import os,sys; print(os.path.abspath(sys.argv[1]))' "$HOOKS")"
|
|
151
|
+
if ! managed="$(python3 -c 'import json,os,sys; v=json.load(open(sys.argv[1])).get("managedHooksPath"); assert isinstance(v,str) and os.path.isabs(v); print(os.path.abspath(v))' "$STATE_FILE" 2>/dev/null)"; then
|
|
152
|
+
# isabs: install always records an absolute path — a relative one is corrupt, and
|
|
153
|
+
# normalizing it against OUR cwd could bless whatever directory the doctor runs from.
|
|
154
|
+
log " state: CORRUPT — install-state is unreadable or its managedHooksPath is missing/non-absolute: $STATE_FILE"
|
|
155
|
+
hard_fail=1
|
|
156
|
+
elif [ "$managed" != "$expected_hooks" ]; then
|
|
157
|
+
# The state describes a DIFFERENT hooks file than the one agy loads here — so the live hook
|
|
158
|
+
# is unowned: uninstall would leave it in place, and its provenance (what the file held
|
|
159
|
+
# before we wrote it) is unrecorded. A test run that isolated HOME but SHARED XDG_DATA_HOME
|
|
160
|
+
# produces exactly this shape.
|
|
161
|
+
log " state: FOREIGN TARGET — install-state manages '$managed', but agy loads '$expected_hooks' on this host. The live PreInvocation hook is unowned. Re-run install-agy-hooks against this host (and check whether an isolated test leaked its state)."
|
|
162
|
+
hard_fail=1
|
|
163
|
+
else
|
|
164
|
+
local managed_status
|
|
165
|
+
managed_status="$(python3 "$CONFIG_PY" doctor-static "$managed")"
|
|
166
|
+
case "$managed_status" in
|
|
167
|
+
configured\ *) log " state: install-state present; its managed hooks still configure entwurf-agy-imprint." ;;
|
|
168
|
+
file-absent)
|
|
169
|
+
log " state: ORPHANED — install-state records $managed but the file is absent (HOME reset). Auto-cleaning stale state."
|
|
170
|
+
rm -f "$STATE_FILE"
|
|
171
|
+
;;
|
|
172
|
+
*) log " state: DRIFT — install-state records $managed but it no longer configures entwurf-agy-imprint."; hard_fail=1 ;;
|
|
173
|
+
esac
|
|
174
|
+
fi
|
|
175
|
+
else
|
|
176
|
+
case "$status" in
|
|
177
|
+
configured\ *) : ;;
|
|
178
|
+
*) log " note: no install-state and imprint hook not entwurf-owned (never installed — this keeps agy at '?'; run install-agy-hooks)." ;;
|
|
179
|
+
esac
|
|
180
|
+
fi
|
|
181
|
+
|
|
182
|
+
if [ "$hard_fail" -ne 0 ]; then
|
|
183
|
+
fail "doctor found broken agy hooks (invalid JSON / dangling command / drift)."
|
|
184
|
+
fi
|
|
185
|
+
log "doctor: ok."
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
case "${1:-}" in
|
|
189
|
+
install) do_install ;;
|
|
190
|
+
uninstall) do_uninstall ;;
|
|
191
|
+
doctor) do_doctor ;;
|
|
192
|
+
*) echo "usage: agy-hooks-bridge.sh <install|uninstall|doctor>" >&2; exit 2 ;;
|
|
193
|
+
esac
|