Claude Media
Managed Agentsのセルフホストサンドボックスを構築する — Environment worker実装

Managed Agentsのセルフホストサンドボックスを構築する — Environment worker実装

Managed Agentsのツール実行を自社インフラに置くセルフホストサンドボックスを、Environment workerの構築手順とカスタムツール提供まで実装します。

Managed Agentsは既定でAnthropicが管理するクラウドサンドボックス上でツールを実行します。セルフホストサンドボックスは、この実行だけを自社インフラに移し、エージェントのコード・ファイルシステム・ネットワークegressを自分の環境から出さない構成です。オーケストレーション(モデルが動く場所)はAnthropic側に残ったままなので、切り替えても既存のエージェント設計自体は変わりません。

本稿ではEnvironment workerを実際に構築する手順を、常時稼働のワーカーからカスタムツールの提供までコード例つきで扱います。すでにエージェントを作成済みで、これから実行環境だけをクラウドから自社インフラへ移したい人を対象にしています。

セルフホスト化で変わる範囲と事前準備

切り替えで動くのは、ツールを実行する場所だけです。モデルの推論はAnthropic側のままなので、ツールの入出力は制御プレーンへ流れ続けます。モデルが結果を見て次の判断をする以上、これは避けられません。

くらべる

セルフホストで自社に移るもの・残るもの

実行側

自社の管理下に入る

ファイルシステムの読み書き、プロセスの起動、到達できるネットワーク、サンドボックスのライフサイクルです。ファイルやGitHubリポジトリのマウントも自分で面倒を見ます。

制御側

Anthropicに残る

モデルの推論、セッションのキュー、skillsとmemory storesの保管です。ワーカーはセッションの間だけサンドボックスにコピーを置き、memory storeのコピーは終了時に削除します。

エージェントのskillsやmemory storesをどう扱うかは、この線引きの上で決まります。セッション作成時のresourcesにも制約があります。セルフホスト環境が受け付けるのはmemory_storeだけで、fileやgithub_repositoryを含めると400エラーで拒否されます。メッセージは次のとおりです。

Environment env_... is a self-hosted environment. `resources` are not supported with self-hosted environments.

スケジュール実行のデプロイメントも同じ規則に従います。セッション固有の入力ファイルが要るときは、ステップ2の後半で扱うmetadata経由のステージングを使います。

事前に用意するものは次の5つです。

  • 既存のエージェント。無ければ先にQuickstartでエージェントIDを取得しておきます
  • /bin/bash をその正確なパスに持つLinuxホスト。ワーカーのbashツールはPATHを参照せずこのパスを直接呼び出します。TypeScript SDKを使う場合はNode.js 22以上とunzip / tarも必要です
  • ant CLI、またはPython / TypeScript / GoのいずれかのAnthropic SDK。セッション作成とキュー確認のコマンドは、ワーカーホストではなく自分のマシンで走らせます
  • Consoleで発行する環境キー。キー発行はConsole限定で、APIから環境を作った場合でも発行だけはConsoleで行います
  • Claude APIキー。自分のマシンでセッション作成とキュー確認に使います。エージェントのツール呼び出しから読めてしまうので、ワーカーホストには置きません

サンドボックス内のファイル配置も押さえておきます。

パス用途
/workspace用途ツール実行とskillダウンロードの既定ディレクトリ
<workdir>/skills/<name>/用途ダウンロードされたskill。--workdirを変えたら、エージェントのシステムプロンプトも合わせる
/mnt/memory/<store>/用途memory storeごとのディレクトリ。ワーカーが作り、セッション終了時に削除する

antの--workdirはカレントディレクトリが既定なので、/workspaceに揃えたいときは明示します。

ステップ1 — セルフホスト環境を作成しキーを発行する

Console(Workspace > Environments > New > Self-hosted)からでも、APIからでも作成できます。

curl -sS --fail-with-body https://api.anthropic.com/v1/environments \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "anthropic-beta: managed-agents-2026-04-01" \
  -H "content-type: application/json" \
  -d '{
    "name": "self-hosted",
    "config": {"type": "self_hosted"}
  }'

環境を作成したらConsoleでその環境を開き、「Generate environment key」をクリックします。発行された値をワーカーホストにエクスポートします。

