Skip to content

fix(bin): namespace per-task temp root by uid - #5439

Closed
Thoughts-One wants to merge 4 commits into
kunchenguid:mainfrom
Thoughts-One:fm/up-tmp-dir-root
Closed

Thoughts-One wants to merge 4 commits into
kunchenguid:mainfrom
Thoughts-One:fm/up-tmp-dir-root

Conversation

@Thoughts-One

@Thoughts-One Thoughts-One commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Intent

Fix a defect: a stale /tmp/fm- directory owned by another local account blocks every later spawn of that task id. Namespace the per-task temp root by uid so each account gets its own root.

The task temp root becomes /tmp/fm--, with the existing 0700, owner, and not-writable-by-others checks unchanged.

What Changed

  • fm-spawn.sh now creates the per-task temp root at /tmp/fm-<uid>-<id>/ (the numeric uid from id -u). Before this change it used /tmp/fm-<id>/. That path was shared by all accounts, so a stale /tmp/fm-<id> that another local account owned blocked every later spawn of that id. The existing 0700, owner, and not-writable-by-others checks on reuse did not change. The comments in fm-teardown.sh now give the new path. Teardown still removes the path that is recorded in tasktmp=.
  • The test fixtures in the backend, orca, claude-trust, control-relaunch, gotmp, kimi-harness, and session-start suites now expect the uid-namespaced path.
  • A new kimi-harness test puts a world-writable directory at the old /tmp/fm-<id> path. It then checks that spawn succeeds, records and creates its own private (0700) uid-namespaced root, and does not change the old directory.

Related: #3457 (closed stale), #3483 (closed).

Risk Assessment

✅ Low: The change is a narrow one-line path change: TASK_TMP becomes /tmp/fm-$(id -u)-$ID. The existing 0700, owner, and not-writable-by-others checks are unchanged. Teardown reads the recorded tasktmp= value, so it follows the new path with no other change. The fix round did what was asked: it removed the uid guard and updated the promoted-scout fixture path and its cleanup registration. The new regression test runs a real spawn against a world-writable legacy /tmp/fm- directory. The old code refused that spawn and the new code accepts it.

Testing

I drove the real fm-spawn and fm-teardown in a throwaway fm-lab Herdr session, first at base and then at target. The blocker was a /tmp/fm-<id> owned by another uid (524288). Base reproduces the defect: fm-spawn refuses with "task temp root ... is not a private directory owned by this user". Target spawns cleanly: meta records tasktmp=/tmp/fm-1000-<id>, the root is 0700 and owned by uid 1000, the pane prints GOTMPDIR=/tmp/fm-1000-<id>/gotmp, and the foreign directory is not changed. Adversarial checks still hold: a 0777 directory or a symlink at /tmp/fm-<uid>-<id> is refused as before. Teardown removes the uid-namespaced root and leaves the other account's directory. The targeted suites fm-kimi-harness (31 ok) and fm-gotmp (3 ok) pass. fm-control-relaunch gets past the step where base fails on a real uid-1001 /tmp/fm-sm3 on this host. It then fails on one unrelated case because the installed tasks-axi is 0.2.5 and that case needs 0.2.6 or newer. Before this check, a refused spawn has already created a Herdr tab, and this ordering is the same at base. Both lab sessions were torn down, the default session was not touched, and the worktree is clean.

  • Live validation: ✅ go - 4 of 5 scenarios driven live against the product
Scenario Result Live Evidence
A stale /tmp/fm-<id> owned by another local account no longer blocks a spawn of that id (fails at base, passes at target) ✅ pass live live-base-9296f9b.log (rc=1, refusal) vs live-target-f09098b.log (rc=0); the blocker is uid 524288, created with podman unshare
Spawned task gets a private /tmp/fm-<uid>-<id> root recorded in meta and exported as GOTMPDIR in the Herdr pane, and the other account's directory is not changed ✅ pass live live-target-f09098b.log: tasktmp=/tmp/fm-1000-<id>, mode 700 uid 1000, pane shows GOTMPDIR=/tmp/fm-1000-<id>/gotmp, legacy directory still uid 524288 mode 755
Adversarial: a pre-planted world-writable directory or symlink at /tmp/fm-<uid>-<id> is still refused by the existing 0700, owner, and not-writable-by-others checks ✅ pass live live-target-f09098b.log scenario 2: 0777 directory gives rc=1 and the directory is not tightened, symlink gives rc=1, and no meta is written
fm-teardown removes the uid-namespaced temp root and does not touch the other account's legacy directory ✅ pass live live-target-f09098b.log scenario 3: fm-teardown rc=0, /tmp/fm-1000-<id> removed, foreign directory still present
Updated automated tests pass (kimi harness spawn subprocess including the new stale-legacy test; gotmp teardown) ⏸️ untested no The prior payload recorded this scenario as live=false: the automated test suites (fm-kimi-harness 31 ok, fm-gotmp 3 ok) are not a live run against the product, so the prior payload did not establish…
Evidence: Live Herdr-lab e2e script (foreign-uid blocker, podman unshare)

