diff --git a/plugins/magpie-setup/skills/setup/setup_preflight/isolated_fingerprint.py b/plugins/magpie-setup/skills/setup/setup_preflight/isolated_fingerprint.py index 865128204..f00c82004 100644 --- a/plugins/magpie-setup/skills/setup/setup_preflight/isolated_fingerprint.py +++ b/plugins/magpie-setup/skills/setup/setup_preflight/isolated_fingerprint.py @@ -21,4 +21,4 @@ for installs that do not carry the framework source (see `isolated.py`). """ -FRAMEWORK_FINGERPRINT = "sha256:198dc3e80e1af151" +FRAMEWORK_FINGERPRINT = "sha256:802fecf15e0df3ce" diff --git a/tools/agent-isolation/container-gateway-hook.sh b/tools/agent-isolation/container-gateway-hook.sh index 32a289553..829698da8 100755 --- a/tools/agent-isolation/container-gateway-hook.sh +++ b/tools/agent-isolation/container-gateway-hook.sh @@ -120,6 +120,10 @@ done log() { printf '%s: %s\n' "${0##*/}" "$*" >&2; } +# Every git call: never discover a bare repository, which sandboxed code could +# plant as HEAD / config / objects in a project that has no .git of its own. +git() { command git -c safe.bareRepository=explicit "$@"; } + payload="$(cat 2>/dev/null || true)" cwd="$(printf '%s' "$payload" | jq -r '.cwd // empty' 2>/dev/null || true)" [[ -n $cwd ]] || cwd="$PWD" @@ -132,6 +136,36 @@ if [[ $cwd != "$root" && $cwd != "$root"/* ]]; then log "git reports $root as the toplevel of $cwd, which it does not contain; not starting the container gateway" exit 0 fi +# The root as the filesystem shows it, read without any repository config: the +# nearest directory above the cwd holding a .git. git's toplevel must be that +# directory, and core.worktree must be unset: a .git directory planted in a +# sandbox-writable checkout could otherwise name a parent as the toplevel, which +# still contains the cwd but keys the worktree records differently, so an +# existing record would never be consulted. +fs_root="" +probe_dir="$cwd" +while :; do + if [[ -e $probe_dir/.git || -L $probe_dir/.git ]]; then + fs_root="$probe_dir" + break + fi + [[ $probe_dir == / ]] && break + probe_dir="$(dirname -- "$probe_dir")" +done +git_toplevel="$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null || true)" +if [[ -z $fs_root && -n $git_toplevel ]]; then + log "git finds a repository for $cwd but there is no .git on the way up; not starting the container gateway" + exit 0 +fi +if [[ -n $fs_root && $fs_root != "$root" ]]; then + log "git reports $root as the toplevel of $cwd, but the checkout is $fs_root; not starting the container gateway" + exit 0 +fi +# Checked whether or not a .git was found: core.worktree is what moves a toplevel. +if [[ -n "$(git -C "$cwd" config --get core.worktree 2>/dev/null)" ]]; then + log "core.worktree is set for $cwd; not starting the container gateway" + exit 0 +fi # The home directory from the account database (`~user` reads it, not $HOME), # so a $HOME redirected by the environment cannot hide the real ~/.cache, diff --git a/tools/agent-isolation/tests/test_container_gateway_hook.py b/tools/agent-isolation/tests/test_container_gateway_hook.py index a219048e8..99fd232c0 100644 --- a/tools/agent-isolation/tests/test_container_gateway_hook.py +++ b/tools/agent-isolation/tests/test_container_gateway_hook.py @@ -782,3 +782,42 @@ def test_a_writable_record_directory_refuses_even_a_plain_checkout(tmp_path: Pat lay.settings({"sandbox": {"filesystem": {"allowWrite": [str(lay.home / ".claude" / "magpie-gateway")]}}}) done = run("start", lay.project, home=lay.home) assert done.stdout == "" and "is sandbox-writable" in done.stderr + + + +def test_a_planted_core_worktree_cannot_move_the_record_key(tmp_path: Path) -> None: + """A fake .git naming a parent as the toplevel still contains the cwd, but would + key the worktree records differently and skip the existing one.""" + lay = Layout(tmp_path) + worktree = _linked_worktree(lay) + run("start", worktree, home=lay.home) + # A plain .git directory (so the checkout no longer looks linked) whose config + # names the parent as the work tree: git's toplevel then contains the cwd. + (worktree / ".git").unlink() + subprocess.run(["git", "init", "-q", str(worktree)], check=True) + subprocess.run(["git", "-C", str(worktree), "config", "core.worktree", str(lay.base)], check=True) + done = run("stop", worktree, home=lay.home) + assert done.stdout == "" + assert "but the checkout is" in done.stderr + + +def test_core_worktree_on_a_plain_checkout_refuses(tmp_path: Path) -> None: + lay = Layout(tmp_path) + subprocess.run(["git", "-C", str(lay.project), "config", "core.worktree", str(lay.project)], check=True) + done = run("start", lay.project, home=lay.home) + assert done.stdout == "" and "core.worktree is set" in done.stderr + + +def test_a_planted_bare_repository_is_not_discovered(tmp_path: Path) -> None: + """In a project with no .git, HEAD / config / objects planted in the cwd would make + git treat it as a bare repository whose core.worktree names a parent.""" + lay = Layout(tmp_path) + plain = lay.base / "plain" + plain.mkdir() + subprocess.run(["git", "init", "-q", "--bare", str(plain)], check=True) + subprocess.run(["git", "--git-dir", str(plain), "config", "core.worktree", str(lay.base)], check=True) + subprocess.run(["git", "--git-dir", str(plain), "config", "core.bare", "false"], check=True) + done = run("start", plain, home=lay.home) + # Either refused, or served for the directory itself: never for the parent. + assert done.stdout == "" or f"--project={plain.resolve()} " in done.stdout + assert f"--project={lay.base} " not in done.stdout