Claude Media
Claude SDKのcomputer toolsetでデスクトップドライバーを書く

Claude SDKのcomputer toolsetでデスクトップドライバーを書く

BetaAbstractComputerToolset20260801を継承してデスクトップを操作するドライバーを書く手順です。座標のスケール変換、入力チェック、confirmの必須条件、browser toolsetとの併用を扱います。

computer toolsetのドライバーは何を書くのか

Claude SDKのcomputer toolsetは、デスクトップ操作の呼び出しを受ける側を自分のクラスとして書くための仕組みです。PythonとTypeScriptのSDKには、computer use tool用のクラスBetaAbstractComputerToolset20260801が入っています。これを継承し、screenshotやleft_clickのようなメンバーツールごとに1メソッドを実装したものがドライバーです。

SDKが受け持つのは、呼び出しの振り分けとconfirmの呼び出し、tool_resultの組み立てです。デスクトップそのもの、できあいのドライバー、座標の変換、入力の検査は含まれません。VNCクライアントやxdotoolのような操作手段を包む部分は自分で書きます。

ブラウザ側のクラスとの違いは次のとおりです。

項目browser toolsetcomputer toolset
受け付けるオプションbrowser toolsetconfigs・confirm・url_policy・file_policy・tool_configs・状態報告computer toolsetconfigs・confirm・tool_configs
URLポリシー・ファイルポリシーbrowser toolsetありcomputer toolsetなし
状態報告(_browser_state)browser toolset必須computer toolsetなし
失敗後のスキップ文言browser toolsetブラウザ用の文言computer toolsetNot executed: an earlier computer action in this turn failed.

ブラウザ側の書き方はClaude SDKのbrowser toolsetでブラウザドライバーを実装するにあります。configs、エラーの扱い、早期開始、tool runnerなしのループはcomputer toolsetにも同じように当てはまるので、本記事はデスクトップ側で変わる点に絞ります。Messages APIにツール定義を直接書く方法は、Computer Useツールを自前実装する最小構成が扱っています。

5メンバーだけ実装した最小のドライバー

computer toolsetのメンバーは全部で17個あります。すべてを実装する必要はなく、実装しなかったメンバーはAPIに無効として送られます。Claudeが無効なメンバーを呼んだ場合、SDKはエラーを返し、実行は続きます。

次のドライバーはscreenshot・cursor_position・left_click・type・keyの5つだけを実装します。displayは、VNCクライアントやxdotoolを包んだ自作のラッパーを指します。ask_userは、人に許可を尋ねる自作の関数です。

from anthropic import Anthropic
from anthropic.tools.computer import (
    BetaAbstractComputerToolset20260801,
    BetaComputerCursorPositionResult,
    BetaScreenshotResult,
    BetaToolsetCallContext,
)
from anthropic.types.beta import (
    BetaComputerCursorPositionInput,
    BetaComputerKeyInput,
    BetaComputerLeftClickInput,
    BetaComputerScreenshotInput,
    BetaComputerTypeInput,
)
 
 
class MyDesktop(BetaAbstractComputerToolset20260801):
    def __init__(self, display, **options):
        super().__init__(**options)
        self.display = display
 
    def screenshot(
        self, context: BetaToolsetCallContext, input: BetaComputerScreenshotInput
    ) -> BetaScreenshotResult:
        # Claudeの座標は、この画像の中のピクセル位置
        data = self.display.png_base64()
        return BetaScreenshotResult(data=data, media_type="image/png")
 
    def cursor_position(
        self, context: BetaToolsetCallContext, input: BetaComputerCursorPositionInput
    ) -> BetaComputerCursorPositionResult:
        x, y = self.display.cursor()
        return BetaComputerCursorPositionResult(x=x, y=y)
 
    def left_click(
        self, context: BetaToolsetCallContext, input: BetaComputerLeftClickInput
    ) -> None:
        # coordinateは[x, y]かNone(今のカーソル位置)。textは押さえておく修飾キー
        self.display.click(input.coordinate, input.text)
 
    def type(
        self, context: BetaToolsetCallContext, input: BetaComputerTypeInput
    ) -> None:
        self.display.type(input.text)
 
    def key(self, context: BetaToolsetCallContext, input: BetaComputerKeyInput) -> None:
        self.display.press(input.text, input.repeat or 1)
 
    def close(self) -> None:
        super().close()  # 先に親を閉じ、実行中の呼び出しがなくなってから画面を閉じる
        self.display.close()

left_clickのinput.coordinateがNoneのときは、今のカーソル位置でクリックします。input.textにはshiftのような修飾キーが入り、そのクリックの間だけ押さえます。メソッドが何も返さなければ、ClaudeにはClicked.のような短い確認だけが渡ります。

