Skip to content

feat(bin): add named Claude profile pools with auth preflight before spawn - #3

Merged
Freudator86 merged 5 commits into
mainfrom
fm/claude-profile-auth-preflight-v4
Sep 24, 2026
Merged

Freudator86 merged 5 commits into
mainfrom
fm/claude-profile-auth-preflight-v4

Conversation

@Freudator86

Copy link
Copy Markdown
Owner

Intent

we wanted to a local fork..... slim changes

What Changed

  • Added a --claude-profile <id> flag to fm-spawn.sh for local crewmate and scout spawns. The flag is refused on secondmate spawns and on any spawn that resolves to a non-claude harness. A named profile maps to a config_dir in the new per-home config/claude-profiles.json. The spawn checks that profile first with the new bin/fm-claude-auth.sh check --profile <id>, which uses a new claude auth status probe in fm-vendor-auth-probe.sh and confirms first-run onboarding is complete. It refuses to launch if the profile is unconfigured, unauthenticated, indeterminate, or not onboarded, or if a raw launch command sets its own CLAUDE_CONFIG_DIR. When the check passes, the checked store is used for workspace-trust registration and as the worker's CLAUDE_CONFIG_DIR. The default profile is not checked and keeps the current behavior of reusing the caller's CLAUDE_CONFIG_DIR.
  • Each task's record stores its profile as claude_profile. fm-control.sh relaunch keeps that profile when the harness stays claude, and it runs the auth check before stopping the running agent, so a failed check leaves the agent running. The relaunch journal and output now show the profile.
  • Dispatch rules in fm-dispatch-resolve.sh accept claude_profile on claude-harness profiles only, count it when checking for duplicate profiles, and include it in candidate lines and in the resolved profile: line as --claude-profile. config/claude-profiles.json is deliberately left out of config inheritance. The change also updates the configuration, agent-control, architecture, dispatch-auth verification and Claude harness docs, and adds tests for the new helper, spawn, relaunch, dispatch resolution and the probe.

🤖 Generated with Claude Code

Risk Assessment

✅ Low: The fix round reverted grok version parsing to the base sed line and removed the stale default-preflight fixtures. The remaining change is a narrow opt-in preflight for named Claude pools. Default Claude launches stay ambient as before, and tracing the relaunch, trust-store and dispatch paths found no wrong-result case.

Testing

I drove the real bin/fm-spawn.sh and bin/fm-control.sh inside a throwaway fm-lab-* Herdr session, provisioned and torn down through bin/fm-herdr-lab.sh; the default session was untouched and its tripwire held. The real Claude CLI launched in the lab panes. I read each running claude process's environment from /proc to see which CLAUDE_CONFIG_DIR store it got. Results: - A default spawn keeps the ambient store, makes no auth-status call, and records no claude_profile. - A --claude-profile claude-max-a spawn launches claude on the pool's store, writes workspace trust into that store, and records claude_profile=claude-max-a. - A pool whose store the real CLI reports as logged out is refused before any pane or task record is created. - An unconfigured pool id, the flag on a non-claude harness, and a raw launch command that sets its own CLAUDE_CONFIG_DIR are each refused before launch. - fm-control relaunch onto a recorded pool that is now logged out is refused before the stop step: the old claude process and pane stay alive and no relaunch journal is written. I also ran the real probe and check directly against the real logged-in store and an empty store. The five targeted suites (fm-claude-auth, fm-vendor-auth-probe including grok, fm-dispatch-resolve, fm-control-relaunch, fm-spawn-dispatch-profile) all pass. Everything I could drive passed. I did not drive a successful relaunch onto a pool live, because that needs a second logged-in, onboarded Claude account. This is CLI and TUI routing, not a visual UI change, so the evidence is transcripts and process-environment captures rather than screenshots. Afterwards I removed the lab's orphaned treehouse pool dirs.

  • Live validation: ✅ go - 6 of 8 scenarios driven live against the product
Scenario Result Live Evidence
Default Claude spawn (no --claude-profile) runs on the ambient CLAUDE_CONFIG_DIR with no auth preflight and no claude_profile record ✅ pass live claude-profile-herdr-lab.log S1: spawn rc=0; real claude pid env CLAUDE_CONFIG_DIR=<lab>/ambient-store; auth-status call log empty; meta has no claude_profile
Spawn with --claude-profile on a logged-in, onboarded pool launches the worker on that pool's store and records the pool ✅ pass live claude-profile-herdr-lab.log S2: one auth status probe on pool-a; live claude process env CLAUDE_CONFIG_DIR=<lab>/pool-a; meta claude_profile=claude-max-a; trust entries written to pool-a/.claude.json…
Spawn onto a named pool whose store the real Claude CLI reports as logged out is refused before any pane or record is created ✅ pass live claude-profile-herdr-lab.log S3: 'auth=unauthenticated:vendor-probe', rc=1, lab pane count unchanged, no meta
Adversarial: an unconfigured pool id, --claude-profile on a non-claude harness, and a raw launch command that sets CLAUDE_CONFIG_DIR itself are each refused ✅ pass live claude-profile-herdr-lab.log S4: three refusals with specific messages, rc=1 each, no task metadata
fm-control relaunch onto a recorded pool that has become logged out is refused before the running agent is stopped ✅ pass live claude-profile-herdr-lab.log S6: 'not ready for the replacement worker' refusal; same pane; claude process on pool-a still alive; no relaunch journal
Real vendor probe and fm-claude-auth check classify real stores (logged in vs empty) and reject unconfigured/default ids ✅ pass live real-claude-probe-and-check.txt
fm-control relaunch of a claude task keeps its recorded pool and passes --claude-profile to the replacement ⏸️ untested no A live relaunch has to reach an idle Claude composer to type /exit. That needs a second Claude account store that is logged in and onboarded. None is available here, and copying production credentials…
Dispatch resolution forwards claude_profile as --claude-profile without probing auth ⏸️ untested no No live dispatch surface was driven. The dispatch-rule fixture test is the only check, and it is not a live run.
Evidence: Live Herdr lab transcript (S1-S4, S6)

