はじめに

権限リクエストとクローズ

権限リクエストに答え、メッセージを続けて送り、セッションを閉じる

セッションを 1 つ、最初から最後まで動かす。エージェントがファイルを書こうとして権限を求め、スクリプトが許可し、次のメッセージが同じ作業ツリーで続き、最後にセッションを閉じる。SDK のインストールの werft クライアントを使う。キーには sessions:approve と sessions:control も必要である。

ファイルを書くセッションを作る

この台本は Bash の tee で hello.txt を書く。claude-coding-local プロファイルは、セッションのワークスペース内でのファイルの書き込み・編集とローカル git では尋ねず、それ以外のファイルを変えるコマンド (一覧にない tee など) の前に権限を求める。ワークスペース内へのリダイレクト (echo alpha > hello.txt) は尋ねないため、この例では使わない。

approve-and-close.ts
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 を渡すと、ターンを待つ間に出た権限リクエストと質問が handler に届く。

approve-and-close.ts (続き)
// 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 のキー(個人・エージェント)は、自分が始めたセッションのリクエストを承認できない。承認用のキーを分けて使う方法はクライアントの設定とエラーにある。

続けて送る

同じセッションにメッセージを送ると次のターンになる。worker は同じ作業ツリーと同じ Claude Code の会話を引き続き使う。読み取りのコマンドは権限を求めない。

approve-and-close.ts (続き)
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

閉じる

やることを終えたセッションは閉じる。実行中のターンを最後まで動かし、checkpoint を残してから closed になる。制御リクエストには、今のセッションの revision を expected_revision として渡す。SDK はこの値を代わりに読んで埋めず、REVISION_CONFLICT を受けても再送しない。

approve-and-close.ts (続き)
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 ヘッダーが必要である。

次のステップ

このページ