feat(scope): implement workspace-scoped session isolation (A-1) and native Herdr session naming (A-5)

This commit is contained in:
2026-08-05 09:31:57 +09:00
parent 793a221587
commit 51dcf56c80
7 changed files with 89 additions and 57 deletions
+12 -14
View File
@@ -48,7 +48,7 @@ LOCAL_BIN="${LOCAL_BIN:-$HOME/.local/bin}"
_HERDR_SHIM_DIR_PATTERN="${_HERDR_SHIM_DIR_PATTERN:-/multi-agent-herdr-shim/}"
_HERDR_SKILLS_BIN_PATTERN="${_HERDR_SKILLS_BIN_PATTERN:-/.agents/skills/.bin}"
HERDR_SERVER_NAME="${HERDR_SERVER_NAME:-default}"
HERDR_SESSION_NAME="${HERDR_SESSION_NAME:-default}"
_resolve_real_herdr_path() {
_REAL_HERDR_PATH="herdr"
@@ -90,23 +90,17 @@ _resolve_real_herdr() {
}
REAL_HERDR=$(_resolve_real_herdr)
# Support parsing -L <server> before the subcommand
# Support parsing -L <session> before the subcommand
while [ "${1:-}" = "-L" ]; do
if [ $# -lt 2 ]; then
echo "herdr shim: -L requires an argument" >&2
exit 1
fi
export HERDR_SERVER_NAME="$2"
export HERDR_SESSION_NAME="$2"
shift 2
done
# Herdr's real isolation boundary is `--session <name>` (a whole separate
# server + socket, like tmux `-L`) — NOT `workspace create --label`, which is
# just a named subdivision inside ONE server and provides no actual isolation
# (agent/pane commands are server-global regardless of workspace). When
# HERDR_SERVER_NAME names a non-default session, make sure its headless server
# is actually running, then scope every real herdr call to it via `--session`.
_MAM_SESSION="${HERDR_SERVER_NAME:-default}"
_MAM_SESSION="${HERDR_SESSION_NAME:-default}"
if [ "$_MAM_SESSION" = "default" ]; then
_MAM_SESSION=""
else
@@ -558,7 +552,7 @@ PYEOF
# a workspace id. Real isolation is `--session <name>` (see `_MAM_SESSION` in the
# generated wrapper) — a workspace label match provides no actual isolation
# since agent/pane commands are server-global regardless of workspace.
resolve_herdr_workspace() {
resolve_herdr_session() {
local session_name="$1"
MAM_STATE_JSON="$(load_state_json)" SESSION_NAME="$session_name" python3 -c "
import sys, os, json
@@ -566,16 +560,20 @@ name = os.environ['SESSION_NAME']
d = json.loads(os.environ.get('MAM_STATE_JSON', '{}'))
for s in d.get('herdr_sessions', []):
if s.get('name') == name:
print(s.get('herdr_workspace') or s.get('herdr_server') or 'default')
print(s.get('herdr_session') or 'default')
sys.exit(0)
fallback = os.environ.get('HERDR_SERVER_NAME', '')
fallback = os.environ.get('HERDR_SESSION_NAME', '')
if not fallback or fallback == 'default':
pwd = os.path.abspath(os.getcwd())
fallback = os.path.basename(pwd)
fallback = 'mam-' + os.path.basename(pwd).lower().replace('_', '-')
print(fallback or 'default')
"
}
resolve_herdr_workspace() {
resolve_herdr_session "$@"
}
# ---------------------------------------------------------------------------
# derive_session_name <workspace> <agent>
#
@@ -61,7 +61,7 @@ while [ $# -gt 0 ]; do
--session) SESSION_NAME="$2"; shift 2 ;;
--wrapper) USE_WRAPPER=1; shift ;;
--dry-run) DRY_RUN=1; shift ;;
--herdr-server) HERDR_SERVER_OPT="$2"; shift 2 ;;
--herdr-session|--herdr-server) HERDR_SERVER_OPT="$2"; shift 2 ;;
--submit-job) SUBMIT_JOB_PROMPT="$2"; shift 2 ;;
--onboard) ONBOARD=1; shift ;;
--no-onboard) ONBOARD=0; shift ;;
@@ -128,8 +128,8 @@ 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="mam-$ws_slug"
if [ -z "${HERDR_SESSION_NAME:-}" ] || [ "$HERDR_SESSION_NAME" = "default" ]; then
export HERDR_SESSION_NAME="mam-$ws_slug"
fi
# Resolve absolute path of the agent command to prevent herdr PATH inheritance issues (especially on macOS)
@@ -256,14 +256,14 @@ atomic_dump_yaml "$AGENT_SESSIONS_YAML" \
SESSION_NAME="$SESSION_NAME" AGENT="$AGENT" NOW_ISO="$NOW_ISO" \
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}" \
HERDR_SESSION_NAME="${HERDR_SESSION_NAME:-default}" \
DELEGATE_JOB_ID="$DELEGATE_JOB_ID" ROLE="$ROLE" <<'PYEOF'
name = os.environ['SESSION_NAME']
agent = os.environ['AGENT']
role = os.environ['ROLE']
pid = os.environ.get('PANE_PID', '')
epoch = os.environ.get('HERDR_EPOCH', '')
server_name = os.environ.get('HERDR_SERVER_NAME', 'default')
server_name = os.environ.get('HERDR_SESSION_NAME', 'default')
server_opt = f"-L {server_name} " if server_name and server_name != 'default' else ""
sessions = d.setdefault('herdr_sessions', [])
@@ -282,7 +282,7 @@ entry = {
'role': role,
'herdr_session_created_at': os.environ['NOW_ISO'],
'herdr_session_epoch': int(epoch) if epoch.isdigit() else 0,
'herdr_server': server_name,
'herdr_session': server_name,
'delegate_job_id': os.environ.get('DELEGATE_JOB_ID', '') or None,
'pane': {
'index': 0,
@@ -292,13 +292,8 @@ entry = {
'cwd': os.environ['PANE_CWD'],
},
'start_command': os.environ['START_CMD'],
# NOTE: `herdr session attach/stop/delete` operate on whole herdr
# *sessions* (server instances, e.g. "default") — NOT on an individual
# agent by its MAM name. Use the lib.sh tmux-compat shim commands
# instead (`source .agents/skills/lib.sh` first), scoped via the same
# env-var-driven isolation as start_command above.
'attach_command': f'HERDR_SERVER_NAME={server_name} herdr agent attach {name}',
'kill_command': f'HERDR_SERVER_NAME={server_name} herdr kill-session -t {name}',
'attach_command': f'HERDR_SESSION_NAME={server_name} herdr agent attach {name}',
'kill_command': f'HERDR_SESSION_NAME={server_name} herdr kill-session -t {name}',
}
@@ -474,6 +474,11 @@ if herdr_confirmed:
pm = pane_meta(name, srv)
if not pm:
continue
# A-1 게이트: pane cwd가 현재 workspace_root 하위가 아니면 타 워크스페이스 세션으로 판단하여 오등록 방지
pane_cwd_abs = os.path.realpath(pm['cwd']) if pm.get('cwd') else ''
ws_root_abs = os.path.realpath(workspace_root)
if not pane_cwd_abs or not (pane_cwd_abs == ws_root_abs or pane_cwd_abs.startswith(ws_root_abs + os.sep)):
continue
if agent == 'claude':
cmd_full = 'claude --dangerously-skip-permissions'
elif agent == 'agy':
@@ -488,11 +493,10 @@ if herdr_confirmed:
'status': 'running',
'herdr_session_created_at': datetime.fromtimestamp(t['created'], tz=timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'),
'herdr_session_epoch': t['created'],
'herdr_server': srv,
'herdr_session': srv,
'pane': {'index': 0, 'pid': pm['pid'], 'cmd': agent, 'cmd_full': cmd_full, 'cwd': pm['cwd']},
# P2: cwd 인용
'start_command': f'herdr {server_opt}new-session -d -s "{name}" -x 140 -y 40 -c "{pm["cwd"]}" "{cmd_full}"',
'attach_command': f'herdr {server_opt}agent attach {name}',
'start_command': f'HERDR_SESSION_NAME={srv} herdr new-session -d -s "{name}" -x 140 -y 40 -c "{pm["cwd"]}" "{cmd_full}"',
'attach_command': f'HERDR_SESSION_NAME={srv} herdr agent attach {name}',
'kill_command': f'herdr {server_opt}kill-session -t {name}',
'last_visible_status': 'running',
'last_visible_note': 'auto-registered by monitor',
@@ -44,8 +44,8 @@ if [ -z "$UUID" ]; then
exit 1
fi
HERDR_SERVER_NAME="$(resolve_herdr_workspace "$SESSION_NAME")"
export HERDR_SERVER_NAME
HERDR_SESSION_NAME="$(resolve_herdr_session "$SESSION_NAME")"
export HERDR_SESSION_NAME
# 2. If herdr is alive, print warning or attach.
if herdr has-session -t "$SESSION_NAME" 2>/dev/null; then
@@ -37,8 +37,8 @@ done
[ -n "$UUID" ] || { echo "ERROR: --uuid required" >&2; exit 2; }
[ -f "$AGENT_SESSIONS_YAML" ] || { echo "ERROR: $AGENT_SESSIONS_YAML not found" >&2; exit 1; }
HERDR_SERVER_NAME="$(resolve_herdr_workspace "$SESSION_NAME")"
export HERDR_SERVER_NAME
HERDR_SESSION_NAME="$(resolve_herdr_session "$SESSION_NAME")"
export HERDR_SESSION_NAME
# --agent 미지정 시 이름 suffix 로 fallback (P1-F: 가능하면 --agent 명시)
if [ -z "$AGENT" ]; then
@@ -101,19 +101,21 @@ for s in d.get('herdr_sessions', []):
break
if target is None:
server_name = os.environ.get('HERDR_SERVER_NAME', 'multi-agent-mux')
pwd = os.path.abspath(ws_root)
default_server = 'mam-' + os.path.basename(pwd).lower().replace('_', '-')
server_name = os.environ.get('HERDR_SESSION_NAME', default_server)
target = {
'name': name,
'status': 'running',
'role': role,
'herdr_session_created_at': now,
'herdr_session_epoch': epoch,
'herdr_server': server_name,
'herdr_session': server_name,
'delegate_job_id': None,
'pane': {'index': 0, 'pid': int(pane_pid) if pane_pid.isdigit() else 0, 'cmd': agent, 'cwd': ws_root},
'start_command': f'HERDR_SERVER_NAME={server_name} herdr agent attach {name}',
'attach_command': f'HERDR_SERVER_NAME={server_name} herdr agent attach {name}',
'kill_command': f'HERDR_SERVER_NAME={server_name} herdr kill-session -t {name}',
'start_command': f'HERDR_SESSION_NAME={server_name} herdr agent attach {name}',
'attach_command': f'HERDR_SESSION_NAME={server_name} herdr agent attach {name}',
'kill_command': f'HERDR_SESSION_NAME={server_name} herdr kill-session -t {name}',
}
d.setdefault('herdr_sessions', []).append(target)