Source: Live Herdr lab transcript (S1-S4, S6)

lab session=fm-lab-claudeprof-2963896-5271

=== S1 default (no --claude-profile): ambient store kept, no preflight ===
warning: <lab>~/deflt-a1/launch-brief.md records no delivery contract line (scaffolded before ship briefs recorded one); launching on the explicit --mode local-only - confirm its definition of done matches
notice: deflt-a1 ships mode=local-only while the standing posture for proj is no-mistakes - less rigor than the captain's standing posture; proceed only on a current explicit captain instruction or an intake judgment you can state
spawned deflt-a1 harness=claude kind=ship mode=local-only yolo=on window=fm-lab-claudeprof-2963896-5271:w1:p2 worktree=~/.treehouse/proj-d15dea/1/proj
spawn rc=0
--- meta ---
harness=claude
herdr_pane_id=w1:p2
~/.no-mistakes/evidence/01M33CJ8GETQSZKRTQKC6RX3RT/claude-profile-herdr-lab.sh: line 88: /proc/2966211/cmdline: No such file or directory
~/.no-mistakes/evidence/01M33CJ8GETQSZKRTQKC6RX3RT/claude-profile-herdr-lab.sh: line 88: /proc/2966221/cmdline: No such file or directory
~/.no-mistakes/evidence/01M33CJ8GETQSZKRTQKC6RX3RT/claude-profile-herdr-lab.sh: line 88: /proc/2966230/cmdline: No such file or directory
--- live claude processes (pid, env CLAUDE_CONFIG_DIR, argv) ---
2966176 CLAUDE_CONFIG_DIR=<lab>/ambient-store | claude --dangerously-skip-permissions --settings {"feedbackD
You are a crewmate: an autonomous worker agent managed by fi
This section establishes your current identity before every 
Do the assigned work yourself and report only to firstmate; 
Your steering inbox is `<lab>/ho
Never inspect or change any other home's endpoint namespace;
When this task works on Firstmate itself, the repository roo
Project instructions still govern the work wherever they do 

# Task
## Captain's intent
lab deflt-a1

## Firstmate spec
nothing 
~/.no-mistakes/evidence/01M33CJ8GETQSZKRTQKC6RX3RT/claude-profile-herdr-lab.sh: line 88: /proc/2966482/cmdline: No such file or directory
~/.no-mistakes/evidence/01M33CJ8GETQSZKRTQKC6RX3RT/claude-profile-herdr-lab.sh: line 88: /proc/2966484/cmdline: No such file or directory
--- pane screen (real claude in lab pane) ---
quartermaster@quartermaster:<lab>/proj$ treehouse get
🌳 Setting up worktree...
🌳 Entered worktree at ~/.treehouse/proj-d15dea/1/proj. Type 'exit' to return.
quartermaster@quartermaster:~/.treehouse/proj-d15dea/1/proj$ export GOTMPDIR=/tmp/fm-deflt-a1/gotmp
quartermaster@quartermaster:~/.treehouse/proj-d15dea/1/proj$ export COMPACT_ADVISER_DISABLE=1
quartermaster@quartermaster:~/.treehouse/proj-d15dea/1/proj$ export FM_TASK_ID=deflt-a1
quartermaster@quartermaster:~/.treehouse/proj-d15dea/1/proj$ . '/tmp/fm-deflt-a1+c8ccaf80b19606b6622e0799abf1dae25f7f7f0
fa6b38670ea0a142c618b3e8d/launch.s1790043012.2963971.5569.sh'
Welcome to Claude Code v2.1.276
..........................................................
     *                                       █████▓▓░
                                 *         ███▓░     ░░
--- auth status calls ---
~/.no-mistakes/evidence/01M33CJ8GETQSZKRTQKC6RX3RT/claude-profile-herdr-lab.sh: line 88: /proc/2967293/cmdline: No such file or directory
RESULT S1: pass

=== S2 named pool logged in + onboarded: worker pinned to pool store ===
warning: <lab>~/pool-a2/launch-brief.md records no delivery contract line (scaffolded before ship briefs recorded one); launching on the explicit --mode local-only - confirm its definition of done matches
notice: pool-a2 ships mode=local-only while the standing posture for proj is no-mistakes - less rigor than the captain's standing posture; proceed only on a current explicit captain instruction or an intake judgment you can state
spawned pool-a2 harness=claude kind=ship mode=local-only yolo=on window=fm-lab-claudeprof-2963896-5271:w1:p3 worktree=~/.treehouse/proj-d15dea/2/proj
spawn rc=0
--- meta ---
harness=claude
claude_profile=claude-max-a
herdr_pane_id=w1:p3
~/.no-mistakes/evidence/01M33CJ8GETQSZKRTQKC6RX3RT/claude-profile-herdr-lab.sh: line 88: /proc/2968654/cmdline: No such file or directory
--- live claude processes (pid, env CLAUDE_CONFIG_DIR, argv) ---
2966176 CLAUDE_CONFIG_DIR=<lab>/ambient-store | claude --dangerously-skip-permissions --settings {"feedbackD
You are a crewmate: an autonomous worker agent managed by fi
This section establishes your current identity before every 
Do the assigned work yourself and report only to firstmate; 
Your steering inbox is `<lab>/ho
Never inspect or change any other home's endpoint namespace;
When this task works on Firstmate itself, the repository roo
Project instructions still govern the work wherever they do 

# Task
## Captain's intent
lab deflt-a1

## Firstmate spec
nothing 
2968627 CLAUDE_CONFIG_DIR=<lab>/pool-a | claude --dangerously-skip-permissions --settings {"feedbackD
You are a crewmate: an autonomous worker agent managed by fi
This section establishes your current identity before every 
Do the assigned work yourself and report only to firstmate; 
Your steering inbox is `<lab>/ho
Never inspect or change any other home's endpoint namespace;
When this task works on Firstmate itself, the repository roo
Project instructions still govern the work wherever they do 

# Task
## Captain's intent
lab pool-a2

## Firstmate spec
nothing 
--- auth status calls ---
auth-status CLAUDE_CONFIG_DIR=<lab>/pool-a
--- trust written to pool store ---
["~/.treehouse/proj-d15dea/2/proj","<lab>/proj"]

~/.no-mistakes/evidence/01M33CJ8GETQSZKRTQKC6RX3RT/claude-profile-herdr-lab.sh: line 88: /proc/2969045/cmdline: No such file or directory
RESULT S2: pass

=== S3 named pool NOT logged in (real claude on empty store): refused before launch ===
error: Claude named profile claude-max-empty is not ready for worker launch; refusing before launch. profile=claude-max-empty auth=unauthenticated:vendor-probe config_dir=<lab>/pool-empty
auth: Claude named profile claude-max-empty is not authenticated (unauthenticated:vendor-probe); authenticate that store before launching it.
spawn rc=1
lab panes before=2 after=2; meta present: no
RESULT S3: pass

=== S4 unconfigured pool / non-claude harness / raw CLAUDE_CONFIG_DIR: refused ===
error: Claude named profile claude-max-zzz could not be resolved; refusing before launch. error: Claude profile not configured in this home: claude-max-zzz; install config/claude-profiles.json with a local absolute config_dir for that named pool
error: --claude-profile names a Claude capacity pool, but this spawn resolved harness sh; drop the flag or spawn on claude
error: the raw launch command sets CLAUDE_CONFIG_DIR itself, which would take effect after the store the Claude profile preflight checked; that worker would use an unchecked account while its record named the checked one. Drop the assignment from the command and select the store with --claude-profile, or launch it on a non-claude command.
rcs=1 1 1; metas: 0
RESULT S4: pass

=== S6 relaunch when the recorded pool is logged out: refused before stopping the agent ===
error: Claude named profile claude-max-a is not ready for the replacement worker, so relaunching pool-a2 onto it would stop the running agent for a launch that must be refused; restore that pool's login with bin/fm-claude-auth.sh before relaunching. profile=claude-max-a auth=unauthenticated:vendor-probe config_dir=<lab>/pool-a
auth: Claude named profile claude-max-a is not authenticated (unauthenticated:vendor-probe); authenticate that store before launching it.
relaunch rc=1
pane still=w1:p3 (was w1:p3)
--- live claude processes after refusal ---
2966176 CLAUDE_CONFIG_DIR=<lab>/ambient-store | claude --dangerously-skip-permissions --settings {"feedbackD
You are a crewmate: an autonomous worker agent managed by fi
This section establishes your current identity before every 
Do the assigned work yourself and report only to firstmate; 
Your steering inbox is `<lab>/ho
Never inspect or change any other home's endpoint namespace;
When this task works on Firstmate itself, the repository roo
Project instructions still govern the work wherever they do 

# Task
## Captain's intent
lab deflt-a1

## Firstmate spec
nothing 
2968627 CLAUDE_CONFIG_DIR=<lab>/pool-a | claude --dangerously-skip-permissions --settings {"feedbackD
You are a crewmate: an autonomous worker agent managed by fi
This section establishes your current identity before every 
Do the assigned work yourself and report only to firstmate; 
Your steering inbox is `<lab>/ho
Never inspect or change any other home's endpoint namespace;
When this task works on Firstmate itself, the repository roo
Project instructions still govern the work wherever they do 

# Task
## Captain's intent
lab pool-a2

## Firstmate spec
nothing 
--- journal ---
(no relaunch journal written)
~/.no-mistakes/evidence/01M33CJ8GETQSZKRTQKC6RX3RT/claude-profile-herdr-lab.sh: line 88: /proc/2970109/cmdline: No such file or directory
RESULT S6: pass

=== summary ===
S1: pass
S2: pass
S3: pass
S4: pass
S6: pass
teardown rc=0
Evidence: Live Herdr lab driver script

Source: Live Herdr lab driver script

#!/usr/bin/env bash
# Live Herdr-lab validation of the named Claude profile preflight.
# Drives the real bin/fm-spawn.sh and bin/fm-control.sh against an isolated
# fm-lab-* Herdr session created through bin/fm-herdr-lab.sh. `claude auth
# status` / `claude --version` are answered by the REAL claude CLI, except a
# store listed in $LAB_AUTHED (a store we cannot log in without copying
# production credentials), which answers loggedIn:true. Any other claude
# invocation (the worker launch itself) prints the store it was given and
# sleeps, so the pane shows which account the worker would run on.
set -u
ROOT=${ROOT:?set ROOT to the worktree}
EVID=${EVID:?set EVID}
REAL_CLAUDE=$(command -v claude)
TMP=$(mktemp -d "${TMPDIR:-/tmp}/fm-claude-profile-lab.XXXXXX")
LAB="$ROOT/bin/fm-herdr-lab.sh"
SESSION=$("$LAB" name claudeprof) || exit 1
export HERDR_SESSION=$SESSION
. "$ROOT/tests/herdr-test-safety.sh"
herdr_forget_inherited_pane
WORKTREES=()
CLEANED=0
RESULTS=()
cleanup() {
  [ "$CLEANED" = 0 ] || return 0; CLEANED=1
  local wt
  for wt in ${WORKTREES[@]+"${WORKTREES[@]}"}; do treehouse return --force "$wt" >/dev/null 2>&1; done
  "$LAB" teardown "$SESSION"; echo "teardown rc=$?"
  rm -rf "$TMP"
}
trap cleanup EXIT
"$LAB" provision "$SESSION" || { echo "provision failed"; exit 1; }
lab() { "$LAB" run "$SESSION" "$@"; }
LAB_SOCKET=$(lab session list --json | jq -r --arg s "$SESSION" '.sessions[] | select(.name==$s) | .socket_path')
echo "lab session=$SESSION"

res() { RESULTS+=("$1: $2"); echo "RESULT $1: $2"; }

FAKEBIN="$TMP/bin"; mkdir -p "$FAKEBIN"
AUTHLOG="$TMP/auth-calls.log"; : > "$AUTHLOG"
cat > "$FAKEBIN/claude" <<SH
#!/usr/bin/env bash
case "\${1:-} \${2:-}" in
  "auth status")
    echo "auth-status CLAUDE_CONFIG_DIR=\${CLAUDE_CONFIG_DIR:-<unset>}" >> "$AUTHLOG"
    case " \${LAB_AUTHED:-} " in *" \${CLAUDE_CONFIG_DIR:-none} "*)
      printf '{"loggedIn": true, "authMethod": "claude.ai"}\n'; exit 0 ;; esac
    exec "$REAL_CLAUDE" "\$@" ;;
  "--version "*) exec "$REAL_CLAUDE" --version ;;