TypeScript版は、メンバーをprotected overrideのメソッドで書きます。アロー関数のフィールドにしてはいけません。SDKがプロトタイプ上でメンバーを探すためです。typeメンバーだけはtype_と綴ります。Pythonはtypeのままです。

入力の型(BetaComputerLeftClickInputなど)は、Pythonではanthropic.types.betaから、TypeScriptでは@anthropic-ai/sdk/resources/betaから取ります。screenshot・cursor_position・left_mouse_down・left_mouse_upは入力フィールドを持たない4つのツールです。

実行する — confirmがないとコンストラクタが落ちる

ドライバーのインスタンスは、そのままtoolsに渡します。

client = Anthropic()
with MyDesktop(
    display,
    confirm=lambda context: ask_user(f'Allow the "{context.member}" tool?'),
) as desktop:
    runner = client.beta.messages.tool_runner(
        model="claude-opus-5-5",
        max_tokens=1024,
        tools=[desktop],
        messages=[{"role": "user", "content": "Open the calculator and compute 17 * 23."}],
    )
    for message in runner:
        print(message)

confirmを渡している点が、browser toolsetの最小例との大きな違いです。クラスがtype・key・hold_keyのどれかを実装していると、confirmを渡すか、configsでそれらを無効にしない限り、コンストラクタが設定エラーを出します。executeを上書いたクラスは、3つすべてを実装しているものとして数えます。

上の例は、すべての呼び出しの前に人へ確認します。使い捨てでないマシンに向けるときのconfirmの書き方は、後の「動かす前の5手順」で扱います。

座標とスクリーンショットのスケール変換

Claudeが送る座標は、すべて「ドライバーが返したスクリーンショットの中のピクセル位置」です。coordinateとstart_coordinate、zoomのregionの両端が該当します。SDKはこれらを変換せず、そのままドライバーに渡します。

実画面が2560×1440で、返すスクリーンショットを1280×720に縮めているなら、ドライバーが両方向の変換を持ちます。

  • Claudeの点を実画面のピクセルに直す(coordinateとstart_coordinate)
  • cursor_positionで報告する位置を、スクリーンショットのピクセルに戻す
from anthropic.tools import ToolError
 
SCREENSHOT_SIZE = (1280, 720)  # ドライバーが返す全画面スクリーンショットの寸法
DISPLAY_SIZE = (2560, 1440)
 
 
def to_display(coordinate: list[int]) -> tuple[int, int]:
    x, y = coordinate
    width, height = SCREENSHOT_SIZE
    if not (0 <= x < width and 0 <= y < height):
        # 範囲外の点は拒否する。範囲内へ寄せると、Claudeが選んでいない場所をクリックする
        raise ToolError(f"[{x}, {y}] is outside the {width}x{height} screenshot")
    return x * DISPLAY_SIZE[0] // width, y * DISPLAY_SIZE[1] // height
 
 
def to_screenshot(x: int, y: int) -> tuple[int, int]:
    width, height = DISPLAY_SIZE
    return x * SCREENSHOT_SIZE[0] // width, y * SCREENSHOT_SIZE[1] // height

to_display([640, 360])は(1280, 720)を返します。範囲の端も確かめられます。[1279, 719]は(2558, 1438)に、[1280, 360]はToolErrorになります。to_screenshotは実画面の(1, 1)を(0, 0)に丸めるので、縮小比が大きいほどカーソル位置の報告は粗くなります。

守る点は3つです。

  1. スクリーンショットの寸法は、セッションの間ずっと同じにする
  2. 縦横比は実画面と合わせる。2880×1800(16対10)なら、1280×800のような16対10の寸法にする
  3. 座標なしのleft_clickやscrollは、今のカーソル位置で動く。変換するものがないので、そのまま通す

zoomは、指定した範囲を実解像度で撮って返すメンバーです。regionはto_displayと同じ倍率で実画面に直します。ただしzoomの画像を返したあとも、次にClaudeが送る座標は全画面スクリーンショットの中の位置のままです。zoom画像の中の位置にはなりません。

落とし穴

画像のリサイズはSDKがやってくれない

SDKは画像を読みも縮めもしません。モデルの上限を超える画像を返すと、APIが拒否して実行が終わります。スクリーンショットとzoomの画像は、返す前にドライバーの中で縮めます。Opus 4.7以降のモデルは長辺2,576px・ビジュアルトークン4,784までで、これより古いモデルは1,568pxまでです。計算式と、Retinaでの2倍問題はComputer Useの座標ズレを直すスケーリング計算式にまとめています。

1回のリクエストに画像が20枚を超える長い実行では、APIが各画像により厳しい上限をかけます。履歴の整理は、computer use toolのページにある「スクリーンショット履歴の管理」の項目が扱っています。

