From 1e0beeddf31857cbc5ad88a6e8f2f19ed94d6eb9 Mon Sep 17 00:00:00 2001 From: JTInventory Date: Thu, 13 Aug 2026 16:04:13 +0000 Subject: [PATCH 1/5] fix: isolate Herdr crew session --- AGENTS.md | 2 + bin/backends/herdr.sh | 132 ++++++++++-- docs/worker-isolation.md | 10 +- .../fm-backend-herdr-presentation-e2e.test.sh | 194 +++++++++--------- 4 files changed, 214 insertions(+), 124 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 70fd3cff8f4..2f2d59efe00 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -373,6 +373,8 @@ Project worktrees start at detached HEAD on a clean default branch; ship briefs After spawning, peek the pane to confirm the crewmate is processing the brief and handle any trust dialog with `harness-adapters`. Add the task to `data/backlog.md` under In flight. +Herdr crew dispatch has an additional session boundary. New `backend=herdr` tasks use the dedicated Herdr session `firstmate`; they never create or close panes in Herdr's default session or its `CAPTAIN`/`w1` workspace. Metadata must retain the exact Herdr session, workspace, tab, and pane ids returned by the provider. If the host lacks `pane.close_bound`, teardown may use legacy `pane.close` only in `firstmate`, after matching the recorded pane's foreground PID and `/proc` start time; an unproven identity or any captain-session target is a refusal. + ### Supervise Covered by section 8. diff --git a/bin/backends/herdr.sh b/bin/backends/herdr.sh index 2dd93fb4cd8..e5511de48f8 100755 --- a/bin/backends/herdr.sh +++ b/bin/backends/herdr.sh @@ -13,7 +13,7 @@ # per task inside that workspace. # Target resolution stays parallel to the tmux adapter in both layouts. # -# Target string shape: ":", e.g. "default:w1:p2" (the +# Target string shape: ":", e.g. "firstmate:ws1:p2" (the # pane id itself contains a colon; the session is always the FIRST field, the # remainder is the whole pane id - fm_backend_herdr_parse_target splits on the # first colon only). This is the value stored in a herdr task's meta window= @@ -67,6 +67,7 @@ FM_BACKEND_HERDR_MIN_PROTOCOL=14 # (14): the adapter's spawn/capture/send primitives work on 14, only the push # subscriber needs 16. FM_BACKEND_HERDR_MIN_EVENTS_PROTOCOL=16 +FM_BACKEND_HERDR_DEDICATED_SESSION=firstmate # Per-pane escalation dedupe marker prefix, under the state dir. One marker per # window (keyed like the watcher's own .stale-): set when a ->blocked edge # is enqueued, cleared on any working edge, so exactly one wake fires per @@ -240,15 +241,25 @@ fm_backend_herdr_bound_tab_close_capable() { 'workspace_id:string,tab_id:string,pane_id:string' } +fm_backend_herdr_legacy_close_capable() { + local schema + schema=$(herdr api schema --json 2>/dev/null) || return 1 + printf '%s' "$schema" | jq -e ' + [.schemas.request.oneOf[]?.properties.method.const] as $methods + | ($methods | index("pane.close") != null) + and ($methods | index("tab.close") != null) + ' >/dev/null 2>&1 +} + # fm_backend_herdr_version_check: refuse loudly on a missing/incompatible -# herdr client or missing bound-mutation capabilities. Verified locally: v0.7.1, +# herdr client or missing mutation capabilities. Verified locally: v0.7.1, # protocol 14 (herdr status --json's .client.protocol; client info is -# session-independent, unlike .server). Live task teardown requires -# pane.close_bound(expected_pid, expected_start_time); workspace/task-tab -# reconciliation requires tab.close_bound(workspace_id, tab_id, pane_id). +# session-independent, unlike .server). The isolated firstmate session may +# fall back to identity-checked pane.close/tab.close on hosts that lack the +# newer bound methods; every other session still requires both bound methods. fm_backend_herdr_version_check() { fm_backend_herdr_tool_check || return 1 - local status protocol version + local status protocol version session pane_bound tab_bound status=$(herdr status --json 2>/dev/null) || { echo "error: 'herdr status --json' failed; is herdr installed correctly?" >&2; return 1; } protocol=$(printf '%s' "$status" | jq -r '.client.protocol // empty' 2>/dev/null) version=$(printf '%s' "$status" | jq -r '.client.version // empty' 2>/dev/null) @@ -262,14 +273,33 @@ fm_backend_herdr_version_check() { echo "error: herdr protocol $protocol (version ${version:-unknown}) is older than the verified minimum $FM_BACKEND_HERDR_MIN_PROTOCOL; update herdr (herdr update) before using backend=herdr" >&2 return 1 fi - if ! fm_backend_herdr_bound_close_capable; then + session=$(fm_backend_herdr_session) || return 1 + if fm_backend_herdr_bound_close_capable; then + pane_bound=1 + else + pane_bound=0 + fi + if fm_backend_herdr_bound_tab_close_capable; then + tab_bound=1 + else + tab_bound=0 + fi + if [ "$pane_bound" != 1 ] \ + && [ "$session" != "$FM_BACKEND_HERDR_DEDICATED_SESSION" ]; then echo "error: herdr provider lacks atomic pane.close_bound(expected_pid); refusing a backend that cannot safely finish live task teardown" >&2 return 1 fi - if ! fm_backend_herdr_bound_tab_close_capable; then + if [ "$tab_bound" != 1 ] \ + && [ "$session" != "$FM_BACKEND_HERDR_DEDICATED_SESSION" ]; then echo "error: herdr provider lacks atomic tab.close_bound(workspace_id,tab_id,pane_id); refusing a backend that cannot safely reconcile task-tab creation" >&2 return 1 fi + if [ "$session" = "$FM_BACKEND_HERDR_DEDICATED_SESSION" ] \ + && { [ "$pane_bound" != 1 ] || [ "$tab_bound" != 1 ]; } \ + && ! fm_backend_herdr_legacy_close_capable; then + echo "error: isolated Herdr session lacks pane.close/tab.close; refusing unbound cleanup" >&2 + return 1 + fi return 0 } @@ -280,14 +310,17 @@ fm_backend_herdr_server_available() { # [ "$running" = true ] } -# fm_backend_herdr_session: resolve which named herdr session this normal -# spawn/op uses. HERDR_SESSION mirrors tmux's $TMUX ambient-selection for -# adapter workspace/tab/pane operations: an operator (or firstmate's own -# isolated test harness) sets it explicitly; absent means herdr's own -# "default" session. Do not use HERDR_SESSION alone for destructive test -# cleanup; tests/herdr-test-safety.sh documents and guards that path. +# fm_backend_herdr_session: resolve which named Herdr session a new normal +# spawn uses. Firstmate must never default to Herdr's captain-owned session; +# the isolated session is the only default. HERDR_SESSION remains an explicit +# selector for focused tests and recovery of already-recorded endpoints. fm_backend_herdr_session() { - printf '%s' "${HERDR_SESSION:-default}" + local session=${HERDR_SESSION:-$FM_BACKEND_HERDR_DEDICATED_SESSION} + if [ "$session" = default ]; then + echo "error: normal Herdr crew dispatch cannot target the captain-owned default session; use '$FM_BACKEND_HERDR_DEDICATED_SESSION'" >&2 + return 1 + fi + printf '%s' "$session" } fm_backend_herdr_provider_close_bound() { @@ -306,7 +339,14 @@ fm_backend_herdr_provider_close_tab_bound() { [ -n "$session" ] && [ -n "$workspace_id" ] && [ -n "$tab_id" ] && [ -n "$pane_id" ] || return 1 socket=$(fm_backend_herdr_socket_path "$session") || return 1 [ -x "$helper" ] || return 1 - "$helper" "$socket" --tab "$workspace_id" "$tab_id" "$pane_id" >/dev/null 2>&1 + if fm_backend_herdr_bound_tab_close_capable; then + "$helper" "$socket" --tab "$workspace_id" "$tab_id" "$pane_id" >/dev/null 2>&1 + return $? + fi + [ "$session" = "$FM_BACKEND_HERDR_DEDICATED_SESSION" ] || return 1 + fm_backend_herdr_tab_pane_identity_matches \ + "$session" "$workspace_id" "$tab_id" "$pane_id" || return 1 + fm_backend_herdr_cli "$session" tab close "$tab_id" >/dev/null 2>&1 } fm_backend_herdr_server_ensure() { # @@ -1839,10 +1879,58 @@ fm_backend_herdr_create_task() { #