esac
echo "WORKER-CLAUDE running with CLAUDE_CONFIG_DIR=\${CLAUDE_CONFIG_DIR:-<unset>}"
printf '%s\n' '────────────────────────────────────────' '❯ ' '────────────────────────────────────────' '  ? for shortcuts'
while IFS= read -r line; do case "\$line" in */exit*) exit 0 ;; esac; done
exec sleep 600
SH
chmod +x "$FAKEBIN/claude"

PROJ="$TMP/proj"; mkdir -p "$PROJ"
git -C "$PROJ" init -q; echo '# scratch' > "$PROJ/README.md"
git -C "$PROJ" add README.md; git -C "$PROJ" -c user.name=t -c user.email=t@e.invalid commit -qm init
git clone -q --bare "$PROJ" "$PROJ.origin.git"; git -C "$PROJ" remote add origin "file://$PROJ.origin.git"

H="$TMP/home"; mkdir -p "$H/state" "$H/config" "$H/data"
printf 'off\n' > "$H/config/herdr-presentation-spaces"
FAKE_HOME="$TMP/userhome"; mkdir -p "$FAKE_HOME"
AMBIENT="$TMP/ambient-store"; mkdir -p "$AMBIENT"
POOL_A="$TMP/pool-a"; POOL_EMPTY="$TMP/pool-empty"; mkdir -p "$POOL_A" "$POOL_EMPTY"
printf '{"hasCompletedOnboarding":true}\n' > "$POOL_A/.claude.json"
printf '{"hasCompletedOnboarding":true}\n' > "$POOL_EMPTY/.claude.json"
cat > "$H/config/claude-profiles.json" <<EOF
{"profiles":[{"id":"claude-max-a","config_dir":"$POOL_A"},{"id":"claude-max-empty","config_dir":"$POOL_EMPTY"}]}
EOF