入力チェックは自分のドライバーで書く

SDKは呼び出しの入力を検査しません。ドライバーが使うフィールドを検査し、実行できない呼び出しはToolError(TypeScriptではthrow)で拒否します。Claudeはそのメッセージを読み、直した呼び出しを送り直せます。

ToolError以外の例外も実行は止めませんが、Claudeに渡るのはClassName: messageの形です。ローカルのパスを含みうるので、ドライバーの中で例外を捕まえ、自分の文面でToolErrorを投げます。SDKは、ツールが返すテキストやエラー文のローカルパスを隠しません。Claudeが読む長さは4,096文字までです。

検査の対象は、wait・hold_keyのduration、keyのrepeat、scrollのscroll_amountが代表です。computer use toolのページにはrepeatが1〜100、durationが300秒までと書かれていますが、SDKはどれも制限しません。数分のwaitや極端に大きいrepeatは、値を範囲内へ丸めずに拒否します。丸めると、Claudeが指定していない動作になるからです。

次のスケッチは、上のMyDesktopに検査を足した形です。上限の数値(waitは10秒、repeatは20、scroll_amountは20)は、使う画面に合わせて決める値で、公式の推奨値ではありません。

MAX_WAIT_SECONDS = 10
MAX_REPEAT = 20
MAX_SCROLL = 20
 
 
class CheckedDesktop(MyDesktop):
    def left_click(self, context, input):
        point = to_display(input.coordinate) if input.coordinate else None
        self.display.click(point, input.text)
 
    def key(self, context, input):
        repeat = input.repeat or 1
        if not 1 <= repeat <= MAX_REPEAT:
            raise ToolError(f"repeat must be between 1 and {MAX_REPEAT}")
        self.display.press(input.text, repeat)
 
    def wait(self, context, input):
        if not 0 < input.duration <= MAX_WAIT_SECONDS:
            raise ToolError(f"duration must be between 0 and {MAX_WAIT_SECONDS} seconds")
        self.display.sleep(input.duration)
 
    def scroll(self, context, input):
        if not 1 <= input.scroll_amount <= MAX_SCROLL:
            raise ToolError(f"scroll_amount must be between 1 and {MAX_SCROLL}")
        point = to_display(input.coordinate) if input.coordinate else None
        self.display.scroll(input.scroll_direction, input.scroll_amount, point)

waitとscrollを実装すると、Claudeにはその2つも提供されます。使わないメンバーはconfigsで無効にできます。ここでのself.display.sleepとself.display.scrollも、自作ラッパーの想定です。

メンバーが返す値で、Claudeの読むものが決まる

ツール返すものClaudeが読むもの
screenshot、zoom返すものBetaScreenshotResultClaudeが読むもの画像ブロック1つ
cursor_position返すものBetaComputerCursorPositionResultClaudeが読むものX={x},Y={y}
そのほかのツール返すもの何も返さない、または1行のテキストClaudeが読むものClicked.のような短い確認と、返した1行を入れた別のテキストブロック

クリックの結果を伝えたいときは、1行のテキストを返します。画面の状態そのものをテキストで返す仕組みは、computer toolsetにはありません。browser toolsetのbrowser_stateに当たるものはなく、Claudeは次のscreenshotで画面を確かめます。

動かす前の5手順

画面に映っているものは、Claudeの次の操作を左右します。ウェブページや通知が、別のウィンドウへ入力させようとすることもあります。使い捨てでないマシンに向ける前に、次の5つを済ませます。

手順

使い捨てでないデスクトップに向ける前に

  1. 1

    デスクトップを隔離する

    セッションごとに、権限を絞ったコンテナかVMを用意します。認証情報とホストのマウントは持たせず、作業に要るアプリだけを入れます。APIを呼ぶコードは、その外で動かします。

  2. 2

    影響のある操作を confirm で止める

    購入やメッセージ送信も、普通のleft_click・type・keyです。confirmが受け取るのはツールと入力で、画面ではありません。デスクトップ上で到達できるアプリを見て、どのツールを承認制にするか決めます。

  3. 3

    ターミナルや起動ランチャーを置かない

    キーボードフォーカスのあるターミナルでは、type・key・hold_keyが入力した内容を実行します。クリックでプログラムが始まることもあります。作業に必要なら、キー入力とクリックの前に人へ確認し、コマンドの届く範囲を隔離で絞ります。

  4. 4

    受け付けない入力を拒否する

    スクリーンショット外の点、数分のwait、極端なrepeatやscroll_amountが対象です。範囲内へ寄せず、ToolErrorで返します。

  5. 5

    画面上のものを信用しない

    ウィンドウタイトルもクリップボードの文字列も、実行や受け渡しの前に確かめます。

