2026-08-04 세션 — P0 착수: P0-1 스폰 가드레일 + P0-2 카드↔실행 배선 [코디]
페페 지시 = "판정 가중치는 관리페이지 관리항목으로 넣어놔라. 내 의견과 devplan 매니저의 문맥적 파악으로 결정하겠다. 당장은 규칙 지정 보류다. P0부터 이제 작업 시작해." → 개발 축 P0-1(파일쓰기 가드레일) 착수. 기획서 = devplan
ai-stack-map.html8절.
논의·대안
메운 구멍 — task 단계 승인은 broker(7777)에 연결돼 있는데(runner.py:7,34,47), 실행 중 개별 툴콜은 전역 훅 whitelist.classify()가 _is_ops_file이 아니면 전부 AUTO_ALLOW라 파일쓰기에 사실상 게이트가 없었다.
- 대안A(기각) — broker
classify()를 고쳐 파일쓰기를 조인다. 페페의 대화형 세션 정책까지 같이 바뀐다(권한 알람 최소화는 페페가 고른 정책이다). 스폰 세션만의 문제에 전역 정책을 건드리는 건 범위 초과. - 대안B(채택) — 스폰 지점(
runner.py)에서 CLI 차단목록을 인자로 박는다. broker와 독립이라 broker가 죽거나 훅이 우회돼도 CLI가 자체적으로 막는다 = 기획서가 말한 이중화. 신규 정책파일 0. - 대안C(보류) — 허용목록(allowlist) 모드. 진짜로 닫히지만 범용 작업 러너를 과차단할 위험이 커서 페페 결정 사항으로 남긴다.
결정 — 문법을 추측하지 않고 실측으로 확정했다
claude -p 종단 6회를 돌려 규칙 문법을 확인한 뒤에 코드를 썼다. 추측했으면 조용히 안 걸리는 규칙을 넣을 뻔했다.
| 실측 | 결과 |
|---|---|
--disallowedTools Write 로 파일 생성 요청 |
🔴막히지 않는다 — 모델이 Bash로 우회해 파일을 만든다 |
Write + Bash + Edit 동시 차단 |
✅ BLOCKED, 파일 없음 |
Write(ops/**) 경로 규칙 |
🔴무시된다. CLI가 경고까지 찍는다 — "only Edit(path) rules are ... Edit rules cover all file-editing tools" |
Edit(**/ops/**) 경로 규칙 |
✅ 차단 경로만 BLOCKED, 다른 경로는 정상 생성 |
Bash(rm:*) 콜론형 / Bash(rm *) 공백형 |
✅ 둘 다 차단. 설정파일 문법과 같은 콜론형으로 통일 |
MultiEdit |
⚠없는 도구다 — CLI가 "matches no known tool" 경고 → 목록에서 뺐다 |
🔑핵심 함정 = 경로 규칙은 Edit(...)로만 쓴다. 기획서 문구(Edit/Write/MultiEdit)를 그대로 옮겼으면 Write(경로) 규칙이 조용히 무시되고, MultiEdit 경고까지 떴을 것이다.
산출물
runner.py—SPAWN_DENY_RULES18개를 모듈 상수로 박고_build_claude_args가--disallowedTools로 넘긴다.- 운영·비밀 경로 10 =
deploy-live·.secrets·.env*·*credentials*·.github/workflows·nginx.conf·gunicorn.conf.py·Dockerfile*·docker-compose*(brokerOPS_FILE_PATTERNS와 같은 취지를 CLI 레벨에 복제) - 되돌릴 수 없는 셸 8 =
rm·rmdir·del·git push·git reset --hard·schtasks·reg·shutdown - 쉼표 하나로 넘긴다 — 가변인자(
<tools...>)로 흩뿌리면 뒤 인자를 삼킬 수 있다. runner._selftest()신설 — 인자 형태·Write(금지·MultiEdit금지·필수 규칙 4종·쉼표 충돌·dry_run 동일성. 실행 =cd src && python -m cody_mcp_gateway.runner(상대 임포트라 파일 직접 실행 불가).- timeout 3600초는 그대로 뒀다 — 기획서가 현상으로만 적었고, 낮추면 정상 장기 작업이 끊긴다. 환경변수로 이미 조절 가능(
CODY_GATEWAY_TIMEOUT_SECONDS). 손대려면 별건.
검증
runner._selftest()— 규칙 18개·단일 인자·Edit(경로)문법·dry_run 동일 PASS.- E2E(실제 배포될 규칙 문자열 그대로) —
deploy-live/app.py쓰기 요청 → BLOCKED, 파일 없음 · CLI 경고 0(18개 규칙 전부 정상 파싱). - 과차단 없음 — 같은 규칙에서 일반 파일(
safe.py) 쓰기는 정상 생성. - 🔴남은 구멍(실측으로 뚫림) — 같은 규칙이 전부 걸린 상태에서
python -c "open('deploy-live/leak.txt','w').write('x')"는 그대로 통과했다. 차단목록은 증명이 아니다 — 직접 편집 도구와 파괴 셸 동사는 막지만 임의 인터프리터는 못 막는다. 코드 주석에도 이 실측을 그대로 박아뒀다.
다음
⏳ 페페 결정 1건 — 남은 구멍을 닫을까
1. 그대로 둔다(추천) — 스폰 세션은 우리가 낸 지시로만 돌고, task 단계 broker 승인이 앞단에 있다. 구멍은 "실수"가 아니라 "우회 의지"가 있어야 열린다.
2. 허용목록 모드 — --allowedTools로 도구를 좁힌다. 진짜로 닫히지만 범용 작업 러너가 자주 막힌다(운영 부담).
3. broker에 Bash-ops 규칙 추가 — 명령문에 ops 경로가 보이면 텔레그램 승인. 페페 대화형 세션도 같이 걸린다(그래서 이번엔 안 했다).
코디 잔여 = P0-2(카드↔실행 배선). P0-3(판정층)은 가중치 규칙이 페페 보류라 규칙기반 1차는 세우지 않고 대기.
관계
- 주제 그룹: gptbutler(개발 실행 기반) — P0-1은 기반 안전성, P0-2부터 devplan 매니저 제품 루프
- 관련 문서: gptbutler-handoff, gptbutler-session-2026-08-04-ai-stack-map, devplan-rules
- devplan:
ai-stack-map.html8절(P0-1 항목) ·priority_rules.md(⏸ 보류 중인 규칙 결정)
P0-2 — 카드 ↔ 실행 배선 (같은 세션, 26.8.4) [코디]
페페 지시 = "다음 작업 진행하고 ai stack map 페이지 최상단에 진행상태 표시해. 거기서 진행상태를 확인할게."
메운 구멍 — 실행 통로(run_claude_task)도 상태층(project_state.py)도 둘 다 이미 있었다. 없던 것은 그 사이를 잇는 코드다(카드↔실행 grep 0건). 그래서 매니저가 "이름만 매니저"였다.
설계 결정 3개
1. 🔑새 툴을 만들지 않았다 — 게이트웨이 툴 9개가 상한이다(handoff 규약). run_claude_code_task에 card 인자 하나를 붙여 카드 모드로 돌린다. 툴 수 그대로 9.
2. 상태 갱신은 JSON 직접 쓰기가 아니라 project_state.py CLI 호출 — 운영규칙(보류=사유+재개조건, 회차+1=--why)이 그 CLI에 코드로 강제돼 있다. 우회하면 규칙이 죽는다.
3. 단계(stage)는 안 건드린다 — 단계 전환은 페페 승인 영역. 카드 모드는 진행 보고만 남긴다.
동작 — card 주면 ①카드의 next_action·이번 회차 기준·설명으로 지시문을 만들고 ②기존 위험분류+broker 승인을 그대로 타고 ③--disallowedTools(P0-1)가 걸린 세션으로 실행한 뒤 ④성공했을 때만 카드에 진행 보고를 되돌려 쓴다.
사람 몫을 대신하지 않는다(방어 3종)
- 보류 카드 → 실행 거부(사유·재개조건을 그대로 돌려준다).
- next_owner == pepe 카드 → 실행 거부. 페페 결재가 필요한 걸 코디가 대신 눌러버리면 승인 게이트가 무의미해진다.
- 없는 카드 → 후보 이름과 함께 거부.
- dry run은 상태를 갱신하지 않는다 — 안 돌린 것을 진행으로 적으면 카드가 거짓말을 한다.
🆕 페이지 최상단 진행상태(페페 지시) — ai-stack-map-progress.json + 10초 폴링.
🔑정적 JSON으로 구동한 이유 = devplan 8799는 상승권한이라 코디가 재시작할 수 없다. 라우트를 새로 만들었으면 페페가 재시작할 때까지 죽은 코드였다(design-refs catalog.json·말풍선 notes.json과 같은 선례). 항목을 끝내면 코디는 JSON의 status·note만 고친다.
- 17항목 = 개발 9 + 현업 8. 축별 (완료/전체) 헤더 + 상단 요약 칩(완료·페페 차례·보류·대기·전체).
- 남이 쓴 텍스트는 textContent로만 넣는다(innerHTML 금지).
검증
- butler_tools._selfcheck_cards() — 없는카드·페페몫·보류 방어 + 지시문 생성(카드 맥락·추가 지시 포함) PASS(기존 selfcheck에 연결).
- E2E dry run — card='gptbutler' → 지시문 생성·decision=allow·state_update="상태 갱신 안 함(dry run)" 확인. card='ai-stack-map'(페페 몫) → blocked. 인자 둘 다 없음 → invalid.
- 상태 갱신 실경로 확인 — card_progress('gptbutler', …) 실행 → 카드 report·last_progress 실제 갱신됨(CLI 경유).
- 실브라우저 — 진행상태 17행 렌더 · 축 헤더 개발 3/9 현업 0/8 · 요약 칩 · 페이지 가로스크롤 0 · 13px 미만 0 · 최상단 위치(스크롤 0에서 sticky 헤더에 안 가림: hdr 120 < prog 136) · 콘솔 = favicon 404뿐.
- 게이트 = check_theme/check_font FAIL 0 · runner/plan_tools selftest PASS · 모듈 임포트 정상.
남긴 것(정직하게) - 🔴대시보드 댓글(app.py QA봇) → 실행 경로는 안 이었다. app.py에 라우트를 넣으면 8799 재시작 전까지 죽은 코드다(코디는 재시작 못 한다). 지금 배선의 입구는 매니저(Codex) 쪽이고, 댓글 입구는 페페가 재시작할 때 붙이는 게 맞다. - 카드 모드는 한 번에 한 카드다. 큐·병렬은 안 만들었다(YAGNI).
다음 = 개발 P1-1(mem_find 연결) 또는 현업 P0-1~3. P0-3은 가중치 보류라 대기.
3차 묶음 — P1-1 · P1-3 · P2-2 · 현업 P0-1 (같은 세션) [코디]
페페 지시 = "
projects/gptbutler/handoff.md정리하고, 나머지 진행가능한 항목들은 병행 및 순차 계획 잡아서 작업 진행해."
handoff 정리 — handoff.md 31,930 → 8,919 bytes(h2 16 → 7). 완료된 작업 서사 10절을 archive.md로 원문 cut + handoff엔 제목 목록만. archive.md register_doc 등록(229엔트리, 검증 OK). 유실 검사 = 이관 10절 본문 전부 archive에 존재 확인.
⚠남은 8.9KB는 시드 권장(6KB)을 넘지만 전부 "재논의 금지"로 못박은 확정 사항(그릴링 8건·매니저 확정 2차·플랫폼 제약·session-relay 통합·불변 핵심)이라 더 자르지 않았다.
계획 = 병행 2트랙 · 축 내부는 순차
- 병행 가능 = 개발 축(gateway·devplan) ↔ 현업 축(poin-agent) — 리포가 달라 서로 안 막는다.
- 순차 제약 = 현업 P0-1(테스트 게이트) → P0-2(관측) → P0-3(통짜주입)(관측이 있어야 P0-3 전후를 숫자로 본다) · 개발 P1-4는 P0-2 뒤.
- 이 계획은 ai-stack-map-progress.json의 _plan 필드에 박아뒀다(다음 세션이 그대로 읽는다).
이번에 끝낸 4건
1. ✅개발 P1-1 — 창고 검색기 연결. search_memory()가 mem_find.py(FTS5)를 부르고, 카드 모드 지시문에 과거 기록을 주입한다. 실측 809자 회수. ⚠색인 5분 주기라 방금 쓴 건 안 보인다 → 브리핑의 통짜읽기는 그대로 둔다(덤이지 대체가 아니다). 검색 실패는 조용히 무시 — 참고자료 때문에 실행이 막히면 안 된다. 🔑PYTHONUTF8=1을 subprocess env에 넣었다(한글 print cp949 사고 예방).
2. ✅개발 P1-3 — 스폰 세션 사후 계측. _spawn_metrics.jsonl에 소요초·rc·출력크기·timeout·task 앞 80자. 실시간 릴레이는 안 만들었다(헤드리스는 끊을 방법이 없어 과설계). selftest는 임시파일로 형식만 확인해 실제 로그를 더럽히지 않는다.
3. ✅개발 P2-2 — 헤드리스 호출부 3곳 대조표 (docs/headless-call-sites.md). 공용 wrapper는 안 만든다(세 곳의 목적이 다르다), 동기화 체크리스트로 쓴다.
4. ✅현업 P0-1 — 회귀테스트 선행 규칙화. rules.md 1항목 + watcher.py PROMPT 정의부 바로 위 주석 4줄. 코드 로직 변경 0. 테스트(test_watcher_rules.py 8항목) 통과 재확인 + 구문 검사.
🔴 P2-2 표를 만들다 나온 발견 — 안전은 "도구 × 훅" 두 축이다
| gateway runner | devplan QA봇 | poin-agent watcher | |
|---|---|---|---|
| 도구 제한 | 차단목록 18 | 전면 차단(답변 전용) | 🔴없음 |
| 훅(broker) | 살아 있음 | 꺼짐(--settings '{"hooks":{}}') |
살아 있음 |
| 판정 | 이중 | 일관되게 닫힘 | 가장 약함 |
- devplan QA봇이 훅을 끈 건 의도다 — 도구가 0이라 훅이 볼 일이 없다. 설계가 맞다.
- 🔴watcher만
--disallowedTools가 없다 = 셋 중 모델이 파일을 쓸 수 있는 유일한 호출부. 전역 훅은 ops 파일이 아닌 쓰기를AUTO_ALLOW한다. - ⚠"poin-agent 도구 0"은 MCP 도구 이야기였다 — 내장 도구까지 0이라는 뜻이 아니다. 기획서 매트릭스 8층(▲ 의도적 미연결)의 근거가 절반만 맞았다. 두 말이 다르다는 걸 이 표가 처음 드러냈다.
- → 현업 P0-4로 신규 등록(⏳페페 대기). 상시 가동 30초 데몬이라 코디가 임의로 안 건드린다 — 승인 + 드라이런 관찰 후.
검증 = runner selftest(계측 형식 포함) · _selfcheck_cards() · mem_find 실호출 809자 · 카드 지시문에 과거기록 주입 확인 · test_watcher_rules.py 8항목 PASS · watcher 구문 검사 · 실브라우저 18행(개발 6/9 · 현업 1/9 · 완료 7 · 가로스크롤 0) · 매니페스트 검증 OK.
다음 순번(계획 그대로) = ①현업 P0-2 관측 ②현업 P0-3 임계치 ③개발 P1-4 되먹임 ④현업 P1-2 비전 ⑤현업 P1-3 학습 ⑥개발 P2-1 ⑦현업 P2-1.