brief() { mkdir -p "$H/data/$1"; printf '# Task\n## Captain'"'"'s intent\nlab %s\n\n## Firstmate spec\nnothing\n' "$1" > "$H/data/$1/brief.md"; }

spawn() { # <id> [args...]
  local id=$1; shift; brief "$id"
  env -u HERDR_ENV -u HERDR_PANE_ID -u HERDR_SOCKET_PATH PATH="$FAKEBIN:$PATH" CLAUDE_CONFIG_DIR="$AMBIENT" LAB_AUTHED="$POOL_A" \
    HERDR_SESSION="$SESSION" \
    FM_SPAWN_NO_GUARD=1 FM_HOME="$H" FM_ROOT_OVERRIDE="$ROOT" \
    "$ROOT/bin/fm-spawn.sh" "$id" "$PROJ" claude --mode local-only --yolo on --backend herdr "$@"
}
pane_of() { grep 2>/dev/null '^herdr_pane_id=' "$H/state/$1.meta" | cut -d= -f2-; }
screen() { lab pane read "$(pane_of "$1")" --source recent --lines 200 2>/dev/null; }
track() { local wt; wt=$(grep '^worktree=' "$H/state/$1.meta" 2>/dev/null | cut -d= -f2-); [ -z "$wt" ] || WORKTREES+=("$wt"); }
claude_procs() { # prints "pid CLAUDE_CONFIG_DIR" for live claude processes under the lab tmp
  local d pid cfg
  for d in /proc/[0-9]*; do pid=${d#/proc/}
    tr '\0' ' ' < "$d/cmdline" 2>/dev/null | grep -q 'claude' || continue
    cfg=$(tr '\0' '\n' < "$d/environ" 2>/dev/null | grep '^CLAUDE_CONFIG_DIR=' | head -1)
    case "$cfg" in *"$TMP"*) echo "$pid $cfg | $(tr '\0' ' ' < "$d/cmdline" | cut -c1-60)";; esac
  done
}
has_proc() { local i; for i in $(seq 1 30); do claude_procs | grep -q "CLAUDE_CONFIG_DIR=$1 " && return 0; sleep 1; done; return 1; }
wait_screen() { local i; for i in $(seq 1 30); do screen "$1" | grep -q "$2" && return 0; sleep 1; done; return 1; }