export ANTHROPIC_ENVIRONMENT_KEY="sk-ant-oat01-..."
export ANTHROPIC_ENVIRONMENT_ID="env_..."

ステップ2 — Environment workerを動かす

self_hosted環境は1つのワークキューとして振る舞います。セッションがこの環境に割り当てられると、Anthropicがそのセッションをワークアイテムとしてキューに積み、ワーカーがそれを取得(claim)して実行コンテキストを立ち上げ、skillをダウンロードし、ツール呼び出しを実行して結果を投げ返します。取得の方式は常時ポーリングのalways-onか、session.status_run_startedイベントで起きるwebhook-triggeredのどちらかです。

always-on(ant CLI)はもっとも手数が少ない構成です。

ant beta:worker poll --workdir "/workspace"

このコマンドはANTHROPIC_ENVIRONMENT_KEYとANTHROPIC_ENVIRONMENT_IDを環境変数から読み、ワークアイテムの取得・skillダウンロード・ツール実行・結果送信までを1プロセスで担います。SIGTERMやSIGINTを受けると、実行中のツール呼び出しをキャンセルしてエラー結果を投げ、ワークアイテムを解放してから終了します。

セッションごとに独立したファイルシステムやリソース制限が要る場合は、セッションごとに別サンドボックスを起動する構成にします。antを組み込みant beta:worker runをエントリポイントにしたイメージを用意し、ポーラーからのスポーンスクリプトで起動します。

spawn.sh の基本形(memory storesを使わない場合)
#!/bin/bash
# spawn.sh: called once per claimed work item
mkdir -p "/host/outputs/$ANTHROPIC_SESSION_ID"
exec docker run --rm \
  -e ANTHROPIC_SESSION_ID -e ANTHROPIC_ENVIRONMENT_KEY \
  -e ANTHROPIC_WORK_ID -e ANTHROPIC_ENVIRONMENT_ID -e ANTHROPIC_BASE_URL \
  -v "/host/outputs/$ANTHROPIC_SESSION_ID":/workspace \
  your-image

ポーラーはANTHROPIC_SESSION_ID / ANTHROPIC_WORK_ID / ANTHROPIC_ENVIRONMENT_ID / ANTHROPIC_ENVIRONMENT_KEYをスクリプトの環境変数へ注入し、取得したワークアイテムをJSONで標準入力に渡します。ホスト側の/host/outputsをサンドボックスの/workspaceにバインドマウントしておけば、セッション終了後に成果物とダウンロード済みskillを回収できます。

memory storesを使う場合は、もう一手間かかります。セッションごとのsecretはワークアイテムのJSONに入っていますが、ポーラーは環境変数に設定してくれません。スクリプトで取り出し、ANTHROPIC_WORK_SECRETとしてサンドボックスへ渡します。

#!/bin/bash
# spawn.sh(memory stores対応版): stdinのワークアイテムJSONからsecretを取り出す
ANTHROPIC_WORK_SECRET="$(jq -r '.secret // empty')"
export ANTHROPIC_WORK_SECRET
mkdir -p "/host/outputs/$ANTHROPIC_SESSION_ID"
exec docker run --rm \
  -e ANTHROPIC_SESSION_ID -e ANTHROPIC_ENVIRONMENT_KEY \
  -e ANTHROPIC_WORK_ID -e ANTHROPIC_ENVIRONMENT_ID -e ANTHROPIC_BASE_URL \
  -e ANTHROPIC_WORK_SECRET \
  -v "/host/outputs/$ANTHROPIC_SESSION_ID":/workspace \
  your-image

jqはポーラーのホストに必要です。ant beta:worker runのイメージもmemory storesに対応しますが、/mnt/memoryを書き込み可能にしておく必要があります。ホストへのバインドマウントは要りません。ワーカーが終了前にストアへアップロードするためです。同じストアを添付するセッションが同一ホストで並走すると、同じパスを取り合って失敗します。セッションごとに別サンドボックスにすれば、この衝突は起きません。

起動はポーラーにスクリプトを渡すだけです。

ant beta:worker poll --on-work ./spawn.sh

always-on(SDK)はTypeScriptなら次の形になります。

import Anthropic from "@anthropic-ai/sdk";
import { EnvironmentWorker } from "@anthropic-ai/sdk/helpers/beta/environments";
 
