refactor(isolation): simplify agent session isolation and remove legacy home-isolation helpers

This commit is contained in:
2026-07-23 22:05:29 +09:00
parent 15ffc8f6bb
commit f0a2103edf
7 changed files with 91 additions and 347 deletions
+3 -5
View File
@@ -53,10 +53,9 @@ $ bash .agents/skills/multi-agent-mux-create/scripts/create_session.sh \
--agent claude \
--role developer \
--session my-project-dev-claude \
--isolate \
--herdr-workspace multi-agent-mux
--herdr-server multi-agent-mux
```
* `--isolate` 옵션을 주면 `.mam/agent_homes/<uuid>/` 하위에 로그인 및 설정은 유지하되 대화 내역은 격리되는 홈이 형성됩니다.
* 모든 세션은 사용자의 전역 에이전트 설정(Global Config)을 공유하며, herdr 프로세스 격리 및 대화 UUID 단위로 독립 구동됩니다.
### 2) 세션 접속 (Attach)
백그라운드에서 구동된 에이전트 TUI 화면에 들어갑니다.
@@ -66,7 +65,7 @@ $ herdr session attach my-project-dev-claude
* **화면 탈출**: 대화 중 세션을 유지한 채 터미널로 돌아오려면 `Ctrl + B`를 누른 뒤 `D` 키를 차례로 입력합니다.
### 3) 에이전트 상태 복원 (Resume)
세션이 중지되었거나, 호스트 재기동으로 tmux가 소멸한 경우에도 이전 대화 ID 및 격리 디렉토리를 원자적으로 이어받아 다시 기동할 수 있습니다.
세션이 중지되었거나, 호스트 재기동으로 tmux가 소멸한 경우에도 이전 대화 ID를 원자적으로 이어받아 다시 기동할 수 있습니다.
```bash
# 1단계: 복원 대상 세션의 UUID 자동 조회 (DB/YAML 레지스트리 기반)
$ WORKSPACE="/path/to/your/project"
@@ -80,7 +79,6 @@ $ UUID=$(bash .agents/skills/multi-agent-mux-resume/scripts/resolve_session_id.s
$ [ -n "$UUID" ] || { echo "[ERROR] 매칭되는 활성 세션 이력이 없습니다. create_session.sh를 통해 먼저 세션을 생성해 주세요."; exit 1; }
# 2단계: 세션 재기동 (이전 대화 컨텍스트 복원 기동)
# (주의: 만약 create 시 격리(--isolate) 세션으로 생성했다면, CLAUDE_CONFIG_DIR 환경변수를 YAML에 기록된 isolation.root 경로로 지정하여 띄워야 합니다. 상세 격리 복원 커맨드는 .agents/skills/multi-agent-mux-resume/SKILL.md 문서를 필독해 주세요.)
$ herdr run -d --name "$SESSION_NAME" --workspace "$WORKSPACE" -- \
"claude --dangerously-skip-permissions -r $UUID"
+33 -158
View File
@@ -546,7 +546,11 @@ for s in d.get('herdr_sessions', []):
if s.get('name') == name:
print(s.get('herdr_workspace') or s.get('herdr_server') or 'default')
sys.exit(0)
print(os.environ.get('HERDR_SERVER_NAME', 'default'))
fallback = os.environ.get('HERDR_SERVER_NAME', '')
if not fallback or fallback == 'default':
pwd = os.path.abspath(os.getcwd())
fallback = os.path.basename(pwd)
print(fallback or 'default')
"
}
@@ -890,13 +894,12 @@ def verify_session_uuid(ws, agent, uuid, row=None, home_dir=None, claude_dir=Non
row = row or {}
epoch = row.get("herdr_session_epoch", 0)
cwd = row.get("pane", {}).get("cwd", "") or ws
iso = row.get("isolation", {}).get("root") if isinstance(row.get("isolation"), dict) else None
if workspace_key(cwd) != workspace_key(ws):
return False
if agent == "claude":
base = f"{iso}/projects" if iso else c_dir
base = c_dir
key = workspace_key(ws)
path = f"{base}/{key}/{uuid}.jsonl"
if not os.path.exists(path):
@@ -917,21 +920,24 @@ def verify_session_uuid(ws, agent, uuid, row=None, home_dir=None, claude_dir=Non
return False
elif agent == "agy":
base = f"{iso}/.gemini/antigravity-cli/conversations" if iso else f"{home}/.gemini/antigravity-cli/conversations"
base = f"{home}/.gemini/antigravity-cli/conversations"
path = f"{base}/{uuid}.db"
if not os.path.exists(path):
return False
if epoch and os.path.getmtime(path) < epoch:
return False
if mode == "discover":
lc = f"{iso}/.gemini/antigravity-cli/cache/last_conversations.json" if iso else f"{home}/.gemini/antigravity-cli/cache/last_conversations.json"
lc = f"{home}/.gemini/antigravity-cli/cache/last_conversations.json"
cache_match = False
if os.path.exists(lc):
try:
with open(lc) as f:
lc_data = json.load(f)
if lc_data.get(cwd) != uuid:
return False
cache_match = (lc_data.get(cwd) == uuid)
except Exception:
cache_match = False
if not cache_match:
if uuid in (row.get("_sibling_claimed_uuids") or []):
return False
try:
conn = sqlite3.connect(path)
@@ -943,7 +949,7 @@ def verify_session_uuid(ws, agent, uuid, row=None, home_dir=None, claude_dir=Non
return False
elif agent == "hermes":
hdb = f"{iso}/.hermes/state.db" if iso else f"{home}/.hermes/state.db"
hdb = f"{home}/.hermes/state.db"
if not os.path.exists(hdb):
return False
if epoch and os.path.getmtime(hdb) < epoch:
@@ -958,7 +964,7 @@ def verify_session_uuid(ws, agent, uuid, row=None, home_dir=None, claude_dir=Non
return False
elif agent == "cline":
base = f"{iso}/sessions" if iso else f"{home}/.cline/data/sessions"
base = f"{home}/.cline/data/sessions"
path = f"{base}/{uuid}/{uuid}.json"
if not os.path.exists(path):
return False
@@ -1265,169 +1271,34 @@ capture_conversation_id() {
}
# ---------------------------------------------------------------------------
# Session isolation (all-L2) — implementation_plan.session_isolation.md Rev.3
# Session isolation — Universal Global Config (Option A, Job 536a6625)
#
# Every isolated session gets its own state home `.mam/agent_homes/<uuid>/`;
# auth/config files are SYMLINKED into it (never copied — token refresh must
# converge on the real files). Levers per agent (Phase 0 measured matrix):
# claude: CLAUDE_CONFIG_DIR=<root> conv: <root>/projects/<key>/<uuid>.jsonl
# cline: --data-dir <root> conv: <root>/sessions/<id>/<id>.json
# agy: HOME=<root> conv: <root>/.gemini/antigravity-cli/conversations/
# hermes: HOME=<root> conv: <root>/.hermes/state.db
# Config-home isolation (.mam/agent_homes/<uuid>/) was removed in favor of:
# 1. Universal Global Config: all agents read/write standard ~/.claude, ~/.gemini,
# ~/.hermes, ~/.cline user configuration and credential stores.
# 2. Process Isolation: each agent-workspace pair runs in its own herdr pane.
# 3. Conversation Isolation: session UUIDs discriminate conversation history.
#
# provision_isolation <agent> <root> — mkdir + seed; prints comma-joined seeded list
# isolation_lever <agent> — claude_config_dir|cline_data_dir|home
# isolation_env_prefix <agent> <root> — "VAR=<root> " spawn prefix ('' for cline)
# isolation_cmd_args <agent> <root> — extra spawn CLI args ('' unless cline)
# Stubbed isolation functions kept for backward compatibility:
# ---------------------------------------------------------------------------
provision_isolation() {
local agent="$1" root="$2" seeded="" f base
mkdir -p "$root"
case "$agent" in
claude)
if [ -e "$HOME/.claude.json" ]; then ln -sfn "$HOME/.claude.json" "$root/.claude.json"; seeded=".claude.json"; fi
if [ -e "$HOME/.claude/.credentials.json" ]; then ln -sfn "$HOME/.claude/.credentials.json" "$root/.credentials.json"; seeded="${seeded:+$seeded,}.credentials.json"; fi
if [ -e "$HOME/.claude/settings.json" ]; then
if [ ! -e "$root/settings.json" ]; then cp -a "$HOME/.claude/settings.json" "$root/settings.json"; fi
seeded="${seeded:+$seeded,}settings.json"
fi
if [ -d "$HOME/.claude/plugins" ]; then ln -sfn "$HOME/.claude/plugins" "$root/plugins"; seeded="${seeded:+$seeded,}plugins"; fi
if [ -d "$HOME/.claude/session-env" ]; then ln -sfn "$HOME/.claude/session-env" "$root/session-env"; seeded="${seeded:+$seeded,}session-env"; fi
if [ -d "$HOME/.claude/sessions" ]; then ln -sfn "$HOME/.claude/sessions" "$root/sessions"; seeded="${seeded:+$seeded,}sessions"; fi
if [ -d "$HOME/.claude/cache" ]; then ln -sfn "$HOME/.claude/cache" "$root/cache"; seeded="${seeded:+$seeded,}cache"; fi
;;
cline)
# CLI 가 --data-dir <root> 를 읽을 때 최상위 루트 하위에서 설정을 찾으므로 다이렉트 맵핑
mkdir -p "$root/settings"
for f in "$HOME/.cline/data/settings/"*; do
[ -e "$f" ] || continue
base="$(basename "$f")"
ln -sfn "$f" "$root/settings/$base"; seeded="${seeded:+$seeded,}settings/$base"
done
if [ -e "$HOME/.cline/data/globalState.json" ]; then
ln -sfn "$HOME/.cline/data/globalState.json" "$root/globalState.json"; seeded="${seeded:+$seeded,}globalState.json"
fi
# 인증과 DB 바인딩을 연계하여 Welcome Screen 튕김 방지
mkdir -p "$root/db"
for f in "$HOME/.cline/data/db/"*; do
[ -e "$f" ] || continue
base="$(basename "$f")"
# DB 락 경쟁 크래시 방지를 위해 물리 복사(cp -p)로 직접 주입
cp -p "$f" "$root/db/$base"
seeded="${seeded:+$seeded,}db/$base"
done
;;
agy)
mkdir -p "$root/.gemini/antigravity-cli"
for f in oauth_creds.json google_accounts.json installation_id settings.json state.json; do
if [ -e "$HOME/.gemini/$f" ]; then ln -sfn "$HOME/.gemini/$f" "$root/.gemini/$f"; seeded="${seeded:+$seeded,}.gemini/$f"; fi
done
for f in antigravity-oauth-token installation_id settings.json conversation_summaries.db jetski_state.pbtxt; do
if [ -e "$HOME/.gemini/antigravity-cli/$f" ]; then ln -sfn "$HOME/.gemini/antigravity-cli/$f" "$root/.gemini/antigravity-cli/$f"; seeded="${seeded:+$seeded,}.gemini/antigravity-cli/$f"; fi
done
if [ -d "$HOME/.gemini/antigravity" ]; then
ln -sfn "$HOME/.gemini/antigravity" "$root/.gemini/antigravity"
seeded="${seeded:+$seeded,}.gemini/antigravity"
fi
if [ -d "$HOME/.gemini/antigravity-ide" ]; then
if [ ! -d "$root/.gemini/antigravity-ide" ]; then cp -a "$HOME/.gemini/antigravity-ide" "$root/.gemini/antigravity-ide"; fi
seeded="${seeded:+$seeded,}.gemini/antigravity-ide"
fi
if [ -d "$HOME/.gemini/config" ]; then
ln -sfn "$HOME/.gemini/config" "$root/.gemini/config"
seeded="${seeded:+$seeded,}.gemini/config"
fi
# On macOS, seed ~/Library/Keychains to allow isolated agy to query Keychain Access credentials
# Also seed Preferences and Application Support for Antigravity settings/TOS/Theme (using cp -a for write isolation)
if [ "$(uname)" = "Darwin" ]; then
mkdir -p "$root/Library"
if [ -d "$HOME/Library/Keychains" ]; then
ln -sfn "$HOME/Library/Keychains" "$root/Library/Keychains"
seeded="${seeded:+$seeded,}Library/Keychains"
fi
mkdir -p "$root/Library/Preferences"
for plist in com.google.antigravity.plist com.google.antigravity-ide.plist com.google.GeminiMacOS.plist com.google.GeminiMacOS.shareddata.plist com.google.GeminiMacOS.launcher.plist; do
if [ -f "$HOME/Library/Preferences/$plist" ]; then
if [ ! -f "$root/Library/Preferences/$plist" ]; then cp -a "$HOME/Library/Preferences/$plist" "$root/Library/Preferences/$plist"; fi
seeded="${seeded:+$seeded,}Library/Preferences/$plist"
fi
done
mkdir -p "$root/Library/Application Support"
if [ -d "$HOME/Library/Application Support/Antigravity" ]; then
if [ ! -d "$root/Library/Application Support/Antigravity" ]; then cp -a "$HOME/Library/Application Support/Antigravity" "$root/Library/Application Support/Antigravity"; fi
seeded="${seeded:+$seeded,}Library/Application Support/Antigravity"
fi
if [ -d "$HOME/Library/Application Support/Antigravity IDE" ]; then
if [ ! -d "$root/Library/Application Support/Antigravity IDE" ]; then cp -a "$HOME/Library/Application Support/Antigravity IDE" "$root/Library/Application Support/Antigravity IDE"; fi
seeded="${seeded:+$seeded,}Library/Application Support/Antigravity IDE"
fi
if [ -d "$HOME/Library/Application Support/com.google.GeminiMacOS" ]; then
if [ ! -d "$root/Library/Application Support/com.google.GeminiMacOS" ]; then cp -a "$HOME/Library/Application Support/com.google.GeminiMacOS" "$root/Library/Application Support/com.google.GeminiMacOS"; fi
seeded="${seeded:+$seeded,}Library/Application Support/com.google.GeminiMacOS"
fi
if [ -d "$HOME/Library/Group Containers/group.com.google.gemini" ]; then
mkdir -p "$root/Library/Group Containers"
ln -sfn "$HOME/Library/Group Containers/group.com.google.gemini" "$root/Library/Group Containers/group.com.google.gemini"
seeded="${seeded:+$seeded,}Library/Group Containers/group.com.google.gemini"
fi
else
# On Linux/Unix, seed XDG config and local data dirs for Antigravity settings/TOS (using cp -a for write isolation)
local xdg_config="${XDG_CONFIG_HOME:-$HOME/.config}"
local xdg_data="${XDG_DATA_HOME:-$HOME/.local/share}"
if [ -d "$xdg_config/Antigravity" ]; then
mkdir -p "$root/.config"
if [ ! -d "$root/.config/Antigravity" ]; then cp -a "$xdg_config/Antigravity" "$root/.config/Antigravity"; fi
seeded="${seeded:+$seeded,}.config/Antigravity"
elif [ -d "$xdg_config/antigravity" ]; then
mkdir -p "$root/.config"
if [ ! -d "$root/.config/antigravity" ]; then cp -a "$xdg_config/antigravity" "$root/.config/antigravity"; fi
seeded="${seeded:+$seeded,}.config/antigravity"
fi
if [ -d "$xdg_data/Antigravity" ]; then
mkdir -p "$root/.local/share"
if [ ! -d "$root/.local/share/Antigravity" ]; then cp -a "$xdg_data/Antigravity" "$root/.local/share/Antigravity"; fi
seeded="${seeded:+$seeded,}.local/share/Antigravity"
elif [ -d "$xdg_data/antigravity" ]; then
mkdir -p "$root/.local/share"
if [ ! -d "$root/.local/share/antigravity" ]; then cp -a "$xdg_data/antigravity" "$root/.local/share/antigravity"; fi
seeded="${seeded:+$seeded,}.local/share/antigravity"
fi
fi
;;
hermes)
mkdir -p "$root/.hermes"
for f in auth.json config.yaml .env; do
if [ -e "$HOME/.hermes/$f" ]; then ln -sfn "$HOME/.hermes/$f" "$root/.hermes/$f"; seeded="${seeded:+$seeded,}.hermes/$f"; fi
done
;;
esac
printf '%s\n' "$seeded"
local agent="$1" root="$2"
printf ''
}
isolation_lever() {
case "$1" in
claude) echo "claude_config_dir" ;;
cline) echo "cline_data_dir" ;;
agy|hermes) echo "home" ;;
*) echo "" ;;
claude|agy|hermes|cline) echo "none" ;;
*) echo "" ;;
esac
}
isolation_env_prefix() {
local agent="$1" root="$2"
case "$agent" in
claude) printf 'CLAUDE_CONFIG_DIR=%q ' "$root" ;;
agy|hermes) printf 'HOME=%q ' "$root" ;;
*) : ;;
esac
:
}
isolation_cmd_args() {
local agent="$1" root="$2"
case "$agent" in
cline) printf -- '--data-dir %q' "$root" ;;
*) : ;;
esac
:
}
# ---------------------------------------------------------------------------
@@ -1596,11 +1467,15 @@ wait_for_tui_ready() {
for i in {1..30}; do
if _pane_dialog_open "$sess"; then
if printf '%s\n' "$(_pane_tail "$sess" 5)" | grep -q 'Press Enter to continue'; then
_sks_herdr send-keys -t "$sess" Enter || true
sleep 1
fi
sleep 1
continue
fi
local content
content=$($local_herdr capture-pane -p -t "$sess" 2>/dev/null || echo "")
content=$(_sks_herdr capture-pane -p -t "$sess" 2>/dev/null || echo "")
if [ -n "$content" ]; then
case "$agent" in
claude)
@@ -36,8 +36,8 @@ Options:
--submit-job PROMPT submit a job to multi-agent-mux-delegate-job registry with the given prompt
--onboard automatically submit a project alignment/orientation job to the new agent
--no-onboard disable automatic onboarding job submission
--no-isolate disable state isolation (shares global configuration/history)
[default: isolated mode is always active]
--no-isolate legacy flag (no-op; all sessions use global configuration)
--isolate legacy flag (no-op; all sessions use global configuration)
-h, --help this help
EOF
}
@@ -65,8 +65,8 @@ while [ $# -gt 0 ]; do
--submit-job) SUBMIT_JOB_PROMPT="$2"; shift 2 ;;
--onboard) ONBOARD=1; shift ;;
--no-onboard) ONBOARD=0; shift ;;
--isolate) ISOLATE=1; shift ;; # legacy compatibility
--no-isolate) ISOLATE=0; shift ;;
--isolate) echo "NOTE: --isolate/--no-isolate is a no-op — config-home isolation was removed; sessions always use global config." >&2; shift ;; # legacy compatibility
--no-isolate) echo "NOTE: --isolate/--no-isolate is a no-op — config-home isolation was removed; sessions always use global config." >&2; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "ERROR: unknown arg: $1" >&2; usage; exit 2 ;;
esac
@@ -121,37 +121,14 @@ if _herdr has-session -t "$SESSION_NAME" 2>/dev/null; then
exit 3
fi
# T3: 세션 격리 프로비저닝 (all-L2) — 격리 홈 생성 + auth/설정 심링크 시딩
# (implementation_plan.session_isolation.md Rev.3 / Phase 0 실측 매트릭스 기준)
ISOLATION_UUID=""
ISOLATION_ROOT=""
ISOLATION_LEVER=""
ISOLATION_SEEDED=""
if [ "$ISOLATE" = "1" ]; then
command -v uuidgen >/dev/null || { echo "ERROR: uuidgen not found (required for --isolate)" >&2; exit 1; }
WORKSPACE_ABS="$(cd "$WORKSPACE" && pwd)"
ISOLATION_UUID="$(uuidgen)"
ISOLATION_ROOT="$WORKSPACE_ABS/.mam/agent_homes/$ISOLATION_UUID"
ISOLATION_LEVER="$(isolation_lever "$AGENT")"
if [ "$DRY_RUN" = "1" ]; then
echo "[dry-run] would provision isolation: lever=$ISOLATION_LEVER root=$ISOLATION_ROOT"
else
ISOLATION_SEEDED="$(provision_isolation "$AGENT" "$ISOLATION_ROOT")"
echo "isolation: lever=$ISOLATION_LEVER root=$ISOLATION_ROOT"
fi
fi
# Config-home isolation was removed in favor of global config + process/UUID isolation.
# herdr 세션 띄우기
LOCAL_BIN="${LOCAL_BIN:-$HOME/.local/bin}"
WRAPPER="$LOCAL_BIN/$SESSION_NAME"
# cmd_full 결정 — T4: isolation 디스패치(env prefix / CLI args)를 시작 명령에 주입.
# 격리 미사용 시 기존 문자열과 byte-identical (V4 회귀 0).
ISO_ENV_PREFIX=""
ISO_CMD_ARGS=""
if [ -n "$ISOLATION_ROOT" ]; then
ISO_ENV_PREFIX="$(isolation_env_prefix "$AGENT" "$ISOLATION_ROOT")"
ISO_CMD_ARGS="$(isolation_cmd_args "$AGENT" "$ISOLATION_ROOT")"
if [ -z "${HERDR_SERVER_NAME:-}" ] || [ "$HERDR_SERVER_NAME" = "default" ]; then
export HERDR_SERVER_NAME="multi-agent-mux"
fi
# Resolve absolute path of the agent command to prevent herdr PATH inheritance issues (especially on macOS)
@@ -161,7 +138,9 @@ if [ "$AGENT" = "cline" ]; then
RESOLVED_BIN="$(command -v cline)"
fi
else
if command -v "$AGENT" >/dev/null 2>&1; then
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
fi
@@ -172,25 +151,27 @@ if [ "$(uname)" = "Darwin" ] && [ -f "$RESOLVED_BIN" ]; then
fi
case "$AGENT" in
claude) CMD_FULL="${ISO_ENV_PREFIX}${RESOLVED_BIN} --dangerously-skip-permissions" ;;
agy) CMD_FULL="${ISO_ENV_PREFIX}${RESOLVED_BIN} --dangerously-skip-permissions" ;;
hermes) CMD_FULL="${ISO_ENV_PREFIX}${RESOLVED_BIN}" ;;
cline) CMD_FULL="${RESOLVED_BIN} -i${ISO_CMD_ARGS:+ $ISO_CMD_ARGS}" ;;
claude) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions" ;;
agy) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions" ;;
hermes) CMD_FULL="${RESOLVED_BIN}" ;;
cline) CMD_FULL="${RESOLVED_BIN} -i" ;;
esac
spawn() {
if [ -z "${HERDR_SERVER_NAME:-}" ] || [ "$HERDR_SERVER_NAME" = "default" ]; then
export HERDR_SERVER_NAME="multi-agent-mux"
fi
case "$AGENT" in
claude)
# 격리 시 wrapper 경로는 env 주입을 운반하지 못하므로 인라인 spawn 강제
if [ "$ISOLATE" != "1" ] && { { [ -x "$WRAPPER" ] && [ "$(basename "$WRAPPER")" != "claude" ]; } || [ "$USE_WRAPPER" = "1" ]; }; then
if { [ -x "$WRAPPER" ] && [ "$(basename "$WRAPPER")" != "claude" ]; } || [ "$USE_WRAPPER" = "1" ]; then
nohup "$WRAPPER" >/dev/null 2>&1 &
disown
else
_herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "$CMD_FULL"
HERDR_SERVER_NAME="$HERDR_SERVER_NAME" _herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "$CMD_FULL"
fi
;;
agy|hermes|cline)
_herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "$CMD_FULL"
HERDR_SERVER_NAME="$HERDR_SERVER_NAME" _herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "$CMD_FULL"
;;
*) echo "ERROR: --agent must be claude, agy, hermes or cline, got: $AGENT" >&2; exit 2 ;;
esac
@@ -209,21 +190,20 @@ cleanup_herdr_on_error() {
if [ $exit_code -ne 0 ]; then
echo "⚠️ Error occurred during initialization. Rolling back and killing herdr session '$SESSION_NAME'..." >&2
_herdr kill-session -t "$SESSION_NAME" 2>/dev/null || true
# T3 rollback: 이 세션용으로 프로비저닝한 격리 홈 제거 (경로 가드 후 rm)
if [ -n "$ISOLATION_ROOT" ] && [ -d "$ISOLATION_ROOT" ]; then
case "$ISOLATION_ROOT" in
*/.mam/agent_homes/*) rm -rf "$ISOLATION_ROOT" ;;
esac
fi
fi
}
trap cleanup_herdr_on_error EXIT
export HERDR_SERVER_NAME="${HERDR_SERVER_NAME:-$(resolve_herdr_server "$SESSION_NAME")}"
# TUI 준비 대기
if ! wait_for_tui_ready "$SESSION_NAME" "$AGENT"; then
echo "ERROR: agent TUI never became ready — aborting (rollback via trap)" >&2
exit 1
fi
if [ "$AGENT" = "claude" ]; then
handle_startup_dialogs "$SESSION_NAME" 15
fi
# pane 메타 캡처
PANE_PID=$(_herdr list-panes -t "$SESSION_NAME" -F '#{pane_pid}' 2>/dev/null || echo "")
@@ -232,11 +212,9 @@ PANE_CMD=$(_herdr list-panes -t "$SESSION_NAME" -F '#{pane_current_command}' 2>/
HERDR_EPOCH=$(date +%s)
NOW_ISO=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
# 시작 명령 (CMD_FULL 은 spawn 전에 isolation 디스패치를 반영해 확정됨 — T4)
# 시작 명령
# NOTE: this must match what `spawn()` actually ran above — env-var-driven
# isolation (HERDR_SERVER_NAME picked up by the lib.sh shim), not a
# `--workspace <label>` flag (real `herdr agent start --workspace` wants an
# actual workspace id like "w2", which HERDR_SERVER_NAME is not).
# herdr server shim (HERDR_SERVER_NAME picked up by the lib.sh shim).
START_CMD="HERDR_SERVER_NAME=${HERDR_SERVER_NAME:-default} herdr new-session -d -s \"$SESSION_NAME\" -x 140 -y 40 -c \"$WORKSPACE\" \"$CMD_FULL\""
# If --onboard is specified, automatically build the onboarding prompt
@@ -285,9 +263,7 @@ atomic_dump_yaml "$AGENT_SESSIONS_YAML" \
HERDR_EPOCH="$HERDR_EPOCH" PANE_PID="$PANE_PID" PANE_CWD="$PANE_CWD" \
CMD_FULL="$CMD_FULL" START_CMD="$START_CMD" CHILD_PID="$CHILD_PID" \
HERDR_SERVER_NAME="${HERDR_SERVER_NAME:-default}" \
DELEGATE_JOB_ID="$DELEGATE_JOB_ID" ROLE="$ROLE" \
ISOLATION_UUID="$ISOLATION_UUID" ISOLATION_ROOT="$ISOLATION_ROOT" \
ISOLATION_LEVER="$ISOLATION_LEVER" ISOLATION_SEEDED="$ISOLATION_SEEDED" <<'PYEOF'
DELEGATE_JOB_ID="$DELEGATE_JOB_ID" ROLE="$ROLE" <<'PYEOF'
name = os.environ['SESSION_NAME']
agent = os.environ['AGENT']
role = os.environ['ROLE']
@@ -331,15 +307,7 @@ entry = {
'kill_command': f'HERDR_SERVER_NAME={server_name} herdr kill-session -t {name}',
}
# T5: isolation 블록 영속화 (all-L2) — resume/resolve/stop 이 재적용의 단일 소스로 사용
iso_uuid = os.environ.get('ISOLATION_UUID', '')
if iso_uuid:
entry['isolation'] = {
'uuid': iso_uuid,
'root': os.environ.get('ISOLATION_ROOT', ''),
'lever': os.environ.get('ISOLATION_LEVER', ''),
'seeded': [x for x in os.environ.get('ISOLATION_SEEDED', '').split(',') if x],
}
if agent == 'claude':
entry['tui'] = {
@@ -319,6 +319,7 @@ exec(os.environ['MAM_VERIFY_PY'])
yaml_path = os.environ['YAML_PATH']
home = os.environ['HOME_DIR']
skills_dir = os.environ.get('SKILLS_DIR', '')
claude_project_dir = os.environ.get('CLAUDE_PROJECT_DIR', f"{home}/.claude/projects")
now_iso = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
@@ -529,8 +530,7 @@ for s in d.get('herdr_sessions', []):
if not cwd:
continue
key = workspace_key(cwd)
iso = s.get('isolation', {}).get('root') if isinstance(s.get('isolation'), dict) else None
proj_dir = f"{iso}/projects/{key}" if iso else f"{claude_project_dir}/{key}"
proj_dir = f"{claude_project_dir}/{key}"
if not os.path.isdir(proj_dir):
continue
jsonls = sorted(glob.glob(f"{proj_dir}/*.jsonl"), key=os.path.getmtime, reverse=True)
@@ -563,16 +563,26 @@ for s in d.get('herdr_sessions', []):
cwd = (s.get('pane') or {}).get('cwd', '')
if not cwd:
continue
iso = s.get('isolation', {}).get('root') if isinstance(s.get('isolation'), dict) else None
conv_dir = f"{iso}/.gemini/antigravity-cli/conversations" if iso else f"{home}/.gemini/antigravity-cli/conversations"
conv_dir = f"{home}/.gemini/antigravity-cli/conversations"
if not os.path.isdir(conv_dir):
continue
dbs = sorted(glob.glob(f"{conv_dir}/*.db"), key=os.path.getmtime, reverse=True)
sibling_claimed = [
other.get('agy_conversation_id_own')
for other in d.get('herdr_sessions', [])
if other is not s
and (other.get('pane') or {}).get('cwd') == cwd
and other.get('status') not in ('stopped', 'terminated')
and other.get('agy_conversation_id_own')
]
s_eval = dict(s)
s_eval['_sibling_claimed_uuids'] = sibling_claimed
valid_candidates = []
for db in dbs:
uuid = os.path.basename(db)[:-3]
if verify_session_uuid(cwd, 'agy', uuid, s, mode="discover"):
if verify_session_uuid(cwd, 'agy', uuid, s_eval, mode="discover"):
valid_candidates.append(uuid)
if len(valid_candidates) == 1:
@@ -597,8 +607,7 @@ for s in d.get('herdr_sessions', []):
cwd = (s.get('pane') or {}).get('cwd', '')
if not cwd:
continue
iso = s.get('isolation', {}).get('root') if isinstance(s.get('isolation'), dict) else None
hdb = f"{iso}/.hermes/state.db" if iso else f"{home}/.hermes/state.db"
hdb = f"{home}/.hermes/state.db"
if not os.path.exists(hdb):
continue
@@ -636,8 +645,7 @@ for s in d.get('herdr_sessions', []):
cwd = (s.get('pane') or {}).get('cwd', '')
if not cwd:
continue
iso = s.get('isolation', {}).get('root') if isinstance(s.get('isolation'), dict) else None
sessions_dir = f"{iso}/sessions" if iso else f"{home}/.cline/data/sessions"
sessions_dir = f"{home}/.cline/data/sessions"
if not os.path.isdir(sessions_dir):
continue
candidates = []
+4 -54
View File
@@ -64,7 +64,7 @@ WORKSPACE=/path/to/project
AGENT=claude # or agy or hermes
SESSION_NAME=<workspace>-creator-<agent> # same convention as multi-agent-mux-create
# Resolve the isolated herdr server name & load isolation utils
# Resolve the isolated herdr server name & load common utils
source .agents/skills/lib.sh
# 1. Resolve the session id (T5: pass session name for target-row isolation check)
@@ -84,45 +84,7 @@ if herdr has-session -t "$SESSION_NAME" 2>/dev/null; then
exec herdr agent attach "$SESSION_NAME"
fi
# 3. Resolve isolation settings for this session (T4/T5 re-apply)
# _get_session_isolation resolves isolation block for the session row
ISO_ROOT=""
ISO_ENV=""
ISO_ARGS=""
ISO_DATA=$(env_python "$AGENT_SESSIONS_YAML" SESSION_NAME="$SESSION_NAME" <<'PYEOF'
import os, json, yaml, sqlite3
name = os.environ['SESSION_NAME']
yaml_path = os.environ['YAML_PATH']
db_path = os.path.splitext(yaml_path)[0] + '.db'
d = {}
try:
if os.path.exists(db_path):
conn = sqlite3.connect(db_path, timeout=60.0)
row = conn.execute('SELECT data FROM sessions WHERE name=?', (name,)).fetchone()
if row:
s = json.loads(row[0])
print(json.dumps(s.get('isolation') or {}))
raise SystemExit(0)
elif os.path.exists(yaml_path):
with open(yaml_path) as f:
d = yaml.safe_load(f) or {}
except Exception:
pass
for s in d.get('herdr_sessions', []):
if s.get('name') == name:
print(json.dumps(s.get('isolation') or {}))
raise SystemExit(0)
print("{}")
PYEOF
)
ISO_ROOT=$(printf '%s' "$ISO_DATA" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("root",""))')
if [ -n "$ISO_ROOT" ]; then
ISO_ENV="$(isolation_env_prefix "$AGENT" "$ISO_ROOT")"
ISO_ARGS="$(isolation_cmd_args "$AGENT" "$ISO_ROOT")"
echo "Re-applying isolation: root=$ISO_ROOT env=$ISO_ENV args=$ISO_ARGS"
fi
# Determine CMD_FULL with isolation applied
# 3. Determine CMD_FULL
case "$AGENT" in
claude) CMD_FULL="claude --dangerously-skip-permissions -r $UUID" ;;
agy) CMD_FULL="agy --dangerously-skip-permissions --conversation $UUID" ;;
@@ -130,22 +92,10 @@ case "$AGENT" in
cline) CMD_FULL="cline -i --id $UUID" ;;
esac
# Prepend env prefix and append command args (T4)
if [ -n "$ISO_ENV" ]; then
CMD_FULL="$ISO_ENV $CMD_FULL"
fi
if [ -n "$ISO_ARGS" ]; then
CMD_FULL="$CMD_FULL $ISO_ARGS"
fi
# 4. Spawn new herdr session + run agent with the saved id (and re-applied isolation)
# 4. Spawn new herdr session + run agent with the saved id
case "$AGENT" in
claude)
if [ -z "$ISO_ROOT" ] && [ -x "$HOME/.local/bin/canary-projects-multi-agent-mux-creator-claude" ]; then
START_CMD="herdr new-session -d -s \"$SESSION_NAME\" -x 140 -y 40 -c \"$WORKSPACE\" \"$HOME/.local/bin/canary-projects-multi-agent-mux-creator-claude\""
else
START_CMD="herdr new-session -d -s \"$SESSION_NAME\" -x 140 -y 40 -c \"$WORKSPACE\" \"$CMD_FULL\""
fi
START_CMD="herdr new-session -d -s \"$SESSION_NAME\" -x 140 -y 40 -c \"$WORKSPACE\" \"$CMD_FULL\""
eval "$START_CMD"
# auto-handle trust / bypass dialogs
handle_startup_dialogs "$SESSION_NAME" 20
@@ -9,7 +9,7 @@ usage() {
Usage: $0 --workspace <path> --agent <claude|agy|hermes|cline> --session <name> [--dry-run]
Options:
--dry-run Simulates resume flow (resolves binary, environment, isolation) without writing
--dry-run Simulates resume flow (resolves binary, environment) without writing
any updates to YAML or DB. Safe to execute inside active write transactions.
EOF
}
@@ -60,45 +60,6 @@ if herdr has-session -t "$SESSION_NAME" 2>/dev/null; then
exit 0
fi
# 3. Resolve isolation settings for this session
ISO_ROOT=""
ISO_ENV=""
ISO_ARGS=""
ISO_DATA=$(env_python "$AGENT_SESSIONS_YAML" SESSION_NAME="$SESSION_NAME" <<'PYEOF'
import os, json, yaml, sqlite3
name = os.environ['SESSION_NAME']
yaml_path = os.environ['YAML_PATH']
db_path = os.path.splitext(yaml_path)[0] + '.db'
d = {}
try:
if os.path.exists(db_path):
conn = sqlite3.connect(db_path, timeout=60.0)
row = conn.execute('SELECT data FROM sessions WHERE name=?', (name,)).fetchone()
if row:
s = json.loads(row[0])
print(json.dumps(s.get('isolation') or {}))
raise SystemExit(0)
elif os.path.exists(yaml_path):
with open(yaml_path) as f:
d = yaml.safe_load(f) or {}
except Exception:
pass
for s in d.get('herdr_sessions', []):
if s.get('name') == name:
print(json.dumps(s.get('isolation') or {}))
raise SystemExit(0)
print("{}")
PYEOF
)
ISO_ROOT=$(printf '%s' "$ISO_DATA" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("root",""))')
if [ -n "$ISO_ROOT" ]; then
ISO_ENV="$(isolation_env_prefix "$AGENT" "$ISO_ROOT")"
ISO_ARGS="$(isolation_cmd_args "$AGENT" "$ISO_ROOT")"
echo "Re-applying isolation: root=$ISO_ROOT env=$ISO_ENV args=$ISO_ARGS"
fi
# Resolve absolute path of the agent command to prevent herdr PATH inheritance issues (especially on macOS)
RESOLVED_BIN="$AGENT"
if [ "$AGENT" = "cline" ]; then
@@ -116,7 +77,7 @@ if [ "$(uname)" = "Darwin" ] && [ -f "$RESOLVED_BIN" ]; then
xattr -d com.apple.quarantine "$RESOLVED_BIN" 2>/dev/null || true
fi
# Determine CMD_FULL with isolation applied
# Determine CMD_FULL
case "$AGENT" in
claude) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions -r $UUID" ;;
agy) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions --conversation $UUID" ;;
@@ -125,20 +86,6 @@ case "$AGENT" in
*) echo "ERROR: unsupported agent: $AGENT" >&2; exit 2 ;;
esac
# Prepend env prefix and append command args (T4)
if [ -n "$ISO_ENV" ]; then
CMD_FULL="$ISO_ENV $CMD_FULL"
fi
if [ -n "$ISO_ARGS" ]; then
CMD_FULL="$CMD_FULL $ISO_ARGS"
fi
# Validate isolation root if it was configured
if [ -n "$ISO_ROOT" ] && [ ! -d "$ISO_ROOT" ]; then
echo "ERROR: Isolation root directory does not exist: $ISO_ROOT" >&2
exit 1
fi
# Validate binary exists and is executable
if [ -f "$RESOLVED_BIN" ] || [[ "$RESOLVED_BIN" == /* ]] || [[ "$RESOLVED_BIN" == ~/* ]]; then
if [ ! -x "$RESOLVED_BIN" ]; then
+3 -5
View File
@@ -53,10 +53,9 @@ $ bash .agents/skills/multi-agent-mux-create/scripts/create_session.sh \
--agent claude \
--role developer \
--session my-project-dev-claude \
--isolate \
--herdr-workspace multi-agent-mux
--herdr-server multi-agent-mux
```
* `--isolate` 옵션을 주면 `.mam/agent_homes/<uuid>/` 하위에 로그인 및 설정은 유지하되 대화 내역은 격리되는 홈이 형성됩니다.
* 모든 세션은 사용자의 전역 에이전트 설정(Global Config)을 공유하며, herdr 프로세스 격리 및 대화 UUID 단위로 독립 구동됩니다.
### 2) 세션 접속 (Attach)
백그라운드에서 구동된 에이전트 TUI 화면에 들어갑니다.
@@ -66,7 +65,7 @@ $ herdr session attach my-project-dev-claude
* **화면 탈출**: 대화 중 세션을 유지한 채 터미널로 돌아오려면 `Ctrl + B`를 누른 뒤 `D` 키를 차례로 입력합니다.
### 3) 에이전트 상태 복원 (Resume)
세션이 중지되었거나, 호스트 재기동으로 herdr 서버가 소멸한 경우에도 이전 대화 ID 및 격리 디렉토리를 원자적으로 이어받아 다시 기동할 수 있습니다.
세션이 중지되었거나, 호스트 재기동으로 herdr 서버가 소멸한 경우에도 이전 대화 ID를 원자적으로 이어받아 다시 기동할 수 있습니다.
```bash
# 1단계: 복원 대상 세션의 UUID 자동 조회 (DB/YAML 레지스트리 기반)
$ WORKSPACE="/path/to/your/project"
@@ -80,7 +79,6 @@ $ UUID=$(bash .agents/skills/multi-agent-mux-resume/scripts/resolve_session_id.s
$ [ -n "$UUID" ] || { echo "[ERROR] 매칭되는 활성 세션 이력이 없습니다. create_session.sh를 통해 먼저 세션을 생성해 주세요."; exit 1; }
# 2단계: 세션 재기동 (이전 대화 컨텍스트 복원 기동)
# (주의: 만약 create 시 격리(--isolate) 세션으로 생성했다면, CLAUDE_CONFIG_DIR 환경변수를 YAML에 기록된 isolation.root 경로로 지정하여 띄워야 합니다. 상세 격리 복원 커맨드는 .agents/skills/multi-agent-mux-resume/SKILL.md 문서를 필독해 주세요.)
$ herdr run -d --name "$SESSION_NAME" --workspace "$WORKSPACE" -- \
"claude --dangerously-skip-permissions -r $UUID"