diff --git a/bin/fm-backend.sh b/bin/fm-backend.sh index e505b99f75..50be95f499 100644 --- a/bin/fm-backend.sh +++ b/bin/fm-backend.sh @@ -452,10 +452,10 @@ fm_backend_validate_task_endpoint() { # fi ;; herdr) - [ "$binding" = "$id" ] || { - echo "REFUSED: legacy Herdr endpoint metadata for task $id lacks an exact task binding; preserving task state." >&2 + if [ -n "$binding" ] && [ "$binding" != "$id" ]; then + echo "REFUSED: Herdr endpoint metadata is bound to task $binding, not $id; preserving task state." >&2 return 1 - } + fi recorded_session=$(fm_backend_meta_exact_value "$meta" herdr_session) || recorded_session= workspace=$(fm_backend_meta_exact_value "$meta" herdr_workspace_id) || workspace= tab=$(fm_backend_meta_exact_value "$meta" herdr_tab_id) || tab= @@ -471,10 +471,10 @@ fm_backend_validate_task_endpoint() { # fi ;; zellij) - [ "$binding" = "$id" ] || { - echo "REFUSED: legacy Zellij endpoint metadata for task $id lacks an exact task binding; preserving task state." >&2 + if [ -n "$binding" ] && [ "$binding" != "$id" ]; then + echo "REFUSED: Zellij endpoint metadata is bound to task $binding, not $id; preserving task state." >&2 return 1 - } + fi recorded_session=$(fm_backend_meta_exact_value "$meta" zellij_session) || recorded_session= tab=$(fm_backend_meta_exact_value "$meta" zellij_tab_id) || tab= pane=$(fm_backend_meta_exact_value "$meta" zellij_pane_id) || pane= @@ -487,10 +487,10 @@ fm_backend_validate_task_endpoint() { # fi ;; orca) - [ "$binding" = "$id" ] || { - echo "REFUSED: legacy Orca endpoint metadata for task $id lacks an exact task binding; preserving task state." >&2 + if [ -n "$binding" ] && [ "$binding" != "$id" ]; then + echo "REFUSED: Orca endpoint metadata is bound to task $binding, not $id; preserving task state." >&2 return 1 - } + fi terminal=$(fm_backend_meta_exact_value "$meta" terminal) || terminal= worktree_id=$(fm_backend_meta_exact_value "$meta" orca_worktree_id) || worktree_id= [ -n "$terminal" ] || { @@ -510,10 +510,10 @@ fm_backend_validate_task_endpoint() { # window=$terminal ;; cmux) - [ "$binding" = "$id" ] || { - echo "REFUSED: legacy cmux endpoint metadata for task $id lacks an exact task binding; preserving task state." >&2 + if [ -n "$binding" ] && [ "$binding" != "$id" ]; then + echo "REFUSED: cmux endpoint metadata is bound to task $binding, not $id; preserving task state." >&2 return 1 - } + fi workspace=$(fm_backend_meta_exact_value "$meta" cmux_workspace_id) || workspace= surface=$(fm_backend_meta_exact_value "$meta" cmux_surface_id) || surface= if [ -z "$workspace" ] || [ -z "$surface" ] || [ "$window" != "$workspace:$surface" ] \ diff --git a/docs/configuration.md b/docs/configuration.md index 7b90f8ec15..0133d2a40f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -80,7 +80,7 @@ Only metadata-routed task selectors carry secondmate-marker and Codex-harness co These five sentences are the single owner of the task-selector vocabulary; backend guides and other documents point here instead of restating the resolution order. `fm-teardown.sh ` takes a task id directly and validates the complete metadata-only endpoint identity before any runtime dispatch or cleanup mutation. Missing, empty, duplicate, malformed, backend-inconsistent, or task-mismatched endpoint records are preserved and refused. -Legacy tmux metadata remains cleanup-compatible when its exact window name is `fm-`; opaque non-tmux endpoints require their recorded `endpoint_task_id=` binding. +Legacy tmux metadata remains cleanup-compatible when its exact window name is `fm-`; opaque non-tmux endpoints refuse only when a recorded `endpoint_task_id=` binding is present and mismatches the task id, so legacy metadata predating that field also stays cleanup-compatible. `FM_HOME` determines Herdr's home label: the primary home uses `firstmate`, and a secondmate home marked by `.fm-secondmate-home` uses `2ndmate-`. [`herdr-backend.md`](herdr-backend.md#watching-and-task-containers) owns launcher-bound workspace placement, the label-only fallback, collision handling, and recovery behavior. The optional local `config/herdr-presentation-spaces` presence flag instead enables Herdr's default-off disposable single-task visual projection; [Optional presentation spaces](herdr-backend.md#optional-presentation-spaces) owns its behavior, safety limits, recovery contract, and narrow locked session-start cleanup of exact restored idle-shell children. diff --git a/tests/fm-teardown-endpoint-safety.test.sh b/tests/fm-teardown-endpoint-safety.test.sh index 5786102cd3..4f417e260c 100755 --- a/tests/fm-teardown-endpoint-safety.test.sh +++ b/tests/fm-teardown-endpoint-safety.test.sh @@ -136,6 +136,36 @@ test_supported_backend_endpoint_records_validate() { "backend=cmux" "cmux_workspace_id=workspace-1" "cmux_surface_id=surface-2" fm_backend_validate_task_endpoint "$dir/home/state/$id.meta" "$id" || fail "valid cmux endpoint refused" + id=herdr-legacy + fm_write_meta "$dir/home/state/$id.meta" \ + "window=lab:w1:p2" "worktree=$dir/worktree" "project=$dir/project" \ + "backend=herdr" "herdr_session=lab" "herdr_workspace_id=w1" "herdr_tab_id=w1:t2" "herdr_pane_id=w1:p2" + fm_backend_validate_task_endpoint "$dir/home/state/$id.meta" "$id" || fail "legacy Herdr endpoint (no binding) refused" + [ "$FM_BACKEND_VALIDATED_BACKEND" = herdr ] || fail "legacy Herdr did not select its backend" + [ "$FM_BACKEND_VALIDATED_TARGET" = "lab:w1:p2" ] || fail "legacy Herdr target mismatch" + + id=zellij-legacy + fm_write_meta "$dir/home/state/$id.meta" \ + "window=lab:7" "worktree=$dir/worktree" "project=$dir/project" \ + "backend=zellij" "zellij_session=lab" "zellij_tab_id=3" "zellij_pane_id=7" + fm_backend_validate_task_endpoint "$dir/home/state/$id.meta" "$id" || fail "legacy Zellij endpoint (no binding) refused" + [ "$FM_BACKEND_VALIDATED_BACKEND" = zellij ] || fail "legacy Zellij did not select its backend" + + id=orca-legacy + fm_write_meta "$dir/home/state/$id.meta" \ + "window=fm-$id" "terminal=term-7" \ + "worktree=$dir/worktree" "project=$dir/project" "backend=orca" "orca_worktree_id=worktree-9" + fm_backend_validate_task_endpoint "$dir/home/state/$id.meta" "$id" || fail "legacy Orca endpoint (no binding) refused" + [ "$FM_BACKEND_VALIDATED_BACKEND" = orca ] || fail "legacy Orca did not select its backend" + [ "$FM_BACKEND_VALIDATED_TARGET" = term-7 ] || fail "legacy Orca did not select its terminal" + + id=cmux-legacy + fm_write_meta "$dir/home/state/$id.meta" \ + "window=workspace-1:surface-2" "worktree=$dir/worktree" "project=$dir/project" \ + "backend=cmux" "cmux_workspace_id=workspace-1" "cmux_surface_id=surface-2" + fm_backend_validate_task_endpoint "$dir/home/state/$id.meta" "$id" || fail "legacy cmux endpoint (no binding) refused" + [ "$FM_BACKEND_VALIDATED_BACKEND" = cmux ] || fail "legacy cmux did not select its backend" + for backend in tmux herdr zellij orca cmux; do set +e fm_backend_kill "$backend" "" >/dev/null 2>&1 @@ -143,7 +173,7 @@ test_supported_backend_endpoint_records_validate() { set -e [ "$target" -ne 0 ] || fail "$backend generic kill accepted an empty target" done - pass "cleanup identity: valid tmux, Herdr, Zellij, Orca, and cmux records validate while every empty backend target refuses" + pass "cleanup identity: valid and legacy tmux, Herdr, Zellij, Orca, and cmux records validate while every empty backend target refuses" } test_tmux_empty_target_refuses_without_invocation() {