merge: fix/test-herdr-leak (tests no longer reach a real herdr server: a *-shim ancestor hid the mock, the sandbox kept the pane's socket; v4.9.1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
This commit is contained in:
GodopuandClaude Opus 5.5 committed 2026-10-13 10:33:04 +09:00
commit 2ccf43911c
35 files changed
+717 -31

No files matched your search

@@ -0,0 +1,14 @@
{
"loop_id": "loop-20261013T001819Z-131fa6",
"base_sha": "8b0b4a07dc76a4915895bdde6170dc8eed9b5f74",
"head_sha": "325b056057fcdd717d2c81017fc5ef57183cb4d0",
"tree_sha": "7905896f005159d596e3872d0ee2c3c12061f09a",
"review_commit": "325b056057fcdd717d2c81017fc5ef57183cb4d0",
"reviewer": "planner-reviewer-claude-01",
"job_id": "63919c5d",
"verdict": "PASS",
"transport": "herdr",
"evidence": "review-tree",
"isolation": "worktree",
"verified_at": "2026-10-13T00:57:46Z"
}
@@ -0,0 +1,59 @@
# 테스트의 실제 herdr 서버 누출 수정 리뷰 (job 63919c5d)
- 리뷰어: planner-reviewer-claude-01 (Reviewer)
- 리뷰 identity: loop-20261013T001819Z-131fa6 / base 8b0b4a07 .. head 325b0560 (커밋 2개: 525df2b 수정, 325b056 s11 기준 이동 / 4 파일) / tree 7905896f
- 검증 경로: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261013T001819Z-131fa6/1` — HEAD·tree 가 identity 와 일치, `git status --porcelain` 0 줄(시작·종료 시), `.mam` 디렉터리도 생기지 않음. 본 체크아웃 HEAD 8b0b4a0(main) 변동 없음.
- 실행 안전: 내 pane 은 `HERDR_SOCKET_PATH`·`HERDR_PANE_ID`·`HERDR_TAB_ID`·`HERDR_WORKSPACE_ID`·`HERDR_SESSION_NAME`·`HERDR_SESSION`·`HERDR_CONFIG_PATH`·`HERDR_BIN_PATH`·`HERDR_ENV` 를 물려받고 있어, 모든 테스트·탐침은 `env -i`(PATH/HOME/LANG/TMPDIR 만)로 돌려 브리프의 `-u` 목록을 포함한 전부를 비웠다. basetemp 는 세션 scratchpad 아래(경로에 "shim" 없음). 실제 바이너리를 부른 것은 일회용 HOME 에서의 `status server`(읽기 전용, 서버 미기동)뿐이다. Python 은 본 체크아웃 `.venv`(3.9.6, paho 2.1.0). `.mam.env` 는 읽지 않았다.
## 1. 원인 판정이 맞는가
**원인 1 (조상 `*-shim` 이 mock 을 가림).** base 의 `_is_shim_path` 는 `*-shim/*` 를 `"/$1/"` 전체에 대해 매칭해 조상 디렉터리까지 걸렀고(`lib.sh` base), 생성 shim 의 `_resolve_real_herdr` 도 `[[ "$dir" != *"-shim"* ]]` 로 PATH 항목 문자열 어디든 걸렀다. basetemp `pytest-pr56-shim` 아래 mock `bin/` 은 둘 다에서 건너뛰어졌고 다음 후보인 `/opt/homebrew/bin/herdr` 가 "real" 이 된다. 수정 전 lib.sh 로 되돌린 scratch 복사본에서 L1 이 정확히 그 상태로 실패했다: `_resolve_real_herdr_path` 가 `…/pytest-x-shim/case0/bin/herdr` 대신 다음 PATH 항목의 디코이 `…/decoy/bin/herdr` 를 골랐다(`assert '/…/decoy/bin/herdr' == '/…/case0/bin/herdr'`). 판정 맞음.
**원인 2 (pane 의 `HERDR_SOCKET_PATH`).** 실제 herdr 0.9.3 바이너리를 일회용 HOME 에서 `status server` 로 읽기만 해 확인했다:
| 환경 | 바이너리가 가리킨 소켓 |
|---|---|
| `HERDR_SOCKET_PATH=/nonexistent/s.sock` + `--session t-zz` | `/nonexistent/s.sock` |
| `HERDR_SOCKET_PATH=/nonexistent/s.sock` + `HERDR_SESSION=t-zz` | `/nonexistent/s.sock` |
| `HERDR_SESSION=t-zz` 만 | `$HOME/.config/herdr/sessions/t-zz/herdr.sock` (가짜 HOME 아래) |
| `HERDR_SOCKET_PATH` 만 | `/nonexistent/s.sock` |
즉 `HERDR_SOCKET_PATH` 는 **`--session` 보다도** 우선한다. shim 이 항상 `--session <샌드박스 서버>` 를 붙여도 이 변수가 남아 있으면 실제 서버로 간다 — 원인 1 로 실제 바이너리가 선택되는 순간 샌드박스 HOME 은 아무 보호가 되지 못했다는 오케스트레이터의 실측과 일치한다. 두 원인은 독립적이고 둘 다 막아야 한다(1 만 고치면 pane 변수 누출 시 다른 경로의 실제 호출이 여전히 실제 서버로, 2 만 고치면 숨은 mock 대신 실제 바이너리가 가짜 HOME 에 서버를 띄움). 판정 맞음.
## 2. 수정 내용 검토
- `lib.sh:113-124 _is_shim_path`: 첫 `case` 에서 `*-shim/*` 만 빠지고 `.mam/shim`·`$_HERDR_SHIM_DIR_PATTERN`(`/multi-agent-herdr-shim/`)·`$_HERDR_SKILLS_BIN_PATTERN`(`/.agents/skills/.bin`) 은 그대로. `*-shim` 은 `"${1%/}"` 즉 인자 자신의 끝에만 적용. `lib.sh:130-136 _resolve_real_herdr_path` 는 PATH 항목(`_is_shim_path "$dir"`)과 후보 파일의 **canonical dirname** 둘 다 검사한다 — L1 의 mock 은 `tmp/bin/herdr` 로의 symlink 라 canonical dirname 은 `tmp/bin` 이고, 선택 결과는 PATH 항목 쪽 경로(`$dir/herdr`)다. 기존 래퍼 디렉터리 제외 여부: `.mam/shim` → 첫 case(부분 문자열, 조상 포함) 유지; `multi-agent-herdr-shim` → 패턴 상수 유지(`"/…/multi-agent-herdr-shim/…/"`); `.agents/skills/.bin` → 패턴 유지(이 저장소에는 그 디렉터리가 없다); 테스트의 `herdr-shim`(L1b)·`py-shim`·`ps-shim`(`test_issue17_item4_decisions.py:208`, `test_issue21_acceptance.py:283`, `test_issue30_stall_watch_stop.py:95`) 은 디렉터리 자신의 이름이 `*-shim` 이라 계속 제외.
- 생성 shim(`lib.sh:173-178`): `[[ "${dir%/}" != *-shim ]]`. `.mam/shim` 부분 문자열 검사는 그대로.
- `tests/conftest.py:293-298 mam_sandbox`: `HERDR_SOCKET_PATH`·`HERDR_PANE_ID`·`HERDR_TAB_ID` 제거. 기존의 `HERDR_SESSION_NAME`·`HERDR_SERVER_NAME`·`HERDR_WORKSPACE_ID`·`HERDR_CONFIG_PATH`·`XDG_CONFIG_HOME` 제거와 합쳐 pane 이 내보내는 라우팅 변수 중 실제 서버에 닿을 수 있는 것은 모두 빠진다(아래 비차단 메모의 `HERDR_SESSION` 참조).
- bash 3.2: `${1%/}`·`${dir%/}`·`case` 뿐, `bash -n` 통과. Python 3.9 컴파일 통과.
- s11 기준 커밋 525df2b: 생성 shim heredoc 이 바뀌었으므로 기준을 그 커밋으로 옮긴 것은 #45 때와 같은 방식이고, 검증 경로에서 s11 PASSED. 525df2b 는 이 브랜치의 첫 커밋으로 검증 경로에 존재한다.
## 3. 충분한가 — 다른 경로로 실제 서버에 닿는 테스트
- `mock_herdr`(`conftest.py:310`)·`mock_agents`(`:1412`) 는 `mam_sandbox` 에 의존하므로 mock 을 PATH 에 두는 테스트는 모두 샌드박스 환경(라우팅 변수 제거, HOME 교체)이다.
- `mam_sandbox` 없이 herdr 를 흉내 내는 파일은 `test_issue14_delivery_ack.py`·`test_issue18_liveness_contract.py` 둘(bash 함수 `_sks_herdr` 대체, HOME=tmp). 이 둘이 herdr **바이너리**를 한 번이라도 부르는지 보려고 호출을 기록하는 디코이 `herdr` 를 PATH 맨 앞에 두고 두 파일을 돌렸다: **43 passed, 디코이 호출 0 건**(로그 파일이 생기지 않음). 즉 이 둘은 라우팅 변수가 남아도 바이너리를 실행하지 않는다. (이 실행의 `1 error` 는 세션 teardown 의 #55 agent-home 가드가 `~/.gemini/antigravity-cli/conversations/f9bf3d55-….db-shm/.db-wal` 을 잡은 것으로, 브리프가 예고한 다른 워크스페이스의 실행 중 agy 대화 WAL/SHM 이다. 테스트 산출물이 아니므로 판정에 쓰지 않고 보고만 한다.)
- `test_issue6_fail_closed.py`·`test_issue6_turn_handshake.py` 는 `mock_herdr` 를 쓰므로 샌드박스다.
- 실제 바이너리가 아는 라우팅 변수는 `HERDR_SOCKET_PATH`(최우선)와 `HERDR_SESSION`(HOME 상대 경로)뿐임을 위 표로 확인했다. `HERDR_SESSION_NAME` 은 바이너리가 모르고 shim 만 `--session` 으로 바꾼다(어제 리뷰에서 확인). 따라서 `HERDR_SOCKET_PATH` 제거 + 가짜 HOME 이면 shim 경로든 직접 호출이든 실제 서버에는 닿지 않는다.
## 4. 테스트 결과
| 항목 | 결과 |
|---|---|
| head: `test_herdr_real_server_leak.py` + `test_issue23_24_startup_path.py` + `test_herdr_shim_contract.py` + `test_issue17_workspace_root_isolation.py` + `test_issue9_server_env.py` + `test_issue45_herdr_config.py` | **130 passed, 34 skipped**(skip 은 모두 `needs tomllib (Python 3.11+)` 류의 알려진 3.9 skip). L1·L1b·L2·s11 은 모두 PASSED 로 실행됨 |
| 수정 전 lib.sh(8b0b4a0) + 새 테스트 | **L1 실패**(디코이 선택), **L1b 통과**(자기 이름 `*-shim` 은 전후 모두 제외) |
| 수정 전 conftest(8b0b4a0) + 새 테스트 | **L2 실패**: `['HERDR_PANE_ID', 'HERDR_SOCKET_PATH', 'HERDR_TAB_ID']` 가 남음 |
| 디코이 sweep(`test_issue14`, `test_issue18`) | 43 passed, 디코이 호출 0 |
수정 전 재현에는 실제 herdr 가 실행될 수 있는 구성을 쓰지 않았다(테스트 설계대로 디코이가 PATH 에서 실제 바이너리보다 앞서고, `env -i` 로 소켓 변수도 없음).
## 5. 공유 제어 평면에 주는 영향
- 생성 shim 은 lib.sh 를 source 하는 모든 실행에서 `<워크스페이스>/.mam/shim/herdr` 로 다시 쓰이므로, 머지 뒤 실제 제어 평면의 shim 도 두 줄이 바뀐다. 바뀌는 것은 "조상 이름에 `-shim` 이 든 PATH 항목을 더 이상 건너뛰지 않음" 뿐이고, `.mam/shim` 과 자기 이름이 `*-shim` 인 디렉터리는 계속 건너뛴다. 내 pane 의 PATH 에서 `-shim` 을 포함하는 항목은 `~/.cmuxterm/cmux-cli-shims/<uuid>` 하나인데(`herdr` 없음, `amp claude codex hermes pi` 만) 전후 어느 규칙에도 해당하지 않아 영향 없다. 본 체크아웃의 `.mam/shim/herdr` 는 이 리뷰 중 건드리지 않았다(아직 base 내용).
- 비차단 메모 1: README(`README.md:102`, `README.ko.md:84`)가 설명하는 레거시 레이아웃 `/tmp/multi-agent-herdr-shim/<server>/herdr` 는 디렉터리 자신의 이름이 서버명이라, 생성 shim 의 새 규칙으로는 제외되지 않는다(lib.sh 의 `_is_shim_path` 는 패턴 상수로 계속 제외). 현재 코드는 그 레이아웃을 만들지 않고 이 호스트에도 없으므로 동작 영향은 없지만, 생성 shim 에도 `*"/multi-agent-herdr-shim/"*` 를 두거나 README 를 현재 레이아웃(`.mam/shim`)으로 고치면 깔끔하다.
- 비차단 메모 2: `mam_sandbox` 는 `HERDR_SESSION`(바이너리가 아는 변수, 내 pane 도 내보냄)을 지우지 않는다. 가짜 HOME 아래 경로로 풀리므로 실제 서버 누출은 아니지만, `lib.sh:1425 _herdr_caller_server` 와 `pane_lock.caller_server` 가 이 값을 "호출자 서버" 로 읽어 pane 안/밖에서 테스트 분기가 달라질 수 있다. 결정성을 위해 함께 지우는 것을 권한다(이 PR 의 범위 밖).
## 6. 잔류물
- scratch 복사본·basetemp·디코이·로그는 모두 세션 scratchpad 아래. 일회용 HOME `/tmp/j639-h` 제거(서버를 띄운 적 없음). 실제 `.mam`·`.mam.env`·모니터·실제 서버(multi-agent-mux, default)·`~/.config/herdr`·에이전트 홈은 건드리지 않았다.
## 결론
두 원인 판정은 코드와 실측(특히 `HERDR_SOCKET_PATH` 가 `--session` 보다 우선)으로 확인되고, 수정은 각각을 막으며 회귀 테스트는 수정 전 코드에서 정확히 그 이유로 실패한다. 기존 래퍼 디렉터리 제외는 유지되고, mock 없이 돌아가는 두 테스트 파일은 herdr 바이너리를 아예 부르지 않는다.
[VERDICT: PASS]
@@ -0,0 +1,14 @@
{
"loop_id": "loop-20261013T010208Z-f90dd4",
"base_sha": "325b056057fcdd717d2c81017fc5ef57183cb4d0",
"head_sha": "db7f4cab9c60ff5db009cf3fc625e9a86a465ef6",
"tree_sha": "f32cabd3a6c8d775c9223509967f1a810f0cf827",
"review_commit": "db7f4cab9c60ff5db009cf3fc625e9a86a465ef6",
"reviewer": "planner-reviewer-claude-01",
"job_id": "834fa032",
"verdict": "PASS",
"transport": "herdr",
"evidence": "review-tree",
"isolation": "worktree",
"verified_at": "2026-10-13T01:21:50Z"
}
@@ -0,0 +1,29 @@
# 테스트 실제 herdr 서버 누수 수정 2차 리뷰 — 커밋 db7f4ca (job 834fa032)
- 리뷰어: planner-reviewer-claude-01 (Reviewer)
- 리뷰 identity: loop-20261013T010208Z-f90dd4 / base 325b0560 .. head db7f4cab (커밋 1개, 2 파일) / tree f32cabd3
- 검증 경로: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261013T010208Z-f90dd4/1` — HEAD·tree 일치, `git status --porcelain` 0 줄(시작·종료), `.mam` 생성 없음. 본 체크아웃 HEAD 8b0b4a0(main) 변동 없음.
- 실행 안전: 1차와 같이 모든 테스트·탐침은 `env -i`(PATH/HOME/LANG/TMPDIR 만, 필요한 변수는 명시 추가)로 돌려 브리프의 `-u` 목록 전부가 비워진다. basetemp 는 scratchpad 아래(이름에 "shim" 없음). 실제 바이너리는 일회용 HOME 에서 `status server` 읽기만(서버 미기동). Python 은 본 체크아웃 `.venv`(3.9.6). 전체 스위트는 돌리지 않았다. 범위는 이 커밋만.
## 1. 변경
- `tests/conftest.py:299`: `mam_sandbox` 가 `HERDR_SESSION` 도 지운다(주석 `:296-297`). 현재 제거 목록: `HERDR_CONFIG_PATH`, `HERDR_PANE_ID`, `HERDR_SERVER_NAME`, `HERDR_SESSION_NAME`, `HERDR_SESSION`, `HERDR_SOCKET_PATH`, `HERDR_TAB_ID`, `HERDR_WORKSPACE_ID`, `XDG_CONFIG_HOME`.
- `tests/test_herdr_real_server_leak.py:76`: L2 의 `PANE_VARS` 에 `HERDR_SESSION` 추가. 수정 전 conftest(325b056)에 새 테스트를 얹은 scratch 복사본에서 L2 는 `AssertionError: ['HERDR_SESSION']` 로 실패 → 테스트가 이 변경을 정확히 겨눈다.
- Python 3.9 컴파일 통과. 제품 코드 변경 없음.
## 2. HERDR_SESSION 을 지워서 깨지는 테스트가 있는가 (검토 관점 1)
- 소비자: `lib.sh:1425 _herdr_caller_server`, `lib_py/pane_lock.py:49 caller_server`, `lib_py/loop/watcher.py:64` — 셋 다 "있으면 호출자 서버, 없으면 `HERDR_SOCKET_PATH` 모양" 이고 `HERDR_SOCKET_PATH` 는 이미 지워지므로 샌드박스에서는 일관되게 "pane 밖" 으로 판정된다.
- 테스트 쪽 bare `HERDR_SESSION` 참조는 전부 테스트가 **스스로** 넣는 값이다(픽스처 뒤라 영향 없음): `test_issue23_24_startup_path.py:264,369,395`(S1g·watcher 케이스는 `env={...}` 로 명시), `test_issue34_job_tree.py:580`, `test_issue30_stall_watch.py:486`, `test_issue45_restore.py:42`(`SCOPE_ENV` 로 이미 `HERDR_SESSION`·`HERDR_ENV` 까지 자체 제거). 반대로 "`HERDR_SESSION` 이 없어야" 하는 테스트(`test_s2b…:188`, S2 계열 "pane 밖 호출자") 는 이제 pane 안에서 돌려도 전제가 보장된다 — 이것이 이 변경의 목적이다.
- 실측: 같은 6 파일(`test_herdr_real_server_leak`, `test_issue50_pane_lock`, `test_issue23_24_startup_path`, `test_herdr_shim_contract`, `test_issue45_herdr_config`, `test_issue9_server_env`)을 (a) 깨끗한 환경, (b) 바깥 환경에 `HERDR_SESSION=pane-real-srv` 를 넣은 환경에서 각각 돌렸다: **둘 다 139 passed, 34 skipped** (skip 은 모두 Python 3.9 의 tomllib skip). pane 안/밖 결과가 같다.
## 3. 다른 라우팅 변수가 남았는가 (검토 관점 2)
- 내 pane 이 내보내는 `HERDR_*` 는 `HERDR_BIN_PATH`, `HERDR_CONFIG_PATH`, `HERDR_ENV`, `HERDR_PANE_ID`, `HERDR_SESSION`, `HERDR_SESSION_NAME`, `HERDR_SOCKET_PATH`, `HERDR_TAB_ID`, `HERDR_WORKSPACE_ID` 아홉 개. 샌드박스가 지우지 않는 것은 `HERDR_BIN_PATH`·`HERDR_ENV` 둘뿐이다.
- 실제 바이너리(0.9.3) 에 각각을 단독으로 주고 `status server` 를 읽었다: `HERDR_ENV`, `HERDR_BIN_PATH`, `HERDR_CONFIG_PATH`, `HERDR_WORKSPACE_ID`, `HERDR_PANE_ID` 모두 소켓은 `$HOME/.config/herdr/herdr.sock`(가짜 HOME) — 서버 선택에 영향 없음. 1차에서 확인한 대로 소켓을 바꾸는 것은 `HERDR_SOCKET_PATH`(최우선)와 `HERDR_SESSION`(HOME 상대) 둘뿐이며 이제 둘 다 제거된다.
- `.agents`·`tests` 에서 `HERDR_BIN_PATH`·`HERDR_ENV` 를 읽는 코드는 없다(`test_issue45_restore.py:42` 가 자체 정리 목록에 넣어 둔 것뿐). 남은 라우팅 변수 없음.
## 4. 잔류물
- scratch 복사본·basetemp·로그는 scratchpad 아래. 일회용 HOME `/tmp/j834-h` 제거(서버 미기동). 실제 `.mam`·`.mam.env`·모니터·실제 서버·`~/.config/herdr`·에이전트 홈은 건드리지 않았다.
## 결론
`HERDR_SESSION` 제거는 호출자 서버 판정을 pane 안/밖에서 동일하게 만들고, 그 값을 쓰는 테스트는 모두 자체 설정이라 깨지지 않는다(두 환경에서 동일하게 139 passed). 실제 바이너리의 서버 선택에 관여하는 변수는 이제 모두 샌드박스에서 제거된다.
[VERDICT: PASS]
@@ -0,0 +1,14 @@
{
"loop_id": "loop-20261013T010208Z-f90dd4",
"base_sha": "325b056057fcdd717d2c81017fc5ef57183cb4d0",
"head_sha": "db7f4cab9c60ff5db009cf3fc625e9a86a465ef6",
"tree_sha": "f32cabd3a6c8d775c9223509967f1a810f0cf827",
"review_commit": "db7f4cab9c60ff5db009cf3fc625e9a86a465ef6",
"reviewer": "planner-reviewer-codex-01",
"job_id": "b1696427",
"verdict": "PASS",
"transport": "herdr",
"evidence": "review-tree",
"isolation": "worktree",
"verified_at": "2026-10-13T01:21:50Z"
}
@@ -0,0 +1,55 @@
# Job b1696427 — HERDR_SESSION isolation review
판정: PASS. 지정된 두 파일 변경에서 차단할 결함을 발견하지 않았다. 관련 테스트 **84건 모두 통과**했고, 별도 base/head 탐침에서 상속 세션 제거와 fixture 이후의 명시적 설정 보존을 확인했다.
## 검증 identity
- loop: `loop-20261013T010208Z-f90dd4`
- base: `325b056057fcdd717d2c81017fc5ef57183cb4d0`
- head: `db7f4cab9c60ff5db009cf3fc625e9a86a465ef6`
- tree: `f32cabd3a6c8d775c9223509967f1a810f0cf827`
- 검증 cwd: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261013T010208Z-f90dd4/1`
- Python: `/Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux/.venv/bin/python`
HEAD/tree가 지정 identity와 일치하고 `git status --short`는 비어 있었다. `git diff --check 325b056`도 통과했다. 이 커밋만 검토했으며 운영 코드·생성 shim 변경은 없다. 소스 수정, creator 작업 트리 실행, 커밋·push는 하지 않았다.
## 코드 검토
1. `tests/conftest.py:298–301`에서 기존 socket/pane/tab 제거 사이에 `monkeypatch.delenv("HERDR_SESSION", raising=False)`를 추가한 것은 적절하다. 값이 없는 실행도 허용하고, pytest monkeypatch의 기존 복원 수명 주기를 따른다. `.agents/skills/lib.sh:1425–1427`, `lib_py/pane_lock.py:49–50`, `lib_py/loop/watcher.py:64–66`은 이 값을 우선해서 caller server로 읽으므로 sandbox 시작 전에 상속값을 제거해야 pane 안/밖의 기본 분기가 같아진다.
2. `tests/test_herdr_real_server_leak.py:73–88`은 새 변수를 가짜 pane 환경에 넣은 뒤 실제 `mam_sandbox` 이후 남지 않는지 확인한다. 외부 실행 환경을 미리 scrub했더라도 시험이 스스로 값을 주입하므로 추가된 제거 동작을 검증한다.
3. 테스트가 의도적으로 지정하는 값은 보존된다. `test_issue23_24_startup_path.py:31–42`의 helper는 fixture 이후 subprocess 환경을 구성하고 명시적 `env`를 마지막에 적용한다. `:261–266`의 HERDR_SESSION 우선순위 시험을 포함한 모듈이 통과했다. `test_issue50_pane_lock.py:63–80`도 명시적인 환경 mapping으로 caller/lock 식별을 검사한다. 추가 탐침에서도 fixture 이후 설정한 값이 두 helper에 그대로 전달됐다.
4. 확인한 라우팅·범위 변수 `HERDR_SESSION`, `HERDR_SESSION_NAME`, `HERDR_SERVER_NAME`, `HERDR_WORKSPACE_ID`, `HERDR_SOCKET_PATH`, `HERDR_PANE_ID`, `HERDR_TAB_ID`와 설정 경로 `HERDR_CONFIG_PATH`, `XDG_CONFIG_HOME`은 모두 `conftest.py:290–306`에서 제거된다. 실제 fixture setup에 이 9개를 주입한 독립 탐침에서도 head에는 하나도 남지 않았다. 검사한 repository의 caller/scope/config 경로에서 추가 누락 라우팅 변수는 찾지 못했다. 이는 알려진 경로의 검증이며 임의의 미래 herdr 환경변수까지 일괄 제거한다는 의미는 아니다.
`mam_sandbox`를 사용하지 않는 테스트의 환경 계약은 이 두 파일 변경으로 바뀌지 않는다. 예를 들어 실제 서버용 `test_issue50_dispatch_herdr.py`는 모든 HERDR_*를 별도로 제거하고, `test_issue45_restore.py`의 Rig도 HERDR_SESSION을 포함한 scope 변수를 자체 제거한다. 이 실제 서버 모듈은 이번 리뷰에서 실행하지 않았다.
## 실행 검증
안전 launcher `/tmp/b1696427-review/run.py`를 사용해 아래 관련 모듈만 실행했다.
```text
env -u HERDR_SOCKET_PATH -u HERDR_PANE_ID -u HERDR_TAB_ID -u HERDR_WORKSPACE_ID -u HERDR_SESSION_NAME -u HERDR_SESSION -u WORKSPACE_ROOT -u MAM_ENV_FILE <Python> /tmp/b1696427-review/run.py tests/test_herdr_real_server_leak.py tests/test_issue50_pane_lock.py tests/test_issue23_24_startup_path.py
```
- **84 passed in 195.48s**, exit 0. skip/failure 없음.
- 로그: `/tmp/b1696427-review/tests.log`
- basetemp: `/tmp/b1696427-mxmhd4h3/bt` — 검증 트리 밖이며 이름에 shim 없음.
- `PYTHONDONTWRITEBYTECODE=1`, `-p no:cacheprovider`, 일회용 HOME, 지정 AGENT_PYTHON_BIN 사용.
- launcher는 상속 HERDR_*/MAM_*/MQTT_*/SKS_* 및 제어·에이전트 경로 환경을 추가로 제거했다. 실제 설치 herdr에 도달하기 전에 기록 후 exit 97로 중단하는 fallback 디코이를 PATH에 두었다. **fallback 호출 기록 없음.** 테스트별 mock과 suite의 로컬 broker를 사용했다.
- 전체 스위트, 실제 herdr, 운영 모니터·서버는 실행하지 않았다. `.mam.env` export/출력이나 실제 사용자 에이전트 홈 변경도 하지 않았다.
## 독립 base/head 탐침
산출물: `/tmp/b1696427-review/probe.py`, `/tmp/b1696427-review/probe.log`.
재현은 위와 같은 `env -u ...` 및 `PYTHONDONTWRITEBYTECODE=1`로 지정 Python에서 probe.py를 실행한다. 검증 트리의 git base와 head에서 `mam_sandbox` 함수 AST를 추출해 pytest decorator만 제거하고, 실제 함수 본문을 별도 `/tmp` 디렉터리에 실행했다. `pytest.MonkeyPatch`를 사용하고, 이 setup에서 시작하는 데몬이 없으므로 등록되는 reaper finalizer는 실행하지 않았다. 서버를 실행하는 코드는 호출하지 않았다.
절차와 결과:
1. fixture 전에 위 9개 변수에 안전한 sentinel 값을 주입한다.
2. base fixture 실행 후 남은 변수는 `HERDR_SESSION` 하나이고 값은 `caller-sentinel`이었다. 실제 Python `pane_lock.caller_server`와 검증 트리에서 추출한 셸 `_herdr_caller_server` 모두 `caller-sentinel`을 반환했다.
3. head fixture 실행 후 남은 변수는 없었다. 두 helper 모두 빈 값을 반환했다. 기대한 기본 호출자 분기 복원이 확인됐다.
4. 각 fixture 실행 뒤 `HERDR_SESSION=chosen-by-test`를 설정하면 두 helper 모두 `chosen-by-test`를 반환했다. 명시적 테스트 설정은 제거되지 않는다.
탐침 exit 0, 모든 assertion 통과. 임시 root는 `/tmp/b1696427-probe-6o1gbryi`였다. 이전 리뷰의 결과를 현재 커밋 검증으로 대체하지 않고 이번 identity에서 새로 실행했다.
[VERDICT: PASS]
@@ -0,0 +1,14 @@
{
"loop_id": "loop-20261013T001819Z-131fa6",
"base_sha": "8b0b4a07dc76a4915895bdde6170dc8eed9b5f74",
"head_sha": "325b056057fcdd717d2c81017fc5ef57183cb4d0",
"tree_sha": "7905896f005159d596e3872d0ee2c3c12061f09a",
"review_commit": "325b056057fcdd717d2c81017fc5ef57183cb4d0",
"reviewer": "planner-reviewer-codex-01",
"job_id": "efb52908",
"verdict": "PASS",
"transport": "herdr",
"evidence": "review-tree",
"isolation": "worktree",
"verified_at": "2026-10-13T00:57:46Z"
}
@@ -0,0 +1,80 @@
# Job efb52908 — herdr leak fix review
판정: PASS. 지정된 2개 커밋/4개 파일에서 이번 변경을 차단할 결함은 발견하지 않았다. 관련 테스트 **105 passed, 1 deselected**이고, base/head 디코이 탐침으로 경로 선택 수정과 기존 래퍼 제외 동작을 확인했다. 아래 기존 해석기 차이와 운영상 한계는 이번 변경의 회귀와 구분한다.
## 검증 identity 및 범위
- loop: `loop-20261013T001819Z-131fa6`
- base: `8b0b4a07dc76a4915895bdde6170dc8eed9b5f74`
- head: `325b056057fcdd717d2c81017fc5ef57183cb4d0`
- tree: `7905896f005159d596e3872d0ee2c3c12061f09a`
- 검증 cwd: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261013T001819Z-131fa6/1`
- Python: `/Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux/.venv/bin/python`
검증 종료 시에도 HEAD/tree가 위 identity와 일치하고 `git status --short`는 비어 있었다. `git diff --check <base>`와 `/bin/bash -n .agents/skills/lib.sh` 모두 통과했다. 리뷰 소스 수정, creator 작업 트리 실행, 커밋·push는 하지 않았다.
## 변경 검토
### L1: 조상 디렉터리 이름 때문에 mock이 가려지는 문제
`.agents/skills/lib.sh:113–124`는 일반 `*-shim` 제외를 디렉터리 자체의 접미사로 제한한다. `:132`에서 PATH 디렉터리를 검사하고, `:134–136`에서는 심볼릭 링크 해석 후 후보 파일의 **dirname**을 검사한다. 따라서 파일 이름 `herdr` 때문에 접미사 검사가 무력화되지 않고, wrapper를 가리키는 링크도 기존처럼 제외된다. 생성 shim의 `:172–176`도 일반 접미사 조건을 같은 방향으로 좁힌다.
검증 트리의 base와 head에서 해당 함수 본문만 추출해 `/bin/bash`로 실행했다. PATH 후보는 모두 `/tmp`에 만든 디코이이며 실제 herdr는 후보에 없었다.
| 후보 위치 | base 결과 | head 결과 |
|---|---|---|
| `pytest-x-shim/case/bin/herdr` | 두 해석기 모두 후보를 건너뛰고 다음 디코이 선택 | 두 해석기 모두 첫 후보 선택 |
| `herdr-shim/herdr` | 두 해석기 모두 제외 | 두 해석기 모두 제외 |
| `.mam/shim/herdr` | 두 해석기 모두 제외 | 두 해석기 모두 제외 |
| `multi-agent-herdr-shim/herdr` | 두 해석기 모두 제외 | 두 해석기 모두 제외 |
| `.agents/skills/.bin/herdr` | lib 해석기는 제외; 생성 shim은 선택 | 동일: lib 해석기는 제외; 생성 shim은 선택 |
추가로 lib 해석기에서 일반 `front/herdr` 심볼릭 링크가 `wrapped-shim/herdr` 또는 `.agents/skills/.bin/herdr`를 가리키면 제외됨을 확인했다. 링크 대상이 `parent-shim/bin/herdr`이면 허용됐다. 조상 이름과 wrapper 자체의 이름을 구분하려는 변경에 부합한다.
### L2: pane 라우팅 환경 격리
`tests/conftest.py:290–303`에서 기존 세션 이름·workspace·config 격리에 socket/pane/tab 삭제가 추가됐다. `tests/test_herdr_real_server_leak.py:73–87`은 안전한 가짜 socket 값과 pane 변수를 먼저 주입하고 `mam_sandbox` 이후 남지 않는지 검증하며 통과했다. 바깥 실행 환경을 미리 정리했더라도 이 시험은 자체적으로 오염 환경을 다시 만들어 검사한다.
브리프에 기록된 실제 herdr의 socket 우선순위 실측과 일치하는 수정이다. 이번 리뷰에서는 운영 서버 접촉을 금지한 조건에 따라 실제 서버 라우팅을 재현하지 않았다. 따라서 socket 우선순위의 실서버 증거는 브리프의 실측이고, 본 리뷰의 독립 증거는 fixture 삭제 동작 및 디코이 호출 경로 검증이다.
### mam_sandbox 밖의 경로와 HERDR_SESSION
삭제 범위는 `mam_sandbox`를 사용하는 시험에 한정된다. 검토한 비-sandbox 경로에서 이번 사고와 같은 추가 실제 서버 변경 경로는 찾지 못했다.
- `tests/test_issue50_dispatch_herdr.py:73–85`의 실제 서버용 Box 환경은 모든 `HERDR_*`를 제거하고 일회용 HOME/서버를 지정한다.
- `tests/test_issue45_restore.py:41–57`의 Rig는 socket/pane/tab/workspace 및 `HERDR_SESSION`, config 관련 값을 제거한다. 이 실서버 모듈들은 이번 리뷰에서 실행하지 않고 코드만 확인했다.
- `tests/test_issue17_item4_decisions.py:224–233`은 별도 환경 구성에서 `HERDR_*` 전체를 제거한다. 다른 비-sandbox 단위 시험은 in-process stub, 추출한 shell 함수, 또는 명시적인 mock을 사용하는 경로인지 확인했다.
- `mam_sandbox`는 `HERDR_SESSION` 자체를 삭제하지 않는다. 해당 값은 `lib.sh:1425–1427`의 caller 식별 및 `lib_py/pane_lock.py:45–65`의 lock 이름에 사용될 수 있다. lock 경로는 sandbox HOME 아래이며, socket·config 경로가 제거된 상태에서 이 변수만으로 운영 서버에 접속하는 재현은 얻지 않았다. 따라서 이를 이번 수정의 차단 결함으로 확대하지 않는다. 향후 공통 환경 정리 시 함께 다룰 수 있는 격리 보강 항목이다.
## 공유 생성 shim의 영향
이번 변경은 생성 heredoc의 한 조건을 의도적으로 바꾼다. `_init_herdr_isolation`은 `lib.sh:149–157`에서 제어 루트 아래 임시 shim을 만들고 `:1389–1390`에서 rename으로 교체한다. 별도 wrapper 내용 변경 외의 프로토콜 변경은 없다.
`test_issue23_24_startup_path.py:295–315`의 기준을 `525df2b`로 이동한 것은 이 의도적인 heredoc 변경을 반영하며 해당 테스트가 실제로 통과했다. 다만 이 테스트는 head와 그 새 기준의 동일성을 확인할 뿐, 현재 실행 중인 모든 체크아웃이 새 버전을 사용한다는 증명은 아니다. 오래된 코드와 새 코드가 동일 제어 루트를 공유하며 shim을 다시 생성하면 마지막 writer의 버전이 적용된다. 배포 시 같은 제어 루트의 생성 주체를 같은 버전으로 맞추는 것이 필요하다. 리뷰 중에는 운영 제어 루트의 shim을 읽거나 갱신하지 않았다.
## 비차단 기존 차이: 생성 shim의 .bin 제외 누락
`lib.sh:115`의 lib 해석기는 `.agents/skills/.bin`을 제외하지만, `:176`의 생성 shim 해석기는 그 별도 규칙을 가지고 있지 않다. 위 디코이 탐침에서 base와 head 모두 동일하게 확인했다. 그러므로 **두 해석기가 모든 wrapper 경로에 대해 동일한 제외 규칙을 갖는다**고 결론 내릴 수는 없다.
재현: `.agents/skills/.bin/herdr` 디코이를 PATH 첫 번째에, 다른 디렉터리의 디코이를 그 다음에 놓고 각 해석기를 호출한다. lib는 두 번째를 반환하고 생성 shim은 첫 번째를 반환한다. 원하는 완전한 일관성 기준에서는 생성 shim도 첫 번째를 제외해야 한다. 별도 보강 시 두 해석기의 명시적 wrapper 경로 규칙을 맞추고 이 경우를 회귀 테스트에 추가하는 방향이 적절하다. base부터 있던 차이이고 이번 접미사 축소로 새로 생긴 동작이 아니므로 본 변경의 PASS를 막지 않는다.
## 실행 결과와 재현
안전 launcher: `/tmp/efb52908-review/run.py`
```text
env -u HERDR_SOCKET_PATH -u HERDR_PANE_ID -u HERDR_TAB_ID -u HERDR_WORKSPACE_ID -u HERDR_SESSION_NAME -u WORKSPACE_ROOT -u MAM_ENV_FILE <Python> /tmp/efb52908-review/run.py tests/test_herdr_real_server_leak.py tests/test_herdr_shim_contract.py tests/test_issue23_24_startup_path.py tests/test_issue9_server_env.py -k 'not test_h10_real_herdr_schema_match'
```
- 결과: **105 passed, 1 deselected in 177.07s**, exit 0.
- 실제 설치 herdr의 schema를 조회하는 `test_h10_real_herdr_schema_match`만 명시적으로 제외했다. 전체 프로젝트 스위트는 실행하지 않았다.
- 로그: `/tmp/efb52908-review/tests.log`
- basetemp: `/tmp/efb52908-bbxxt8p8/bt` (이름에 shim 없음).
- launcher는 추가로 모든 상속 `HERDR_*`, `MAM_*`, `MQTT_*`, `SKS_*` 및 제어/에이전트 경로 변수를 제거하고 일회용 HOME, 지정 Python, `PYTHONDONTWRITEBYTECODE=1`, `-p no:cacheprovider`를 사용했다.
- fallback PATH에 호출을 기록하고 exit 97로 종료하는 herdr 디코이를 설치했다. **fallback 호출 기록 없음.** 테스트별 mock이 가려져 설치 herdr까지 내려가는 상황을 허용하지 않았다.
- broker는 기존 suite의 로컬 fixture를 사용했다. 운영 `.mam.env`를 export/출력하지 않았고 실제 사용자 홈·서버·모니터를 변경하지 않았다. agent-home 가드 오탐도 없었다.
- 독립 경로 탐침: `/tmp/efb52908-review/probe.py`, 로그 `/tmp/efb52908-review/probe.log`. 검증 cwd에서 같은 `env -u ... <Python>`으로 실행 가능하다. 5개 경로 × 2개 해석기의 base/head 관측과 3개 링크 사례를 기록했다. 위 기존 .bin 차이를 제외한 기대 조건 assertion은 모두 통과했다. 실제 herdr를 실행하지 않았다.
소스 변경 없이 지정 범위의 검토를 마쳤으며 재계획이 필요한 결함은 발견하지 않았다.
[VERDICT: PASS]
@@ -0,0 +1,14 @@
{
"loop_id": "loop-20261013T001819Z-131fa6",
"base_sha": "8b0b4a07dc76a4915895bdde6170dc8eed9b5f74",
"head_sha": "325b056057fcdd717d2c81017fc5ef57183cb4d0",
"tree_sha": "7905896f005159d596e3872d0ee2c3c12061f09a",
"review_commit": "325b056057fcdd717d2c81017fc5ef57183cb4d0",
"reviewer": "reviewer-agy-01",
"job_id": "a86376f9",
"verdict": "PASS",
"transport": "herdr",
"evidence": "review-tree",
"isolation": "worktree",
"verified_at": "2026-10-13T00:57:46Z"
}
@@ -0,0 +1,87 @@
# Code Review Report: `fix/test-herdr-leak`
- **Job ID**: `a86376f9`
- **Reviewer**: `agy` (Session: `herdr:reviewer-agy-01`, Role: `Reviewer`)
- **Date**: 2026-10-13
- **Target Branch**: `fix/test-herdr-leak`
- **Base Commit**: `8b0b4a07dc76a4915895bdde6170dc8eed9b5f74` (`main`)
- **Head Commit**: `325b056057fcdd717d2c81017fc5ef57183cb4d0`
- **Verification Path**: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261013T001819Z-131fa6/1`
- **Verification Environment**: macOS (Darwin 24.3.0, arm64), Python 3.9.6, bash 3.2.57(1)-release, `herdr` 0.9.3
---
## 1. Executive Summary
This code review evaluated the fixes in `fix/test-herdr-leak` (`8b0b4a0..325b056`, 2 commits, 4 files), resolving an incident where test suites executed from an agent pane with `--basetemp .../pytest-pr56-shim` inadvertently created 7 workspaces on the pane's real herdr server (`multi-agent-mux`).
### Root Causes Addressed
1. **Overspecified `-shim` Pattern Match**:
In [`lib.sh:_is_shim_path`](file:///var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261013T001819Z-131fa6/1/.agents/skills/lib.sh#L53) and the generated shim heredoc, the pattern `*-shim/*` matched *any* path having `-shim/` anywhere in its ancestor hierarchy. When pytest was invoked with `--basetemp .../pytest-pr56-shim`, any mock herdr binary placed under `$basetemp/.../bin/herdr` was misclassified as a shim and skipped. The real-herdr resolver continued down `$PATH` until finding system `/opt/homebrew/bin/herdr`.
2. **Host Socket Leaked Through `mam_sandbox`**:
`mam_sandbox` previously unset `HERDR_SESSION_NAME`, `HERDR_SERVER_NAME`, and `HERDR_WORKSPACE_ID`, but left `HERDR_SOCKET_PATH`, `HERDR_PANE_ID`, and `HERDR_TAB_ID` untouched. In `herdr` 0.9.3, `HERDR_SOCKET_PATH` overrides `$HOME`, so any unintended call to the real herdr binary connected directly to the host pane's herdr daemon socket.
### The Fix
1. In [`lib.sh`](file:///var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261013T001819Z-131fa6/1/.agents/skills/lib.sh):
- Refined `_is_shim_path()`: removed `*-shim/*` from substring globbing and added `case "${1%/}" in *-shim) return 0 ;; esac` so only directories whose leaf name matches `*-shim` are considered shims.
- Passed `$(dirname "$cand")` into `_is_shim_path` in `_resolve_real_herdr_path`.
- Updated generated shim heredoc: replaced `[[ "$dir" != *"-shim"* ]]` with `[[ "${dir%/}" != *-shim ]]`.
2. In [`tests/conftest.py:mam_sandbox`](file:///var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261013T001819Z-131fa6/1/tests/conftest.py#L293):
- Explicitly strips `HERDR_SOCKET_PATH`, `HERDR_PANE_ID`, and `HERDR_TAB_ID`.
3. In [`tests/test_issue23_24_startup_path.py`](file:///var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261013T001819Z-131fa6/1/tests/test_issue23_24_startup_path.py):
- Updated the generated shim byte-identity pin to base commit `525df2b`.
4. In [`tests/test_herdr_real_server_leak.py`](file:///var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261013T001819Z-131fa6/1/tests/test_herdr_real_server_leak.py):
- Added unit and regression tests reproducing both leak vectors and verifying proper resolution.
**Verdict: PASS**.
---
## 2. Technical Evaluation
### 2.1 Diagnosis & Resolution Accuracy
- **Pattern Match Tightening**:
Testing with mock binaries located under `/path/to/pytest-x-shim/case0/bin/herdr` confirms that:
- `$(dirname "$cand")` is `/path/to/pytest-x-shim/case0/bin`.
- Leaf directory name is `bin`, which does not match `*-shim`.
- The mock binary is accepted and executed; system `herdr` is never reached.
- Dedicated wrapper directories whose leaf name is `*-shim` (e.g. `tmp / "herdr-shim" / "herdr"`) are still properly skipped.
- **Routing Variable Cleansing**:
Unsetting `HERDR_SOCKET_PATH`, `HERDR_PANE_ID`, and `HERDR_TAB_ID` eliminates the socket bypass vector in herdr 0.9.3. If a test ever fails to mock herdr, the real binary fails with `server_not_running` rather than attaching to the active multi-agent session.
### 2.2 Secondary Leak Paths & Environmental Isolation
- All known `HERDR_*` variables are accounted for in `mam_sandbox`:
- `HERDR_SESSION_NAME`, `HERDR_SERVER_NAME`, `HERDR_WORKSPACE_ID`
- `HERDR_SOCKET_PATH`, `HERDR_PANE_ID`, `HERDR_TAB_ID`
- `HERDR_CONFIG_PATH`, `XDG_CONFIG_HOME`
- Non-sandbox integration tests (such as [`tests/test_issue50_dispatch_herdr.py`](file:///var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261013T001819Z-131fa6/1/tests/test_issue50_dispatch_herdr.py)) explicitly scrub all environment variables starting with `HERDR_` and isolate `$HOME` under `/tmp`.
### 2.3 Exclusion of Real Shim Directories
The narrowed `_is_shim_path` pattern continues to exclude all designated shim locations:
- `*/.mam/shim/*`: matched by `*"/.mam/shim/"*`.
- `multi-agent-herdr-shim`: matched by `*"$_HERDR_SHIM_DIR_PATTERN"*`.
- `.agents/skills/.bin`: matched by `*"$_HERDR_SKILLS_BIN_PATTERN"/*`.
- Arbitrary test wrapper directories ending in `-shim`: matched by `${1%/}` -> `*-shim`.
### 2.4 Control Plane Impact
The generated shim in `.mam/shim/herdr` is modified only to use `[[ "${dir%/}" != *-shim ]]`. The byte-identity assertion in [`test_issue23_24_startup_path.py::test_s11`](file:///var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261013T001819Z-131fa6/1/tests/test_issue23_24_startup_path.py#L200) was updated to compare against commit `525df2b`, consistent with historical updates for intentional shim modifications (#45).
---
## 3. Test Verification Results
All tests executed with sanitized environment variables (`env -u HERDR_SOCKET_PATH -u HERDR_PANE_ID -u HERDR_TAB_ID -u HERDR_WORKSPACE_ID -u HERDR_SESSION_NAME -u WORKSPACE_ROOT -u MAM_ENV_FILE`):
| Test Suite / Target | Result | Execution Time |
|---|---|---|
| `tests/test_herdr_real_server_leak.py` (New regression suite) | **3 Passed** | 1.39s |
| `tests/test_issue23_24_startup_path.py` (Full suite, 70 tests) | **70 Passed** | 141.34s |
| `tests/test_herdr_shim_contract.py` (Full suite, 18 tests) | **18 Passed** | 27.48s |
- `test_l1_ancestor_named_shim_does_not_hide_the_mock`: Verifies mock binary under `...-shim/.../bin` is used, decoy is not touched.
- `test_l1_a_directory_named_shim_is_still_skipped`: Verifies wrapper directories named `*-shim` remain excluded.
- `test_l2_sandbox_drops_the_pane_routing`: Verifies `mam_sandbox` purges all pane routing variables.
---
[VERDICT: PASS]
@@ -0,0 +1,14 @@
{
"loop_id": "loop-20261013T010208Z-f90dd4",
"base_sha": "325b056057fcdd717d2c81017fc5ef57183cb4d0",
"head_sha": "db7f4cab9c60ff5db009cf3fc625e9a86a465ef6",
"tree_sha": "f32cabd3a6c8d775c9223509967f1a810f0cf827",
"review_commit": "db7f4cab9c60ff5db009cf3fc625e9a86a465ef6",
"reviewer": "reviewer-agy-01",
"job_id": "e375099a",
"verdict": "PASS",
"transport": "herdr",
"evidence": "review-tree",
"isolation": "worktree",
"verified_at": "2026-10-13T01:21:50Z"
}
@@ -0,0 +1,60 @@
# Code Review Report (2nd Round): `fix/test-herdr-leak`
- **Job ID**: `e375099a`
- **Reviewer**: `agy` (Session: `herdr:reviewer-agy-01`, Role: `Reviewer`)
- **Date**: 2026-10-13
- **Target Branch**: `fix/test-herdr-leak`
- **Base Commit**: `325b056057fcdd717d2c81017fc5ef57183cb4d0` (1st round review head)
- **Head Commit**: `db7f4cab9c60ff5db009cf3fc625e9a86a465ef6` (Fix commit)
- **Verification Path**: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261013T010208Z-f90dd4/1`
- **Verification Environment**: macOS (Darwin 24.3.0, arm64), Python 3.9.6, bash 3.2.57(1)-release, `herdr` 0.9.3
---
## 1. Executive Summary
This 2nd-round review evaluated commit `db7f4cab9c60ff5db009cf3fc625e9a86a465ef6`, addressing non-blocking feedback from 1st-round reviews (claude `63919c5d` and grok `191f0ec3`):
- **Finding**: While `HERDR_SOCKET_PATH`, `HERDR_PANE_ID`, and `HERDR_TAB_ID` were stripped in `mam_sandbox`, `HERDR_SESSION` was still inherited from the ambient host environment.
- **Impact**: Although `HERDR_SESSION` on its own resolves under `$HOME/.config/herdr/sessions/<session>` (so sandboxing `$HOME` prevented socket file leaks), ambient-server helpers like [`_herdr_caller_server`](file:///var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261013T010208Z-f90dd4/1/.agents/skills/lib.sh#L1453) and [`pane_lock.caller_server()`](file:///var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261013T010208Z-f90dd4/1/.agents/skills/lib_py/pane_lock.py#L671) read `HERDR_SESSION` first. Consequently, tests branched on whether they were executed from within an agent pane or outside a pane.
- **The Fix**:
1. [`tests/conftest.py:mam_sandbox`](file:///var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261013T010208Z-f90dd4/1/tests/conftest.py#L296): added `monkeypatch.delenv("HERDR_SESSION", raising=False)`.
2. [`tests/test_herdr_real_server_leak.py`](file:///var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261013T010208Z-f90dd4/1/tests/test_herdr_real_server_leak.py#L64): added `"HERDR_SESSION": "real"` to `PANE_VARS`, covered by `test_l2_sandbox_drops_the_pane_routing`.
**Verdict: PASS**.
---
## 2. Technical Evaluation
### 2.1 Environmental Isolation & Branching Consistency
- In `lib_py/pane_lock.py:caller_server()`:
```python
def caller_server(env: Mapping[str, str]) -> str:
if env.get("HERDR_SESSION"):
return env["HERDR_SESSION"]
```
And in `lib.sh`:
`HERDR_SESSION` indicates that the current process runs inside an active herdr pane.
By stripping `HERDR_SESSION` in `mam_sandbox`, every test begins with a clean non-pane baseline environment. Test behavior is now strictly reproducible regardless of whether the test runner runs inside an interactive terminal, CI, or an autonomous agent herdr session.
### 2.2 Test Compatibility
- Tests that explicitly test `HERDR_SESSION` configuration (e.g. `test_issue50_dispatch_herdr.py:769`, `test_issue45_restore.py`, `test_tier1_unit.py`) set the environment variable directly within the test body or pass it via `env=...` in subprocess calls. Because test bodies execute after fixture setup, their explicit settings take precedence and function properly.
- All 6 unit tests in `tests/test_issue50_pane_lock.py` (`test_p1` to `test_p5`) and contract tests in `tests/test_herdr_shim_contract.py` pass cleanly.
---
## 3. Test Verification Results
All tests executed with sanitized environment variables (`env -u HERDR_SOCKET_PATH -u HERDR_PANE_ID -u HERDR_TAB_ID -u HERDR_WORKSPACE_ID -u HERDR_SESSION_NAME -u HERDR_SESSION -u WORKSPACE_ROOT -u MAM_ENV_FILE`):
| Test Suite / Target | Result | Execution Time |
|---|---|---|
| `tests/test_herdr_real_server_leak.py` | **3 Passed** | 1.44s |
| `tests/test_herdr_shim_contract.py:test_h26` | **2 Passed** | 3.98s |
| `tests/test_issue50_pane_lock.py:test_p*` | **6 Passed** | 4.57s |
- `test_l2_sandbox_drops_the_pane_routing`: Verifies that `HERDR_SESSION` is stripped by `mam_sandbox` alongside `HERDR_SOCKET_PATH`, `HERDR_PANE_ID`, `HERDR_TAB_ID`, `HERDR_WORKSPACE_ID`, and `HERDR_SESSION_NAME`.
---
[VERDICT: PASS]
@@ -0,0 +1,14 @@
{
"loop_id": "loop-20261013T001819Z-131fa6",
"base_sha": "8b0b4a07dc76a4915895bdde6170dc8eed9b5f74",
"head_sha": "325b056057fcdd717d2c81017fc5ef57183cb4d0",
"tree_sha": "7905896f005159d596e3872d0ee2c3c12061f09a",
"review_commit": "325b056057fcdd717d2c81017fc5ef57183cb4d0",
"reviewer": "reviewer-grok-01",
"job_id": "191f0ec3",
"verdict": "PASS",
"transport": "herdr",
"evidence": "review-tree",
"isolation": "worktree",
"verified_at": "2026-10-13T00:57:46Z"
}
@@ -0,0 +1,51 @@
# Review 191f0ec3 — a test run from a pane no longer opens workspaces on that pane's server
Head `325b056057fcdd717d2c81017fc5ef57183cb4d0` (tree `7905896f005159d596e3872d0ee2c3c12061f09a`) is two commits on base `8b0b4a07dc76a4915895bdde6170dc8eed9b5f74`: `525df2b` (the leak fix) and `325b056` (the generated-shim pin moves to `525df2b`). Four files, +106/−5. `git diff --check` is clean. Snapshot porcelain empty. Control-plane main is still `8b0b4a07dc76a4915895bdde6170dc8eed9b5f74`, porcelain empty.
Both measured causes are right, and the fix matches them.
The suite that left seven workspaces on `multi-agent-mux` ran with `--basetemp .../pytest-pr56-shim`. `mock_herdr` puts its `bin/herdr` under that basetemp. Both resolvers used to skip any PATH directory whose path contained `-shim` anywhere, so they walked past the mock and executed `/opt/homebrew/bin/herdr`. The real binary follows `HERDR_SOCKET_PATH` ahead of `HOME`, and `mam_sandbox` left the pane's socket in the environment.
`_is_shim_path` (`lib.sh:113-125`) still matches `/.mam/shim/`, `/multi-agent-herdr-shim/`, and `/.agents/skills/.bin`. The substring `*-shim/*` is gone. A second `case` matches only when the path with one trailing slash stripped is itself `*-shim`. `_resolve_real_herdr_path` (`:127-143`) skips a PATH directory on that test, then skips the candidate when `_is_shim_path` matches `dirname` of the canonical file (`:136`). It still prints the PATH entry, `$dir/herdr`. The generated shim's `_resolve_real_herdr` (`:172-186`) now requires `${dir%/}` to fail the pattern `*-shim`, and it still rejects a directory containing `.mam/shim`.
`mam_sandbox` deletes `HERDR_SOCKET_PATH`, `HERDR_PANE_ID`, and `HERDR_TAB_ID` (`tests/conftest.py:296-298`).
## Wrapper directories
A decoy-only probe sourced this snapshot's `lib.sh` with `WORKSPACE_ROOT=/tmp/m191ws`. PATH was the case directory, a stand-in, `/usr/bin`, and `/bin`. The real binary was not on it.
| PATH directory | `_resolve_real_herdr_path` | generated shim |
|---|---|---|
| `.../pytest-x-shim/case0/bin` | that mock | that mock |
| a directory whose own name is `herdr-shim` | stand-in | stand-in |
| `.../multi-agent-herdr-shim/srv` | stand-in | that directory |
| `.../.agents/skills/.bin` | stand-in | that directory |
| `.../.mam/shim/extra` | stand-in | stand-in |
An ancestor named `pytest-x-shim` above `bin/` is selected by both resolvers. A directory that is itself `*-shim` is skipped by both. `.mam/shim` and a child of it are skipped by both. L1 and L1b assert the same two outcomes inside the suite, with a decoy behind the mock, and both passed.
`lib.sh` still excludes `/multi-agent-herdr-shim/` and `/.agents/skills/.bin`. The generated shim excludes a PATH entry whose own name ends in `-shim`, so the directory `multi-agent-herdr-shim` itself is skipped by both. A child of that directory, whose own name does not end in `-shim`, is excluded only by `_resolve_real_herdr_path`. `README.md:102` and `README.ko.md:84` still describe `/tmp/multi-agent-herdr-shim/<HERDR_SERVER_NAME>/herdr`. No script in this tree creates that path; `_init_herdr_isolation` writes `$WORKSPACE_ROOT/.mam/shim/herdr`. The `.agents/skills/.bin` gap on the generated shim is older than this commit: the previous generated check was the substring `*"-shim"*`, and `.bin` never matched it. Nothing in `lib.sh` prepends `.agents/skills/.bin` onto PATH.
## What still reaches the real binary
A direct call of `mam_sandbox`, with probe values set first, left `HERDR_SESSION` and `HERDR_ENV` set and removed `HERDR_SOCKET_PATH`, `HERDR_PANE_ID`, `HERDR_TAB_ID`, `HERDR_SESSION_NAME`, `HERDR_WORKSPACE_ID`, and `HERDR_CONFIG_PATH`. `HOME` was the sandbox. L2's `PANE_VARS` (`tests/test_herdr_real_server_leak.py:74-75`) are the socket, the pane and tab ids, the workspace id, and `HERDR_SESSION_NAME`. The test passed. It does not include `HERDR_SESSION`.
This reviewer process inherited `HERDR_SESSION` from its pane. `lib.sh:1411-1415` records that herdr 0.8.2 left `HERDR_SESSION` unset on a default-server pane and exported the socket to every pane. `docs/contracts/issue-6/implementation_plan.md` records the measurement that the real binary follows `HERDR_SESSION`, `HERDR_SOCKET_PATH`, and `--session`, and that `HERDR_SESSION_NAME` alone reaches the default socket. The calls that created the seven workspaces go through these resolvers, which now return the mock, so they do not reach the binary that reads `HERDR_SESSION`. A later call that misses the mock still inherits `HERDR_SESSION`.
The suite's intentional real-server tests already drop it. `tests/test_issue45_restore.py` lists `HERDR_SESSION` in `SCOPE_ENV` and builds the server env from what remains. `tests/test_issue50_dispatch_herdr.py` `Box.env` drops every `HERDR_*` name, and `Box.herdr` passes only `HOME`, `PATH`, and `--session`. `tests/test_herdr_shim_contract.py::test_h10_real_herdr_schema_match` calls `herdr api schema` with the inherited environment and does not use `mam_sandbox`. That test was already in the tree. I did not run it, and I did not run the real binary. `api schema` is a read. The workspace-creating path was the resolver.
`HERDR_ENV` also survived the fixture call. The measured contract names `HERDR_SESSION`, `HERDR_SOCKET_PATH`, and `--session`. I did not treat `HERDR_ENV` as a second route.
## Shared control plane
`test_s11_this_branch_does_not_change_the_generated_shim` (`tests/test_issue23_24_startup_path.py:294-315`) compares the shim heredoc with `git show 525df2b:.agents/skills/lib.sh`, unless `MAM_BASE_SHA` overrides it. It passed, so the heredoc at `325b056` is the heredoc at `525df2b`. The second commit changes the pin and the comment. Sourcing `lib.sh` rewrites `$WORKSPACE_ROOT/.mam/shim/herdr`. This review sourced it with `WORKSPACE_ROOT` pointed at `/tmp/m191ws` and at pytest sandboxes. The narrowing becomes the control plane's shim when this branch is merged and something sources `lib.sh` with that workspace root.
## What was run
Control-plane venv, Python 3.9.6, pytest 8.4.2, `PYTHONDONTWRITEBYTECODE=1`, `-p no:cacheprovider`, `--basetemp=/tmp/m191bt-leak2` (the path contains neither `shim` nor `mam-review`). Inherited `HERDR_SOCKET_PATH`, `HERDR_PANE_ID`, `HERDR_TAB_ID`, `HERDR_WORKSPACE_ID`, `HERDR_SESSION_NAME`, `HERDR_SESSION`, `HERDR_SERVER_NAME`, `HERDR_CONFIG_PATH`, `HERDR_ENV`, `WORKSPACE_ROOT`, `MAM_ENV_FILE`, `HOME_DIR`, `CLAUDE_PROJECT_DIR`, and `CODEX_HOME` were unset.
`tests/test_herdr_real_server_leak.py` and `tests/test_issue23_24_startup_path.py::test_s11_this_branch_does_not_change_the_generated_shim`: 4 passed in 1.50s. The session guards that travel with that run completed. The #55 agent-home guard did not fire.
User `~/.config/herdr/config.toml` mtime stayed `1784456394`. Snapshot porcelain stayed empty after the run.
[VERDICT: PASS]
@@ -0,0 +1,14 @@
{
"loop_id": "loop-20261013T010208Z-f90dd4",
"base_sha": "325b056057fcdd717d2c81017fc5ef57183cb4d0",
"head_sha": "db7f4cab9c60ff5db009cf3fc625e9a86a465ef6",
"tree_sha": "f32cabd3a6c8d775c9223509967f1a810f0cf827",
"review_commit": "db7f4cab9c60ff5db009cf3fc625e9a86a465ef6",
"reviewer": "reviewer-grok-01",
"job_id": "9cd9b3f0",
"verdict": "PASS",
"transport": "herdr",
"evidence": "review-tree",
"isolation": "worktree",
"verified_at": "2026-10-13T01:21:50Z"
}
@@ -0,0 +1,35 @@
# Review 9cd9b3f0 — the sandbox also drops HERDR_SESSION
Head `db7f4cab9c60ff5db009cf3fc625e9a86a465ef6` (tree `f32cabd3a6c8d775c9223509967f1a810f0cf827`) is the one commit `db7f4ca test(sandbox): mam_sandbox also drops HERDR_SESSION` on base `325b056057fcdd717d2c81017fc5ef57183cb4d0`. Two files, +5/−1. `git diff --check` is clean. Snapshot porcelain empty. Control-plane main is still `8b0b4a07dc76a4915895bdde6170dc8eed9b5f74`, porcelain empty.
`mam_sandbox` deletes `HERDR_SESSION` next to `HERDR_SOCKET_PATH` (`tests/conftest.py:298-299`). L2's pane model now includes `HERDR_SESSION=real` (`tests/test_herdr_real_server_leak.py:73-76`) and asserts that name is gone with the others (`:86-88`).
## What the variable does inside a sandbox
The herdr binary treats `HERDR_SESSION` as a session name: the string beside it is `session name cannot be empty`. A named session's socket is `$HOME/.config/herdr/sessions/<name>/herdr.sock`. `mam_sandbox` sets `HOME` to the sandbox, so that name resolves inside the sandbox. The route that reached the caller's real server was `HERDR_SOCKET_PATH`, which the previous commit already deletes.
`_herdr_caller_server` (`lib.sh:1424-1427`), `pane_lock.caller_server` (`lib_py/pane_lock.py:44-50`), and `watcher.caller_server` (`lib_py/loop/watcher.py:50-66`) return `HERDR_SESSION` whenever it is set, ahead of the socket. A test run inside a pane therefore took the in-pane branch, and the same test outside a pane took the other. Dropping the variable makes those helpers see a process outside a pane unless the test supplies the name itself.
## Tests that supply the name
No test calls `monkeypatch.setenv("HERDR_SESSION")`. The tests that need a value build the child environment themselves:
- `tests/test_issue23_24_startup_path.py` `_run_lib` (`:36-40`) pops `HERDR_SESSION` from the inherited environment and then applies the dict the test passes. `test_s1g` passes `HERDR_SESSION=from-herdr` and expects `caller=from-herdr`.
- `tests/test_issue34_job_tree.py` `_run_lib` (`:590-591`) drops every `HERDR_*` name and then applies `SAME_SERVER_PANE`, which contains `HERDR_SESSION=multi-agent-mux`.
- `tests/test_issue50_pane_lock.py` passes an explicit mapping into `lock_path` and into the `pane_lock path` subprocess. `test_p1b` builds that subprocess environment from the keys that do not start with `HERDR_`, then adds the socket it wants.
A direct call of `mam_sandbox` with `HERDR_SESSION=probe-session` left the name unset. A `setenv` after that call left `HERDR_SESSION=set-by-test`. The fixture setup runs before the test body, so a test that sets the name for itself keeps it. The one `setenv` of a `HERDR_` name in a test body is `HERDR_SERVER_NAME` in `tests/test_tier3_integration.py:31`, and that line runs after the fixture.
## What the sandbox still leaves
`HERDR_ENV` stays. In this pane its value is `1`. The binary's text says `Requires HERDR_ENV=1` beside the inception check, so this is a feature flag. `HERDR_BIN_PATH` stays, and its value names the herdr binary. `HERDR_CLIENT_SOCKET_PATH` sits beside `HERDR_SOCKET_PATH` in the binary's pane-export cluster. This process does not have that name, and the sandbox does not delete it.
## What was run
Control-plane venv, Python 3.9.6, pytest 8.4.2, `PYTHONDONTWRITEBYTECODE=1`, `-p no:cacheprovider`, `--basetemp=/tmp/m9cdbt`. Inherited `HERDR_SOCKET_PATH`, `HERDR_PANE_ID`, `HERDR_TAB_ID`, `HERDR_WORKSPACE_ID`, `HERDR_SESSION_NAME`, `HERDR_SESSION`, `HERDR_SERVER_NAME`, `HERDR_CONFIG_PATH`, `HERDR_ENV`, `HERDR_BIN_PATH`, `WORKSPACE_ROOT`, `MAM_ENV_FILE`, `HOME_DIR`, `CLAUDE_PROJECT_DIR`, and `CODEX_HOME` were unset. The real herdr binary was not run.
`tests/test_herdr_real_server_leak.py`, `tests/test_issue23_24_startup_path.py`, and `tests/test_issue50_pane_lock.py`: 84 passed in 195.84s. That is the same 84 the orchestrator reported. The session guards on that run completed. The #55 agent-home guard did not fire.
User `~/.config/herdr/config.toml` mtime stayed `1784456394`. Snapshot porcelain stayed empty after the run.
[VERDICT: PASS]
+10 -4
View File
@@ -29,7 +29,7 @@ export WORKSPACE_ROOT
# Framework semantic version. Single runtime source of truth; kept in lockstep # Framework semantic version. Single runtime source of truth; kept in lockstep
# with VERSIONS.md and the 8 SKILL.md frontmatters by tests/test_version_consistency.py. # with VERSIONS.md and the 8 SKILL.md frontmatters by tests/test_version_consistency.py.
# NOTE: unlike other MAM_* variables this one is intentionally NOT env-overridable. # NOTE: unlike other MAM_* variables this one is intentionally NOT env-overridable.
MAM_VERSION="4.9.0" MAM_VERSION="4.9.1"
export MAM_VERSION export MAM_VERSION
AGENT_SESSIONS_YAML="${AGENT_SESSIONS_YAML:-$WORKSPACE_ROOT/.mam/agent-sessions.yaml}" AGENT_SESSIONS_YAML="${AGENT_SESSIONS_YAML:-$WORKSPACE_ROOT/.mam/agent-sessions.yaml}"
@@ -112,9 +112,15 @@ _canonical_file() {
_is_shim_path() { _is_shim_path() {
case "/$1/" in case "/$1/" in
*"/.mam/shim/"*|*-shim/*|*"$_HERDR_SHIM_DIR_PATTERN"*|*"$_HERDR_SKILLS_BIN_PATTERN"/*) *"/.mam/shim/"*|*"$_HERDR_SHIM_DIR_PATTERN"*|*"$_HERDR_SKILLS_BIN_PATTERN"/*)
return 0 ;; return 0 ;;
esac esac
# A wrapper directory named *-shim, the directory itself only: an ancestor
# such as a pytest basetemp "pytest-x-shim" above a mock's bin/ made the
# resolver skip the mock and run the real herdr.
case "${1%/}" in
*-shim) return 0 ;;
esac
return 1 return 1
} }
@@ -127,7 +133,7 @@ _resolve_real_herdr_path() {
[ -x "$dir/herdr" ] || continue [ -x "$dir/herdr" ] || continue
cand="$(_canonical_file "$dir/herdr" 2>/dev/null)" || cand="$dir/herdr" cand="$(_canonical_file "$dir/herdr" 2>/dev/null)" || cand="$dir/herdr"
[ -n "$cand" ] || cand="$dir/herdr" [ -n "$cand" ] || cand="$dir/herdr"
_is_shim_path "$cand" && continue _is_shim_path "$(dirname "$cand")" && continue
real_path="$dir/herdr" real_path="$dir/herdr"
break break
done done
@@ -167,7 +173,7 @@ _resolve_real_herdr() {
local dir save_ifs="$IFS" real_path="" local dir save_ifs="$IFS" real_path=""
IFS=: IFS=:
for dir in $PATH; do for dir in $PATH; do
if [[ "$dir" != *".mam/shim"* ]] && [[ "$dir" != *"-shim"* ]] && [ -x "$dir/herdr" ]; then if [[ "$dir" != *".mam/shim"* ]] && [[ "${dir%/}" != *-shim ]] && [ -x "$dir/herdr" ]; then
real_path="$dir/herdr" real_path="$dir/herdr"
break break
fi fi
+1 -1
View File
@@ -13,7 +13,7 @@ import sys
from typing import List, Sequence from typing import List, Sequence
# Framework version locked with framework constants # Framework version locked with framework constants
FRAMEWORK_VERSION = "4.9.0" FRAMEWORK_VERSION = "4.9.1"
CORE_MODULES: Sequence[str] = ( CORE_MODULES: Sequence[str] = (
# Standard library modules # Standard library modules
@@ -1,7 +1,7 @@
--- ---
name: multi-agent-mux-create name: multi-agent-mux-create
description: "Create a new agent session (claude, antigravity/agy) in a dedicated herdr session for context-preserving long-running work. Always creates a herdr session — never backgrounds with nohup/disown. Writes the new session to .mam/agent-sessions.yaml. Use when you want to start a fresh agent (no prior UUID) for a new project workspace." description: "Create a new agent session (claude, antigravity/agy) in a dedicated herdr session for context-preserving long-running work. Always creates a herdr session — never backgrounds with nohup/disown. Writes the new session to .mam/agent-sessions.yaml. Use when you want to start a fresh agent (no prior UUID) for a new project workspace."
version: 4.9.0 version: 4.9.1
author: godopu author: godopu
license: MIT license: MIT
platforms: [linux, macos] platforms: [linux, macos]
@@ -1,7 +1,7 @@
--- ---
name: multi-agent-mux-delegate-job name: multi-agent-mux-delegate-job
description: "Delegate a unit of work to any autonomous agent (claude-code, hermes, agy, grok-build, codex, or a human) and observe it asynchronously over an MQTT event channel. Supported roles include orchestrator, worker, and reviewer." description: "Delegate a unit of work to any autonomous agent (claude-code, hermes, agy, grok-build, codex, or a human) and observe it asynchronously over an MQTT event channel. Supported roles include orchestrator, worker, and reviewer."
version: 4.9.0 version: 4.9.1
author: godopu author: godopu
license: MIT license: MIT
platforms: [linux, macos, windows] platforms: [linux, macos, windows]
+1 -1
View File
@@ -1,7 +1,7 @@
--- ---
name: multi-agent-mux-loop name: multi-agent-mux-loop
description: "Run an autonomous planning-execution-review loop using multiple agents (Planner, Creator, Reviewers) in the workspace. Automatically orchestrates plan discussion, code changes, and peer reviews until a unanimous PASS is achieved or the maximum iteration limit is reached." description: "Run an autonomous planning-execution-review loop using multiple agents (Planner, Creator, Reviewers) in the workspace. Automatically orchestrates plan discussion, code changes, and peer reviews until a unanimous PASS is achieved or the maximum iteration limit is reached."
version: 4.9.0 version: 4.9.1
author: godopu author: godopu
license: MIT license: MIT
platforms: [linux, macos] platforms: [linux, macos]
@@ -1,7 +1,7 @@
--- ---
name: multi-agent-mux-monitor name: multi-agent-mux-monitor
description: "Run a long-lived reconciler that watches .mam/agent-sessions.yaml against the actual herdr/agent runtime state and reconciles them. Use when you want live visibility into which agent sessions are running, which are dead, which have stale YAML entries, and which have new session ids that haven't been recorded yet. Runs as a persistent loop (`reconcile.sh --subscribe`) that keeps going until it times out, idles out, or is interrupted." description: "Run a long-lived reconciler that watches .mam/agent-sessions.yaml against the actual herdr/agent runtime state and reconciles them. Use when you want live visibility into which agent sessions are running, which are dead, which have stale YAML entries, and which have new session ids that haven't been recorded yet. Runs as a persistent loop (`reconcile.sh --subscribe`) that keeps going until it times out, idles out, or is interrupted."
version: 4.9.0 version: 4.9.1
author: godopu author: godopu
license: MIT license: MIT
platforms: [linux, macos] platforms: [linux, macos]
@@ -1,7 +1,7 @@
--- ---
name: multi-agent-mux-orc-onboard name: multi-agent-mux-orc-onboard
description: "Register current or specified orchestrator session UUID into agent-sessions.yaml orchestrator_uuids list to prevent sub-agent discovery capture." description: "Register current or specified orchestrator session UUID into agent-sessions.yaml orchestrator_uuids list to prevent sub-agent discovery capture."
version: 4.9.0 version: 4.9.1
author: godopu author: godopu
license: MIT license: MIT
platforms: [linux, macos] platforms: [linux, macos]
@@ -1,7 +1,7 @@
--- ---
name: multi-agent-mux-resume name: multi-agent-mux-resume
description: "Resume an existing agent (claude, antigravity/agy) conversation by UUID into a herdr session. Reads .mam/agent-sessions.yaml for the saved session/conversation id, spawns (or reuses) a herdr session of the matching name, and runs `claude -r <id>` or `agy --conversation <id>` inside. Use when you want to reattach to a previous session's context, or revive a session whose herdr died but the agent's conversation is still on disk." description: "Resume an existing agent (claude, antigravity/agy) conversation by UUID into a herdr session. Reads .mam/agent-sessions.yaml for the saved session/conversation id, spawns (or reuses) a herdr session of the matching name, and runs `claude -r <id>` or `agy --conversation <id>` inside. Use when you want to reattach to a previous session's context, or revive a session whose herdr died but the agent's conversation is still on disk."
version: 4.9.0 version: 4.9.1
author: godopu author: godopu
license: MIT license: MIT
platforms: [linux, macos] platforms: [linux, macos]
@@ -1,7 +1,7 @@
--- ---
name: multi-agent-mux-status name: multi-agent-mux-status
description: "Read-only instant snapshot of all agent herdr sessions — name, YAML status, herdr alive, pane cmd/cwd, resume UUID on disk, and any drift. No mutation. Reuses reconcile.sh --dry-run for the diff logic. Use when you want to know 'what's running RIGHT NOW' without spinning up the monitor loop." description: "Read-only instant snapshot of all agent herdr sessions — name, YAML status, herdr alive, pane cmd/cwd, resume UUID on disk, and any drift. No mutation. Reuses reconcile.sh --dry-run for the diff logic. Use when you want to know 'what's running RIGHT NOW' without spinning up the monitor loop."
version: 4.9.0 version: 4.9.1
author: godopu author: godopu
license: MIT license: MIT
platforms: [linux, macos] platforms: [linux, macos]
+1 -1
View File
@@ -1,7 +1,7 @@
--- ---
name: multi-agent-mux-stop name: multi-agent-mux-stop
description: "Stop an agent herdr session (claude, antigravity/agy) and update .mam/agent-sessions.yaml. Default stops gracefully and marks status=stopped with conversation preserved for resume. Does NOT delete on-disk conversation artifacts (jsonl/db) — those are preserved unless --purge-conversation is passed. Use when ending a work session, switching to a different one, or cleaning up before a fresh start." description: "Stop an agent herdr session (claude, antigravity/agy) and update .mam/agent-sessions.yaml. Default stops gracefully and marks status=stopped with conversation preserved for resume. Does NOT delete on-disk conversation artifacts (jsonl/db) — those are preserved unless --purge-conversation is passed. Use when ending a work session, switching to a different one, or cleaning up before a fresh start."
version: 4.9.0 version: 4.9.1
author: godopu author: godopu
license: MIT license: MIT
platforms: [linux, macos] platforms: [linux, macos]
+1 -1
View File
@@ -2,7 +2,7 @@
본 문서는 `herdr_agent_orchestration` 오케스트레이션 및 메시징 백플레인 워크플로우를 새로운 프로젝트에 도입하여 이식하고, 새로운 개발자/에이전트가 초기 가동을 시작할 때 수행해야 하는 환경 설정 및 구축 절차를 안내합니다. 본 문서는 `herdr_agent_orchestration` 오케스트레이션 및 메시징 백플레인 워크플로우를 새로운 프로젝트에 도입하여 이식하고, 새로운 개발자/에이전트가 초기 가동을 시작할 때 수행해야 하는 환경 설정 및 구축 절차를 안내합니다.
[![Version: v4.9.0](https://img.shields.io/badge/version-v4.9.0-blue.svg)](VERSIONS.md) [![Version: v4.9.1](https://img.shields.io/badge/version-v4.9.1-blue.svg)](VERSIONS.md)
새로운 에이전트는 이 안내서에 기술된 절차를 순차적으로 실행하여 초기 환경을 안정적으로 설정할 수 있습니다. 새로운 에이전트는 이 안내서에 기술된 절차를 순차적으로 실행하여 초기 환경을 안정적으로 설정할 수 있습니다.
+1 -1
View File
@@ -2,7 +2,7 @@
This document guides you through the setup and initialization procedures required to adopt the `herdr_agent_orchestration` orchestration and messaging backplane workflow in a new project, enabling a new developer or agent to get up and running quickly. This document guides you through the setup and initialization procedures required to adopt the `herdr_agent_orchestration` orchestration and messaging backplane workflow in a new project, enabling a new developer or agent to get up and running quickly.
[![Version: v4.9.0](https://img.shields.io/badge/version-v4.9.0-blue.svg)](VERSIONS.md) [![Version: v4.9.1](https://img.shields.io/badge/version-v4.9.1-blue.svg)](VERSIONS.md)
A new agent can follow the steps in this guide sequentially to establish a stable and reliable initial environment. A new agent can follow the steps in this guide sequentially to establish a stable and reliable initial environment.
+1 -1
View File
@@ -1,6 +1,6 @@
# Messaging System Technical Analysis & Architecture Report # Messaging System Technical Analysis & Architecture Report
Applies to: MAM v4.9.0 Applies to: MAM v4.9.1
This report provides a comprehensive, deep-dive analysis of the messaging system implemented in the `multi-agent-mux-delegate-job` skill. It covers the MQTT broker architecture, event protocols, job lifecycles, codebase internals, cross-system integration, and a list of known limitations along with production recommendations. This report provides a comprehensive, deep-dive analysis of the messaging system implemented in the `multi-agent-mux-delegate-job` skill. It covers the MQTT broker architecture, event protocols, job lifecycles, codebase internals, cross-system integration, and a list of known limitations along with production recommendations.
+1 -1
View File
@@ -2,7 +2,7 @@
Herdr와 MQTT 브로커를 기반으로 구축된 고신뢰성 **다중 에이전트 오케스트레이션 및 메시징 백플레인** 프레임워크입니다. `claude`, `agy`, `hermes`, `grok`, `opencode`, `codex` 등 다중 LLM 백엔드 에이전트 전반에 걸쳐 장시간 수행되는 작업(코드 생성, 리팩토링, 보안 검토 등)을 조정, 격리 및 감사할 수 있도록 설계되었습니다. Herdr와 MQTT 브로커를 기반으로 구축된 고신뢰성 **다중 에이전트 오케스트레이션 및 메시징 백플레인** 프레임워크입니다. `claude`, `agy`, `hermes`, `grok`, `opencode`, `codex` 등 다중 LLM 백엔드 에이전트 전반에 걸쳐 장시간 수행되는 작업(코드 생성, 리팩토링, 보안 검토 등)을 조정, 격리 및 감사할 수 있도록 설계되었습니다.
[![Version: v4.9.0](https://img.shields.io/badge/version-v4.9.0-blue.svg)](VERSIONS.md) [![Version: v4.9.1](https://img.shields.io/badge/version-v4.9.1-blue.svg)](VERSIONS.md)
--- ---
+1 -1
View File
@@ -2,7 +2,7 @@
An advanced, high-reliability **Multi-Agent Orchestration & Messaging Backplane** framework built on Herdr and MQTT. It is designed to coordinate, isolate, and audit long-running agent tasks (such as code generation, refactoring, and security reviews) across multiple LLM backend clients (`claude`, `agy`, `hermes`, `grok`, `opencode`, `codex`). An advanced, high-reliability **Multi-Agent Orchestration & Messaging Backplane** framework built on Herdr and MQTT. It is designed to coordinate, isolate, and audit long-running agent tasks (such as code generation, refactoring, and security reviews) across multiple LLM backend clients (`claude`, `agy`, `hermes`, `grok`, `opencode`, `codex`).
[![Version: v4.9.0](https://img.shields.io/badge/version-v4.9.0-blue.svg)](VERSIONS.md) [![Version: v4.9.1](https://img.shields.io/badge/version-v4.9.1-blue.svg)](VERSIONS.md)
--- ---
+24 -11
View File
@@ -6,8 +6,8 @@
## 📌 현재 버전 개요 (Current Release) ## 📌 현재 버전 개요 (Current Release)
- **프레임워크 버전**: `v4.9.0` - **프레임워크 버전**: `v4.9.1`
- **최신 릴리스 일시**: 2026-10-12 (KST) - **최신 릴리스 일시**: 2026-10-13 (KST)
- **기준 브랜치**: `main` - **기준 브랜치**: `main`
- **핵심 아키텍처**: - **핵심 아키텍처**:
- **Terminal 상태 단방향 불변성 (`engine.py`, Issue #6 Phase 1)**: `completed`, `error`, `cancelled` 터미널 상태 진입 후 후속 이벤트나 업데이트에 의한 덮어쓰기를 엄격히 차단(Terminal Immutability). `registry.py close` 서브커맨드 및 fail-closed 조기 종료 도입. - **Terminal 상태 단방향 불변성 (`engine.py`, Issue #6 Phase 1)**: `completed`, `error`, `cancelled` 터미널 상태 진입 후 후속 이벤트나 업데이트에 의한 덮어쓰기를 엄격히 차단(Terminal Immutability). `registry.py close` 서브커맨드 및 fail-closed 조기 종료 도입.
@@ -24,23 +24,36 @@
## 🧭 스킬 패키지 버전 매트릭스 (Skills Version Matrix) ## 🧭 스킬 패키지 버전 매트릭스 (Skills Version Matrix)
모든 8개 스킬은 YAML frontmatter 메타데이터(`author`, `version`, `platforms`, `environments`) 표준화를 통해 `v4.9.0`로 동기화되어 배포됩니다. 모든 8개 스킬은 YAML frontmatter 메타데이터(`author`, `version`, `platforms`, `environments`) 표준화를 통해 `v4.9.1`로 동기화되어 배포됩니다.
| 스킬명 | 버전 | 역할 및 주요 책임 | 상태 | | 스킬명 | 버전 | 역할 및 주요 책임 | 상태 |
| :--- | :---: | :--- | :---: | | :--- | :---: | :--- | :---: |
| **`multi-agent-mux-create`** | `4.9.0` | 에이전트 세션 신규 생성 및 Herdr 컨테이너 격리 스폰 | ✅ 배포 | | **`multi-agent-mux-create`** | `4.9.1` | 에이전트 세션 신규 생성 및 Herdr 컨테이너 격리 스폰 | ✅ 배포 |
| **`multi-agent-mux-stop`** | `4.9.0` | 대화 UUID 원자적 캡처 및 세션 안전 종료 (Graceful Stop) | ✅ 배포 | | **`multi-agent-mux-stop`** | `4.9.1` | 대화 UUID 원자적 캡처 및 세션 안전 종료 (Graceful Stop) | ✅ 배포 |
| **`multi-agent-mux-resume`** | `4.9.0` | 온디스크 대화 컨텍스트 기반 Tier-1 초고속 세션 복원 | ✅ 배포 | | **`multi-agent-mux-resume`** | `4.9.1` | 온디스크 대화 컨텍스트 기반 Tier-1 초고속 세션 복원 | ✅ 배포 |
| **`multi-agent-mux-status`** | `4.9.0` | 실시간 Herdr 세션 및 레지스트리 드리프트 스냅샷 조회 | ✅ 배포 | | **`multi-agent-mux-status`** | `4.9.1` | 실시간 Herdr 세션 및 레지스트리 드리프트 스냅샷 조회 | ✅ 배포 |
| **`multi-agent-mux-monitor`** | `4.9.0` | YAML ↔ 런타임 상태 간 자율 조정자 (Reconciler Loop) | ✅ 배포 | | **`multi-agent-mux-monitor`** | `4.9.1` | YAML ↔ 런타임 상태 간 자율 조정자 (Reconciler Loop) | ✅ 배포 |
| **`multi-agent-mux-delegate-job`** | `4.9.0` | MQTT 이벤트 채널 기반 비동기 단위 작업 위임 | ✅ 배포 | | **`multi-agent-mux-delegate-job`** | `4.9.1` | MQTT 이벤트 채널 기반 비동기 단위 작업 위임 | ✅ 배포 |
| **`multi-agent-mux-loop`** | `4.9.0` | Planner-Creator-Reviewer 3자 자율 계획·실행·피어리뷰 루프 | ✅ 배포 | | **`multi-agent-mux-loop`** | `4.9.1` | Planner-Creator-Reviewer 3자 자율 계획·실행·피어리뷰 루프 | ✅ 배포 |
| **`multi-agent-mux-orc-onboard`** | `4.9.0` | 오케스트레이터 UUID 격리 등록 및 서브 세션 오염 방지 | ✅ 배포 | | **`multi-agent-mux-orc-onboard`** | `4.9.1` | 오케스트레이터 UUID 격리 등록 및 서브 세션 오염 방지 | ✅ 배포 |
--- ---
## 📋 버전별 상세 변경 내역 (Changelog) ## 📋 버전별 상세 변경 내역 (Changelog)
### 🚀 `v4.9.1` — 테스트가 실제 herdr 서버에 세션을 만들지 않는다 (2026-10-13)
> **패치 (PATCH Release)**: v4.9.0 병합 검증 중 에이전트 pane 에서 `--basetemp .../pytest-pr56-shim` 으로 돌린 스위트가 그 pane 의 실제 서버(`multi-agent-mux`)에 워크스페이스 7개를 남겼다(정리 완료). 제품 코드 변경은 실제 herdr 해석기의 `-shim` 규칙 한 가지.
#### 🌟 주요 개선 사항
1. **원인 1 — 조상 `*-shim` 이 mock 을 가렸다**: 실제 herdr 해석기(`lib.sh` 의 `_is_shim_path`, heredoc 으로 생성되는 `.mam/shim/herdr`)가 경로 어디에든 `-shim` 이 든 PATH 디렉터리를 건너뛰었다. basetemp 이름이 mock 의 `bin/` 조상이라 mock 이 건너뛰어지고 실제 herdr 가 실행됐다. **수정**: 디렉터리 자신의 이름이 `*-shim` 일 때만 래퍼로 본다(`.mam/shim`, `multi-agent-herdr-shim`, `.agents/skills/.bin` 규칙은 그대로). 생성 shim 이 바뀌었으므로 `test_s11` 의 기준 커밋을 525df2b 로 옮겼다(#45 와 같은 방식).
2. **원인 2 — 샌드박스가 pane 의 소켓을 남겼다**: 실제 herdr 는 sandbox `HOME` 보다 `HERDR_SOCKET_PATH` 를 따른다(실측). `mam_sandbox` 가 `HERDR_SOCKET_PATH`·`HERDR_PANE_ID`·`HERDR_TAB_ID` 와 `HERDR_SESSION`(호출자 서버 판정이 읽어 pane 안/밖에서 분기가 달라짐, 리뷰 비차단 메모)을 지운다.
#### ✅ 품질 검증
- **회귀 테스트**: `tests/test_herdr_real_server_leak.py` — L1(조상 `*-shim`, 두 해석기; 실제 herdr 대신 디코이)·L2(pane 라우팅 변수 제거)는 수정 전 실패, L1b(자기 이름이 `*-shim` 인 래퍼는 계속 제외).
- **리뷰 (`--review-only`, `--all-reviewer`)**: 1회차(@325b056) **4/4 PASS**, 2회차(@db7f4ca, `HERDR_SESSION`) **4/4 PASS**. 비차단으로 남긴 기존 차이: 생성 shim 은 `.agents/skills/.bin` 을 제외하지 않음(codex), README 의 레거시 `/tmp/multi-agent-herdr-shim/<server>` 레이아웃 설명(claude). 리포트는 `.agents/reports/<session>/`.
- **전체 스위트 (macOS·Python 3.9·bash 3.2·herdr 0.9.3, 에이전트 pane 환경 그대로, MQTT 환경변수 없이)**: 325b056 **1845 passed / 0 failed / 56 skipped**(74 분; 새 워크트리라 `nats-docker` 서브모듈이 없어 `test_deploy_freshness` 18건이 추가로 건너뛰어졌고, 병합 뒤 main 에서 따로 실행). `~/.claude/projects` 에 새 항목 없음. 실행 전후 실제 서버(`default`, `multi-agent-mux`)의 pane 목록이 같았다.
### 🚀 `v4.9.0` — Issue #50·#55: 온보딩·위임이 레지스트리로 기다리고, pane 배치는 서버마다 하나씩, 테스트는 실제 에이전트 홈에 쓰지 않는다 (2026-10-12) ### 🚀 `v4.9.0` — Issue #50·#55: 온보딩·위임이 레지스트리로 기다리고, pane 배치는 서버마다 하나씩, 테스트는 실제 에이전트 홈에 쓰지 않는다 (2026-10-12)
> **마이너 (MINOR Release)**: 새 종료 코드(`create --onboard` 의 44·45)와 새 대기 동작(위임 래퍼의 레지스트리 대기, pane 배치 잠금). PR #56. > **마이너 (MINOR Release)**: 새 종료 코드(`create --onboard` 의 44·45)와 새 대기 동작(위임 래퍼의 레지스트리 대기, pane 배치 잠금). PR #56.
+9
View File
@@ -290,6 +290,15 @@ def mam_sandbox(tmp_path, monkeypatch, request):
monkeypatch.delenv("HERDR_SESSION_NAME", raising=False) monkeypatch.delenv("HERDR_SESSION_NAME", raising=False)
monkeypatch.delenv("HERDR_SERVER_NAME", raising=False) monkeypatch.delenv("HERDR_SERVER_NAME", raising=False)
monkeypatch.delenv("HERDR_WORKSPACE_ID", raising=False) monkeypatch.delenv("HERDR_WORKSPACE_ID", raising=False)
# A herdr pane exports its server's socket (and pane/tab ids). The real
# herdr follows HERDR_SOCKET_PATH over the sandbox HOME, so a call that
# missed the mock reached the caller's real server and opened panes there.
# HERDR_SESSION resolves under the sandbox HOME, but the caller-server
# helpers read it, so tests branched on whether they ran in a pane.
monkeypatch.delenv("HERDR_SOCKET_PATH", raising=False)
monkeypatch.delenv("HERDR_SESSION", raising=False)
monkeypatch.delenv("HERDR_PANE_ID", raising=False)
monkeypatch.delenv("HERDR_TAB_ID", raising=False)
# Issue #45: the shim bootstrap writes a herdr server config next to the # Issue #45: the shim bootstrap writes a herdr server config next to the
# user's; with these unset that is $HOME/.config/herdr, inside the sandbox. # user's; with these unset that is $HOME/.config/herdr, inside the sandbox.
# Every pane of a MAM server inherits HERDR_CONFIG_PATH. # Every pane of a MAM server inherits HERDR_CONFIG_PATH.
+88
View File
@@ -0,0 +1,88 @@
"""Tests must not reach a real herdr server (found 2026-10-12).
A suite run from an agent pane with --basetemp .../pytest-pr56-shim left seven
workspaces on that pane's real server. Two causes, one test each:
- L1: the real-herdr resolvers skipped every PATH directory with "-shim"
anywhere in its path, so the basetemp hid the mock's bin/ and the real
herdr ran;
- L2: the sandbox kept the pane's HERDR_SOCKET_PATH, which the real herdr
follows over the sandbox HOME.
"""
import os
import subprocess
import pytest
DECOY = "#!/bin/sh\necho \"$@\" >> \"$DECOY_LOG\"\necho '{}'\n"
def test_l1_ancestor_named_shim_does_not_hide_the_mock(mam_sandbox, mock_herdr):
tmp = mam_sandbox
# The mock's bin/ under an ancestor named *-shim, as under that basetemp.
mock_bin = tmp / "pytest-x-shim" / "case0" / "bin"
mock_bin.mkdir(parents=True)
(mock_bin / "herdr").symlink_to(tmp / "bin" / "herdr")
# A decoy next in PATH stands in for the real herdr: with the old match it
# is what ran. The real herdr is never reached either way.
decoy_bin = tmp / "decoy" / "bin"
decoy_bin.mkdir(parents=True)
(decoy_bin / "herdr").write_text(DECOY)
(decoy_bin / "herdr").chmod(0o755)
decoy_log = tmp / "decoy.log"
env = dict(os.environ, PATH=f"{mock_bin}:{decoy_bin}:{os.environ['PATH']}",
DECOY_LOG=str(decoy_log))
lib = tmp / ".agents" / "skills" / "lib.sh"
script = f"""
source "{lib}"
_init_herdr_isolation
_resolve_real_herdr_path
"$WORKSPACE_ROOT/.mam/shim/herdr" workspace list >/dev/null
"""
res = subprocess.run(["bash", "-c", script], capture_output=True, text=True,
cwd=str(tmp), env=env)
assert res.returncode == 0, res.stderr
# lib.sh's resolver picks the mock's directory, not the decoy.
assert res.stdout.strip().splitlines()[0] == f"{mock_bin}/herdr", res.stdout
# The generated shim ran the mock too.
assert not decoy_log.exists(), decoy_log.read_text()
def test_l1_a_directory_named_shim_is_still_skipped(mam_sandbox, mock_herdr):
"""A wrapper directory that is itself *-shim stays excluded."""
tmp = mam_sandbox
wrapper = tmp / "herdr-shim"
wrapper.mkdir()
(wrapper / "herdr").write_text(DECOY)
(wrapper / "herdr").chmod(0o755)
decoy_log = tmp / "decoy.log"
env = dict(os.environ, PATH=f"{wrapper}:{os.environ['PATH']}", DECOY_LOG=str(decoy_log))
lib = tmp / ".agents" / "skills" / "lib.sh"
script = f"""
source "{lib}"
_init_herdr_isolation
_resolve_real_herdr_path
"$WORKSPACE_ROOT/.mam/shim/herdr" workspace list >/dev/null
"""
res = subprocess.run(["bash", "-c", script], capture_output=True, text=True,
cwd=str(tmp), env=env)
assert res.returncode == 0, res.stderr
assert res.stdout.strip().splitlines()[0] == f"{tmp}/bin/herdr", res.stdout
assert not decoy_log.exists(), decoy_log.read_text()
PANE_VARS = {"HERDR_SOCKET_PATH": "/nonexistent/sessions/real/herdr.sock",
"HERDR_PANE_ID": "w9:p1", "HERDR_TAB_ID": "w9:t1",
"HERDR_WORKSPACE_ID": "w9", "HERDR_SESSION_NAME": "real",
"HERDR_SESSION": "real"}
@pytest.fixture
def _agent_pane_env(monkeypatch):
"""What an agent pane on a named server carries; set before mam_sandbox."""
for k, v in PANE_VARS.items():
monkeypatch.setenv(k, v)
def test_l2_sandbox_drops_the_pane_routing(_agent_pane_env, mam_sandbox):
left = sorted(k for k in PANE_VARS if k in os.environ)
assert left == [], left
+4 -2
View File
@@ -298,9 +298,11 @@ def test_s11_this_branch_does_not_change_the_generated_shim(mam_sandbox):
byte-identical to base is what makes "the control plane is not modified" byte-identical to base is what makes "the control plane is not modified"
true at runtime. Issue #45 changed the heredoc on purpose (the bootstrap true at runtime. Issue #45 changed the heredoc on purpose (the bootstrap
starts the server with restore-on-restart off, and its review R1 passes starts the server with restore-on-restart off, and its review R1 passes
REAL_HERDR to the config check), so the base is that commit.""" REAL_HERDR to the config check), and so did the real-server leak fix
(only a directory that is itself *-shim is skipped), so the base is
that commit."""
repo = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) repo = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
base = os.environ.get("MAM_BASE_SHA", "811b080") base = os.environ.get("MAM_BASE_SHA", "525df2b")
show = subprocess.run(["git", "show", "%s:.agents/skills/lib.sh" % base], show = subprocess.run(["git", "show", "%s:.agents/skills/lib.sh" % base],
capture_output=True, text=True, cwd=repo) capture_output=True, text=True, cwd=repo)
if show.returncode != 0: if show.returncode != 0: