ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Lobster의 approval 게이트 — 승인을 프롬프트로 부탁하는 것과 런타임이 멈추는 것은 다른 물건이다
    IT 2026. 9. 17. 22:00

    ▶ 동영상 개요 — Lobster의 approval 게이트 — 승인을 프롬프트로 부탁하는 것과 런타임이 멈추는 것은 다른 물건이다

    4분 57초 — approval 단계에서 멈추는 주체가 모델이 아니라 런타임이다 — 승인자 신원 검사 4케이스 중 셋이 차단됐고, 거부만은 신원 없이 통과한다. 재개 토큰은 상… — NotebookLM 동영상 개요로 생성

    🎧 오디오 개요 — Lobster의 approval 게이트 — 승인을 프롬프트로 부탁하는 것과 런타임이 멈추는 것은 다른 물건이다

    20분 26초 — approval 단계에서 멈추는 주체가 모델이 아니라 런타임이다 — 승인자 신원 검사 4케이스 중 셋이 차단됐고, 거부만은 신원 없이 통과한다. 재개 토큰은 상…. 화면은 이 글의 인포그래픽 한 장 고정 — NotebookLM 오디오 개요로 생성

    AI 에이전트에게 위험한 일을 시킬 때 다들 같은 문장을 프롬프트에 적는다. "보내기 전에 반드시 나한테 물어봐." 나도 적었고, 대체로 잘 지켜졌다. 문제는 "대체로"다. 이 문장은 부탁이지 제약이 아니다. 모델이 맥락을 조금 다르게 읽으면, 혹은 대화가 길어져 그 지시가 뒤로 밀리면, 물어보지 않고 그냥 보낸다. 그리고 메일은 되돌아오지 않는다.

    여기서 흔히 나오는 대응이 "그럼 사람이 확인하는 단계를 넣자"인데, 막상 짜 보면 생각보다 성가시다. 확인을 기다리는 동안 진행 중이던 작업 상태를 어디에 둘 것인가. 프로세스를 띄워 둔 채 기다리면 그 사이 서버가 재시작되면 날아가고, 처음부터 다시 돌리면 이미 처리한 앞단계를 또 실행하게 된다. "물어보고 기다린다"는 한 문장이 실제로는 일시정지와 재개가 되는 실행 엔진을 요구한다.

    Lobster는 그 엔진을 기본형으로 갖고 있다. OpenClaw(github.com/openclaw/openclaw, 개인용 AI 어시스턴트 오픈소스)에 선택 플러그인으로 붙는 워크플로우 런타임이고, .lobster 워크플로우 파일에 approval: 한 줄을 넣으면 그 자리에서 실행이 실제로 멈춘다. 이 글은 그 멈춤이 프롬프트의 부탁과 무엇이 다른지를 실측으로 따라간다. 아래 결과는 OpenClaw v2026.6.8에 들어 있는 Lobster 코어 @clawdbot/lobster 2026.5.22를 직접 돌린 것이다.

    전체 지도 — 같은 "물어보기"인데 멈추는 주체가 다르다

    diagram

    ▲ 프롬프트로 부탁하는 방식 — 판단하는 쪽과 멈추는 쪽이 같다

    다이어그램 설명. 이 구조의 약점은 갈래가 둘이라는 게 아니라, 어느 갈래로 갈지를 정하는 것이 통제 대상인 모델 자신이라는 데 있다. "모델이 그 지시를 기억하고 있는가"라는 판정을 모델이 내리고, 그 결과에 따라 멈출지 말지도 모델이 정한다. 감시자와 피감시자가 같은 사람인 셈이다. 그래서 이 방식은 잘 동작할 때도 왜 잘 동작했는지 보장할 수 없다. 지시를 잘 지킨 것인지, 이번에 우연히 그런 것인지 구분할 방법이 없다.

    diagram

    ▲ approval 게이트 — 멈추는 주체가 런타임으로 넘어간다

    다이어그램 설명. 바뀐 건 멈추는 주체 하나다. "런타임이 실행을 중단한다"에는 모델이 관여하지 않는다 — 워크플로우 파일을 실행하던 코드가 그 단계를 만나면 다음 줄로 넘어가지 않을 뿐이다. 모델이 무엇을 기억하는지, 지시를 어떻게 읽었는지는 이 지점에서 상관이 없어진다. 그 뒤가 실용적으로 중요한 부분인데, "여기까지의 상태를 디스크에 저장"과 "재개 토큰을 돌려준다"가 앞서 말한 성가심을 푼다. 기다리는 동안 아무 프로세스도 살아 있을 필요가 없다. 서버가 재시작돼도 파일은 남고, 나중에 토큰을 들고 오면 멈춘 자리에서 이어진다. 흔한 오해를 하나 짚으면, 이건 "모델을 못 믿어서" 넣는 장치가 아니다. 사람이 짠 스크립트도 배포 직전에는 멈춰서 물어봐야 하고, 그 요구는 모델의 신뢰도와 무관하게 원래 있던 것이다.

    실제로 멈춰 보기

    배포를 흉내 낸 워크플로우 파일을 만들었다. 대상을 모으고, 승인을 받고, 배포하는 세 단계다.

    name: deploy-guard
    steps:
      # 1단: 무엇을 배포할지 모은다 (실제로는 여기서 배포 대상을 조회한다)
      - id: collect
        run: echo '{"targets":["api","web","worker"],"risk":"high"}'
    
      # 2단: 게이트 — 이 단계에서 실행이 멈춘다
      #      stdin으로 앞 단계 결과를 넘겨 승인자가 무엇을 승인하는지 보게 한다
      - id: gate
        approval:
          prompt: "3개 서비스를 배포합니다. 진행할까요?"
          required_approver: "release-manager"     # 이 사람만 승인 가능
          initiated_by: "dev-lead"                  # 요청한 사람
          require_different_approver: true         # 요청자 본인은 승인 불가
        stdin: $collect.json
    
      # 3단: 승인된 경우에만 실행 — 조건은 파서가 평가한다
      - id: deploy
        run: echo '{"deployed":true}'
        condition: $gate.approved
    

    코드 설명. 눈여겨볼 곳은 approval 아래 세 줄이다. 승인을 "받는다"에서 끝나지 않고 누가 승인할 수 있는지까지 파일에 박아 둔다. required_approver는 지정된 사람만 승인할 수 있게 하고, require_different_approver는 요청자 본인의 승인을 막는다. 두 번째 것이 이른바 4-eyes 원칙(four-eyes principle — 중요한 결정은 반드시 두 사람의 눈을 거치게 하는 통제 방식)인데, 보통은 별도 승인 시스템을 붙여야 하는 요구사항이 워크플로우 파일 한 줄로 들어간다. 그리고 stdin: $collect.json도 형식적인 줄이 아니다. 이게 있어야 승인 요청에 "무엇을 승인하는 것인지"가 실려 나간다 — 내용 없이 "진행할까요?"만 보이는 승인은 승인이 아니라 요식이다.

    돌려 봤다. 배포 단계까지 가지 않고 멈춘다.

    {
      "status": "needs_approval",
      "output": [],
      "requiresApproval": {
        "prompt": "3개 서비스를 배포합니다. 진행할까요?",
        "items": [ { "targets": ["api","web","worker"], "risk": "high" } ],
        "initiatedBy": "dev-lead",
        "requiredApprover": "release-manager",
        "requireDifferentApprover": true,
        "resumeToken": "eyJwcm90b2NvbFZlcnNpb24iOjEsInYiOjEsImtpbmQiOiJ3b3Jr...",
        "approvalId": "d2bc4eba"
      }
    }
    

    실측 설명. statusneeds_approval이고 output은 비어 있다 — 배포 단계는 실행되지 않았다. 그리고 items에 앞 단계가 만든 배포 대상이 그대로 실려 있다. 승인자가 "api·web·worker 세 개, 위험도 높음"을 보고 결정할 수 있다는 뜻이다. 승인 정책 세 줄도 응답에 그대로 나오는데, 이건 승인 화면을 만드는 쪽이 "이건 지정 승인자만, 요청자 본인은 안 됨"을 표시할 수 있게 하려는 것이다. 그리고 resumeToken — 이 문자열이 나중에 이어 달리기 위한 열쇠다.

    핵심 — 승인자 신원을 실제로 검사하는가

    정책을 파일에 적는 것과 그 정책이 실제로 강제되는 것은 다른 문제다. 그래서 네 가지 경우로 나눠 각각 승인을 시도해 봤다. 승인자 신원은 환경변수 LOBSTER_APPROVAL_APPROVED_BY로 전달된다.

    ### A. 신원을 밝히지 않고 승인
    Workflow step gate approval requires approver identity; set LOBSTER_APPROVAL_APPROVED_BY
                                                              → 차단
    
    ### B. 요청자 본인(dev-lead)이 승인
    Workflow step gate approval requires approver 'release-manager', got 'dev-lead'
                                                              → 차단
    
    ### C. 관계없는 제3자(someone-else)가 승인
    Workflow step gate approval requires approver 'release-manager', got 'someone-else'
                                                              → 차단
    
    ### D. 지정 승인자(release-manager)가 승인
    "status": "ok"
    "deployed": true                                          → 통과, 배포 단계 실행됨
    

    실측 설명. 네 경우 중 셋이 막히고 하나만 통과했다. 여기서 확인되는 게 두 가지인데, 첫째는 정책이 문서가 아니라 코드로 강제된다는 것이다. 승인 요청 응답에 "요청자 본인은 안 됨"이라고 적혀 있는 것과, 요청자 본인이 실제로 시도했을 때 거절당하는 것은 전혀 다른 수준의 보장이다. 둘째는 에러 메시지가 무엇이 왜 막혔는지 정확히 말해 준다는 점이다. got 'dev-lead'처럼 누가 시도했는지가 남으니 감사 기록으로 쓸 수 있다. 사례 A가 특히 중요한데, 신원을 아예 안 밝히면 기본값으로 통과시키지 않고 거절한다. 실패했을 때 안전한 쪽으로 넘어지는 설계(fail closed — 실패 시 권한을 여는 게 아니라 닫는 원칙)다.

    비대칭 — 승인은 신원이 필요하고 거부는 필요 없다

    그런데 승인이 아니라 거부를 해 보면 결과가 다르다. 신원을 전혀 밝히지 않고 거부를 시도했다.

    ### 신원 없이 거부(--approve no)
    {
      "ok": true,
      "status": "cancelled",
      "output": [],
      "requiresApproval": null
    }
    

    실측 설명. 아무 신원 없이도 거부는 그냥 된다. 워크플로우는 cancelled로 끝나고 배포 단계는 실행되지 않는다. 처음 봤을 때는 구멍처럼 보였는데, 방향을 따져 보면 일관된 설계다. 신원 검사가 막으려는 것은 승인되지 않은 부수 효과다. 거부는 부수 효과를 만들지 않고 없애는 쪽이니 검사할 이유가 없다. 오히려 거부까지 신원을 요구하면 "위험한 걸 눈치챘는데 권한이 없어 멈추지 못하는" 상황이 생긴다. 안전한 방향으로 가는 문은 누구에게나 열어 두는 것이 이 비대칭의 취지다. 물론 이걸 뒤집어 말하면 악의적인 제3자가 남의 워크플로우를 취소시킬 수는 있다는 뜻이라, 토큰 자체를 아무나 못 얻게 하는 건 여전히 호출하는 쪽 책임이다.

    재개 토큰은 상태가 아니라 포인터다

    멈춘 워크플로우를 이어 달리려면 토큰이 필요하다. 그런데 이 토큰이 무엇을 담고 있는지가 설계상 중요한 지점이다. 실제 토큰을 디코드해 봤다.

    토큰 길이: 155자
    
    디코드 결과:
    {"protocolVersion":1,"v":1,"kind":"workflow-file",
     "stateKey":"workflow_resume_869cfb47-cbe6-4637-8824-5f86aa1cf796"}
    
    실제 상태 파일: ~/.lobster/state/workflow_resume_869cfb47-....json  (936바이트)
    

    실측 설명. 토큰 155자 안에는 작업 내용이 하나도 없다. 상태 파일을 가리키는 열쇠 하나뿐이고, 진짜 상태 936바이트는 디스크에 따로 있다. 이 분리가 왜 값을 하냐면, 토큰은 승인 요청과 함께 바깥으로 나가는 물건이기 때문이다. 텔레그램 메시지에 실리고, 웹 화면의 버튼에 박히고, 로그에 남는다. 여기에 처리 중이던 메일 본문이나 배포 대상 전체가 통째로 들어 있으면 그 모든 경로가 민감 정보를 나르게 된다. 포인터만 나가면 새어 나갈 것이 참조 하나로 줄어든다.

    디스크에 남은 상태 파일을 열어 보면 무엇을 들고 기다리는지가 분명해진다.

    {
      "resumeAtIndex": 2,
      "steps": {
        "collect": { "json": { "targets": ["api","web","worker"], "risk": "high" } },
        "gate":    { "initiatedBy": "dev-lead" }
      },
      "approvalStepId": "gate",
      "approvalIdentity": {
        "initiatedBy": "dev-lead",
        "requiredApprover": "release-manager",
        "requireDifferentApprover": true
      }
    }
    

    실측 설명. 세 가지가 들어 있다. resumeAtIndex가 2라는 건 재개할 때 앞의 두 단계를 다시 실행하지 않는다는 뜻이다 — 배포 대상 조회를 두 번 하지 않고, 이미 보낸 메일을 또 보내지 않는다. steps에는 이미 끝난 단계의 결과가 그대로 있어서 뒷단계가 그 값을 받아 쓸 수 있다. 그리고 가장 중요한 것이 approvalIdentity다 — 승인 정책이 멈추는 시점에 이미 파일에 박힌다. 재개할 때 정책을 다시 받아서 검사하는 게 아니라, 멈출 때 정해진 정책으로 검사한다. 이 순서가 뒤바뀌면 승인하러 오는 쪽이 "나는 승인 자격이 있다"고 같이 주장할 수 있게 되고, 그 순간 정책은 없는 것이나 마찬가지가 된다.

    토큰을 두 번 쓸 수 있는지도 확인해 봤다. 정상 승인으로 한 번 소비한 토큰을 그대로 다시 넣었다.

    ### 이미 사용한 토큰으로 재시도
    { "ok": false, "error": { "type": "runtime_error",
                              "message": "Workflow resume state not found" } }
    

    실측 설명. 한 번 쓰면 상태 파일이 사라지고 같은 토큰은 죽는다. 토큰이 로그나 메신저 기록에 남더라도 그걸 주워서 배포를 한 번 더 돌릴 수는 없다는 뜻이다. 승인 한 번이 실행 한 번에 대응한다는 게 코드로 지켜진다.

    OpenClaw 안에서 쓸 때 알아 둘 제약

    여기까지는 Lobster를 단독으로 돌린 결과다. OpenClaw 플러그인으로 쓸 때는 에이전트가 lobster 도구를 호출하고, 멈추면 사용자에게 승인 프롬프트를 보여 준 뒤 action: "resume"approve: true로 이어 간다. 이때 걸리는 제약이 몇 가지 있다.

    첫째, 기본 타임아웃이 20초다. 임베디드 러너의 timeoutMs 기본값이 20000이고 출력 상한은 512KB다. 승인 대기 시간과는 무관하다는 점을 헷갈리기 쉬운데 — 멈춰서 기다리는 동안에는 프로세스가 살아 있지 않으니 타임아웃과 상관없다. 이 값이 재는 것은 단계들이 실제로 실행되는 시간이다. 메일 20건을 조회하고 분류하는 데 20초가 넘으면 늘려 줘야 한다.

    둘째, 워크플로우 안에서 OpenClaw 도구를 부르는 openclaw.invoke는 임베디드 모드에서 신뢰할 수 없다. 이건 내 관찰이 아니라 공식 문서가 직접 밝히는 제약이다. 플러그인은 워크플로우를 게이트웨이 프로세스 안에서 돌리는데, 그 안에서는 중첩된 도구 호출이 게이트웨이 주소와 인증 맥락을 물려받지 못한다. 앞서 인용한 OpenClaw 예제 파일들이 바로 이 openclaw.invoke 형태로 분류 단계를 짜 놓았으니, 그대로 가져다 쓰면 막힌다. 현재로서는 판단 단계를 워크플로우 밖에서 별도 도구 호출로 처리하거나, 독립 실행형 Lobster CLI를 쓰는 쪽으로 우회한다.

    셋째, lobster는 기본으로 꺼져 있는 선택 도구다. 워크플로우가 부수 효과를 일으킬 수 있으니 명시적으로 켜야 한다. 설정에서 tools.alsoAllowlobster를 더하는 방식이 권장되는데, tools.allow로 쓰면 그것만 허용하는 제한 모드가 되어 다른 기본 도구가 함께 꺼진다.

    정리 — 되돌릴 수 없는 일 앞에서는 멈추는 주체를 바꾼다

    프롬프트에 "물어봐"라고 적는 것과 approval: 한 줄을 넣는 것의 차이는 문장의 강도가 아니다. 멈추는 주체가 누구인가다. 앞의 방식은 통제받아야 할 모델이 스스로 멈춰 주기를 기대하고, 뒤의 방식은 모델과 무관한 런타임이 다음 줄로 넘어가지 않는다. 그리고 그 멈춤에 신원 검사·상태 저장·일회용 토큰이 딸려 오면서, 별도 승인 시스템을 붙여야 했을 요구사항 몇 개가 파일 몇 줄로 접힌다.

    그래서 자동화를 짤 때 나눠 볼 선은 이거다 — "이 단계가 실패하면 되돌릴 수 있는가?" 조회하고 분류하고 요약하는 단계는 틀려도 다시 하면 그만이라 모델에게 맡겨도 된다. 하지만 메일을 보내고, PR을 닫고, 배포를 시작하는 단계는 한 번 지나가면 끝이다. 그 자리에는 부탁이 아니라 멈춤을 놓는다. 되돌릴 수 없는 일 앞에서 잠깐 멈추는 비용은, 되돌릴 수 없는 일이 잘못 실행됐을 때의 비용과 비교할 물건이 아니다.


    참고한 공개 자료:


    이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.

Designed by Tistory.