meta
기능을 거부하는 법 — AI 페어가 자기 로드맵을 근거를 남기고 쳐냈다
· Ascendy Engineering
TL;DR
- 우리는 보통 무엇을 만들었나로 진척을 잰다. 그런데 로드맵에 있던 걸 “안 만들기로” 결정하는 것도 똑같이 엔지니어링이다 — 잘하면.
- redteam(적대적 에이전트-페어 하네스, Apache-2.0)이 이슈 #37(“pluggable reviewer execution”)을 종결한 방식이 그 예다. 3단계로 쪼갰던 작업 중 1개만 출시하고 2개를 거부했다.
- 핵심은 어떻게 거부했냐다 — 거부를 흔적 없이 버리지 않고, 분석 + 가드레일 + 되살림 조건을 결정문(PR로 머지)으로 남겼다. “안 함”도 커밋 로그에 남는 결정이 된다.
- 그리고 거부를 정당화한 건 작은 보안 함정 하나였다: 한쪽은 정규화된 식별자로, 다른 쪽은 raw 키로 비교하면, 키 하나로 동등성 검사가 뚫린다.
소스 노트. redteam 팀 인테이크를 정제한 글이다. 전부 public OSS(
AscendyProject/redteam, Apache-2.0)라 이슈·PR 번호와 상태는 공개 repo에서 gh로 직접 확인했다. 같은 적대적 페어 결의 한 AI가 쓴 PR을 다른 AI가 리뷰해 HIGH 결함 4개를 잡은 글과 이어진다.
만드는 것만 진척이 아니다
기능을 추가하면 커밋이 쌓이고, PR이 머지되고, 진척이 보인다. 그런데 **“이건 안 만들기로 했다”**는? 보통은 이슈를 조용히 닫거나, 백로그에서 썩히거나, 머릿속에서만 결정하고 만다. 흔적이 안 남는다. 그래서 6개월 뒤 누군가 똑같은 걸 다시 제안하고, 똑같은 분석을 처음부터 다시 한다.
redteam이 이슈 #37을 닫은 방식은 달랐다. #37은 “적대적 리뷰어를 어댑터 seam 뒤에서 어떻게 구동할까”를 세 단계로 쪼갰던 작업이다:
- step 4 — fail-closed 폴백 사다리: 1차 헤드리스 리뷰어가 인프라로 실패할 때(CLI 없음·미인증·타임아웃·파싱 불가)만 작동하는 안전 사다리. → 0.3.0에 출시됨.
- step 6 — 터미널 멀티플렉서 화면을 긁어오는 트랜스포트: 거부.
- step 5 — 서브에이전트 리뷰어 어댑터(코딩 세션 안에서 보안 리뷰어 서브에이전트를 직접 띄우기): 이번 사이클 거부. 포커스 이슈(#67)로 떼어내 추적하다
NOT_PLANNED로 닫음.
결과: #37 엄브렐러가 완전히 종결됐다 — 세 갈래 중 하나만 출시되고, 둘은 거부. 그리고 그 거부 결정문은 다른 프로바이더의 모델(Codex)이 교차 리뷰한 뒤 머지됐다(read-only 샌드박스에서 실제 브랜치 코드를 열어 검토, 1라운드 승인).
왜 거부했나 — 한계 이득 vs 떠안는 비용
step 5(서브에이전트 어댑터)의 유일한 이득은 “세션 안에서 리뷰가 도는 게 보이고 중간에 조종할 수 있다”는 가시성뿐이었다. 그런데 헤드리스 리뷰어(claude -p --permission-mode plan)가 이미 “Claude가 리뷰어로 들어오는 cross-provider” 케이스를 커버하고 있었다.
그 한계 이득을 위해 떠안아야 하는 건 둘이었다:
- 새 실행 표면. 세션 안에서 서브에이전트를 띄우는 건, 하네스의 두 불변식 — 어느 프로젝트에서나 돈다(project-agnostic) 와 런타임 의존성 0(zero-runtime-deps) — 을 정면으로 압박한다.
- 까다로운 보안 선결조건(아래).
이득은 작고 비용은 크다. 그래서 거부했다. 거부도 정당한 엔지니어링 결정이라는 게 이 글의 핵심이다 — 단, 흔적을 남기는 거부여야 한다. YAGNI(“필요해지기 전엔 안 만든다”)를 결정문으로 적고, 나중에 되살리려면 다시 통과해야 할 가드레일을 함께 박아뒀다.
거부를 정당화한 보안 함정 — 식별자 정규화
step 5를 거부하게 만든 결정적 디테일은 작고 일반적이다. redteam에는 자기-리뷰 붕괴를 막는 가드가 있다 — 같은 프로바이더의 모델이 자기 코드를 리뷰하는 걸 fail-closed로 거부한다(적대적 페어는 반드시 cross-provider여야 하니까).
문제는 그 가드가 양쪽을 다른 형식으로 비교했다는 데 있었다. 한쪽(코드를 쓴 worker)은 정규화된 프로바이더 family로 식별하고, 다른 쪽(리뷰어)은 raw 어댑터 식별자로 식별했다. 형식이 어긋난 비교라, 새 어댑터를 하나 들이면 그 동일성 검사가 cross-provider인 척 통과해 — 같은 프로바이더의 자기-리뷰가 가드를 우회할 수 있었다. 서브에이전트 어댑터를 붙이는 순간 정확히 이 틈이 열렸을 것이다. 설계 검토에서 HIGH로 잡혔고, 결정문에 하드 선결조건으로 박혔다: 비교 전에 양쪽을 같은 정규형(family)으로 맞춘다.
일반화하면 이렇다 — 동등성/식별 검사에서 한쪽은 정규화된 형식으로, 다른 쪽은 raw 값으로 비교하면, 형식 차이를 파고드는 입력 하나로 검사가 뚫린다. 비교 전에 양쪽을 같은 정규형으로 맞춰라. 인증·권한·중복 제거·자기-리뷰 가드, 식별자를 비교하는 모든 곳에 적용된다.
정직하게 — 이번엔 격렬한 논쟁이 없었다
redteam 글의 단골 줄기는 “Claude와 Codex가 여러 라운드 갈렸다”인데, 이번 사이클엔 그게 없었다. 거부 결정문에 대한 Codex 교차 리뷰는 1라운드 만에 승인이었고, substantive하게 갈린 지점이 없었다. 그래서 “치열한 토론”으로 포장하지 않고 그냥 비워둔다 — 없는 긴장을 지어내는 게 더 나쁘니까.
가져갈 것
- “안 만들기로 함”도 결정이다 — 흔적을 남겨라. 거부를 조용히 이슈 닫기로 끝내면, 6개월 뒤 같은 분석을 처음부터 다시 한다. 분석·가드레일·되살림 조건을 결정문으로 남기면, 거부가 재사용 가능한 자산이 된다.
- YAGNI는 게으름이 아니라 규율이다 — 되살림 조건을 함께 적을 때. “지금은 안 만든다”에 “되살리려면 이 가드레일을 다시 통과해야 한다”를 붙이면, 미래의 나를 막는 게 아니라 돕는다.
- 식별자는 비교 전에 정규화하라. 한쪽 family·다른 쪽 raw-key처럼 형식이 어긋난 비교는 값 하나로 뚫린다. 인증·권한·동일성 검사 전반의 함정.
- 없는 긴장은 지어내지 마라. 이번엔 논쟁이 없었으면 없었다고 적는다. 그게 다음에 진짜 논쟁이 있을 때의 신뢰를 만든다.
저장소: github.com/AscendyProject/redteam (Apache-2.0).
저작·인용: 이 글은 Ascendy Engineering이 작성했으며 출처 표기 시 재인용 가능합니다. 잘못된 정보를 발견하면 GitHub 이슈로 알려주세요.
Tags: decision-record, oss, agent-pair, yagni, trust