echo; echo "=== S1 default (no --claude-profile): ambient store kept, no preflight ==="
: > "$AUTHLOG"
spawn deflt-a1 > "$TMP/s1.out" 2>&1; rc=$?; track deflt-a1
cat "$TMP/s1.out" | tail -5; echo "spawn rc=$rc"
echo "--- meta ---"; grep -E '^(harness|claude_profile|herdr_pane_id)=' "$H/state/deflt-a1.meta"
has_proc "$AMBIENT"; echo "--- live claude processes (pid, env CLAUDE_CONFIG_DIR, argv) ---"; claude_procs
sleep 3; echo "--- pane screen (real claude in lab pane) ---"; screen deflt-a1 | grep -v '^\s*$' | head -12
echo "--- auth status calls ---"; cat "$AUTHLOG"
if [ $rc = 0 ] && claude_procs | grep -q "CLAUDE_CONFIG_DIR=$AMBIENT " && [ ! -s "$AUTHLOG" ] && ! grep -q '^claude_profile=' "$H/state/deflt-a1.meta"; then res S1 pass; else res S1 fail; fi

echo; echo "=== S2 named pool logged in + onboarded: worker pinned to pool store ==="
: > "$AUTHLOG"
spawn pool-a2 --claude-profile claude-max-a > "$TMP/s2.out" 2>&1; rc=$?; track pool-a2
tail -5 "$TMP/s2.out"; echo "spawn rc=$rc"
echo "--- meta ---"; grep -E '^(harness|claude_profile|herdr_pane_id)=' "$H/state/pool-a2.meta"
has_proc "$POOL_A"; echo "--- live claude processes (pid, env CLAUDE_CONFIG_DIR, argv) ---"; claude_procs
echo "--- auth status calls ---"; cat "$AUTHLOG"
echo "--- trust written to pool store ---"; jq -c '.projects | keys' "$POOL_A/.claude.json" 2>&1 | head -c 400; echo
if [ $rc = 0 ] && claude_procs | grep -q "CLAUDE_CONFIG_DIR=$POOL_A " && grep -q '^claude_profile=claude-max-a$' "$H/state/pool-a2.meta"; then res S2 pass; else res S2 fail; fi

echo; echo "=== S3 named pool NOT logged in (real claude on empty store): refused before launch ==="
before=$(lab pane list 2>/dev/null | jq '[.result.panes[]?] | length')
spawn empty-a3 --claude-profile claude-max-empty > "$TMP/s3.out" 2>&1; rc=$?
cat "$TMP/s3.out" | tail -3; echo "spawn rc=$rc"
after=$(lab pane list 2>/dev/null | jq '[.result.panes[]?] | length')
echo "lab panes before=$before after=$after; meta present: $([ -e "$H/state/empty-a3.meta" ] && echo yes || echo no)"
if [ $rc != 0 ] && grep -q 'unauthenticated:vendor-probe' "$TMP/s3.out" && [ ! -e "$H/state/empty-a3.meta" ] && [ "$before" = "$after" ]; then res S3 pass; else res S3 fail; fi

echo; echo "=== S4 unconfigured pool / non-claude harness / raw CLAUDE_CONFIG_DIR: refused ==="
spawn nope-a4 --claude-profile claude-max-zzz > "$TMP/s4a.out" 2>&1; rc1=$?; tail -1 "$TMP/s4a.out"
brief nope-b4
env -u HERDR_PANE_ID -u HERDR_ENV PATH="$FAKEBIN:$PATH" HERDR_SESSION="$SESSION" FM_SPAWN_NO_GUARD=1 FM_HOME="$H" FM_ROOT_OVERRIDE="$ROOT" \
  "$ROOT/bin/fm-spawn.sh" nope-b4 "$PROJ" "sh -c 'echo hi; sleep 60'" --mode local-only --yolo on --backend herdr --claude-profile claude-max-a > "$TMP/s4b.out" 2>&1; rc2=$?; tail -1 "$TMP/s4b.out"
