feat(isolation): implement Phase 1-3 session isolation with stop purge and resume safety
This commit is contained in:
@@ -35,6 +35,8 @@ Options:
|
||||
--tmux-server NAME specify isolated tmux server name
|
||||
--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
|
||||
--isolate provision an isolated per-session state home (.mam/agent_homes/<uuid>)
|
||||
so same-CLI multi-role sessions never share a conversation id
|
||||
-h, --help this help
|
||||
EOF
|
||||
}
|
||||
@@ -48,6 +50,7 @@ DRY_RUN=0
|
||||
TMUX_SERVER_OPT=""
|
||||
SUBMIT_JOB_PROMPT=""
|
||||
ONBOARD=0
|
||||
ISOLATE=0
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
@@ -60,6 +63,7 @@ while [ $# -gt 0 ]; do
|
||||
--tmux-server) TMUX_SERVER_OPT="$2"; shift 2 ;;
|
||||
--submit-job) SUBMIT_JOB_PROMPT="$2"; shift 2 ;;
|
||||
--onboard) ONBOARD=1; shift ;;
|
||||
--isolate) ISOLATE=1; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "ERROR: unknown arg: $1" >&2; usage; exit 2 ;;
|
||||
esac
|
||||
@@ -111,28 +115,58 @@ if _tmux 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
|
||||
|
||||
# tmux 세션 띄우기
|
||||
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")"
|
||||
fi
|
||||
case "$AGENT" in
|
||||
claude) CMD_FULL="${ISO_ENV_PREFIX}claude --dangerously-skip-permissions" ;;
|
||||
agy) CMD_FULL="${ISO_ENV_PREFIX}agy --dangerously-skip-permissions" ;;
|
||||
hermes) CMD_FULL="${ISO_ENV_PREFIX}hermes" ;;
|
||||
cline) CMD_FULL="cline -i${ISO_CMD_ARGS:+ $ISO_CMD_ARGS}" ;;
|
||||
esac
|
||||
|
||||
spawn() {
|
||||
case "$AGENT" in
|
||||
claude)
|
||||
if { [ -x "$WRAPPER" ] && [ "$(basename "$WRAPPER")" != "claude" ]; } || [ "$USE_WRAPPER" = "1" ]; then
|
||||
# 격리 시 wrapper 경로는 env 주입을 운반하지 못하므로 인라인 spawn 강제
|
||||
if [ "$ISOLATE" != "1" ] && { { [ -x "$WRAPPER" ] && [ "$(basename "$WRAPPER")" != "claude" ]; } || [ "$USE_WRAPPER" = "1" ]; }; then
|
||||
nohup "$WRAPPER" >/dev/null 2>&1 &
|
||||
disown
|
||||
else
|
||||
_tmux new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "claude --dangerously-skip-permissions"
|
||||
_tmux new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "$CMD_FULL"
|
||||
fi
|
||||
;;
|
||||
agy)
|
||||
_tmux new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "agy --dangerously-skip-permissions"
|
||||
;;
|
||||
hermes)
|
||||
_tmux new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "hermes"
|
||||
;;
|
||||
cline)
|
||||
_tmux new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "cline -i"
|
||||
agy|hermes|cline)
|
||||
_tmux 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
|
||||
@@ -151,6 +185,12 @@ cleanup_tmux_on_error() {
|
||||
if [ $exit_code -ne 0 ]; then
|
||||
echo "⚠️ Error occurred during initialization. Rolling back and killing tmux session '$SESSION_NAME'..." >&2
|
||||
_tmux 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_tmux_on_error EXIT
|
||||
@@ -165,15 +205,7 @@ PANE_CMD=$(_tmux list-panes -t "$SESSION_NAME" -F '#{pane_current_command}' 2>/d
|
||||
TMUX_EPOCH=$(date +%s)
|
||||
NOW_ISO=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
|
||||
|
||||
# cmd_full 결정
|
||||
case "$AGENT" in
|
||||
claude) CMD_FULL='claude --dangerously-skip-permissions' ;;
|
||||
agy) CMD_FULL='agy --dangerously-skip-permissions' ;;
|
||||
hermes) CMD_FULL='hermes' ;;
|
||||
cline) CMD_FULL='cline -i' ;;
|
||||
esac
|
||||
|
||||
# 시작 명령
|
||||
# 시작 명령 (CMD_FULL 은 spawn 전에 isolation 디스패치를 반영해 확정됨 — T4)
|
||||
local_tmux="tmux"
|
||||
if [ -n "${TMUX_SERVER_NAME:-}" ] && [ "$TMUX_SERVER_NAME" != "default" ]; then
|
||||
local_tmux="tmux -L $TMUX_SERVER_NAME"
|
||||
@@ -181,10 +213,10 @@ fi
|
||||
|
||||
case "$AGENT" in
|
||||
claude)
|
||||
if [ -x "$WRAPPER" ]; then
|
||||
if [ "$ISOLATE" != "1" ] && [ -x "$WRAPPER" ]; then
|
||||
START_CMD="$WRAPPER # ~/.local/bin 의 래퍼"
|
||||
else
|
||||
START_CMD="$local_tmux new-session -d -s \"$SESSION_NAME\" -x 140 -y 40 -c \"$WORKSPACE\" \"claude --dangerously-skip-permissions\""
|
||||
START_CMD="$local_tmux new-session -d -s \"$SESSION_NAME\" -x 140 -y 40 -c \"$WORKSPACE\" \"$CMD_FULL\""
|
||||
fi
|
||||
;;
|
||||
agy|hermes|cline)
|
||||
@@ -238,7 +270,9 @@ atomic_dump_yaml "$AGENT_SESSIONS_YAML" \
|
||||
TMUX_EPOCH="$TMUX_EPOCH" PANE_PID="$PANE_PID" PANE_CWD="$PANE_CWD" \
|
||||
CMD_FULL="$CMD_FULL" START_CMD="$START_CMD" CHILD_PID="$CHILD_PID" \
|
||||
TMUX_SERVER_NAME="${TMUX_SERVER_NAME:-default}" \
|
||||
DELEGATE_JOB_ID="$DELEGATE_JOB_ID" ROLE="$ROLE" <<'PYEOF'
|
||||
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'
|
||||
name = os.environ['SESSION_NAME']
|
||||
agent = os.environ['AGENT']
|
||||
role = os.environ['ROLE']
|
||||
@@ -277,6 +311,16 @@ entry = {
|
||||
'kill_command': f'tmux {server_opt}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'] = {
|
||||
'model': '(unknown — capture after first message)',
|
||||
|
||||
Reference in New Issue
Block a user