ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Lobster에서 멈춘 워크플로우를 다시 달리게 하기 — 재개 토큰과 승인자 신원은 같은 호출에서 만난다
    IT 2026. 9. 17. 23:00

    ▶ 동영상 개요 — Lobster에서 멈춘 워크플로우를 다시 달리게 하기 — 재개 토큰과 승인자 신원은 같은 호출에서 만난다

    8분 12초 — 멈춘 워크플로우를 다시 달리게 하는 재개 호출 하나가 서로 다른 두 가지를 싣는다 — 재개 토큰(어느 멈춤을 이어갈지 가리키는 포인터, 상태가 아니라 열쇠라 한… — NotebookLM 동영상 개요로 생성

    🎧 오디오 개요 — Lobster에서 멈춘 워크플로우를 다시 달리게 하기 — 재개 토큰과 승인자 신원은 같은 호출에서 만난다

    22분 23초 — 멈춘 워크플로우를 다시 달리게 하는 재개 호출 하나가 서로 다른 두 가지를 싣는다 — 재개 토큰(어느 멈춤을 이어갈지 가리키는 포인터, 상태가 아니라 열쇠라 한…. 화면은 이 글의 인포그래픽 한 장 고정 — NotebookLM 오디오 개요로 생성

    승인 게이트를 붙여 놓은 워크플로우를 처음 돌려 보면 당황스러운 장면을 만난다. 실행이 중간에 멈추더니, 결과 대신 resumeToken이라는 알 수 없는 긴 문자열 한 줄을 돌려준다. 그리고 아무 일도 일어나지 않는다. 여기서 실무자가 실제로 물어야 하는 질문은 두 개다. 이 토큰을 어디에 어떻게 다시 넣어야 멈춘 작업이 이어지는가. 그리고 다시 넣을 때 "내가 승인했다"는 걸 시스템이 대체 누구의 승인으로 받아들이는가.

    이 글은 그 두 질문을 실제 응답과 명령으로 따라간다. 주인공은 Lobster다. OpenClaw(github.com/openclaw/openclaw, 개인용 AI 어시스턴트 오픈소스)에 선택 플러그인으로 붙는 워크플로우 런타임으로, .lobster 워크플로우 파일에 approval: 한 줄을 넣으면 그 자리에서 실행이 실제로 멈춘다. 아래 실측은 전부 OpenClaw v2026.6.8에 들어 있는 Lobster 코어 @clawdbot/lobster 2026.5.22를 직접 돌린 결과다. 미리 결론을 한 줄로 당겨 두면 — 재개 토큰과 승인자 신원은 서로 다른 물건이고, 재개 호출이라는 한 번의 동작에서 처음 만난다.

    전체 지도 — 멈춤에서 재개까지는 한 번의 호출로 이어진다

    diagram

    다이어그램 설명. 이 그림 하나가 글 전체의 뼈대다. 멈춘 다음에 벌어지는 일을 위에서부터 순서대로 읽으면 된다. "approval 단계에서 런타임이 멈춘다"에서 나오는 것이 두 갈래로 갈라지는데, 하나는 재개 토큰이고 하나는 승인 정책이다 — 이 둘을 헷갈리지 않는 게 이 글의 절반이다. 그다음 "상태는 디스크에 저장되고 프로세스는 사라진다"가 실무적으로 가장 고마운 부분인데, 승인을 기다리는 동안 아무 프로세스도 살아 있을 필요가 없다는 뜻이다. 서버가 재시작돼도 파일은 남는다. 그리고 다시 달리기 시작하는 지점이 "재개 호출 — 토큰과 승인자 신원을 함께 싣는다"이다. 여기서 처음으로 "어느 멈춤을 이어갈지"(토큰)"지금 누가 승인하는지"(신원)가 한 호출에 실린다. 마지막 갈림길에서 통과와 차단이 갈리는데, 그 판정을 누가 내리는지가 다음 절부터의 이야기다.

    토큰은 어디서 받나 — 멈춘 응답 안에 들어 있다

    토큰은 따로 발급받는 물건이 아니다. 워크플로우가 승인 단계에서 멈추는 바로 그 응답에 실려 나온다. 배포를 흉내 낸 워크플로우(대상을 모으고, 승인을 받고, 배포하는 세 단계)를 돌리면 배포 단계까지 가지 않고 이렇게 멈춘다.

    {
      "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은 비어 있다 — 배포 단계는 한 줄도 실행되지 않았다. 이 응답에서 재개에 필요한 것을 골라내면 딱 하나, resumeToken이다. 이게 다음에 다시 넣을 열쇠다. 나머지 필드는 승인 화면을 만들어 주는 재료다 — items에는 승인자가 무엇을 승인하는지(세 서비스, 위험도 높음)가 실려 있고, requiredApprover·requireDifferentApprover는 "이건 지정 승인자만, 요청자 본인은 안 됨"을 화면에 표시하라고 알려 준다. 여기서 놓치기 쉬운 함정 하나 — 이 응답에 정책이 적혀 있다고 해서 정책이 강제된다는 뜻은 아직 아니다. 적혀 있는 것과 강제되는 것의 차이는 뒤에서 네 가지 시도로 확인한다.

    토큰 안에는 상태가 없다 — 포인터일 뿐이다

    토큰을 다시 넣기 전에, 이게 무엇을 담고 있는지 한 번 열어 볼 값이 있다. 155자짜리 문자열을 디코드해 봤다.

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

    실측 설명. 토큰 안에는 작업 내용이 하나도 없다. 상태 파일을 가리키는 열쇠(stateKey) 하나뿐이고, 진짜 상태 936바이트는 디스크에 따로 있다. 이 분리가 왜 값을 하냐면, 토큰은 승인 요청과 함께 바깥으로 나가는 물건이기 때문이다. 알림 메시지에 실리고, 웹 화면 버튼에 박히고, 로그에 남는다. 여기에 처리 중이던 데이터가 통째로 들어 있으면 그 모든 경로가 민감 정보를 나르게 된다. 포인터만 나가면 새어 나갈 것이 참조 하나로 줄어든다. 재개할 때 실무적으로 알아 둘 점은 이거다 — 토큰을 잃어버려도 상태는 디스크에 살아 있고, 상태 파일이 지워지면 토큰이 멀쩡해도 죽는다. 둘은 짝이지만 수명이 다르다.

    신원은 어디서 오나 — 토큰이 아니라 재개 호출이 실어 온다

    이제 핵심 질문이다. 재개할 때 "내가 승인한다"를 시스템은 어떻게 아는가? 답은 명확하다 — 토큰은 신원을 담지 않는다. 방금 봤듯 토큰 안에는 상태 파일 열쇠뿐이고 승인자가 누구인지는 없다. 신원은 재개 호출이 바깥에서 따로 실어 온다. 독립 실행형 Lobster에서는 환경변수 LOBSTER_APPROVAL_APPROVED_BY로 전달한다.

    diagram

    다이어그램 설명. 재개 호출 하나가 서로 다른 두 가지를 싣는다는 걸 그린 그림이다. "재개 토큰"은 어느 멈춤을 이어갈지를 정하고, "승인자 신원"은 지금 누가 승인하는지를 밝힌다. 둘이 만나는 자리가 "멈출 때 파일에 박힌 승인 정책"이다. 여기가 설계의 급소인데 — 누가 승인할 수 있는지를 정하는 정책은 재개할 때 받는 게 아니라, 멈추던 시점에 이미 상태 파일에 저장돼 있다. 실제 상태 파일을 열면 이렇게 들어 있다.

    {
      "resumeAtIndex": 2,
      "approvalStepId": "gate",
      "approvalIdentity": {
        "initiatedBy": "dev-lead",
        "requiredApprover": "release-manager",
        "requireDifferentApprover": true
      }
    }
    

    코드 설명. approvalIdentity가 그 박혀 있는 정책이다. 재개하러 오는 쪽은 "나는 누구다"라고 신원만 주장할 수 있을 뿐, "나는 승인 자격이 있다"고 정책까지 같이 주장할 수는 없다. 자격 판단의 기준은 멈출 때 이미 얼려졌기 때문이다. 만약 이 순서가 뒤바뀌어 재개할 때 정책까지 받는다면, 승인하러 온 사람이 신원과 자격을 동시에 들고 와 스스로를 통과시킬 수 있게 된다 — 그 순간 정책은 없는 것이나 마찬가지가 된다. resumeAtIndex가 2인 것도 실무적으로 중요한데, 재개할 때 앞의 두 단계를 다시 실행하지 않는다는 뜻이다. 이미 조회한 대상을 또 조회하지 않고, 이미 보낸 것을 또 보내지 않는다.

    네 가지로 승인을 시도해 봤다 — 신원 검사의 실제

    정책이 파일에 적혀 있는 것과 실제로 강제되는 것은 다른 문제라고 했다. 그래서 같은 멈춤에 대해 신원만 바꿔 가며 네 번 재개를 시도했다. 재개 호출의 형태는 이렇게 두 조각이다 — 토큰을 도로 넣고, 신원을 환경변수로 밝힌다.

    # 멈춰서 받은 토큰을 그대로 다시 넣는다.
    # 지금 승인하는 사람이 누구인지는 환경변수로 함께 실어 보낸다.
    LOBSTER_APPROVAL_APPROVED_BY=release-manager \
      lobster resume --token "eyJwcm90b2NvbFZlcnNpb24iOjEs..." --approve yes
    

    코드 설명. 재개에서 값을 하는 건 두 부분이다. --token에 멈출 때 받은 그 문자열을 그대로 넣고, 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                                          → 통과, 배포 단계 실행됨
    

    실측 설명. 네 경우 중 셋이 막히고 하나만 통과했다. 신원 식별이 실제로 어떻게 작동하는지가 이 네 줄에 다 들어 있다. 사례 A가 특히 중요하다 — 신원변수를 비우면 기본값으로 통과시키지 않고 거절한다. 실패했을 때 안전한 쪽으로 넘어지는 설계(fail closed — 실패 시 권한을 여는 게 아니라 닫는 원칙)다. 사례 B는 4-eyes 원칙(four-eyes principle — 중요한 결정은 반드시 두 사람의 눈을 거치게 하는 통제 방식)의 실물인데, 요청한 사람(dev-lead)이 자기가 요청한 걸 자기가 승인하는 걸 막는다. 사례 C는 정책에 적힌 그 사람이 아니면 통과 못 한다는 걸 보여 준다. 그리고 에러 메시지가 got 'dev-lead'처럼 누가 시도했는지를 남긴다 — 이대로 감사 기록이 된다. 핵심은 이거다. Lobster는 신원을 스스로 알아내지 않는다. 호출하는 쪽이 "나는 release-manager다"라고 주장하고, 얼려 둔 정책이 그 주장을 대조할 뿐이다. 그래서 그 주장이 참인지(정말 그 사람이 부른 게 맞는지)를 보증하는 건 토큰을 아무나 못 얻게 하는 바깥 계층의 몫으로 남는다.

    거부는 신원이 필요 없다 — 방향이 다르기 때문이다

    승인이 아니라 거부를 하면 결과가 반대다. 신원변수를 전혀 밝히지 않고 거부를 시도했다.

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

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

    토큰은 한 번만 산다 — 승인 한 번이 실행 한 번

    재개를 실무에 쓸 때 마지막으로 확인해야 할 것 — 이 토큰을 두 번 쓸 수 있는가? 정상 승인으로 한 번 소비한 토큰을 그대로 다시 넣어 봤다.

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

    실측 설명. 한 번 쓰면 상태 파일이 사라지고 같은 토큰은 죽는다(앞서 본 stateKey가 가리키던 파일이 없어지니 당연한 결과다). 토큰이 로그나 알림 기록에 남더라도 그걸 주워서 배포를 한 번 더 돌릴 수는 없다는 뜻이다. 승인 한 번이 실행 한 번에 대응한다는 게 코드로 지켜진다. 되돌릴 수 없는 일을 자동화할 때 정확히 필요한 성질이다 — 승인 버튼을 실수로 두 번 눌러도, 토큰이 새어 나가 재전송돼도, 위험한 단계는 딱 한 번만 실행된다.

    OpenClaw 플러그인 안에서 재개할 때

    여기까지는 Lobster를 독립 실행형으로 돌린 결과다. OpenClaw 플러그인으로 쓰면 재개 호출의 겉모습이 바뀐다. 에이전트가 lobster 도구를 호출해 워크플로우를 시작하고, 멈추면 사용자에게 승인 프롬프트를 보여 준 뒤, 이런 형태로 이어 간다.

    {
      "action": "resume",
      "resumeToken": "eyJwcm90b2NvbFZlcnNpb24iOjEs...",
      "approve": true
    }
    

    코드 설명. 조각은 독립 실행형과 똑같다 — resumeToken으로 어느 멈춤인지 가리키고, approve로 승인·거부를 정하고, 승인자 신원을 함께 실어 얼려 둔 정책과 대조한다. 겉모습만 환경변수에서 JSON 필드로 바뀌었을 뿐, 토큰과 신원이 한 호출에서 만난다는 구조는 그대로다. 다만 플러그인으로 쓸 때 걸리는 제약이 몇 가지 있으니 미리 알아 두는 게 좋다.

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

    둘째, 워크플로우 안에서 OpenClaw 도구를 부르는 openclaw.invoke는 임베디드 모드에서 신뢰할 수 없다. 이건 공식 문서가 직접 밝히는 제약이다. 플러그인은 워크플로우를 게이트웨이 프로세스 안에서 돌리는데, 그 안에서는 중첩된 도구 호출이 게이트웨이 주소와 인증 맥락을 물려받지 못한다. 판단 단계를 워크플로우 밖에서 별도 도구 호출로 처리하거나, 독립 실행형 Lobster를 쓰는 쪽으로 우회한다.

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

    정리 — 재개 호출에 무엇을 실을지가 전부다

    멈춘 워크플로우를 다시 달리게 하는 일은 결국 재개 호출 하나에 두 가지를 정확히 싣는 일이다. 하나는 재개 토큰 — 어느 멈춤을 이어갈지 가리키는 포인터이고, 상태가 아니라 열쇠라서 바깥으로 흘러도 위험이 참조 하나로 줄고, 한 번 쓰면 죽는다. 다른 하나는 승인자 신원 — 토큰이 담지 않는 정보라 호출하는 쪽이 따로 주장해야 하고, 그 주장을 검사하는 정책은 멈추던 시점에 이미 얼려져 있다. 이 순서 덕분에 승인하러 온 사람이 자기 자격을 스스로 정할 수 없다.

    그래서 자동화를 짤 때 기억할 한 줄은 이거다 — 되돌릴 수 없는 단계 앞에서는 "누가 승인했는가"가 "승인했는가"만큼 중요하다. 배포를 시작하고, 메일을 보내고, 무언가를 지우는 단계는 한 번 지나가면 끝이라, 그저 멈추는 것만으로는 부족하다. 멈춘 뒤에 지정된 사람이, 딱 한 번, 되돌아오지 않는 토큰으로 다시 달리게 하는 것 — Lobster의 재개는 그 세 가지를 한 호출에 접어 넣는다.


    참고한 공개 자료:


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

Designed by Tistory.