Source: Live Herdr-lab e2e script (foreign-uid blocker, podman unshare)

#!/usr/bin/env bash
# Live e2e: real bin/fm-spawn.sh + bin/fm-teardown.sh on an isolated fm-lab-* Herdr
# session. A stale /tmp/fm-<id> truly owned by ANOTHER uid (subordinate uid via
# podman unshare) stands in the way. Usage: <script> <checkout-root> <label>
set -u
ROOT=$(cd "$1" && pwd); LABEL=$2
WT_ROOT=~/.no-mistakes/worktrees/7605832e1988/01M37E1WM59T8HSCSHJH7MT2AH
. "$WT_ROOT/tests/herdr-test-safety.sh"
herdr_forget_inherited_pane
say() { printf '[%s] %s\n' "$LABEL" "$*"; }
TMP_ROOT=$(mktemp -d "/tmp/fm-live-uidtmp.XXXXXX")
HELPER="$WT_ROOT/bin/fm-herdr-lab.sh"
SESSION=$("$HELPER" name uidtmp) || exit 1
export HERDR_SESSION=$SESSION
UID_=$(id -u); SFX=$$
ID1=uidtmpA$SFX; ID2=uidtmpB$SFX
LEGACY1=/tmp/fm-$ID1; NEW1=/tmp/fm-$UID_-$ID1; NEW2=/tmp/fm-$UID_-$ID2
WORKTREES=()
cleanup() {
  for wt in ${WORKTREES[@]+"${WORKTREES[@]}"}; do treehouse return --force "$wt" >/dev/null 2>&1; done
  "$HELPER" teardown "$SESSION"; say "lab teardown rc=$?"
  podman unshare rm -rf "$LEGACY1" 2>/dev/null; rm -rf "$NEW1" "$NEW2" "$TMP_ROOT"
}
trap cleanup EXIT
"$HELPER" provision "$SESSION" || { say "provision failed"; exit 1; }
say "lab session: $SESSION"
lab() { "$HELPER" run "$SESSION" "$@"; }
HOME_=$TMP_ROOT/home; mkdir -p "$HOME_/state" "$HOME_/config"
printf 'off\n' > "$HOME_/config/herdr-presentation-spaces"
PROJ=$TMP_ROOT/proj; mkdir -p "$PROJ"; git -C "$PROJ" init -q; echo x > "$PROJ/README.md"
git -C "$PROJ" add README.md; git -C "$PROJ" -c user.name=t -c user.email=t@e.invalid commit -qm i
git clone -q --bare "$PROJ" "$PROJ.origin.git"; git -C "$PROJ" remote add origin "file://$PROJ.origin.git"
for id in $ID1 $ID2; do mkdir -p "$HOME_/data/$id"; printf '# Task\n## Captain'"'"'s intent\nx\n\n## Firstmate spec\ny\n' > "$HOME_/data/$id/brief.md"; done
spawn() { env -u HERDR_ENV -u HERDR_PANE_ID -u HERDR_SOCKET_PATH HERDR_SESSION="$SESSION" FM_SPAWN_NO_GUARD=1 \
  FM_HOME="$HOME_" FM_ROOT_OVERRIDE="$ROOT" "$ROOT/bin/fm-spawn.sh" "$1" "$PROJ" \
  "sh -c 'echo GOTMPDIR=\$GOTMPDIR; sleep 600'" --backend herdr --mode no-mistakes --yolo off; }

