8월 10일, 저희는 Microsoft의 Agent Governance Toolkit 저장소에 이슈 #3661을 제기했습니다. 이 문제는 정책 linter가 조건이 절대 일치할 수 없는 규칙도 허용한다는 것이었고, 평가자(evaluator)는 해당 규칙들이 일치하지 않을 경우 기본 동작(default action)을 조용히 적용했습니다. 10월 20일, 한 유지보수 담당자가 이 문제에 대한 전체 감사 보고서를 작성하여 답변했습니다. 그들의 감사는 저희가 보고한 내용보다 더 많은 실패 모드를 발견했으며, 수정 사항이 어디에 반영될지, 그리고 반영되지 않을지를 명확히 제시했고, 불편하지만 확실한 사실 하나를 확인시켜 주었습니다. 바로 배포된 패키지들이 여전히 유통되고 있음에도 불구하고 절대 일치하지 않는 규칙을 허용하고 기본 동작을 조용히 적용한다는 것입니다. 이것이 스레드에서 언급하는 내용이며, 이 문제가 단 하나의 저장소에만 국한되지 않고 더 큰 의미를 갖는 이유입니다.
저희가 보고한 문제점
툴킷의 agt lint-policy는 정책 파일을 "일반적인 실수"를 위해 검사(linting)한다고 설명합니다. 하지만 저희는 이것이 놓치고 있는 사례를 발견했습니다. 즉, 거버넌스(governance) 기능이 활성화되고, 정책 파일이 로드되며, 규칙 자체가 구문적으로 유효함에도 불구하고, 실제로 발생할 수 있는 어떤 것과도 일치하지 않는 경우입니다.
배포된 PyPI 패키지들(agent-governance-toolkit 4.1.0, agent-os-kernel 3.7.0, clean venv)을 대상으로 재현한 세 가지 변형 사례는 다음과 같습니다:
toolname(실제로는 tool_name의 오타)에 대한 거부(deny) 규칙은 절대 발동되지 않습니다.
- 비어 있는
in: [] 값을 가진 거부 규칙은 절대 발동되지 않습니다.
- 더 높은 우선순위를 가진 포괄적인 허용(blanket allow) 규칙에 의해 가려진 거부 규칙은 절대 발동되지 않습니다.
이 세 가지 경우 모두 linter를 통과하며 "No issues found."라는 메시지를 반환합니다. 평가자는 "규칙과 일치하는 것이 없으므로 기본 동작을 적용함"이라는 이유와 함께 allowed=True를 반환합니다. 이 과정에서 예외나 경고는 발생하지 않습니다. 만약 default_action: deny가 설정되어 있다면, 이는 안전하게 실패(fails closed)하여 혼란스러운 거부 결과를 초래합니다. 그러나 default_action: allow인 경우, 이는 위험하게 열리는(fails open) 상황이 되며, 이 허용 동작은 README의 quickstart 정책(production-policy)에서 기본값으로 설정되어 있습니다.
스레드에서 발생한 일
커뮤니티 기여자가 현재 main 브랜치에서 하루 만에 이 문제를 재현했고, 복잡성을 파악했습니다. 보고서가 목표로 했던 linter는 이제 main 브랜치에서는 존재하지 않으며, ACS 마이그레이션 과정에서 대체되었습니다. 반면 실패하는 레거시 평가기(evaluator)는 여전히 PyPI를 통해 배포되고 있습니다. 9월에 PR #4130은 단방향 v4-to-v5 마이그레이터가 명백히 불만족스러운 규칙들, 즉 빈 in 리스트, 빈 or 조건, 모순되는 and 형제들을 거부하도록 만들었습니다. 10월에 이 문제는 보류 처리되었고, 저희는 공개된 패키지들과 다시 확인하여 원래의 재현이 여전히 열려(open) 실패한다는 것을 확인했으며, #4130이 마이그레이터 경로를 수정했을 뿐, pip 사용자들이 여전히 설치하는 4.1.0 평가기와 linter는 수정하지 않았음을 지적했습니다. 이 문제는 계속 열린 상태로 남았습니다.
유지 관리자의 감사(audit)
10월 20일, 유지 관리자인 MohammadHaroonAbuomar가 프로젝트의 결정을 게시했습니다. 공을 돌리자면: 그들은 저희 세 가지 사례를 개별적으로 검토하는 대신, 현재 소스 전체 클래스를 대상으로 실행하여 스레드에 없던 변형까지 포함해 모든 변종들을 나열했습니다:
- 어떤 연산자로든 오타가 난 필드는 수용되며, 절대 일치할 수 없는 Rego 접근자(accessor)를 컴파일합니다.
- 표준 v4 컨텍스트 키
tool_name은 input.snapshot.tool_name으로 컴파일되지만, ACS 스냅샷은 이 이름을 tool_call.name 아래에 가지고 있습니다. 올바르게 철자가 된 v4 deny는 절대 발동되지 않는 규칙으로 마이그레이션되며, 녹색 마이그레이션 보고서를 생성합니다.
- 더 높은 우선순위의 allow가 의해 가려진(shadowed) deny는 수용됩니다.
not_in: []를 가진 allow 규칙은 수용되며 항상 발동됩니다.
- 최상위 레벨의
default_action: deny는 무시되고, 생성된 번들(bundle)은 기본적으로 allow로 설정됩니다.
별도로, 그들은 main 브랜치의 README quickstart가 이제 다른 (agentmesh) 문자열 조건 엔진을 사용하며, 여기에서도 default_action: allow이고 빈 in 리스트나 오타 난 경로는 로그 없이 false를 반환한다고 언급했습니다.
두 번째 사례를 다시 읽어보세요. 심지어 사용자가 모든 것을 올바르게 수행했더라도—올바른 필드 이름, 작동하는 거부 규칙(deny rule), 성공적인 마이그레이션—결국 정책이 한 번도 발동되지 않는 상황에 놓입니다. 왜냐하면 해당 필드가 정책 버전 사이에 이름이 바뀌었는데 아무것도 확인하지 않았기 때문입니다. 실패가 올바른 규칙의 올바른 마이그레이션을 살아남은 것입니다.
해결책과 그 한계점
유지보수자들은 4.x 평가기(evaluator)를 패치하지 않을 것이며, 그 소스 코드는 더 이상 저장소에 존재하지 않습니다. 그들이 밝힌 계획은 다음과 같습니다:
- 마이그레이터 (Migrator) (agt-policies, PyPI에서 5.2.0으로 배포 예정): v4 컨텍스트 키를 스냅샷 경로로 매핑합니다 (
tool_name을 tool_call.name으로); 구성 요소 이름, 알 수 없는 필드, 알 수 없는 최상위 키가 포함된 경우 오류와 함께 거부하고, not_in: []이 있는 허용 규칙(allow rule)과 상위 우선순위의 반대 동작 규칙에 의해 명백히 커버되는 규칙은 거부하며, 기본-거부(default_action: deny)를 준수하기 위해 기본 거부 번들(default-deny bundle)을 방출하고 다른 모든 값은 거부합니다. 테스트는 생성된 번들을 실제 pre_tool_call 스냅샷과 OPA를 사용하여 실행하여 매핑된 tool_name 거부가 실제로 발동됨을 증명해야 합니다.
- 린트 및 문서 (Lint and docs) (agent-compliance): README의 빠른 시작 가이드(quickstart)는
default_action: deny로 이동합니다; agt lint-policy는 레거시 v4 문서를 그러한 것으로 식별하고 agt migrate v4-to-v5를 가리킵니다; LIMITATIONS 항목에는 게시된 4.x 패키지가 일치하지 않는 규칙(never-match rules)을 수용하며 기본적으로 침묵한다는 내용이 명시될 것입니다.
- agentmesh 엔진: 별도의 이슈로 다룹니다.
in []에서, 허용 규칙의 not_in: []에서, 그리고 거부 규칙에서 인식할 수 없는 구문(unrecognized syntax)에서 로드 시간에 경고를 발생시킵니다.
- 나중: ACS 검증기(validator)가
opa check --strict을 실행하고 매니페스트의 쿼리가 번들이 정의한 규칙 이름을 지정하는지 확인합니다.
두 가지 솔직한 주의사항이 있습니다. 첫째, 이것은 계획이며 초청된 PR(Pull Request)일 뿐, 출시된 릴리스는 아닙니다. 오늘 기준으로 이 중 어느 것도 설치할 수 있는 패키지에 포함되지 않았습니다. 둘째, 유지 관리자들 스스로의 결정에 따라 이미 게시된 4.x 패키지들은 기존 동작을 유지합니다. LIMITATIONS 항목은 이를 변경하기보다는 문서화할 것입니다. 만약 PyPI에서 agent-governance-toolkit 4.1.0을 설치했다면, 해당 라인에는 패치가 오지 않을 것입니다.
녹색 보고서가 강제가 아니다 (A green report is not enforcement)
더 깊은 교훈은 하나의 툴킷에 관한 것이 아니며, 유지 관리자들 스스로의 테스트 요구 사항이 이를 가장 잘 보여줍니다. 이 모든 경우를 포착할 수 있는 검사는 그들이 현재 작성하고 있는 것입니다: 생성된 정책을 현실적인 입력값에 대해 실행하여 거부(deny)가 작동하는지 증명하는 것입니다. 그 이전의 모든 것은 정책에 대한 진술이었습니다: 파일이 구문 분석되고, 스키마가 유효성을 검사하며, 린터(linter)가 녹색이고, 마이그레이션 보고서가 녹색이라는 것이었습니다. 이들 중 어느 것도 정책이 작동하고 있다는 의미는 아닙니다.
우리는 다른 각도에서 같은 주장을 해왔습니다: 지침은 승인(authorization)이 아니며, 공개 키(public key)는 권한(permission)이 아니다. 린트 보고서도 같은 목록에 속합니다. 올바르게 읽히는 정책 파일은 의도의 진술일 뿐이며, 강제(enforcement)는 실제 요청이 도착했을 때 발생하는 것입니다. 이 둘 사이의 간극이야말로 이러한 모든 버그가 존재했던 곳입니다—오타, 빈 리스트, 이름이 바뀐 필드, 누락된 기본값 등. 각각은 개별적으로 지루하지만, 모두 조용히 '거부'를 '기본 허용(allow by default)'으로 변환시킵니다.
발견하는 방향이 발견 자체보다 더 중요합니다. 유지 관리자들은 두 달 동안의 스레드에서 나열된 경우보다 한 번의 감사 과정에서 더 많은 사례를 발견했습니다. 이는 사례들이 숨겨져 있었기 때문이 아니라, 아무도 소스 코드를 결함 유형(class of defect)에 대해 실행해 보지 않았기 때문입니다. 이것이 바로 실제 검토가 제공하는 가치입니다: 녹색 보고서가 아니라, 실제로 그 무언가를 실행하는 사람을 얻는 것입니다.
오늘 v4 정책을 실행할 때 확인할 사항들
- 게시된 4.x 패키지에서는 리터가 이 유형의 결함을 감지할 수 없습니다. 모든
deny 규칙의 필드 이름을 평가자(evaluator)의 컨텍스트 키와 직접 비교하고, 절대로 빈 in 또는 not_in 목록을 배포하지 마십시오.
- v4에서 v5로 마이그레이션한 후에는 OPA를 사용하여 실제
pre_tool_call 스냅샷에 대해 생성된 번들을 실행하고, deny가 작동하는지 증명해야 합니다. 녹색(green) 마이그레이션 보고서만으로는 충분한 증거가 아닙니다.
- 어떤 버전이든 상관없이:
default_action: deny를 선호하고,