API 유즈케이스 가이드
유즈케이스별로 어떤 API를 어떤 순서로 부르는지 실행 가능한 명령으로 보여 준다
이 문서는 API 사용자가 "어떤 endpoint를 어떤 순서로 호출하는가"를 빠르게 찾는 실행 가이드다. 요청·응답 스키마의 정본은 docs/openapi.json이며, 배포 서버의 인증 없는 GET /docs가 읽기 전용 참조를 같은 origin의 /docs/scalar.js와 함께 제공한다. 이 사이트의 HTTP API 페이지도 같은 명세에서 만든다. 여기서는 스키마를 복제하지 않고 operation 이름으로 연결한다. 아래 bash 블록은 CI가 compose의 fake Messages API로 그대로 실행하므로 Anthropic 계정이나 실제 호출 비용이 들지 않는다.
예시는 로컬 기본 주소 http://127.0.0.1:3000을 쓴다. 배포 환경에서는 API만 바꾼다.
1. API key로 인증하고 scope 확인하기
OpenAPI: getApiRoot
목적. Bearer API key를 발급하고, 이후 유즈케이스에 필요한 최소 scope를 한 번에 확인한다. 읽기·세션 입력·승인·제어·복구는 각각 sessions:read, sessions:write, sessions:approve, sessions:control, sessions:recover로 나뉜다.
순서. 로컬 스택을 key 없이 도는 fake 모델(--fake-model, 개발용 서버 실행)로 띄우고 key를 발급한 뒤, GET /v1으로 key가 유효한지 확인한다. 운영에서는 플랫폼 관리자가 발급한 key를 KEY에 넣고 scripts/local.sh 두 줄은 생략한다.
scripts/local.sh up --fake-model
KEY=$(scripts/local.sh key api-guide \
--scopes sessions:read,sessions:write,sessions:approve,sessions:control,sessions:recover)
API=http://127.0.0.1:3000
AUTH=(-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json')
DOCS_STATUS=$(curl -sS -o /tmp/werft-api-docs.html -w '%{http_code}' "$API/docs")
SCALAR_STATUS=$(curl -sS -o /tmp/werft-scalar.js -w '%{http_code}' "$API/docs/scalar.js")
printf 'GET /docs %s, GET /docs/scalar.js %s\n' "$DOCS_STATUS" "$SCALAR_STATUS"
test "$DOCS_STATUS" = 200
test "$SCALAR_STATUS" = 200
grep -q 'src="/docs/scalar.js"' /tmp/werft-api-docs.html
test -s /tmp/werft-scalar.js
wait_for() {
for _ in $(seq 1 180); do
[ "$(curl -sS "$API$1" "${AUTH[@]}" | jq -r "$2")" = "$3" ] && return 0
sleep 1
done
echo "timed out: $1 $2 != $3" >&2
curl -sS "$API$1" "${AUTH[@]}" >&2
return 1
}
wait_for_pending() {
for _ in $(seq 1 180); do
pending=$(curl -sS "$API/v1/sessions/$SID/pending-requests" "${AUTH[@]}")
[ "$(echo "$pending" | jq -r '.items[0].kind // empty')" = "$1" ] && return 0
sleep 1
done
echo "timed out: pending request kind != $1" >&2
echo "$pending" >&2
return 1
}
revision() {
curl -sS "$API/v1/sessions/$SID" "${AUTH[@]}" | jq .revision
}
curl -sS "$API/v1" "${AUTH[@]}" | jq -e '.owner_id != null'기대 응답. GET /v1은 인증된 owner_id를 포함한 200 JSON을 돌려준다.
흔한 오류. 401 UNAUTHORIZED면 key가 비었거나 다른 설치에서 발급됐다. 403 FORBIDDEN이면 호출할 operation의 scope가 빠졌다. key를 로그나 문서에 붙이지 말고 환경 변수나 secret manager로 전달한다.
2. 세션 만들기: profile·repository 선택
OpenAPI: createSession, getSession
목적. 실행 profile과 repository를 명시해 세션과 첫 turn을 내구적으로 접수한다.
순서. 고를 수 있는 profile과 그 모델은 GET /v1/profiles(listProfiles)로 본다. profile_id, repository_id, 첫 message를 보내고, 응답의 session_id와 turn_id를 저장한다. 이 예시는 다음 절에서 승인 요청을 보여 주기 위해 파일 쓰기 도구를 호출하는 fake 대본을 쓴다. 실제 Claude 스택(scripts/local.sh up)에서는 profile_id가 claude-coding-real이고 message는 자유 문장이다. 선택 필드 model로 profile이 허용한 모델(카탈로그의 model이나 models) 중 하나를 고를 수 있다. 생략하면 profile의 기본 model로 돌고, 고른 모델은 GET /v1/sessions/{id}의 runtime.model에 나온다. 세션의 profile을 바꾸면(POST /v1/sessions/{id}/profile) 그 뒤로는 새 profile의 기본 model로 돈다.
목록에는 운영자 profile(source: operator)과, 워크스페이스 owner·admin이 콘솔에서 좁혀 만든 판(source: workspace, id가 ws_로 시작)이 함께 나온다. 워크스페이스 판은 workspace.state가 usable일 때만 새 세션에 쓸 수 있다. 규칙은 운영자 문서에 있다.
repository_id를 보낼지는 profile의 repository_mode(GET /v1/profiles)가 정한다. required면 꼭 보내고(GitHub 설정이 있는 에이전트의 세션과 워크스페이스 상한이 정하는 profile은 빼도 저장소 없이 시작한다), none이면 보내지 않으며, optional이면 둘 다 된다. 시작 저장소가 없는 세션은 빈 작업 디렉터리에서 시작하고, 세션 조회의 repository_id가 null이다. 저장소가 없으므로 push 요청은 409 PUSH_SOURCE_MISSING이고, optional profile이 저장소의 CLAUDE.md를 읽게 해 두었어도 이 세션에서는 읽지 않는다. 에이전트 release로 만든 세션에서 repository_id를 빼면 GitHub 설정이 있는 에이전트는 저장소 없이 시작하고 필요한 저장소만 붙인다(설정이 없는 옛 에이전트는 release의 저장소로 시작한다. 저장소 없는 세션). fake-model 스택에서는 general profile이 저장소 없이 돈다. 그 생성 body는 다음과 같다.
{ "profile_id": "general", "message": "회의 메모를 세 줄로 요약해 줘." }CREATE_BODY=$(jq -nc --arg message 'GATE-SPEC {"id":"guide-permission","steps":[{"tool":"Bash","input":{"command":"echo guide | tee api-guide.txt","description":"write api-guide.txt"}}],"final":"wrote api-guide.txt"}' \
'{profile_id:"claude-coding-local", repository_id:"sample-app", message:$message}')
CREATE_KEY=$(uuidgen)
created=$(curl -sS "$API/v1/sessions" "${AUTH[@]}" \
-H "Idempotency-Key: $CREATE_KEY" --data "$CREATE_BODY")
echo "$created" | jq .
echo "$created" | jq -e '.status == "queued" and .turn_id == "1"'
SID=$(echo "$created" | jq -r .session_id)기대 응답. 201과 함께 session_id, turn_id: "1", receipt_id, receipt_status: "accepted", status: "queued"가 온다. 실제 완료 결과는 생성 receipt의 옛 acceptance body가 아니라 turn 조회로 판정한다.
세션 제목
선택 필드 title로 세션 이름을 줄 수 있다. 주면 공백·줄바꿈 연속을 공백 하나로 줄이고 앞뒤를 자른 값이 그대로 저장된다. 그 뒤로 비거나 80자(grapheme cluster, 이모지·결합 문자 하나가 1자)를 넘으면 400이다. 생략하면 첫 message에서 모델을 부르지 않고 같은 규칙으로 만든다. 공백을 줄인 결과가 80자 이하면 그대로 쓰고, 넘으면 앞 79자에서 끝 공백을 떼고 …를 붙인다. 같은 메시지는 늘 같은 제목이 된다. 공백만 있는 메시지면 title은 null이다. 이 필드가 생기기 전 세션도 backfill하지 않고 null로 읽힌다. 화면은 이때 세션 id를 보여 준다.
title은 GET /v1/sessions 목록 항목과 GET /v1/sessions/{id} 상세에 있다. 이름을 바꾸려면 PATCH /v1/sessions/{id}(updateSession)에 {"title": "..."}를 보낸다. Idempotency-Key는 필요 없고, 같은 규칙으로 정리해 저장하며, 응답은 200과 상세 본문이다. 세션 revision은 오르지 않고 receipt도 남지 않는다. 제목이 실제로 바뀌면 SSE에 status 이벤트가 하나 생기고 data.data.title에 새 제목이 실린다(phase는 그때의 세션 status). 같은 제목이면 이벤트가 없다. 바꿀 수 있는 사람은 권한 요청에 답하는 사람과 같다. 만든 사람(key가 만든 세션이면 그 key), 그 에이전트의 관리자, owner·admin이다. 응답이 상세 본문이므로 key에는 sessions:write와 sessions:read가 둘 다 있어야 한다. 공유로 보기만 하는 사람은 403(reason_code: role), 볼 수 없는 사람은 404다.
curl -sS -X PATCH "$API/v1/sessions/$SID" "${AUTH[@]}" \
--data '{"title":"권한 요청 예시 세션"}' | jq '{id, title, revision}'흔한 오류. 422 UNSUPPORTED_CAPABILITY는 profile 또는 repository 선택이 유효하지 않다는 뜻이다. required profile에 repository_id가 없으면(위 예외인 세션 제외) 메시지가 Profile <id> needs a repository: send repository_id이고, none profile에 보내면 Profile <id> runs without a repository: omit repository_id다. repository_id: null은 생략이 아니라 400이다. createSession은 계약상 429 RATE_LIMITED를 반환할 수 있으므로 나중에 재시도한다. 입력 queue 상한은 메시지 추가에 적용된다(4절). 저장소 상한은 413 STORAGE_LIMIT_EXCEEDED로 나타나며 /v1/limits를 본다. 실행 slot이 모자라면 세션은 거절되지 않고 queued로 기다린다. 세션 비용·token 상한은 요청을 거절하지 않고, 넘은 뒤 세션 attention의 BUDGET_EXCEEDED와 /usage의 budget_exceeded: true로 나타난다(8절). 409 IDEMPOTENCY_CONFLICT면 같은 key에 다른 본문을 보냈다.
3. pending-requests의 승인·질문에 답하기
OpenAPI: listPendingRequests, answerPendingRequest, getReceipt
목적. worker가 기다리는 도구 승인과 AskUserQuestion을 같은 pending-requests 흐름으로 처리한다.
순서. 원하는 종류의 pending request가 목록에 나타날 때까지 기다리고 request_id를 읽는다. 권한 요청은 action(allow·deny), 질문 요청은 각 question_id의 selected_option_ids 또는 free_text로 답한다. 응답은 202 receipt이므로 receipt와 turn을 다시 조회한다.
wait_for_pending permission
echo "$pending" | jq -e '.items[0].kind == "permission"'
PERMISSION_REQUEST_ID=$(echo "$pending" | jq -r '.items[0].request_id')
permission_answer=$(curl -sS "$API/v1/sessions/$SID/answers" "${AUTH[@]}" \
-H "Idempotency-Key: $(uuidgen)" \
--data "$(jq -nc --arg request_id "$PERMISSION_REQUEST_ID" \
'{request_id:$request_id, kind:"permission", action:"allow"}')")
echo "$permission_answer" | jq -e '.receipt_status == "accepted"'
echo "$permission_answer" | jq -e '.approval.surface == "api" and (.approval.decision_id | length) == 36'
wait_for "/v1/sessions/$SID/turns/1" .status completed
wait_for "/v1/receipts/$(echo "$permission_answer" | jq -r .receipt_id)" .status succeeded
curl -sS "$API/v1/receipts/$(echo "$permission_answer" | jq -r .receipt_id)" \
"${AUTH[@]}" | jq -e '.operation == "answer" and .status == "succeeded"'
question_message=$(jq -nc --arg message 'GATE-SPEC {"id":"guide-question","steps":[{"tool":"AskUserQuestion","input":{"questions":[{"question":"Which environment?","header":"Environment","options":[{"label":"staging","description":"safe"},{"label":"production","description":"live"}],"multiSelect":false}]}}],"final":"selected"}' \
'{message:$message}')
curl -sS "$API/v1/sessions/$SID/messages" "${AUTH[@]}" \
-H "Idempotency-Key: $(uuidgen)" --data "$question_message" | jq -e '.turn_id == "2"'
wait_for_pending question
echo "$pending" | jq -e '.items[0].kind == "question"'
QUESTION_REQUEST_ID=$(echo "$pending" | jq -r '.items[0].request_id')
QUESTION_ID=$(echo "$pending" | jq -r '.items[0].questions[0].question_id')
OPTION_ID=$(echo "$pending" | jq -r '.items[0].questions[0].options[0].option_id')
question_answer=$(curl -sS "$API/v1/sessions/$SID/answers" "${AUTH[@]}" \
-H "Idempotency-Key: $(uuidgen)" \
--data "$(jq -nc --arg request_id "$QUESTION_REQUEST_ID" --arg question_id "$QUESTION_ID" --arg option_id "$OPTION_ID" \
'{request_id:$request_id, kind:"question", answers:[{question_id:$question_id, selected_option_ids:[$option_id]}]}')")
echo "$question_answer" | jq -e '.receipt_status == "accepted"'
wait_for "/v1/sessions/$SID/turns/2" .status completed
wait_for "/v1/receipts/$(echo "$question_answer" | jq -r .receipt_id)" .status succeeded기대 응답. 목록의 items에는 kind: "permission" 또는 kind: "question"과 만료 시각이 있다. 답변 응답의 approval에는 답한 사람(answered_by: principal과 그 뒤의 사람), 표면(surface: web·api·slack), 답변을 허용한 session.approve 판정의 decision_id가 있다. 같은 값이 승인 행(pending_requests)에 남고, 같은 Idempotency-Key로 다시 보내면 같은 approval이 돌아온다. 거부(action: "deny")의 reason은 선택이다. 비우면 worker가 모델에 사유 없는 거부를 전한다. 답변은 receipt_status: "accepted"로 접수되고, worker가 답을 소비하면 receipt가 succeeded, turn이 completed가 된다. worker는 답 소비를 다음 poll에 보고하므로 turn이 먼저 completed가 될 수 있다. receipt는 한 번 조회하지 말고 succeeded까지 기다린다.
흔한 오류. 409 REQUEST_EXPIRED면 만료되거나 worker가 이미 포기한 요청이므로 새 세션/turn에서 다시 요청한다. 404 NOT_FOUND면 현재 principal이 보지 못하는 세션·요청이거나 이미 닫혔다. 승인 scope가 없으면 403 FORBIDDEN이다.
4. 메시지 보내기와 turn 상태 보기
OpenAPI: appendSessionMessage, getSessionTurn
목적. 기존 세션의 같은 engine 대화에 입력을 추가하고, 결과가 terminal인지 확인한다.
순서. 메시지 POST의 turn_id를 저장하고 그 turn을 조회한다. 세션의 요약 status나 SSE 연결 종료 여부 대신 turn의 status가 completed, failed, interrupted, cancelled, outcome_unknown 중 하나인지로 종료를 판정한다.
message=$(curl -sS "$API/v1/sessions/$SID/messages" "${AUTH[@]}" \
-H "Idempotency-Key: $(uuidgen)" \
--data "$(jq -nc --arg message 'GATE-SPEC {"id":"guide-message","steps":[],"final":"continued"}' '{message:$message}')")
TURN_ID=$(echo "$message" | jq -r .turn_id)
echo "$message" | jq -e '.receipt_status == "accepted" and .turn_id == "3"'
wait_for "/v1/sessions/$SID/turns/$TURN_ID" .status completed
curl -sS "$API/v1/sessions/$SID/turns/$TURN_ID" "${AUTH[@]}" \
| jq -e '.status == "completed"'기대 응답. 메시지는 202와 새 turn_id를 돌려주고, turn 조회는 최종 상태와 커밋된 결과를 돌려준다.
completed는 engine이 turn을 정상 종료했다는 뜻이지, 요청한 일을 다 했다는 뜻이 아니다. 모델이 "먼저 파일 목록을 보겠다"며 도구 하나만 부르고 답을 끝내도, 도구가 오류 tool_result를 받았어도 turn은 completed가 된다. 플랫폼은 모델의 답을 해석해 과제 완료를 판정하지 않는다. 일이 끝났는지는 클라이언트가 5절의 assistant·tool_use·tool_result 이벤트로 확인하고, 덜 끝났으면 같은 세션에 다음 메시지를 보낸다. 5절의 첫 연결은 이전 turn의 이벤트도 재생하므로 data.turn_id가 이 turn인 이벤트만 본다.
흔한 오류. 409 SESSION_PAUSED나 RECOVERY_REQUIRED면 입력을 더 보내지 말고 각각 resume 또는 복구 결정을 먼저 한다. 429면 세션별 queued input 상한을 확인한다.
긴 세션의 transcript 상한. checkpoint 하나가 실을 수 있는 transcript는 part 하나와 합계(root와 모든 subagent)에 상한이 있다. 세션이 이 상한에 다가가면 미리 알리고, 넘으면 새 turn을 받지 않는다.
- 마지막으로 커밋된 checkpoint의 transcript가 합계 상한의 80% 이상이면 세션 상세
attention이{ "code": "TRANSCRIPT_LIMIT", "reason": "transcript_near_limit", "transcript_bytes": …, "limit_bytes": … }가 된다. 처음 넘는 checkpoint가 커밋될 때 SSE에system이벤트(subtype: "transcript_near_limit",transcript_bytes,warn_bytes,limit_bytes,checkpoint_revision)가 한 번 온다. 아직 막는 것은 없다. 하던 일을 마무리하고 새 세션으로 옮길 때다. - 상한을 넘으면 그 turn은 실행된 대로
completed로 남지만 새 checkpoint는 없다.durability.checkpoint_pending_reason이transcript_too_large(part 하나가 넘으면transcript_part_too_large)가 되어 일반 게시 실패publish_failed와 구분된다.attention.reason도 같은 값이고limit_bytes는 그 reason이 가리키는 상한이다. 처음 기록될 때 SSE에system이벤트(subtype: "transcript_limit_reached",reason,limit_bytes)가 한 번 온다. - 이 상태의 세션은 새 메시지를 409
CHECKPOINT_UNAVAILABLE로 거절하고, 이미 queue에 들어 있던 입력도 실행하지 않는다. 이전 turn과 마지막 checkpoint는 그대로이고 그 checkpoint가 복원 지점이다. transcript는 같은 engine 세션 안에서 줄지 않으므로 기다려도 풀리지 않는다. 새 세션을 열거나, 세션이recovery_required가 되면start_fresh로 맥락 없이 이어 간다(7절). 이 세션의 close도 409CHECKPOINT_UNAVAILABLE이다. 닫으려면 terminate한 뒤 recovery close를 쓴다.
5. SSE로 진행 보기와 Last-Event-ID 재연결
OpenAPI: streamSessionEvents
목적. 이미 저장된 이벤트를 재생한 뒤 live 진행을 받고, 연결이 끊기면 마지막 cursor 다음부터 이어 본다.
순서. 첫 연결에서 마지막 id를 저장하고, 재연결 요청의 Last-Event-ID 헤더로 보낸다. 스트림은 15초 keepalive 때문에 정상 상태에서도 닫히지 않으므로 예시만 --max-time으로 끊는다.
FIRST_CODE=$(curl -sSN --max-time 5 "$API/v1/sessions/$SID/events" \
-H "Authorization: Bearer $KEY" -o /tmp/api-guide-events.txt -w '%{http_code}' || true)
test "$FIRST_CODE" = 200
LAST_EVENT_ID=$(awk '/^id: / { id=$2 } END { sub(/\r$/, "", id); print id }' /tmp/api-guide-events.txt)
test -n "$LAST_EVENT_ID"
RECONNECT_CODE=$(curl -sSN --max-time 2 "$API/v1/sessions/$SID/events" \
-H "Authorization: Bearer $KEY" -H "Last-Event-ID: $LAST_EVENT_ID" \
-o /tmp/api-guide-reconnected-events.txt -w '%{http_code}' || true)
test "$RECONNECT_CODE" = 200기대 응답. 200 text/event-stream이며 각 frame은 id, event, data를 가진다. 재연결은 지정한 cursor 이후의 저장 이벤트를 재생하고 live stream으로 이어진다.
흔한 오류. 410 CURSOR_EXPIRED면 보존 범위 밖 cursor이므로 세션/turn 현재 상태를 HTTP로 다시 읽고 새 stream을 시작한다. 429면 동시 stream 수를 줄인다. 지금 서버는 이벤트를 trim하지 않아 410을 만들지 않지만 클라이언트는 처리해야 한다.
6. interrupt, pause/resume, terminate와 receipt 확인
OpenAPI: interruptSession, pauseSession, resumeSession, terminateSession, getReceipt
목적. turn 하나만 멈추거나, checkpoint를 남겨 worker를 내렸다 복원하거나, 실행을 강제로 종료한다. 모든 비동기 제어는 202 receipt의 최종 상태까지 확인한다.
순서. interrupt는 실행 중인 target_turn_id를 보낸다. pause·resume·terminate는 직전에 읽은 revision을 expected_revision으로 보내 경쟁 변경을 막는다. terminate는 외부 효과를 되돌리지 않는다.
slow=$(curl -sS "$API/v1/sessions/$SID/messages" "${AUTH[@]}" \
-H "Idempotency-Key: $(uuidgen)" \
--data "$(jq -nc --arg message 'GATE-SPEC {"id":"guide-interrupt","steps":[],"final":"too late","finalDelayMs":300000}' '{message:$message}')")
SLOW_TURN=$(echo "$slow" | jq -r .turn_id)
wait_for "/v1/sessions/$SID/turns/$SLOW_TURN" .status running
interrupt=$(curl -sS "$API/v1/sessions/$SID/interrupt" "${AUTH[@]}" \
-H "Idempotency-Key: $(uuidgen)" --data "$(jq -nc --arg turn "$SLOW_TURN" '{target_turn_id:$turn}')")
wait_for "/v1/sessions/$SID/turns/$SLOW_TURN" .status interrupted
wait_for "/v1/receipts/$(echo "$interrupt" | jq -r .receipt_id)" .status succeeded
curl -sS "$API/v1/receipts/$(echo "$interrupt" | jq -r .receipt_id)" "${AUTH[@]}" \
| jq -e '.result.terminal == "interrupted" and .result.no_op == false'
pause=$(curl -sS "$API/v1/sessions/$SID/pause" "${AUTH[@]}" \
-H "Idempotency-Key: $(uuidgen)" \
--data "$(jq -nc --argjson revision "$(revision)" '{expected_revision:$revision, reason:"api guide"}')")
wait_for "/v1/receipts/$(echo "$pause" | jq -r .receipt_id)" .status succeeded
wait_for "/v1/sessions/$SID" .admission_state paused
resume=$(curl -sS "$API/v1/sessions/$SID/resume" "${AUTH[@]}" \
-H "Idempotency-Key: $(uuidgen)" \
--data "$(jq -nc --argjson revision "$(revision)" '{expected_revision:$revision}')")
wait_for "/v1/receipts/$(echo "$resume" | jq -r .receipt_id)" .status succeeded
wait_for "/v1/sessions/$SID" .admission_state active
doomed=$(curl -sS "$API/v1/sessions/$SID/messages" "${AUTH[@]}" \
-H "Idempotency-Key: $(uuidgen)" \
--data "$(jq -nc --arg message 'GATE-SPEC {"id":"guide-terminate","steps":[],"final":"never","finalDelayMs":300000}' '{message:$message}')")
DOOMED_TURN=$(echo "$doomed" | jq -r .turn_id)
wait_for "/v1/sessions/$SID/turns/$DOOMED_TURN" .status running
terminate=$(curl -sS "$API/v1/sessions/$SID/terminate" "${AUTH[@]}" \
-H "Idempotency-Key: $(uuidgen)" \
--data "$(jq -nc --argjson revision "$(revision)" '{expected_revision:$revision, reason:"api guide"}')")
echo "$terminate" | jq -e '.receipt_status == "accepted" and .external_effects_reverted == false'
wait_for "/v1/receipts/$(echo "$terminate" | jq -r .receipt_id)" .status succeeded기대 응답. 각 POST는 receipt_id를 돌려준다. interrupt receipt의 result는 실제 terminal과 no_op을, pause/resume receipt는 resulting state와 checkpoint revision을, terminate receipt는 실행 제거 결과와 external_effects_reverted: false를 기록한다.
흔한 오류. 409 TURN_NOT_STARTED면 interrupt 대상이 아직 queued다. 409 REVISION_CONFLICT면 세션을 다시 읽고 사용자의 의도가 여전히 유효한지 판단한 뒤 새 revision으로 재시도한다. 409 RECOVERY_REQUIRED면 다음 절의 결정을 먼저 한다.
진행 중인 세션의 프로필 바꾸기
OpenAPI: changeSessionProfile
세션은 만들 때 고른 프로필로 돈다. 대화를 이어 가면서 모델·도구·provider를 바꾸려면 POST /v1/sessions/{id}/profile에 expected_revision과 새 profile_id를 보낸다. 권한은 pause·resume과 같다(control scope, 세션을 볼 수 있는 사람). 응답은 200과 이미 succeeded인 receipt이고, receipt result에 previous_profile_id와 profile_id가 남는다. 세션 revision은 1 오른다.
curl -sS "$API/v1/sessions/$SID/profile" "${AUTH[@]}" \
-H "Idempotency-Key: $(uuidgen)" \
--data "$(jq -nc --argjson revision "$(revision)" '{expected_revision:$revision, profile_id:"claude-coding-haiku"}')"이미 돌고 있는 turn은 시작한 프로필로 끝난다. 그 뒤 들어오는 입력은 새 프로필의 worker가 마지막 checkpoint에서 같은 engine 대화를 복원해 처리한다. 어느 turn이 어떤 프로필로 돌았는지는 turn 조회의 attempts[].profile_id로 본다. 같은 프로필로 바꾸면 아무것도 바뀌지 않고 receipt만 남는다.
흔한 오류. 422 UNSUPPORTED_CAPABILITY는 새 프로필이 카탈로그에 없거나, 세션의 repository와 짝이 아니거나(저장소 없이 만든 세션이면 새 프로필의 repository_mode가 required이거나. 워크스페이스 상한이 정하는 프로필은 예외), runtime 종류·버전이 다르거나(checkpoint를 복원할 수 없다), 과금 방식(provider.billing)이 다를 때, 또는 세션의 지금 프로필이 카탈로그에서 빠졌거나 다른 설정으로 바뀌어 runtime을 비교할 수 없을 때다. 409 RELEASE_PROFILE_MISMATCH는 agent release로 만든 세션이라 프로필을 release가 정한다는 뜻이다. 409 REVISION_CONFLICT면 세션을 다시 읽는다.
세션 끝내기: close·terminate·recovery close
OpenAPI: closeSession, terminateSession, decideSessionRecovery
세션을 끝내는 길은 셋이다. 일을 마친 세션은 close로 닫는다. terminate와 recovery close는 각각 강제 종료와 복구 포기다. receipt와 이벤트에서 셋이 구분된다.
| close | terminate | recovery close | |
|---|---|---|---|
| 경로 | POST /v1/sessions/{id}/close | POST /v1/sessions/{id}/terminate | POST /v1/sessions/{id}/recovery-decisions, decision: "close" |
| 쓰는 때 | 할 일을 마친 세션을 닫는다 | 폭주하거나 멈춰야 하는 실행을 당장 내린다 | 복구할 수 없는 세션을 운영자가 끝낸다 |
| scope | sessions:control | sessions:control | sessions:recover |
| 받는 상태 | active, paused, resume할 수 있는 stopped | closed만 빼고 전부 | recovery_required, stopping, resume할 수 없는 stopped |
| 진행 중 turn | 끝까지 돌리고 checkpoint를 확정한다. 강제로 죽이지 않는다 | 실행을 죽인다. turn이 outcome_unknown으로 남을 수 있다 | 이미 멈춘 세션이 대상이다 |
| 대기 입력 | closed가 되는 트랜잭션에서 취소(SESSION_CLOSED) | 접수 즉시 취소(SESSION_STOPPED) | 결정 즉시 취소(SESSION_CLOSED) |
| 결과 상태 | closing 뒤 closed(되돌릴 수 없음) | stopping 뒤 stopped(resume 가능) | closed(되돌릴 수 없음) |
receipt operation과 result | close, {resulting_admission_state, checkpoint_revision, cancelled_turn_count} | terminate, {execution_gone, checkpoint_revision, unconfirmed_turn_id, external_effects_reverted} | recovery_decision, {resulting_admission_state, checkpoint_revision, resumable} |
| 이벤트 | status closing·closed, system subtype: session_closed(closed_by: "close") | status stopping·stopped | system subtype: recovery_decision, decision: "close" |
close는 pause와 같은 방식으로 drain한다. worker가 붙은 active 세션은 closing이 되고, 진행 중 turn이 끝나 checkpoint가 확정되고 실행이 사라진 것을 본 뒤 closed가 된다. receipt도 그때 succeeded가 된다. worker가 없는 세션, paused, resume할 수 있는 stopped는 요청 안에서 바로 닫힌다. drain이 60초를 넘기면 세션 attention에 CLOSE_BLOCKED와 이유(long_turn, pending_request, mirror_error, checkpoint_unavailable, execution_pending)가 보인다. 열린 승인·질문은 close를 막는다. 답하거나 interrupt하면 풀린다. 끝내 닫지 못하면 receipt가 실패하고 세션은 active로 돌아간다. 이때 대기 입력은 그대로 남는다. 그 상태에서 빠져나오려면 terminate를 쓴다. 진행 중인 close는 CONTROL_SUPERSEDED로 실패한다.
자기 turn 안에서 자기 세션을 close하는 에이전트는 receipt를 기다리지 않는다. drain은 그 turn이 끝나기를 기다리므로, accepted 응답이 그 turn이 보는 마지막 답이다. 결과를 push하려면 close가 succeeded가 된 뒤 최종 checkpoint로 push를 요청한다. 마지막 turn 안에서 요청한 push는 요청 뒤 그 turn에서 commit을 더 만들었다면, close가 확정한 checkpoint의 commit이 달라 승인할 때 CHECKPOINT_MOVED로 거절된다. close는 push 요청을 건드리지 않는다.
아래 예시는 별도 세션을 하나 만들어 turn을 마치고 닫는다.
close_session=$(curl -sS "$API/v1/sessions" "${AUTH[@]}" \
-H "Idempotency-Key: $(uuidgen)" \
--data "$(jq -nc --arg message 'GATE-SPEC {"id":"guide-close","steps":[],"final":"work is done"}' \
'{profile_id:"claude-coding-local", repository_id:"sample-app", message:$message}')")
CSID=$(echo "$close_session" | jq -r .session_id)
wait_for "/v1/sessions/$CSID/turns/1" .status completed
CLOSE_KEY=$(uuidgen)
CLOSE_BODY=$(jq -nc --argjson revision "$(curl -sS "$API/v1/sessions/$CSID" "${AUTH[@]}" | jq .revision)" \
'{expected_revision:$revision, reason:"work is done"}')
close=$(curl -sS "$API/v1/sessions/$CSID/close" "${AUTH[@]}" \
-H "Idempotency-Key: $CLOSE_KEY" --data "$CLOSE_BODY")
CLOSE_RECEIPT=$(echo "$close" | jq -r .receipt_id)
wait_for "/v1/receipts/$CLOSE_RECEIPT" .status succeeded
wait_for "/v1/sessions/$CSID" .admission_state closed
curl -sS "$API/v1/receipts/$CLOSE_RECEIPT" "${AUTH[@]}" \
| jq -e '.operation == "close" and .result.resulting_admission_state == "closed" and .result.checkpoint_revision != null'
# The same key and body answer the same receipt.
curl -sS "$API/v1/sessions/$CSID/close" "${AUTH[@]}" \
-H "Idempotency-Key: $CLOSE_KEY" --data "$CLOSE_BODY" \
| jq -e --arg receipt "$CLOSE_RECEIPT" '.receipt_id == $receipt'
CLOSED_STATUS=$(curl -sS -o /tmp/werft-closed.json -w '%{http_code}' \
"$API/v1/sessions/$CSID/messages" "${AUTH[@]}" \
-H "Idempotency-Key: $(uuidgen)" --data '{"message":"one more"}')
test "$CLOSED_STATUS" = 409
jq -e '.error.code == "SESSION_CLOSED"' /tmp/werft-closed.json기대 응답. close는 202와 receipt를 돌려준다. receipt는 실행이 사라진 것을 본 뒤 succeeded가 되고, result.checkpoint_revision은 세션의 마지막 checkpoint다(turn을 한 번도 돌리지 않은 세션만 null). 그 뒤 메시지·resume·pause·terminate·close·복구 결정은 모두 409 SESSION_CLOSED다.
흔한 오류. 409 SESSION_CLOSING이면 close가 아직 drain 중이다. receipt를 기다린다. 409 CHECKPOINT_UNAVAILABLE이면 신뢰할 checkpoint가 마지막 turn을 덮지 않는다. drain할 worker가 없거나, transcript가 상한을 넘어(transcript_too_large·transcript_part_too_large) drain이 checkpoint를 만들 수 없는 경우다. terminate한 뒤 recovery close를 쓴다. 409 RECOVERY_REQUIRED면 세션이 recovery_required이거나 resume할 수 없는 stopped다. recovery close를 쓴다. 409 SESSION_PAUSED(pausing)나 SESSION_STOPPED(stopping)면 그 제어가 끝난 뒤 다시 보낸다.
운영 메모. worker 쪽 attempt의 end_reason은 close로 내려가도 pause로 남는다. close와 pause는 서버 로그의 control 결과(operation: close)와 receipt로 구분한다. 이 기능을 되돌리기 전에는 closing 세션이 없는지 확인하고, 있으면 terminate로 빼낸다(이전 바이너리는 closing 세션을 정착시키지 못한다).
7. outcome_unknown·RECOVERY_REQUIRED 복구 결정
OpenAPI: decideSessionRecovery, resumeSession, getSessionTurn
목적. terminate나 worker 상실 뒤 외부 효과를 확정할 수 없는 turn을 운영자가 명시적으로 판정한다. outcome_unknown을 성공이나 실패로 추측하지 않는다.
순서. 세션의 admission_state: recovery_required와 대상 turn의 outcome_unknown을 확인한다. 증거에 맞춰 abandon, confirm_completed, close, start_fresh, retry_restore 중 하나를 선택한다. 여기의 close는 복구를 포기하는 운영자 결정이다. 일을 마친 세션을 닫는 close와의 차이는 6절의 비교 표를 본다. 여기서는 외부 효과가 없다는 fake 시나리오이므로 abandon 뒤 resume한다.
wait_for "/v1/sessions/$SID" .admission_state recovery_required
curl -sS "$API/v1/sessions/$SID/turns/$DOOMED_TURN" "${AUTH[@]}" \
| jq -e '.status == "outcome_unknown"'
decision=$(curl -sS "$API/v1/sessions/$SID/recovery-decisions" "${AUTH[@]}" \
-H "Idempotency-Key: $(uuidgen)" \
--data "$(jq -nc --arg turn "$DOOMED_TURN" --argjson revision "$(revision)" \
'{decision:"abandon", expected_revision:$revision, reason:"fake call had no external effect", target_turn_id:$turn}')")
wait_for "/v1/receipts/$(echo "$decision" | jq -r .receipt_id)" .status succeeded
curl -sS "$API/v1/receipts/$(echo "$decision" | jq -r .receipt_id)" "${AUTH[@]}" \
| jq -e '.result.resumable == true'
recovery_resume=$(curl -sS "$API/v1/sessions/$SID/resume" "${AUTH[@]}" \
-H "Idempotency-Key: $(uuidgen)" \
--data "$(jq -nc --argjson revision "$(revision)" '{expected_revision:$revision}')")
wait_for "/v1/receipts/$(echo "$recovery_resume" | jq -r .receipt_id)" .status succeeded
wait_for "/v1/sessions/$SID" .admission_state active기대 응답. 복구 receipt의 result.resumable이 true면 마지막 checkpoint에서 resume할 수 있다. abandon은 대상 turn을 cancelled로 바꾸지만 이미 발생한 외부 효과를 되돌리지는 않는다.
흔한 오류. 409 RECOVERY_REQUIRED는 아직 필요한 결정이 남았다는 뜻이다. confirm_completed에는 검증 가능한 evidence_ref가 필요하다. checkpoint가 없으면 resume이 CHECKPOINT_UNAVAILABLE로 거절될 수 있으므로 receipt의 resumable을 먼저 본다.
8. 사용량·비용·한도 조회
OpenAPI: getSessionUsage, getUsageSummary, getInstallationLimits
목적. 세션의 추정 provider 비용과 입력 queue 상태, 기간 동안 내가 시작한 세션의 합계, 설치 전체의 실행 slot·저장소·turn 상한을 읽는다.
순서. 세션별 /usage, 기간 합계 /v1/usage/summary, 설치별 /v1/limits를 각각 읽는다. /v1/limits와 scope=installation은 로그인한 workspace owner만 읽는다(웹 콘솔의 cookie). API key는 403이다. 에이전트 하나의 합계는 scope=agent&agent_id=…로 읽고, workspace owner·admin과 그 에이전트의 관리자만 읽는다(API key는 403, 다른 workspace의 에이전트는 404). 구성원은 GET /v1/limits?scope=me로 세션 단위 한도 네 개만 읽고, 기간 합계를 프로필별로 나누려면 group_by=profile을 더한다(세션의 현재 프로필 기준). 비용은 청구액이 아니라 platform 가격표로 계산한 추정치이며, cost.complete가 false면 아직 열린 turn이 있거나 보고가 빠져 실제 합계를 알 수 없다. /v1/limits의 usage.outputs.used_bytes는 보존 중인 세션 성과물의 바이트로, usage.storage와 따로 세고 상한 집행에 쓰지 않는다. outputs.complete가 false면 성과물을 기록하기 전 checkpoint가 남아 그만큼 빠져 있다.
usage=$(curl -sS "$API/v1/sessions/$SID/usage" "${AUTH[@]}")
echo "$usage" | jq .
echo "$usage" | jq -e '.period == "session_lifetime" and .cost.kind == "estimated"'
summary=$(curl -sS "$API/v1/usage/summary?scope=me&since=$(date -u +%Y-%m-01)" "${AUTH[@]}")
echo "$summary" | jq .
echo "$summary" | jq -e '.scope == "me" and .basis.sessions == "created_in_period"'
curl -sS -o /dev/null -w '%{http_code}\n' "$API/v1/limits" "${AUTH[@]}" | grep -qx 403기대 응답. 세션 응답은 cost.amount_usd, complete, 보고/미보고/open turn 수, 비용 상한, 누적 provider token 수(token_count)와 token 상한(token_limit), queued input 수를 준다. token_limit이 null이면 설치에 token 상한이 없다. budget_exceeded는 비용과 token 중 어느 상한에든 닿으면 true다. 세션의 profile이 과금 없음(billing: none)을 선언했으면 cost.kind가 "unmetered"다. 이때 비용은 늘지 않고 비용 상한이 그 세션에 적용되지 않아 cost_limit_usd가 null이지만, token 상한은 그대로 걸린다(설정 레퍼런스). 합계 응답은 기간(period, UTC)에 만든 세션의 수, 비용 합(cost_usd, 각 세션 cost.amount_usd의 합), 그 가운데 하나라도 미완이면 false인 cost_complete, token 합, turn 벽시계 시간 합(run_seconds)을 준다. 개인 key는 만든 사람의 세션을, 만든 사람이 없는 key는 그 key가 시작한 세션을 더한다. 공유받은 세션은 더하지 않는다. owner가 읽는 설치 응답은 설정된 상한(session_token_limit 포함)과 현재 execution slot·queued input·저장소 사용량을 준다.
흔한 오류. 404 NOT_FOUND면 세션이 없거나 현재 owner에게 보이지 않는다. 403 FORBIDDEN은 owner가 아닌 호출자가 /v1/limits나 scope=installation을 읽었다는 뜻이다. 400은 since가 없거나 offset 없는 시각이거나 until이 since보다 앞설 때다. complete: false를 0원으로 해석하지 않는다. 503이면 DB나 사용량 집계를 포함한 설치 상태를 확인한다.
9. Idempotency-Key로 안전하게 재시도하기
OpenAPI: appendSessionMessage, getReceipt
목적. 응답 유실이나 timeout 뒤 같은 명령을 중복 실행하지 않고 원래 접수 결과를 다시 받는다.
순서. mutation 하나에 UUID key를 만들고, 재시도할 때 key와 body를 둘 다 그대로 보낸다. 새 사용자의 새 의도에는 새 key를 쓴다.
RETRY_KEY=$(uuidgen)
RETRY_BODY=$(jq -nc --arg message 'GATE-SPEC {"id":"guide-idempotency","steps":[],"final":"once"}' '{message:$message}')
first=$(curl -sS "$API/v1/sessions/$SID/messages" "${AUTH[@]}" \
-H "Idempotency-Key: $RETRY_KEY" --data "$RETRY_BODY")
second=$(curl -sS "$API/v1/sessions/$SID/messages" "${AUTH[@]}" \
-H "Idempotency-Key: $RETRY_KEY" --data "$RETRY_BODY")
test "$first" = "$second"
RETRY_TURN=$(echo "$first" | jq -r .turn_id)
wait_for "/v1/sessions/$SID/turns/$RETRY_TURN" .status completed기대 응답. 두 호출은 같은 status와 acceptance body를 돌려주며 turn은 한 번만 만들어진다. receipt의 result는 최초 acceptance body를 보존하고, 현재 성공/실패는 receipt의 최상위 status와 turn 조회로 읽는다.
흔한 오류. 같은 key에 다른 body를 보내면 409 IDEMPOTENCY_CONFLICT다. timeout 뒤 결과가 불분명하다고 새 key로 다시 보내면 중복 실행될 수 있으므로 먼저 같은 key로 재시도한다. 429와 503 BACKEND_UNAVAILABLE(DB 장애, 30초 처리 시한 초과)에는 기다릴 시간(초)을 담은 Retry-After header가 붙는다. 응답에 이 header가 있으면 그 시간이 지난 뒤 같은 key와 body로 다시 보낸다.
10. 세션 결과의 push 요청과 승인·거부
OpenAPI: createPushRequest, listPushRequests, getPushRequest, approvePushRequest, rejectPushRequest, getReceipt
목적. 세션이 만든 commit을 저장소의 agent/<session-id> 브랜치로 내보내도록 요청하고, sessions:approve를 가진 사람이 그 commit을 승인하거나 거부한다. push는 worker가 아니라 플랫폼(API)이 승인 뒤 몇 초 안에 한다.
순서. 요청은 body 없이({}) 보낸다. 플랫폼이 세션의 최신 checkpoint revision과 그 git_commit, 세션의 repository_id, 브랜치 agent/<session-id>를 고정한다. 읽어 둔 revision을 못 박으려면 checkpoint_revision을 함께 보낸다. 그 뒤 더 새 revision이 저장됐으면 checkpoint_moved로 거절된다. 어느 쪽이든 그 revision 뒤에 세션이 한 일은 담기지 않았을 수 있다. 승인하거나 거부할 사람은 고정된 git_commit을 확인하고, /approve나 /reject body에 같은 값을 넣는다. 다른 commit을 적은 승인은 거절된다. 한 세션에 열린 요청(requested·approved·pushing)은 하나뿐이므로, 거부한 뒤에 새로 요청한다.
push_request=$(curl -sS "$API/v1/sessions/$SID/push-requests" "${AUTH[@]}" \
-H "Idempotency-Key: $(uuidgen)" --data '{}')
echo "$push_request" | jq -e --arg branch "agent/$SID" \
'.state == "requested" and .target_branch == $branch and (.git_commit | test("^[0-9a-f]{40}$"))'
PUSH_ID=$(echo "$push_request" | jq -r .id)
PUSH_COMMIT=$(echo "$push_request" | jq -r .git_commit)
rejected=$(curl -sS "$API/v1/sessions/$SID/push-requests/$PUSH_ID/reject" "${AUTH[@]}" \
-H "Idempotency-Key: $(uuidgen)" \
--data "$(jq -nc --arg commit "$PUSH_COMMIT" '{git_commit:$commit, reason:"not yet"}')")
echo "$rejected" | jq -e '.state == "rejected" and .resolved_by != null'
curl -sS "$API/v1/receipts/$(echo "$rejected" | jq -r .resolution_receipt_id)" "${AUTH[@]}" \
| jq -e '.operation == "resolve_push" and .result.verdict == "allow" and .result.action == "reject"'
again=$(curl -sS -w '\n%{http_code}' "$API/v1/sessions/$SID/push-requests/$PUSH_ID/approve" "${AUTH[@]}" \
-H "Idempotency-Key: $(uuidgen)" \
--data "$(jq -nc --arg commit "$PUSH_COMMIT" '{git_commit:$commit}')")
test "$(echo "$again" | tail -n1)" = 409
echo "$again" | sed '$d' | jq -e '.error.code == "PUSH_REQUEST_RESOLVED"'
curl -sS "$API/v1/receipts/$(echo "$again" | sed '$d' | jq -r .error.details.receipt_id)" "${AUTH[@]}" \
| jq -e '.status == "failed" and .result.refusal == "already_resolved"'
push_request=$(curl -sS "$API/v1/sessions/$SID/push-requests" "${AUTH[@]}" \
-H "Idempotency-Key: $(uuidgen)" --data '{}')
PUSH_ID=$(echo "$push_request" | jq -r .id)
approved=$(curl -sS "$API/v1/sessions/$SID/push-requests/$PUSH_ID/approve" "${AUTH[@]}" \
-H "Idempotency-Key: $(uuidgen)" \
--data "$(jq -nc --arg commit "$(echo "$push_request" | jq -r .git_commit)" '{git_commit:$commit}')")
echo "$approved" | jq -e '.state == "approved"'
curl -sS "$API/v1/sessions/$SID/push-requests" "${AUTH[@]}" | jq -e '.items[1].state == "rejected"'
for _ in $(seq 1 30); do
pushed=$(curl -sS "$API/v1/sessions/$SID/push-requests/$PUSH_ID" "${AUTH[@]}")
echo "$pushed" | jq -e '.state == "pushed" or .state == "failed"' >/dev/null && break
sleep 1
done
curl -sS "$API/v1/receipts/$(echo "$pushed" | jq -r .push_receipt_id)" "${AUTH[@]}" \
| jq '{operation, status, refusal: .result.refusal, remote_ref: .result.remote_ref, git_commit: .result.git_commit, approved_by: .result.approved_by.principal}'기대 응답. 요청은 201로 state: "requested"와 고정한 값들을 돌려준다. 결정은 200으로 approved 또는 rejected와 resolved_by를 돌려준다. 요청·결정·거절된 시도마다 receipt(request_push·resolve_push)가 남고, 거절이면 오류의 error.details.receipt_id가 그 receipt를 가리킨다. receipt의 result에는 판정(verdict), 거절 사유(refusal), 요청 id, checkpoint revision, commit, 대상 브랜치가 있고 actor에는 누가 했는지가 있다. 승인된 요청은 pushing을 거쳐 pushed나 failed가 되고, push_receipt_id가 가리키는 push receipt에 원격 URL·ref(refs/heads/agent/<session-id>)·이전 commit·승인자, 실패면 refusal이 있다. 사유별 뜻은 API 서버 문서에 있다.
흔한 오류. 409 PUSH_SOURCE_MISSING이면 세션에 저장소나 checkpoint가 아직 없다. 409 PUSH_REQUEST_OPEN이면 열린 요청이 있다. 409 PUSH_REQUEST_RESOLVED면 이미 결정한 요청이다. 409 CHECKPOINT_MOVED면 승인 body의 git_commit이 고정된 commit과 다르거나, 요청 뒤에 세션이 다른 commit으로 checkpoint를 남겼다. 요청 뒤 checkpoint가 다시 생겼어도 commit이 같으면 승인된다. 이 요청은 거부하고 새로 요청한다. sessions:approve가 없는 key로 결정하면 403 FORBIDDEN이고 receipt가 남는다. worker의 자격(launch nonce·session token)은 /v1에서 인증되지 않아 401이다.
11. 세션 성과물 목록과 내려받기
OpenAPI: listSessionOutputs, downloadSessionOutput
목적. 세션이 작업 디렉터리의 out/ 아래에 남긴 파일을 사람이 받는다. 목록은 세션의 최신 commit된 checkpoint가 기록한 것이라 worker나 작업 디렉터리가 사라져도 남는다. 마지막 checkpoint 뒤에 쓴 파일은 다음 checkpoint가 commit될 때까지 목록에 없다.
순서. 목록을 읽고, 받을 파일의 path를 그대로 한 path segment로 percent-encode해서(out/report.md는 out%2Freport.md) 내려받는다. 권한은 세션 읽기(sessions:read)와 같다.
outputs=$(curl -sS "$API/v1/sessions/$SID/outputs" "${AUTH[@]}")
echo "$outputs" | jq .
echo "$outputs" | jq -e --arg sid "$SID" '.session_id == $sid and (.items | type == "array")'
OUT_PATH=$(echo "$outputs" | jq -r '[.items[] | select(.omitted == null)][0].path // empty')
if [ -n "$OUT_PATH" ]; then
curl -sS -o /tmp/werft-output "${AUTH[@]}" \
"$API/v1/sessions/$SID/outputs/$(jq -rn --arg path "$OUT_PATH" '$path | @uri')"
test "$(shasum -a 256 /tmp/werft-output | cut -d' ' -f1)" = \
"$(echo "$outputs" | jq -r --arg path "$OUT_PATH" '.items[] | select(.path == $path) | .sha256')"
fi기대 응답. 목록은 checkpoint_revision과 items(path, bytes, sha256, omitted)를 준다. checkpoint가 아직 없으면 checkpoint_revision: null과 빈 items다. 상한을 넘은 파일은 omitted: "too_large", sha256: null로만 보이고 내용은 저장되지 않았다. 내려받기는 application/octet-stream, Content-Disposition: attachment(파일 이름은 filename*), X-Content-Type-Options: nosniff로 파일 내용을 그대로 준다. 받은 내용의 sha256은 목록의 sha256과 같다. 저장소의 객체가 checkpoint가 확인한 내용과 다르면 전송이 끝나기 전에 끊긴다.
흔한 오류. 404 NOT_FOUND면 세션이 없거나 보이지 않거나, path가 목록에 없거나(/를 인코딩하지 않은 경우 포함) omitted 파일이다. 409 CHECKPOINT_UNAVAILABLE이면 최신 checkpoint의 manifest나 그 파일 객체를 읽을 수 없다.
12. 로컬 예시 정리
목적. 이 문서가 만든 로컬 데이터와 worker 자원을 제거한다.
순서. 기본 compose project를 데이터 volume과 함께 내린다. 운영 배포에는 실행하지 않는다.
scripts/local.sh down기대 응답. local.sh: deleted가 출력되고 로컬 세션·checkpoint·key가 삭제된다.
흔한 오류. 다른 project나 installation을 수동 Docker 명령으로 지우지 않는다. 기본 포트를 다른 스택이 쓰고 있다면 이 예시를 시작하지 말고 개발용 서버 실행의 포트 점검을 먼저 따른다.