권한 요청과 닫기
권한 요청에 답하고, 메시지를 이어 보내고, 세션을 닫는다
세션 하나를 처음부터 끝까지 돈다. 에이전트가 파일을 쓰려다 권한을 묻고, 스크립트가 허락하고, 다음 메시지가 같은 작업 트리에서 이어지고, 마지막에 세션을 닫는다. SDK 설치의 werft 클라이언트를 쓴다. key에 sessions:approve와 sessions:control도 있어야 한다.
파일을 쓰는 세션 만들기
이 대본은 Bash의 tee로 hello.txt를 쓴다. claude-coding-local 프로필은 세션 작업 공간 안의 파일 쓰기·편집과 로컬 git은 묻지 않고, 그 밖의 파일을 바꾸는 명령(tee처럼 목록에 없는 것)은 실행 전에 권한을 묻는다. 작업 공간 안으로 리다이렉트하는 echo alpha > hello.txt는 묻지 않으므로 이 예제에 쓰지 않는다.
import {
closeResult,
type PendingRequestHandlers,
waitForReceipt,
waitForTurn,
} from "@werft-ai/sdk";
import { werft } from "./werft.ts";
const signal = AbortSignal.timeout(5 * 60_000);
const COMMAND = "echo alpha | tee hello.txt";
const created = await werft.sessions.create({
profile_id: "claude-coding-local",
repository_id: "sample-app",
message: `GATE-SPEC ${JSON.stringify({
id: "first",
steps: [
{
tool: "Bash",
input: { command: COMMAND, description: "write hello.txt" },
},
],
final: "wrote hello.txt",
})}`,
});
const sessionId = created.session_id;권한 요청에 답하기
세션이 권한을 묻고 멈추면 status가 needs_input이 된다. waitForTurn에 handler를 넘기면 turn을 기다리는 동안 생긴 권한 요청과 질문이 handler로 온다.
// Allow the exact command this script expects, deny everything else.
const approvals: PendingRequestHandlers = {
permission(request) {
console.log("asked:", request.tool, request.input);
const command = (request.input as { command?: unknown } | null)?.command;
if (request.tool === "Bash" && command === COMMAND)
return { action: "allow" };
return { action: "deny", reason: `This script only allows: ${COMMAND}` };
},
};
const first = await waitForTurn(werft, sessionId, created.turn_id, {
signal,
onRequest: approvals,
onAnswer: (outcome) => console.log(`answered: ${outcome.outcome}`),
});
console.log(`turn ${first.turn_id}: ${first.status}`);asked: Bash { command: 'echo alpha | tee hello.txt', description: 'write hello.txt' }
answered: accepted
turn 1: completed- handler가
undefined를 돌려주거나 그 종류의 handler가 없으면 요청을 열어 두고 사람에게 맡긴다. - 거절은
{ action: "deny", reason }이다. 답하지 않은 요청은 30분 뒤 거절로 끝난다. 자세한 내용은 승인 대기와 재개에 있다. - workspace의 key(개인·에이전트)는 자기가 시작한 세션의 요청을 승인하지 못한다. 승인용 key를 따로 쓰는 방법은 클라이언트 설정과 오류에 있다.
이어서 보내기
같은 세션에 메시지를 보내면 다음 turn이 된다. worker는 같은 작업 트리와 같은 Claude Code 대화를 이어 쓴다. 읽기 명령은 권한을 묻지 않는다.
const sent = await werft.sessions.messages.send(sessionId, {
message:
'GATE-SPEC {"id":"second","steps":[{"tool":"Bash","input":{"command":"cat hello.txt","description":"read hello.txt"}}],"final":"read it"}',
});
const second = await waitForTurn(werft, sessionId, sent.turn_id, { signal });
console.log(`turn ${second.turn_id}: ${second.status}`);turn 2: completed닫기
할 일을 마친 세션은 닫는다. 진행 중인 turn을 끝까지 돌리고 checkpoint를 남긴 뒤 closed가 된다. 제어 요청에는 지금 세션의 revision을 expected_revision으로 넘긴다. SDK는 이 값을 대신 읽어 채우지 않고, REVISION_CONFLICT를 받아도 다시 보내지 않는다.
const { revision } = await werft.sessions.get(sessionId);
const accepted = await werft.sessions.close(sessionId, {
expected_revision: revision,
reason: "first session done",
});
const receipt = await waitForReceipt(werft, accepted.receipt_id, { signal });
console.log(`close: ${receipt.status}`, closeResult(receipt));
const session = await werft.sessions.get(sessionId);
console.log(session.status, session.admission_state);close: succeeded {
checkpoint_revision: 1,
cancelled_turn_count: 0,
resulting_admission_state: 'closed'
}
stopped closed닫힌 세션에는 메시지를 더 보낼 수 없다(409 SESSION_CLOSED).
HTTP API로 할 때
권한 요청은 GET /v1/sessions/{id}/pending-requests로 읽고 POST /v1/sessions/{id}/answers로 답한다. 메시지는 POST /v1/sessions/{id}/messages, 닫기는 POST /v1/sessions/{id}/close다. POST에는 모두 Idempotency-Key 헤더가 필요하다.
다음 단계
- 클라이언트 설정과 오류: 옵션, 오류 종류, 재시도
- 시작: 세션 생성 요청의 필드와 오류
- 만들기: 에이전트를 정의하고 세션에 묶는다