Codex가 서브 에이전트 프롬프트를 암호화했습니다. 생성 계획을 제어하세요.
요약
Codex의 업데이트로 인해 서브 에이전트에게 전달되는 프롬프트가 암호화되면서, 사후 작업 감사 추적(audit trail)이 불가능해진 현상을 분석합니다. 저자는 사후 검사 대신 핸드오프 직전의 권한을 검증하는 '사전 파견 권한 부여(Pre-dispatch authorization)' 방식의 중요성을 강조합니다.
핵심 포인트
- Codex의 변경 사항으로 인해 서브 에이전트의 작업 기록이 암호문으로 변해 가독성이 상실됨
- 사후 검사(Post-hoc inspection)는 통제가 아닌 단순 추적(tracking)의 문제임
- 핸드오프 직전 평문 상태에서 권한을 검증하는 '사전 파견 권한 부여'가 필요함
- 오케스트레이터가 페이로드를 암호화하면 인간의 사후 관찰이 차단됨
**AI 서브 에이전트(sub-agents)를 위한 사전 파견 권한 부여(Pre-dispatch authorization)**란, 사후에 트레이스(trace)를 읽는 방식이 아니라, 핸드오프(handoff) 직전 마지막 평문(plaintext) 시점에 자식 생성물(child spawn)의 권한 봉투(grant envelope)(역할, 도구, 경로 범위 및 토큰 예산)를 부모의 정책과 대조하여 확인하는 것을 의미합니다. 오케스트레이터(orchestrator)가 핸드오프를 암호화하면, 사후 관점(after-view)은 보이지 않게 됩니다. 하지만 사전 관점(before-view)은 타임라인상 더 앞선 시점에 위치하므로 보이지 않는 상태가 되지 않습니다.
7월 14일, "Codex가 서브 에이전트 프롬프트를 암호화하기 시작했다"는 소식은 Hacker News에서 하루 만에 408개의 추천과 240개의 댓글을 기록했습니다. 이 현상의 배후에 있는 추적 버그는 openai/codex#28058로 등록되어 있으며, 제목은 _"회귀(Regression): 암호화된 MultiAgentV2 메시지가 읽을 수 있는 작업 감사 추적(task audit trail)을 제거함"_입니다. 한 변경 사항이 오케스트레이터에서 서브 에이전트로 전달되는 페이로드(payload)를 암호화했고, 인간이 사후에 읽을 수 있었던 평문 작업 기록이 암호문(ciphertext)으로 변했습니다. 파견 후 서브 에이전트에게 무엇이 지시되었는지 검사하던 사람들은 더 이상 이를 읽을 수 없게 되었습니다.
이는 인지해야 할 중요한 사항이며, 평문을 다시 돌려달라고 요청함으로써 해결하려 한다면 더 나쁜 상황이 될 것입니다.
AI 공개. 저는 AI 어시스턴트와 함께
subagent_dispatch_gate.py를 작성하고 직접 실행했습니다: Python 3.13.5, 오프라인, 표준 라이브러리만 사용, 네트워크 없음, 키 없음, 자금 없음. 아래의 모든 숫자, 종료 코드 및 sha256은 실제 로컬 실행에서 복사한 것입니다. 데모 전체를 두 번 실행했으며, 두 개의output.txt파일은 바이트 단위로 동일합니다 (sha256 5af48191642d66f7c364c429c50d2ad1a021f09004f5566ba878c7be87fcaaf1). 유일한 합성(synthetic) 부분은 명확히 표시되어 있습니다:encrypt_artifact()는 Codex의 실제 암호화가 아니라 암호화의 관찰 가능한 결과(observable consequence)(필드로 다시 파싱할 수 없는 불투명한 바이트)를 모델링합니다. 그리고 여기에 언급된 Codex에 관한 모든 사실은 해당 HN 스레드와 해당 이슈에서 가져온 것입니다. 저는 그들의 시스템을 재현하지 않았습니다.
요약하자면:
요약하자면:
- 제기된 문제(issue)들은 읽을 수 있는 감사 사본(readable audit copy) 복원을 요구합니다. 이는 _추적(tracking)_을 복원할 뿐입니다. 이는 _통제(control)_를 복원하는 것은 아니며, 저는 이 두 가지가 스레드에서 혼동되었다고 생각합니다.
- 사후 검사(Post-hoc inspection)는 변경되기 전날 실제로 작동했습니다. 제 대조 실행(contrast run)에서는 아티팩트가 평문(plaintext)일 때 4개의 필드를 가진 계획을 복구했지만, 아티팩트가 불투명한 블롭(opaque blob)이 되었을 때는 0개 필드였습니다. 유일한 변수는 암호화 여부입니다. 가짜 시나리오(strawman)는 아닙니다.
- 디스패치 전 게이트(pre-dispatch gate)는 부모가 자식 에이전트에게 작업을 넘기기 전에, 부모가 아직 처리 중인 계획을 읽습니다. 부모는 스폰 계획(spawn plan)을 보유합니다: 각 자식에 대해 역할(role), 도구 권한 집합(set of tool grants), 접근할 수 있는 경로 집합(set of paths it may touch), 그리고 토큰 예산(token budget)이 있습니다. 디스패치 전 승인(Pre-dispatch authorization)은 이 모든 것을 부모가 이미 소유하고 있는 정책과 비교하며, 어떤 자식이 정책이 허용하는 것보다 더 많이 요청하면 스폰을 거부합니다. 이는 계획이 여전히 메모리 내의 일반 객체일 때 발생합니다. 아무것도 전송되지 않았고, 암호화되지 않았으며, 실행된 것도 없습니다.
AI 서브 에이전트를 위한 디스패치 전 승인(pre-dispatch authorization)이란 무엇인가요?
이는 부모가 자식 에이전트에게 작업을 넘기기 전에 부모 측에서 실행되는 검사입니다. 부모는 스폰 계획을 보유합니다: 각 자식에 대해 역할, 도구 권한 집합, 접근할 수 있는 경로 집합, 그리고 토큰 예산이 있습니다. 디스패치 전 승인은 이 모든 것을 부모가 이미 소유하고 있는 정책과 비교하며, 어떤 자식이 정책이 허용하는 것보다 더 많이 요청하면 스폰을 거부합니다. 이는 계획이 여전히 메모리 내의 일반 객체일 때 발생합니다. 아무것도 전송되지 않았고, 암호화되지 않았으며, 실행된 것도 없습니다.
중요한 차이점은 이것입니다: 이것은 _액션 (action)_에 대한 승인이 아니라, _스폰 (spawn, 생성)_에 대한 승인이며, _트레이스 (trace, 추적)_의 화해(reconciliation)가 아닙니다. 이는 AI 에이전트를 위한 실행 전 게이트 (pre-execution gate for AI agents)와 같은 계열이지만, 한 단계 상위 수준으로 이동하여 "이 액션이 실행되어도 되는가"에서 "이 자식(child)이 이러한 권한을 가지고 존재해도 되는가"로 바뀐 것입니다.
사건: 감사 추적(audit trail)이 암전되었고, 스레드는 이를 되돌려달라고 요청했습니다
이슈 #28058을 원문 그대로 읽어보십시오. 하나의 PR(Pull Request)이 MultiAgentV2 메시지 페이로드(payload)에 암호화를 추가했습니다. 의도는 합리적이었습니다. 오케스트레이터(orchestrator)와 서브 에이전트(sub-agent) 사이의 모델 대면 채널이 디스크 상에 평문(plaintext) 상태로 방치되어서는 안 된다는 것이었습니다. 부작용은 사람이 어떤 작업이 어떤 서브 에이전트에게 위임되었는지 검토하기 위해 열어보는 로컬 롤아웃 히스토리(local rollout history)를 읽을 수 없게 되었다는 점입니다. 해당 이슈의 요청 사항은 암호화된 전달 방식과 병행하여 사람이 읽을 수 있는 감사 사본(audit copy)을 유지해달라는 것입니다.
그 요청이 무엇인지 이해합니다. 하지만 저는 그것이 조용히 게임을 포기하는 것이라고 생각합니다. 만약 서브 에이전트가 무엇을 할 수 있는지에 대한 당신의 통제권이 사후에 기록(transcript)을 읽을 수 있는 능력에 달려 있다면, 당신의 통제권은 언제나 작업의 하류(downstream)에 있었던 것입니다. 서브 에이전트는 이미 실행되었습니다. 토큰은 이미 소모되었습니다. shell.run은 이미 발생했습니다. 읽을 수 있는 사본을 복구하는 것은 더 나은 사후 분석(postmortem)을 작성할 수 있게 해줄 뿐입니다. 그것은 다음 스폰(spawn)을 막지는 못합니다.
이것은 제가 이 클러스터 전체를 관통하며 계속해서 되돌아오는 문장입니다: 추적(tracking)은 통제(control)가 아닙니다. 트레이스(trace)는 무슨 일이 일어났는지 알려줍니다. 무엇이 허용되는지를 결정하지는 않습니다. 트레이스가 평문일 때는 이 둘이 같다고 가장할 수 있습니다. 언제든 가서 읽고 개입할 수 있기 때문입니다. 암호화는 이러한 가식을 깨뜨립니다. 암호화는 사후 검토(after-view)를 빼앗아 가며 다음과 같은 질문을 강요합니다: 만약 당신이 트레이스를 전혀 읽을 수 없다면, 당신의 통제권은 실제로 어디에 존재하겠습니까?
왜 사후(post-hoc) 방식은 죽고, 사전 배포(pre-dispatch) 방식은 살아남는가?
코드로 실행해 본 그 질문에 대한 솔직한 버전입니다. 동일한 스폰, 두 개의 세계, 하나의 변수입니다.
def encrypt_artifact(plan_obj):
"""오케스트레이터(orchestrator)가 암호화한 후의 휴지 상태(at-rest) / 핸드오프(handoff) 아티팩트를 모델링합니다.
실제 암호화는 아닙니다: 관찰 가능한 결과, 즉 읽는 이가 읽을 수 없는 바이트를 모델링합니다.
...```
`posthoc_inspect`의 핵심은 이것이 조작되지 않았다는 점입니다. 평문(plaintext)을 전달하면 제 역할을 수행합니다. `python3 subagent_dispatch_gate.py --contrast`를 실행해 보세요:
CONTRAST: 사후 검사(post-hoc inspection) vs 배포 전 게이트(pre-dispatch gate) (동일한 스폰)
world=PLAINTEXT post-hoc: recovered plan=yes fields=4
world=ENCRYPTED post-hoc: recovered plan=no fields=0 <- 눈먼 상태 (BLIND)
...```
어제의 검사기는 4개의 필드를 복구했습니다. 오늘의 검사기는 0개를 복구합니다. 검사기의 성능이 나빠진 것이 아닙니다. 세상이 검사기 밑에서 움직여 버린 것입니다. 배포 전(pre-dispatch) 뷰는 encrypt_artifact가 손대기 전, 부모가 메모리 내에 구축한 계획을 읽기 때문에 두 세계 모두에서 동일하게 children=1을 보고합니다. 이것이 다섯 줄의 출력 결과가 보여주는 논점의 전부입니다: 암호화는 사후 뷰(after-view)에는 치명적이지만, 사전 뷰(before-view)에는 무관하며, 저는 이 결론을 도출하기 위해 사후 검사(post-hoc)의 점수를 깎아내리지 않았습니다. 계획이 읽을 수 있는 상태일 때, 그것은 정직하게 계획을 복구합니다.
사전 뷰가 존재할 수 있는 이유는 지루하지만 근본적인 이유 때문입니다: 부모는 계획을 전송하기 위해 반드시 평문(plaintext)으로 계획을 구축해야 합니다. Codex 자체의 암호화조차 전달(delivery) 및 휴지 상태(at-rest) 저장 _주변_에 위치합니다. 페이로드(payload)는 먼저 투명하게(in the clear) 조립됩니다. 모든 스폰(spawn) 시점에는 부모가 들고 있는 전체 계획이 일반 객체(plain object)인 순간이 반드시 존재합니다. 게이트(gate)는 바로 그 순간에 위치해야 합니다.
게이트: 실행 흔적(trace)이 아닌 스폰 계획(spawn plan)을 승인하라
네 가지 체크, 하나의 정책, 한 번에 하나의 자식. 특별히 영리한 것은 없습니다.
WILDCARD_PATH_MARKERS = ("~", "$HOME", "*")
WRITE_CAPS = {"fs.write", "fs.delete", "shell.run", "wallet.transfer"}
...
두 가지 설계 선택이 실질적인 역할을 수행하고 있습니다. 첫째, fs.write는 허용 목록 (allowlist)에 포함되어 있음에도 여전히 차단됩니다. 이는 fs.write가 쓰기 권한 (write capability)이며, 정책에서 allow_write=false라고 명시했기 때문입니다. 도구를 허용 목록에 넣는 것과 쓰기 권한을 부여하는 것은 서로 다른 두 가지 결정이며, 이 둘을 하나로 합치는 것이 바로 "읽기 전용 에이전트 (read-only agent)"가 조용히 "쓰기가 가능한 에이전트"로 변질되는 방식입니다. 둘째, _escapes_workspace는 모호함에 대해 기본적으로 거부 (deny-by-default)하는 방식을 취합니다. 저는 ~가 무엇으로 해석될지에 대해 영리하게 대처하려 하지 않습니다. 경로가 루트 (root) 내부에 있음이 "증명"되지 않는다면, 그것은 범위를 벗어난 것으로 간주합니다. 첫 번째 초안에서는 이 부분을 잘못 작성했습니다. 저의 와일드카드 (wildcard) 목록에 문자 그대로 /가 포함되어 있어서, 부분 문자열 (substring) 체크가 모든 절대 경로 (absolute path)를 차단했고, 정당한 PASS 피스처 (fixture)가 exit 1로 반환되었습니다. 실제 실행을 통해 이 문제를 발견했습니다. 이것이 제가 글을 쓰기 전에 반드시 실행을 먼저 해보는 유일한 이유입니다.
이 문제의 범위 (scope) 측면은 단일 API 키의 폭발 반경 (blast radius)을 점수화하는 것과 형제 격인 문제입니다. "이 권한 부여가 얼마나 넓은가"라는 동일한 축을 공유하지만, 대상 객체와 출력값이 다릅니다. 이 게이트 (gate)는 하나의 자격 증명 (credential)에 대한 0-100 점수 산출이 아니라, 전체 생성 엔벨로프 (spawn envelope)에 대한 이진 거부 (binary refusal)입니다.
60초 안에 실행하기
다음은 절대로 생성되어서는 안 되는 자식 에이전트입니다. 이를 plan_block.json으로 저장하세요:
{
"policy": {
"allowed_tools": ["fs.read", "fs.write", "http.get"],
...
home-cleaner는 확인 없이 파견해서는 안 되는 종류의 생성물입니다. 광범위한 파일 권한, 셸 (shell), 홈 디렉토리 접근 권한, 그리고 터무니없는 예산(budget)을 가지고 있습니다. 실행해 보세요:
$ python3 subagent_dispatch_gate.py plan_block.json
SUBAGENT-DISPATCH-GATE REPORT
policy: 3 tool(s) allowed, root=/repo/workspace, budget_cap=200000, allow_write=False
...
doc-reader는 동일한 계획에서 통과한다는 점에 주목하세요. 게이트는 모든 것을 차단하는 벽이 아닙니다. 이 게이트는 여섯 가지 특정 근거로 인해 하나의 자식은 거부하고, 범위가 제한된(scoped) 자식은 통과시켰습니다. 이제 역할은 그대로 유지한 채 범위 (scope)만 바꿔보겠습니다. plan_pass.json에서 home-cleaner는 /repo/workspace/task-1에 대한 fs.read 권한과 40,000 토큰의 예산을 가집니다.
$ python3 subagent_dispatch_gate.py plan_pass.json
SUBAGENT-DISPATCH-GATE 보고서
policy: 3개 도구 허용됨, root=/repo/workspace, budget_cap=200000, allow_write=False
...
Exit 1을 통해 0으로 종료하며, 두 계획 사이의 실제 차이점을 결정합니다. 만약 게이트(gate)가 두 가지 결과 모두를 생성할 수 없다면, 그것은 제어 장치가 아니라 단순한 장식에 불과합니다. 이 게이트는 거부(refusal) 시에는 afed5edb…를, 승인(authorization) 시에는 de81c4d8…를 생성하며, 두 해시(hash) 값은 실행 간에 안정적으로 유지됩니다.
왜 잘못된 입력은 '실패 폐쇄(fail closed)' 방식으로 작동하는가?
제가 가장 우려하는 실패는 조용한 실패입니다. 아무것도 전달받지 못했을 때 "모두 통과"를 반환하는 게이트는 게이트가 없는 것보다 더 나쁩니다. 왜냐하면 "확인하지 않음"을 "승인함"으로 세탁하기 때문입니다. 따라서 계획이 없거나 비어 있는 경우 exit 2를 반환하며, 그 이유를 명시합니다:
$ python3 subagent_dispatch_gate.py plan_empty.json # "plan": []
ERROR: spawn plan is empty (fail-closed: nothing to authorize is not the same as everything authorized)
exit=2
...
동일한 반사 작용이 각 자식(child) 내부에서도 작동합니다. 도구가 없다고 선언하는 spawn은 무해한 것으로 간주되어 통과되는 것이 아니라 차단됩니다. budget_tokens가 누락되었거나 정수가 아닌 경우에도 차단됩니다. 제한 없는 spawn은 루프 내에서 자식들을 조용히 분기(fork)시키고 그 모든 비용을 청구하는 주범이기 때문입니다. 비어 있는 상태는 안전한 것이 아닙니다. 비어 있는 상태는 알 수 없는 상태이며, 알 수 없는 상태는 '실패 폐쇄(fail closed)' 방식으로 처리됩니다. 이 시리즈의 이전 도구는 이를 반대로 처리하여 0바이트 입력을 통과로 간주했다가 검토 과정에서 폐기되었으며, 저는 차라리 과하게 교정하는 쪽을 택하겠습니다.
이것이 나머지 요소들과 어떻게 맞물리는가
이것은 하나의 클러스터(cluster)를 구성하는 바퀴살 중 하나이며, 여기서는 가장자리(edges)가 평소보다 더 중요합니다.
근접 이웃(near neighbor)은 트레이스(trace)를 허용된 사항과 대조하여 조정하는 인가 게이트(authz gate)입니다. 해당 게이트는 트레이스가 존재한다고 가정합니다. 이는 사후에 스팬 로그(span log)를 읽고 기록된 각 액션(action)을 정책과 대조합니다. Issue #28058은 그 가정이 무너지는 순간입니다. 페이로드(payload)를 암호화하면 대조할 트레이스가 존재하지 않게 됩니다. 따라서 제어권이 사라지는 것이 아니라, 타임라인 상의 더 앞선 시점으로 재배치됩니다. 즉, 이제는 암호문(ciphertext)이 되어버린 스팬 로그에서, 여전히 평문(plaintext) 상태인 생성 계획(spawn plan)으로 이동하는 것입니다. 만약 여러분이 사후 조정(post-hoc reconciliation)을 수행해 왔다면, 해당 아티클이 여러분이 머물던 곳이었고, 로그가 암전(dark)될 때 여러분이 가게 될 곳은 바로 이 아티클입니다.
또한 이는 lethal-trifecta 게이트와 유사한 실행 전 매니페스트 체크(pre-run manifest check)로, 동일한 매니페스트에 대해 다른 질문을 던집니다. 즉,
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기