郭立 (leeguoo)

# Claude Code で Codex の Computer Use を使う方法

Codex の Computer Use は OpenAI 自身の署名だけを認識し、ほかの agent が実際の操作を呼び出すと -10000 で弾かれる。私はこの署名の門番をアセンブリまで分解し、30 行ほどの DYLD フックで読み取られる team id を書き換え、Computer Use 全体を Claude Code に接続した。この記事はその解析であり、そのまま真似できる接続方法でもある。

2026年7月7日 · 記事 · 公開

このページの目次
1.まず{結論|けつろん}:これは「{画像|がぞう}を{見|み}て{座標|ざひょう}をクリック」ではない2.{二|ふた}つのプロセス、Sky というコードネーム3.Skyshot:{同一|どういつ}フレームの{中|なか}に、1{枚|まい}のスクリーンショットと1{本|ぽん}のツリー4.モデルに{見|み}せるあのツリーはどんな{形|かたち}か5.10 個のツール、2 種類のアドレッシング6.変化した部分だけ送る:増分 diff7.入力はどう打ち込まれるのか、そして目立たないガードレール8.自分で止まる:確認ポリシー9.-10000 の門:どうやって「OpenAI かどうか」を見分けるのか10.門をくぐる:自分で読み出したその文字列を差し替える11.Claude Code に接続する12.それに倣って、opencua というものを書いた13.分解し終えて覚えていること

<ruby>探偵<rt>たんてい</rt></ruby>の<ruby>子犬<rt>こいぬ</rt></ruby>ビーグルが<ruby>虫眼鏡<rt>むしめがね</rt></ruby>で Mac を<ruby>分解<rt>ぶんかい</rt></ruby>していて、そばに UI ワイヤーフレームとアクセシビリティツリーの<ruby>模式図<rt>もしきず</rt></ruby>がある

私の環境かんきょうには Codex の Computer Use プラグインをれてあり、しばらく使つかってみたところ、App の操作そうさはこれまでたもののなかでもかなり安定あんていしていました。クリックは正確せいかくで、スクロールも適切てきせつ空振からぶりもほとんどありません。そこで 2 つのかんがえが同時どうじかびました。なぜここまで安定あんていしているのか。そして、ほかの agent、たとえば毎日まいにち起動きどうしている Claude Code でもこれを使つかえないか。そこで分解ぶんかいしてみたところ、最終的さいしゅうてき接続せつぞくもできました。

解析かいせき過程かていは、基本的きほんてきにはシンボルと文字列もじれつ静的せいてきみ、みきれない部分ぶぶんだけぎゃくアセンブルをる、というものです。実際じっさいにやったことは 3 つあります。公式こうしきの Codex を一度いちど本当ほんとううごかすこと、それを理解りかいしながら再現版さいげんばんくこと、そして最後さいごに Claude Code へ接続せつぞくすることです。以下いかではこの順番じゅんばんで、Computer Use がどううごくのか、OpenAI の署名しょめいだけをみとめるもんがどう他者たしゃめているのか、そして私がどうそのもん通過つうかして Claude Code に接続せつぞくしたのかを説明せつめいします。

まず結論けつろん:これは「画像がぞう座標ざひょうをクリック」ではない

なかの computer use のおおくは、このながれです:スクリーンショット → モデルが画像がぞうる → ピクセル座標ざひょう出力しゅつりょくする → クリックする。純粋じゅんすい視覚しかくベースの方式ほうしき欠点けってん明白めいはくです。座標ざひょうはずれるし、ちいさなコントロールは正確せいかくにクリックしにくいし、スクロールすると無効むこうになり、画像がぞうは token もいます。

Codex はべつみちっています:アクセシビリティツリー(Accessibility Tree)を優先ゆうせんし、スクリーンショットは最後さいご保険ほけん。その主要しゅようツールの自己説明じこせつめい文字列もじれつには、かなり率直そっちょくにこうかれています。

必要ひつようならアプリ使用しようセッションを開始かいしし、そのうえでアプリのキーウィンドウの状態じょうたい取得しゅとくし、スクリーンショットとアクセシビリティツリーをかえす。アプリを操作そうさするまえに、assistant の各ターンで一度いちどこれはばれなければならない。

