@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,182 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# smoke-pi-provider-state — regression gate for the pi provider install adapter (#46 Task 2):
|
|
3
|
+
# register-pi-provider.py (ownership-classified install/remove, user+project scopes) + the
|
|
4
|
+
# read-only doctor-pi-provider.ts (effective shadow view). Runs in an ISOLATED HOME + XDG with a
|
|
5
|
+
# fake stable bin and isolated settings files (no real ~/.pi). Asserts:
|
|
6
|
+
# - user install ownership matrix: absent→create+state; managed-legacy(repo start.sh)→bare bin +
|
|
7
|
+
# state(preimage audit, NOT restored) + siblings preserved; managed-current→idempotent;
|
|
8
|
+
# user-override→NOT overwritten, NO state (unowned).
|
|
9
|
+
# - user remove: state-based honest inverse (managed-* → remove OUR key, a legacy repo path is
|
|
10
|
+
# NOT restored; siblings kept; parent tidied); user-override → no state → nothing to undo.
|
|
11
|
+
# - legacy bundle prune (session-bridge/pi-tools-bridge repo path) alongside, user my-own kept.
|
|
12
|
+
# - project scope: NO state; install normalizes the bare bin, remove strips our-managed shapes
|
|
13
|
+
# (bare bin AND legacy repo path), a user override left in place.
|
|
14
|
+
# - doctor: effective = project shadows user; user-only/both-bare → ok green; project-STALE
|
|
15
|
+
# (user bare, project legacy) → effective legacy note (the "doctor green runtime red" guard);
|
|
16
|
+
# state-owned DRIFT → FAIL; bare-but-dangling → FAIL; malformed settings → FAIL; symlink refuse.
|
|
17
|
+
# - checkout stays byte-identical (nothing under $REPO).
|
|
18
|
+
# Offline + deterministic (deps: bash + python3 + node --experimental-strip-types).
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
|
|
21
|
+
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
22
|
+
REG="$REPO_DIR/scripts/register-pi-provider.py"
|
|
23
|
+
DOCTOR="$REPO_DIR/scripts/doctor-pi-provider.ts"
|
|
24
|
+
|
|
25
|
+
pass=0
|
|
26
|
+
ok() { printf ' ok %s\n' "$1"; pass=$((pass + 1)); }
|
|
27
|
+
die() { printf 'FAIL: %s\n' "$1" >&2; exit 1; }
|
|
28
|
+
want() { eval "$2" && ok "$1" || die "$1"; }
|
|
29
|
+
|
|
30
|
+
REPO_BEFORE="$(cd "$REPO_DIR" && git status --porcelain)"
|
|
31
|
+
|
|
32
|
+
SB="$(mktemp -d)"
|
|
33
|
+
trap 'rm -rf "$SB"' EXIT
|
|
34
|
+
export HOME="$SB/home"
|
|
35
|
+
export XDG_DATA_HOME="$SB/xdg"
|
|
36
|
+
GLOBAL="$SB/global.json"
|
|
37
|
+
PROJECT="$SB/proj/.pi/settings.json"
|
|
38
|
+
STATE="$XDG_DATA_HOME/entwurf/pi-provider/install-state.json"
|
|
39
|
+
mkdir -p "$SB/proj/.pi" "$SB/bin" "$SB/home"
|
|
40
|
+
|
|
41
|
+
# A fake repo anchor for the managed-legacy predicate (endswith /entwurf/mcp/entwurf-bridge/start.sh).
|
|
42
|
+
FAKE_REPO="$SB/checkout/entwurf"
|
|
43
|
+
LEGACY_CMD="$FAKE_REPO/mcp/entwurf-bridge/start.sh"
|
|
44
|
+
|
|
45
|
+
# fake stable bin on PATH so the bare command RESOLVES in the doctor. Keep THIS fake
|
|
46
|
+
# authoritative: drop the dir that holds a REAL entwurf-bridge (this dev host exposes one at
|
|
47
|
+
# ~/.local/bin) so the dangling test is not masked by it.
|
|
48
|
+
printf '#!/usr/bin/env bash\necho fake-bridge\n' > "$SB/bin/entwurf-bridge"
|
|
49
|
+
chmod +x "$SB/bin/entwurf-bridge"
|
|
50
|
+
_real_bridge="$(command -v entwurf-bridge 2>/dev/null || true)"
|
|
51
|
+
_real_dir="${_real_bridge%/*}"
|
|
52
|
+
if [ -n "$_real_dir" ]; then
|
|
53
|
+
PATH="$(printf '%s' "$PATH" | tr ':' '\n' | grep -vFx "$_real_dir" | paste -sd: -)"
|
|
54
|
+
fi
|
|
55
|
+
export PATH="$SB/bin:$PATH"
|
|
56
|
+
|
|
57
|
+
reg() { python3 "$REG" "$@"; }
|
|
58
|
+
doctor() { PI_PROVIDER_GLOBAL_SETTINGS="$GLOBAL" PI_PROVIDER_PROJECT_SETTINGS="$PROJECT" PI_PROVIDER_STATE="$STATE" \
|
|
59
|
+
node --experimental-strip-types "$DOCTOR"; }
|
|
60
|
+
cmd_of() { python3 -c 'import json,sys
|
|
61
|
+
try:
|
|
62
|
+
d=json.load(open(sys.argv[1])); print(d["entwurfProvider"]["mcpServers"]["entwurf-bridge"]["command"])
|
|
63
|
+
except Exception: print("<none>")' "$1"; }
|
|
64
|
+
|
|
65
|
+
# ── A: user install — absent → create + state(ownership=absent) ───────────────
|
|
66
|
+
printf '{"defaultProvider":"x"}\n' > "$GLOBAL"
|
|
67
|
+
reg install "$GLOBAL" "$FAKE_REPO" --scope user --state "$STATE" >/dev/null
|
|
68
|
+
want "user/absent: command is bare stable bin" "[ \"\$(cmd_of '$GLOBAL')\" = entwurf-bridge ]"
|
|
69
|
+
want "user/absent: state ownership=absent" \
|
|
70
|
+
"python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))[\"ownership\"])' '$STATE' | grep -qx absent"
|
|
71
|
+
|
|
72
|
+
# ── B: user install — managed-legacy(repo start.sh) → bare bin + preimage + siblings kept ──
|
|
73
|
+
rm -f "$STATE"
|
|
74
|
+
python3 -c 'import json,sys; json.dump({"entwurfProvider":{"appendSystemPrompt":False,"skillPlugins":["/p"],"mcpServers":{"entwurf-bridge":{"command":sys.argv[2]}}},"packages":["y"]}, open(sys.argv[1],"w"))' "$GLOBAL" "$LEGACY_CMD"
|
|
75
|
+
OUT="$(reg install "$GLOBAL" "$FAKE_REPO" --scope user --state "$STATE")"
|
|
76
|
+
want "user/managed-legacy: reported as managed-legacy" "printf '%s' \"\$OUT\" | grep -q 'managed-legacy'"
|
|
77
|
+
want "user/managed-legacy: normalized to bare bin" "[ \"\$(cmd_of '$GLOBAL')\" = entwurf-bridge ]"
|
|
78
|
+
want "user/managed-legacy: siblings preserved (skillPlugins/appendSystemPrompt)" \
|
|
79
|
+
"python3 -c 'import json,sys; ep=json.load(open(sys.argv[1]))[\"entwurfProvider\"]; sys.exit(0 if ep.get(\"skillPlugins\")==[\"/p\"] and ep.get(\"appendSystemPrompt\") is False else 1)' '$GLOBAL'"
|
|
80
|
+
want "user/managed-legacy: state ownership=managed-legacy" \
|
|
81
|
+
"python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))[\"ownership\"])' '$STATE' | grep -qx managed-legacy"
|
|
82
|
+
want "user/managed-legacy: preimage records the repo path (audit, not restored)" \
|
|
83
|
+
"python3 -c 'import json,sys; p=json.load(open(sys.argv[1]))[\"preimage\"]; sys.exit(0 if p and p.get(\"command\")==sys.argv[2] else 1)' '$STATE' '$LEGACY_CMD'"
|
|
84
|
+
want "user/managed-legacy: timing honesty label emitted" \
|
|
85
|
+
"printf '%s' \"\$OUT\" | grep -q 'existing pi sessions unaffected until restart'"
|
|
86
|
+
|
|
87
|
+
# ── C: user install — managed-current(bare) → idempotent ──────────────────────
|
|
88
|
+
OUT="$(reg install "$GLOBAL" "$FAKE_REPO" --scope user --state "$STATE")"
|
|
89
|
+
want "user/managed-current: idempotent (stays bare)" "[ \"\$(cmd_of '$GLOBAL')\" = entwurf-bridge ]"
|
|
90
|
+
want "user/managed-current: reported managed-current" "printf '%s' \"\$OUT\" | grep -q 'managed-current'"
|
|
91
|
+
|
|
92
|
+
# ── D: user install — user-override → NOT overwritten, NO state ───────────────
|
|
93
|
+
rm -f "$STATE"
|
|
94
|
+
printf '{"entwurfProvider":{"mcpServers":{"entwurf-bridge":{"command":"/my/own/thing"}}}}\n' > "$GLOBAL"
|
|
95
|
+
OUT="$(reg install "$GLOBAL" "$FAKE_REPO" --scope user --state "$STATE")"
|
|
96
|
+
want "user/override: command left untouched" "[ \"\$(cmd_of '$GLOBAL')\" = /my/own/thing ]"
|
|
97
|
+
want "user/override: NO state written (unowned)" "[ ! -f '$STATE' ]"
|
|
98
|
+
want "user/override: reported preserved user override" "printf '%s' \"\$OUT\" | grep -qi 'user override'"
|
|
99
|
+
|
|
100
|
+
# ── E: user remove — state-based honest inverse (key removed, siblings kept) ──
|
|
101
|
+
python3 -c 'import json,sys; json.dump({"entwurfProvider":{"skillPlugins":["/p"],"mcpServers":{"entwurf-bridge":{"command":sys.argv[2]}}}}, open(sys.argv[1],"w"))' "$GLOBAL" "$LEGACY_CMD"
|
|
102
|
+
reg install "$GLOBAL" "$FAKE_REPO" --scope user --state "$STATE" >/dev/null
|
|
103
|
+
reg remove "$GLOBAL" "$FAKE_REPO" --scope user --state "$STATE" >/dev/null
|
|
104
|
+
want "user/remove: our key removed (NOT restored to repo path)" "[ \"\$(cmd_of '$GLOBAL')\" = '<none>' ]"
|
|
105
|
+
want "user/remove: sibling skillPlugins preserved" \
|
|
106
|
+
"python3 -c 'import json,sys; sys.exit(0 if json.load(open(sys.argv[1]))[\"entwurfProvider\"].get(\"skillPlugins\")==[\"/p\"] else 1)' '$GLOBAL'"
|
|
107
|
+
want "user/remove: state removed" "[ ! -f '$STATE' ]"
|
|
108
|
+
reg remove "$GLOBAL" "$FAKE_REPO" --scope user --state "$STATE" >/dev/null 2>&1
|
|
109
|
+
ok "user/remove: no-state is idempotent (nothing to undo)"
|
|
110
|
+
|
|
111
|
+
# ── F: legacy bundle prune alongside, user-authored server kept ───────────────
|
|
112
|
+
python3 -c 'import json,sys; json.dump({"entwurfProvider":{"mcpServers":{"entwurf-bridge":{"command":sys.argv[2]},"session-bridge":{"command":sys.argv[3]},"my-own":{"command":"/custom"}}}}, open(sys.argv[1],"w"))' "$GLOBAL" "$LEGACY_CMD" "$FAKE_REPO/mcp/session-bridge/start.sh"
|
|
113
|
+
reg install "$GLOBAL" "$FAKE_REPO" --scope user --state "$STATE" >/dev/null
|
|
114
|
+
want "legacy-prune: session-bridge removed" \
|
|
115
|
+
"python3 -c 'import json,sys; sys.exit(0 if \"session-bridge\" not in json.load(open(sys.argv[1]))[\"entwurfProvider\"][\"mcpServers\"] else 1)' '$GLOBAL'"
|
|
116
|
+
want "legacy-prune: user-authored my-own kept" \
|
|
117
|
+
"python3 -c 'import json,sys; sys.exit(0 if \"my-own\" in json.load(open(sys.argv[1]))[\"entwurfProvider\"][\"mcpServers\"] else 1)' '$GLOBAL'"
|
|
118
|
+
reg remove "$GLOBAL" "$FAKE_REPO" --scope user --state "$STATE" >/dev/null; rm -f "$STATE"
|
|
119
|
+
|
|
120
|
+
# ── G: project scope — NO state; install normalizes, remove strips managed shapes ──
|
|
121
|
+
python3 -c 'import json,sys; json.dump({"entwurfProvider":{"mcpServers":{"entwurf-bridge":{"command":sys.argv[2]}}}}, open(sys.argv[1],"w"))' "$PROJECT" "$LEGACY_CMD"
|
|
122
|
+
reg install "$PROJECT" "$FAKE_REPO" --scope project >/dev/null
|
|
123
|
+
want "project/install: normalized to bare bin" "[ \"\$(cmd_of '$PROJECT')\" = entwurf-bridge ]"
|
|
124
|
+
want "project/install: NO user-scope state created" "[ ! -f '$STATE' ]"
|
|
125
|
+
reg remove "$PROJECT" "$FAKE_REPO" --scope project >/dev/null
|
|
126
|
+
want "project/remove: strips the bare bin (our-managed)" "[ \"\$(cmd_of '$PROJECT')\" = '<none>' ]"
|
|
127
|
+
|
|
128
|
+
# ── H: doctor — effective shadow view + verdicts ──────────────────────────────
|
|
129
|
+
# H-1: user-only bare → effective user, ok green
|
|
130
|
+
printf '{"entwurfProvider":{"mcpServers":{"entwurf-bridge":{"command":"entwurf-bridge"}}}}\n' > "$GLOBAL"
|
|
131
|
+
printf '{}\n' > "$PROJECT"; rm -f "$STATE"
|
|
132
|
+
OUT="$(doctor)"; RC=$?
|
|
133
|
+
want "doctor(user-only bare): exits 0" "[ '$RC' -eq 0 ]"
|
|
134
|
+
want "doctor(user-only bare): ok — bare bin resolves" "printf '%s' \"\$OUT\" | grep -q 'bare stable bin'"
|
|
135
|
+
|
|
136
|
+
# H-2: project STALE (user bare, project legacy) → effective=project=legacy note (green-runtime-red guard)
|
|
137
|
+
printf '{"entwurfProvider":{"mcpServers":{"entwurf-bridge":{"command":"entwurf-bridge"}}}}\n' > "$GLOBAL"
|
|
138
|
+
python3 -c 'import json,sys; json.dump({"entwurfProvider":{"mcpServers":{"entwurf-bridge":{"command":sys.argv[2]}}}}, open(sys.argv[1],"w"))' "$PROJECT" "$LEGACY_CMD"
|
|
139
|
+
OUT="$(doctor)"; RC=$?
|
|
140
|
+
want "doctor(project-stale): exits 0 (honest note, not a crash)" "[ '$RC' -eq 0 ]"
|
|
141
|
+
want "doctor(project-stale): EFFECTIVE is project (shadows user)" "printf '%s' \"\$OUT\" | grep -q 'EFFECTIVE (project)'"
|
|
142
|
+
want "doctor(project-stale): flags legacy managed path (not adopted)" "printf '%s' \"\$OUT\" | grep -q 'LEGACY managed repo path'"
|
|
143
|
+
|
|
144
|
+
# H-3: state-owned DRIFT → FAIL (state says managed but effective is foreign)
|
|
145
|
+
printf '{"entwurfProvider":{"mcpServers":{"entwurf-bridge":{"command":"/foreign/thing"}}}}\n' > "$GLOBAL"
|
|
146
|
+
printf '{}\n' > "$PROJECT"
|
|
147
|
+
python3 -c 'import json,sys; json.dump({"schemaVersion":1,"managedSettingsPath":sys.argv[1],"scope":"user","ownership":"managed-legacy","command":"entwurf-bridge","preimage":None}, open(sys.argv[2],"w"))' "$GLOBAL" "$STATE"
|
|
148
|
+
if doctor >/dev/null 2>&1; then die "doctor(drift): should FAIL (state owns but effective drifted)"; fi
|
|
149
|
+
ok "doctor(drift): FAILS on state-owned + effective-drifted"
|
|
150
|
+
rm -f "$STATE"
|
|
151
|
+
|
|
152
|
+
# H-4: bare-but-dangling (bin not on PATH) → FAIL
|
|
153
|
+
printf '{"entwurfProvider":{"mcpServers":{"entwurf-bridge":{"command":"entwurf-bridge"}}}}\n' > "$GLOBAL"
|
|
154
|
+
printf '{}\n' > "$PROJECT"
|
|
155
|
+
mv "$SB/bin/entwurf-bridge" "$SB/bin/entwurf-bridge.hidden"
|
|
156
|
+
if doctor >/dev/null 2>&1; then die "doctor(dangling): should FAIL (bare bin not resolvable)"; fi
|
|
157
|
+
ok "doctor(dangling): FAILS when the bare bin does not resolve"
|
|
158
|
+
mv "$SB/bin/entwurf-bridge.hidden" "$SB/bin/entwurf-bridge"
|
|
159
|
+
|
|
160
|
+
# H-5: malformed settings → FAIL (SSOT read throws)
|
|
161
|
+
printf 'not json{{{' > "$GLOBAL"; printf '{}\n' > "$PROJECT"
|
|
162
|
+
if doctor >/dev/null 2>&1; then die "doctor(malformed): should FAIL"; fi
|
|
163
|
+
ok "doctor(malformed): FAILS on malformed settings (SSOT read throws)"
|
|
164
|
+
|
|
165
|
+
# H-6: none configured → honest '?' note, exits 0
|
|
166
|
+
printf '{}\n' > "$GLOBAL"; printf '{}\n' > "$PROJECT"
|
|
167
|
+
OUT="$(doctor)"; RC=$?
|
|
168
|
+
want "doctor(none): exits 0" "[ '$RC' -eq 0 ]"
|
|
169
|
+
want "doctor(none): honest never-installed '?' note" "printf '%s' \"\$OUT\" | grep -q 'never installed'"
|
|
170
|
+
|
|
171
|
+
# ── I: symlink target → install REFUSES ───────────────────────────────────────
|
|
172
|
+
printf '{}\n' > "$SB/real.json"; rm -f "$GLOBAL"; ln -s "$SB/real.json" "$GLOBAL"
|
|
173
|
+
if reg install "$GLOBAL" "$FAKE_REPO" --scope user --state "$STATE" >/dev/null 2>&1; then die "symlink: install should REFUSE"; fi
|
|
174
|
+
ok "symlink: install refused (someone else's SSOT)"
|
|
175
|
+
rm -f "$GLOBAL"
|
|
176
|
+
|
|
177
|
+
# ── checkout purity ───────────────────────────────────────────────────────────
|
|
178
|
+
REPO_AFTER="$(cd "$REPO_DIR" && git status --porcelain)"
|
|
179
|
+
want "purity: checkout unchanged (all writes stayed in the sandbox HOME+XDG)" \
|
|
180
|
+
"[ \"\$REPO_BEFORE\" = \"\$REPO_AFTER\" ]"
|
|
181
|
+
|
|
182
|
+
printf '\nsmoke-pi-provider-state: %d checks passed\n' "$pass"
|
|
@@ -23,6 +23,18 @@ command -v python3 >/dev/null || { echo "FAIL: python3 not on PATH"; exit 1; }
|
|
|
23
23
|
|
|
24
24
|
TMP="$(mktemp -d -t psa-user-citizen.XXXXXX)"
|
|
25
25
|
trap 'rm -rf "$TMP"' EXIT
|
|
26
|
+
|
|
27
|
+
# Isolation is a property of every root run.sh writes through, not of the one this gate happens
|
|
28
|
+
# to override. `remove-user-scope` today reads PI_CODING_AGENT_DIR (settings) and XDG_DATA_HOME
|
|
29
|
+
# (ownership state) — passing the operator's real XDG_DATA_HOME is what let this gate delete the
|
|
30
|
+
# live MCP key during `pnpm check` (2026-07-14). It does not touch HOME *today*; sandbox HOME and
|
|
31
|
+
# the whole XDG trio anyway, so the next root run.sh reaches for is already fenced (AGENTS rule 11).
|
|
32
|
+
export HOME="$TMP/home"
|
|
33
|
+
export XDG_DATA_HOME="$TMP/xdg"
|
|
34
|
+
export XDG_STATE_HOME="$TMP/state"
|
|
35
|
+
export XDG_CACHE_HOME="$TMP/cache"
|
|
36
|
+
mkdir -p "$HOME" "$XDG_DATA_HOME" "$XDG_STATE_HOME" "$XDG_CACHE_HOME"
|
|
37
|
+
|
|
26
38
|
S="$TMP/settings.json"
|
|
27
39
|
FAKE_REPO="$TMP/fake/entwurf"; mkdir -p "$FAKE_REPO"
|
|
28
40
|
RESOLVED="$(cd "$FAKE_REPO" && pwd)"
|
|
@@ -125,5 +137,55 @@ assert '../../repos/gh/andenken' in p, 'andenken over-removed'
|
|
|
125
137
|
assert '/home/me/entwurf-notes' in p, 'entwurf-notes over-removed (substring over-delete)'
|
|
126
138
|
" 2>/dev/null; then ok "--remove drops entwurf, preserves unrelated + look-alikes (symmetric with install)"; else bad "--remove over-deleted a look-alike or missed entwurf"; fi
|
|
127
139
|
|
|
140
|
+
# 9. --dry-run remove REPORTS the count and writes NOTHING (backs run.sh's project
|
|
141
|
+
# `remove` pointer note that suggests the global inverse only when relevant).
|
|
142
|
+
cat > "$S" <<JSON
|
|
143
|
+
{"packages": ["$RESOLVED", "../../repos/gh/andenken"]}
|
|
144
|
+
JSON
|
|
145
|
+
MT_DR="$(stat -c %Y "$S")"; sleep 1
|
|
146
|
+
OUT_DR="$(python3 "$REG" "$S" "$FAKE_REPO" --remove --dry-run)"
|
|
147
|
+
MT_DR2="$(stat -c %Y "$S")"
|
|
148
|
+
if printf '%s' "$OUT_DR" | grep -q 'would remove 1'; then ok "--dry-run reports 'would remove' count"; else bad "--dry-run did not report the would-remove count: $OUT_DR"; fi
|
|
149
|
+
if [ "$MT_DR" = "$MT_DR2" ] && has_pkg "$RESOLVED"; then ok "--dry-run writes nothing (entry intact, mtime stable)"; else bad "--dry-run mutated settings (mtime $MT_DR -> $MT_DR2)"; fi
|
|
150
|
+
OUT_DR_NONE="$(python3 "$REG" "$TMP/absent.json" "$FAKE_REPO" --remove --dry-run)"
|
|
151
|
+
if printf '%s' "$OUT_DR_NONE" | grep -q 'no entwurf'; then ok "--dry-run on an entwurf-free file reports nothing to remove"; else bad "--dry-run false-positive on absent entry: $OUT_DR_NONE"; fi
|
|
152
|
+
|
|
153
|
+
# 9b. --dry-run WITHOUT --remove must FAIL LOUD and write nothing. A flag named
|
|
154
|
+
# "dry-run" that falls through to the register write path and mutates settings
|
|
155
|
+
# is an install-hygiene footgun (GPT blocker, 2026-07-03).
|
|
156
|
+
cat > "$S" <<'JSON'
|
|
157
|
+
{"packages": ["../../repos/gh/andenken"]}
|
|
158
|
+
JSON
|
|
159
|
+
MT_DRR="$(stat -c %Y "$S")"; sleep 1
|
|
160
|
+
if python3 "$REG" "$S" "$FAKE_REPO" --dry-run >/dev/null 2>&1; then bad "--dry-run without --remove should fail loud, not register"; else ok "--dry-run without --remove fails loud (never a silent write)"; fi
|
|
161
|
+
MT_DRR2="$(stat -c %Y "$S")"
|
|
162
|
+
if [ "$MT_DRR" = "$MT_DRR2" ] && ! has_pkg "$RESOLVED"; then ok "--dry-run without --remove wrote nothing (mtime stable, no entwurf entry added)"; else bad "--dry-run without --remove mutated settings (mtime $MT_DRR -> $MT_DRR2)"; fi
|
|
163
|
+
if python3 "$REG" "$S" "$FAKE_REPO" --bogus >/dev/null 2>&1; then bad "unknown flag should be rejected"; else ok "unknown flag rejected (fail-loud parser)"; fi
|
|
164
|
+
|
|
165
|
+
# 10. the run.sh SHELL path `remove-user-scope` reaches the SAME SSOT against a
|
|
166
|
+
# PI_CODING_AGENT_DIR-overridden ~/.pi/agent — proves install's user-scope
|
|
167
|
+
# inverse is REACHABLE from run.sh, not only from the python SSOT (the gap GPT
|
|
168
|
+
# flagged: remove symmetry existed at the SSOT but no run.sh path exercised it).
|
|
169
|
+
# REPO_DIR (run.sh's own dir) is the real checkout, so seed THAT as the entry.
|
|
170
|
+
RUN="$REPO/run.sh"
|
|
171
|
+
AGENT_DIR="$TMP/agent"; mkdir -p "$AGENT_DIR"
|
|
172
|
+
US="$AGENT_DIR/settings.json"
|
|
173
|
+
cat > "$US" <<JSON
|
|
174
|
+
{"defaultProvider": "openai-codex", "packages": ["$REPO", "../../repos/gh/andenken"]}
|
|
175
|
+
JSON
|
|
176
|
+
# remove-user-scope uses BOTH PI_CODING_AGENT_DIR (settings target) and XDG_DATA_HOME
|
|
177
|
+
# (ownership-state authority). Isolating only the former lets the fake inverse consume the
|
|
178
|
+
# operator's real state, follow its recorded managedSettingsPath, and remove the live MCP key.
|
|
179
|
+
XDG_DATA_HOME="$TMP/xdg" PI_CODING_AGENT_DIR="$AGENT_DIR" bash "$RUN" remove-user-scope >/dev/null 2>&1 || bad "run.sh remove-user-scope exited non-zero"
|
|
180
|
+
if python3 -c "
|
|
181
|
+
import json
|
|
182
|
+
d=json.load(open('$US')); p=d['packages']
|
|
183
|
+
assert '$REPO' not in p, 'run.sh remove-user-scope did not drop the global entwurf entry'
|
|
184
|
+
assert '../../repos/gh/andenken' in p, 'run.sh remove-user-scope over-removed an unrelated package'
|
|
185
|
+
assert d['defaultProvider']=='openai-codex', 'run.sh remove-user-scope dropped an unrelated key'
|
|
186
|
+
" 2>/dev/null; then ok "run.sh remove-user-scope drops the global citizen, preserves unrelated (SSOT reached via shell)"; else bad "run.sh remove-user-scope path failed"; fi
|
|
187
|
+
# idempotent: a second remove-user-scope is a clean no-op (no crash on absent entry)
|
|
188
|
+
if XDG_DATA_HOME="$TMP/xdg" PI_CODING_AGENT_DIR="$AGENT_DIR" bash "$RUN" remove-user-scope >/dev/null 2>&1; then ok "run.sh remove-user-scope is idempotent (no-op second run)"; else bad "run.sh remove-user-scope second run crashed"; fi
|
|
189
|
+
|
|
128
190
|
echo
|
|
129
191
|
if [ "$fail" -eq 0 ]; then echo "smoke-user-scope-citizen: PASS"; else echo "smoke-user-scope-citizen: FAIL (see above)"; exit 1; fi
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Serialize a command that rebuilds OR reads mcp/entwurf-bridge/dist against every
|
|
4
|
+
# other such command: `with-dist-lock.sh <cmd...>` runs <cmd> holding an atomic
|
|
5
|
+
# lock over the shared dist dir.
|
|
6
|
+
#
|
|
7
|
+
# Why the lock has to wrap the WHOLE command, not just the emit:
|
|
8
|
+
# build-bridge does `rm -rf dist` then a tsc emit; `npm pack` runs build-bridge as
|
|
9
|
+
# its prepack hook and then, AFTER prepack returns, walks the tree to read dist into
|
|
10
|
+
# the tarball. So the window where dist must stay intact is [prepack build ‥ npm's
|
|
11
|
+
# post-build read] — the entire pack, not the emit alone. A lock scoped to just the
|
|
12
|
+
# emit (released when build-bridge exits) is not merely insufficient, it BACKFIRES:
|
|
13
|
+
# it staggers concurrent packs so each run's lock-free read lands squarely inside
|
|
14
|
+
# the next run's `rm -rf dist`, turning an occasional race into a near-deterministic
|
|
15
|
+
# phantom "dist missing" pack failure (measured: 7/8 packs MISSING). Wrapping the
|
|
16
|
+
# whole pack fixes it — the read is now inside the lock (2026-07-03).
|
|
17
|
+
#
|
|
18
|
+
# Reentrancy: the wrapped `npm pack` re-invokes build-bridge as its prepack, which
|
|
19
|
+
# is itself wrapped by this script (package.json). ENTWURF_BUILD_LOCK_HELD, exported
|
|
20
|
+
# once the outer wrapper acquires, tells the nested invocation to run through
|
|
21
|
+
# without re-acquiring, so the prepack never self-deadlocks against its own gate.
|
|
22
|
+
#
|
|
23
|
+
# mkdir is the atomic primitive (POSIX, no `flock` dependency — portable across the
|
|
24
|
+
# npm/pnpm lifecycle on the Linux publish host). Mirrors the entwurf-v2 dispatch
|
|
25
|
+
# lock discipline: atomic acquire, holder pid recorded for human cleanup, same-host
|
|
26
|
+
# stale reclaim (a SIGKILL'd holder never wedges every future build), and a
|
|
27
|
+
# bounded wait that fails loud rather than hanging or building into a half-wiped dir.
|
|
28
|
+
set -euo pipefail
|
|
29
|
+
|
|
30
|
+
if [ "$#" -eq 0 ]; then
|
|
31
|
+
echo "usage: with-dist-lock.sh <command> [args...]" >&2
|
|
32
|
+
exit 2
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# An ancestor already holds the dist lock (e.g. this is `npm pack`'s prepack
|
|
36
|
+
# build-bridge under a gate that wraps the whole pack) — run reentrantly.
|
|
37
|
+
if [ -n "${ENTWURF_BUILD_LOCK_HELD:-}" ]; then
|
|
38
|
+
exec "$@"
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
REPO_DIR=$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
|
42
|
+
LOCK_DIR="$REPO_DIR/.tmp-verify/dist.lock"
|
|
43
|
+
mkdir -p "$REPO_DIR/.tmp-verify"
|
|
44
|
+
|
|
45
|
+
try_acquire() {
|
|
46
|
+
if mkdir "$LOCK_DIR" 2>/dev/null; then
|
|
47
|
+
return 0
|
|
48
|
+
fi
|
|
49
|
+
# Same-host stale reclaim: a holder killed with SIGKILL leaves the dir behind.
|
|
50
|
+
# If its recorded pid is gone, the lock is dead — reclaim it so a crashed build
|
|
51
|
+
# never wedges every future one. (Builds/packs always run on the local host, so
|
|
52
|
+
# a same-host pid probe is authoritative.)
|
|
53
|
+
local holder
|
|
54
|
+
holder=$(cat "$LOCK_DIR/pid" 2>/dev/null || echo "")
|
|
55
|
+
if [ -n "$holder" ] && ! kill -0 "$holder" 2>/dev/null; then
|
|
56
|
+
rm -rf "$LOCK_DIR"
|
|
57
|
+
mkdir "$LOCK_DIR" 2>/dev/null && return 0
|
|
58
|
+
fi
|
|
59
|
+
return 1
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Bounded wait (~120s): a full pack (prepack tsc emit + npm tree walk) is a few
|
|
63
|
+
# seconds, so this clears several serialized contenders with headroom, then fails
|
|
64
|
+
# loud rather than hanging forever.
|
|
65
|
+
acquired=0
|
|
66
|
+
for _ in $(seq 1 1200); do
|
|
67
|
+
if try_acquire; then acquired=1; break; fi
|
|
68
|
+
sleep 0.1
|
|
69
|
+
done
|
|
70
|
+
if [ "$acquired" != 1 ]; then
|
|
71
|
+
echo "[with-dist-lock] could not acquire dist lock after ~120s: $LOCK_DIR" >&2
|
|
72
|
+
echo "[with-dist-lock] if no other build/pack is running, remove it: rm -rf \"$LOCK_DIR\"" >&2
|
|
73
|
+
exit 1
|
|
74
|
+
fi
|
|
75
|
+
echo "$$" > "$LOCK_DIR/pid" 2>/dev/null || true
|
|
76
|
+
# Release on ANY exit (incl. the wrapped command failing) so a red run never wedges
|
|
77
|
+
# the next. The nested prepack build-bridge sees this and skips re-acquiring.
|
|
78
|
+
export ENTWURF_BUILD_LOCK_HELD=$$
|
|
79
|
+
trap 'rm -rf "$LOCK_DIR"' EXIT
|
|
80
|
+
|
|
81
|
+
"$@"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|