From 7c8267240d62faf4b4a073e6b91bfed2b54b6a2e Mon Sep 17 00:00:00 2001 From: Godopu Date: Sun, 28 Jun 2026 10:27:36 +0900 Subject: [PATCH] feat: enforce required agent roles at creation and role immutability in registry --- .agents/skills/lib.sh | 9 +++++++++ .agents/skills/multi-agent-mux-create/SKILL.md | 2 +- .../multi-agent-mux-create/scripts/create_session.sh | 10 ++++++++-- SKILL_FEATURES.md | 3 ++- 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/.agents/skills/lib.sh b/.agents/skills/lib.sh index 1888acd..f4fb789 100644 --- a/.agents/skills/lib.sh +++ b/.agents/skills/lib.sh @@ -305,6 +305,8 @@ def _validate(d): raise SystemExit(f"VALIDATE: tmux_sessions[{i}] not a mapping") if not s.get('name') or not s.get('status'): raise SystemExit(f"VALIDATE: tmux_sessions[{i}] missing name/status") + if s.get('role') is not None and (not isinstance(s['role'], str) or not s['role'].strip()): + raise SystemExit(f"VALIDATE: tmux_sessions[{i}] {s.get('name')!r} role must be a non-empty string") if s['status'] not in valid: raise SystemExit(f"VALIDATE: tmux_sessions[{i}] {s.get('name')!r} bad status {s['status']!r}") if not isinstance(s.get('pane'), dict): @@ -366,10 +368,17 @@ try: d['tmux_sessions'] = [] old_terminals = get_terminal_set(d) + old_roles = {s.get('name'): s.get('role') for s in db_sessions if s.get('role')} # --- caller mutation (module scope: sees d, yaml, os, glob, subprocess) --- exec(compile(os.environ['AGENT_SESSIONS_MUTATION'], '', 'exec'), globals()) + # Role immutability check + for s in d.get('tmux_sessions', []): + name = s.get('name') + if name in old_roles and s.get('role') != old_roles[name]: + raise SystemExit(f"VALIDATE: role of session {name!r} cannot be modified from {old_roles[name]!r} to {s.get('role')!r}") + _validate(d) # Separate globals and sessions for normalization diff --git a/.agents/skills/multi-agent-mux-create/SKILL.md b/.agents/skills/multi-agent-mux-create/SKILL.md index 70983e9..d0eed2e 100644 --- a/.agents/skills/multi-agent-mux-create/SKILL.md +++ b/.agents/skills/multi-agent-mux-create/SKILL.md @@ -173,7 +173,7 @@ Use the `agent-sessions-yaml-edit` script in `scripts/` to safely append (preser ```bash bash .agents/skills/multi-agent-mux-create/scripts/create_session.sh \ - --workspace "$WORKSPACE" --agent "$AGENT" --session "$SESSION_NAME" + --workspace "$WORKSPACE" --agent "$AGENT" --role "$ROLE" --session "$SESSION_NAME" ``` The script handles the YAML append, pane capture, and the `last_visible_status` placeholder. diff --git a/.agents/skills/multi-agent-mux-create/scripts/create_session.sh b/.agents/skills/multi-agent-mux-create/scripts/create_session.sh index 6204b52..8691ca4 100755 --- a/.agents/skills/multi-agent-mux-create/scripts/create_session.sh +++ b/.agents/skills/multi-agent-mux-create/scripts/create_session.sh @@ -23,11 +23,12 @@ source "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/lib.sh" usage() { cat < --agent [options] +Usage: $0 --workspace --agent --role [options] Options: --workspace PATH project directory (required) --agent AGENT claude | agy | hermes | cline (required) + --role ROLE assigned role (required) --session NAME tmux session name (default: derived from workspace) --wrapper force use of ~/.local/bin/ wrapper even if not present --dry-run print commands without executing @@ -39,6 +40,7 @@ EOF WORKSPACE="" AGENT="" +ROLE="" SESSION_NAME="" USE_WRAPPER=0 DRY_RUN=0 @@ -49,6 +51,7 @@ while [ $# -gt 0 ]; do case "$1" in --workspace) WORKSPACE="$2"; shift 2 ;; --agent) AGENT="$2"; shift 2 ;; + --role) ROLE="$2"; shift 2 ;; --session) SESSION_NAME="$2"; shift 2 ;; --wrapper) USE_WRAPPER=1; shift ;; --dry-run) DRY_RUN=1; shift ;; @@ -66,6 +69,7 @@ fi # Preflight [ -n "$WORKSPACE" ] || { echo "ERROR: --workspace required" >&2; usage; exit 2; } [ -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 tmux >/dev/null || { echo "ERROR: tmux not installed" >&2; exit 1; } command -v "$AGENT" >/dev/null || { echo "ERROR: $AGENT CLI not in PATH" >&2; exit 1; } @@ -212,9 +216,10 @@ 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" <<'PYEOF' + 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('TMUX_EPOCH', '') server_name = os.environ.get('TMUX_SERVER_NAME', 'default') @@ -233,6 +238,7 @@ sessions[:] = [s for s in sessions if s.get('name') != name] entry = { 'name': name, 'status': 'running', + 'role': role, 'tmux_session_created_at': os.environ['NOW_ISO'], 'tmux_session_epoch': int(epoch) if epoch.isdigit() else 0, 'tmux_server': server_name, diff --git a/SKILL_FEATURES.md b/SKILL_FEATURES.md index 5736e4d..0600866 100644 --- a/SKILL_FEATURES.md +++ b/SKILL_FEATURES.md @@ -44,7 +44,8 @@ * `hermes`: `hermes status`를 통한 연동 상태 검증 * `cline`: `cline history --json` 동작 및 설정 상태 사전 검증 * **Tmux 세션 생성 및 초기화**: 에이전트별 최적화된 화면 크기(`-x 140 -y 40`) 및 작업 디렉터리(`-c`)를 적용해 세션 백그라운드 생성. - * **초기 상태 YAML 등록**: `status: running`, `pane` 세부정보(인덱스, PID, CWD, CMD_FULL), 시작 명령 및 `mcp_attachments` 기록. + * **초기 상태 YAML 등록**: 사용자 필수 지정 역할(`--role`), `status: running`, `pane` 세부정보(인덱스, PID, CWD, CMD_FULL), 시작 명령 및 `mcp_attachments` 기록. + * **역할 불변성 보장**: 에이전트 생성 시 부여된 역할(`role`)은 사후 수정이 불가하며, 임의 변경 시도 시 데이터 검증(`atomic_dump_yaml`) 단계에서 예외 처리되어 방어됨. ### 3.2. `multi-agent-mux-resume` (재개 스킬) * **용도**: 중지되었거나 유실된 에이전트의 이전 컨텍스트 그대로 Tmux 세션 및 TUI 연결 복원.