echo "=== Scenario 1: stale /tmp/fm-<id> owned by another local account ==="
mkdir "$LEGACY1" && podman unshare chown 1:1 "$LEGACY1"
say "planted: $(stat -c 'uid=%u mode=%a %n' "$LEGACY1")  (this account uid=$UID_)"
spawn "$ID1" >"$TMP_ROOT/s1.out" 2>"$TMP_ROOT/s1.err"; rc=$?
say "fm-spawn rc=$rc"; sed 's/^/  stderr: /' "$TMP_ROOT/s1.err" | tail -5
META=$HOME_/state/$ID1.meta
if [ "$rc" -eq 0 ]; then
  wt=$(grep '^worktree=' "$META" | cut -d= -f2-); [ -n "$wt" ] && WORKTREES+=("$wt")
  say "meta: $(grep '^tasktmp=' "$META")"
  say "root: $(stat -c 'uid=%u mode=%a %n' "$NEW1")  gotmp: $(stat -c 'mode=%a %n' "$NEW1/gotmp")"
  say "legacy after spawn: $(stat -c 'uid=%u mode=%a %n' "$LEGACY1")"
  pane=$(grep '^herdr_pane_id=' "$META" | cut -d= -f2-); sleep 2
  say "pane output: $(lab pane read "$pane" 2>/dev/null | jq -r '.result.text // .result.content // empty' 2>/dev/null | grep -m1 GOTMPDIR || lab pane read "$pane" 2>&1 | grep -o 'GOTMPDIR=[^ \"\\]*' | head -1)"
  [ "$LABEL" = target ] || exit 0
  echo "=== Scenario 3: teardown removes the uid-namespaced root, leaves foreign dir ==="
  FM_ROOT_OVERRIDE="$ROOT" FM_STATE_OVERRIDE="$HOME_/state" FM_DATA_OVERRIDE="$HOME_/data" FM_CONFIG_OVERRIDE="$HOME_/config" \
    "$ROOT/bin/fm-teardown.sh" "$ID1" >"$TMP_ROOT/td.out" 2>&1; say "fm-teardown rc=$?"
  WORKTREES=()
  [ -e "$NEW1" ] && say "FAIL: $NEW1 still exists" || say "$NEW1 removed"
  say "foreign legacy dir still: $(stat -c 'uid=%u %n' "$LEGACY1")"
else
  exit 0
fi

echo "=== Scenario 2 (adversarial): unsafe pre-planted /tmp/fm-<uid>-<id> still refused ==="
mkdir "$NEW2"; chmod 777 "$NEW2"
say "planted: $(stat -c 'uid=%u mode=%a %n' "$NEW2")"
panes_before=$(lab pane list 2>/dev/null | jq '[.result.panes[]?] | length')
spawn "$ID2" >"$TMP_ROOT/s2.out" 2>"$TMP_ROOT/s2.err"; rc=$?
say "fm-spawn rc=$rc"; sed 's/^/  stderr: /' "$TMP_ROOT/s2.err" | tail -3
say "meta exists: $([ -f "$HOME_/state/$ID2.meta" ] && echo yes || echo no); panes before=$panes_before after=$(lab pane list 2>/dev/null | jq '[.result.panes[]?] | length')"
say "dir after: $(stat -c 'mode=%a %n' "$NEW2")"
rm -rf "$NEW2"; ln -s "$TMP_ROOT" "$NEW2"
spawn "$ID2" >"$TMP_ROOT/s3.out" 2>"$TMP_ROOT/s3.err"; rc=$?
say "symlink variant: fm-spawn rc=$rc"; sed 's/^/  stderr: /' "$TMP_ROOT/s3.err" | tail -2
rm -f "$NEW2"
Evidence: Base 9296f9b reproduces the defect: spawn refused by foreign-owned /tmp/fm-<id>

Source: Base 9296f9b reproduces the defect: spawn refused by foreign-owned /tmp/fm-<id>

[base] planted: uid=524288 mode=755 /tmp/fm-uidtmpA713795 (this account uid=1000) [base] fm-spawn rc=1 stderr: error: task temp root /tmp/fm-uidtmpA713795 already exists and is not a private directory owned by this user; refusing to stage the launch command there

[base] lab session: fm-lab-uidtmp-713832-20969
=== Scenario 1: stale /tmp/fm-<id> owned by another local account ===
[base] planted: uid=524288 mode=755 /tmp/fm-uidtmpA713795  (this account uid=1000)
[base] fm-spawn rc=1
  stderr: warning: /tmp/fm-live-uidtmp.XDftUM/home/data/uidtmpA713795/launch-brief.md records no delivery contract line (scaffolded before ship briefs recorded one); launching on the explicit --mode no-mistakes - confirm its definition of done matches
  stderr: error: task temp root /tmp/fm-uidtmpA713795 already exists and is not a private directory owned by this user; refusing to stage the launch command there; inspect and remove it, then retry