まいターン、うごかすまえに、まず一度いちど状態じょうたいり、スクリーンショットアクセシビリティツリー同時どうじます。構造化こうぞうかされたコントロールがあればツリーない番号ばんごうでクリックする(正確せいかく安定あんていやすい)。どうしてもなければ、ピクセル指定していのクリックへもどる(汎用はんよう)。この「二本足にほんあし」の設計せっけいこそ、体感たいかんのよさのっこです。

ふたつのプロセス、Sky というコードネーム

これはスクリプトではなく、ネイティブな macOS アプリ一式いっしきです。arm64 Swift で、最低さいてい macOS 14.4。.appひらいてみると、ふたつの実行じっこうファイルがえます。

  • SkyComputerUseClient(11MB):MCP フロントエンド。Codex はこれをつうじてツールを公開こうかいし、会話かいわターンにわせてがったりちたりする、短命たんめいなプロセス。
  • SkyComputerUseService(17MB):常駐じょうちゅうエンジン。スクリーンショット、ツリーの走査そうさ入力にゅうりょく注入ちゅうにゅう実際じっさいになう。

両者りょうしゃあいだは XPC + Mach port で通信つうしんします(転送てんそうクラスは ComputerUseIPCXPCTransport、チャネルめいCodexComputerUseIPC-1)。また、おなじ App Group を共有きょうゆうして生存せいぞん状態じょうたいたもちます。さらにちいさな App がふた付属ふぞくしています。ひとつは自動化じどうかあいだにロック画面がめんふせぐもの(CUALockScreenGuardian)、もうひとつは権限けんげん付与ふよ案内あんないするものです。

内部ないぶコードネームは Sky。スクリーンショットにツリーをくわえたそのスナップショットを Skyshot観察かんさつ録画ろくが機能きのうSkysight入力にゅうりょく抽象ちゅうしょうのプレフィックスを SAIんでいます。

なぜふたつのプロセスにけるのか? ターンとターンのあいだ状態じょうたいかしておく必要ひつようがあるからです。あのツリー、イベントストリーム、権限けんげん認可にんかはいずれも service がわのこしておく必要ひつようがあり、フロントエンドはプロトコル変換へんかんだけをおこないます。これこそが、あとで「増分ぞうぶん diff」が前提ぜんていでもあります。

スクリーンショットとインデントされたアウトラインツリーが<ruby>半分<rt>はんぶん</rt></ruby>ずつ<ruby>映<rt>うつ</rt></ruby>った<ruby>画面<rt>がめん</rt></ruby>を<ruby>見<rt>み</rt></ruby>ている<ruby>小<rt>ちい</rt></ruby>さなビーグル犬

Skyshot:同一どういつフレームのなかに、1まいのスクリーンショットと1ぽんのツリー

Skyshot は、この仕組しく全体ぜんたいなかもっと真似まねする価値かちがあるてんです。

スクリーンショットがわは ScreenCaptureKit を使つかい、取得しゅとくしたら JPEG に圧縮あっしゅくし、かなりちいさくします。実際じっさいに TextEdit ウィンドウをらせてみると、かえってきた画像がぞうは 586×488、23.5KB でした。ちいさい画像がぞうは token を節約せつやくできますが、本当ほんとう情報じょうほうにな画像がぞうではなく、あのツリーです。

ツリーがわは、システムのアクセシビリティ API で辿たどれる実際じっさいのコントロールを、モデルがめるテキストへ投影とうえいしたものです。そのクラスめいはそのまま LMReadableElement(LM = Language Model)です。スクリーンショットはモデルに「ちらっと確認かくにん」させるだけで、どこをクリックするか、どのあたいむかは、すべてツリーに依存いぞんしています。

モデルにせるあのツリーはどんなかたち

言葉ことばだけではつたわらないので、TextEdit ウィンドウの状態じょうたい一度いちどらせて、実際じっさいかえってきたものをしました(一部いちぶ):

