From 5f8984d134d1b3e6a4137e4fb563e1bfb79479d9 Mon Sep 17 00:00:00 2001 From: Kaz Takahashi Date: Wed, 5 Aug 2026 11:31:32 +0900 Subject: [PATCH] fix(resolve): detect a type's agent process from its manifest, not a fixed list _agmsg_agent_binaries mapped a type to the process names that identify it with a hardcoded case whose last arm answered "claude codex gemini". That arm was not the exception it reads as: every type added since the list was written and never given an arm -- cursor, grok-build, hermes -- landed there, and their type.conf detect_proc (cursor-agent, grok) was never consulted at all. The consequence is a cross-type identity mix-up. agmsg_pid_is_agent accepted an enclosing Claude Code process as a cursor agent, so agmsg_agent_pid walking up from a slash command found the CALLER's claude process and called it the cursor member's own. agmsg_resolve_project step 1 then read that session's project marker, and a cursor member's project resolved to the project of whoever was asking: resolve("", cursor) before -> .../leader-project (the caller's project) after -> .../member-worktree (the member's own) Read detect_proc from the type registry, dropping its glob tokens since the matcher already tries "-*" for each entry, and keep the case as the fallback for a manifest without the key (antigravity, copilot). Memoized per type: this runs inside a ppid walk of up to 20 hops, and a manifest read is a filesystem scan. The same misresolution reaches actas lock ownership and instance-id keying for those types, which is the wider reason to fix it at the mapping rather than at each caller. Closes #626. --- scripts/lib/resolve-project.sh | 57 +++++++++++++++++++++++++++------ tests/test_resolve_project.bats | 47 +++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 9 deletions(-) diff --git a/scripts/lib/resolve-project.sh b/scripts/lib/resolve-project.sh index e18e22cdf..0c63cadd3 100644 --- a/scripts/lib/resolve-project.sh +++ b/scripts/lib/resolve-project.sh @@ -46,6 +46,13 @@ # shellcheck disable=SC1091 . "$SKILL_DIR/scripts/lib/instance-id.sh" +# agmsg_type_get: _agmsg_agent_binaries below reads each type's detect_proc from +# its manifest rather than a hardcoded list. type-registry.sh resolves its own +# lib dir and pulls in driver-registry.sh; neither sources this file, so there is +# no cycle. Double-source guarded. +# shellcheck disable=SC1091 +. "$SKILL_DIR/scripts/lib/type-registry.sh" + _agmsg_run_dir() { printf '%s/run' "$SKILL_DIR"; } # Canonicalize a directory path by resolving symlinks to its physical location. @@ -228,16 +235,48 @@ agmsg_find_registered_project_variant() { } # Map an agent type to the binary basename(s) its process may carry. +# Process names that identify an agent of , taken from the type manifest's +# detect_proc (drivers/types//type.conf) so a type added by dropping in a +# directory is recognized here too. Glob tokens ("cursor-agent-*") are dropped: +# the matcher below already tries "-*" for every entry it is given. +# +# The case arms are the fallback for a type whose manifest carries no detect_proc +# (antigravity, copilot). Reaching the last one used to be routine rather than +# exceptional: every type without an arm — cursor, grok-build, hermes — matched +# against "claude codex gemini", so agmsg_pid_is_agent accepted an enclosing +# Claude Code process as, say, a cursor agent. agmsg_resolve_project step 1 then +# read THAT session's project marker, and a cursor member's project resolved to +# the project of whoever was asking. reset.sh, handed a correct path, looked for +# the registration under the caller's project and reported "No registrations +# removed" while it sat in the roster. +# +# Memoized per type: agmsg_pid_is_agent runs inside agmsg_agent_pid's ppid walk +# (up to 20 hops), and a manifest read per hop is a filesystem scan per hop. _agmsg_agent_binaries() { - case "$1" in - claude-code) echo "claude" ;; - codex) echo "codex" ;; - gemini) echo "gemini" ;; - antigravity) echo "antigravity" ;; - copilot) echo "copilot" ;; - opencode) echo "opencode" ;; - *) echo "claude codex gemini" ;; - esac + local type="$1" cache_var procs tok out="" + cache_var="_AGMSG_AGENT_BINS_$(printf '%s' "$type" | tr -c '[:alnum:]' '_')" + if [ -n "${!cache_var:-}" ]; then printf '%s\n' "${!cache_var}"; return 0; fi + + if declare -F agmsg_type_get >/dev/null 2>&1; then + procs="$(agmsg_type_get "$type" detect_proc "" 2>/dev/null || true)" + for tok in $procs; do + case "$tok" in *'*'*) continue ;; esac + out="${out:+$out }$tok" + done + fi + if [ -z "$out" ]; then + case "$type" in + claude-code) out="claude" ;; + codex) out="codex" ;; + gemini) out="gemini" ;; + antigravity) out="antigravity" ;; + copilot) out="copilot" ;; + opencode) out="opencode" ;; + *) out="claude codex gemini" ;; + esac + fi + printf -v "$cache_var" '%s' "$out" + printf '%s\n' "$out" } # Does currently look like an agent process of ? Checks both the diff --git a/tests/test_resolve_project.bats b/tests/test_resolve_project.bats index cd4c7d476..d6af6a6db 100644 --- a/tests/test_resolve_project.bats +++ b/tests/test_resolve_project.bats @@ -330,6 +330,53 @@ JSON [ "$status" -ne 0 ] } +@test "agent-binaries: process names come from the type manifest, not a hardcoded list" { + # cursor and grok-build have no case arm here and used to fall through to the + # "claude codex gemini" guess — so their detect_proc was ignored entirely. + run _agmsg_agent_binaries cursor + [ "$output" = "cursor-agent" ] + run _agmsg_agent_binaries grok-build + [ "$output" = "grok" ] + run _agmsg_agent_binaries codex + [ "$output" = "codex" ] +} + +@test "pid-is-agent: a claude process is not accepted as an agent of another type" { + # This is what made a cursor member's project resolve to the project of + # whoever was asking: pid_is_agent said yes for the caller's own Claude Code + # process, so resolution took step 1 and read THAT session's project marker + # in preference to the member's own registration. + skip_on_windows "process argv faking via exec -a (#349)" + bash -c 'exec -a claude sleep 5' 3>&- & + local p=$! + sleep 0.3 + run agmsg_pid_is_agent "$p" cursor + local st_cursor=$status + run agmsg_pid_is_agent "$p" claude-code + local st_cc=$status + kill "$p" 2>/dev/null || true + [ "$st_cursor" -ne 0 ] # not a cursor agent + [ "$st_cc" -eq 0 ] # still detected as its own type +} + +@test "resolve: a member's project is not rewritten to the caller's by a cross-type marker" { + # End-to-end shape of the leak: a leader (claude-code) resolving a cursor + # member's path. The marker belongs to the leader's process and must not be + # consulted for a different type. + skip_on_windows "process argv faking via exec -a (#349)" + local member="$ROOT/sub/deep" + reg T cursoragent "$member" cursor + bash -c 'exec -a claude sleep 5' 3>&- & + local p=$! + sleep 0.3 + agmsg_write_project_marker "$p" "/leader/project" + run env AGMSG_AGENT_PID="$p" bash -c \ + 'SKILL_DIR="$1"; . "$SKILL_DIR/scripts/lib/resolve-project.sh"; agmsg_resolve_project "$2" cursor' \ + _ "$SKILL_DIR" "$member" + kill "$p" 2>/dev/null || true + [ "$output" != "/leader/project" ] +} + # --- end-to-end through entry scripts --- @test "whoami: subdir invocation resolves to the registered identity" {