사내 코드 리뷰 자동화를 운영하며 겪은 일을 비식별화해 정리했다. 저장소, 채널, 계정, Pull Request 번호는 생략하고 구조와 교훈만 남겼다.

TL;DR

  • 터미널에서 로그인이 성공했다고 자동화도 인증된 것은 아니다.
  • 인증은 계정의 상태가 아니라 실제로 명령을 실행하는 환경의 상태다.
  • 샌드박스 실패를 곧바로 인증 만료로 판단하면 정상 자동화까지 멈출 수 있다.
  • 자동화는 작업 전에 같은 실행 경로로 권한을 검증하고, 실패와 후보 없음도 구분해야 한다.
  • 권한을 넓히는 것보다 검증 경로와 실패 규칙을 명시하는 것이 먼저다.

자동 리뷰가 갑자기 멈췄다

코드 리뷰 요청을 주기적으로 확인하는 자동화를 운영하고 있다. 협업 채널에서 요청을 찾고, Pull Request의 현재 상태와 변경 내용을 읽고, 리뷰를 남긴 뒤 원래 요청이 있던 스레드에 완료 여부를 알리는 흐름이다.

그날 자동화는 협업 채널까지는 정상적으로 읽었다. 하지만 GitHub 상태를 확인하기 직전에 멈췄다.

no oauth token found
token is invalid

이상한 점은 내 터미널에서는 로그인이 정상이었다는 것이다. 인증 래퍼를 실행해도 계정과 저장소 접근 권한이 확인됐다. 조금 전까지 같은 계정으로 Pull Request도 보고 있었다.

처음에는 인증 정보가 만료됐거나 앱을 재시작하면서 환경 변수가 사라졌다고 생각했다. 다시 로그인하고, 앱을 재시작하고, 인증 상태를 또 확인했다. 그런데 결과는 계속 엇갈렸다.

  • 사용자 터미널: 성공
  • 자동화의 샌드박스 셸: 실패
  • 사용자 승인을 거친 샌드박스 밖 실행 환경: 성공

계정 권한이 풀린 게 아니었다. 같은 컴퓨터 안에서도 명령이 실행되는 환경마다 보이는 인증 정보가 달랐다.

로그인은 하나지만 인증 경로는 여러 개였다

내가 사용하는 환경에는 GitHub에 접근하는 경로가 하나가 아니었다.

  1. 사용자가 직접 여는 터미널
  2. Codex가 명령을 실행하는 샌드박스
  3. macOS keychain에서 토큰을 읽어 주입하는 래퍼
  4. 별도로 인증된 GitHub 커넥터

화면에서는 모두 같은 GitHub 계정을 사용하는 것처럼 보인다. 하지만 실제로는 서로 다른 프로세스와 저장소에서 인증 정보를 읽는다.

사용자 터미널의 gh auth login이 성공해도 이미 실행 중인 자동화 프로세스에 환경 변수가 전달된다는 보장은 없다. 커넥터가 GitHub를 읽을 수 있어도 로컬 gh 명령이 같은 인증을 공유하는 것도 아니다. 샌드박스에서는 macOS keychain 접근 자체가 차단될 수 있다.

따라서 다음 두 문장은 같은 의미가 아니다.

내 GitHub 로그인이 정상이다.
자동 리뷰가 GitHub에 접근할 수 있다.

자동화에서 중요한 건 첫 번째 문장이 아니라 두 번째 문장이다.

문제는 실패보다 오판이었다

샌드박스에서 인증을 읽지 못한 것 자체는 이상한 일이 아니다. 격리 환경이라면 오히려 자연스러운 제한일 수 있다. 진짜 문제는 자동화가 이 결과를 계정 인증 전체의 실패로 해석했다는 데 있었다.

더 위험한 경우도 있다. 인증 실패나 API 조회 실패를 후보가 없는 것으로 취급하면 자동화는 조용히 끝난다.

리뷰 요청이 없음

리뷰 요청을 확인할 권한이 없음

은 전혀 다른 상태다. 전자는 정상적인 무작업이고, 후자는 관측 실패다. 둘을 구분하지 않으면 요청은 남아 있는데 자동화만 성공한 것처럼 보일 수 있다.

이번에는 안전하게 중단했지만, 인증 실패를 늦게 알렸고 처음에는 잘못된 원인을 안내했다. 자동화를 멈추지 않게 만드는 것뿐 아니라 왜 멈췄는지 정확히 분류하는 규칙도 필요했다.

실행 전에 같은 경로로 검증한다

규칙을 다음처럼 고쳤다.

1. GitHub 작업에 사용할 바로 그 실행 경로로 인증을 검증한다.
2. 샌드박스에서 keychain 접근 때문에 실패하면 사용자 승인을 거친 실행 환경에서 한 번 재검증한다.
3. 두 환경에서 모두 실패할 때만 실제 인증 실패로 판정한다.
4. 인증 실패를 후보 없음으로 처리하지 않는다.
5. 실패하면 외부에 아무것도 쓰지 않고, 진행 상태도 전진시키지 않는다.
6. 어느 환경에서 무엇이 실패했는지 즉시 알린다.