[base] lab teardown rc=0
Evidence: Target f09098b: spawn succeeds, 0700 uid root, GOTMPDIR in pane, teardown cleans, unsafe roots still refused

Source: Target f09098b: spawn succeeds, 0700 uid root, GOTMPDIR in pane, teardown cleans, unsafe roots still refused

[target] planted: uid=524288 mode=755 /tmp/fm-uidtmpA742672 [target] fm-spawn rc=0 [target] meta: tasktmp=/tmp/fm-1000-uidtmpA742672 [target] root: uid=1000 mode=700 /tmp/fm-1000-uidtmpA742672 [target] legacy after spawn: uid=524288 mode=755 /tmp/fm-uidtmpA742672 [target] pane output: GOTMPDIR=/tmp/fm-1000-uidtmpA742672/gotmp [target] fm-teardown rc=0 [target] /tmp/fm-1000-uidtmpA742672 removed [target] planted: uid=1000 mode=777 /tmp/fm-1000-uidtmpB742672 [target] fm-spawn rc=1 (refused: not a private directory owned by this user) [target] symlink variant: fm-spawn rc=1 (refused)

[target] lab session: fm-lab-uidtmp-742733-10982
=== Scenario 1: stale /tmp/fm-<id> owned by another local account ===
[target] planted: uid=524288 mode=755 /tmp/fm-uidtmpA742672  (this account uid=1000)
[target] fm-spawn rc=0
  stderr: warning: /tmp/fm-live-uidtmp.2JxxmY/home/data/uidtmpA742672/launch-brief.md records no delivery contract line (scaffolded before ship briefs recorded one); launching on the explicit --mode no-mistakes - confirm its definition of done matches
[target] meta: tasktmp=/tmp/fm-1000-uidtmpA742672
[target] root: uid=1000 mode=700 /tmp/fm-1000-uidtmpA742672  gotmp: mode=755 /tmp/fm-1000-uidtmpA742672/gotmp
[target] legacy after spawn: uid=524288 mode=755 /tmp/fm-uidtmpA742672
[target] pane output: GOTMPDIR=/tmp/fm-1000-uidtmpA742672/gotmp
=== Scenario 3: teardown removes the uid-namespaced root, leaves foreign dir ===
[target] fm-teardown rc=0
[target] /tmp/fm-1000-uidtmpA742672 removed
[target] foreign legacy dir still: uid=524288 /tmp/fm-uidtmpA742672
=== Scenario 2 (adversarial): unsafe pre-planted /tmp/fm-<uid>-<id> still refused ===
[target] planted: uid=1000 mode=777 /tmp/fm-1000-uidtmpB742672
[target] fm-spawn rc=1
  stderr: warning: /tmp/fm-live-uidtmp.2JxxmY/home/data/uidtmpB742672/launch-brief.md records no delivery contract line (scaffolded before ship briefs recorded one); launching on the explicit --mode no-mistakes - confirm its definition of done matches
  stderr: error: task temp root /tmp/fm-1000-uidtmpB742672 already exists and is not a private directory owned by this user; refusing to stage the launch command there; inspect and remove it, then retry
[target] meta exists: no; panes before=0 after=1
[target] dir after: mode=777 /tmp/fm-1000-uidtmpB742672
[target] symlink variant: fm-spawn rc=1
  stderr: warning: /tmp/fm-live-uidtmp.2JxxmY/home/data/uidtmpB742672/launch-brief.md records no delivery contract line (scaffolded before ship briefs recorded one); launching on the explicit --mode no-mistakes - confirm its definition of done matches
  stderr: error: task temp root /tmp/fm-1000-uidtmpB742672 already exists and is not a private directory owned by this user; refusing to stage the launch command there; inspect and remove it, then retry
[target] lab teardown rc=0
Evidence: fm-kimi-harness test run

Source: fm-kimi-harness test run

