release(v4.9.1): tests no longer reach a real herdr server - archive the review reports and bump the lockstep version

Review loop (--review-only, --all-reviewer): round 1 (@325b056) 4/4 PASS,
round 2 (@db7f4ca, HERDR_SESSION) 4/4 PASS. Full suite at 325b056 on
macOS / Python 3.9 / bash 3.2 / herdr 0.9.3 from an agent pane:
1845 passed / 0 failed / 56 skipped (18 test_deploy_freshness skips: no
nats-docker submodule in the fresh worktree); the panes of the real
servers (default, multi-agent-mux) and ~/.claude/projects were unchanged.

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
1 parent db7f4cab9c
commit 50c8da8269
32 files changed
+607 -26

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]
+1 -1
View File
@@ -29,7 +29,7 @@ export WORKSPACE_ROOT
# 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.
# 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
AGENT_SESSIONS_YAML="${AGENT_SESSIONS_YAML:-$WORKSPACE_ROOT/.mam/agent-sessions.yaml}"
+1 -1
View File
@@ -13,7 +13,7 @@ import sys
from typing import List, Sequence
# Framework version locked with framework constants
FRAMEWORK_VERSION = "4.9.0"
FRAMEWORK_VERSION = "4.9.1"
CORE_MODULES: Sequence[str] = (
# Standard library modules
@@ -1,7 +1,7 @@
---
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."
version: 4.9.0
version: 4.9.1
author: godopu
license: MIT
platforms: [linux, macos]
@@ -1,7 +1,7 @@
---
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."
version: 4.9.0
version: 4.9.1
author: godopu
license: MIT
platforms: [linux, macos, windows]
+1 -1
View File
@@ -1,7 +1,7 @@
---
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."
version: 4.9.0
version: 4.9.1
author: godopu
license: MIT
platforms: [linux, macos]
@@ -1,7 +1,7 @@
---
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."
version: 4.9.0
version: 4.9.1
author: godopu
license: MIT
platforms: [linux, macos]
@@ -1,7 +1,7 @@
---
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."
version: 4.9.0
version: 4.9.1
author: godopu
license: MIT
platforms: [linux, macos]
@@ -1,7 +1,7 @@
---
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."
version: 4.9.0
version: 4.9.1
author: godopu
license: MIT
platforms: [linux, macos]
@@ -1,7 +1,7 @@
---
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."
version: 4.9.0
version: 4.9.1
author: godopu
license: MIT
platforms: [linux, macos]
+1 -1
View File
@@ -1,7 +1,7 @@
---
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."
version: 4.9.0
version: 4.9.1
author: godopu
license: MIT
platforms: [linux, macos]