Files
multi-agent-mux/.agents/reports/canary-projects-multi-agent-mux-creator-claude/plan-5650172e.md
T

292 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Job 7e1de86e — Rev.2 계획서: 챌린지 `43ebc4cc` 반영
- **Job**: `7e1de86e` · **Role**: Planner · **Rev.1**: `5650172e` · **Challenge**: `43ebc4cc` (agy)
- **판정**: **우려는 채택, 처방은 기각.** 챌린지가 지목한 자동탐지 공백은 실재한다(Rev.1 이 40개 중 3개 실패).
그러나 §3 의 처방 두 가지는 **이 워크스페이스에서 실측한 결과 둘 다 같은 잘못된 id 를 반환한다**.
- **검증 요약**: `HEAD 6/40 · Rev.1 37/40 · Rev.2 40/40` · 변이 **15/15 검출**(챌린지 처방 M11·M12 포함) ·
전체 **244 passed**, 회귀 0
- **산출물**: `claude-reports/proposed/` (`skill/`, `test_orc_onboard.py`, `lib.sh.patch`, `deploy.patch`, `rev1-to-rev2.patch`)
> Rev.1 의 `lib.sh` 게이트 5 hunk, `_validate` 절, YAML 해시 접기, 배포 계약은 **변경 없음**.
> 챌린지가 그 부분을 전적으로 채택했고, 나 역시 재검토 결과 바꿀 이유를 찾지 못했다.
> 이번 개정은 전부 `orc_onboard.sh` 자동탐지에 국한된다(`rev1-to-rev2.patch`, +159/48).
---
## 1. 챌린지 판정
| 챌린지 주장 | 판정 | 근거 |
|---|---|---|
| Rev.1 자동탐지가 CLI `argv` 만 본다 | **맞다** | 그대로다 |
| 그래서 fresh 오케스트레이터가 온보딩 불가 | **맞다** | Rev.1 이 O-28/O-29/O-31 에서 실패 |
| **agy 는 argv 에 UUID 를 노출하지 않는다** | **틀렸다** | 실측: `agy --dangerously-skip-permissions --conversation 72d2d251-...` — Rev.1 이 이미 정상 탐지한다 |
| **처방 A**: env chain 을 **1순위**로 검사 | **기각** | 이 호스트에서 **틀린 id 를 반환**한다 (§2.1) |
| **처방 B**: `last_conversations.json[cwd]` 역매핑 | **기각** | 이 스킬이 없애려는 휴리스틱 그 자체다 (§2.2) |
| 환경변수를 근거로 쓸 수 있다는 착안 | **채택(형태를 바꿔서)** | 단, **에이전트 family 일치 조건** 필수 (§3.1) |
챌린지가 못 본, 그리고 **내 Rev.1 이 더 나빴던** 결함 하나를 §2.3 에 별도로 적는다.
---
## 2. 처방을 실측했다
### 2.1 처방 A — 환경변수 체인: 이 워크스페이스에서 틀린 답을 낸다
실행 중인 세 프로세스의 환경을 직접 읽었다:
```
pid 17410 (claude pane) ANTIGRAVITY_CONVERSATION_ID=0f84dbf7-5ddf-4619-a813-7e1ae35be009
pid 18521 (cline pane) ANTIGRAVITY_CONVERSATION_ID=0f84dbf7-5ddf-4619-a813-7e1ae35be009
```
**두 프로세스가 같은 값을 갖는다.** 원인도 확인했다 — 이 값은 herdr 서버(pid 17047)에서 상속된 것이고,
herdr 서버 자신이 agy 의 `run_command` 에서 기동되었다. 해당 프로세스의 `ANTIGRAVITY_SOURCE_METADATA`
안에 세 세션을 resume 한 그 명령이 그대로 들어 있다. 즉 **환경변수는 여기서 프로세스별 값이 아니라,
장수 서버를 통해 트리 전체로 새는 값**이다.
레지스트리의 실제 값과 대조:
| 출처 | 값 |
|---|---|
| claude 행 own id | `01eae7cf-1db6-4395-ba48-5fb02f4b6b1f` |
| agy 행 own id | `72d2d251-5a06-486b-92e5-7e46a7a80d2e` |
| cline 행 own id | `1785635248957_fajon` |
| **상속된 `ANTIGRAVITY_CONVERSATION_ID`** | `0f84dbf7-...`**어느 행과도 일치하지 않는다** |
챌린지 §3 의 체인을 그대로 실행했다:
```
challenge §3 layer-1 resolves to: 0f84dbf7-5ddf-4619-a813-7e1ae35be009
(nearest agent ancestor is claude; this is an agy conversation id)
```
`CLAUDE_SESSION_ID` 는 **존재하지 않는 변수명**이라 체인이 그대로 통과하고, claude 세션에서
**agy 대화 id 를 오케스트레이터 id 로 등록**한다. 같은 상황에서 Rev.2 는 `rc=3` 으로 거부한다.
### 2.2 처방 B — `last_conversations.json[cwd]` 역매핑: 결함 그 자체다
이 캐시는 **cwd → 가장 최근 대화** 매핑이다. 그리고 오케스트레이터는 자기가 띄우는 모든
서브에이전트와 **cwd 를 공유한다** — Rev.1 §1 에서 `verify_tui_viewport` 를 기각한 것과 정확히 같은 이유다.
게다가 이 테이블은 `find_workspace_uuid` 의 agy tier-2(lib.sh:1499-1507)와 reconcile 의 agy drift-C 가
**서브에이전트를 해석하는 데 쓰는 바로 그 테이블**이다. 서브에이전트 해석표를 읽어서 오케스트레이터를
정하겠다는 것이고, 그 값이 서브에이전트의 것이면 **그 서브에이전트를 발견에서 영구 배제**한다.
실측한 이 워크스페이스의 캐시 값:
```
cache["/Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux"] = 0f84dbf7-...
```
§2.1 의 누출값과 **같은 값**이고, 역시 어느 레지스트리 행과도 일치하지 않는다.
두 처방이 서로 독립적으로 같은 오답에 도달한다.
O-33 이 이 폴백의 부재를 고정하고, 변이 **M12**(처방 B 를 구현한 것)가 O-33 에서 검출된다.
### 2.3 챌린지도 나도 못 봤던 것 — Rev.1 은 cline 에서 **조용히 남의 id 를 등록한다**
`cline` 의 실제 프로세스:
```
node /Users/godopu16/.nvm/versions/node/v24.15.0/bin/cline -i --id 1785635248957_fajon
```
**argv[0] 의 basename 이 `node`** 다. Rev.1 §4.3 의 하드닝은 basename 이 `claude|agy|hermes|cline`
인 조상에서 멈추게 했으므로, cline 프로세스를 **그냥 지나친다.** 그리고 그 위에 있는 아무 에이전트나
줍는다. Rev.1 을 이 상황에 그대로 돌린 결과:
```
orcfix cline (node launcher, non-uuid): [dry-run] would add orchestrator uuid: 01eae7cf-1db6-4395-ba48-5fb02f4b6b1f
orcrev2 cline (node launcher, non-uuid): [dry-run] would add orchestrator uuid: 1785635248957_fajon
```
Rev.1 이 반환한 `01eae7cf`**이 report 를 쓰고 있는 claude 세션의 id** 다. 즉 Rev.1 은 cline 에서
"탐지 실패"가 아니라 **다른 에이전트의 대화 id 를 조용히 등록**한다 — Rev.1 §4.3 에서 M7 로 막았다고
선언했던 바로 그 실패 양식이, basename 매칭의 허점으로 되살아나 있었다.
부수적으로 **cline 의 id 는 UUID 가 아니다**`1785635248957_fajon` 이다.
Rev.1 의 `UUID_RE` 는 명시적 `--uuid` 로 넘겨도 이 값을 거부한다. cline 오케스트레이터는
Rev.1 로는 **어떤 방법으로도** 온보딩할 수 없었다.
이 두 가지는 챌린지가 제기한 것보다 심각하고, 챌린지의 처방으로는 고쳐지지 않는다.
---
## 3. Rev.2 설계 (`orc_onboard.sh` 자동탐지)
**가장 가까운** 에이전트 조상 하나에서, 다음 순서로 해석하고 멈춘다.
```
1. argv : claude -r / --session-id · agy --conversation · cline --id
2. env (family): CLAUDE_CODE_SESSION_ID · ANTIGRAVITY_CONVERSATION_ID
HERMES_SESSION_ID · CLINE_SESSION_ID
3. 그 외 → exit 3 (추측하지 않는다)
```
### 3.1 환경변수는 **family 가 일치할 때만** 증거다
§2.1 이 이 규칙의 전부다. 변수는 트리로 새지만, **어느 family 의 변수인지는 새지 않는다.**
가장 가까운 에이전트 조상이 `claude``CLAUDE_CODE_SESSION_ID` 만 읽고
`ANTIGRAVITY_CONVERSATION_ID` 는 무시한다.
이 규칙이 실측 3개 사례 전부에서 옳은 답을 낸다:
| 실행 위치 | 가장 가까운 에이전트 조상 | 해석 결과 | 정답? |
|---|---|---|---|
| 이 claude 세션 | claude | `CLAUDE_CODE_SESSION_ID` = `01eae7cf-...` | ✅ 레지스트리와 일치 |
| resumed agy | agy | argv `--conversation` = `72d2d251-...` | ✅ 레지스트리와 일치 |
| cline pane | cline (node 뒤에 있음) | argv `--id` = `1785635248957_fajon` | ✅ 레지스트리와 일치 |
| claude 세션 + 누출된 agy 변수만 존재 | claude | **거부, exit 3** | ✅ (처방 A 는 `0f84dbf7` 반환) |
변이 **M11**(처방 A 를 구현한 것)이 O-27 에서 검출된다.
### 3.2 argv 가 env 보다 우선한다
argv 는 그 프로세스가 **실제로 무엇으로 떴는지**의 기록이고, 환경변수는 어디서든 상속될 수 있다.
챌린지 §3 은 env 를 1순위로 두었다. 순서를 뒤집는 변이 **M14** 가 O-30 에서 검출된다.
### 3.3 env 계층이 실제로 해결하는 것
`CLAUDE_CODE_SESSION_ID` 는 claude 프로세스가 **자기 자식들에게** 내보내는 값이고,
내 환경에서 `01eae7cf-...` 로 정확히 일치했다. `-r` 없이 뜬 **fresh 오케스트레이터**는 argv 에 id 가
없으므로, 이 계층이 없으면 Rev.1 처럼 exit 3 이 된다. **이것이 챌린지의 우려가 옳았던 지점이고,
Rev.2 가 채택한 부분이다**(O-29).
주의: `CLAUDE_CODE_SESSION_ID` 는 claude **프로세스 자신의** 환경에는 없다(`ps eww -p 17410` 로 확인).
자식에게만 내보낸다. 그래서 조상의 환경을 읽는 게 아니라 **우리 자신의 환경**을 읽되,
family 판정만 조상에서 가져온다.
### 3.4 에이전트 판정을 argv 전체 경로 토큰으로 한다
§2.3 때문이다. `argv[0]` basename 만 보면 `node .../bin/cline` 을 놓친다.
Rev.2 는 첫 `-` 옵션 전까지의 경로 토큰들을 훑어 `claude|agy|hermes|cline` 을 찾는다.
변이 **M13**(basename only) 이 O-31 에서 검출된다.
### 3.5 id 형식을 uuid cline 형식으로 넓히되, 느슨해지지 않는다
```bash
_MAM_UUID_RE_G='[0-9a-fA-F]{8}-...-[0-9a-fA-F]{12}'
_MAM_CLINE_RE_G='[0-9]{10,}_[0-9A-Za-z]+'
```
"아무 문자열이나 허용"으로 무너지지 않았는지 O-32 가 확인한다
(`not-a-uuid`, `fajon`, `1785635248957`, `../../etc/passwd`, `a b` 전부 rc=2).
변이 **M15**(`[ -n "$1" ]`)가 O-16·O-32 에서 검출된다.
---
## 4. 검증 결과
### 4.1 3-트리 비교 (40 케이스)
| 트리 | 결과 | 실패 항목 |
|---|---|---|
| HEAD (`orcbase`) | **6 / 40** | — |
| Rev.1 (`orcfix`) | **37 / 40** | O-28 (family env 미사용) · O-29 (fresh claude) · O-31 (node 뒤의 cline) |
| **Rev.2 (`orcrev2`)** | **40 / 40** | — |
Rev.1 이 실패하는 3개가 **챌린지 우려의 실체**다. 동시에 Rev.1 은 O-27·O-33 을 **통과**한다 —
환경도 캐시도 아예 안 보기 때문이다. 즉 챌린지의 처방을 그대로 받았다면 3개를 고치면서
**2개를 새로 깨뜨렸을 것**이고, 그 2개가 §2.1·§2.2 다.
### 4.2 변이 테스트 — 15/15 검출
| 변이 | 검출 | 잡은 테스트 |
|---|---|---|
| M1M10 (Rev.1 결정 전체) | ✅ 10/10 | 변동 없음 |
| **M11 챌린지 §3: env chain 1순위, family 무시** | ✅ | O-27 |
| **M12 챌린지 §3: `last_conversations.json` 폴백** | ✅ | O-33 |
| M13 에이전트 판정을 argv[0] basename 으로만 | ✅ | O-31 |
| M14 env 를 argv 보다 우선 | ✅ | O-30 |
| M15 id 형식을 "비어있지 않음"으로 완화 | ✅ | O-16, O-32 |
M7(“id 없는 에이전트를 지나쳐 등반”)은 Rev.2 에서 O-19b·**O-27·O-33** 세 개가 동시에 잡는다 —
family 게이트와 캐시 부재가 같은 하드닝에 기대고 있다는 뜻이다.
### 4.3 신규 테스트 6개 (O-27..O-33)
| ID | 고정하는 것 |
|---|---|
| O-27 | 다른 family 의 누출 변수를 무시한다 (§2.1 / M11) |
| O-28 | family 가 맞으면 실제로 쓴다 — O-27 의 대조군 |
| O-29 | fresh claude 가 `CLAUDE_CODE_SESSION_ID` 로 해석된다 (챌린지 우려의 채택분) |
| O-30 | argv 가 env 를 이긴다 (§3.2 / M14) |
| O-31 | `node` 런처 뒤의 cline + 비-uuid id (§2.3 / M13) |
| O-32 | 형식 완화가 "아무거나 통과"로 무너지지 않는다 (M15) |
| O-33 | 워크스페이스 캐시 폴백이 없다 (§2.2 / M12) |
O-27·O-33 은 **negative test** 라 대조군이 필수다. O-28 이 그 역할을 한다 — env 를 통째로 무시하는
탐지기도 O-27 을 통과하기 때문이다.
테스트 하네스 주의사항 하나: 이 pytest 프로세스 자체가 claude 안에서 돌기 때문에
`CLAUDE_CODE_SESSION_ID` 가 상속되어 있다. 스크럽하지 않으면 픽스처가 아니라 하네스가 답을 정한다.
실제로 O-19b 가 이 때문에 먼저 깨졌고, 모든 자동탐지 테스트가 4개 변수를 제거한 환경에서 돈다.
### 4.4 전체 회귀
```
244 passed in 546.23s (기존 204 + 신규 40, 회귀 0)
```
---
## 5. 변경 범위
| 파일 | Rev.1 대비 |
|---|---|
| `.agents/skills/lib.sh` | **변경 없음** (5 hunk 그대로, `lib.sh.patch`) |
| `deploy/remove.sh`, `deploy/gitea-ci.yml` | **변경 없음** (`deploy.patch`) |
| `.../orc_onboard.sh` | **+159 / 48** (`rev1-to-rev2.patch`) — 자동탐지 3계층, family 게이트, 에이전트 판정, id 형식 |
| `.../SKILL.md` | 자동탐지 절 재작성 (해석 순서 · 세 가지 안전 규칙 · cline 예외) |
| `tests/test_orc_onboard.py` | 33 → **40** 케이스 |
실행 순서는 Rev.1 §6 과 동일하되, 6번(온보딩 실행)은 이제 fresh 오케스트레이터에서도
`--uuid` 없이 동작한다.
---
## 6. 남은 리스크 · 측정하지 않은 것
Rev.1 §7 의 6개 항목은 그대로 유효하다. 이번 개정으로 추가되는 것:
1. **같은 family 내부의 누출은 family 게이트로 못 막는다.** agy 조상 + 누출된
`ANTIGRAVITY_CONVERSATION_ID` 조합은 family 가 일치하므로 그 값을 쓴다(측정: R6 에서
`0f84dbf7` 반환). 실제 agy 세션에서는 argv 가 먼저 이겨서 옳은 값(`72d2d251`)이 나오지만,
**argv 도 env 도 오염된 fresh agy** 는 잘못된 값을 등록할 수 있다.
이 경우의 방어선은 §4.2(exit 1) live-owner 가드 하나뿐이며, 그것은 대상 id 가
**running 행에 이미 있을 때만** 발동한다. 이 잔여 위험은 닫지 못했다.
2. **`HERMES_SESSION_ID` · `CLINE_SESSION_ID` 는 이름을 추정했다.** hermes 는 이 호스트에서
실행 중이 아니었고, cline 프로세스 환경에서 해당 변수를 찾지 못했다. 두 변수는 존재하지
않을 수 있다. 다만 **없으면 그냥 argv 로 떨어질 뿐** 이고 (cline 은 argv `--id` 로 이미
해결된다), 오답을 만들지는 않는다. 실제 이름 확인은 별도 항목이다.
3. **`CLAUDE_CODE_SESSION_ID` 가 herdr 서버를 통해 오염되는 경우는 재현하지 못했다.**
이 호스트에서 깨끗했던 이유는 herdr 서버가 claude 가 아니라 agy 에서 기동되었기 때문이다.
claude 에서 기동된 herdr 서버에서는 `ANTIGRAVITY_CONVERSATION_ID` 와 같은 오염이
`CLAUDE_CODE_SESSION_ID` 에도 발생할 수 있다 — 구조상 가능하나 **측정하지 않았다.**
§6.1 과 같은 잔여 위험 범주다.
4. **`ps eww` 는 macOS 기준으로만 측정했다.** Linux `/proc/<pid>/environ` 경로는 확인하지 않았다.
Rev.2 는 조상의 환경이 아니라 **자기 자신의** 환경을 읽으므로 `ps eww` 의존은 실제로 없지만
(family 판정은 `ps -o command=` 만 쓴다), §2.1 의 **측정 자체**는 macOS 에서만 수행했다.
---
## 7. 결론
챌린지의 **우려는 정확했다** — Rev.1 은 fresh 오케스트레이터를 온보딩할 수 없었고, 그 지점이
Rev.1 이 40개 중 3개를 실패하는 자리다. 환경변수를 근거로 쓰자는 착안도 옳았다.
**처방은 채택하지 않았다.** 두 처방 모두 이 워크스페이스에서 실측한 결과 `0f84dbf7` — 어느 레지스트리
행과도 일치하지 않는 id — 를 반환한다. 특히 처방 B 는 서브에이전트 해석에 쓰이는 바로 그 cwd 캐시를
읽는 것이라, 이 스킬이 없애려는 휴리스틱을 다른 문으로 되들이는 셈이다. 두 처방을 변이 M11·M12 로
구현해 스위트가 잡는지 확인했고, 둘 다 검출된다.
채택한 형태는 **family 일치 조건을 붙인 환경변수 계층**이고, 순서는 **argv 우선**이다.
이 조합이 실측 4개 시나리오 전부에서 정답을 낸다.
그리고 이 개정에서 가장 중요한 발견은 챌린지도 나도 제기하지 않았던 §2.3 이다 — Rev.1 은
`node` 런처 뒤의 cline 을 지나쳐 **내 claude 세션 id 를 조용히 등록**하고 있었다. Rev.1 §4.3 에서
막았다고 선언한 실패 양식이 basename 매칭의 허점으로 되살아나 있었다. 도전을 검증하러 프로세스
테이블을 실제로 읽지 않았다면 찾지 못했을 것이다.
[AGREEMENT: REACHED]