const environmentKey = process.env.ANTHROPIC_ENVIRONMENT_KEY!;
const environmentId = process.env.ANTHROPIC_ENVIRONMENT_ID!;
const client = new Anthropic({ authToken: environmentKey });
const controller = new AbortController();
process.once("SIGINT", () => controller.abort());
process.once("SIGTERM", () => controller.abort());
 
await new EnvironmentWorker({
  client,
  environmentId,
  environmentKey,
  workdir: "/workspace",
  signal: controller.signal
}).run();

プロセスをkillするのではなくAbortControllerで中断させているのは、これによってワーカーが変更済みのmemory storeファイルをアップロードしてから終了できるためです。webhook-triggeredを選ぶ場合は、Consoleでsession.status_run_startedを購読するWebhookエンドポイントを定義し、署名検証用のWebhook Signing Keyを別途エクスポートします(Python実装ではpip install "anthropic[webhooks]"が必要です)。

セッション固有の入力ファイルを渡したいときは、resourcesが使えないのでmetadataを経由します。セッション作成時にmetadataへS3パスやコミットSHAなどの参照を入れます。spawnスクリプトか--on-workハンドラがGET /v1/sessions/{session_id}でmetadataを読み、ツール実行が始まる前に作業ディレクトリへファイルを置く流れです。ワークアイテムが持つのはセッションIDだけで、metadataは入っていません。

ステップ3 — ワーカーの接続と動作を確かめる

ワーカーを起動したら、自分のマシンから接続状態を見て、実際にツールがホストで動いたかまで確かめます。

手順

接続確認から動作確認まで

  1. 1

    キューの状態を読む

    自分のマシンでANTHROPIC_API_KEY(環境キーではなくClaude APIキー)とANTHROPIC_ENVIRONMENT_IDを設定し、ant beta:environments:work statsを実行します。workers_pollingが1以上なら接続できています。

  2. 2

    セッションを作る

    ant beta:sessions createに--agentと--environment-idを渡します。ワーカーがいない間、セッションは失敗せずキューで待ちます。

  3. 3

    タスクを送る

    user.messageイベントで「uname -aの出力を作業ディレクトリのhello.txtに書いて」と頼みます。

  4. 4

    ホストで結果を読む

    ワーカーホストでcat /workspace/hello.txtを実行します。自分のホストのカーネル情報が入っていれば、ツールはそこで動いています。

セッション作成からタスク送信までのコマンドは次のとおりです。

ant beta:environments:work stats --environment-id "$ANTHROPIC_ENVIRONMENT_ID"
 
SESSION_ID=$(ant beta:sessions create \
  --agent "$AGENT_ID" \
  --environment-id "$ANTHROPIC_ENVIRONMENT_ID" \
  --transform id --raw-output)
 
ant beta:sessions:events send --session-id "$SESSION_ID" <<'YAML'
events:
  - type: user.message
    content:
      - type: text
        text: Write the output of "uname -a" to hello.txt in your working directory.
YAML

statsが返すのは4つのフィールドです。depthは未取得の件数、pendingは取得済みだが未確認応答の件数、oldest_queued_atは最古の待機項目の時刻、workers_pollingは直近にポーリングしたワーカー数です。ワーカーのヘルスチェックにはworkers_pollingの下限、詰まりの検知にはdepthの増加を使えます。pendingは通常ほぼ0で、0以外が続くならワーカーが取得と確認応答の間で止まっています。

ステップ4 — サンドボックスからカスタムツールを提供する

カスタムツールは自分のコードが実行するツールで、エージェントがagent.custom_tool_useイベントを出し、対応するuser.custom_tool_resultを待ちます。ワーカー自身をこの実装先にすると、ツールはサンドボックスに設定した内部サービス・認証情報・ネットワークegressにだけ到達し、それ以外には出ません。結果の送信は環境キーで認可されるため、Claude APIキーをワーカーホストに置く必要もありません。

まずエージェント側のtoolsにcustomエントリを追加し、ワーカーが登録する名前と一致させます。

{
  "type": "custom",
  "name": "get_order_status",
  "description": "Look up an order in the internal fulfillment system by order ID.",
  "input_schema": {
    "type": "object",
    "properties": {
      "order_id": { "type": "string", "description": "The order ID" }
    },
    "required": ["order_id"]
  }
}