Computer Use state (CUA App Version: 857)
<app_state>
App=/System/Applications/TextEdit.app/ (bundleID com.apple.TextEdit, pid 71221)
Window: "Untitled", App: TextEdit.
0 standard window Untitled, Secondary Actions: Raise
	1 scroll area Secondary Actions: Scroll Left, Scroll Right, Scroll Up, Scroll Down
		2 text entry area (settable, string) Value: <本文>, ID: First Text View
	21 container
		22 checkbox bold, Help: Bold text, Value: 0
	28 pop up button typeface, Help: Choose the typeface, Value: Helvetica
The focused UI element is 2 text entry area (settable, string) …
</app_state>

このフォーマットこそが、それの「インターフェース言語げんご」です。フィールドごとに分解ぶんかいしてると:

要素ようそれい意味いみ
行頭ぎょうとう整数せいすう2 text entry area …要素番号ようそばんごう。click / set_value / scroll はすべてこれで位置いち特定とくていします
インデント(Tab)ノードはおやノードより一段いちだんふかツリーのふかさをエンコード
ロールstandard windowpop up button平易へいい言葉ことばで、モデルがそのまま理解りかいできる
括弧内かっこない特徴とくちょう(settable, string)(disabled, settable, float)あたい設定せっていできるか、どんなかたか、無効むこうかどうか
フィールドValue:Help:ID:現在値げんざいち、ホバー説明せつめい、コントロール識別子しきべつし
Secondary ActionsRaiseShow color panel追加ついかのアクセシビリティアクション。べつのツールで起動きどうします
末尾まつびのフォーカスThe focused UI element is 2 …現在げんざいのフォーカスを直接ちょくせつしめし、モデルの推論すいろん一回いっかいはぶきます

リッチなコントロールをふくむ TextEdit ウィンドウ全体ぜんたいでも、シリアライズはわずか 3.6KB でした。じゅんテキスト、行頭ぎょうとう番号ばんごう、Tab インデント、平易へいいなロール。token を節約せつやくでき、diff にもやさしく、モデルの学習がくしゅうコストもゼロです。(settable) という括弧かっこがとても巧妙こうみょうで、これは「ここは変更へんこうできるか」をモデルに直接ちょくせつおしえているのとおなじで、そのあとの set_value と差分さぶん diff までひとつづきにつないでいます。

10 個のツール、2 種類のアドレッシング

フロントエンドのバイナリからは、完全なツール表を引き出せます。全部で 10 個です。中心になるものは次のとおりです。

  • get_app_state:状態を取得します(スクリーンショット + ツリー)。各ターンで必ず最初に 1 回呼び出す必要があります。
  • click:番号でクリックするか、スクリーンショット上のピクセル座標でクリックします。
  • type_text / press_key:文字入力、ショートカットキーの押下。
  • scroll:ある要素を「ページ」単位でスクロールします。小数ページにも対応しています。
  • set_value:設定可能な値コントロールの値を直接設定します。1 文字ずつ打ちません。
  • perform_secondary_action:上に出てきた Secondary Actions を実行します。

click には細かいポイントがあります。element_indexx/y の両方を受け取るのです。これは二者択一の場当たり的なものではなく、意図されたフォールバックの連鎖です。構造を使えるなら、ピクセルは決して使いません。set_value も同じです。キーを 1 つずつ入力するシミュレーションより、コントロールの値を目標値に直接設定するほうが、速く、安定し、入力メソッドにも邪魔されません。さらにモデルには、「ツリー内のそのテキスト自体」を使ってカーソル位置を特定させ、テキストが一意でない場合だけ前後の接頭辞・接尾辞で曖昧さを解消させます。

変化した部分だけ送る:増分 diff

これはいちばんコストを節約できる一方で、いちばん見落とされやすい点でもあります。バイナリには、モデル向けの次のような文言がそのまま入っています。

The following is a diff from the previous accessibility tree
The following is a cumulative diff from the initial accessibility tree
There has been no change in the accessibility tree for …

初回は完全なツリーを渡し、その後の各ターンでは差分だけを送ります。前のターンとの差分、または初期状態からの累積差分です。変化がなければ、単に「変化なし」と伝えます。大型 App の完全なツリーは数万 token になることもあり、毎ターン再送するのはコストを燃やしているようなものです。

