Adopt CASAN like a normal tool (codegraph-style): install the harness ONCE
per machine, then `casan init` per project writes CONFIG ONLY — the harness
is no longer copied into every repo.
- install.sh / install.ps1: global bootstrap (curl|sh / irm|iex or local
source). Installs harness to $CASAN_HOME/versions/<ver>, writes a `casan`
launcher that resolves the shared harness + the current project's .specify,
and records a gate-code integrity hash. CASAN_NO_PATH_LINK for tests.
- harness_hash.py: deterministic content hash over gate code (scripts/bash,
scripts/python, security, level5) — the pin+verify anchor.
- casan-init.py: `casan init` writes .casan/{config,version.lock,agentic.env},
.specify/ marker, and the Plan-20 client hooks — no harness copy. `verify`
recomputes the harness hash LIVE and compares to the project pin (drift/
tamper -> rc 3), preserving the Plan-16 trusted-gates guarantee off-repo.
- bin/casan: new `init` and `verify-harness` commands.
- hybrid-install-tests.sh: 21/21 (install, config-only init, no-copy, pin,
verify ok, tamper drift, bridge runs against project state via global harness).
- docs: CASAN_INSTALL_HYBRID.md + Plan-21.
The path model (casan-paths.sh) already separated harness/state/domain roots,
so this is installer + init, not a core rewrite. Remote dist tarball, real
Windows run, and signed .harness-hash are the documented next steps.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
70 lines
4.3 KiB
Bash
Executable File
70 lines
4.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -uo pipefail
|
|
|
|
# CASAN Plan-21 — hybrid install + `casan init` acceptance tests.
|
|
#
|
|
# Proves the codegraph-style flow: global install once, then per-project
|
|
# `casan init` that writes CONFIG ONLY (no harness copy), with a pin+verify
|
|
# integrity guarantee on the shared harness. Deterministic, offline.
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
|
|
|
PASS=0; FAIL=0
|
|
pass() { echo "PASS: $1"; PASS=$((PASS + 1)); }
|
|
fail() { echo "FAIL: $1"; FAIL=$((FAIL + 1)); }
|
|
|
|
WORK="$(mktemp -d)"
|
|
export CASAN_HOME="$WORK/home/.casan"
|
|
export CASAN_SRC="$REPO_ROOT"
|
|
export CASAN_NO_PATH_LINK=1
|
|
cleanup() { rm -rf "$WORK"; }
|
|
trap cleanup EXIT
|
|
|
|
CASAN="$CASAN_HOME/bin/casan"
|
|
|
|
echo "===== ① global install ====="
|
|
if sh "$REPO_ROOT/install.sh" >/dev/null 2>&1; then pass "install.sh completes"; else fail "install.sh failed"; fi
|
|
[[ -x "$CASAN" ]] && pass "global launcher created" || fail "launcher missing"
|
|
[[ -f "$CASAN_HOME/current/.harness-hash" ]] && pass "integrity hash recorded at install" || fail "no .harness-hash"
|
|
"$CASAN" version >/dev/null 2>&1 && pass "casan version works via launcher" || fail "casan version failed"
|
|
|
|
echo "===== ② casan init (config only, no harness copy) ====="
|
|
PROJ="$WORK/proj/my-app"; mkdir -p "$PROJ"; echo '{"name":"x"}' > "$PROJ/package.json"
|
|
( cd "$PROJ" && "$CASAN" init --project my-app --mode enforce >/dev/null 2>&1 ) \
|
|
&& pass "casan init completes" || fail "casan init failed"
|
|
for f in .casan/config.json .casan/version.lock .casan/agentic.env .claude/settings.json .codex/hooks.json .specify/.gitignore; do
|
|
[[ -f "$PROJ/$f" ]] && pass "init wrote $f" || fail "init missing $f"
|
|
done
|
|
if [[ -d "$PROJ/packages/casan-harness" ]]; then fail "harness was copied into the repo (should not be)"; else pass "harness NOT copied into repo (hybrid model)"; fi
|
|
|
|
echo "===== ③ version.lock pins the installed harness ====="
|
|
LOCK_HASH="$(python3 -c 'import json;print(json.load(open("'"$PROJ"'/.casan/version.lock"))["harness_hash"])')"
|
|
REC_HASH="$(cat "$CASAN_HOME/current/.harness-hash")"
|
|
[[ -n "$LOCK_HASH" && "$LOCK_HASH" == "$REC_HASH" ]] && pass "version.lock pins the installed gate-code hash" || fail "lock hash mismatch ($LOCK_HASH vs $REC_HASH)"
|
|
|
|
echo "===== ④ verify-harness: ok before tamper, drift after ====="
|
|
( cd "$PROJ" && "$CASAN" verify-harness >/dev/null 2>&1 ) && pass "verify-harness OK on a clean install" || fail "verify-harness reported drift on clean install"
|
|
echo "# tampered $(date)" >> "$CASAN_HOME/current/packages/casan-harness/scripts/bash/security-check.sh"
|
|
VRC=0; ( cd "$PROJ" && "$CASAN" verify-harness >/dev/null 2>"$WORK/vh.err" ) || VRC=$?
|
|
[[ "$VRC" -eq 3 ]] && pass "verify-harness detects tamper (rc=3)" || fail "tamper not detected (rc=$VRC)"
|
|
grep -q "HARNESS_INTEGRITY_DRIFT" "$WORK/vh.err" && pass "drift message emitted" || fail "no drift message"
|
|
|
|
echo "===== ⑤ agentic bridge runs against the PROJECT state via GLOBAL harness ====="
|
|
BR="$CASAN_HOME/current/packages/casan-harness/scripts/python/agentic_bridge.py"
|
|
B=$(CASAN_APP_ROOT="$PROJ" CASAN_AGENTIC_ENFORCEMENT_MODE=enforce \
|
|
bash -c 'echo '\''{"op":"begin","client":"claude-code","project":"'"$PROJ"'","session":"s","prompt":"add fn","integration_mode":"project_hook"}'\'' | python3 "'"$BR"'" run')
|
|
echo "$B" | grep -q '"decision": "allow"' && pass "bridge admits a normal turn via the global harness" || fail "bridge begin failed ($B)"
|
|
if find "$PROJ/.specify" -name 'turn-*.json' | grep -q .; then pass "admission state lands in the PROJECT .specify"; else fail "no admission state in project"; fi
|
|
if [[ -d "$CASAN_HOME/current/.specify" ]]; then fail "runtime state leaked into the global install"; else pass "no runtime state in the global install"; fi
|
|
|
|
echo "===== ⑥ init defaults project id from dir name + is re-runnable ====="
|
|
PROJ2="$WORK/proj2/Some_App"; mkdir -p "$PROJ2"
|
|
( cd "$PROJ2" && "$CASAN" init >/dev/null 2>&1 ) && pass "init works with a defaulted project id" || fail "init default id failed"
|
|
PID=$(python3 -c 'import json;print(json.load(open("'"$PROJ2"'/.casan/config.json"))["project_id"])' 2>/dev/null)
|
|
[[ "$PID" =~ ^[a-z][a-z0-9-]{1,62}$ ]] && pass "defaulted project id is sanitized ($PID)" || fail "bad default project id ($PID)"
|
|
|
|
echo ""
|
|
echo "===== HYBRID INSTALL SUMMARY: PASS=$PASS FAIL=$FAIL ====="
|
|
[[ "$FAIL" -eq 0 ]] || exit 1
|