Browser Sub-Agent とは
Google Antigravity IDE の Browser Sub-Agent は、AI エージェントに「手」を与えるための機能です。従来の Web スクレイピングツールやテスト自動化ツールと異なり、自然言語命令を理解した AI が、ブラウザを通じて人間のように Web サイトを操作します。
具体的には以下が可能になります:
- Web スクレイピング: JavaScript レンダリングに対応した複雑なサイトからのデータ抽出
- フォーム自動入力: CAPTCHA 回避(自社サイトの場合)、多段階ログイン、複雑なチェックボックス選択
- E2E テスト: ユーザーシナリオに基づいた自動テスト実行・スクリーンショット取得
- 自動化ワークフロー: 日次業務の完全自動化(例: 競合商品の価格監視、定期レポート生成)
Browser Sub-Agent のアーキテクチャ
内部動作フロー
┌─────────────────────────────────────┐
│ AI Agent (自然言語命令受け取り) │
└──────────────┬──────────────────────┘
│
↓
┌─────────────────────────────────────┐
│ Intent Parser │
│ 命令を「アクション」に分解 │
│ - "ボタンをクリック" → click(xy) │
│ - "テキスト入力" → type(selector) │
└──────────────┬──────────────────────┘
│
↓
┌─────────────────────────────────────┐
│ DOM Interpreter │
│ セレクター解析・要素位置計算 │
└──────────────┬──────────────────────┘
│
↓
┌─────────────────────────────────────┐
│ Chromium Browser Instance │
│ (Headless or Headful mode) │
│ - ページロード │
│ - インタラクション実行 │
│ - スクリーンショット取得 │
└──────────────┬──────────────────────┘
│
↓
┌─────────────────────────────────────┐
│ Output Processor │
│ - DOM スナップショット │
│ - スクリーンショット │
│ - テキスト抽出 │
└─────────────────────────────────────┘
重要な制限と対応策
| 制限事項 | 対応策 |
| JavaScript レンダリング遅延(3~5秒) | waitForTimeout・waitForSelector で待機 |
| ネットワーク速度に依存 | タイムアウト値を長めに設定(デフォルト 30秒) |
| CORS ポリシーによる API 呼び出し不可 | Server-side proxy を用意 |
| セッション保持(クッキー) | persistCookies: true で Cookie を保存 |
Web スクレイピング:実装例
基本パターン
// browser-scraper.ts
import { launch } from 'puppeteer';
interface ScrapedProduct {
title: string;
price: number;
rating: number;
url: string;
}
class EcommerceScraper {
private browser: any;
async initialize() {
this.browser = await launch({
headless: true,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-blink-features=AutomationControlled', // ロボット検出回避
],
});
}
async scrapeProductListing(
baseUrl: string,
pageCount: number = 3
): Promise<ScrapedProduct[]> {
const results: ScrapedProduct[] = [];
const page = await this.browser.newPage();
// User-Agent をスプーフィング
await page.setUserAgent(
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
);
for (let i = 1; i <= pageCount; i++) {
const url = `${baseUrl}?page=${i}`;
console.log(`Scraping: ${url}`);
try {
// ページ読み込み待機
await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
// 動的コンテンツの読み込み待機
await page.waitForSelector('.product-item', { timeout: 10000 });
// DOM から製品情報を抽出
const products = await page.evaluate(() => {
const items = document.querySelectorAll('.product-item');
return Array.from(items).map((item) => {
const titleEl = item.querySelector('.product-title');
const priceEl = item.querySelector('.product-price');
const ratingEl = item.querySelector('.product-rating');
const linkEl = item.querySelector('a.product-link');
return {
title: titleEl?.textContent?.trim() || 'N/A',
price: parseFloat(
priceEl?.textContent?.replace(/[^\d.]/g, '') || '0'
),
rating: parseFloat(ratingEl?.textContent || '0'),
url: linkEl?.getAttribute('href') || '',
};
});
});
results.push(...products);
console.log(`Page ${i}: ${products.length} products found`);
// レート制限回避: ページ間に遅延を入れる
await page.waitForTimeout(2000 + Math.random() * 3000);
} catch (error) {
console.error(`Error scraping page ${i}:`, error);
// フォールバック: AI に手動確認を依頼
}
}
await page.close();
return results;
}
async close() {
await this.browser.close();
}
}
// 使用例
async function main() {
const scraper = new EcommerceScraper();
await scraper.initialize();
const products = await scraper.scrapeProductListing(
'https://example-ecommerce.com/products',
3
);
console.log(`Total products scraped: ${products.length}`);
console.log(JSON.stringify(products, null, 2));
await scraper.close();
}
main().catch(console.error);
レート制限への対応
class RateLimitedScraper {
private requestTimes: number[] = [];
private maxRequestsPerMinute: number = 10;
async ensureRateLimit() {
const now = Date.now();
const oneMinuteAgo = now - 60 * 1000;
// 1分以内のリクエストを数える
this.requestTimes = this.requestTimes.filter((t) => t > oneMinuteAgo);
if (this.requestTimes.length >= this.maxRequestsPerMinute) {
// レート制限に引っかかった場合、待機
const waitTime = 60 * 1000 - (now - this.requestTimes[0]);
console.log(`Rate limit reached. Waiting ${waitTime}ms...`);
await new Promise((r) => setTimeout(r, waitTime));
}
this.requestTimes.push(now);
}
async scrapePage(url: string) {
await this.ensureRateLimit();
// スクレイピング処理...
}
}
出力例:
[
{
"title": "高性能 AI ノートパソコン",
"price": 1580,
"rating": 4.8,
"url": "https://example.com/product/ai-laptop-pro"
},
{
"title": "USB-C ドッキングステーション",
"price": 25800,
"rating": 4.5,
"url": "https://example.com/product/usb-c-dock"
}
]
フォーム自動入力とログイン
複雑なチェックボックスと条件分岐
class FormFiller {
async fillComplexForm(page: any) {
// ステップ1: テキスト入力フィールドを埋める
await page.type('#name-input', 'Masaki Hirokawa');
await page.type('#email-input', 'user@example.com');
// ステップ2: ラジオボタン選択
await page.click('input[value="business"]');
// ステップ3: 条件分岐(選択内容に応じて出現するフィールド)
await page.waitForSelector('#company-name-field', { timeout: 5000 });
await page.type('#company-name-field', 'Dolice Labs');
// ステップ4: マルチセレクト(複数選択可能)
const tags = ['AI Development', 'Web Automation', 'Agent System'];
for (const tag of tags) {
// Cmd/Ctrl + click で複数選択
await page.click(`label:contains("${tag}")`, { modifiers: ['Control'] });
}
// ステップ5: ファイルアップロード
const uploadInput = await page.$('input[type="file"]');
await uploadInput.uploadFile('/path/to/resume.pdf');
// ステップ6: 利用規約に同意
await page.click('#terms-checkbox');
await page.click('#privacy-checkbox');
// ステップ7: 送信ボタンをクリック
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle0' }),
page.click('#submit-button'),
]);
console.log('Form submitted successfully');
}
}
セッション保持とログイン状態の維持
class AuthenticatedSession {
private cookiesFile = './session-cookies.json';
async saveCookies(page: any) {
const cookies = await page.cookies();
require('fs').writeFileSync(this.cookiesFile, JSON.stringify(cookies, null, 2));
}
async restoreCookies(page: any) {
if (require('fs').existsSync(this.cookiesFile)) {
const cookies = JSON.parse(
require('fs').readFileSync(this.cookiesFile, 'utf8')
);
await page.setCookie(...cookies);
console.log('Cookies restored from previous session');
}
}
async login(page: any, email: string, password: string) {
await page.goto('https://example.com/login');
// ログイン処理
await page.type('#email', email);
await page.type('#password', password);
await page.click('button[type="submit"]');
// ログイン完了待機
await page.waitForNavigation({ waitUntil: 'networkidle0' });
// クッキーを保存(次回以降の復元用)
await this.saveCookies(page);
}
async accessProtectedPage(url: string) {
const browser = await launch();
const page = await browser.newPage();
// 前回のセッションを復元
await this.restoreCookies(page);
await page.goto(url);
return await page.content();
}
}
E2E テスト自動化
ユーザーシナリオベースのテスト
class E2ETestRunner {
async testCheckoutFlow(baseUrl: string) {
const browser = await launch();
const page = await browser.newPage();
try {
// シナリオ1: 商品検索
console.log('Test 1: Search for product');
await page.goto(`${baseUrl}/`);
await page.type('#search-input', 'AI notebook');
await page.click('#search-button');
await page.waitForSelector('.product-grid', { timeout: 10000 });
// シナリオ2: 商品詳細ページへ移動
console.log('Test 2: Navigate to product detail');
await page.click('a.product-link:first-child');
await page.waitForSelector('.product-detail', { timeout: 5000 });
// シナリオ3: カートに追加
console.log('Test 3: Add to cart');
await page.click('#add-to-cart-button');
const cartBadge = await page.$eval(
'.cart-count',
(el) => el.textContent
);
console.assert(cartBadge === '1', 'Cart should have 1 item');
// シナリオ4: チェックアウト
console.log('Test 4: Proceed to checkout');
await page.click('a[href="/checkout"]');
await page.waitForSelector('#shipping-form', { timeout: 5000 });
// シナリオ5: 配送情報入力
console.log('Test 5: Fill shipping info');
await page.type('#address', '123 Tech Street, San Francisco');
await page.select('#country', 'US');
// シナリオ6: 決済情報入力(本番では Stripe テストキーを使用)
console.log('Test 6: Fill payment info');
// iframe 内の Stripe フォーム
const stripeFrame = page.frames().find((f) =>
f.url().includes('stripe.com')
);
if (stripeFrame) {
await stripeFrame.type('[placeholder="Card number"]', 'YOUR_TEST_CARD_NUM');
}
// シナリオ7: 注文完了
console.log('Test 7: Complete order');
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle0' }),
page.click('#place-order-button'),
]);
// 確認ページのスクリーンショット
const screenshot = await page.screenshot({
path: './test-results/order-confirmation.png',
});
console.log('✓ Checkout flow completed successfully');
return true;
} catch (error) {
console.error('✗ Test failed:', error);
await page.screenshot({ path: './test-results/error-screenshot.png' });
return false;
} finally {
await browser.close();
}
}
}
// テスト実行
const runner = new E2ETestRunner();
runner.testCheckoutFlow('https://example-ecommerce.com')
.then((success) => process.exit(success ? 0 : 1));
本番環境でのセキュリティ設計
認証情報の安全な管理
class SecureAutomation {
private credentialsManager: CredentialsManager;
async runSecurelyWithCredentials(task: string) {
// 認証情報を環境変数または Vault から取得
const credentials = await this.credentialsManager.get(task);
// メモリ内暗号化
const decrypted = await this.credentialsManager.decrypt(credentials);
// 使用後は即座にメモリをクリア
try {
return await this.executeTask(task, decrypted);
} finally {
// セキュアクリア(オーバーライト)
decrypted.password = Buffer.alloc(decrypted.password.length, '\0');
}
}
}
IP ローテーションとプロキシ
class ProxyRotatingBrowser {
private proxyList = [
'http://proxy1.example.com:8080',
'http://proxy2.example.com:8080',
'http://proxy3.example.com:8080',
];
async launchWithProxy(proxyIndex: number = 0) {
const proxy = this.proxyList[proxyIndex % this.proxyList.length];
return await launch({
args: [`--proxy-server=${proxy}`],
});
}
async scrapeWithRotation(urls: string[]) {
for (let i = 0; i < urls.length; i++) {
const browser = await this.launchWithProxy(i);
const page = await browser.newPage();
try {
await page.goto(urls[i]);
// スクレイピング処理...
} finally {
await browser.close();
}
}
}
}
リクエストヘッダースプーフィング
async function setupRealisticHeaders(page: any) {
// 人間らしいリクエストヘッダーを設定
await page.setExtraHTTPHeaders({
'Accept-Language': 'ja-JP,ja;q=0.9,en-US;q=0.8,en;q=0.7',
'Accept-Encoding': 'gzip, deflate, br',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Cache-Control': 'max-age=0',
'DNT': '1',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1',
});
// JavaScript による ブラウザ自動化検出回避
await page.evaluateOnNewDocument(() => {
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined,
});
Object.defineProperty(navigator, 'plugins', {
get: () => [1, 2, 3, 4, 5], // ダミープラグイン
});
// その他のチェック回避...
});
}
ベストプラクティス
-
エラーハンドリングは徹底的に
- ネットワーク障害・タイムアウト・CAPTCHA への対応を実装
- フェイルオーバー戦略を用意(リトライ・ログと通知)
-
ページ読み込み待機を適切に
waitForNavigation(): ページ遷移時
waitForSelector(): 動的コンテンツ読み込み時
waitForTimeout(): は最後の手段(できるだけ避ける)
-
メモリリークを防ぐ
- ブラウザインスタンスを適切にクローズ
- 大量の Cookie/ローカルストレージをクリア
-
ログを活用する
- 各ステップでログを記録
- スクリーンショットを取得(失敗時は必須)
自律実行の権限設計 —「黙って自動承認」から学ぶ
Browser Sub-Agent を夜間の自動処理に組み込むと、必ず向き合うことになるのが「どこまでをエージェントの判断に委ねるか」という問いです。
2026年7月の Antigravity CLI 1.1.3 では、ヘッドレス実行(-p)にまつわる二つの修正が入りました。確認が必要なツールでエージェントが固まる、あるいは黙って自動承認してしまう挙動が改められ、今後はソフト拒否したうえで、許可に必要な allow ルール名を stderr に示すようになっています。always-proceed モードでワークスペース外へファイルを書き込む操作が誤って自動承認されていた問題も塞がれました。
この修正が静かに示しているのは、「自動で進める」ことと「何を自動で進めてよいか」は別の設計だという事実です。ブラウザ操作は、とりわけこの線引きが効いてきます。フォーム送信ひとつが決済や退会につながる場面では、自動承認の既定値がそのまま事故の入口になります。
私自身、個人開発で続けている壁紙アプリの運用で、管理画面の在庫チェックを自動化したとき、最初はすべてのブラウザ操作を無条件に通していました。ある晩、セッション切れの状態でエージェントがログインフォームに空の認証情報を送り続け、アカウントが一時的にロックされたことがあります。ログが薄かったせいで、原因にたどり着くまで丸一日を費やしました。それ以来、ブラウザ操作を「読み取り専用」「状態変更」「不可逆」の三層に分け、後ろの二つには必ず allow ルールと事前確認を挟むようにしています。個人開発では、こうした事故の復旧を代わってくれる人がいません。だからこそ、止まる設計を先に用意しておくことが、任せ続けるための前提になります。
次のコードは、その線引きをエージェントの実行前に一枚挟むための、操作分類の最小実装です。
type ActionRisk = "read" | "mutate" | "irreversible";
// 操作の意図を三層に分類する
function classifyAction(action: { type: string; url: string }): ActionRisk {
const irreversible = /\/(delete|cancel|withdraw|charge|pay|checkout)/i;
const mutate = /(submit|post|put|update|create)/i;
if (irreversible.test(action.url)) return "irreversible";
if (mutate.test(action.type)) return "mutate";
return "read";
}
// 分類に応じて自動承認するか、人間の確認を挟むかを決める
async function guard(
action: { type: string; url: string },
allowRules: Set<string>,
) {
const risk = classifyAction(action);
if (risk === "read") return true; // 読み取りは自動で通す
const ruleName = `${risk}:${new URL(action.url).hostname}`;
if (!allowRules.has(ruleName)) {
// ソフト拒否。必要な allow ルール名を明示して人間に委ねる
console.error(`[guard] blocked ${risk}. add allow rule: ${ruleName}`);
return false;
}
return true;
}
ポイントは、拒否したときに「どの allow ルールを足せば通るか」を必ず出力することです。CLI 1.1.3 が stderr に allow ルール名を示すようになったのと同じ発想で、止まった理由が翌朝ログから一目で分かる状態にしておきます。ここが曖昧だと、私が一日を溶かしたのと同じ切り分けコストを毎回払うことになります。
任せる範囲を決める判断フレームワーク
操作を三層に分けたら、それぞれに既定の扱いと保護を割り当てます。私が実際に使っている対応表を示します。
| 層 | 操作の例 | 既定の扱い | 必要な保護 |
| 読み取り専用 | ページ遷移、テキスト抽出、スクリーンショット | 自動承認 | レート制限とログのみ |
| 状態変更 | フォーム送信、設定更新、下書き保存 | allow ルール必須 | ドメイン単位の許可+差分ログ |
| 不可逆 | 決済、退会、削除、公開 | 人間の事前確認 | ドライラン+二段階承認+通知 |
この表を眺めていると、判断が難しいのは中央の「状態変更」だと気づきます。読み取りと不可逆は迷いません。曖昧なのは、下書き保存のように「戻せるが影響が残る」操作です。ここは失敗時の影響が金銭やユーザー体験にどれだけ波及するか、そして復旧手順が明文化されているかで、上下どちらの層に寄せるかを決めています。判断軸を数値ではなく「戻せるか」「誰が困るか」で持っておくと、新しい操作が出てきても迷いません。
検証してから任せる — 段階的な移行
権限設計ができたら、いきなり夜間の無人運用に載せず、段階を踏みます。私は次の順序を守っています。
- 手元で有人実行。すべての操作を承認プロンプト付きで一度通し、分類が意図どおりかを目視します。
- 観測メトリクスとアラートを設置。操作種別ごとの成功率・所要時間・拒否回数を記録し、拒否が急増したら通知が飛ぶようにします。
- 限定ロールアウトで本番検証。まず読み取り専用の操作だけを無人化し、一週間ログを見てから状態変更層を解禁します。
- 不可逆層は最後まで人間の手に残す。ここを自動化する誘惑に負けないことが、長く安全に任せ続けるための一線です。
この順序は遠回りに見えますが、事故が起きたときの復旧コストを考えれば、結局いちばん速い道でした。
まとめ
Browser Sub-Agent は、AI に「手」を与える強力な機能です。だからこそ、動かすことと同じ熱量で、任せる範囲を設計する価値があります。今日できる次の一歩として、既存の自動化スクリプトの操作を「読み取り/状態変更/不可逆」の三層に分類し、後ろの二つに allow ルールを一つずつ足してみてください。
私自身もまだ設計を磨いている途中です。実装の参考になれば幸いです。お読みいただき、ありがとうございました。