0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ブラウザを触れるAI agentは、E2Eを書く前に権限境界を決める

0
Posted at

ブラウザを触れるAI agentは、E2Eを書く前に権限境界を決める

localhostの表示崩れを見てもらうために、coding agentへ普段使いのChrome profileをそのまま渡す。手早く試せますが、agentに見せる必要のない状態まで一緒に渡ります。

自分のcookieが残ったtab、管理画面を開いたsession、保存済みの入力内容。その状態で「productionには行かないで」とpromptへ書いても、あまり安心できません。

GitHub CopilotのBrowser toolsでは、VS Codeのagentがページ遷移、クリック、入力、画面の読み取り、console確認、screenshot取得、scripted flowの実行まで扱えます。UI修正と確認が一つのloopに入るのは便利です。ただし、クリック精度を評価する前に決めることがあります。

  • どのsessionを見せるか
  • どのoriginへ接続させるか
  • どの操作で止めるか
  • 何を実行証拠として残すか

Playwrightを外側のharnessとして使い、この4つを小さく実装します。Copilot Browser toolsの内部設定を再現するものではありません。統合先が変わっても使える境界の作り方に絞ります。

promptの禁止事項はsecurity boundaryではない

「productionを開かない」「submitしない」はtaskの意図を伝えるには有効です。けれど、指示を忘れたrunでnetwork requestを止めてはくれません。

自分なら次の3層に分けます。

役割
policy text agentへ目的と停止条件を伝える submit直前で停止する
runtime enforcement session、network、actionを機械的に制限する 専用context、origin allowlist、POST拒否
evidence agentの外側で結果を確認する trace、console log、screenshot、exit status

Microsoft LearnのAL向け事例でも、build、publish、diagnostics、debugを明示的なtool surfaceとしてagentへ渡し、構造化されたerror hintを返しています。対象はBusiness Centralですが、「開発環境を何でも触れるshellではなく、戻り方を持つAPIとして渡す」という設計はbrowser操作にも使えます。

Googleのsecure agentic coding Codelabも、最後のreviewだけに頼らず、test、pre-commit、agent hook、local repair loopへguardrailを置いています。promptを厚くするより、失敗する場所を実行経路へ作るほうが追いやすいです。

最初に境界表を書く

実装へ入る前に、1枚の表へ落とします。曖昧な行は、そのまま未設計の権限です。

境界 許可するもの 拒否するもの / 人へ戻すもの 検証方法
session 専用test account 個人、admin、実決済のsession account表示とcookie名を確認
origin localhost、専用staging production、未登録redirect先 routeのblock log
action navigate、read、screenshot submit、upload、download、permission付与 tool eventと停止位置
evidence console、trace、screenshot、test結果 agentの「確認済み」という文章だけ 別processでartifactを確認

productionは最初からallowlistへ入れません。localhostとstagingも、同じrunで両方必要なければ設定を分けます。

使い捨てのbrowser contextを作る

まず、普段使いのbrowser profileから切り離します。

import { mkdir } from "node:fs/promises";
import { chromium } from "@playwright/test";

await mkdir("artifacts/agent-run", { recursive: true });

const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
  // 専用test accountから作ったstateだけを読む
  storageState: "tests/.auth/agent-test-user.json",
  permissions: [],
  acceptDownloads: false,
  serviceWorkers: "block",
});

await context.tracing.start({
  screenshots: true,
  snapshots: true,
});

const page = await context.newPage();

try {
  // このpageをbrowser agentまたはE2Eの入口にする
} finally {
  await context.tracing.stop({ path: "artifacts/agent-run/trace.zip" });
  await context.close();
  await browser.close();
}

storageStateには専用test accountだけを入れます。個人accountから書き出したstateを流用すると、contextを分けた意味が薄れます。state fileはcommitせず、期限の短いcredentialで作り直せるようにします。

run中に増えたcookieやlocalStorageは、context.close()で捨てます。次のrunでは保存済みの初期stateから新しいcontextを作るので、前回の探索結果を引き継ぎません。

permissions: []はpermissionを事前付与しない設定です。あらゆるpermission promptをOSやbrowser policyのレベルで拒否する設定と同じではありません。hard denyが必要なら、agent側のtool permissionや管理対象browser policyでも止めます。ここは「空配列にしたから安全」と読み替えないほうがいいです。

origin allowlistを実行時に強制する

次に、requestをoriginで止めます。観測専用runでは、変更系のHTTP methodも同じ場所で拒否できます。

const allowedOrigins = new Set([
  "http://localhost:3000",
  "https://staging.example.com",
]);

const readOnlyMethods = new Set(["GET", "HEAD", "OPTIONS"]);
const blockedRequests: Array<{
  reason: "origin" | "method";
  method: string;
  url: string;
}> = [];

await context.route("**/*", async (route) => {
  const request = route.request();
  const url = new URL(request.url());

  if (!allowedOrigins.has(url.origin)) {
    blockedRequests.push({
      reason: "origin",
      method: request.method(),
      url: request.url(),
    });
    return route.abort("blockedbyclient");
  }

  if (!readOnlyMethods.has(request.method())) {
    blockedRequests.push({
      reason: "method",
      method: request.method(),
      url: request.url(),
    });
    return route.abort("blockedbyclient");
  }

  await route.continue();
});