brief nope-c4
env -u HERDR_PANE_ID -u HERDR_ENV PATH="$FAKEBIN:$PATH" LAB_AUTHED="$POOL_A" HERDR_SESSION="$SESSION" FM_SPAWN_NO_GUARD=1 FM_HOME="$H" FM_ROOT_OVERRIDE="$ROOT" \
  "$ROOT/bin/fm-spawn.sh" nope-c4 "$PROJ" "CLAUDE_CONFIG_DIR=/tmp/other claude" --mode local-only --yolo on --backend herdr --claude-profile claude-max-a > "$TMP/s4c.out" 2>&1; rc3=$?; tail -1 "$TMP/s4c.out"
echo "rcs=$rc1 $rc2 $rc3; metas: $(ls "$H/state" | grep -c '^nope-' || true)"
if [ $rc1 != 0 ] && [ $rc2 != 0 ] && [ $rc3 != 0 ] && grep -q 'not configured in this home' "$TMP/s4a.out" && grep -q 'names a Claude capacity pool' "$TMP/s4b.out" && grep -q 'sets CLAUDE_CONFIG_DIR itself' "$TMP/s4c.out" && ! ls "$H/state" | grep -q '^nope-.*\.meta$'; then res S4 pass; else res S4 fail; fi

ctl() { env -u HERDR_PANE_ID -u HERDR_ENV PATH="$FAKEBIN:$PATH" CLAUDE_CONFIG_DIR="$AMBIENT" LAB_AUTHED="${1}" HERDR_SESSION="$SESSION" \
  FM_SPAWN_NO_GUARD=1 FM_HOME="$H" FM_ROOT_OVERRIDE="$ROOT" "$ROOT/bin/fm-control.sh" pool-a2 relaunch --note "herdr lab relaunch"; }
echo; echo "=== S6 relaunch when the recorded pool is logged out: refused before stopping the agent ==="
pane=$(pane_of pool-a2)
ctl "" > "$TMP/s6.out" 2>&1; rc=$?; tail -3 "$TMP/s6.out"; echo "relaunch rc=$rc"
echo "pane still=$(pane_of pool-a2) (was $pane)"; echo "--- live claude processes after refusal ---"; claude_procs
echo "--- journal ---"; ls "$H/state" | grep -i relaunch || echo "(no relaunch journal written)"
if [ $rc != 0 ] && grep -q 'not ready for the replacement worker' "$TMP/s6.out" && [ "$(pane_of pool-a2)" = "$pane" ] && claude_procs | grep -q "CLAUDE_CONFIG_DIR=$POOL_A "; then res S6 pass; else res S6 fail; fi

echo; echo "=== summary ==="; printf '%s\n' "${RESULTS[@]}"
for id in deflt-a1 pool-a2; do
  env HERDR_SESSION="$SESSION" FM_HOME="$H" FM_ROOT_OVERRIDE="$ROOT" "$ROOT/bin/fm-teardown.sh" "$id" --force >/dev/null 2>&1 || true
done
Evidence: Real claude probe and fm-claude-auth check transcript

Source: Real claude probe and fm-claude-auth check transcript

$ bin/fm-vendor-auth-probe.sh claude   # ambient real store, real claude 2.1.276
probe=claude status=authenticated version=2.1.276 versionVerified=yes
rc=0
$ CLAUDE_CONFIG_DIR=<empty temp store> bin/fm-vendor-auth-probe.sh claude
probe=claude status=unauthenticated version=2.1.276 versionVerified=yes
rc=0
$ fm-claude-auth.sh check --profile claude-real   # config_dir=$HOME/.claude (real logged-in store)
profile=claude-real auth=unonboarded:first-run-onboarding-incomplete config_dir=~/.claude
setup: Claude profile claude-real is logged in, but its store (~/.claude) has not completed Claude first-run onboarding (unonboarded:first-run-onboarding-incomplete), so a named worker would open on an interactive setup screen. Run claude once with CLAUDE_CONFIG_DIR=~/.claude and finish onboarding, then retry.
rc=1
$ fm-claude-auth.sh check --profile claude-empty  # config_dir=<empty temp store>
profile=claude-empty auth=unauthenticated:vendor-probe config_dir=<tmp>/empty-store
auth: Claude named profile claude-empty is not authenticated (unauthenticated:vendor-probe); authenticate that store before launching it.
rc=1
$ fm-claude-auth.sh check --profile claude-missing
error: Claude profile not configured in this home: claude-missing; install config/claude-profiles.json with a local absolute config_dir for that named pool
rc=2
$ fm-claude-auth.sh check --profile default
error: default Claude profile is ambient and is not preflighted here
rc=2
Evidence: Live claude process pinned to the named pool store
spawned pool-a2 harness=claude ...
meta: claude_profile=claude-max-a
live claude processes:
2966176 CLAUDE_CONFIG_DIR=<lab>/ambient-store | claude --dangerously-skip-permissions ... (deflt-a1, no profile)
2968627 CLAUDE_CONFIG_DIR=<lab>/pool-a | claude --dangerously-skip-permissions ... (pool-a2, --claude-profile claude-max-a)
Evidence: Targeted suite logs