SDKが担うのは2番目のconfirmの呼び出しだけで、残りはドライバーとデプロイの側の仕事です。confirmを渡さなければ、SDKは承認を求めずにすべての呼び出しを実行します。クリックも例外ではありません。

次のconfirmは、キーボード入力の前だけ人に確認します。承認制にするツールを増やすなら、ASK_FIRSTにクリック系のツールを足します。

from anthropic.tools.computer import BetaComputerConfirmContext
 
KEYBOARD = {"type", "key", "hold_key"}
CLICKS = {
    "left_click", "right_click", "middle_click", "double_click", "triple_click",
    "left_click_drag", "left_mouse_down", "left_mouse_up",
}
ASK_FIRST = KEYBOARD  # クリックも確認するなら KEYBOARD | CLICKS
 
 
def confirm(context: BetaComputerConfirmContext) -> bool:
    if context.member not in ASK_FIRST:
        return True
    detail = context.input.to_dict()
    return ask_user(f'Allow the "{context.member}" tool with this input?\n{detail}')
 
 
desktop = MyDesktop(display, confirm=confirm)

拒否された呼び出しはエラー結果になり、実行は続きます。拒否されたtypeについて、ClaudeはThe user did not grant permission to run 'type'. Do not retry it unless the user asks you to.と読みます。

このconfirmは、ASK_FIRSTにないツールをすべて承認します。後から実装したツールも同じです。失敗側に倒したい場合は逆にして、画面に影響しないscreenshot・zoom・cursor_position・waitだけを承認し、ほかはすべて確認します。

browser toolsetと同じリクエストで使う

2つのインスタンスをtoolsに並べて渡します。各呼び出しにはtoolset_name(browserかcomputer)が付き、tool runnerが対応するインスタンスへ振り分けます。

# MyBrowser・backend・url_policyはbrowser側の記事の例、confirmは前節の関数
with (
    MyBrowser(backend, url_policy=url_policy) as browser,
    MyDesktop(display, confirm=confirm) as desktop,
):
    runner = client.beta.messages.tool_runner(
        model="claude-opus-5-5",
        max_tokens=1024,
        tools=[browser, desktop],
        messages=[
            {"role": "user", "content": "Copy the heading on example.com into the open text editor."}
        ],
        stream=True,
        run_tools_eagerly=True,
    )
    for stream in runner:
        print(stream.get_final_message())

失敗した呼び出しのあと、tool runnerが飛ばすのは同じターンの後続のうち、同じツールセットの呼び出しだけです。computerの呼び出しが失敗しても、同じターンのbrowserの呼び出しは飛ばされません。

tool runnerを使わずにループを自分で書くときは、toolset_nameが一致するインスタンスのtool_resultで答えます。失敗した呼び出しはターンごとにツールセット別に数え、飛ばした呼び出しにはそのツールセット自身の文言を返します。computerならNot executed: an earlier computer action in this turn failed.です。

URLポリシーとegress制限は、browser側のドライバーの安全策です。書き方はBrowser use SDKのURLポリシーとegress制限にあります。デスクトップ側にURLポリシーはないため、ブラウザを2つのツールセットのどちらからも操作できる構成では、ブラウザへの入り口をどちらに寄せるかも設計になります。

使えない条件と制約

  • SDKは、デスクトップ、できあいのドライバー、URLポリシーを含まない。computer toolsetの書き方は、claude-quickstartsリポジトリにある最小のVNC例(PythonとTypeScript)が出発点になるが、本番用のコードではない
  • 1つのツールセットの呼び出しは、1つずつ順に実行される。無効にする設定はない
  • confirmが受け取るのはツールと入力で、画面ではない。ある点へのクリックが何を起こすかは、入力からは分からない
  • SDKは画像を縮めない。上限を超えた画像はAPIが拒否する
  • 17個のメンバーはすべて既定で有効で、zoomも含む。画面からzoom用の画像を作れないなら、有効のままエラーを返すのでなくconfigsで無効にする

computer use tool自体にも制限があります。レイテンシは人が直接操作するより遅く、座標やツール選択を誤ることがあります。スクロールが効かないアプリでは、Page Downのようなキーが代わりになります。ウェブページや画像に埋め込まれた指示によるプロンプトインジェクションも、ほかのAIと同じように起こりえます。

まとめ

デスクトップドライバーの実装量は、メンバーを5つほど書くだけなら小さく済みます。重いのはメンバーの外側です。縮小したスクリーンショットと実画面の座標変換、範囲外の入力の拒否、承認の対象を決めるconfirmの3つは、SDKが肩代わりしないので、ドライバーの設計に含めます。使い捨てでない画面に向けるなら、confirmを後から足すのではなく、承認が必要なツールを決めたうえで最初のコンストラクタに渡す形にします。

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