Files
yvgude--lean-ctx/rust/tests/tcc_sandbox.sh
T
wehub-resource-sync 26382a7ac6
CI / Clippy (push) Failing after 15m13s
CI / Test (ubuntu-latest) (push) Failing after 16m1s
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Build (no embeddings / no ORT) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Cookbook (Node) (push) Has been cancelled
CI / Pi Extension (Node) (push) Has been cancelled
CI / Rust SDK (lean-ctx-client) (push) Has been cancelled
CI / Embed SDK (lean-ctx-sdk) (push) Has been cancelled
CI / Python SDK (leanctx) (push) Has been cancelled
CI / Hermes Plugin (Python) (push) Has been cancelled
CI / SDK Conformance Matrix (push) Has been cancelled
CI / Coverage (push) Has been cancelled
CI / cargo-deny (push) Has been cancelled
CI / Adversarial Safety (push) Has been cancelled
CI / Benchmarks (push) Has been cancelled
CI / Output-Quality Gate (eval A/B) (push) Has been cancelled
CI / Documentation (push) Has been cancelled
CI / CI Green (push) Has been cancelled
JetBrains Plugin / Actionlint (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (rust) (push) Has been cancelled
JetBrains Plugin / Validation (push) Has been cancelled
JetBrains Plugin / Build (push) Has been cancelled
JetBrains Plugin / Test (push) Has been cancelled
Security Check / Security Scan (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:30 +08:00

180 lines
6.6 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# tcc_sandbox.sh — #356 regression guard (macOS only).
#
# Boots the lean-ctx foreground daemon as a *TCC-standalone* process (the
# end-user LaunchAgent condition, forced via LEAN_CTX_TCC_STANDALONE=1) in three
# runs:
# 1. control — no sandbox, proves the throwaway HOME is sane.
# 2. detection — deny ~/Documents + ~/Desktop + ~/Downloads with SIGKILL, so
# any stray access kills the daemon and fails the test.
# 3. production — the exact `sandbox-exec -p` wrapper lean-ctx bakes into the
# LaunchAgent (silent EPERM deny), proving it boots + survives.
# The daemon is told about a project living under ~/Documents (via
# LEAN_CTX_PROJECT_ROOT and a stored .lean-ctx.toml there) — exactly the setup
# that made #356 recur. If any boot path stats / reads / canonicalizes those
# dirs, the kernel kills the daemon and this test fails.
#
# This codifies the empirical method used to root-cause #356. It is the only
# check that reproduces the real end-user condition: running `lean-ctx update`
# (or tests) from a terminal masks the bug, because the terminal already holds
# the Documents TCC grant.
#
# A control run (same conditions, no sandbox) first proves the daemon boots in
# this throwaway HOME, so a sandbox-run death is unambiguously a ~/Documents
# access rather than an unrelated environment problem.
#
# Gated: only runs when LEAN_CTX_TCC_SANDBOX_TEST=1. Needs macOS + sandbox-exec.
# Binary: LEAN_CTX_BIN, else `lean-ctx` on PATH.
# Tunables: LEAN_CTX_TCC_SOAK_SECS (default 10).
set -uo pipefail
if [[ "${LEAN_CTX_TCC_SANDBOX_TEST:-0}" != "1" ]]; then
echo "SKIP: set LEAN_CTX_TCC_SANDBOX_TEST=1 to run this regression (macOS only)"
exit 0
fi
if [[ "$(uname)" != "Darwin" ]]; then
echo "SKIP: macOS only (TCC is a macOS feature)"
exit 0
fi
if ! command -v sandbox-exec >/dev/null 2>&1; then
echo "SKIP: sandbox-exec not available"
exit 0
fi
BIN="${LEAN_CTX_BIN:-}"
[[ -z "$BIN" ]] && BIN="$(command -v lean-ctx 2>/dev/null || true)"
if [[ -z "$BIN" || ! -x "$BIN" ]]; then
echo "FAIL: no lean-ctx binary — set LEAN_CTX_BIN or put lean-ctx on PATH"
exit 1
fi
SOAK="${LEAN_CTX_TCC_SOAK_SECS:-10}"
# Keep this base path SHORT: the daemon binds a Unix domain socket under
# "$HOME/Library/Application Support/lean-ctx/daemon.sock", and sun_path caps at
# ~104 bytes on macOS. TMPDIR (/var/folders/...) is already long enough to blow
# that budget, so anchor under /tmp.
ROOT_TMP="$(mktemp -d /tmp/lc.XXXXXX)"
# Resolve symlinks (/tmp -> /private/tmp, /var -> /private/var): the kernel
# matches sandbox subpath filters against the *canonical* path, and so must the
# daemon's HOME, or the deny rule silently misses.
ROOT_TMP="$(cd "$ROOT_TMP" && pwd -P)"
PIDS=()
cleanup() {
for p in "${PIDS[@]:-}"; do
[[ -n "$p" ]] && kill -9 "$p" 2>/dev/null || true
done
rm -rf "$ROOT_TMP" 2>/dev/null || true
}
trap cleanup EXIT
# Seed a throwaway HOME with a project under ~/Documents (markers + local config).
seed_home() {
local home="$1"
local proj="$home/Documents/proj"
mkdir -p "$proj/.git"
printf 'fn main() {}\n' >"$proj/main.rs"
printf '[context]\nmax_tokens = 1234\n' >"$proj/.lean-ctx.toml"
}
# Boot the foreground daemon and report whether it survives the soak.
# Args: <home> <soak> <mode: off|kill|prod>. Returns 0 if alive, 1 if it died.
boot_and_soak() {
local home="$1" soak="$2" mode="$3"
local proj="$home/Documents/proj"
local log="$home/daemon.log"
# Production-equivalent deny set: read+write under all three TCC-protected
# home dirs — mirrors src/core/tcc_guard_sandbox.rs. `file-read*` covers
# stat / open-read / read_dir / realpath; `file-write*` covers writes — both
# trip the TCC prompt (#356). `(allow default)` then a later `(deny ...)`:
# last match wins in SBPL.
local denies='(subpath "'"$home"'/Documents") (subpath "'"$home"'/Desktop") (subpath "'"$home"'/Downloads")'
local -a cmd=()
case "$mode" in
kill)
# SIGKILL on any access makes a stray ~/Documents touch *detectable*.
local profile="$home/deny-tcc.sb"
cat >"$profile" <<SB
(version 1)
(allow default)
(deny file-read* file-write* $denies (with send-signal SIGKILL))
SB
cmd+=(sandbox-exec -f "$profile")
;;
prod)
# The exact form lean-ctx bakes into the LaunchAgent ProgramArguments:
# a silent deny (EPERM, no SIGKILL) passed inline via `-p`.
local prof="(version 1) (allow default) (deny file-read* file-write* $denies)"
cmd+=(sandbox-exec -p "$prof")
;;
off) ;;
esac
cmd+=("$BIN" serve --_foreground-daemon)
# cwd=/ mimics a real LaunchAgent; env -i isolates from the caller's XDG/LEAN_CTX.
(
cd / || exit 1
exec env -i \
HOME="$home" \
PATH="/usr/bin:/bin:/usr/sbin:/sbin" \
TMPDIR="${TMPDIR:-/tmp}" \
LEAN_CTX_TCC_STANDALONE=1 \
LEAN_CTX_PROJECT_ROOT="$proj" \
"${cmd[@]}"
) >"$log" 2>&1 &
local pid=$!
PIDS+=("$pid")
local s
for ((s = 0; s < soak; s++)); do
if ! kill -0 "$pid" 2>/dev/null; then
return 1
fi
sleep 1
done
kill -0 "$pid" 2>/dev/null && return 0 || return 1
}
# --- Control run: prove the daemon boots in this throwaway HOME (no sandbox). ---
CTRL_HOME="$ROOT_TMP/ctrl"
seed_home "$CTRL_HOME"
if ! boot_and_soak "$CTRL_HOME" 4 off; then
echo "FAIL(inconclusive): daemon did not stay up even WITHOUT the sandbox."
echo " The environment, not #356, is the problem. Daemon log:"
echo "----- control daemon log -----"
cat "$CTRL_HOME/daemon.log" 2>/dev/null || true
exit 1
fi
echo "ok: control daemon booted and survived (env is sane)"
# --- Detection run: SIGKILL on any access under the three TCC-protected dirs. -
SB_HOME="$ROOT_TMP/sandbox"
seed_home "$SB_HOME"
if ! boot_and_soak "$SB_HOME" "$SOAK" kill; then
echo "FAIL: daemon was killed under the deny-TCC sandbox."
echo " A TCC-standalone boot path accessed ~/Documents — #356 has regressed."
echo "----- sandbox daemon log -----"
cat "$SB_HOME/daemon.log" 2>/dev/null || true
exit 1
fi
echo "ok: no boot path stats/reads/canonicalizes ~/Documents/~Desktop/~Downloads"
# --- Production run: the exact LaunchAgent wrapper (silent deny via `-p`). -----
# Proves the real production invocation form boots and survives (EPERM, no kill),
# i.e. the seatbelt wrapper does not break normal daemon operation.
PROD_HOME="$ROOT_TMP/prod"
seed_home "$PROD_HOME"
if ! boot_and_soak "$PROD_HOME" 4 prod; then
echo "FAIL: daemon did not survive under the production 'sandbox-exec -p' wrapper."
echo "----- production daemon log -----"
cat "$PROD_HOME/daemon.log" 2>/dev/null || true
exit 1
fi
echo "PASS: production seatbelt wrapper boots clean; no ~/Documents access (#356 fixed)"
exit 0