fix(loop): eliminate tmp script copy and trap leak in delegate_job_safe (P2-1/B-6)

This commit is contained in:
2026-08-15 10:46:39 +09:00
parent af3dc1600c
commit b490713471
4 changed files with 211 additions and 18 deletions
@@ -87,16 +87,27 @@ fi
MAM_LOOP_MARKER="${MAM_LOOP_MARKER:-$REPO_ROOT/.mam/loop-guard-active}"
_mam_release_guard() { mam_release_loop_lock "$MAM_LOOP_MARKER" || true; }
# Runs the delegate-job wrapper in place. Deliberately creates no copy and
# installs no trap:
# * a copy inside .agents/skills/ pollutes the source tree and leaks on
# SIGKILL (B-6). It never protected across turns anyway — the copy is made
# per call, so a wrapper broken in turn N is copied broken in turn N+1;
# * every call site is a command substitution, so a trap set here fires when
# that subshell ends. `$$` is still the parent's pid there, so
# _mam_release_guard passed its ownership check and dropped the loop lock
# after the first delegated job (D1).
# The callers' own "Failed to register ..." branches are unreachable when the
# wrapper exits non-zero (set -e aborts the assignment first), so the diagnosis
# has to be emitted here.
delegate_job_safe() {
local orig_script="$REPO_ROOT/.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job"
local tmp_script
tmp_script="${orig_script}.${RANDOM}_$$.tmp"
cp "$orig_script" "$tmp_script"
trap 'rm -f "$tmp_script"' EXIT INT TERM HUP
local rc=0
bash "$tmp_script" "$@" || rc=$?
rm -f "$tmp_script"
trap _mam_release_guard EXIT INT TERM HUP
bash "$orig_script" "$@" || rc=$?
if [ "$rc" -ne 0 ]; then
log_error "delegate_job_safe failed (exit $rc): $orig_script"
log_error " if this loop edits framework skills in place, check that file's syntax:"
log_error " bash -n \"$orig_script\""
fi
return $rc
}
@@ -134,6 +145,7 @@ case "$_mam_acquire_rc" in
;;
esac
trap _mam_release_guard EXIT INT TERM HUP
rm -f "$REPO_ROOT/.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job".*.tmp 2>/dev/null || true
# --all-reviewer silently takes precedence over an explicit --reviewer list;
# warn so the discarded list isn't mistaken for having been honored (P2-1).
+21 -3
View File
@@ -74,8 +74,20 @@
- **실측(ecef05a3)**: `df --output=target` 은 여전히 `rc=64` 로 실패하나 `ea36e81` 에서 추가된 `df -P` 폴백이 정상 동작합니다(macOS 실측: `/System/Volumes/Data`). **결론("감지 실패")은 더 이상 참이 아니므로 종결을 권고합니다.**
- **잔여분 → B-11 로 분리 권고**: `mount | grep -E "$mountpoint.*(nfs|cifs|smb|sshfs)"` 가 마운트포인트를 이스케이프 없이 ERE 에 보간하여 경로의 `.` 이 임의 문자로 해석됩니다(이론상 오탐).
### **B-6: 스킬 트리에 임시 파일 복사 및 유출**
- `run_loop.sh::delegate_job_safe`가 래퍼 스크립트를 `.agents/skills/...` 트리 내부에 `.tmp`로 복사하여 버전 관리 트리를 오염시키고 rsync 배포 시 외부로 유출니다.
### **B-6: 스킬 트리에 임시 파일 복사 및 유출** — ✅ **완료 (Stage 1)**
- `run_loop.sh::delegate_job_safe`가 래퍼 스크립트를 `.agents/skills/...` 트리 내부에 `.tmp`로 복사하여 버전 관리 트리를 오염시키고 rsync 배포 시 외부로 유출되던 문제를, 임시 사본 생성 없이 원본 스크립트를 직접 인플레이스 실행(`bash "$orig_script" "$@"`)하도록 개선하여 해결했습니다.
- 기존에 이미 존재하던 완화 조치(`.gitignore:18``*.tmp`, `deploy/install_mam.sh:126``--exclude='*.tmp'`, `deploy/install.sh:190``*.tmp` skip)에 더해, 소스 트리 내 누출 경로 자체를 소멸시켰습니다.
- `run_loop.sh` 기동 시 잔여 `.tmp` 스윕 구문(`rm -f .../multi-agent-mux-delegate-job.*.tmp`) 및 실패 시 진단 로깅을 추가했습니다.
### **B-12: 명령 치환 서브셸의 EXIT 트랩으로 인한 루프 락 조기 해제 (D1)** — ✅ **완료 (P0)**
- `run_loop.sh``PLAN_JOB_OUTPUT=$(delegate_job_safe submit ...)` 등 11개 호출부가 명령 치환(`$(...)`) 서브셸로 실행될 때, `delegate_job_safe` 내부의 `trap _mam_release_guard EXIT INT TERM HUP` 이 서브셸 종료 시 발화하는 결함입니다.
- bash 서브셸의 `$$` 가 부모 PID를 유지하므로 `mam_release_loop_lock` 의 소유권 검사를 통과하여 첫 번째 위임 잡 종료 시점에 `.mam/loop-guard-active` 마커가 삭제되었습니다.
- 이로 인해 O-3 Invocation-Aware Scoped Guard의 파일 수정 차단 및 O-2 단일 루프 락이 첫 번째 잡 이후 무력화되는 P0 결함을 `delegate_job_safe` 내 로컬 트랩을 전면 제거함으로써 근본 해결했습니다.
- `tests/test_o3_scoped_guard.py` 내 Z-9 테스트를 3종 행위 기반 테스트(`test_z9_loop_lock_survives_delegation`, `test_z9_probe_detects_the_defect`, `test_z9_no_tmp_copy_left_in_skill_tree`, `test_z9_exit_code_and_diagnostics_propagation`)로 교체하여 재발을 방지했습니다.
### **B-13: 셀프 호스팅 멀티에이전트 루프 중 턴 간 스킬 오염 (In-Flight Tooling Mutation)** — 🟡 **Stage 2 분리 과제**
- `/multi-agent-mux-loop``multi-agent-mux` 자체를 수정/리팩터링할 때, Turn 1에서 Worker가 `.agents/skills/...` 를 수정(문법 오류나 미완성 코드 포함)하면 후속 Turn 2의 Reviewer 잡 제출 시 래퍼가 비정상 종료되어 자가 치유(Corrective/Rebuttal) 단계로 진입하지 못하고 루프가 중단될 수 있는 위험입니다.
- Stage 1에서는 실패 시 명확한 구문 진단 로그(`log_error "delegate_job_safe failed (exit $rc)..."`)를 제공하도록 보강하였으며, 근본 해결을 위해 루프 기동 시 1회 임시 디렉터리에 런타임 스냅샷을 동결하는 Stage 2 구현(래퍼 지시문 경로 오버라이드 포함)이 제안되었습니다.
### **B-8: `send_keys_safe` agy 경로 검증 이탈**
- agy 세션 주입 시 주입 실패 여부를 검증하지 않고 무조건 `return 0`을 남겨 실패 시에도 성공으로 보고됩니다.
@@ -108,7 +120,13 @@
---
## 5. 🎉 완료된 과제 (Completed Tasks — 12건)
## 5. 🎉 완료된 과제 (Completed Tasks — 13건)
### **P2-1 (B-6 / B-12): `delegate_job_safe` 임시 사본 제거 및 서브셸 루프 락 조기 해제 차단 조치** — ✅ 완료
- `.agents/skills/multi-agent-mux-loop/scripts/run_loop.sh``delegate_job_safe``.agents/skills/...` 경로 내에 `.tmp` 사본을 생성하던 방식을 제거하고 원본 래퍼 스크립트를 인플레이스로 직접 실행(`bash "$orig_script" "$@"`)하도록 개선하여 버전 관리 트리 오염 및 rsync 배포 유출(B-6)을 완전히 해소했습니다.
- 명령 치환(`$(delegate_job_safe ...)`) 서브셸 내에 설치되던 `trap _mam_release_guard EXIT` 로 인해 첫 번째 위임 잡 종료 시점에 `.mam/loop-guard-active` 마커가 삭제되어 O-3 Scoped Guard 및 O-2 락이 무력화되던 인접 P0 결함(**B-12 / D1**)을 로컬 트랩 제거를 통해 근본 해결했습니다.
- 래퍼 스크립트 실행 실패 시 도달 불가능하던 에러 진단을 `delegate_job_safe` 내부에서 직접 표준오류로 출력(`log_error "delegate_job_safe failed (exit $rc)..."`)하도록 진단 로깅을 보강했습니다.
- `tests/test_o3_scoped_guard.py` 내 Z-9 테스트를 4개 세부 행위 기반 테스트(`test_z9_loop_lock_survives_delegation`, `test_z9_probe_detects_the_defect`, `test_z9_no_tmp_copy_left_in_skill_tree`, `test_z9_exit_code_and_diagnostics_propagation`)로 교체 검증했습니다.
### **multi-agent-mux-orc-onboard: 오케스트레이터 온보딩 스킬 및 `orchestrator_uuids` 배제 게이트 조치** — ✅ 완료
- `.agents/skills/multi-agent-mux-orc-onboard/` 스킬 및 `.agents/skills/multi-agent-mux-orc-onboard/scripts/orc_onboard.sh` 헬퍼 모듈을 생성하여, 메인 오케스트레이터가 프로젝트 맥락 파악 시 자신의 대화 UUID를 포착해 `.mam/agent-sessions.yaml` 및 SQLite DB 내 `orchestrator_uuids` 리스트로 원자적 기록하는 구조를 구축했습니다.
+10 -2
View File
@@ -1,6 +1,6 @@
# 📝 Multi-Agent Mux 작업 세션 기록 (`LOG.md`)
- **최종 기록일시**: 2026-08-08 00:23 (KST)
- **최종 기록일시**: 2026-08-15 10:45 (KST)
- **작업 저장소**: `tmpl/multi-agent-mux` (Branch: `main`)
- **작업 상태**: 모든 작업 완료, 세션 안전 종료(stopped), 저장소 상태 Clean!
@@ -8,7 +8,15 @@
## 📌 1. 금일 작업 내용 요약
### 1) **multi-agent-mux-orc-onboard: 오케스트레이터 온보딩 스킬 및 `orchestrator_uuids` 배제 게이트 구축** — **완료**
### 1) **P2-1 (B-6 / B-12): `delegate_job_safe` 임시 사본 제거 및 서브셸 루프 락 조기 해제 차단 조치** — **완료**
- **배경**: `run_loop.sh::delegate_job_safe``.agents/skills/...` 내부에 `.tmp` 사본을 생성하여 트리 오염 및 배포 시 유출(B-6)되던 문제와, 명령 치환 서브셸 내의 `trap _mam_release_guard EXIT` 로 인해 첫 번째 잡 위임 시 루프 락 마커(`.mam/loop-guard-active`)가 조기 삭제되어 O-3 가드레일이 무력화되던 결함(**B-12 / D1**) 조치.
- **주요 구현**:
- [`.agents/skills/multi-agent-mux-loop/scripts/run_loop.sh`](file:///Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux/.agents/skills/multi-agent-mux-loop/scripts/run_loop.sh): `delegate_job_safe` 를 임시 사본 및 서브셸 트랩 없이 인플레이스로 직접 실행(`bash "$orig_script" "$@"`)하도록 개선하고 실패 시 진단 로깅 추가. 루프 기동 시 기존 잔여 `.tmp` 스윕 구문 추가.
- [`IMPROVEMENTS.md`](file:///Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux/IMPROVEMENTS.md): B-6 완료 상태 갱신, B-12 (D1) 결함 명세 및 B-13 (턴 간 스킬 오염) Stage 2 과제 등록.
- [`tests/test_o3_scoped_guard.py`](file:///Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux/tests/test_o3_scoped_guard.py): 취약한 문자열 검사 Z-9를 4개 행위 기반 테스트(`test_z9_loop_lock_survives_delegation`, `test_z9_probe_detects_the_defect`, `test_z9_no_tmp_copy_left_in_skill_tree`, `test_z9_exit_code_and_diagnostics_propagation`)로 교체.
- **검증**: `pytest tests/ -q` 실행 결과 **259 passed (100%)** 달성.
### 2) **multi-agent-mux-orc-onboard: 오케스트레이터 온보딩 스킬 및 `orchestrator_uuids` 배제 게이트 구축** — **완료**
- **배경**: 오케스트레이터(`agy`)가 서브 에이전트 생성/정지/복원 시 자기 대화 UUID가 `agent-sessions.yaml` 서브 세션으로 오염 캡처되어 SQLite DB 락(`database is locked`) 및 대화 충돌이 발생하던 결함 조치.
- **주요 구현**:
- [`.agents/skills/multi-agent-mux-orc-onboard/SKILL.md`](file:///Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux/.agents/skills/multi-agent-mux-orc-onboard/SKILL.md) 및 [`.agents/skills/multi-agent-mux-orc-onboard/scripts/orc_onboard.sh`](file:///Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux/.agents/skills/multi-agent-mux-orc-onboard/scripts/orc_onboard.sh): 오케스트레이터의 신원 UUID를 포착하여 `.mam/agent-sessions.yaml` 및 SQLite DB 내 `orchestrator_uuids` 리스트로 원자적 등록하는 스킬 구축.
+161 -6
View File
@@ -147,16 +147,171 @@ def test_z8_dead_pid_marker(mam_sandbox):
assert res["decision"] == "allow"
# Z-9: delegate_job_safe restores _mam_release_guard trap
def test_z9_delegate_job_safe_restores_trap():
def _extract_delegate_job_safe():
run_loop = REPO_ROOT / ".agents" / "skills" / "multi-agent-mux-loop" / "scripts" / "run_loop.sh"
content = run_loop.read_text()
# Check inside delegate_job_safe definition that trap - is replaced with trap _mam_release_guard
func_start = content.find("delegate_job_safe() {")
assert func_start != -1
func_body = content[func_start:func_start+400]
assert "trap _mam_release_guard EXIT INT TERM HUP" in func_body
assert "trap - EXIT INT TERM HUP" not in func_body
func_end = content.find("\n}\n", func_start)
assert func_end != -1
return content[func_start:func_end + 3]
# Z-9: delegate_job_safe preserves loop lock across delegations without tmp copies/traps (B-6, D1)
def test_z9_loop_lock_survives_delegation(tmp_path):
loop_lock_sh = REPO_ROOT / ".agents" / "skills" / "multi-agent-mux-loop" / "scripts" / "loop_lock.sh"
skill_dir = tmp_path / ".agents" / "skills" / "multi-agent-mux-delegate-job"
skill_dir.mkdir(parents=True)
stub_wrapper = skill_dir / "multi-agent-mux-delegate-job"
stub_wrapper.write_text("#!/usr/bin/env bash\necho 'JOB_ID: 12345678'\n")
stub_wrapper.chmod(0o755)
extracted_func = _extract_delegate_job_safe()
script = f"""#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="{tmp_path}"
MAM_LOOP_MARKER="$REPO_ROOT/.mam/loop-guard-active"
source "{loop_lock_sh}"
log_error() {{ echo "ERROR: $@" >&2; }}
log_info() {{ echo "INFO: $@" >&2; }}
_mam_release_guard() {{ mam_release_loop_lock "$MAM_LOOP_MARKER" || true; }}
mam_acquire_loop_lock "$MAM_LOOP_MARKER"
trap _mam_release_guard EXIT INT TERM HUP
{extracted_func}
PLAN_JOB_OUTPUT=$(delegate_job_safe submit --task test)
if [ -f "$MAM_LOOP_MARKER" ]; then
echo "MARKER: HELD"
else
echo "MARKER: RELEASED"
fi
"""
res = subprocess.run(["bash", "-c", script], capture_output=True, text=True)
assert res.returncode == 0
assert "MARKER: HELD" in res.stdout
def test_z9_probe_detects_the_defect(tmp_path):
loop_lock_sh = REPO_ROOT / ".agents" / "skills" / "multi-agent-mux-loop" / "scripts" / "loop_lock.sh"
skill_dir = tmp_path / ".agents" / "skills" / "multi-agent-mux-delegate-job"
skill_dir.mkdir(parents=True)
stub_wrapper = skill_dir / "multi-agent-mux-delegate-job"
stub_wrapper.write_text("#!/usr/bin/env bash\necho 'JOB_ID: 12345678'\n")
stub_wrapper.chmod(0o755)
defective_func = """
delegate_job_safe() {
local orig_script="$REPO_ROOT/.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job"
local tmp_script
tmp_script="${orig_script}.${RANDOM}_$$.tmp"
cp "$orig_script" "$tmp_script"
trap 'rm -f "$tmp_script"' EXIT INT TERM HUP
local rc=0
bash "$tmp_script" "$@" || rc=$?
rm -f "$tmp_script"
trap _mam_release_guard EXIT INT TERM HUP
return $rc
}
"""
script = f"""#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="{tmp_path}"
MAM_LOOP_MARKER="$REPO_ROOT/.mam/loop-guard-active"
source "{loop_lock_sh}"
log_error() {{ echo "ERROR: $@" >&2; }}
log_info() {{ echo "INFO: $@" >&2; }}
_mam_release_guard() {{ mam_release_loop_lock "$MAM_LOOP_MARKER" || true; }}
mam_acquire_loop_lock "$MAM_LOOP_MARKER"
trap _mam_release_guard EXIT INT TERM HUP
{defective_func}
PLAN_JOB_OUTPUT=$(delegate_job_safe submit --task test)
if [ -f "$MAM_LOOP_MARKER" ]; then
echo "MARKER: HELD"
else
echo "MARKER: RELEASED"
fi
"""
res = subprocess.run(["bash", "-c", script], capture_output=True, text=True)
assert res.returncode == 0
assert "MARKER: RELEASED" in res.stdout
def test_z9_no_tmp_copy_left_in_skill_tree(tmp_path):
loop_lock_sh = REPO_ROOT / ".agents" / "skills" / "multi-agent-mux-loop" / "scripts" / "loop_lock.sh"
skill_dir = tmp_path / ".agents" / "skills" / "multi-agent-mux-delegate-job"
skill_dir.mkdir(parents=True)
stub_wrapper = skill_dir / "multi-agent-mux-delegate-job"
stub_wrapper.write_text("#!/usr/bin/env bash\necho 'JOB_ID: 12345678'\n")
stub_wrapper.chmod(0o755)
extracted_func = _extract_delegate_job_safe()
script = f"""#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="{tmp_path}"
MAM_LOOP_MARKER="$REPO_ROOT/.mam/loop-guard-active"
source "{loop_lock_sh}"
log_error() {{ echo "ERROR: $@" >&2; }}
log_info() {{ echo "INFO: $@" >&2; }}
_mam_release_guard() {{ mam_release_loop_lock "$MAM_LOOP_MARKER" || true; }}
mam_acquire_loop_lock "$MAM_LOOP_MARKER"
trap _mam_release_guard EXIT INT TERM HUP
{extracted_func}
PLAN_JOB_OUTPUT=$(delegate_job_safe submit --task test)
"""
res = subprocess.run(["bash", "-c", script], capture_output=True, text=True)
assert res.returncode == 0
tmp_files = list(skill_dir.glob("*.tmp"))
assert tmp_files == []
def test_z9_exit_code_and_diagnostics_propagation(tmp_path):
loop_lock_sh = REPO_ROOT / ".agents" / "skills" / "multi-agent-mux-loop" / "scripts" / "loop_lock.sh"
skill_dir = tmp_path / ".agents" / "skills" / "multi-agent-mux-delegate-job"
skill_dir.mkdir(parents=True)
stub_wrapper = skill_dir / "multi-agent-mux-delegate-job"
stub_wrapper.write_text("#!/usr/bin/env bash\necho 'syntax error' >&2\nexit 7\n")
stub_wrapper.chmod(0o755)
extracted_func = _extract_delegate_job_safe()
script = f"""#!/usr/bin/env bash
REPO_ROOT="{tmp_path}"
MAM_LOOP_MARKER="$REPO_ROOT/.mam/loop-guard-active"
source "{loop_lock_sh}"
log_error() {{ echo "ERROR: $@" >&2; }}
log_info() {{ echo "INFO: $@" >&2; }}
_mam_release_guard() {{ mam_release_loop_lock "$MAM_LOOP_MARKER" || true; }}
mam_acquire_loop_lock "$MAM_LOOP_MARKER"
trap _mam_release_guard EXIT INT TERM HUP
{extracted_func}
rc=0
delegate_job_safe submit --task test || rc=$?
echo "DELEGATE_RC: $rc"
"""
res = subprocess.run(["bash", "-c", script], capture_output=True, text=True)
assert "DELEGATE_RC: 7" in res.stdout
assert "delegate_job_safe failed (exit 7):" in res.stderr
assert "bash -n" in res.stderr
# Z-10: Reused PID with stale lstart is NOT blocked