Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
e4953e2
feat(bin): add fm-live-lab.sh, a one-command live supervision lab bui…
kunchenguid Sep 28, 2026
530ff2e
fix(bin): exact lab windows, per-lab task ids, self-safe teardown
kunchenguid Sep 28, 2026
4f23477
fix(bin): target lab windows by id, stop lab descendants, add readine…
kunchenguid Sep 28, 2026
1b688ff
fix(bin): keep Claude's auto-updater off in live labs; list fm-live-l…
kunchenguid Sep 28, 2026
17c4c9b
fix(bin): start the lab tmux server without user config
kunchenguid Sep 28, 2026
0a9bf8a
no-mistakes(review): Scope lab teardown to its store, root, and task ids
kunchenguid Sep 28, 2026
4578588
no-mistakes(review): Record selected user stores at up for check and …
kunchenguid Sep 28, 2026
caa0e0a
no-mistakes(document): Clarify live lab documentation and remove stal…
kunchenguid Sep 28, 2026
b3367f0
no-mistakes(ci): Fixed the CI failure by checking for an existing lab…
kunchenguid Sep 28, 2026
e2dc0e5
no-mistakes(ci): Fixed all four Greptile findings: teardown signals o…
kunchenguid Sep 28, 2026
ac6637d
no-mistakes(ci): Fixed the CI test’s dependence on an installed Claud…
kunchenguid Sep 28, 2026
8902321
no-mistakes(ci): Fixed all three selected findings in bin/fm-live-lab…
kunchenguid Sep 28, 2026
eeb406a
no-mistakes(ci): Fixed the pre-primary settle wait, worker gate instr…
kunchenguid Sep 28, 2026
1381c1c
no-mistakes(ci): Fixed teardown to track pre-kill lab processes by PI…
kunchenguid Sep 28, 2026
637779a
no-mistakes(ci): Fixed teardown tracking for children spawned during …
kunchenguid Sep 29, 2026
c7a6594
no-mistakes(ci): Fixed ci-2 and ci-4 in bin/fm-live-lab.sh and tests/…
kunchenguid Sep 29, 2026
f51ad54
no-mistakes(ci): Fixed teardown so an observed-empty process group is…
kunchenguid Sep 29, 2026
25ef89f
no-mistakes(ci): Fixed ci-1 in bin/fm-live-lab.sh and tests/fm-live-l…
kunchenguid Sep 29, 2026
95d7b2b
no-mistakes(ci): Fixed down’s teardown wait to require two empty iden…
kunchenguid Sep 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 46 additions & 5 deletions bin/fm-claude-trust.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@
#
# Usage: fm-claude-trust.sh <worktree> <project>
# fm-claude-trust.sh --secondmate-home <home> <id>
# fm-claude-trust.sh --lab-home <home>
# <worktree> the isolated task worktree this spawn launches into
# <project> the primary checkout that worktree belongs to
# <home> the seeded secondmate home this spawn launches into
# <id> the secondmate id that home must already be marked for
# --lab-home the disposable lab home bin/fm-live-lab.sh launches a lab
# primary in
# Prints one line naming what it registered; refuses loudly on anything else.
#
# WHY THIS EXISTS. Claude Code gates a folder it has never seen behind an
Expand Down Expand Up @@ -144,15 +147,25 @@
# argument to gate external-imports consent against, so the two import flags
# are never written there.
#
# LAB-HOME MODE. A disposable lab primary (bin/fm-live-lab.sh) launches in a lab
# home that is neither a task worktree nor a seeded secondmate home.
# The evidence is structural: the home must carry bin/fm-lab-home.sh's marker
# (a regular file this user owns, never a symlink, holding the token
# bin/fm-gate-refuse-lib.sh owns), hold
# AGENTS.md and bin/, and be a primary git checkout whose top level is exactly
# the argument, because Claude Code keys the launch to that root. It is
# trust-only for the same reason as a secondmate home, and bin/fm-live-lab.sh
# removes the entry again when it tears the lab down.
#
# Only the launching user's own store is written. In worktree mode: the
# projects entries for the worktree path and the resolved canonical project
# path in ${CLAUDE_CONFIG_DIR:-$HOME}/.claude.json, which must be a regular
# file this uid owns; every unrelated key and project entry is preserved, and
# both entries land in one atomic replacement. In secondmate-home mode: the
# single projects entry for the registered home path, same store, same atomic
# replacement. fm-spawn.sh forwards CLAUDE_CONFIG_DIR onto the claude launch
# verbatim rather than resolving it, and the pane starts in the registered
# directory, so only an absolute value names the same store on both sides; a
# both entries land in one atomic replacement. In secondmate-home and lab-home
# mode: the single projects entry for the registered home path, same store,
# same atomic replacement. fm-spawn.sh forwards CLAUDE_CONFIG_DIR onto the
# claude launch verbatim rather than resolving it, and the pane starts in the
# registered directory, so only an absolute value names the same store on both sides; a
# relative one is refused below rather than guessed at.
set -u
# Path resolution here must answer from the filesystem, never from the caller's
Expand All @@ -174,6 +187,7 @@ unset CDPATH \
usage() {
echo "usage: fm-claude-trust.sh <worktree> <project>" >&2
echo " fm-claude-trust.sh --secondmate-home <home> <id>" >&2
echo " fm-claude-trust.sh --lab-home <home>" >&2
exit 2
}