次にワーカーのtoolsファクトリへ、標準ツールセットと並べて実装を渡します。

import Anthropic from "@anthropic-ai/sdk";
import { EnvironmentWorker } from "@anthropic-ai/sdk/helpers/beta/environments";
import { betaTool } from "@anthropic-ai/sdk/helpers/beta/json-schema";
import { betaAgentToolset20260401 } from "@anthropic-ai/sdk/tools/agent-toolset/node";
 
const getOrderStatus = betaTool({
  name: "get_order_status",
  description: "Look up an order in the internal fulfillment system by order ID.",
  inputSchema: {
    type: "object",
    properties: { order_id: { type: "string", description: "The order ID" } },
    required: ["order_id"]
  },
  run: async ({ order_id }) => `Order ${order_id}: shipped`
});
 
await new EnvironmentWorker({
  client,
  environmentId,
  environmentKey,
  workdir: "/workspace",
  tools: (ctx) => [...betaAgentToolset20260401(ctx), getOrderStatus]
}).run();

エージェントに宣言だけあってワーカー・クライアントのどちらにも実装が登録されていないカスタムツールは、何かが結果を投げ返すまでrequires_actionの停止理由でセッションが止まったままになります。動かないときは、まずこの登録漏れを疑うのが早道です。

自社ネットワーク内のMCPサーバーを使いたい場合は、MCP tunnelsでAnthropic側から到達させる方法のほかに、ワーカー自身をMCPクライアントにしてサーバーのツールをカスタムツールとして宣言する方法があります。この場合MCPサーバーは外部からのインバウンド接続を一切必要とせず、ワーカーがサンドボックス内から自分のMCPセッション越しに呼び出しを転送します。両者は独立した仕組みで、トンネル経由でもパブリックなMCPサーバーでも、セルフホストのセッションは使えます。実行場所とツール到達経路の両方を自社境界内に収めたいときは併用します。ラップするときの制約は3つあります。input_schemaは$refと、トップレベルのoneOf / anyOf / allOfが使えません。ツール名はエージェント内で一意にし、bashやreadなどの組み込み名とmcp__接頭辞は避けます。2つのサーバーが同じツール名を出すときは、接頭辞付きの名前で自前のラッパーを定義します。

MCPクライアントにタイムアウトを付けないと、ハングした呼び出しはバックストップが働くまでエラーになりません。バックストップはSDKごとに違い、Pythonはワーカー自身のツール呼び出し上限(約2分半)、TypeScriptはMCP SDKの既定リクエストタイムアウト(約1分)、Goは120秒です。ラップしたツールの名前・説明・結果はモデルのコンテキストに入るため、運用しているか信頼できるサーバーだけを宣言します。承認ステップが要るなら、ワーカーが実行するツールのコード側に入れます。カスタムツールの宣言形式そのものはAgent SDKのカスタムツール実装と同じ考え方です。

症状から原因をたどる

症状ごとの原因と直し方を並べます。

症状原因直し方
workers_pollingが0のまま原因ワーカーがキューに届いていない直し方ワーカーホストでANTHROPIC_ENVIRONMENT_KEYとANTHROPIC_ENVIRONMENT_IDの設定を確かめる
セッションがキューに留まる原因取得するワーカーがいない。失敗ではなく待機直し方workers_pollingとdepthを見る
セッションがrequires_actionで止まり、カスタムツールが返らない原因エージェントに宣言があるが、ワーカーにもクライアントにも実装が登録されていない直し方ワーカーのtoolsに実装を渡す。antのワーカーは登録手段が無いのでSDKへ切り替える
セッションがidleでrequires_action、エラーイベントも出ない原因ワーカーがmemory storeをマウントできず、ワークアイテムを失敗させた直し方ホストで原因を直し、user.interruptイベントを送ると再キューされて次のワーカーが再試行する
ログにthe work item carried no sessions token原因セッションごとのsecretがワーカーに届いていない直し方ANTHROPIC_WORK_SECRETとして転送する。ポーラーとワーカーが同一プロセスでも出るならサポートへ
ログにsomething already exists at the memory store's path原因前のセッションのディレクトリが残っている。ワーカーが終了処理前に落とされたときに多い直し方ログが示すディレクトリを消す。未同期の編集は失われる
ログにcannot create the memory store's folder原因ワーカーのユーザーが/mnt/memory配下にディレクトリを作れない直し方/mnt/memoryを作り、そのユーザーにchownする
400エラーresources are not supported原因セッションにfileかgithub_repositoryを入れた直し方memory_storeだけにして、ファイルはmetadata経由で置く
/bin/bashが無い原因bashツールはPATHを経由せず、そのパスを直接呼ぶ直し方正確に/bin/bashへ置く
Claude Platform on AWSで環境キーが通らない原因この構成の認証はIAMかAWS Console発行のAPIキー直し方認証方式を切り替える。memory storesはこの構成で添付できない