의사 코드로 쓰면 단순하다.

const sandboxAuth = await verifyGitHubAuth({ mode: 'sandbox' });

const auth = sandboxAuth.ok
  ? sandboxAuth
  : await verifyGitHubAuth({ mode: 'approved-runtime' });

if (!auth.ok) {
  return {
    status: 'blocked',
    reason: 'github-auth-unavailable',
    advanceCursor: false,
    publish: false,
  };
}

return runReviewPipeline();

핵심은 자동으로 권한 상승을 시도하는 것이 아니다. 자동화가 허용받은 경계 안에서만 재검증하고, 허용되지 않은 작업은 명확하게 멈춘다. 편의를 위해 샌드박스를 없애거나 토큰을 광범위하게 노출하는 것은 해결책이 아니다.

auth status만으로 충분하지 않았다

인증 도구가 저장된 계정을 보여주는 것과 실제 API 요청이 성공하는 것도 다를 수 있다. 그래서 상태 출력만 믿지 않고, 리뷰에 사용할 동일한 래퍼를 통해 최소 권한 API를 호출해 보는 편이 낫다.

./github-wrapper.sh api user --jq .login
./github-wrapper.sh pr view <number> --repo <owner/repository>

첫 명령은 어떤 계정으로 요청하는지 확인하고, 두 번째 명령은 실제 대상 저장소를 읽을 수 있는지 확인한다. 리뷰 게시 권한까지 필요하다면 읽기 성공과 쓰기 권한을 별도로 구분해야 한다.

토큰이 있다는 사실, 계정이 표시된다는 사실, 대상 작업을 수행할 수 있다는 사실은 서로 다른 검증 항목이다.

복구 뒤에 확인한 것

규칙을 바꾼 뒤 자동화는 다음 실행에서 정상적으로 동작했다.

  • 협업 채널에서 리뷰 후보 탐색
  • 현재 Pull Request와 commit 재확인
  • 변경 내용 재리뷰
  • GitHub 리뷰 게시
  • 원본 스레드에 결과 반영

중요한 건 “다시 됐다”가 아니다. 어떤 실행 경로에서 왜 실패했고, 어떤 조건을 확인한 뒤 다시 진행했는지 설명할 수 있게 됐다는 점이다.

복구가 우연한 재시작에 기대면 같은 문제가 다시 생겼을 때 또 처음부터 추측해야 한다. 실패 조건과 확인 명령을 규칙에 남기면 다음 실행은 추측이 아니라 판정으로 시작할 수 있다.

자동화의 권한도 코드처럼 다뤄야 한다

에이전트 자동화를 만들 때는 보통 프롬프트와 작업 로직에 먼저 집중한다.

  • 어떤 요청을 후보로 볼 것인가
  • 어떤 변경을 문제로 판단할 것인가
  • 언제 승인하고 언제 중단할 것인가
  • 어디에 결과를 남길 것인가

하지만 이 모든 로직 앞에는 더 기본적인 전제가 있다.

지금 이 자동화가 필요한 정보를 읽고 결과를 쓸 수 있는가?

이 전제를 암묵적인 환경 설정으로 두면 자동화는 어느 날 갑자기 멈춘다. 더 나쁘게는 일부 단계만 성공한 채 완료된 것처럼 보일 수 있다.

그래서 권한도 코드와 비슷하게 다뤄야 한다.

  • 명시한다. 필요한 시스템과 읽기·쓰기 범위를 구분한다.
  • 실행 전에 검증한다. 실제 작업과 같은 프로세스와 경로를 사용한다.
  • 실패를 분류한다. 무작업, 조회 실패, 인증 실패, 권한 부족을 나눈다.
  • 안전하게 멈춘다. 실패한 상태에서 cursor나 완료 상태를 전진시키지 않는다.
  • 복구를 검증한다. 로그인 화면이 아니라 실제 최소 API 호출로 확인한다.
  • 감사 가능하게 남긴다. 어느 환경에서 어떤 검증이 실패했는지 기록한다.

마치며

그날 자동 리뷰의 코드는 멀쩡했다. GitHub 계정도 멀쩡했다. 문제는 둘 사이에 있는 실행 환경이었다.

사람에게는 “이 컴퓨터에서 로그인했다”가 충분할 수 있다. 하지만 자동화에는 어느 프로세스가, 어떤 격리 수준에서, 어느 인증 저장소를 읽어, 어떤 명령을 실행하는지까지가 권한의 일부다.

자동화를 오래 운영하려면 작업 규칙만 정교하게 만들 수 없다. 그 규칙을 실행할 수 있는지 확인하는 규칙도 함께 있어야 한다.

이번에 고친 건 리뷰 로직이 아니라 그 앞의 한 단계였다.

나는 로그인되어 있는가?

가 아니라,

이 자동화는 지금 허용된 경로로 필요한 작업을 수행할 수 있는가?

를 묻도록 바꾼 것이다.