Source: Targeted suite logs

ok - allowlist=absent preserves the operational floor and filters only when opted in
ok - allowlist=missing-config preserves the operational floor and filters only when opted in
ok - allowlist=enabled preserves the operational floor and filters only when opted in
ok - allowlist=empty preserves the operational floor and filters only when opted in
ok - invalid allowlist names refuse before launch or task publication
ok - inaccessible config with present allowlist refuses before launch or task publication
ok - inaccessible config with absent allowlist refuses before launch or task publication
ok - inaccessible ancestor with present allowlist refuses before launch or task publication
ok - inaccessible ancestor with absent allowlist refuses before launch or task publication
ok - secondmate launch inherits the allowlist for subsequent worker launches
ok - local inheritance preserves the allowlist on source errors and mirrors proven absence
ok - remote inheritance preserves the allowlist on source errors and mirrors proven absence
ok - fm-spawn: actual ship/scout launch commands deliver the worker role contract
ok - no --model/--effort records defaults and types the claude launch instructions
ok - non-cursor launches clear inherited Cursor identity markers
ok - relative home overrides ignore CDPATH and become absolute before spawn launch construction
ok - FM_HOME defaults resolve relative paths and preserve absolute spellings
ok - absolute override spellings are preserved in spawn launch paths
ok - unresolvable relative spawn overrides fail with named diagnostics
ok - active crew-dispatch profile requires an explicit harness for ship spawns
ok - active crew-dispatch profile requires an explicit harness for scout spawns
ok - active crew-dispatch profile allows an explicit resolved harness
ok - active crew-dispatch profile allows the legacy positional harness form
ok - active crew-dispatch profile allows the raw launch-command escape hatch
ok - claude receives --model and --effort profile flags
ok - codex receives --model and model_reasoning_effort profile flags
ok - codex Luna receives --model and model_reasoning_effort max profile flags
ok - codex omits max for models without the catalog capability
ok - a codex crewmate launches with no hook layer and keeps its turn-end signal
ok - a codex secondmate keeps the project hook layer its primary session runs on
ok - grok receives --model and --reasoning-effort profile flags
ok - grok omits unsupported max reasoning effort
ok - grok omits unsupported xhigh reasoning effort
ok - cursor receives its model-qualified reasoning class and exact task workspace
ok - cursor refuses model ids absent from its resolved binary's live catalog
ok - cursor preserves the requested model when its live catalog is unreachable
ok - opencode receives --model and omits the unsupported effort axis
ok - native effort validator checks harness and model as separate axes
ok - Ultra is explicit for native Pi and Pi-signed, including direct-PR, and refuses unsupported profiles before provisioning
ok - batch dispatch preserves native Ultra in metadata and launch flags
ok - pi receives --model and --thinking max profile flags
ok - Pi launch probing omits --tui-mode on older Pi and preserves it on supporting Pi
ok - pi-signed shares Pi launch semantics while preserving its configured and recorded identity
ok - pi-signed refuses safely and actionably when the selected executable is unavailable
ok - pi-signed is a distinct persistent secondmate runtime with shared Pi supervision semantics
ok - batch dispatch forwards shared --harness, --model, --effort, and --claude-profile to every pair
ok - claude spawn refuses an unauthenticated named profile before endpoint launch
ok - claude spawn refuses a logged-in named profile whose first-run onboarding is incomplete
ok - claude forwards firstmate's CLAUDE_CONFIG_DIR so the crewmate uses the same credential store
ok - the primary-owned Lavish server address reaches every worker launch
ok - absent Lavish configuration preserves the destination environment
ok - claude adds no CLAUDE_CONFIG_DIR prefix and trusts the ambient store when the variable is unset
ok - claude pins the checked named profile config dir over the ambient variable
ok - a pools-only Claude profile config keeps serving spawns that name no profile
ok - config/claude-permission-mode=bypass launches exactly as an absent file does
ok - config/claude-permission-mode=auto replaces --dangerously-skip-permissions with --permission-mode auto
ok - config/claude-permission-mode=auto reaches scout launches too
ok - an unrecognized config/claude-permission-mode token refuses before any endpoint or metadata
ok - config/claude-permission-mode changes claude launches only
ok - non-claude harnesses do not receive the claude CLAUDE_CONFIG_DIR prefix
ok - a Claude capacity pool cannot be attached to a non-Claude spawn
ok - a raw claude command cannot silently rescope the store the preflight checked
ok - an unconfigured Claude pool refuses by naming the configuration gap, not a measured login state
ok - a claude task launch establishes only Firstmate's task control channels through the system prompt
ok - a persistent claude secondmate keeps its supervisor contract without a task-worker authority overlay
ok - fm-spawn: a Claude launch longer than 1024 bytes is delivered intact through the staging path
ok - a claude crewmate launch carries the attribution-off policy in its own settings
ok - a claude secondmate launch carries the attribution-off policy too
ok - secondmate spawns refuse named Claude pools instead of promising recovery support
ok - active crew-dispatch profile does not block secondmate launches
# all fm-spawn-dispatch-profile tests passed

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 2 issues found → auto-fixed ✅
  • ⚠️ bin/fm-vendor-auth-probe.sh:186 - In round 1 the user chose to fix 'grok-version-refactor', which asked to leave the grok path unchanged. The grok version path still changed. grok_version no longer uses the original sed -nE &#39;s/.*[^0-9]([0-9]+\.[0-9]+\.[0-9]+).*/\1/p&#39;, which takes the last semver on the line and needs a non-digit before it. It now uses grep -oE ... | head -n 1, which takes the first semver. Example: a grok --version line that contains two version strings now gives a different VERSION, and so possibly a different versionVerified. The Claude preflight does not need this change. Fix: restore the original sed line in grok_version so the grok probe matches the base.
  • ⚠️ tests/fm-backend.test.sh:47 - In round 1 the user chose to fix 'stale-default-preflight-fixtures', but most of those leftovers from the dropped default-profile preflight are still here. (1) tests/fm-backend.test.sh:47-49 says 'A claude spawn is refused unless the default profile's ambient store probes logged in and has finished first-run onboarding' and onboards SPAWN_HOME. The same fm_test_onboard_claude_store setup appears in fm-backend-orca.test.sh:19 and fm-trace-context-spawn.test.sh. (2) fm-secondmate-harness.test.sh:444-457 and 700-713 add fake claude CLIs with the comment 'The Claude profile preflight runs the bounded vendor probe before every claude launch'. Lines 481-482 and 725-726 onboard stores with the comment 'A claude secondmate is refused until its store finished first-run onboarding'. Both claims are false now: default launches are not preflighted, and secondmates refuse --claude-profile. (3) fm-dispatch-resolve.test.sh:26-31 writes a claude-profiles.json with id 'default' (which the helper rejects), a .credentials.json holding a fake refresh token, and an onboarded store. Lines 158-176 add a fake claude CLI that dispatch never calls. (4) tests/fixtures.sh adds FM_FAKE_CLAUDE_LOGGED_IN / FM_TEST_NO_CLAUDE_AUTH plumbing for the removed default probe. Fix: revert these fixture and comment edits in the non-profile suites and keep only what the named-pool tests use.

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 6 of 8 scenarios driven live against the product
Scenario Result Live Evidence
Default Claude spawn (no --claude-profile) runs on the ambient CLAUDE_CONFIG_DIR with no auth preflight and no claude_profile record ✅ pass live claude-profile-herdr-lab.log S1: spawn rc=0; real claude pid env CLAUDE_CONFIG_DIR=<lab>/ambient-store; auth-status call log empty; meta has no claude_profile
Spawn with --claude-profile on a logged-in, onboarded pool launches the worker on that pool's store and records the pool ✅ pass live claude-profile-herdr-lab.log S2: one auth status probe on pool-a; live claude process env CLAUDE_CONFIG_DIR=<lab>/pool-a; meta claude_profile=claude-max-a; trust entries written to pool-a/.claude.json…
Spawn onto a named pool whose store the real Claude CLI reports as logged out is refused before any pane or record is created ✅ pass live claude-profile-herdr-lab.log S3: 'auth=unauthenticated:vendor-probe', rc=1, lab pane count unchanged, no meta
Adversarial: an unconfigured pool id, --claude-profile on a non-claude harness, and a raw launch command that sets CLAUDE_CONFIG_DIR itself are each refused ✅ pass live claude-profile-herdr-lab.log S4: three refusals with specific messages, rc=1 each, no task metadata
fm-control relaunch onto a recorded pool that has become logged out is refused before the running agent is stopped ✅ pass live claude-profile-herdr-lab.log S6: 'not ready for the replacement worker' refusal; same pane; claude process on pool-a still alive; no relaunch journal
Real vendor probe and fm-claude-auth check classify real stores (logged in vs empty) and reject unconfigured/default ids ✅ pass live real-claude-probe-and-check.txt
fm-control relaunch of a claude task keeps its recorded pool and passes --claude-profile to the replacement ⏸️ untested no A live relaunch has to reach an idle Claude composer to type /exit. That needs a second Claude account store that is logged in and onboarded. None is available here, and copying production credentials…
Dispatch resolution forwards claude_profile as --claude-profile without probing auth ⏸️ untested no No live dispatch surface was driven. The dispatch-rule fixture test is the only check, and it is not a live run.
  • bin/fm-vendor-auth-probe.sh claude against the real logged-in ambient store and against an empty temp CLAUDE_CONFIG_DIR (real claude 2.1.276)
  • bin/fm-claude-auth.sh check --profile &lt;id&gt; for four profiles: the real ~/.claude store, an empty store, an unconfigured id, and default
  • Live Herdr lab claude-profile-herdr-lab.sh (evidence dir) via bin/fm-herdr-lab.sh provision/run/teardown on session fm-lab-claudeprof-*: S1 default spawn, S2 named pool spawn, S3 logged-out pool refusal, S4 unconfigured/non-claude/raw-CLAUDE_CONFIG_DIR refusals, S6 fm-control relaunch refusal before stop
  • bash tests/fm-claude-auth.test.sh
  • bash tests/fm-vendor-auth-probe.test.sh
  • bash tests/fm-dispatch-resolve.test.sh
  • bash tests/fm-control-relaunch.test.sh
  • bash tests/fm-spawn-dispatch-profile.test.sh
✅ **Document** - passed

✅ No issues found.

⚠️ **Lint** - 1 warning
  • ⚠️ linter found issues (exit code 1)
✅ **Push** - passed

✅ No issues found.

@Freudator86
Freudator86 merged commit a740636 into main Sep 24, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants