2 Commits
9 changed files with 155 additions and 44 deletions
+39 -11
View File
@@ -404,6 +404,7 @@ except Exception:
fi
;;
set-buffer)
buf="tmp_buffer"
text=""
while [ $# -gt 0 ]; do
case "$1" in
@@ -412,18 +413,25 @@ except Exception:
echo "Error: -b requires a value" >&2
exit 1
fi
buf="$2"
buf="$(echo "$2" | tr -cd 'A-Za-z0-9_.-')"
if [ -z "$buf" ]; then buf="tmp_buffer"; else buf="buf_$buf"; fi
shift 2
;;
*) text="$1"; shift ;;
esac
done
echo -n "$text" > "$wrapper_dir/tmp_buffer"
echo -n "$text" > "$wrapper_dir/$buf"
;;
paste-buffer)
buf="tmp_buffer"
sess=""
while [ $# -gt 0 ]; do
case "$1" in
-b)
buf="$(echo "$2" | tr -cd 'A-Za-z0-9_.-')"
if [ -z "$buf" ]; then buf="tmp_buffer"; else buf="buf_$buf"; fi
shift 2
;;
-t)
if [ $# -lt 2 ]; then
echo "Error: -t requires a value" >&2
@@ -435,12 +443,26 @@ except Exception:
*) shift ;;
esac
done
if [ -f "$wrapper_dir/tmp_buffer" ]; then
_real_herdr agent send "$sess" "$(cat "$wrapper_dir/tmp_buffer")" >/dev/null 2>&1 || true
if [ -f "$wrapper_dir/$buf" ]; then
_real_herdr agent send "$sess" "$(cat "$wrapper_dir/$buf")" >/dev/null 2>&1 || true
else
echo "Error: buffer $buf not found" >&2
exit 1
fi
;;
delete-buffer)
rm -f "$wrapper_dir/tmp_buffer"
buf="tmp_buffer"
while [ $# -gt 0 ]; do
case "$1" in
-b)
buf="$(echo "$2" | tr -cd 'A-Za-z0-9_.-')"
if [ -z "$buf" ]; then buf="tmp_buffer"; else buf="buf_$buf"; fi
shift 2
;;
*) shift ;;
esac
done
rm -f "$wrapper_dir/$buf"
;;
ls)
_real_herdr agent list 2>/dev/null | python3 -c "
@@ -451,7 +473,7 @@ try:
for a in res.get('agents', []):
try:
name = a.get('name') or a.get('agent') or 'unknown'
print(f\"{name}|0\")
print(f\"{name}|\")
except Exception:
pass
except Exception:
@@ -659,8 +681,11 @@ env_python() {
_check_is_nfs() {
local f="$1"
local mountpoint
mountpoint="$(df --output=target "$f" 2>/dev/null | tail -1)" || return 1
if mount | grep -q "$mountpoint.*nfs\|$mountpoint.*cifs\|$mountpoint.*fuse.sshfs"; then
mountpoint="$(df --output=target "$f" 2>/dev/null | tail -1)"
if [ -z "$mountpoint" ]; then
mountpoint="$(df -P "$f" 2>/dev/null | tail -1 | awk '{print $6}')"
fi
if [ -n "$mountpoint" ] && mount | grep -i -q -E "$mountpoint.*(nfs|cifs|smb|sshfs)"; then
return 0 # is NFS
fi
return 1 # not NFS
@@ -1221,7 +1246,10 @@ elif agent == 'cline':
emit(cand)
ai = {}
db_path = f"{mam_dir}/agent-sessions.db" if 'mam_dir' in locals() else os.path.join(ws, ".mam", "agent-sessions.db")
yaml_path = f"{mam_dir}/agent-sessions.yaml" if 'mam_dir' in locals() else os.path.join(ws, ".mam", "agent-sessions.yaml")
try:
import yaml
if os.path.exists(db_path):
conn = sqlite3.connect(db_path, timeout=60.0)
row = conn.execute('SELECT data FROM state WHERE id=1').fetchone()
@@ -1232,8 +1260,8 @@ try:
with open(yaml_path) as f:
d = yaml.safe_load(f) or {}
ai = d.get('agent_identities', {})
except Exception:
pass
except Exception as e:
print(f"WARN: tier-3 identity lookup failed: {e}", file=sys.stderr)
ai_agent = ai.get(agent) or {}
if ai_agent.get('project_cwd') == ws:
@@ -1242,7 +1270,7 @@ if ai_agent.get('project_cwd') == ws:
if cand and verify_session_uuid(ws, agent, cand, mode="revalidate"):
emit(cand)
elif agent == 'agy':
cand = ai.get('conversation_id')
cand = ai_agent.get('conversation_id')
if cand and verify_session_uuid(ws, agent, cand, mode="revalidate"):
emit(cand)
elif agent == 'hermes':
@@ -81,7 +81,7 @@ fi
[ -n "$AGENT" ] || { echo "ERROR: --agent required" >&2; usage; exit 2; }
[ -n "$ROLE" ] || { echo "ERROR: --role required" >&2; usage; exit 2; }
[ -d "$WORKSPACE" ] || { echo "ERROR: workspace $WORKSPACE not a directory" >&2; exit 1; }
command -v herdr >/dev/null || { echo "ERROR: herdr not installed" >&2; exit 1; }
command -v herdr >/dev/null || type -P herdr >/dev/null || { echo "ERROR: herdr not installed" >&2; exit 1; }
command -v "$AGENT" >/dev/null || { echo "ERROR: $AGENT CLI not in PATH" >&2; exit 1; }
# Auth Check (OAuth check for agy, loggedIn check for claude, status for hermes)
@@ -127,22 +127,15 @@ fi
LOCAL_BIN="${LOCAL_BIN:-$HOME/.local/bin}"
WRAPPER="$LOCAL_BIN/$SESSION_NAME"
ws_slug="$(derive_session_name "$WORKSPACE" "$AGENT" | sed 's/-creator-.*//')"
if [ -z "${HERDR_SERVER_NAME:-}" ] || [ "$HERDR_SERVER_NAME" = "default" ]; then
export HERDR_SERVER_NAME="multi-agent-mux"
export HERDR_SERVER_NAME="mam-$ws_slug"
fi
# Resolve absolute path of the agent command to prevent herdr PATH inheritance issues (especially on macOS)
RESOLVED_BIN="$AGENT"
if [ "$AGENT" = "cline" ]; then
if command -v cline >/dev/null 2>&1; then
RESOLVED_BIN="$(command -v cline)"
fi
else
if [ "$AGENT" = "claude" ] && [ -x "$HOME/.nvm/versions/node/v24.15.0/bin/claude" ]; then
RESOLVED_BIN="$HOME/.nvm/versions/node/v24.15.0/bin/claude"
elif command -v "$AGENT" >/dev/null 2>&1; then
RESOLVED_BIN="$(command -v "$AGENT")"
fi
if command -v "$AGENT" >/dev/null 2>&1; then
RESOLVED_BIN="$(command -v "$AGENT")"
fi
# On macOS, clear quarantine attribute for the agent binary to prevent Gatekeeper hangs
@@ -159,7 +152,7 @@ esac
spawn() {
if [ -z "${HERDR_SERVER_NAME:-}" ] || [ "$HERDR_SERVER_NAME" = "default" ]; then
export HERDR_SERVER_NAME="multi-agent-mux"
export HERDR_SERVER_NAME="mam-$ws_slug"
fi
case "$AGENT" in
claude)
@@ -194,7 +187,8 @@ cleanup_herdr_on_error() {
}
trap cleanup_herdr_on_error EXIT
export HERDR_SERVER_NAME="${HERDR_SERVER_NAME:-$(resolve_herdr_server "$SESSION_NAME")}"
RESOLVED_SERVER="$(resolve_herdr_workspace "$SESSION_NAME")"
export HERDR_SERVER_NAME="${HERDR_SERVER_NAME:-$RESOLVED_SERVER}"
# TUI 준비 대기
if ! wait_for_tui_ready "$SESSION_NAME" "$AGENT"; then
@@ -45,13 +45,14 @@ from mqtt_common import (
logger = logging.getLogger("delegate_job.publish_event")
VALID_EVENTS = ("started", "permission_required", "progress", "completed", "error")
TERMINAL_EVENTS = ("completed", "error")
VALID_EVENTS = ("started", "permission_required", "progress", "completed", "error", "cancelled")
TERMINAL_EVENTS = ("completed", "error", "cancelled")
# event -> registry status to sync as a best-effort side effect
EVENT_TO_STATUS = {
"started": "running",
"completed": "completed",
"error": "error",
"cancelled": "cancelled",
}
CONNECT_ACK_TIMEOUT = 10 # seconds to wait for CONNACK
@@ -147,6 +147,9 @@ except Exception:
elif [ "$status" = "error" ]; then
log_error "Job '$job_id' finished with errors."
return 1
elif [ "$status" = "cancelled" ]; then
log_error "Job '$job_id' was cancelled."
return 1
fi
sleep "$check_interval"
done
@@ -131,9 +131,9 @@ for s in d.get('herdr_sessions', []):
if s.get('delegate_job_id') == _jid and s.get('status') == 'running':
_name = s.get('name')
_srv = s.get('herdr_workspace') or s.get('herdr_server') or 'default'
if _event == 'completed':
if _event in ('completed', 'cancelled'):
s['delegate_job_id'] = None
print('MQTT Monitor: job completed on ' + str(_name) + ' — session kept alive', flush=True)
print('MQTT Monitor: job ' + _event + ' on ' + str(_name) + ' — session kept alive', flush=True)
_changed = True
else:
s['status'] = 'terminated'
@@ -216,7 +216,7 @@ def on_message(_client, _userdata, msg):
print(f"MQTT Monitor: recorded event {event} for job {jid} (seq={seq})", flush=True)
if event in ("completed", "error"):
if event in ("completed", "error", "cancelled"):
print(f"MQTT Monitor: received terminal event {event} for job {jid}", flush=True)
handle_terminal(jid, event)
except Exception as e:
@@ -227,8 +227,13 @@ def on_connect(_c, _u, _flags, reason_code, _props):
rc = mqtt_common.reason_code_value(reason_code)
if rc == 0:
state['connected'] = True
_c.subscribe("python/mqtt/jobs/+/events", qos=1)
print("MQTT Monitor: subscribed to python/mqtt/jobs/+/events", flush=True)
ws_path = os.path.abspath(workspace_root) if workspace_root else os.getcwd()
import hashlib
fp = hashlib.sha256(ws_path.encode('utf-8')).hexdigest()[:12]
topic = f"mam/{fp}/jobs/+/events"
_c.subscribe(topic, qos=1)
_c.subscribe("python/mqtt/jobs/+/events", qos=1) # legacy fallback during transition
print(f"MQTT Monitor: subscribed to {topic}", flush=True)
else:
state['failed'] = True
print(f"MQTT Monitor connection failed: rc={rc}", flush=True)
@@ -76,7 +76,7 @@ if [ -z "$UUID" ]; then
exit 1
fi
export HERDR_SERVER_NAME="$(resolve_herdr_server "$SESSION_NAME")"
export HERDR_SERVER_NAME="$(resolve_herdr_workspace "$SESSION_NAME")"
# 2. If herdr is alive, attach. Done.
if herdr has-session -t "$SESSION_NAME" 2>/dev/null; then
@@ -215,11 +215,12 @@ if [ "$PURGE" = "1" ] && [ "$HERDR_ALIVE" = "1" ]; then
echo " Refusing registry removal — records preserved (no state was modified)." >&2
echo " Diagnose the stuck TUI (herdr session attach '$SESSION_NAME'), then re-run" >&2
echo " stop_session.sh --purge-conversation --yes (retry is safe/idempotent)." >&2
delegate_publish_event "$DELEGATE_JOB_ID" error "purge aborted: herdr session still alive"
delegate_publish_event "$DELEGATE_JOB_ID" cancelled "purge aborted: session still alive; no state was modified"
exit 4
fi
fi
# INVARIANT: Terminal events for a job must be published AFTER atomic_dump_yaml updates the session status
atomic_dump_yaml "$AGENT_SESSIONS_YAML" \
SESSION_NAME="$SESSION_NAME" AGENT="$AGENT" PURGE="$PURGE" \
NOW_ISO="$NOW_ISO" NOW_EPOCH="$NOW_EPOCH" LAST_STATUS="$LAST_STATUS" \
@@ -358,7 +359,7 @@ else:
print(f"updated: {name} status={target['status']}", flush=True)
PYEOF
delegate_publish_event "$DELEGATE_JOB_ID" completed "session terminated"
delegate_publish_event "$DELEGATE_JOB_ID" cancelled "session stopped by operator before job completion"
echo
echo "=== stop complete ==="
+4 -9
View File
@@ -13,6 +13,10 @@
#
# SECURITY: never put real secrets in this template. Secret-bearing vars use a
# `replace_me` placeholder — fill them in only in your local `.mam.env`.
# SECURITY NOTE:
# The default MQTT broker (broker.hivemq.com) is public and unencrypted. Job event
# payloads (including task details) sent to the default broker can be read by anyone.
# For production or private workloads, configure a private broker with TLS and Auth below.
# ---------------------------------------------------------------------------
# ===========================================================================
@@ -23,10 +27,6 @@
#default: <workspace>/.mam/agent-sessions.yaml
# AGENT_SESSIONS_YAML=/path/to/workspace/.mam/agent-sessions.yaml
# Where the monitor (reconcile.sh) keeps its drift-state cache.
#default: <workspace>/.cache/multi-agent-mux-monitor
# AGENT_SESSIONS_STATE_DIR=/path/to/workspace/.cache/multi-agent-mux-monitor
# Root directory that holds Claude Code per-project conversation logs (*.jsonl).
#default: $HOME/.claude/projects
# CLAUDE_PROJECT_DIR=$HOME/.claude/projects
@@ -35,11 +35,6 @@
#default: $HOME/.local/bin
# LOCAL_BIN=$HOME/.local/bin
# tmux server socket name (`tmux -L <name>`). "default" = the normal tmux server
# (no -L). Set this to opt into an isolated server for all skill tmux calls.
#default: default
# TMUX_SERVER_NAME=default
# ===========================================================================
# delegate-job / MQTT broker
# ===========================================================================
+84
View File
@@ -0,0 +1,84 @@
# 📊 Multi-Agent Mux 코드베이스 미해결 결함 보고서
- **작성일**: 2026-08-05 (최종 갱신: 수정 완료 항목 삭제 반영)
- **검증 방식**: `multi-agent-mux-loop --all-reviewer` 오케스트레이션 및 Planner(`claude`), Creator(`agy`), Reviewer(`cline`) 만장일치 교차 리뷰
- **대상**: `.agents/skills/**` 전체 (HERDR_SERVER_NAME 바인딩 및 tmux 시프트 계층 제외 범위)
- **남은 미해결 결함 건수**: **16건** (아키텍처 3건, 엣지케이스 8건, 레거시 잔재 5건)
> [!NOTE]
> 본 보고서에서 **A-4 (stop 시 completed 오발행 ➔ cancelled 교정)**, **B-2 (resolve_herdr_server 불일치 교정)**, **C-5 (TMUX_SERVER_NAME 노브 삭제)** 등 최근 수정 완료된 항목들은 제외/삭제 처리되었습니다.
---
## 1. 개요 및 검증 수단
본 보고서는 `HERDR_SERVER_NAME` 환경변수 바인딩 누락 및 `tmux` 호환 래퍼 shim 의존성을 넘어, `multi-agent-mux` 코드베이스 전반의 **아키텍처 구조적 안정성, MQTT 백플레인 보안, 런타임 엣지케이스 및 레거시 잔재** 중 아직 수정되지 않고 남아있는 문제점들을 정리한 잔여 결함 목록입니다.
---
## 2. 🔴 아키텍처 결함 (Architecture Flaws — 3건)
### **A-1 (herdr 서버명 전역 고정 및 타 워크스페이스 세션 오등록)**
- **현상**: `HERDR_SERVER_NAME`이 워크스페이스별 파생값이 아닌 `multi-agent-mux`라는 머신 전역 상수로 하드코딩되어 있습니다.
- **파급 효과**: 한 머신에서 다수의 MAM 워크스페이스 사용 시, 백그라운드 모니터(`reconcile.sh` drift-B)가 다른 워크스페이스의 세션을 내 `.mam/agent-sessions.yaml` 레지스트리에 무단 등록하여 세션 오염을 발생시킵니다.
### **A-2 (공개 브로커 + HMAC 인증 Off + 와일드카드 전파)**
- **현상**: `mqtt_common.py`의 기본 브로커가 공개 서버(`broker.hivemq.com`), HMAC 무조건 True 반환으로 설정되어 있습니다.
- **파급 효과**: 외부에서 유입되는 malicious `error` 이벤트 수신 시 `reconcile.sh`가 라이브 에이전트 pane을 `kill-session`으로 강제 파괴하는 치명적 보안/안정성 위험이 존재합니다.
### **A-3 (시프트 버퍼 단일 파일 공유 및 동시 주입 오염)**
- **현상**: `send_keys_safe` 시프트의 `set/paste/delete-buffer``-b` 세션 버퍼 이름을 무시하고 단일 `.mam/shim/tmp_buffer` 파일 하나만을 공유합니다.
- **파급 효과**: 다중 에이전트 동시 주입 시 대화 텍스트 교차 오염 및 무음 유실(t5 silent loss)이 발생합니다.
---
## 3. 🟠 엣지 케이스 및 런타임 버그 (Edge-case Bugs — 8건)
### **B-1 (`find_workspace_uuid` tier-3 `NameError`로 영구 사망)**
- `lib.sh` 내 agy 대화 복원용 tier-3 경로가 미정의 변수(`db_path`, `yaml_path`) 및 `yaml` import 누락으로 항상 `NameError` 예외를 내고 삼켜져 agy 대화 복원이 거부됩니다.
### **B-3 (`command -v herdr` 프리플라이트 무력화)**
- `create_session.sh`의 프리플라이트 검사 시 `command -v herdr``lib.sh`에 정의된 bash 함수(`herdr()`)를 호명하여 실제 시스템 `herdr` 바이너리가 없어도 프리플라이트를 무조건 통과해버립니다.
### **B-4 (시프트 `ls`의 `created=0` 하드코딩으로 재개 가드 무력화)**
- `herdr ls` 서브커맨드 래퍼가 세션 생성시각을 상수 `0`으로 리턴하여 `reconcile.sh` drift-B 등록 시 epoch 0이 되어 오래된 대화 jsonl 배제 가드가 붕괴됩니다.
### **B-5 (`df --output` GNU 전용 플래그 사용으로 macOS NFS 감지 실패)**
- macOS/BSD 환경에서 `df --output` 구문 오류로 NFS 감지가 실패하고 "NFS 아님"으로 오판되어 SQLite WAL 포맷을 강행합니다.
### **B-6 (스킬 트리에 임시 파일 복사 및 유출)**
- `run_loop.sh::delegate_job_safe`가 래퍼 스크립트를 `.agents/skills/...` 트리 내부에 `.tmp`로 복사하여 버전 관리 트리를 오염시키고 rsync 배포 시 외부로 유출됩니다.
### **B-7 (`run_loop.sh` 상대경로 cwd 의존 및 미추적 파일 누락)**
- 저장소 루트 밖에서 `run_loop.sh` 구동 시 `wait_for_job`이 3900초 무음 타임아웃을 발생시키며, `git diff`가 Creator가 새로 추가한 미추적 신규 파일을 리뷰어에게 누락합니다.
### **B-8 (`send_keys_safe` agy 경로 검증 이탈)**
- agy 세션 주입 시 주입 실패 여부를 검증하지 않고 무조건 `return 0`을 남겨 실패 시에도 성공으로 보고됩니다.
### **B-9 (`LOGS_DIR` import 시점 cwd 고정)**
- `mqtt_common.py` 모듈 로드 시점의 cwd로 감사 로그 경로가 1회 고정됩니다.
---
## 4. 🟡 레거시 잔재 및 죽은 코드 (Legacy Remnants — 5건)
### **C-1 (Kanban 문서 29회 언급 vs 실제 구현 0건)**
- SKILL.md 파일들에 Kanban 지원 및 상태 파일 서술이 29회 언급되어 있으나 스크립트 구현은 0건입니다.
### **C-2 (미사용 `.cache/` 상태 디렉터리 생성)**
- `reconcile.sh``.cache/multi-agent-mux-monitor` 디렉터리를 `mkdir`만 하고 아무것도 읽거나 쓰지 않습니다.
### **C-3 (격리 스텁 4종 및 `stop_session.sh` 미사용 isolation 코드 잔존)**
- `provision_isolation` 등 4개 스텁 함수와 `stop_session.sh``.mam/agent_homes` 가드 코드가 호출자 0건인 채 잔존합니다.
### **C-4 (참조 0회 미사용 심볼 7종)**
- `_HERDR_SHIM_DIR_PATTERN`, `_REAL_HERDR_PATH`, `TERMINAL_STATUSES`, `ISOLATE`, `local_herdr` 등 7개 미사용 심볼이 잔존합니다.
### **C-6 (`stop_session.sh` 도움말 문서 구버전 표기)**
- 스크립트 도움말에는 `--mode soft|hard` 등이 서술되어 있으나 실제 옵션 파서는 `exit 2`로 거부합니다.
---
## 5. 결론 및 향후 보완 방향
향후 코드베이스 개편 시 본 문서에 서술된 잔여 16가지 결함 항목을 순차적으로 조치하여 Multi-Agent Mux 프레임워크의 완전한 안정성과 격리성을 보장해야 합니다.