ブラウザを触れる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文字列を正規表現で推測するより、uploadやsubmitを独立した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です。
-
https://example.comへの直接遷移がblockedbyclientで失敗し、block logへ残る。 - stagingから未登録originへredirectしても、遷移が完了しない。
- formを送信してもPOSTがblockされ、server側のtest dataが変わらない。
- 個人・admin accountのcookieがcontextに存在しない。
- contextを作り直すと、前runで追加したlocalStorageとsessionStorageが消えている。
- 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でも同じ条件を使えます。