27 KiB
B-3 구현 계획서 Rev.2 — herdr 프리플라이트가 실제 바이너리를 검사하도록 수정
- Job:
9840cee7/ Role: Planner (Rev.1 = Job1876c7c1) - 대상: IMPROVEMENTS.md B-3 — "
command -v herdr프리플라이트 무력화" - 기준 커밋:
924e77e(clean tree) - 반영: Creator
agyChallenge Report (Joba56c0c5f) — 심링크 맹점 1건 - 검증 상태: Rev.1 + Rev.2 수정 전량을
git archive HEAD클론 3벌(HEAD / Rev.1 / Rev.2)에 실제 구현하여 재측정 완료.
0. Rev.1 → Rev.2 변경 요지
| 이의 | 판정 | 근거 |
|---|---|---|
제시된 기제 — macOS /var↔/private/var, /tmp↔/private/tmp, ~/projects→/Volumes/... 심링크로 인해 .mam/shim 디렉터리 패턴 매칭이 우회됨 |
기각 (REFUTED) | 인용된 심링크 형태 전부 이미 SKIP 된다. 구조적 이유: 심링크 해석은 경로의 접두부를 바꾸는데 패턴은 접미부 /.mam/shim/ 를 본다. 같은 디렉터리의 어떤 표기든 접미부는 동일하므로 정규화가 결과를 바꿀 수 없다. |
제시된 처방 — PATH 항목마다 cd -P/pwd -P 로 canonical 비교 |
기각 (REJECTED) | 실제로 존재하는 유일한 상대경로 구멍(.mam/shim)조차 cwd 가 WORKSPACE_ROOT 와 같을 때만 닫는다. 다르면 상대 cd -P 가 실패해 구멍이 그대로 열린다(실측). 게다가 PATH 항목마다 서브셸을 띄운다. |
| 결론 — "심링크로 shim 가드를 우회할 수 있다" | ✅ 인용 (CORRECT) | 맞다. 다만 기제가 디렉터리가 아니라 파일이다. 평범한 bin 디렉터리 안의 심링크(/usr/local/bin/herdr → <ws>/.mam/shim/herdr)는 어떤 디렉터리명 패턴으로도 걸러낼 수 없고, Rev.1 이 이를 실제 바이너리로 오판한다(실측 확인). |
| (부수) 자체 발견 | 수정 | 상대 PATH 항목 .mam/shim(선행 / 나 ./ 없음)이 Rev.1 패턴을 통과한다. |
총평: agy 의 결론은 옳았고 분석은 틀렸다. 지적한 세 가지 심링크 시나리오는 모두 이미 처리되고 있었으며 제안 코드는 그들이 상정한 문제도, 실재하는 문제도 해결하지 못한다. 그러나 "심링크로 우회 가능"이라는 판단 자체는 참이었고, 그 참인 판단을 검증하는 과정에서 Rev.1 의 실제 결함 2건을 찾아냈다. 이 지적이 없었다면 두 구멍은 남았을 것이다.
1. 요약
B-3 은 실재하며 추적 항목이 서술한 것보다 나쁘다.
- 원인이 1개가 아니라 독립적인 우회 경로 2개다.
- 결과가 "무조건 통과"가 아니라 행(hang) 이다 — 실측에서 120초 후 SIGKILL 로만 종료됐다.
command -v 대신 type -P 를 쓰는 자연스러운 수정은 작동하지 않는다. type -P herdr 는 .mam/shim/herdr 래퍼를 찾아내기 때문이다. HEAD 의 프리플라이트 줄에는 이미 type -P 가 있으나 || 체인 앞단에서 단락되어 아무 일도 하지 않는다.
해결: lib.sh 의 스텁 _resolve_real_herdr_path() 를 구현하고 has_real_herdr() 술어를 노출한다. 🆕 Rev.2 에서 문자열 패턴만으로는 불충분함이 드러나, 후보 파일에 한해 심링크를 해석하는 단계를 추가했다(§2-a).
2. 현황 측정
2-a. 🆕 심링크 우회는 파일 수준에서만 발생한다 (Rev.2 핵심)
디렉터리 표기 12종을 Rev.1 패턴에 통과시킨 실측:
/Users/x/proj/.mam/shim SKIP
/var/folders/q_/abc/T/proj/.mam/shim SKIP ← agy 시나리오 1
/private/var/folders/q_/abc/T/proj/.mam/shim SKIP ← agy 시나리오 1 (정규화 표기)
/tmp/proj/.mam/shim SKIP ← agy 시나리오 1
/private/tmp/proj/.mam/shim SKIP ← agy 시나리오 1 (정규화 표기)
/Volumes/Data/mam/.mam/shim SKIP ← agy 시나리오 2
./.mam/shim SKIP ← agy 시나리오 3 (상대경로)
.mam/shim KEEP ←── 실재하는 구멍 (agy 미지적)
/usr/local/bin KEEP (정상)
이의제기가 든 심링크 사례는 전부 이미 막혀 있다. 이유는 우연이 아니다 — 심링크 해석은 경로 접두부를 바꾸고, 패턴은 접미부 /.mam/shim/ 를 검사한다. 같은 디렉터리를 가리키는 어떤 표기든 .../.mam/shim 으로 끝나므로 양쪽 표기가 동일하게 매칭된다. canonical 화는 이 판정을 바꿀 수 없다.
그러나 파일 수준 심링크는 다르다. 평범한 bin 디렉터리 안의 링크는 디렉터리명이 아무 패턴에도 걸리지 않는다:
$ ln -s <ws>/.mam/shim/herdr <ws>/symbin/herdr
$ PATH=<ws>/symbin:... ; has_real_herdr
Rev.1 -> ACCEPTED <-- shim 래퍼를 진짜 herdr 로 오판
resolved: <ws>/symbin/herdr
(readlink -> <ws>/.mam/shim/herdr, inside .mam/shim: True)
이것이 B-3 결함의 재발 경로다. 오판된 "실제 herdr" 는 shim 이므로 §2-d 의 행(hang)이 그대로 재현된다.
2-b. 우회 경로 2개 (Rev.1 에서 확립, 변경 없음)
herdr 가 전혀 없는 PATH 에서 lib.sh 를 source 한 뒤:
command -v herdr -> [herdr] rc=0 ← 우회 ①: lib.sh:498 herdr() 함수
type -t herdr -> [function]
type -P herdr -> [<WS>/.mam/shim/herdr] rc=0 ← 우회 ②: shim 래퍼
- 우회 ①:
lib.sh:498이herdr()셸 함수를 정의.command -v는 함수를 매칭. - 우회 ②:
lib.sh:1742가 source 시점에_init_herdr_isolation을 호출 →.mam/shim을 PATH 맨 앞에 prepend.
2-c. HEAD 의 프리플라이트는 "고쳐진 척" 되어 있다
create_session.sh:84
command -v herdr >/dev/null || type -P herdr >/dev/null || { echo "ERROR: herdr not installed" >&2; exit 1; }
git blame: ea36e816. type -P 가 || 체인 뒤쪽이라 앞의 command -v 가 rc=0 을 내면 평가조차 되지 않는다. 평가되더라도 우회 ②로 통과한다. 실측: PREFLIGHT: PASS <-- binary absent, yet passes.
2-d. 실제 결과는 "통과"가 아니라 "행(hang)"
claude 는 있고 herdr 만 없는 상태의 HEAD 실측:
subprocess.TimeoutExpired: ... timed out after 120 seconds
<Popen: returncode: -9 ...> ← SIGKILL 로만 종료
프리플라이트 통과 후 shim 을 통해 세션을 만들려다 블록된다. shim 내부 _resolve_real_herdr 는 실제 herdr 를 못 찾으면 리터럴 "herdr" 로 폴백하고 그 지점에서 멈춘다. 사용자 영향은 "잘못된 성공"이 아니라 "원인 불명의 멈춤"이다.
2-e. 올바른 스캔 로직은 이미 저장소 안에 있다
lib.sh:53-56 의 _resolve_real_herdr_path() 는 스캔을 전혀 하지 않는 스텁이다(C-4 가 미사용 심볼로 추적 중). 반면 shim 스크립트 내부에는 .mam/shim / *-shim* 을 건너뛰는 스캔이 이미 구현되어 있다 → 승격하면 된다. (단 Rev.2 는 여기에 파일 수준 해석을 추가한다.)
2-f. 기존 테스트가 못 잡은 이유
conftest.py 의 mock_herdr 픽스처는 항상 실행 가능한 mock 바이너리를 PATH 에 올린다. 즉 "herdr 가 없는" 경로가 한 번도 실행되지 않았다.
3. 변경 명세
F1 — .agents/skills/lib.sh (핵심) 🆕 Rev.2 개정
스텁을 3개 함수로 교체한다.
# Canonicalise a file path: follow symlinks (bounded), then resolve the parent
# directory with `cd -P`. Hand-rolled rather than shelling out to `readlink -f`
# or `realpath`: both exist on this host and on recent macOS, but neither is
# guaranteed on older macOS or on minimal container images, and this runs inside
# a pre-flight that must not itself depend on an optional tool.
_canonical_file() {
local p="$1" t d b i=0
while [ -L "$p" ] && [ "$i" -lt 40 ]; do
t="$(readlink "$p" 2>/dev/null)" || break
case "$t" in
/*) p="$t" ;;
*) p="$(dirname "$p")/$t" ;;
esac
i=$((i + 1))
done
d="$(cd -P "$(dirname "$p")" 2>/dev/null && pwd -P)" || return 1
b="$(basename "$p")"
printf '%s/%s\n' "$d" "$b"
}
# True if a path lies inside any wrapper/shim location.
# The argument is wrapped as "/$1/" so the leading-slash patterns match even a
# bare relative entry such as `.mam/shim` (an absolute path just gains a
# harmless leading `//`).
_is_shim_path() {
case "/$1/" in
*"/.mam/shim/"*|*-shim/*|*"$_HERDR_SHIM_DIR_PATTERN"*|*"$_HERDR_SKILLS_BIN_PATTERN"/*)
return 0 ;;
esac
return 1
}
# Resolve the REAL herdr binary, skipping every wrapper/shim location.
# Prints the absolute path on stdout; returns 1 when only wrappers exist.
#
# B-3: neither of the obvious one-liners can answer "is herdr installed?":
# * `command -v herdr` matches the herdr() shell FUNCTION defined below.
# * `type -P herdr` matches $WORKSPACE_ROOT/.mam/shim/herdr, because
# _init_herdr_isolation prepends that dir to PATH when this file is sourced.
_resolve_real_herdr_path() {
local dir cand save_ifs="$IFS" real_path=""
IFS=:
for dir in $PATH; do
[ -n "$dir" ] || continue
# Cheap string reject first: skips wrapper dirs without touching the disk.
_is_shim_path "$dir" && continue
[ -x "$dir/herdr" ] || continue
# Only now — for an actual candidate, so at most once per call — resolve
# symlinks. A link like /usr/local/bin/herdr -> <ws>/.mam/shim/herdr sits in
# a directory no string pattern can reject; only the resolved target tells
# the truth. Falls back to the literal path if canonicalisation fails.
cand="$(_canonical_file "$dir/herdr" 2>/dev/null)" || cand="$dir/herdr"
[ -n "$cand" ] || cand="$dir/herdr"
_is_shim_path "$cand" && continue
real_path="$dir/herdr"
break
done
IFS="$save_ifs"
[ -n "$real_path" ] || return 1
_REAL_HERDR_PATH="$real_path"
export _REAL_HERDR_PATH
printf '%s\n' "$real_path"
}
# Preflight predicate: true iff a real herdr BINARY is installed (not the
# herdr() function, not the .mam/shim wrapper). Use this in every preflight
# instead of `command -v herdr` / `type -P herdr`.
has_real_herdr() {
_resolve_real_herdr_path >/dev/null 2>&1
}
설계 근거
| 결정 | 이유 |
|---|---|
| 🆕 문자열 먼저, canonical 은 후보에만 | 정확성상 필요한 지점은 "받아들이려는 후보"뿐이다. PATH 전체를 canonical 화하는 것은 불필요하고 비싸다(§6-C 실측 11.07 → 2.70 ms, 서브셸 20회 → 1회). |
🆕 "/$dir/" 정규화 |
선행 슬래시를 강제해 상대 항목 .mam/shim 까지 매칭. 절대경로는 무해한 // 접두부만 얻는다. 비용 0. |
🆕 _canonical_file 을 직접 구현 |
readlink -f/realpath 는 이 호스트와 최신 macOS 에는 있으나(실측 /usr/bin/readlink, /bin/realpath) 구형 macOS·최소 컨테이너에서는 보장되지 않는다. 프리플라이트가 선택적 도구에 의존해서는 안 된다. |
| 배열 미사용 | 실행 bash 가 3.2.57 이고 set -u 이므로 빈 배열 전개가 치명적이다(O-1 라운드 실측). IFS 순회 + case 만 사용. |
| 해석 깊이 40 제한 | 심링크 순환에서 무한 루프 방지. |
| canonical 실패 시 리터럴 폴백 | 권한 없는 디렉터리 등에서 cd -P 가 실패해도 스캔이 중단되지 않도록. |
C-4 교차: _REAL_HERDR_PATH, _HERDR_SHIM_DIR_PATTERN, _HERDR_SKILLS_BIN_PATTERN 3종이 미사용 → 사용 상태로 전환된다. C-4 목록에서 제외 필요(본 계획은 IMPROVEMENTS 를 수정하지 않음 — GM 재량).
F2 — create_session.sh:84
# before
command -v herdr >/dev/null || type -P herdr >/dev/null || { echo "ERROR: herdr not installed" >&2; exit 1; }
# after
# B-3: `command -v herdr` matches lib.sh's herdr() function and `type -P herdr`
# matches the .mam/shim wrapper, so both pass with no herdr installed.
has_real_herdr || { echo "ERROR: herdr not installed" >&2; exit 1; }
lib.sh 는 22행에서 이미 source 되므로 84행 시점에 정의되어 있다.
F3 — multi-agent-mux-delegate-job:448
# before
if ! command -v herdr >/dev/null 2>&1; then
# after
# B-3: must probe for the real binary, not the herdr() function / shim wrapper.
if ! has_real_herdr; then
F4 — 문서 2건
create/SKILL.md:37, status/SKILL.md:31 이 프리플라이트 예제로 command -v herdr 를 제시한다. 문서가 결함을 재생산하는 경로이므로 함께 고친다(부록 B).
F5 — tests/test_b3_herdr_preflight.py (신규 218줄, Rev.1 163줄 → +55)
mock_herdr 를 의도적으로 쓰지 않고 mam_sandbox 만 사용하며, PATH 를 BARE_PATH 로 고정해 개발자 머신의 herdr 가 회귀를 가리지 못하게 한다.
| ID | 검증 대상 |
|---|---|
| W-1 | 우회 2개 특성화 — 미래 리팩터가 one-liner 로 되돌리지 못하게 고정 |
| W-2 | 바이너리 없음 → FALSE |
| W-3 | 바이너리 있음 → TRUE, shim 이 아닌 실제 경로 반환 |
| W-4 | my-shim / multi-agent-herdr-shim 디렉터리는 불인정 (×2) |
| W-5 | create_session.sh rc≠0 이고 메시지가 herdr 게이트의 것 |
| W-6 | 실제 바이너리를 거부하지 않음 |
| W-7 | claude 만 있고 herdr 없음 → herdr 게이트에서 정지 |
| 🆕 W-8 | 평범한 bin 디렉터리 안의 shim 심링크 → 거부 |
| 🆕 W-9 | 실제 바이너리를 가리키는 심링크 → 수용 (전면 심링크 거부가 아님을 고정) |
| 🆕 W-10 | 상대 PATH 항목 .mam/shim → 거부 |
W-9 가 중요하다. W-8 만 있으면 "심링크는 전부 거부" 라는 잘못된 구현으로도 통과한다. W-9 가 그 오답을 배제한다.
F6 — IMPROVEMENTS.md
게이트 통과 후 B-3 을 §5 완료로 이관, §2 헤더 8건→7건, §5 4건→5건, 총계 조정. B-3 본문에 §2-a(파일 수준 심링크)·§2-d(행 발생)를 반영 권고.
4. 적용 순서
F1 → F2 → F3 → F5 → red/green 측정 → F4 → 회귀 비교 → F6.
5. 이의제기 심층 판정
5-A. 제시된 기제(디렉터리 심링크) — 기각
주장: /var↔/private/var 등 심링크 표기 차이로 .mam/shim 디렉터리 패턴이 우회된다.
반증: §2-a 표 참조. 인용된 6개 심링크 형태가 전부 SKIP 된다. 구조적 이유가 있다 — 심링크 해석은 접두부를 바꾸고 패턴은 접미부 /.mam/shim/ 를 본다. 어떤 표기로 쓰든 그 디렉터리는 .../.mam/shim 으로 끝난다. 정규화가 판정을 바꿀 수 없다.
5-B. 제시된 처방(PATH 항목별 canonical 비교) — 기각
제안 코드는 실재하는 유일한 상대경로 구멍(.mam/shim)조차 조건부로만 닫는다. 실측:
cwd == WORKSPACE_ROOT : canonical_dir=[<ws>/.mam/shim] → SKIP (구멍 닫힘)
cwd != WORKSPACE_ROOT : canonical_dir=[<empty>] → KEEP (구멍 열린 채)
상대 cd -P ".mam/shim" 는 cwd 기준이므로 cwd 가 워크스페이스가 아니면 실패하고, 그 경우 비교가 성립하지 않아 통과한다. cwd 가 WORKSPACE_ROOT 와 다른 것은 특수 상황이 아니라 일상적이다.
더불어 PATH 항목마다 서브셸을 띄운다(§6-C: 20항목 PATH 에서 11.07 ms/call, 후보 한정 방식의 4.1배). 성능은 부차적 근거이며 기각의 주된 이유는 정확성이다.
5-C. 결론(심링크로 우회 가능) — 인용
옳다. 다만 기제가 디렉터리가 아니라 파일이다. 이의제기 문서의 마지막 권고에 "실체 파일 경로/inode 를 배제" 라는 표현이 스쳐 지나가는데, 제안 코드는 그것을 구현하지 않았다(디렉터리만 canonical 화). 실측으로 이 경로가 실재함을 확인하고(§2-a) Rev.2 에서 후보 파일 canonical 화로 닫았다.
5-D. 자체 발견 — 상대 PATH 항목
Rev.1 검증 중 .mam/shim(선행 /·./ 없음)이 패턴을 통과함을 발견했다. _init_herdr_isolation 은 절대경로를 넣으므로(WORKSPACE_ROOT 기본값이 pwd 출력) 이 경로로는 도달 불가하며, 사람이나 외부 도구가 PATH 에 직접 넣어야 재현된다. 도달성은 낮지만 비용 0의 한 토큰 수정("$dir/" → "/$dir/")으로 닫히므로 닫았다. 도달성을 부풀리지 않고 기록한다.
6. 검증 게이트
G-A — 신규 테스트 red→green
HEAD (unpatched) : 2 failed, 5 passed in 0.37s [W-1..W-6]
+ W-7 별도: FAILED (120s timeout, rc=-9)
Rev.1 : W-8 FAILED, W-10 FAILED, W-9 passed
Rev.2 (전량) : 11 passed in 0.66s
| 테스트 | HEAD | Rev.1 | Rev.2 | red 의 성격 |
|---|---|---|---|---|
| W-1 | PASS | PASS | PASS | 특성화 |
| W-2 | PASS† | PASS | PASS | 공허한 통과 |
| W-3 | FAIL | PASS | PASS | 오염된 red |
| W-4 | PASS† | PASS | PASS | 공허한 통과 |
| W-5 | FAIL | PASS | PASS | 진짜 red |
| W-6 | PASS | PASS | PASS | 회귀 핀 |
| W-7 | FAIL(hang) | PASS | PASS | 진짜 red — 가장 강함 |
| 🆕 W-8 | — | FAIL | PASS | 진짜 red (Rev.1 대비) |
| 🆕 W-9 | — | PASS | PASS | 오답 배제 핀 |
| 🆕 W-10 | — | FAIL | PASS | 진짜 red (Rev.1 대비) |
red 의 정직성 — 반드시 함께 읽을 것 †W-2 / W-4 는 HEAD 에서
has_real_herdr가 존재하지 않아 command-not-found → 비영 → FALSE 분기를 타서 공허하게 통과한다. red 가 아니라 회귀 핀이다. W-3 은 존재하지 않는 심볼을 호명해 실패하므로 오염된 red다. W-5 는 진짜 red 다. HEAD 에서 rc≠0 이지만 herdr 게이트가 아니라 다음 claude 게이트에서 죽는다. 단언을rc != 0하나로 뒀다면 HEAD 에서 통과해 결함을 놓쳤을 것이다. W-7 이 가장 강하다 — 새 심볼을 일절 호명하지 않고 관측 동작만 검사한다. HEAD 결과는 assertion failure 가 아니라 120초 타임아웃 + SIGKILL 이었다. W-8 / W-10 은 Rev.1 대비 진짜 red 다. 이 2건이 이번 이의제기의 실질 산출이다. → 누적 진짜 red 4건: W-5·W-7(HEAD 대비), W-8·W-10(Rev.1 대비).
G-B — 회귀 위험의 직접 측정
이 변경의 유일한 실질 회귀 위험은 정당한 herdr 를 거부하는 것이다.
mock_herdr 픽스처와 동일 레이아웃(<tmp>/bin/herdr) : ACCEPTED
실제 바이너리를 가리키는 심링크 : ACCEPTED (W-9)
bash 3.2 + set -euo pipefail 로 has_real_herdr : OK (rc=0)
G-C — 🆕 비용 실측 (제안 방식 대비)
20항목 PATH, herdr 는 마지막 디렉터리(양쪽 최악 조건):
PATH 항목마다 canonical (agy 제안) : 11.07 ms/call 서브셸 20회
후보에만 canonical (Rev.2) : 2.70 ms/call 서브셸 1회
4.1배 차이. 다만 기각 사유의 주된 근거는 §5-B 의 정확성 문제이며 성능은 부차적이다.
G-D — 회귀 비교 (동일 스위트, 두 클론)
create_session.sh 를 구동하는 tests/test_sanity.py 를 patched / unpatched 동일 조건 실행:
PATCHED : 2 failed in 194.52s HEAD : 2 failed in 194.71s
FAILED test_create_session_dry_run - assert '[dry-run] ... (양쪽 동일)
FAILED test_create_session_full - KeyError: 'isolation' (양쪽 동일)
실패 집합·오류 메시지·소요 시간이 동일 → 선행 결함이며 본 변경이 유발한 회귀는 0건이다. (KeyError: 'isolation' 은 C-3 영역으로 보이나 본 계획 범위 밖이며 수정하지 않는다.)
전체 스위트(
pytest tests/)는 실행하지 않았다. 본 과제의 앞선 두 위임(c3763464,a5639650)이 정확히 이 시도에서 예산을 소진했다 — 호스트에서 실 오케스트레이션 루프가 동시 구동 중이라 pytest 가 반복적으로 SIGKILL(exit 137/144)됐다. 대신 변경이 실제로 닿는 경로를 두 클론에서 동일 비교하는 방식으로 판정했고, 이 비교는 절대 통과율과 무관하게 "내 변경이 유발했는가"에 결정적으로 답한다. Creator 는 한가한 시점에pytest tests/1회 완주를 권고한다.
G-E — 정적 검사
bash -n lib.sh / create_session.sh / multi-agent-mux-delegate-job → syntax OK (3 files)
잔여 오지침(`command -v herdr` / `type -P herdr`) → 설명 주석 2건만 잔존 (지침 아님)
측정된 변경 규모 (Rev.2)
-2 +68 .agents/skills/lib.sh (Rev.1 +34 → +68)
-1 +3 .agents/skills/multi-agent-mux-create/scripts/create_session.sh
-1 +2 .agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job
-1 +4 .agents/skills/multi-agent-mux-create/SKILL.md
-1 +5 .agents/skills/multi-agent-mux-status/SKILL.md
new 218 tests/test_b3_herdr_preflight.py (Rev.1 163줄 → +55)
7. 리스크
| ID | 리스크 | 평가 / 완화 |
|---|---|---|
| RK-A | 정당한 herdr 설치를 거부 | G-B 로 직접 측정 — 픽스처 레이아웃·실 바이너리 심링크 모두 수용. W-3/W-6/W-9 가 핀. |
| RK-B | 정상 디렉터리명에 -shim 이 우연히 포함 |
해당 herdr 는 무시된다. 의도된 동작(shim 을 실제로 오인하는 편이 더 위험)이며 W-4 가 명시. _HERDR_SHIM_DIR_PATTERN 으로 조정 가능. |
| RK-C | bash 3.2 + set -u 이식성 |
배열 미사용 구현으로 회피. G-B 에서 /bin/bash 3.2.57 + set -euo pipefail 직접 실행 확인. |
| RK-D | PATH 빈 항목(::)이 . 로 해석 |
[ -n "$dir" ] || continue 로 방어. |
| RK-E 🆕 | _canonical_file 이 cd -P 실패 시 오작동 |
리터럴 경로로 폴백해 스캔을 계속한다. 최악의 경우 Rev.1 수준(문자열 패턴만)으로 안전 퇴화하며, 더 나빠지지 않는다. |
| RK-F 🆕 | 심링크 순환 | 해석 깊이 40 상한. 초과 시 마지막 경로로 판정 — 폴백과 동일하게 안전 퇴화. |
| RK-G 🆕 | 후보 canonical 화 비용 | 호출당 최대 1회 서브셸(G-C 실측 2.70 ms). 프리플라이트는 세션 생성/위임 시 1회만 호출되므로 무시 가능. |
| RK-H | C-4 목록이 낡음 | 심볼 3종이 사용 상태로 전환. C-4 에서 제외 필요 — GM 재량. |
| RK-I | 문서와 코드가 따로 감 | F2/F3(코드)와 F4(문서)를 한 커밋으로 묶을 것을 권고. 문서가 command -v 를 계속 가르치면 결함이 재도입된다. |
| RK-J | test_sanity.py 선행 실패 2건이 본 변경 탓으로 오인 |
G-D 가 양쪽 클론 동일 실패를 명시. 리뷰 시 이 표를 근거로 제시할 것. |
8. 역할 경계
본 문서는 설계 산출물이며 저장소 코드·문서는 한 줄도 수정하지 않았다. 모든 구현·측정은 세션 스크래치패드 내 git archive HEAD 클론 3벌(HEAD / Rev.1 / Rev.2)에서만 수행했고, 작업 트리는 924e77e 기준 clean 을 유지한다.
MULTI_AGENT_RULES.md §1 에 따라 구현은 Creator, 커밋은 GM 소관이다. 차단 항목 없음.
부록 A — 신규 테스트 3종 전문 (W-8 / W-9 / W-10)
Rev.1 의 W-1…W-7 은 그대로 유지된다. 아래는 Rev.2 추가분이다.
# W-8 — symlink bypass: a link in an ORDINARY bin dir pointing at the shim.
# No directory-name pattern can reject this; only the resolved target
# reveals it.
def test_b3_symlink_into_shim_is_rejected(mam_sandbox):
_bash(mam_sandbox, "true") # materialise the shim
shim = mam_sandbox / ".mam" / "shim" / "herdr"
assert shim.exists(), "shim wrapper was not created by _init_herdr_isolation"
linkdir = mam_sandbox / "usrlocalbin"
linkdir.mkdir(parents=True, exist_ok=True)
(linkdir / "herdr").symlink_to(shim)
res = _bash(mam_sandbox,
'if has_real_herdr; then echo RESULT:TRUE; else echo RESULT:FALSE; fi',
path=f"{linkdir}:{BARE_PATH}")
assert "RESULT:FALSE" in res.stdout, (
"a symlink pointing into .mam/shim was accepted as a real herdr:\n"
f"{res.stdout}\n{res.stderr}")
# W-9 — ... but a symlink to a REAL binary must still be accepted, so the
# canonicalisation cannot be a blanket "reject all symlinks".
def test_b3_symlink_to_real_binary_is_accepted(mam_sandbox):
real = _make_herdr(mam_sandbox / "realbin")
linkdir = mam_sandbox / "linkbin"
linkdir.mkdir(parents=True, exist_ok=True)
(linkdir / "herdr").symlink_to(real)
res = _bash(mam_sandbox,
'if has_real_herdr; then echo RESULT:TRUE; else echo RESULT:FALSE; fi',
path=f"{linkdir}:{BARE_PATH}")
assert "RESULT:TRUE" in res.stdout, \
f"a symlink to a genuine herdr was rejected:\n{res.stdout}\n{res.stderr}"
# W-10 — a bare relative PATH entry '.mam/shim' (no leading '/' or './') must
# still be rejected. Requires normalising the dir with a leading slash.
def test_b3_bare_relative_shim_entry_is_rejected(mam_sandbox):
_bash(mam_sandbox, "true") # materialise the shim
res = _bash(mam_sandbox,
'if has_real_herdr; then echo RESULT:TRUE; else echo RESULT:FALSE; fi',
path=f".mam/shim:{BARE_PATH}")
assert "RESULT:FALSE" in res.stdout, (
"a bare relative '.mam/shim' PATH entry was accepted:\n"
f"{res.stdout}\n{res.stderr}")
Rev.2 해석기에 대한 전 시나리오 실측:
real binary : ACCEPT <ws>/realbin/herdr
symlink -> real : ACCEPT <ws>/goodlink/herdr
symlink -> shim : reject
2-hop chain -> shim : reject
shim dir directly : reject
wrapper dir (my-shim): reject
nothing installed : reject
부록 B — 문서 수정 전문 (F4)
create/SKILL.md
# 1) herdr available and isolated server status
# Use lib.sh's has_real_herdr, NOT `command -v herdr` / `type -P herdr`: once
# lib.sh is sourced the former matches its herdr() function and the latter
# matches the .mam/shim wrapper, so both pass on a host with no herdr (B-3).
has_real_herdr || { echo "ERROR: herdr not installed"; exit 1; }
status/SKILL.md
```bash
has_real_herdr || { echo "ERROR: herdr not installed"; exit 1; } # lib.sh helper (B-3)
command -v python3
test -f .mam/agent-sessions.yaml
```
> `command -v herdr` and `type -P herdr` both report success even when herdr is
> not installed — they match lib.sh's `herdr()` function and the `.mam/shim`
> wrapper respectively. Always use `has_real_herdr`.
[AGREEMENT: REACHED]