ok - Kimi hook install is idempotent and removal restores every foreign config byte
ok - Kimi hook removal preserves owned newline boundaries and pristine bytes
ok - Kimi hook install refuses missing, malformed, and surprising config without writing
ok - Kimi hook install refuses without jq before any config write
ok - fm-spawn: kimi launches, delivers its brief, and registers a guarded turn-end token
ok - fm-spawn: unsafe task roots are refused, owned roots are tightened, and launch files stay unique and 0600
ok - fm-spawn: a stale directory at the old account-agnostic temp path never blocks or is touched by a uid-namespaced spawn
ok - Kimi hook stays silent and inert without a Firstmate registry token
ok - fm-spawn: unsafe Kimi global config refuses before pane creation
ok - fm-teardown: Kimi task pointer and registry token are removed
ok - fm-spawn: Kimi fallback expands the active HOME
ok - fm-spawn: missing Kimi executable refuses before pane creation
ok - fm-spawn: kimi treats a silent pointer drop as a failed spawn
ok - fm-spawn: kimi never sends the brief pointer before an observable ready signal
ok - fm-spawn: a fresh Kimi worktree answers the exact trust dialog once and verifies advancement
ok - fm-spawn: a swallowed Kimi trust keypress is re-sent until the dialog clears
ok - fm-spawn: a Kimi banner captured before the trust dialog paints does not read as ready
ok - fm-spawn: an answered Kimi trust dialog left in scrollback neither re-answers nor fails the spawn
ok - fm-spawn: a blank Kimi viewport frame costs its poll and nothing else
ok - fm-spawn: Kimi refuses a backend that cannot read the viewport, before launching
ok - fm-spawn: a Kimi trust dialog with its hint wrapped across rows is answered normally
ok - fm-spawn: a blank Kimi frame between banners restarts the two-capture ready count
ok - fm-spawn: a failed Kimi viewport read fails readiness with the backend named
ok - fm-spawn: Kimi refuses the ready verdict while trust dialog markers remain
ok - fm-spawn: a Kimi trust dialog must visibly clear before brief delivery
ok - fm-spawn: Kimi trust detection requires every observed dialog signal
ok - fm-harness: markerless kimi keeps its ancestry identity under an inherited marker
lock acquired: harness pid 1151144
ok - fm-lock recognizes Kimi ancestry and live lock holders
ok - busy detection: real Kimi moon-plus-middot captures require its harness while idle labels stay idle
ok - fm-watch classifies Kimi as unknown rather than from its spinner, and Grok's fallback stays isolated
ok - composer classifier: kimi's existing bordered > shape is already safe without an override
Evidence: fm-gotmp test run

Source: fm-gotmp test run

ok - fm-teardown removes the dir pointed to by tasktmp= in meta
ok - fm-teardown skips gracefully when tasktmp= is absent (backward compat)
ok - fm-teardown skips gracefully when tasktmp= points to a nonexistent dir
Evidence: fm-control-relaunch on target (one failure: tasks-axi 0.2.5 < 0.2.6 required)

Source: fm-control-relaunch on target (one failure: tasks-axi 0.2.5 < 0.2.6 required)

ok - fm-control relaunch: a same-harness relaunch replaces the agent in the same endpoint and worktree
ok - fm-control relaunch: pending composer text refuses before the exit command is typed
ok - fm-control relaunch: an unreadable composer fails safe before the exit command is typed
ok - fm-control relaunch: a linked spawning home preserves committed and unfinished work in the recorded copy
ok - fm-control relaunch: durable task metadata survives replacement launch publication
ok - fm-control relaunch: delivery and concurrent task metadata publication serialize
ok - fm-control relaunch: disabling tracing clears metadata and pane context
ok - fm-control relaunch: progress and the Firstmate-worktree worker identity reach the replacement
ok - fm-control relaunch: a ship task refuses without the progress note its replacement needs
ok - fm-control relaunch: switching harness is one ordinary relaunch, and the old wiring goes with the old agent
ok - fm-control relaunch: a harness switch resets model and effort unless they are named too
ok - fm-control relaunch: a prefixed recorded harness can switch adapters transactionally
ok - fm-control relaunch: a prefixed command requires an explicit replacement harness
ok - fm-control relaunch: a same-harness relaunch keeps the profile axes it was running with
ok - native Ultra relaunch preserves its profile and rejects an unsupported model before stopping
ok - fm-control relaunch: explicit model and effort win over the recorded ones
ok - fm-control relaunch: refuses to relaunch onto an adapter with no verified mechanics
ok - fm-control relaunch: the retired incarnation's global turn-end token is revoked
ok - fm-control relaunch: wiring cleanup failure refuses replacement arming
ok - fm-control-lib: one owner resolves each harness's turn-end registry entry, and refuses a malformed token
ok - fm-control relaunch: a secondmate relaunch re-resolves its durable configured harness pin
ok - fm-control relaunch: invalid configured effort is ignored before stop
ok - fm-control relaunch: an adapter unverified for this task kind refuses before the agent is stopped
ok - fm-control relaunch: explicit secondmate harness resets unnamed profile axes
ok - fm-control relaunch: a ship task keeps its recorded harness instead of re-reading crew config
ok - fm-spawn --relaunch: with no explicit harness it reuses the task's recorded one, never the crew default
ok - fm-promote/fm-spawn --relaunch: the current ship contract supersedes stale scout delivery text
ok - fm-spawn --relaunch: wiring armed under a prefixed harness name is still retired
ok - fm-spawn --relaunch: switching away from muse retires its session binding
ok - fm-spawn --relaunch: switching away from cursor retires its session binding
ok - fm-control relaunch: an unaccountable local copy refuses before the agent is touched
ok - fm-control relaunch: a worker with nothing to work from is never launched
ok - fm-control relaunch: a refusal before the agent is stopped leaves the durable record untouched
ok - fm-control relaunch: checkpoint inspection failures refuse before stopping
ok - fm-control relaunch: a launch failure after the stop keeps the prior record and reports the real state
ok - fm-control relaunch: unpublished rollback keeps concurrent durable metadata
ok - fm-control relaunch: post-publication failure keeps the new durable record
ok - fm-control relaunch: partial stop reconciles actual agent state
ok - fm-control relaunch: failed journal replacement preserves durable phase
ok - fm-spawn relaunch: prepublication abort removes replacement state
ok - fm-control relaunch: the checkpoint records the exact unlanded work it preserved
ok - fm-control relaunch: a secondmate's child work is accounted for and its charter is left alone
ok - fm-control relaunch: a secondmate home that is not this secondmate's is refused
ok - fm-control relaunch: unreadable child records fail checkpoint
ok - fm-control relaunch: unlistable state fails checkpoint
ok - fm-control relaunch: a vanished watcher scratch file does not fail the child-record checkpoint
ok - fm-control relaunch: two control actions on one task serialize instead of interleaving
ok - fm-spawn relaunch: direct entry participates in lifecycle serialization
ok - fm-promote: promotion participates in lifecycle serialization
ok - fm-spawn --relaunch: refuses to launch a second agent into a live endpoint
ok - fm-spawn --relaunch: symlinked records refuse before inspection
ok - fm-spawn --relaunch: keeps its early meta lock continuous
ok - fm-spawn --relaunch: pending closes refuse before replacement begins
ok - fm-spawn --relaunch: every identity axis comes from the record, and a contradicting flag refuses
ok - fm-spawn --relaunch: an unrecorded task is refused
ok - fm-spawn --relaunch: refuses to start a replacement outside the copy holding its work
ok - tmux: a window absent from its session refuses both verbs rather than being assumed gone
ok - tmux: an unfindable session refuses both verbs, so a live agent is never duplicated
ok - tmux: a dead server on this socket refuses both verbs rather than proving absence
ok - reclaim: an unclassifiable endpoint is still refused, so two agents cannot share one
ok - reclaim: a herdr pane that outlived its stopped server is adopted, never orphaned beside a new tab
ok - fm-control exit: a herdr pane that outlived its stopped server is already-stopped, not gone
ok - reclaim: a herdr rebind is created in the session the record names, never the ambient one
ok - reclaim: a herdr agent that came back with its server refuses, so one worktree keeps one agent
ok - reclaim: a herdr reclaim rebinds the endpoint and leaves the whole rest of the task alone
ok - reclaim: a herdr secondmate whose endpoint is gone is sent to its own respawn owner
ok - reclaim: a rebind refused from a plain shell reports the real cause, not a fabricated session mismatch
not ok - a relaunch must not re-run a transition the row already reflects
warning: /tmp/fm-control-relaunch.UWZwf6/reverify-4120/home/data/rl40/launch-brief.md records no delivery contract line (scaffolded before ship briefs recorded one); launching on the explicit --mode no-mistakes - confirm its definition of done matches
error: task rl40 cannot be dispatched because its backlog data directory is inaccessible: /tmp/fm-control-relaunch.UWZwf6/reverify-4120/home/data (automatic backlog transitions require tasks-axi 0.2.6 or newer with the required update and mv features)
error: the replacement agent for rl40 could not be launched on claude
error: rl40's agent was stopped but the replacement did not launch; no agent is running, and its work plus the recorded progress note are preserved at /tmp/fm-control-relaunch.UWZwf6/reverify-4120/wt: expected exit 0, got 1
Evidence: fm-control-relaunch on base: fails on the defect itself (/tmp/fm-sm3 owned by uid 1001 on this host)