この例はかなり厳しめです。staging API、CDN、認証redirectなどが別originなら、画面は途中で壊れます。それで構いません。最初から*へ逃げず、block logを見て必要なoriginだけ足します。

GraphQLのqueryがPOSTを使うアプリでは、観測だけでもPOST許可が必要になるかもしれません。その場合は「POSTを全部許可」ではなく、staging originとpath、operation名まで絞った別ruleにします。GETで副作用を起こすendpointがあるならmethod gateでは守れないので、endpoint単位のdenyかnetwork proxy側の制限が必要です。

context.route()だけを完全なnetwork sandboxとも考えません。browser統合、WebSocket、拡張機能など、harnessの外を通る経路がある構成では、proxyやfirewallも併用します。

action gateはbrowser toolの手前に置く

HTTP requestを止めても、file pickerでローカルfileを選ぶ操作そのものは起きています。upload、download、permission付与のような能力は、agentへ公開するtoolの時点で落としたいです。

wrapperに置く判定は、この程度の小さな関数から始められます。特定agentのschemaには依存していません。

type BrowserAction =
  | { kind: "navigate"; url: string }
  | { kind: "read" }
  | { kind: "screenshot" }
  | { kind: "submit" }
  | { kind: "upload"; path: string }
  | { kind: "download" }
  | { kind: "grant-permission"; name: string };

type Decision =
  | { allow: true }
  | { allow: false; reason: string };

function decideBrowserAction(action: BrowserAction): Decision {
  switch (action.kind) {
    case "navigate":
    case "read":
    case "screenshot":
      return { allow: true };
    default:
      return {
        allow: false,
        reason: `${action.kind}は観測runでは許可されていません`,
      };
  }
}

実際には、利用中のagentが出すtool名と正規化済みinputへ合わせます。shell文字列を正規表現で推測するより、uploadsubmitを独立したactionとして扱えるtool surfaceのほうがgateを書きやすいです。

変更操作が必要になったら、この関数へ例外を増やすのではなく、観測runとは別の承認済みrunbookを作ります。test account、対象path、許可する1操作、後片付けまで固定したrunです。

task promptは目的より停止条件を詳しくする

runtimeで止めたうえで、agentにも狭いtaskを渡します。自分が使うなら、このくらいまで書きます。

目的:
  stagingのcheckout画面で不具合を再現し、console errorとscreenshotを返す

許可:
  navigate / click / read / screenshot

禁止:
  production / 実決済 / submit / upload / download / permission grant

停止:
  allowlist外への遷移
  submit直前
  認証情報の再入力を要求された時点

返却:
  操作手順
  最終URL
  console error
  screenshotとtraceのpath
  完了できなかった場合は停止理由

promptは探索範囲を狭め、止まった理由を揃えるために使います。origin allowlistやaction gateの代用品ではありません。

happy pathより先に境界をtestする

表示確認が成功しただけでは、境界が効いているか分かりません。Playwrightを入れたprojectなら、通常のE2Eとは別に境界用specを置きます。

pnpm add -D @playwright/test
pnpm exec playwright install chromium
pnpm exec playwright test tests/agent-boundary.spec.ts --trace on

最初に確認するのは次のfailure pathです。

  1. https://example.comへの直接遷移がblockedbyclientで失敗し、block logへ残る。
  2. stagingから未登録originへredirectしても、遷移が完了しない。
  3. formを送信してもPOSTがblockされ、server側のtest dataが変わらない。
  4. 個人・admin accountのcookieがcontextに存在しない。
  5. contextを作り直すと、前runで追加したlocalStorageとsessionStorageが消えている。
  6. agentの最終文とは別に、console log、screenshot、trace、test exit statusが残る。

3番はrequestがblockされたことだけで終わらせず、staging APIやfixture側でも状態が変わっていないことを確認します。実装と採点を同じagentへ任せると、「止まりました」という文章だけがきれいに返ってくることがあります。

任せる範囲は環境ごとに変える

自分の初期設定はこうです。

利用場面 任せ方
localhostの表示崩れ、console調査 専用contextとlocalhost allowlistで自動化する
stagingの回帰確認 test account、action gate、trace保存を付ける
SSO、決済、admin操作 観測と再現まで。変更直前で人へ戻す
production default deny。別の承認済みrunbookがなければ渡さない

UI修正から実画面の確認まで閉じるloopはかなり使いたいです。そのため、最初のrunでは普段使いのprofileを渡さず、localhostのreadとscreenshotだけを許可します。外部redirectを意図的に踏ませ、ちゃんと失敗することを確認する。そこまで通ってから、stagingの操作を一つずつ増やします。

promptへ禁止事項を追記し続けるより、失敗できる小さなbrowser環境を作るほうが、次のrunでも同じ条件を使えます。

参考資料

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?