Skip to content

Commit 64f7b22

Browse files
tooling: mechanical soup dist guards (tiann#921/tiann#962 class) (#62)
* tooling: mechanical soup dist guards (tiann#921/tiann#962 class) Wire production-mutation guard into Cursor hooks, shared pattern lib, and smoke tests. verify-soup-web-dist now derives integrity markers from driver/web/src (routes, lazy chunks, feature namespaces, scratchlist, mermaid data attrs, overseer debug UI) instead of a hardcoded list. Co-authored-by: Cursor <cursoragent@cursor.com> * tooling: install script copies soup-dogfood rule to ~/.cursor/rules Hooks alone only wire shell deny; alwaysApply mdc was a manual copy target. Align doc with Windows muzzle pattern (canonical in scripts/tooling, deployed by installer). Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent aa741d3 commit 64f7b22

7 files changed

Lines changed: 451 additions & 57 deletions

docs/tooling/driver-soup.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,28 @@ hapi-driver-rebuild --build-web [--verify]
4343
# 3. Dogfood on :3006 — follow feature-work-lifecycle.md § Soup dogfood decision tree
4444
```
4545

46-
**Forbidden on `driver/`:** hand-edits, `cp` from other checkouts, local commits, raw `bun run build` in `web/` for production dogfood.
46+
**Forbidden on `driver/`:** hand-edits, `cp` from other checkouts, local commits, raw `bun run build` in `web/` for production dogfood, **feat/worktree `web/dist` → `driver/web/dist` copy** (#921 rollback class).
47+
48+
### Mechanical guards (2026-06-24 — #921 + #962 incidents)
49+
50+
Cursor **`hapi-production-mutation-guard.sh`** (install via `scripts/tooling/hapi-install-cursor-hooks.sh`) **denies** agent shell:
51+
52+
- `cp`/`rsync`/`mv` from `worktrees/*/web/dist` into `driver/web/dist`
53+
- raw `bun run build` / `vite build` under `driver/web`
54+
- `git merge|cherry-pick|reset` on `~/coding/hapi/driver`
55+
- `hapi-driver-rebuild` without `--build-web` (manifest merge — meta/operator only)
56+
57+
**Allowed:** `hapi-driver-build-web`, `hapi-driver-rebuild --build-web [--verify]`, `hapi-verify-web-dist`, `hapi-restart-hub`.
58+
59+
**`verify-soup-web-dist`** (runs inside `hapi-driver-build-web` by default) also fails closed on:
60+
61+
- missing soup marker strings **derived from `driver/web/src`** (feature routes, lazy chunks, assistant-ui modules, scratchlist lib, overseer debug UI) in dist assets / chunk filenames
62+
- main bundle or precache regression vs `web/dist.prev`
63+
- dist meta not from `build_web_atomic` / HEAD mismatch
64+
65+
If rebuild or verify fails: **STOP** — do not workaround. Report to operator.
66+
67+
Rule file for agents: canonical source `scripts/tooling/cursor-rules/hapi-driver-soup-dogfood.mdc`. **`hapi-install-cursor-hooks.sh` copies it to `~/.cursor/rules/`** (soft alwaysApply guidance). Shell hooks wire the mutation guard only — two layers, not redundant.
4768

4869
**Operator scripts** (`scripts/tooling/hapi-pr-session-emoji.sh`, `hapi-pr-emoji-batch.sh`, etc.) belong on **fork `main` in `~/coding/hapi`**, not in the driver tree. Rebuild reads tooling from the primary mirror (`HAPI_PRIMARY`, default `~/coding/hapi`). Uncommitted scripts vanish on sync/reset — **commit them to fork main**.
4970

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
description: Fail-closed dogfood on driver soup — blocks feat-dist swap and driver hand-merge
3+
alwaysApply: true
4+
---
5+
6+
# Driver soup dogfood (fail-closed)
7+
8+
When dogfooding on `:3006` (driver/integration + manifest layers):
9+
10+
**NEVER**
11+
- `cp`/`rsync`/`mv` any `worktrees/*/web/dist` into `driver/web/dist` (#921 — rolls back 100+ soup layers to a feat-only bundle)
12+
- `cd driver/web && bun run build` or raw `vite build` (bypasses atomic swap, preflight, verify rollback)
13+
- `git merge` / `git cherry-pick` / `git reset` inside `~/coding/hapi/driver` (#962 — hand-merge half-soups the tree)
14+
- `hapi-driver-rebuild` without `--build-web` (manifest merge — tooling/meta session or operator only)
15+
16+
**ONLY**
17+
- `hapi-driver-build-web` (builds from `driver/web` source on current driver HEAD)
18+
- `hapi-driver-rebuild --build-web [--verify]` when driver already contains your layer
19+
- `hapi-verify-web-dist` to audit (no build)
20+
21+
If rebuild fails: **STOP**. Report blocked to operator. Do not workaround.
22+
23+
See `docs/tooling/driver-soup.md` and `docs/tooling/feature-work-lifecycle.md`.

scripts/tooling/hapi-install-cursor-hooks.sh

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,25 @@ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
1616
HOOKS_JSON="${REPO_ROOT}/.cursor/hooks.json"
1717
PRODUCT_GUARD="${REPO_ROOT}/scripts/tooling/hapi-product-code-guard.sh"
1818
SYSTEMCTL_GUARD="${REPO_ROOT}/scripts/tooling/hapi-systemctl-guard.sh"
19+
MUTATION_GUARD="${REPO_ROOT}/scripts/tooling/hapi-production-mutation-guard.sh"
20+
SOUP_DOGFOOD_RULE="${REPO_ROOT}/scripts/tooling/cursor-rules/hapi-driver-soup-dogfood.mdc"
21+
USER_RULES="${HOME}/.cursor/rules"
1922

20-
for s in "$PRODUCT_GUARD" "$SYSTEMCTL_GUARD"; do
23+
for s in "$PRODUCT_GUARD" "$SYSTEMCTL_GUARD" "$MUTATION_GUARD"; do
2124
if [ ! -x "$s" ]; then
2225
echo "ERROR: ${s} missing or not executable" >&2
2326
exit 1
2427
fi
2528
done
2629

30+
if [ ! -f "$SOUP_DOGFOOD_RULE" ]; then
31+
echo "ERROR: ${SOUP_DOGFOOD_RULE} missing" >&2
32+
exit 1
33+
fi
34+
2735
mkdir -p "${REPO_ROOT}/.cursor"
36+
mkdir -p "$USER_RULES"
37+
cp "$SOUP_DOGFOOD_RULE" "$USER_RULES/hapi-driver-soup-dogfood.mdc"
2838

2939
cat > "$HOOKS_JSON" <<'JSON'
3040
{
@@ -38,6 +48,10 @@ cat > "$HOOKS_JSON" <<'JSON'
3848
{
3949
"command": "./scripts/tooling/hapi-systemctl-guard.sh",
4050
"matcher": "Shell"
51+
},
52+
{
53+
"command": "./scripts/tooling/hapi-production-mutation-guard.sh",
54+
"matcher": "Shell"
4155
}
4256
]
4357
}
@@ -46,11 +60,14 @@ JSON
4660

4761
echo "Wrote ${HOOKS_JSON}"
4862
echo "Hooks installed:"
49-
echo " hapi-product-code-guard.sh -> blocks edits to cli/, hub/, web/, shared/ outside ~/coding/hapi/worktrees/"
50-
echo " hapi-systemctl-guard.sh -> blocks 'sudo systemctl <destructive-verb> hapi-{hub,runner,runner-watchdog}.service'"
63+
echo " hapi-product-code-guard.sh -> blocks edits to cli/, hub/, web/, shared/ outside ~/coding/hapi/worktrees/"
64+
echo " hapi-systemctl-guard.sh -> blocks 'sudo systemctl <destructive-verb> hapi-{hub,runner,runner-watchdog}.service'"
65+
echo " hapi-production-mutation-guard.sh -> blocks feat-dist swap, driver hand-merge, raw driver/web builds, full rebuild"
66+
echo " hapi-driver-soup-dogfood.mdc -> copied to ${USER_RULES}/ (alwaysApply policy; hooks alone do not load this)"
5167
echo
5268
echo "Bypasses when needed (operator-approved):"
5369
echo " HAPI_OPERATOR_PRODUCT_EDIT_OVERRIDE=1 (product-code edits)"
5470
echo " HAPI_OPERATOR_SYSTEMCTL_OVERRIDE=1 (systemctl on hapi units)"
71+
echo " HAPI_OPERATOR_PRODUCTION_MUTATION_OVERRIDE=1 (dist swap / driver hand-merge — TTY only)"
5572
echo
5673
echo "Restart Cursor (or reload) to pick up the hooks."

scripts/tooling/hapi-production-mutation-guard.sh

Lines changed: 16 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -40,46 +40,8 @@ fi
4040

4141
_lc_cmd=$(printf '%s' "$CMD" | tr '[:upper:]' '[:lower:]')
4242

43-
_hapi_production_mutation_match() {
44-
local c="$1"
45-
local lc
46-
lc=$(printf '%s' "$c" | tr '[:upper:]' '[:lower:]')
47-
48-
# Stack switch / promotion tooling
49-
if printf '%s' "$lc" | grep -qE 'hapi-driver-db-prep|hapi-use-worktree|hapi-use-driver|hapi-driver-rebuild.*--activate|hapi-watch-activate-driver|hapi_stack_switch_yes=1'; then
50-
return 0
51-
fi
52-
53-
# Manual hub / port hijack
54-
if printf '%s' "$lc" | grep -qE 'nohup.*(bun run|src/index\.ts)|manual-hub|>>.*manual-hub'; then
55-
return 0
56-
fi
57-
58-
# Kill production listener / hub processes
59-
if printf '%s' "$lc" | grep -qE '(^|[[:space:];|&])(kill|pkill|fuser)[[:space:]].*(3006|hapi-hub|/hub/|src/index\.ts)'; then
60-
return 0
61-
fi
62-
if printf '%s' "$lc" | grep -qE 'kill[[:space:]]+[0-9]+' && printf '%s' "$lc" | grep -qE '3006|hapi-hub|manual-hub|cross-flavor'; then
63-
return 0
64-
fi
65-
66-
# systemd destruction (catch non-sudo paths too — rare but cheap)
67-
if printf '%s' "$lc" | grep -qE 'systemctl[[:space:]]+(stop|restart|kill|disable|mask)[[:space:]]+hapi-(hub|runner|runner-watchdog)'; then
68-
return 0
69-
fi
70-
71-
# Driver tree destruction / sneak promote
72-
if printf '%s' "$lc" | grep -qE 'git reset --hard.*(driver|hapi/driver)|embeddedassets.*driver|cp -r.*embeddedassets'; then
73-
return 0
74-
fi
75-
76-
# Shared DB surgery
77-
if printf '%s' "$lc" | grep -qE '(\.hapi/hapi\.db|hapi\.db\.bak)|sqlite3.*hapi\.db.*(drop|delete|update|insert)'; then
78-
return 0
79-
fi
80-
81-
return 1
82-
}
43+
# shellcheck source=lib/driver-dist-guard-patterns.sh
44+
source "$(dirname "$(readlink -f "$0")")/lib/driver-dist-guard-patterns.sh"
8345

8446
_is_remote_ssh=0
8547
if printf '%s' "$_lc_cmd" | grep -qE '(^|[[:space:]|&;])(ssh|scp|rsync)[[:space:]]|wsl[[:space:]].*ssh'; then
@@ -98,23 +60,28 @@ fi
9860

9961
if [ "$_block" -eq 1 ]; then
10062
DENY_MSG=$(cat <<EOF
101-
Production HAPI mutation BLOCKED (2026-06-20 rogue-hub incident class).
63+
Production HAPI mutation BLOCKED (rogue-hub / feat-dist-swap / hand-merge class).
10264
10365
Command: $CMD
10466
Tool: ${TOOL:-Shell}
10567
106-
Agents must NOT kill, nohup, stack-switch, DB-prep, or reset driver to change what serves :3006.
107-
REFUSE from hapi-use-worktree / hapi-restart-hub means STOP — report to operator.
68+
Blocked classes include:
69+
- kill/nohup/stack-switch/DB-prep/manual hub on :3006
70+
- cp/rsync/mv feat or worktree web/dist into driver/web/dist (#921 rollback)
71+
- raw bun/vite build in driver/web (use hapi-driver-build-web)
72+
- git merge/cherry-pick/reset on driver/integration (#962 hand-merge)
73+
- hapi-driver-rebuild without --build-web (manifest merge — meta/operator only)
74+
75+
Allowed for feature peers on driver soup:
76+
- hapi-driver-build-web [--skip-verify] (builds from driver/web source)
77+
- hapi-driver-rebuild --build-web [--verify]
78+
- hapi-verify-web-dist / hapi-restart-hub (patient)
10879
109-
Windows estate agents: refresh Teemo runner only; use hapi-peer-stack for pre-soup browser proof (:3100+).
110-
Linux soup promotion: manifest + hapi-driver-rebuild — operator or steward only.
80+
REFUSE means STOP — report to operator. Do not workaround with cp or ad-hoc builds.
11181
11282
Bypass (operator TTY only): HAPI_OPERATOR_PRODUCTION_MUTATION_OVERRIDE=1
11383
114-
See:
115-
~/coding/hapi/.cursor/rules/operator-fork.mdc
116-
~/coding/hapi/scripts/tooling/cursor-rules/hapi-windows-estate.mdc
117-
~/coding/hapi/docs/operator/windows-estate-agents.md
84+
See: docs/tooling/driver-soup.md, docs/tooling/feature-work-lifecycle.md
11885
EOF
11986
)
12087
jq -n \
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#!/usr/bin/env bash
2+
# Smoke-test production mutation guard patterns (operator-local).
3+
set -euo pipefail
4+
5+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
6+
GUARD="$ROOT/scripts/tooling/hapi-production-mutation-guard.sh"
7+
8+
expect_deny() {
9+
local cmd="$1"
10+
local label="${2:-$1}"
11+
local out
12+
out=$(printf '%s' "{\"command\":$(jq -cn --arg c "$cmd" '$c')}" | "$GUARD")
13+
if printf '%s' "$out" | jq -e '.permission == "deny"' >/dev/null; then
14+
echo "OK deny: $label"
15+
else
16+
echo "FAIL expected deny: $label" >&2
17+
echo "$out" >&2
18+
exit 1
19+
fi
20+
}
21+
22+
expect_allow() {
23+
local cmd="$1"
24+
local label="${2:-$1}"
25+
local out
26+
out=$(printf '%s' "{\"command\":$(jq -cn --arg c "$cmd" '$c')}" | "$GUARD")
27+
if printf '%s' "$out" | jq -e '.permission == "allow"' >/dev/null; then
28+
echo "OK allow: $label"
29+
else
30+
echo "FAIL expected allow: $label" >&2
31+
echo "$out" >&2
32+
exit 1
33+
fi
34+
}
35+
36+
# #921 feat-dist swap
37+
expect_deny 'cp -a /home/heavygee/coding/hapi/worktrees/scratchlist-attachments-v22/web/dist /home/heavygee/coding/hapi/driver/web/dist' 'feat dist cp'
38+
expect_deny 'DRIVER_WEB=driver/web/dist FEAT_DIST=worktrees/foo/web/dist cp -a $FEAT_DIST $DRIVER_WEB' 'FEAT_DIST pattern'
39+
40+
# #962 hand merge + raw build
41+
expect_deny 'git -C /home/heavygee/coding/hapi/driver cherry-pick c08f327d' 'driver cherry-pick'
42+
expect_deny 'cd /home/heavygee/coding/hapi/driver/web && bun run build' 'raw driver/web build'
43+
44+
# Full rebuild without --build-web
45+
expect_deny 'hapi-driver-rebuild --verify' 'full manifest rebuild'
46+
expect_deny 'hapi-driver-rebuild' 'full rebuild bare'
47+
48+
# Allowed peer paths
49+
expect_allow 'hapi-driver-build-web' 'build-web tool'
50+
expect_allow 'hapi-driver-rebuild --build-web --verify' 'build-web rebuild'
51+
expect_allow 'hapi-verify-web-dist' 'verify only'
52+
expect_allow 'hapi-restart-hub' 'patient hub restart'
53+
54+
echo "hapi-production-mutation-guard.test.sh: all patterns OK"
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
#!/usr/bin/env bash
2+
# Shared shell-command patterns for driver/web/dist and driver/integration guards.
3+
# Sourced by hapi-production-mutation-guard.sh (and test harness).
4+
#
5+
# Return 0 = block command; 1 = do not block on these patterns alone.
6+
7+
_hapi_cmd_is_allowed_driver_dist_tool() {
8+
local lc="$1"
9+
if printf '%s' "$lc" | grep -qE 'hapi-driver-build-web|hapi-driver-rollback-web|hapi-verify-web-dist|verify-soup-web-dist|build_web_atomic|dist\.next|dist\.prev'; then
10+
return 0
11+
fi
12+
if printf '%s' "$lc" | grep -qE 'hapi-driver-rebuild' && printf '%s' "$lc" | grep -qF -- '--build-web'; then
13+
return 0
14+
fi
15+
return 1
16+
}
17+
18+
_hapi_driver_dist_injection_match() {
19+
local c="$1"
20+
local lc
21+
lc=$(printf '%s' "$c" | tr '[:upper:]' '[:lower:]')
22+
23+
# Canonical tools manage dist.next/dist.prev internally — never block those invocations.
24+
if _hapi_cmd_is_allowed_driver_dist_tool "$lc"; then
25+
return 1
26+
fi
27+
28+
# 2026-06-23 #921: feat/worktree dist copied into driver (rollback of full soup UI).
29+
if printf '%s' "$lc" | grep -qE 'feat_dist|worktrees/[^[:space:]"'\''`]+/web/dist'; then
30+
if printf '%s' "$lc" | grep -qE 'driver/web/dist|/hapi/driver/web/dist|coding/hapi/driver/web/dist'; then
31+
return 0
32+
fi
33+
fi
34+
35+
if printf '%s' "$lc" | grep -qE '(cp|rsync|mv)[[:space:]]' \
36+
&& printf '%s' "$lc" | grep -qE 'driver/web/dist|/hapi/driver/web/dist'; then
37+
return 0
38+
fi
39+
40+
# Raw vite/bun build in driver/web bypasses atomic swap + preflight + verify rollback.
41+
if printf '%s' "$lc" | grep -qE 'driver/web' \
42+
&& printf '%s' "$lc" | grep -qE '(bun run build|vite build|npm run build)'; then
43+
return 0
44+
fi
45+
46+
return 1
47+
}
48+
49+
_hapi_driver_integration_hand_edit_match() {
50+
local c="$1"
51+
local lc
52+
lc=$(printf '%s' "$c" | tr '[:upper:]' '[:lower:]')
53+
54+
# Full manifest rebuild (not build-web-only) — #921 partial merge disaster.
55+
if printf '%s' "$lc" | grep -qE '(^|[[:space:]|&;])hapi-driver-rebuild([[:space:]]|$)'; then
56+
if ! printf '%s' "$lc" | grep -qF -- '--build-web'; then
57+
return 0
58+
fi
59+
fi
60+
61+
if _hapi_cmd_is_allowed_driver_dist_tool "$lc"; then
62+
return 1
63+
fi
64+
65+
# 2026-06-24 #962: hand merge/cherry-pick/reset on driver/integration instead of manifest rebuild.
66+
if printf '%s' "$lc" | grep -qE 'git[[:space:]]+(-c[[:space:]]+[^[:space:]]+[[:space:]]+)?(-c[[:space:]]+[^[:space:]]+[[:space:]]+)?(merge|cherry-pick|rebase|reset|checkout[[:space:]]+[^-])' \
67+
&& printf '%s' "$lc" | grep -qE '(^|[[:space:]/"'\''])driver(/integration)?([[:space:]"'\''`/]|$)|coding/hapi/driver'; then
68+
return 0
69+
fi
70+
71+
return 1
72+
}
73+
74+
_hapi_production_mutation_match() {
75+
local c="$1"
76+
local lc
77+
lc=$(printf '%s' "$c" | tr '[:upper:]' '[:lower:]')
78+
79+
if _hapi_driver_dist_injection_match "$c"; then
80+
return 0
81+
fi
82+
if _hapi_driver_integration_hand_edit_match "$c"; then
83+
return 0
84+
fi
85+
86+
# Stack switch / promotion tooling
87+
if printf '%s' "$lc" | grep -qE 'hapi-driver-db-prep|hapi-use-worktree|hapi-use-driver|hapi-driver-rebuild.*--activate|hapi-watch-activate-driver|hapi_stack_switch_yes=1'; then
88+
return 0
89+
fi
90+
91+
# Manual hub / port hijack
92+
if printf '%s' "$lc" | grep -qE 'nohup.*(bun run|src/index\.ts)|manual-hub|>>.*manual-hub'; then
93+
return 0
94+
fi
95+
96+
# Kill production listener / hub processes
97+
if printf '%s' "$lc" | grep -qE '(^|[[:space:];|&])(kill|pkill|fuser)[[:space:]].*(3006|hapi-hub|/hub/|src/index\.ts)'; then
98+
return 0
99+
fi
100+
if printf '%s' "$lc" | grep -qE 'kill[[:space:]]+[0-9]+' && printf '%s' "$lc" | grep -qE '3006|hapi-hub|manual-hub|cross-flavor'; then
101+
return 0
102+
fi
103+
104+
# systemd destruction (catch non-sudo paths too — rare but cheap)
105+
if printf '%s' "$lc" | grep -qE 'systemctl[[:space:]]+(stop|restart|kill|disable|mask)[[:space:]]+hapi-(hub|runner|runner-watchdog)'; then
106+
return 0
107+
fi
108+
109+
# Driver tree destruction / sneak promote
110+
if printf '%s' "$lc" | grep -qE 'git reset --hard.*(driver|hapi/driver)|embeddedassets.*driver|cp -r.*embeddedassets'; then
111+
return 0
112+
fi
113+
114+
# Shared DB surgery
115+
if printf '%s' "$lc" | grep -qE '(\.hapi/hapi\.db|hapi\.db\.bak)|sqlite3.*hapi\.db.*(drop|delete|update|insert)'; then
116+
return 0
117+
fi
118+
119+
return 1
120+
}

0 commit comments

Comments
 (0)