* 이 게시물은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
(폐쇄망 사내 에이전트 ⑦) 규칙
권한 모드(6편)를 넣고도 남는 피로가 있다. 같은 파일을 반복 편집하는 세션에서 매번 뜨는 승인 창. 클로드 코드에는 “이 대상은 다시 묻지 마”가 있는데 에어에는 없었다. 그래서 승인 화면을 y/N에서 y/N/a(항상)의 3택으로 바꾸는 커밋이 나왔다. 문제는 이 편의 기능이 감사 체계를 갉아먹지 않게 만드는 것이었다.
commit 7cb64a0 (2026-08-16)
feat: 승인 규칙 — "다시 묻지 않기"를 감사 가능하게 영속화한다
원칙 — 규칙은 정책도 모드도 이기지 못한다
규칙이 못 하는 것부터 정한다
기능 설계인데 문서의 절반이 ‘못 하는 것’ 목록이다. 이게 맞는 순서라고 생각한다.
- 정책 DENY를 못 되살린다 — 규칙은 ‘물을 것이 확정된 호출의 질문’만 접는다. 차단된 걸 풀 수는 없다.
- DESTRUCTIVE는 규칙이 될 수 없다 — 삭제류엔 a 선택지 자체가 안 보이고, 규칙 파일에 손으로 써넣어도 매칭이 무시한다. 이중 잠금이다.
- deny 규칙은 만들지 않는다 — 차단은 정책의 몫. 두 군데서 차단하면 “왜 막혔는지”의 답이 두 개가 된다.
- 규칙으로 통과한 실행은 RULE_APPROVED로 남는다 — 사람이 누른 것과 구분(6편의 불변식).
특히 두 번째가 좋다. UI에서 숨기는 것만으론 부족하다 — 사용자가 JSON 파일을 직접 고칠 수 있으니, 매칭 로직이 한 번 더 거부해야 잠금이 완성된다. 편의 기능일수록 우회로까지 막아야 한다.
무엇을 기억하나 — 대상마다 다르게
‘같은 대상’의 정의도 도구 성격 따라 다르다. 파일은 정확 일치로 기억한다 — 파일 하나 허락이 폴더 전체로 번지면 안 되니까. 셸 명령은 접미 글롭(npm run test*)으로 기억한다 — 어차피 화이트리스트(5편)가 앞단을 제한하고 있어서, 인자만 다른 같은 명령에 매번 묻는 건 소음이기 때문이다. 저장은 설정과 분리된 approval-rules.json — 런타임이 스스로 쓰는 파일이라서다. 사람이 직접 고쳐도 되고, 깨진 항목은 그 항목만 무시한다(파일 하나 때문에 전체가 죽지 않게).
승인 화면:
filesystem.write → /home/user/report.md
[y] 이번만 허용 [N] 거부 [a] 항상 허용(이 파일)
$ air rules # 저장된 규칙 목록
$ air rules remove 2 # 2번 규칙 삭제 — 모델 안 올리는 가벼운 명령
확장은 기존을 안 깨는 모양으로
코드로 — ‘선택지가 보이면 거짓말이 된다’
DESTRUCTIVE 차단의 실제 구현이 이 프로젝트의 성격을 잘 보여준다. 콘솔 승인 핸들러의 주석과 판정 함수 —
// packages/approval/src/ConsoleApprovalHandler.ts
/**
* 3택 승인. a는 "승인 + 같은 대상은 다시 묻지 않기"다.
* DESTRUCTIVE와 등급 미상 요청에는 a를 보여 주지 않는다
* (어차피 규칙집이 저장을 거부하는데 선택지가 보이면 거짓말이 된다).
*/
function ruleEligible(request: ApprovalRequest): boolean {
return request.riskLevel !== null
&& request.riskLevel !== RiskLevel.DESTRUCTIVE;
}
눈여겨볼 건 !== null — 등급을 모르는 요청도 규칙 부적격이다. “모르면 안전한 쪽으로”가 조건식 하나에 들어 있다. 그리고 UI(선택지 숨김)와 규칙집(저장 거부)이 같은 판정 함수를 공유한다 — 두 곳이 따로 판단하면 언젠가 어긋나고, 어긋난 안전장치는 없느니만 못하다.
승인 화면의 표시 원칙도 주석에 있다: “화면의 주 표시는 Tool 이름이 아니라 수행될 명령이다($ rm -rf build/)”. 사용자가 승인하는 건 ‘shell.execute’라는 추상이 아니라 실제로 실행될 그것이어야 한다 — 2편에서 경로 경계를 승인으로 대체할 수 있었던 전제가 바로 이 정직함이다.
적용해 보기 — ‘기억하는 승인’을 넣기 전 점검
- 기억의 키를 정했나 — 파일은 정확 일치, 명령은 접두/글롭… 도구 성격마다 다르다. 넓은 키는 권한 확산이다.
- 기억이 못 이기는 것(정책·파괴적 동작)을 코드 두 곳 이상에서 못박았나.
- 기억으로 통과한 실행이 감사에서 구분되나.
- 사용자가 기억을 보고 지울 수단(목록·삭제 명령)이 있나 — 없으면 잊힌 권한이 쌓인다.
✅ 확인
- 규칙 파일을 손으로 조작해도 파괴적 동작이 자동 통과되지 않음을 테스트했다.
- UI의 선택지 노출과 저장 로직이 같은 판정을 쓴다.
- 규칙 목록 조회가 모델을 올리지 않고(가볍게) 동작한다.
구현 디테일 하나. 승인을 처리하는 인터페이스는 원래 불리언(허용/거부)이었다. 3택을 넣으면서 기존 구현을 다 고치는 대신, 이중 계약으로 갔다 — 새 메서드 decide()(3택)가 있으면 쓰고, 없으면 종전 불리언을 읽는다. 기존 구현·테스트가 한 줄도 안 바뀌는 확장이다. 웹 승인 다이얼로그에서도 “항상 실행”은 세 번째 버튼, 즉 가장 큰 권한 위임이 가장 먼 자리에 놓였다. 실기 검증으로 a 응답 → 규칙 저장 → 같은 파일 재요청이 승인 화면 없이 실행되고 감사에 RULE_APPROVED가 남는 것까지 확인하고 닫았다.
편의 기능을 안전 시스템에 얹을 때의 공식 — ① 못 하는 것부터 명문화 ② 우회로(파일 직접 수정)까지 차단 ③ 통과 경로를 감사에서 구분 ④ 기존 계약은 깨지 않는 확장. 이 네 개가 갖춰지면 편의는 부채가 아니다.
자주 묻는 질문 (FAQ)
‘다시 묻지 않기’는 결국 보안을 약화시키는 것 아닌가요?
약화되는 건 ‘질문 빈도’지 ‘안전선’이 아닙니다. 정책 차단과 파괴적 동작은 규칙으로 못 뚫고, 규칙 통과는 RULE_APPROVED로 따로 기록돼 추적됩니다. 묻는 피로를 줄이지 않으면 사용자가 auto 모드로 도망가는데, 그게 오히려 더 큰 위임입니다.
승인 규칙과 권한 모드는 어떻게 다른가요?
모드는 세션 전체의 태도(전부 묻기/편집만 자동 등)이고, 규칙은 특정 대상(이 파일, 이 명령)에 대한 기억입니다. 규칙은 모드가 ASK로 답한 호출에만 관여하는, 더 좁고 정밀한 층입니다.

답글 남기기