Source: fm-control-relaunch on base: fails on the defect itself (/tmp/fm-sm3 owned by uid 1001 on this host)

ok - fm-control relaunch: a same-harness relaunch replaces the agent in the same endpoint and worktree
ok - fm-control relaunch: pending composer text refuses before the exit command is typed
ok - fm-control relaunch: an unreadable composer fails safe before the exit command is typed
ok - fm-control relaunch: a linked spawning home preserves committed and unfinished work in the recorded copy
ok - fm-control relaunch: durable task metadata survives replacement launch publication
ok - fm-control relaunch: delivery and concurrent task metadata publication serialize
ok - fm-control relaunch: disabling tracing clears metadata and pane context
ok - fm-control relaunch: progress and the Firstmate-worktree worker identity reach the replacement
ok - fm-control relaunch: a ship task refuses without the progress note its replacement needs
ok - fm-control relaunch: switching harness is one ordinary relaunch, and the old wiring goes with the old agent
ok - fm-control relaunch: a harness switch resets model and effort unless they are named too
ok - fm-control relaunch: a prefixed recorded harness can switch adapters transactionally
ok - fm-control relaunch: a prefixed command requires an explicit replacement harness
ok - fm-control relaunch: a same-harness relaunch keeps the profile axes it was running with
ok - native Ultra relaunch preserves its profile and rejects an unsupported model before stopping
ok - fm-control relaunch: explicit model and effort win over the recorded ones
ok - fm-control relaunch: refuses to relaunch onto an adapter with no verified mechanics
ok - fm-control relaunch: the retired incarnation's global turn-end token is revoked
ok - fm-control relaunch: wiring cleanup failure refuses replacement arming
ok - fm-control-lib: one owner resolves each harness's turn-end registry entry, and refuses a malformed token
not ok - a configured secondmate harness should relaunch
fatal: not a git repository (or any of the parent directories): .git
fatal: not a git repository (or any of the parent directories): .git
warning: secondmate sm3 sync skipped before launch: primary default-branch commit cannot be resolved
error: task temp root /tmp/fm-sm3 already exists and is not a private directory owned by this user; refusing to stage the launch command there; inspect and remove it, then retry
error: the replacement agent for sm3 could not be launched on codex
error: sm3's agent was stopped but the replacement did not launch; no agent is running, and its work plus the recorded progress note are preserved at /tmp/fm-control-relaunch.ceIqJc/smpin-7130/smhome: expected exit 0, got 1
- Outcome: ⚠️ 1 info across 1 run (8m25s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 3 issues found → auto-fixed ✅
  • ⚠️ bin/fm-spawn.sh:4011 - The change adds a new failure path: if id -u fails or prints a non-numeric value, fm-spawn refuses to spawn. The intent only asks for the temp root to become /tmp/fm-<uid>-<id>, with the existing 0700, owner, and not-writable-by-others checks unchanged. id -u is POSIX and effectively never fails, and the existing ownership checks still protect a degraded path like /tmp/fm--<id>. So this guard goes beyond the narrow PR the intent asks for. The smallest honest remedy is to remove it and use TASK_TMP=&#34;/tmp/fm-$(id -u)-$ID&#34; directly. Keep the guard only if you want it as defense in depth.
  • ℹ️ bin/fm-spawn.sh:4018 - Transition edge case: a task spawned before this change has tasktmp=/tmp/fm-&lt;id&gt; in its meta. On fm-spawn --relaunch, TASK_TMP is recomputed, and preserve_relaunch_meta rewrites tasktmp= to the new /tmp/fm-<uid>-<id>. After that, fm-teardown removes only the new root, and the old root, owned by this user, is left in /tmp. The leak is small, one-time, and limited to tasks that were live during the upgrade. Fixing it would mean extra migration logic, which goes beyond the narrow scope the intent asks for. It is noted here only for awareness.
  • ℹ️ tests/fm-control-relaunch.test.sh:1048 - The promoted-scout relaunch fixture still writes tasktmp=/tmp/fm-$id, but the other fixtures in this file were moved to /tmp/fm-$(id -u)-$id. This loop also runs run_spawn ... --relaunch, which creates /tmp/fm-$(id -u)-rl-promoted-&lt;mode&gt;, and it never adds that path to TASK_TMPS, so each test run leaves three directories in /tmp. The leak existed before this change under the old path, but the fixture is now inconsistent with the new format. Fix: update the fixture line to /tmp/fm-$(id -u)-$id and add TASK_TMPS+=(&#34;/tmp/fm-$(id -u)-$id&#34;) in the loop.

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

⚠️ **Test** - 1 info
  • ℹ️ tests/fm-control-relaunch.test.sh - tests/fm-control-relaunch.test.sh fails one case locally ('a relaunch must not re-run a transition the row already reflects') because the installed tasks-axi is 0.2.5 and that case needs 0.2.6 or newer. The case is not related to the temp root. On base, the same suite fails earlier: a real uid-1001 /tmp/fm-sm3 on this host blocks it, which is the defect this change fixes. Remote CI owns the full run.
  • Live validation: ✅ go - 4 of 5 scenarios driven live against the product
Scenario Result Live Evidence
A stale /tmp/fm-<id> owned by another local account no longer blocks a spawn of that id (fails at base, passes at target) ✅ pass live live-base-9296f9b.log (rc=1, refusal) vs live-target-f09098b.log (rc=0); the blocker is uid 524288, created with podman unshare
Spawned task gets a private /tmp/fm-<uid>-<id> root recorded in meta and exported as GOTMPDIR in the Herdr pane, and the other account's directory is not changed ✅ pass live live-target-f09098b.log: tasktmp=/tmp/fm-1000-<id>, mode 700 uid 1000, pane shows GOTMPDIR=/tmp/fm-1000-<id>/gotmp, legacy directory still uid 524288 mode 755
Adversarial: a pre-planted world-writable directory or symlink at /tmp/fm-<uid>-<id> is still refused by the existing 0700, owner, and not-writable-by-others checks ✅ pass live live-target-f09098b.log scenario 2: 0777 directory gives rc=1 and the directory is not tightened, symlink gives rc=1, and no meta is written
fm-teardown removes the uid-namespaced temp root and does not touch the other account's legacy directory ✅ pass live live-target-f09098b.log scenario 3: fm-teardown rc=0, /tmp/fm-1000-<id> removed, foreign directory still present
Updated automated tests pass (kimi harness spawn subprocess including the new stale-legacy test; gotmp teardown) ⏸️ untested no The prior payload recorded this scenario as live=false: the automated test suites (fm-kimi-harness 31 ok, fm-gotmp 3 ok) are not a live run against the product, so the prior payload did not establish…
  • live-foreign-tmp-e2e.sh &lt;base-9296f9b-tree&gt; base: real bin/fm-spawn.sh --backend herdr on a new isolated fm-lab-uidtmp-* session provisioned and torn down through bin/fm-herdr-lab.sh, with /tmp/fm-<id> set to uid 524288 using podman unshare chown
  • live-foreign-tmp-e2e.sh &lt;worktree&gt; target: the same foreign-owned blocker, then fm-spawn, a check of the meta, directory mode and owner, and the pane's GOTMPDIR, then fm-teardown, then an unsafe 0777 directory and a symlink at /tmp/fm-<uid>-<id>
  • bash tests/fm-kimi-harness.test.sh (includes the new test_kimi_spawn_ignores_stale_legacy_shared_temp_root)
  • bash tests/fm-gotmp.test.sh
  • bash tests/fm-control-relaunch.test.sh on target and on the extracted base tree, to compare the results
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

A stale /tmp/fm-<id> directory left behind by another local account could
block every task with that id, since the temp root's path did not depend
on which account was spawning. Fold the spawning account's numeric uid
into the path (/tmp/fm-<uid>-<id>) so two accounts can never compute the
same root, keeping every existing safety guarantee (private 0700 root,
safe reuse only of a real directory owned by this user and writable by
nobody else, refusal of anything else) intact.

Update fm-teardown.sh's comments and every test that hardcoded the old
path to match, and add a regression test asserting a stale directory at
the old account-agnostic path no longer blocks or is touched by spawn.
add_ship_task and add_herdr_ship_task still registered /tmp/fm-$id for
cleanup after relaunch, while fm-spawn now creates /tmp/fm-<uid>-$id,
leaking a temp directory on every relaunch case run.
@Thoughts-One

Copy link
Copy Markdown
Contributor Author

Superseded by #5987, which carries this branch's full content rebased onto a fresh head (the #5433->#5830 supersede pattern). Closing this PR in favor of that one.

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.

1 participant