diff が可能になる前提は、その要素番号がターンをまたいで安定していることです。これは常駐 service が、トランザクション化され、無効化可能なツリー構造を維持しているから実現できます(クラス名は UIElementTreeTransactionUIElementTreeInvalidationMonitor)。純粋な視覚ベースでステートレスな方式では、これはできません。まさにこれこそが、双プロセスのステートフルなアーキテクチャによって得られる見返りです。

入力はどう打ち込まれるのか、そして目立たないガードレール

入力は脆い AppleScript ではなく、Core Graphics のイベント合成を通ります。CGEvent / CGEventSource でマウスとキーボードを合成し、文字列を仮想キー列に変換します(修飾キー込み、キーボード配列非依存)。対応しているコントロールには、直接 AXPress を送ることもできます。

本当に差がつくのはガードレールです。こういうものは demo ではまったく要りませんが、長時間走らせると痛みがわかります。

  • フォーカス横取り防止SystemFocusStealPreventerSyntheticAppFocusEnforcer):合成入力で最大の落とし穴は、入力中にフォーカスが別のウィンドウや通知に奪われ、キー入力が間違った App に落ちることです。対象を前面に固定するための専用サブシステムがあります。
  • ロック画面防止CUALockScreenGuardian):ロック画面では、macOS はアクセシビリティに対してすべての App ウィンドウを隠します。これは後で私が再現したとき、身をもってぶつかった点です。
  • Electron を点灯させるAXManualAccessibility):Chromium / Electron はデフォルトでは DOM ツリーをアクセシビリティに公開しません。この種の App に AXManualAccessibility=true を設定し、その場で Chromium にツリーを構築させます。だから Codex はブラウザ、VS Code、Slack でも、単なるスクリーンショットではなく構造化要素を取得できます。
  • キャプチャ前ハイライト:スクリーンショットの前に画面上へハイライト枠を描画します(IPC メッセージには CaptureAnimation 系列が含まれます)。これにより、「AI がここを見ている」ことが目に見えます。

自分で止まる:確認ポリシー

プラグインに同梱されている SKILL.md には、かなり抑制の効いた確認ポリシーが埋め込まれています。UI 操作には実際の副作用(データ削除、送金、メッセージ送信)があるため、リスクのある操作を 4 段階に分けています:ユーザー自身に任せる、実行前に毎回確認する、初期 prompt の承認で可とする、常に許可する。クラウド上のデータ削除、新しいソフトウェアのインストール、金融取引、認証コードの解読などは「必ず確認」に分類されています。

また、「ユーザーが自分で入力した指示」と「第三者コンテンツに紛れ込んだ指示」も区別しています。後者(Web ページ、PDF、貼り付けたテキスト)は一律で潜在的に悪意あるものとして扱い、決して承認とは見なしません。

この仕組みがその場で動くのを、私は実際に見ました。公式 Codex に Computer Use で WeChat の連絡先へメッセージを送らせたところ、会話まで移動し、入力欄にフォーカスしたあと、自分から停止して、「第三者にメッセージを送るには確認が必要です。確認してください」と返しました。ポリシーはドキュメント上の飾りではなく、本当に止めに入ります。

-10000 の門:どうやって「OpenAI かどうか」を見分けるのか

門番の小型犬が虫眼鏡で、訪問者の掲げる TEAM ID バッジを確認している。横には通過と偽造の 2 つの札がある

私は一時期、Codex から離れて、自分で直接その MCP を呼ぼうとしました。ハンドシェイクと tools/list はどちらも通るのですが、実際のアクションを呼ぶと、service はこう返してきました。

Computer Use server error -10000: Sender process is not authenticated

この門こそが、その中核となる防衛線です。OpenAI 自身が署名した App だけに駆動を許す。私は検証ロジックをアセンブリ層まで掘り下げました。チェーン全体はこうです。

① 相手側の身元を取得する。カーネルレベルで、偽造不可。 getsockopt(fd, …, LOCAL_PEERTOKEN) で socket から相手側の audit token を取り、さらに audit_token_to_pid で pid に変換します。audit token はカーネルが socket 経由で提供するもので、呼び出し側が「pid を偽って申告する」ことでなりすますことはできません。

