개인 프로젝트로 여러 AI 에이전트를 결합한 시스템을 만들고 있습니다. 정보 수집, 자료 준비, 팟캐스트 제작은 물론, 워크플로우 자체의 개선 감사(audit)까지도 하나의 워크플로우로 구동하고 있습니다.
2026년 8월 말경, 이 기반 위에서 서브 Codex를 구동하는 부분에 막혔습니다. 구현 에이전트를 수정하던 중이었는데, 단순히 업무 절차를 조정하는 것만으로는 해결되지 않았고, 결국 Runner Launcher라는 실행 창구(launch window)를 추가하여 실행 경로 자체를 재구축했습니다.
작업을 배분하는 Dispatcher와 실제 실행을 담당하는 Runner 사이에, 실행 환경을 선택하는 창구를 둔 것입니다. 서브 Codex를 사용하는 Job(실행 단위)은 사용자가 명시적으로 Host를 구동하는 경로로 보내고, 스크립트만으로 완결되는 Job은 기존의 실행 환경으로 분리했습니다. 이는 서브 에이전트에 대한 지시사항을 수정하기 전에, 먼저 구동 조건을 통일할 필요가 있었기 때문입니다.
본 글에서는 2026년 8월 말부터 9월 초까지 설계했던 과정과 기존 워크플로우를 재정비한 기록을 정리했습니다. 대상은 당시의 Windows 환경 사례이며, 현재 Codex 전반에 걸친 제약 사항이나 모든 환경에서 동일하게 해결할 수 있음을 의미하지는 않습니다.
당시 확인되었던 상황은 다음과 같았습니다.
- Windows Sandbox 내에서 서브 Codex를 구동하면 모델과의 통신에 실패합니다.
- Codex Desktop에서 사용자가 명시적으로 Escalated Execution을 사용하여 Host 측의 실행 주체로부터 기존 Runner를 구동할 경우, Codex SDK와 출력 검증까지 동작합니다.
- 반면, 외부 프로그램에서 Desktop의 권한 상승(escalation)을 요청하는 공식 API는 확인되지 않았습니다.
여기서 말하는 Host 측은 Sandbox 내부의 실행 주체와 별개로, 사용자가 명시적인 조작으로 구동시킨 실행 주체를 의미합니다. Launcher가 자동으로 권한을 높여주는 것은 아닙니다.
이러한 조건 하에서는 Job에 대한 설명을 자세히 하거나 서브 에이전트에 대한 지시사항을 바꿔도, 통신을 시작하는 환경의 문제는 남아 있었습니다.
또한, 'Host에서 동작했다'는 확인만으로 일반적인 실행 경로 전체를 Host로 옮겨도 좋다는 의미는 아닙니다. 스크립트만으로 완결되는 Job도 존재하며, 승인이 필요한 실행과 구분할 필요가 있습니다.
이에 저는 Job을 선택하여 전달하는 Dispatcher와 Job을 실제로 실행하는 Runner 사이에, 구동을 위한 경계(boundary)를 두기로 했습니다.
새로운 구성에서는 일반적인 Fixed/Dynamic Job을 구동할 때, Dispatcher는 Runner를 직접 구동하지 않습니다. 대신 Runner Launcher에게 최소한의 정보를 전달합니다.
Launcher는 Job의 실행 유형을 보고 구동 경로를 분리합니다.
script:
기존처럼 현재 실행 환경에서 기존 Runner를 구동합니다.
codex:
필요한 승인 대상을 고정하고, 사용자 측의 명시적인 Host 구동을 기다립니다.
여기서 중요한 점은 Launcher가 업무 처리를 담당하지 않는다는 것입니다. 결과물 생성이나 출력 검증은 기존 Runner 그대로 진행됩니다. Launcher는 '무엇을 만들지'를 판단하는 것이 아니라, '이 Job을 어떤 경로로 구동할지'만 처리합니다.
최초 마이그레이션 단계에서는, 팟캐스트 공개와 같은 전용 외부 Runner는 예외적으로 남겨두었습니다. 이는 기존의 승인 절차나 외부 접근 계약을 그 자리에서 일괄 변경하지 않기 위함입니다. 진입점을 정리하는 것과 모든 실행 경로를 한 번에 통합하는 것은 별개의 문제였습니다.
당시 사양으로는, Codex Job을 바로 실행 대기열(Queue)에 넣는 것이 아니라, Host 구동이 필요한 단계에서 사용자에게 입력을 기다리도록 했습니다.
Run(일련의 처리 전체) 상태는 기존의 waiting_for_input을 사용하고, 자체적인 상태를 늘리지 않았습니다. 이 대기 중에는 해당 Run의 Queue를 pending 0 / leased 0으로 설정하여, 실행 대기 중이거나 실행 담당자가 할당된 Job은 남기지 않았습니다.
이는 사소해 보이는 조건이지만, 승인 대기 처리 건을 실행 가능한 작업으로 남겨두지 않기 위해 필요했습니다. 사람의 조작이 필요한데 Queue에는 움직일 수 있는 Job이 있다는 상태는 재시도나 다른 Runner가 이를 가져갔을 때 의미가 모호해집니다.
사용자가 취소한 경우에는 입력 응답을 기록하고 Run을 종료합니다. 승인 대기 상태로 처리를 계속 진행하거나, 실패한 경로로 임의로 되돌아가지 않습니다.
수동으로 구동하더라도 매번 자유로운 명령어를 조합하는 방식은 채택하지 않았습니다.
Launcher는 대상 Job, 승인 요청, 실행 정책, Runner, Host Launcher 등의 참조와 SHA-256 값을 가진 일회성 구동 티켓(launch ticket)을 생성합니다. Host Launcher가 받는 진입점은 오직 그 티켓뿐입니다.
Host 측에서는 실제 실행 주체, 티켓 배치 위치, 대상 Run의 상태, 참조 및 해시(hash), 고정 정책과의 일치 여부를 재확인합니다. 티켓은 배타적으로 클레임되어 동일한 티켓에서 실행이 중복 등록되는 것을 방지합니다.
여기서 말하는 해시는 프로그램 자체의 안전성을 증명하는 것은 아닙니다. 승인된 내용과 실제로 구동하려는 내용이 불일치하지 않도록 하는 대조(照合)입니다. 고정 정책의 생성 및 업데이트 또한, 일반적인 Run이 임의로 수행하는 처리가 아니었습니다.
당시의 흐름은 티켓을 생성하여 입력 대기 상태에 진입하고, 이용자가 고정 Host Launcher를 실행하며, Host 측 검증과 승인을 거쳐 새로운 실행 Job을 등록하는 순서였습니다. 그 후에 기존 Runner가 Job을 받아 일반적인 실행으로 진행합니다.
이러한 '승인 후 별도의 실행 Job 생성' 방식은 9월 9일 자 계약 재검토 사양서에서, 승인 후에도 동일한 Job을 사용하는 방식으로 변경되었습니다. 여기서 설명하는 것은 Launcher 도입 당시의 설계입니다.
또 하나 결정된 사항은, 채팅에서 단순히 'approve'라고 회신한다고 해서 Host 구동에 대한 승인 상태가 되는 것이 아니라는 점입니다.
당시 경로에서는 이용자가 표시된 고정 Host Launcher를 Desktop의 Escalated Execution을 통해 명시적으로 실행합니다. 그 Host 측 검증 결과를 바탕으로 승인과 실행을 연결합니다. 일반적인 채팅에서는 취소(cancel)를 선택할 수 있지만, Host 구동을 대신하는 것은 허용하지 않았습니다.
이는 단순히 채팅상의 회답만으로 Host에서의 검증이 완료된 것으로 간주하지 않기 위함입니다.
또한 자동 폴백(automatic fallback)도 하지 않습니다. Host 구동에 실패했다고 해서 Sandbox 측에서 동일한 Codex Job을 재실행하도록 설계하지 않았습니다. 동작 조건을 변경하려면, 그 조건에 맞는 승인과 검증이 필요하기 때문입니다.
이번 개수(改修)를 통해 정리하고자 했던 것은 단순히 서브 에이전트를 구동하는 방법이 아니라, 실패가 발생한 지점을 구분할 수 있는 구조였습니다.
Job 내용에 문제가 있는지, 모델 입력에 문제가 있는지, 아니면 그 이전에 실행 환경에서 멈춘 것인지. 이것들을 모두 같은 '에이전트 실패'로 묶어버리면, 업무 지시만 계속 고치게 됩니다.
이번에 Launcher에 모은 것은 실행 환경의 선택과 구동 조건입니다. 업무 처리를 담당하는 Runner와 경계를 설정함으로써, 어디를 수정해야 하는지, 어디에서 사람의 조작을 기다려야 하는지를 설명하기 쉬워졌습니다.
멀티 에이전트 시스템에서는 에이전트를 늘리는 부분에 시선이 쏠리기 쉽습니다. 하지만 증가한 업무를 어떤 실행 주체로, 어떤 승인을 기반으로 시작할 것인지 역시 그에 못지않게 중요한 기반 설계였습니다.