memory storesの失敗は、ワーカーのログにだけ出ます。セッション側に届くのは読み取り専用ストアへの書き込み拒否だけなので、原因を探すときはワーカーのログを先に読みます。

ワーカーを止めるときの注意

プロセスをkillすると終了処理が走らず、未同期のmemory編集が消えます。SIGTERMで止め、強制終了までに十分な猶予を取ります。最終アップロードに時間がかかるためです。ant CLIは両シグナルを自分で処理しますが、SDKのEnvironmentWorkerはシグナルハンドラを入れないので、停止の仕組みを自分で書きます。Dockerの既定ではSIGTERMの10秒後にSIGKILLが飛ぶため、docker run --stop-timeoutかオーケストレーターの猶予設定で延ばします。

特定のセッションだけを止めたいときは、運用側からant beta:environments:work stopを呼びます。ワークアイテムはstoppingになり、ワーカーが次のハートビートで進行中のツール呼び出しをキャンセルするとstoppedになります。待たずに即時stoppedにするには--forceを付けます。

数字

覚えておく数字

  • workers_polling

    30秒

    この間にポーリングしたワーカーだけ数える

  • 停止の猶予

    30秒以上

    最終アップロードに要する時間

  • tools配列の上限

    128件

    組み込みツールセットも1件に数える

  • memory対応CLI

    1.33.0以降

    memory storesを扱うワーカーの最低バージョン

責任の分担を先に決める

セルフホストでは、Anthropicが守るのは制御プレーンだけです。サンドボックスのイメージは検査されず、供給網の汚染は制御プレーンから検知できません。ネットワークのegress制限・ログの保管と削除・ツールのblast radius(侵害時の被害範囲)は自社の責任になります。

環境キーは、シークレット管理サービスなどの専用の保管先に置き、環境ファイルやイメージには入れません。漏えいが疑われるときは失効させて再発行します。失効は毎回のリクエストで検証されるため、ワーカーの次の呼び出しから効きます。信頼できないコードを走らせるなら、信頼境界ごとにワークスペースと環境を分けると、キーが及ぶ範囲を1人のセッションに絞れます。

読み取り専用で添付したmemory storeも、守られるのはアップロードだけです。ワーカーのwriteとeditは拒否しますが、bashツールやカスタムツールからのローカル書き換えまでは止まりません。

always-on・webhook-triggered・サンドボックス単位、どれを選ぶか

構成向く場面留意点
always-on(ant CLI)向く場面まず動かして構成を確かめたい。memory storesも既定の同期設定なら使える留意点カスタムツールは登録できない
always-on(SDK)向く場面カスタムツールやmemory storesの同期設定を変えたい留意点シグナルハンドリングを自分で書く必要がある
webhook-triggered(SDK)向く場面アイドルなポーラーを常駐させたくない留意点Webhookエンドポイントと署名検証の実装が別途要る
サンドボックス単位(spawn.sh)向く場面セッションごとに独立したファイルシステム・リソース制限が要る留意点memory storesを使うならsecretの転送と書き込み可能な/mnt/memoryが要る

まとめ

どの構成を選んでも、環境キーの保管とegress制限・ログの扱いは自社に残ります。ant CLIで始めてSDKワーカーへ移る場合も、移行で変わるのはワーカーの起動方法で、決めておく責任分担は変わりません。

本番に出す前に、その分担を誰が持つかを決めておくと、手戻りが減ります。全体像はセキュリティ責任分界にあります。

この記事を共有:XはてブLinkedIn