② responsible プロセスまで貫通する。 直結しているのは CLI ヘルパー(com.openai.sky.CUAService.cli。それ自体はホワイトリストにありません)です。そのため service は dlopen + dlsym でプライベート API を呼び出し、「このヘルパーに本当に責任を持つ App」、つまり Codex / ChatGPT / Atlas を解析します。対応するキーは senderParentResponsibleIdentity です。このステップによって、「自分で殻を作ってヘルパーを包む」手口は潰されます。

③ カーネルが裏書きしたコードオブジェクトを取得する。 audit token を使って SecCodeCopyGuestWithAttributes(call site は 0x10012b5bc 付近)を呼び、システムのコード署名サブシステムによって検証済みの SecCode を取得します。

④ 署名情報を読む。 SecCodeCopySigningInformation0x10012a2cc)のあと、2 つのフィールドを取り出します。kSecCodeInfoTeamIdentifier(Apple チーム ID)と kSecCodeInfoIdentifier(bundle / 署名識別子)です。

⑤ 照合する。 チーム ID は 2DC432GLL2(OpenAI の開発者チーム)と一致していなければならず、bundle id はホワイトリスト内に収まっている必要があります。com.openai.codex(alpha/beta/dev/nightly を含む)、com.openai.chatcom.openai.atlascom.openai.sky.* です。ヒットすれば通過、そうでなければ例の -10000 になります。

この門がなぜ堅牢なのかは、単独で述べる価値があります。これは「IPC サービスに呼び出し元認証をどう実装するか」の良い手本だからです。

  • audit token はカーネル由来であり、呼び出し側から送られてきたデータではないため、なりすませません。
  • responsible プロセスの解析により、「殻をかぶせる」だけでは通用しません。
  • SecCode は audit token から取得され、カーネルに裏書きされています。未署名 / ad-hoc / 再署名されたバイナリでは、チーム ID 2DC432GLL2 を提示できません。

ただし第 ④ ステップには、この門における唯一の弱点が隠れています。次の節では、そこから通り抜けます。

門をくぐる:自分で読み出したその文字列を差し替える

前節の ① ② はカーネルが与えるもので、偽造できません。誰が接続しているのか、誰がそれに責任を持つのか、この 2 点は audit token 経由でカーネルに固定されます。しかし、最後に照合されるチーム ID は、そのプロセス自身の中で、普通の動的リンク関数 SecCodeCopySigningInformation を使って読み出されている。カーネルが担当するのは「どのプロセスを調べるべきか」を見分けるところまでで、「そのプロセスの署名に含まれる team id は何か」という文は、クライアント自身のアドレス空間内で、差し替え可能なシンボルによって返されています。

だから私は署名を偽造せず、カーネル側の 2 ステップにも触りません。署名情報を読み終えた後、照合される前に、その辞書の中の team id だけを書き換えます。手段は DYLD interpose、30 行ほどの dylib です。

$ c
static OSStatus my_SecCodeCopySigningInformation(
    SecStaticCodeRef code, SecCSFlags flags, CFDictionaryRef *info) {
  OSStatus st = SecCodeCopySigningInformation(code, flags, info);   // まず本物を呼ぶ
  if (st == errSecSuccess && info && *info) {
    // 返された辞書内の team id を OpenAI の 2DC432GLL2 に書き換える
    CFMutableDictionaryRef m = CFDictionaryCreateMutableCopy(NULL, 0, *info);
    CFDictionarySetValue(m, kSecCodeInfoTeamIdentifier, CFSTR("2DC432GLL2"));
    CFRelease(*info);
    *info = m;
  }
  return st;
}
// __DATA,__interpose セクションで、本物の関数を上の替え玉に向ける

DYLD_INSERT_LIBRARIES=team_hook.dylib でプロセスを起動すると、それ以降、署名を読むたびに team id は 2DC432GLL2 になります。実測では、この 1 フィールドを変えるだけで門を通過できました。注入前は list_apps が -10000 を返し、注入後はそのまま実際の app 一覧を返します。

これは前節で仕込んだ伏線の裏づけにもなります。SecCodeCheckValidityWithErrors に designated requirement を渡して「これは 2DC432GLL2 によって署名されたものか」をカーネルに判定させているのではなく、自分で team id を文字列として読み出してから比較している。前者なら判定はシステム内に残り、私は手を出せません。後者では、最後の一文を、私が差し替えられるユーザー態関数に委ねています。信頼のアンカーはあのチーム ID に見えますが、本当のアンカーは「誰がそれを読むのか」であり、そこで相手は自分自身で読むことを選んでいました。

(ついでに、初期の誤判定も訂正しておきます。一時期、バイナリ内の 3 本の分岐を NOP 化すれば回避できると思っていました。その 3 本は 0x100019a00 にあり、逆アセンブルすると NSError の description getter でした。書き換えてもエラーメッセージが乱れるだけで、権限は 1 つも gate していません。本当のスイッチは上のフックです。)

Claude Code に接続する

TEAM ID 2DC432GLL2 と書かれた社員証を持った探偵ビーグルが -10000 の入退室ゲートに差し出し、扉が開き、その奥に腕を広げた Claude Code ロボットがいる

門を通ったら、残るのは Claude Code に接続することです。Claude Code は MCP server としてツールを読み込みます。そして SkyComputerUseClient mcp 自体が stdio で動く MCP server であり、公開しているのはまさに get_app_state / click / type_text などの 10 個のツールです。1 行登録すればよく、落とし穴は 1 つだけです。

computer-use という名前は Claude Code 内では予約名で、読み込みが黙って拒否されます。 別の名前、たとえば mac-computer-use にします。

$ bash
claude mcp add mac-computer-use --scope user \
  -e DYLD_INSERT_LIBRARIES="$HOME/.codex/computer-use/team_hook.dylib" -- \
  "…/SkyComputerUseClient.app/Contents/MacOS/SkyComputerUseClient" mcp

-e でフックを持ち込み、Claude Code を再起動すると、その 10 個のツールが現れます。私はこれで WhatsApp に対して get_app_state を一度呼びました(注入前なら -10000 になるあの呼び出しです)。すると完全なアクセシビリティツリーがそのまま返り、続いて要素番号で送信ボタンをクリックしたところ、メッセージが送信されました。使っているのはアクセシビリティツリーで、最初から最後までスクリーンショットは撮っていません。

パッチ適用、フックのコンパイル、MCP 登録の数ステップは install.sh にまとめてあります。リポジトリは leeguooooo/computer-use で、curl … | sh の 1 行でインストールできます。境界も明確にしておきます。これは自分のマシンで、自分が入れた Codex を、自分が普段使う別の agent と相互運用させるためのものです。システムレベルの権限(アクセシビリティ / 画面収録)は今でも macOS の TCC が管理しており、迂回できませんし、迂回すべきでもありません。パッチ後の初回起動では、これまでどおりシステム権限のダイアログが出るので、許可を押せばそれで済みます。

それに倣って、opencua というものを書いた

2 匹の小さなビーグルが並んでいて、左が完成品、右が手描きの再現版

接続する前に、もうひとつ検証したいことがありました。この一式の中核は、公開されている macOS API だけで再現できるのか。そこで自分の理解に沿って、およそ 600 行の Swift で clean-room 版を書きました。名前は opencua。同じようにアクセシビリティツリー + スクリーンショット + CGEvent を使い、OpenAI のコードはゼロ、自分の署名で動かしています。

シンプルなネイティブ App では、ほぼ 1:1 でした。TextEdit の状態を取らせると、出力されたツリーは公式版のその時の結果と行単位で対応していました。

公式 Codex自作の opencua
ルートノード0 standard window Untitled0 standard window "Untitled 3"
テキスト領域2 text entry area (settable, string) Value: …2 text entry area (settable) value=…
フォーカスThe focused UI element is 2focused element index = 2

