allowed_agent_ids 가 worker_agent_ids 가 되기까지
A2A 오케스트레이터의 워커 목록으로 호출까지 막았다가 사흘 만에 뺐다. 고객사 요구에 미리 대비한 통제였는데, 외부에서도 부를 수 있어야 한다는 지금의 요구와 맞지 않았다.
실행 엔진의 에이전트들이 A2A 로 서로를 부를 수 있게 붙이면서, 오케스트레이터 에이전트마다 쓸 워커 목록을 두었다. 필드 이름은 처음에 allowed_agent_ids 였고 지금은 worker_agent_ids 다. 이름이 바뀌기까지 사흘이 걸렸고, 그사이 이 목록이 하는 일은 두 가지에서 하나로 줄었다.
| 목록이 하는 일 | 9월 18일 | 9월 21일 이후 |
|---|---|---|
| 카탈로그를 그 오케스트레이터의 워커로 좁힌다 | 한다 | 한다 |
| 목록 밖 워커를 부르면 거절한다 | 한다 | 안 한다 |
처음에는 호출까지 막았다
9월 18일 아침, 전날 리드와 논의한 내용을 에이전트에게 가져갔다.
오케스트레이터 타입으로 만들면, 어떤 a2a 에이전트 카드들만 이 오케스트레이터에서는 사용하겠다라고 콘솔에서 등록해주면(신규), 이 오케스트레이터는 해당 에이전트 카드들만 정보를 볼수 있는 레지스트리에 접근 가능하도록 하면 좋겠다는거거든. 그게 가능할까? 일단 a2a 기능을 이번 첫 구현에서는 사내만 사용하는걸로 논의도 했고..나중에 외부a2a 연결은 다음 버전에서 가기로 했는데...
요구는 카탈로그 쪽이었다. 오케스트레이터가 카탈로그를 읽을 때 자기 목록에 있는 워커의 카드만 받게 하면 된다.
에이전트는 여기에 하나를 더 얹자고 했다.
카탈로그만 걸러 두면 '안 보일 뿐' 부를 수는 있어서 통제가 아닙니다. 검사는 Redis 조회 한 번이라 비용이 없습니다.
헤더로 호출한 쪽을 믿는 건 사내 전용일 때만 성립하고, 외부를 여는 다음 버전에서는 토큰 클레임으로 바꿔야 한다는 단서도 붙어 있었다. 나는 받아들였다. 받는 서버가 누가 불렀는지 알아야 이 호출을 허용할지 거절할지 정할 수 있으니 당연히 있어야 한다고 생각했다. 나중에 어느 고객사가 오케스트레이터별로 서로 접근하면 안 되는 워커를 설정해 달라고 할 경우에도 미리 대비해 두는 셈이었다. 그렇게 해서 설계 문서 네 곳에 "목록에서 안 보이는 것과 못 부르는 것이 일치해야 통제다"라는 문장이 들어갔다. 에이전트가 쓴 문장이다. 그날 PoC 에서 다른 오케스트레이터 id 로는 카탈로그가 비어 있고, 목록 밖의 워커를 직접 부르면 TaskNotFound 가 나는 것까지 확인했다.
호출한 오케스트레이터가 누구인지는 orch-agent-id 헤더로 알았다. 사내에서는 대화 서비스가 멀티에이전트 호출 때 이미 붙여 보내는 헤더라 따로 할 일이 없었다.
없애 달라는 요청
9월 21일 오후, 리드가 A2A 호출 파라미터에서 orch-agent-id 를 없애 달라고 했다. 오케스트레이터가 이미 자기가 부를 수 있는 워커를 목록으로 들고 있는데 이 헤더가 왜 굳이 필요하냐는 것이었다. 나는 있어야 한다고 주장했다. 누가 불렀는지 모르면 받는 쪽은 허용도 거절도 할 수 없다.
그래서 리드에게 없애려는 근본적인 이유를 물었다. 답은 범용성이었다. 나중에 우리 시스템이 아닌 외부에서 우리 A2A 를 부르게 될 때, 내부 시스템에서만 쓰는 파라미터를 필수로 두면 문제가 된다. 외부 클라이언트는 그 헤더가 무엇인지 모르고, 보낼 방법도 없다. 수긍했고, 에이전트에게 이렇게 설명했다.
orch-agent-id 이게 있으면, 외부에서 우리 a2a 로 에이전트들을 부를때, 호출할수가 없게되니까... 외부사람들은 '저 인자가 도대체 뭐야' 이럴수도 있고.. 실제 호출도 안될테니까... 그래서 외부에서 호출가능하도록 하려는 의도거든. 실제 우리 게이트웨이 서비스쪽에서 인증다 마치고 온다고 가정하고 우리는 처리를 해주는거라..
에이전트는 헤더를 선택 항목으로 남기는 안과 호출 검사를 통째로 빼는 안 두 가지를 내놨고, 이 검사가 기능 통제이지 보안 경계는 아니라고 정리했다.
2번으로 가자. 그러면 allowed_agent_ids 이것도 이름을 바꿔야맞겠네?
검증까지 받을 필요는 없다고 봤다
외부에서 우리 A2A 를 부른다면 게이트웨이에서 인증을 다 받고 들어온다. 그 부분은 안전하다고 간주했다. 그러면 서비스 안에서 이 목록의 주된 목적은 오케스트레이터가 자기가 부를 워커의 범위를 아는 것이고, 등록된 오케스트레이터가 아닌 호출을 서버가 검증해서 거절하는 것까지는 과하다고 판단했다.
처음에 호출까지 막은 건, 어느 고객사가 오케스트레이터별로 워커 접근을 나눠 달라고 할 경우에 대한 대비였다. 아직 실재하지 않는 요구사항 때문에 지금의 요구사항을 무시할 수는 없었다.
이름은 worker_agent_ids 가 됐다. 이제 이 목록은 허용 목록이 아니라 오케스트레이터가 도구로 얹을 워커 목록이다. 옛 이름으로 저장된 레코드도 그대로 읽히게 해서, 개발 클러스터에 등록해 둔 것들은 옮기지 않고 동작했다.
헤더는 선택 항목으로 남아 있었다
다음 날 아침에 이것도 물었다.
궁금한게 orch-agent-id 를
/a2a/agents/{agent_id}/jsonrpc이 api 에서 안받는게 아니라 옵션으로 돌린이유가 있어? 내부적으로는 필요해서 그런건가?
에이전트는 내부에서 필요해서가 아니라 자기가 덜 걷어낸 잔재라고 했다. 전날 방향을 정하는 도중에 헤더를 선택 항목으로 바꾸는 편집부터 시작했는데, 호출 검사를 통째로 빼기로 정한 뒤에도 그 부분이 남아 있었다.
이참에 헤더 규칙을 다시 정했다. 위에서 내려온 사내 헤더는 A2A 엔드포인트가 해석하지 않고 워커로 넘긴다. 바꾸거나 빼는 건 요청 ID 와 추적 체인, 그리고 호출자 자신의 실행 상태를 담은 헤더 몇 개다. 요청 ID 를 그대로 물려줬다가 스트림이 꼬이는 일은 이 엔진에서 이미 겪었고, 이번 PoC 첫날에도 또 겪었다. 그리고 다른 서비스 작업자가 스웨거만 보고도 알 수 있게 헤더 규칙을 표준대로 적어 두라고 했다. 지금 orch-agent-id 를 읽는 곳은 카탈로그 하나다.
이름을 바꾼 비용도 조금 있었다. 워커를 등록하던 다른 에이전트 세션이 보낼 때는 allowed_agent_ids 인데 저장된 레코드에는 worker_agent_ids 가 있어서 헷갈려 했고, 등록 API 가 전체 교체라서 목록만 바꾸려다 기존 스킬 설명을 날릴 뻔했다. 그 뒤로 등록 스크립트는 레코드를 먼저 읽어 목록만 바꿔 보내고, 읽은 스킬이 비어 있으면 멈추게 했다.
그럼 통제는 어디에 있나
목록으로 호출을 막지 않으니, 카탈로그에 안 보이는 워커도 id 만 알면 부를 수 있다. 사내 호출은 클러스터 안이라 헤더를 믿고, 외부 호출은 게이트웨이가 인증을 마치고 사용자 id 를 넣어 준다는 전제다. 실행 엔진은 요청이 안에서 왔는지 밖에서 왔는지 구분하지 못한다. 그래서 게이트웨이가 할 일을 스웨거에 적어 뒀다. 인증한 뒤 사용자 id 를 넣고, 클라이언트가 보낸 사내 헤더는 전부 벗긴다. 안 벗기면 외부 클라이언트가 남의 대화 기록 id 를 실어 보내 워커 실행을 그 기록에 붙일 수 있다.
노드에 커서를 올리면 연결과 설명이 나타납니다.
카드를 등록하고 고치는 API 도 이 전제에 맞춰 옮겨졌다. 처음에는 호출 API 와 같은 /a2a 아래에 있어서, 게이트웨이가 /a2a/* 를 통째로 붙이면 인증 없는 변경 API 까지 딸려 나가게 되어 있었다. 코드 리뷰를 거치면서 /a2a-registry, /internal/a2a-registry 를 지나 /_a2a 로 갔다. /a2a-registry 는 nginx 나 Envoy 처럼 경로를 문자열 접두어로 비교하는 곳에서 /a2a 에 걸리고, internal 은 고객사가 에이전트 이름으로 쓰고 있을 수 있는 흔한 단어라서였다. 에이전트 이름이 경로를 가리는 문제는 Mount 접두어 매칭 때 이미 겪었다.
게이트웨이가 사내 헤더를 벗기는 일은 지금 스웨거에 적힌 요구사항이다. 외부 호출을 여는 날 외부 클라이언트에서 사내 헤더를 실어 불러 보면 제대로 벗기는지 알 수 있을 것 같다.
끝