Expand All @@ -189,6 +203,14 @@ case "${1:-}" in
PROJ_ARG=
SCOPE_NOUN="secondmate home"
;;
--lab-home)
[ "$#" -eq 2 ] || usage
MODE=lab-home
TARGET_ARG=$2
SUB_ID=
PROJ_ARG=
SCOPE_NOUN="lab home"
;;
'' | -h | --help)
usage
;;
Expand All @@ -204,6 +226,9 @@ esac

refuse() { echo "error: refusing to pre-register Claude trust: $1" >&2; exit 1; }

# shellcheck source=bin/fm-gate-refuse-lib.sh
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)/fm-gate-refuse-lib.sh"

real_dir() { (cd -P -- "$1" 2>/dev/null && pwd -P); }

# The fully resolved path of an existing file, or empty. Resolution runs in node
Expand Down Expand Up @@ -303,6 +328,22 @@ if [ "$MODE" = worktree ]; then
[ -n "$CANON_GIT_DIR" ] && [ "$CANON_GIT_DIR" = "$PROJ_COMMON" ] \
|| refuse "project '$PROJ_REAL' is a linked worktree whose primary checkout could not be resolved"
fi
elif [ "$MODE" = lab-home ]; then
LAB_MARKER="$TARGET_REAL/$FM_GATE_LAB_MARKER"
[ ! -L "$LAB_MARKER" ] || refuse "'$LAB_MARKER' is a symlink; a lab home carries the marker as a regular file"
if ! { [ -f "$LAB_MARKER" ] && [ -O "$LAB_MARKER" ] && fm_gate_lab_home "$TARGET_REAL"; }; then
refuse "'$TARGET_REAL' carries no lab-home marker owned by this user, so it is not a disposable lab home"
fi
[ -f "$TARGET_REAL/AGENTS.md" ] || refuse "'$TARGET_REAL' has no AGENTS.md, so it is not a firstmate home"
[ -d "$TARGET_REAL/bin" ] || refuse "'$TARGET_REAL' has no bin/, so it is not a firstmate home"
LAB_TOP=$(git -C "$TARGET_REAL" rev-parse --show-toplevel 2>/dev/null) || true
[ -n "$LAB_TOP" ] && [ "$(real_dir "$LAB_TOP")" = "$TARGET_REAL" ] \
|| refuse "'$TARGET_REAL' is not the top level of a git checkout"
LAB_GIT_DIR=$(git -C "$TARGET_REAL" rev-parse --absolute-git-dir 2>/dev/null) || true
LAB_GIT_DIR=$(real_dir "${LAB_GIT_DIR:-}") || true
LAB_COMMON=$(common_dir_of "$TARGET_REAL") || true
[ -n "$LAB_GIT_DIR" ] && [ "$LAB_GIT_DIR" = "$LAB_COMMON" ] \
|| refuse "'$TARGET_REAL' is a linked worktree, not the primary checkout a lab primary launches in"
else
# The seed evidence, in the order that names the most useful reason first: the
# marker decides whether this is a secondmate home at all, the id decides
Expand Down
Loading
Loading