スクリーンショット、クリック、文字入力もすべて通りました。これを使って TextEdit に «» を含む文章を入力し(非 ASCII も注入できます)、さらに bold のチェックボックスをクリックして、読み戻した値が 0 から 1 に変わったことも確認しました。観察 → 操作 → 再観察、という閉ループが動いたわけです。

分解している途中で、逆向きに立てた結論が現実にその場で反証されたことが 2 つありました。ひとつは Chrome の状態を取ったとき、ウィンドウ数が 0 で、メインウィンドウとして返ってきたのも App 自身だったことです。これはまさに、Chromium が AXManualAccessibility を有効にしない限りツリーを公開しない、という挙動でした。もうひとつは、しばらくのあいだ、どの App の状態を取ってもウィンドウが 0 個と返ってきたことです。かなり調べた末に、マシンがロック画面になっていたと分かりました。ロック中の macOS はアクセシビリティに対して全ウィンドウを隠します。これで、なぜ CUALockScreenGuardian を持っているのかも、ちょうど説明がつきました。

そして、現実世界の壁にぶつかります。opencua に WeChat を操作させて、連絡先を検索し、メッセージを送らせてみました。検索ボックスにはフォーカスでき、文字も入力できます。しかし検索結果は仮想化されたセルの列で、名前はレンダリングされる前にはそもそもアクセシビリティに入ってきません。私のシリアライザが取れたのは、空の virtual_cell の山だけでした。一方、公式版は連絡先名を安定して読み取れます。差はここにあります。

今回の実測は、むしろ分解時の判断を裏づけるものでした。この一式の堀はアルゴリズムにはありません。中核の数百行は再現できます。本当に難しいのは、目立たない残り 20% のプロダクション級の磨き込みです。仮想化リスト、フォーカス管理、タイミングのリトライ、各社の自前描画コントロールへの個別対応。プロトタイプでは削り出せない部分で、オリジナル版はそこを長い時間かけて磨いているのです。

分解し終えて覚えていること

もし自分で macOS computer use を作るなら、次の点はそのまま真似すると思います。

  • アクセシビリティツリーを優先し、スクリーンショットで補完する。各操作は二重にアドレス指定する(番号 / ピクセル)。これが「正確にクリックできる」根っこです。
  • 状態を持つ二重プロセス。短命のフロントエンド + 常駐エンジン。ターン間でツリー状態を保つことが、差分 diff と安定した番号付けの前提になります。
  • ツリーは増分 diff で送る。初回は全量、その後は差分だけ。長いタスクの token コストは、ここが分水嶺です。
  • モデルに渡すツリーは純テキスト + 行頭番号 + Tab インデントにする。「変更できるかどうか」を括弧の中に埋め込みます。
  • AXManualAccessibility で Chromium / Electron を点灯させる。そうしないとブラウザや多くの Electron アプリはスクリーンショット頼みになります。
  • ガードレールがプロダクションと demo の境界。フォーカスの横取り防止、ロック画面対策、キャプチャ前のハイライト、アプリごとの権限付与。
  • 認証で最後の一言をユーザー空間に渡さない。audit token で身元を取り、responsible プロセスを解決するところまでは正しいのですが、最後に自分で team id を読み出して文字列比較するなら、ローカルで注入できる人は interpose 一発で通れてしまいます。SecCodeCheckValidityWithErrors に designated requirement を渡してカーネルに判定させるか、この弱点を認めるしかありません。私が Claude Code に接続したとき、通ったのはまさにそこでした。

それが「最良」なのは、モデルが強いからではなく、engineering が accessibility という API 群を食い尽くしているからです。スクリーンショットはあくまで保険で、本当の媒体は diff 可能でモデルが読めるアクセシビリティツリーです。そこにフォーカス、ロック画面、ハイライト、段階的確認といった目立たない要素を重ねて、信頼性を支えています。堀は prompt ではなく、そういう場所にあります。あの署名の門も同じエンジニアリングの質感を持っています。ただ最後の一歩をユーザー空間に置いたために、この文章の後半が生まれたのです。

次の記事 →
一目で偽物だとわかる、敗因はリズム

コメント

コメントは即時公開されますが、ポリシー違反時は非表示になる場合があります。

最大 1000 文字。