From 4243da507c94e35cc1d83e3dad259b6c628f43eb Mon Sep 17 00:00:00 2001 From: Godopu Date: Sun, 25 Oct 2026 15:16:20 +0000 Subject: [PATCH] fix(lib.sh): one herdr name rule; the shim's agent attach resolves its target (Issue #71) - One _sanitize_herdr_agent_name: the shim gets lib.sh's definition through `declare -f` instead of a second copy. Without shasum/sha1sum/openssl it hashes with python3 (same result as lib_py.agents.sanitize); with none of them it fails instead of truncating or appending 00000000. - Every caller stops on that failure: new-session before any pane is made, the lookups report failure, and has-session takes the name lib.sh computed (the cwd-relative import and its lower()[:32] fallback are gone). - `herdr agent attach ` through the shim resolves the target like get/read/prompt; options after the target are kept. Refs #71 Co-Authored-By: Claude Opus 5.5 --- .agents/skills/lib.sh | 93 +++++++++---------- .../multi-agent-mux-delegate-job | 4 +- 2 files changed, 45 insertions(+), 52 deletions(-) diff --git a/.agents/skills/lib.sh b/.agents/skills/lib.sh index b3188e4..775147e 100644 --- a/.agents/skills/lib.sh +++ b/.agents/skills/lib.sh @@ -33,6 +33,11 @@ MAM_VERSION="4.10.1" export MAM_VERSION AGENT_SESSIONS_YAML="${AGENT_SESSIONS_YAML:-$WORKSPACE_ROOT/.mam/agent-sessions.yaml}" +# The one bash form of lib_py.agents.sanitize.sanitize_herdr_agent_name +# (Issue #71). The herdr shim gets this same definition: _init_herdr_isolation +# writes it in with `declare -f`. A long name needs its SHA-1 suffix; with no +# shasum, sha1sum, openssl or python3 to make it, this fails (stderr, nonzero) +# rather than return a name herdr never registered. _sanitize_herdr_agent_name() { local n="$1" if [ -z "$n" ]; then @@ -46,16 +51,21 @@ _sanitize_herdr_agent_name() { local h="" if command -v shasum >/dev/null 2>&1; then h=$(printf '%s' "$s" | shasum 2>/dev/null | awk '{print $1}' | cut -c 1-8) - elif command -v sha1sum >/dev/null 2>&1; then + fi + if [ "${#h}" -ne 8 ] && command -v sha1sum >/dev/null 2>&1; then h=$(printf '%s' "$s" | sha1sum 2>/dev/null | awk '{print $1}' | cut -c 1-8) - elif command -v openssl >/dev/null 2>&1; then + fi + if [ "${#h}" -ne 8 ] && command -v openssl >/dev/null 2>&1; then h=$(printf '%s' "$s" | openssl sha1 2>/dev/null | awk '{print $NF}' | cut -c 1-8) fi - if [ -n "$h" ]; then - s="${s:0:23}-$h" - else - s="${s:0:32}" + if [ "${#h}" -ne 8 ] && command -v python3 >/dev/null 2>&1; then + h=$(python3 -c 'import hashlib,sys; print(hashlib.sha1(sys.argv[1].encode("utf-8")).hexdigest()[:8])' "$s" 2>/dev/null || true) fi + if [ "${#h}" -ne 8 ]; then + echo "ERROR: cannot make the herdr name of '$n': no shasum, sha1sum, openssl or python3 to hash it" >&2 + return 1 + fi + s="${s:0:23}-${h}" fi printf '%s\n' "$s" } @@ -155,8 +165,10 @@ _init_herdr_isolation() { fi local tmp_file="$wrapper_dir/herdr.tmp.$$.$RANDOM" - cat <<'EOF' > "$tmp_file" -#!/usr/bin/env bash + { + printf '%s\n' '#!/usr/bin/env bash' + declare -f _sanitize_herdr_agent_name + cat <<'EOF' # Herdr-to-Herdr translation shim wrapper set -euo pipefail @@ -290,31 +302,6 @@ _real_herdr() { fi } -_sanitize_herdr_agent_name() { - local n="$1" - if [ -z "$n" ]; then - printf 'agent\n' - return 0 - fi - local s - s=$(echo "$n" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9_-]/-/g') - if [[ ! "$s" =~ ^[a-z] ]]; then s="x-$s"; fi - if [ "${#s}" -gt 32 ]; then - local h="" - if command -v shasum >/dev/null 2>&1; then - h=$(printf '%s' "$s" | shasum 2>/dev/null | awk '{print $1}' | cut -c 1-8) - elif command -v sha1sum >/dev/null 2>&1; then - h=$(printf '%s' "$s" | sha1sum 2>/dev/null | awk '{print $1}' | cut -c 1-8) - elif command -v openssl >/dev/null 2>&1; then - h=$(printf '%s' "$s" | openssl sha1 2>/dev/null | awk '{print $NF}' | cut -c 1-8) - else - h=$(python3 -c "import hashlib,sys; print(hashlib.sha1(sys.argv[1].encode()).hexdigest()[:8])" "$s" 2>/dev/null || echo "00000000") - fi - s="${s:0:23}-${h}" - fi - printf '%s\n' "${s:0:32}" -} - # --------------------------------------------------------------------------- # Workspace scoping (ISSUE-3). # @@ -441,7 +428,7 @@ sys.exit(1) [ -n "$resolved_ws" ] && target_ws="$resolved_ws" fi local sat pid="" - sat=$(_sanitize_herdr_agent_name "$target") + sat=$(_sanitize_herdr_agent_name "$target") || return 1 local cand get_ws if [ "$#" -ge 2 ]; then @@ -534,7 +521,7 @@ case "$cmd" in _resolve_herdr_target() { local raw="$1" local sat - sat=$(_sanitize_herdr_agent_name "$raw") + sat=$(_sanitize_herdr_agent_name "$raw") || return 1 if [ -n "$(_herdr_agent_get_scoped "$sat" 2>/dev/null || true)" ]; then echo "$sat" return 0 @@ -604,6 +591,15 @@ sys.exit(1) exit 1 fi _real_herdr agent read "$tgt" "$@" + elif [ "$sub" = "attach" ] && [ $# -ge 1 ]; then + t="$1" + shift + tgt=$(_resolve_herdr_target "$t" || true) + if [ -z "$tgt" ]; then + echo "Error: no in-scope herdr agent for '$t'" >&2 + exit 1 + fi + _real_herdr agent attach "$tgt" "$@" else _real_herdr agent "$sub" "$@" fi @@ -623,19 +619,15 @@ sys.exit(1) *) shift ;; esac done - if [ -n "$(_herdr_agent_get_scoped "$(_sanitize_herdr_agent_name "$sess")" 2>/dev/null || true)" ] \ + ssess=$(_sanitize_herdr_agent_name "$sess") || exit 1 + if [ -n "$(_herdr_agent_get_scoped "$ssess" 2>/dev/null || true)" ] \ || [ -n "$(_herdr_agent_get_scoped "$sess" 2>/dev/null || true)" ]; then exit 0 fi - if _real_herdr agent list 2>/dev/null | TARGET_NAME="$sess" TARGET_WS="$(_herdr_ws_scope)" python3 -c ' + if _real_herdr agent list 2>/dev/null | TARGET_NAME="$sess" TARGET_SAN="$ssess" TARGET_WS="$(_herdr_ws_scope)" python3 -c ' import sys, json, os -try: - sys.path.insert(0, os.path.abspath(".agents/skills")) - from lib_py.agents.sanitize import sanitize_herdr_agent_name -except Exception: - sanitize_herdr_agent_name = lambda s: s.lower()[:32] if s else "agent" tn = os.environ.get("TARGET_NAME", "") -stn = sanitize_herdr_agent_name(tn) +stn = os.environ.get("TARGET_SAN", "") tws = os.environ.get("TARGET_WS", "") try: d = json.loads(sys.stdin.read()) @@ -682,6 +674,7 @@ sys.exit(1) *) run_cmd="$1"; shift ;; esac done + agent_name=$(_sanitize_herdr_agent_name "$name") || exit 1 if [ -z "$run_cmd" ]; then run_cmd="${SHELL:-/bin/bash}" fi @@ -896,7 +889,6 @@ except Exception: res="" success=0 backoffs=(0.5 1 2) - agent_name=$(_sanitize_herdr_agent_name "$name") for i in $(seq 0 2); do if [ -n "$final_cmd" ]; then res=$(eval "_real_herdr agent start \"$agent_name\" --kind \"$kind\" --pane \"$target_pane\" -- $final_cmd" 2>&1 || true) @@ -967,7 +959,7 @@ except Exception: # `herdr session list`), not individual agent panes — `session stop/delete # "$sess"` with a MAM session name always fails (silently, via `|| true`). # Resolve the real pane_id via `agent get` and close just that pane instead. - agent_target=$(_sanitize_herdr_agent_name "$sess") + agent_target=$(_sanitize_herdr_agent_name "$sess") || exit 1 pane_id=$(_resolve_herdr_pane_id "$sess" 2>/dev/null || true) if [ -n "$pane_id" ]; then _real_herdr pane close "$pane_id" >/dev/null 2>&1 || true @@ -1002,7 +994,7 @@ except Exception: # there is no top-level "pane" key, and no "pid" field at all (only # "pane_id", herdr's own wN:pN identifier). The real OS pid requires a # second call to `pane process-info --pane `. - agent_target=$(_sanitize_herdr_agent_name "$sess") + agent_target=$(_sanitize_herdr_agent_name "$sess") || exit 1 info=$(_real_herdr agent get "$agent_target" 2>/dev/null || _real_herdr agent get "$sess" 2>/dev/null || true) pane_id="" cwd="" cmd="" if [ -n "$info" ]; then @@ -1072,7 +1064,7 @@ except Exception: *) shift ;; esac done - agent_target=$(_sanitize_herdr_agent_name "$sess") + agent_target=$(_sanitize_herdr_agent_name "$sess") || exit 1 pane_id=$(_resolve_herdr_pane_id "$sess" 2>/dev/null || true) if [ -n "$pane_id" ]; then _real_herdr pane read "$pane_id" --source visible --lines 100 2>/dev/null \ @@ -1103,7 +1095,7 @@ except Exception: fi # `pane send-keys` requires a real pane_id ("wN:pN"), not an agent name — # resolve it via `agent get` first (agent-level commands accept names). - agent_target=$(_sanitize_herdr_agent_name "$sess") + agent_target=$(_sanitize_herdr_agent_name "$sess") || exit 1 pane_id=$(_resolve_herdr_pane_id "$sess" 2>/dev/null || true) if [ -n "$pane_id" ]; then _real_herdr pane send-keys "$pane_id" "$key" >/dev/null 2>&1 || true @@ -1387,6 +1379,7 @@ for name, pid in rows: ;; esac EOF + } > "$tmp_file" chmod +x "$tmp_file" 2>/dev/null || true mv -f "$tmp_file" "$wrapper_dir/herdr" 2>/dev/null || rm -f "$tmp_file" 2>/dev/null || true if [[ ":$PATH:" != *":$wrapper_dir:"* ]]; then @@ -2787,7 +2780,7 @@ wait_for_agent_idle() { fi local target - target=$(_sanitize_herdr_agent_name "$sess") + target=$(_sanitize_herdr_agent_name "$sess") || return 3 local raw_status="" agent_st="" raw_status="$(_sks_herdr agent get "$target" 2>/dev/null || _sks_herdr agent get "$sess" 2>/dev/null || true)" @@ -2912,7 +2905,7 @@ send_keys_safe() { fi local agent_target - agent_target=$(_sanitize_herdr_agent_name "$sess") + agent_target=$(_sanitize_herdr_agent_name "$sess") || return 1 # Busy gate: wait up to SKS_BUSY_TIMEOUT only if wait_for_agent_idle returned 3 (unknown) if [ "${_SKS_IDLE_RC:-0}" = "3" ]; then diff --git a/.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job b/.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job index 1127876..b0af734 100755 --- a/.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job +++ b/.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job @@ -895,8 +895,8 @@ finalize_unfinished() { FINAL_JOB_STATUS="$final" if [ "$step" = "probe" ]; then local sess="${target_sess#herdr:}" target_agent diag="no" - target_agent=$(_sanitize_herdr_agent_name "$sess") - _within_deadline "$deadline" _agent_still_working "$sess" "$target_agent" && diag="yes" + target_agent=$(_sanitize_herdr_agent_name "$sess") || target_agent="" + [ -n "$target_agent" ] && _within_deadline "$deadline" _agent_still_working "$sess" "$target_agent" && diag="yes" echo "liveness: no wire event for ${IDLE_TIMEOUT}s on job $job_id; herdr/pane says busy=$diag (diagnostic only, not evidence)" >&2 fi # The answer comes back on fd 3: stdout carries registry.py's close output.