From a866e232a627349507bf6c541ef6c370812f9341 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Tue, 27 Oct 2026 10:06:45 +0000 Subject: [PATCH 1/5] =?UTF-8?q?Add:=20=E5=88=A9=E7=94=A8=E8=80=85=E3=81=AE?= =?UTF-8?q?=20Chrome=20=E3=82=92=E9=96=8B=E7=99=BA=E3=82=B3=E3=83=B3?= =?UTF-8?q?=E3=83=86=E3=83=8A=E3=81=8B=E3=82=89=20CDP=20=E3=81=A7=E6=93=8D?= =?UTF-8?q?=E4=BD=9C=E3=81=99=E3=82=8B=20Skill=20chrome-cdp=20=E3=82=92?= =?UTF-8?q?=E8=B6=B3=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ログインと目視は利用者の Chrome で行い、大量の操作は Cookie を取り出して コンテナの headless で回す。リモートのホスト(EC2 など)の上のコンテナからは、 SSH の RemoteForward とホストの中継(socat)で届かせる。9222 をネットワークへ 開かないこと、対象の origin の Cookie だけを取ることを決まりにする。 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KYHP7zFtYgAYtprVz6q6a2 --- .claude-plugin/marketplace.json | 2 +- README.md | 6 +- docs/ndf-plugin-reference.md | 14 +- plugins/ndf/.claude-plugin/plugin.json | 3 +- plugins/ndf/.codex-plugin/plugin.json | 3 +- plugins/ndf/README.md | 12 +- plugins/ndf/dev.agy/plugin.json | 2 +- plugins/ndf/dev.agy/skills/chrome-cdp | 1 + plugins/ndf/manifests/agy-skills.txt | 1 + plugins/ndf/manifests/claude-skills.txt | 1 + plugins/ndf/manifests/codex-skills.txt | 1 + plugins/ndf/manifests/kiro-skills.txt | 1 + plugins/ndf/skills/chrome-cdp/SKILL.md | 157 ++++++++++++++++++ .../chrome-cdp/references/local-side-setup.md | 67 ++++++++ scripts/tests/test_agy_distribution.py | 2 +- 15 files changed, 252 insertions(+), 21 deletions(-) create mode 120000 plugins/ndf/dev.agy/skills/chrome-cdp create mode 100644 plugins/ndf/skills/chrome-cdp/SKILL.md create mode 100644 plugins/ndf/skills/chrome-cdp/references/local-side-setup.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index d26d4fcac..efec42665 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ { "name": "ndf", "source": "./plugins/ndf", - "description": "Claude Code plugin (v10.22.0): 8 specialized agents (plus 1 worker agent for the 3-layer workflow) and 48 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, statusline, external AI delegation (Codex/agy), transcript retention guard, and optional Slack notifications.", + "description": "Claude Code plugin (v10.22.0): 8 specialized agents (plus 1 worker agent for the 3-layer workflow) and 49 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, statusline, external AI delegation (Codex/agy), transcript retention guard, and optional Slack notifications.", "policy": { "installation": "AVAILABLE", "authentication": "ON_INSTALL" diff --git a/README.md b/README.md index 987977e04..0a250bb3d 100644 --- a/README.md +++ b/README.md @@ -89,12 +89,12 @@ Claude Code では `/ndf:` と打って呼びます。**Skill** は ### NDF プラグインの Skill の一覧 **NDFプラグイン v10.22.0** に入る Skill の数は、AI ツールごとに違います。 -Claude Code向け core 48個、Kiro向け core 45個、Codex向け core 44個、agy向け core 44個です。 -元Skills(48個)の内訳は次のとおりです。 +Claude Code向け core 49個、Kiro向け core 46個、Codex向け core 45個、agy向け core 45個です。 +元Skills(49個)の内訳は次のとおりです。 - Pull Request とレビュー (7): pr, pr-tests, fix, pr-review, cherry-pick-pr, deploy, merged - 開発の進め方 (17): development-workflow, requirements-design, design, document-rebuild, document-sources, document-drafting, tdd-cycle, refactoring, quality-gates, release, release-verification, retrospective, out-of-scope, progress-tracking, backlog-refinement, layout-review, estimate - 原則と書き方 (11): ndf-policies, implementation-plan, plan-to-spec, investigation-rules, decision-request, problem-solving, logging-guidelines, document-style, notion-writing, issue-plan-strategy, ml-model-structure - - 品質と実行環境 (3): qa-security-scan, docker-container-access, official-skills-autoloader + - 品質と実行環境 (4): qa-security-scan, docker-container-access, chrome-cdp, official-skills-autoloader - 外部サービス (2): google-workspace, document-systems - 複数の AI によるレビューとリファクタリング (3): cross-review, cross-refactoring, external-ai - 作業場所 (1): worktree diff --git a/docs/ndf-plugin-reference.md b/docs/ndf-plugin-reference.md index 37ff524cb..d5e790f5b 100644 --- a/docs/ndf-plugin-reference.md +++ b/docs/ndf-plugin-reference.md @@ -6,10 +6,10 @@ NDF は Claude Code / Codex / Kiro CLI 向けの開発支援プラグイン群 | ランタイム | 導入方法 | 公開 Skill | |---|---|---| -| Claude Code | `.claude-plugin/marketplace.json` の `ndf` | 48 個 | -| Codex | `.claude-plugin/marketplace.json` の `ndf` | 44 個 | -| Kiro CLI | `plugins/ndf/dev.kiro/install.sh` | 45 個 | -| agy | `plugins/ndf/dev.agy/` を `agy plugin install` | 44 個 | +| Claude Code | `.claude-plugin/marketplace.json` の `ndf` | 49 個 | +| Codex | `.claude-plugin/marketplace.json` の `ndf` | 45 個 | +| Kiro CLI | `plugins/ndf/dev.kiro/install.sh` | 46 個 | +| agy | `plugins/ndf/dev.agy/` を `agy plugin install` | 45 個 | Skill の実体は `plugins/ndf/skills/` の 1 箇所だけです。ランタイム別の複製はありません。Skill や 共通スクリプトを変更する場合は `plugins/ndf/` を直接編集します。 @@ -20,7 +20,7 @@ Skill の実体は `plugins/ndf/skills/` の 1 箇所だけです。ランタイ plugins/ndf/ ├── .claude-plugin/plugin.json # Claude Code(agents / hooks / skills 配列) ├── .codex-plugin/plugin.json # Codex(hooks / skills 配列) -├── skills/ # 配布 Skill の唯一の実体(48 個) +├── skills/ # 配布 Skill の唯一の実体(49 個) ├── manifests/ # ランタイム別の配布 Skill 一覧 ├── agents/ # Claude Code のサブエージェント(専門 8 個と、3 層の定義 3 個(supervisor 2・worker 1)) ├── hooks/claude.json # Claude Code の SessionStart / Stop hook @@ -37,8 +37,8 @@ plugin install 時に build を実行する必要はありません。 | Runtime | 読むもの | |---|---| -| Claude Code | `.claude-plugin/plugin.json`(`agents/` 11 個(専門 8 個と、3 層の定義 3 個(supervisor 2・worker 1))、`hooks/claude.json`、`skills` 配列 48 個)、`scripts/` | -| Codex | `.codex-plugin/plugin.json`(`hooks/codex.json`、`skills` 配列 44 個)、`scripts/` | +| Claude Code | `.claude-plugin/plugin.json`(`agents/` 11 個(専門 8 個と、3 層の定義 3 個(supervisor 2・worker 1))、`hooks/claude.json`、`skills` 配列 49 個)、`scripts/` | +| Codex | `.codex-plugin/plugin.json`(`hooks/codex.json`、`skills` 配列 45 個)、`scripts/` | | Kiro CLI | `dev.kiro/`(installer・agent config template・workflow prompts)、`manifests/kiro-skills.txt`、`skills/`、`scripts/` | | agy | `dev.agy/`(マニフェスト・`hooks.json`・`skills/` の symlink・`agents` と `scripts` への symlink) | diff --git a/plugins/ndf/.claude-plugin/plugin.json b/plugins/ndf/.claude-plugin/plugin.json index 340442aaf..2aa3004cf 100644 --- a/plugins/ndf/.claude-plugin/plugin.json +++ b/plugins/ndf/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "ndf", "version": "10.22.0", - "description": "Claude Code plugin (v10.22.0): 8 specialized agents (plus 2 supervisor agents and 1 worker agent for the 3-layer workflow) and 48 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, statusline, external AI delegation (Codex/agy), transcript retention guard, and optional Slack notifications.", + "description": "Claude Code plugin (v10.22.0): 8 specialized agents (plus 2 supervisor agents and 1 worker agent for the 3-layer workflow) and 49 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, statusline, external AI delegation (Codex/agy), transcript retention guard, and optional Slack notifications.", "author": { "name": "takemi-ohama", "url": "https://github.com/takemi-ohama" @@ -42,6 +42,7 @@ "./skills/document-style", "./skills/notion-writing", "./skills/docker-container-access", + "./skills/chrome-cdp", "./skills/implementation-plan", "./skills/layout-review", "./skills/investigation-rules", diff --git a/plugins/ndf/.codex-plugin/plugin.json b/plugins/ndf/.codex-plugin/plugin.json index 861baa401..dac9cd22c 100644 --- a/plugins/ndf/.codex-plugin/plugin.json +++ b/plugins/ndf/.codex-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "ndf", "version": "10.22.0", - "description": "Codex plugin (v10.22.0): 44 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, external AI delegation (Codex/agy), and optional Slack completion notifications.", + "description": "Codex plugin (v10.22.0): 45 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, external AI delegation (Codex/agy), and optional Slack completion notifications.", "skills": [ "./skills/cherry-pick-pr", "./skills/cross-refactoring", @@ -9,6 +9,7 @@ "./skills/deploy", "./skills/development-workflow", "./skills/docker-container-access", + "./skills/chrome-cdp", "./skills/external-ai", "./skills/fix", "./skills/implementation-plan", diff --git a/plugins/ndf/README.md b/plugins/ndf/README.md index 768a62d98..bf9f26b76 100644 --- a/plugins/ndf/README.md +++ b/plugins/ndf/README.md @@ -8,10 +8,10 @@ PR 運用、レビュー、調査、実装計画、仕様書化、開発方法 | ランタイム | 公開 Skill | マニフェスト | | --- | --- | --- | -| Claude Code | 48 個 | `.claude-plugin/plugin.json` | -| Codex | 44 個 | `.codex-plugin/plugin.json` | -| Kiro CLI | 45 個 | `dev.kiro/install.sh`(プラグイン機構が無いため installer で導入) | -| agy | 44 個 | `dev.agy/plugin.json`(取得元の登録が無いため clone から導入) | +| Claude Code | 49 個 | `.claude-plugin/plugin.json` | +| Codex | 45 個 | `.codex-plugin/plugin.json` | +| Kiro CLI | 46 個 | `dev.kiro/install.sh`(プラグイン機構が無いため installer で導入) | +| agy | 45 個 | `dev.agy/plugin.json`(取得元の登録が無いため clone から導入) | ## レイアウト @@ -19,7 +19,7 @@ PR 運用、レビュー、調査、実装計画、仕様書化、開発方法 plugins/ndf/ ├── .claude-plugin/plugin.json # Claude Code のマニフェスト ├── .codex-plugin/plugin.json # Codex のマニフェスト -├── skills/ # 配布 Skill の唯一の実体(48 個) +├── skills/ # 配布 Skill の唯一の実体(49 個) ├── skills/AUTHORING.md # Skill 執筆の規約 ├── manifests/ # ランタイム別の配布 Skill 一覧 ├── agents/ # Claude Code のサブエージェント定義(専門 8 個と、3 層の定義 3 個(supervisor 2・worker 1)) @@ -102,7 +102,7 @@ agy plugin install plugins/ndf/dev.agy # 初回 agy plugin uninstall ndf && agy plugin install plugins/ndf/dev.agy # 新しい版へ ``` -導入すると `manifests/agy-skills.txt` に載る Skill 44 個と、エージェント 11 個(専門 8 個と、3 層の定義 3 個(supervisor 2・worker 1))、hook 1 個が +導入すると `manifests/agy-skills.txt` に載る Skill 45 個と、エージェント 11 個(専門 8 個と、3 層の定義 3 個(supervisor 2・worker 1))、hook 1 個が `~/.gemini/config/plugins/ndf/` へコピーされます。symlink は実体へ解決されてコピーされるため、 clone を消しても導入した内容は残ります。 diff --git a/plugins/ndf/dev.agy/plugin.json b/plugins/ndf/dev.agy/plugin.json index 6e4a21bfe..41479cb15 100644 --- a/plugins/ndf/dev.agy/plugin.json +++ b/plugins/ndf/dev.agy/plugin.json @@ -1,5 +1,5 @@ { "name": "ndf", "version": "10.22.0", - "description": "Antigravity CLI plugin (v10.22.0): 44 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, external AI delegation, and worktree guidance hooks." + "description": "Antigravity CLI plugin (v10.22.0): 45 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, external AI delegation, and worktree guidance hooks." } diff --git a/plugins/ndf/dev.agy/skills/chrome-cdp b/plugins/ndf/dev.agy/skills/chrome-cdp new file mode 120000 index 000000000..3c83baec8 --- /dev/null +++ b/plugins/ndf/dev.agy/skills/chrome-cdp @@ -0,0 +1 @@ +../../skills/chrome-cdp \ No newline at end of file diff --git a/plugins/ndf/manifests/agy-skills.txt b/plugins/ndf/manifests/agy-skills.txt index ad4e01093..4b6339464 100644 --- a/plugins/ndf/manifests/agy-skills.txt +++ b/plugins/ndf/manifests/agy-skills.txt @@ -4,6 +4,7 @@ cross-review deploy development-workflow docker-container-access +chrome-cdp external-ai fix implementation-plan diff --git a/plugins/ndf/manifests/claude-skills.txt b/plugins/ndf/manifests/claude-skills.txt index 01a333f2d..2947b2312 100644 --- a/plugins/ndf/manifests/claude-skills.txt +++ b/plugins/ndf/manifests/claude-skills.txt @@ -9,6 +9,7 @@ ndf-policies document-style notion-writing docker-container-access +chrome-cdp implementation-plan layout-review investigation-rules diff --git a/plugins/ndf/manifests/codex-skills.txt b/plugins/ndf/manifests/codex-skills.txt index ad4e01093..4b6339464 100644 --- a/plugins/ndf/manifests/codex-skills.txt +++ b/plugins/ndf/manifests/codex-skills.txt @@ -4,6 +4,7 @@ cross-review deploy development-workflow docker-container-access +chrome-cdp external-ai fix implementation-plan diff --git a/plugins/ndf/manifests/kiro-skills.txt b/plugins/ndf/manifests/kiro-skills.txt index fd8e089d0..483b9a15f 100644 --- a/plugins/ndf/manifests/kiro-skills.txt +++ b/plugins/ndf/manifests/kiro-skills.txt @@ -9,6 +9,7 @@ ndf-policies document-style notion-writing docker-container-access +chrome-cdp implementation-plan layout-review investigation-rules diff --git a/plugins/ndf/skills/chrome-cdp/SKILL.md b/plugins/ndf/skills/chrome-cdp/SKILL.md new file mode 100644 index 000000000..255b5f5ac --- /dev/null +++ b/plugins/ndf/skills/chrome-cdp/SKILL.md @@ -0,0 +1,157 @@ +--- +name: chrome-cdp +description: "Connect from a dev container to the user's own desktop Chrome over CDP, for login and visual checks, then run tests headless with its cookies. Use when the browser must be the user's Chrome(手元のChromeに接続・ローカルChromeを操作・ログイン済みのブラウザを使う)." +allowed-tools: + - Read + - Write + - Bash +--- + +# 利用者の Chrome を開発コンテナから操作する + +開発コンテナの中のエージェントが、**利用者の PC で動いている Chrome** を CDP(Chrome DevTools Protocol)で操作する手順。 + +**使う場面:** 認証のある画面(管理画面・SSO)を確かめるとき。ログインは利用者がいつもの画面で行い、エージェントはその Chrome を見る・操作する。 + +**使わない場面:** 認証の要らないページの自動テスト。コンテナの中の headless の Chromium で足りる。 + +## 方針: ログインは利用者の Chrome、大量の操作はコンテナで + +CDP の操作は 1 回ごとに利用者の PC と往復する。リモートの開発環境では数十 ms の往復が積み重なり、数百回クリックするテストは目に見えて遅くなる。そこで役割を分ける。 + +| 何を | どこで | +| --- | --- | +| ログイン・画面の目視・少数の操作 | 利用者の Chrome(CDP) | +| E2E のような大量の操作 | コンテナの headless の Chromium。利用者の Chrome から**対象の origin の Cookie だけ**を取り出して `storageState` として渡す | +| テスト結果の表示 | `playwright show-report` などを、エディタのポート転送でコンテナから利用者の PC へ出す | + +## 1. 経路を決める + +まずコンテナから利用者の PC へ届く経路を判定する。 + +```bash +getent hosts host.docker.internal && echo "経路A の候補" +cat /proc/net/route | awk 'NR>1 && $2=="00000000"{print "既定ゲートウェイ(16進):", $3}' +env | grep -qE '^REMOTE_CONTAINERS=' && echo "Dev Containers の中" +``` + +| 経路 | 当たる環境 | 利用者の PC までの道 | +| --- | --- | --- | +| A. 同じ PC の Docker | Docker Desktop(macOS / Windows)の上のコンテナ。`host.docker.internal` が引ける | コンテナ → `host.docker.internal:9222` → Chrome | +| B. リモートのホスト | クラウドの VM(EC2 など)の上のコンテナに、エディタが SSH でつないでいる。`host.docker.internal` が引けない | Chrome ← SSH の `RemoteForward` ← ホストの `127.0.0.1:9222` ← 中継コンテナ ← コンテナ | + +どちらにも当たらない(ホストへ SSH していない・PC から VM へ届く経路が無い)ときは、ここで止めて利用者に構成を尋ねる。**Tailscale などで 9222 をネットワークへ開く案は採らない**(下の「安全のための決まり」)。 + +## 2. 利用者の PC 側の設定(利用者が行う) + +利用者に頼む。利用者の PC で動くエージェント(WSL の Claude など)がいるなら、[references/local-side-setup.md](references/local-side-setup.md) をそのまま指示書として渡す。 + +### Chrome を専用プロファイルで起動する + +**Chrome 136 以降は、既定のプロファイルではリモート操作の口が開かない。** 専用の `--user-data-dir` で別の Chrome を起動する。ログインは初回だけ要る。 + +``` +# Windows +"C:\Program Files\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9222 --user-data-dir=%LOCALAPPDATA%\chrome-cdp +# macOS +open -na "Google Chrome" --args --remote-debugging-port=9222 --user-data-dir="$HOME/.chrome-cdp" +``` + +### 経路 B だけ: SSH の転送を足す + +エディタ(VS Code の Remote-SSH など)が使う SSH の設定の、そのホストの節に 1 行足す。**SSH の接続を張り直すまで効かない**(ウィンドウを閉じて開き直す)。 + +``` +Host <リモートのホスト> + RemoteForward 9222 127.0.0.1:9222 +``` + +**エディタのポート転送では代わりにならない。** あれはコンテナのポートを PC へ見せる向きで、PC のポートをコンテナへ届ける向きを持たない。 + +## 3. 経路 B だけ: ホストに中継を立てる + +sshd の既定(`GatewayPorts no`)では、転送はホストの `127.0.0.1` にしか開かない。ブリッジネットワークのコンテナからは届かないので、ホストのネットワークで動く中継を 1 つ立てる。docker のソケットが使えるときは次で足りる。 + +```bash +# コンテナが属するネットワークのゲートウェイ(ホスト側の口) +GW=$(docker network inspect "$(docker inspect "$(hostname)" --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}}{{end}}')" --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}') +docker run -d --name cdp-relay --restart unless-stopped --network host alpine/socat \ + TCP-LISTEN:9222,bind="$GW",fork,reuseaddr TCP:127.0.0.1:9222 +``` + +- `bind` はブリッジのゲートウェイに限る。`0.0.0.0` にしない(VM の外へ 9222 が開く) +- docker のソケットが使えないときは、ホストで同じ socat を動かしてもらうか、sshd に `GatewayPorts clientspecified` を足して `RemoteForward <ゲートウェイ>:9222 127.0.0.1:9222` にしてもらう。どちらもホストの管理者に頼む +- 中継は複数のコンテナで共有してよい。既にあれば作り直さない(`docker ps --filter name=cdp-relay`) + +## 4. つながったか確かめる + +```bash +CDP_HOST=host.docker.internal # 経路 B なら上の $GW +curl -s -m 5 "http://$CDP_HOST:9222/json/version" -H 'Host: localhost:9222' +``` + +**Host ヘッダを `localhost:9222` に上書きする。** Chrome は Host が IP でも localhost でもない接続を拒む(`Host header is specified and is not an IP address or localhost`)。WebSocket の接続も同じ。 + +| 応答 | 意味 | +| --- | --- | +| `Browser` を含む JSON | つながった | +| 空の応答・`socket hang up` | 経路 B で、中継は生きているが、その先(SSH の転送か Chrome)が無い。接続の張り直しと Chrome の起動を利用者に確かめてもらう | +| 接続できない・名前が引けない | 経路の判定が違う。1 へ戻る | + +## 5. 操作する(Playwright) + +Node の `fetch` は Host ヘッダを変えられないので、エンドポイントは `http.get`(または `curl`)で取る。取った WebSocket の URL のホストを、届く側の名前へ差し替える。 + +```js +const http = require('http'); +const { chromium } = require('playwright'); // または '@playwright/test' + +const CDP = { host: process.env.CDP_HOST, port: 9222 }; +const HOST = 'localhost:9222'; + +function wsEndpoint() { + return new Promise((resolve, reject) => { + http.get({ ...CDP, path: '/json/version', headers: { Host: HOST }, timeout: 5000 }, (res) => { + let body = ''; + res.on('data', (c) => { body += c; }); + res.on('end', () => { + const ws = new URL(JSON.parse(body).webSocketDebuggerUrl); + ws.hostname = CDP.host; + resolve(ws.toString()); + }); + }).on('error', reject); + }); +} + +const browser = await chromium.connectOverCDP(await wsEndpoint(), { headers: { Host: HOST } }); +const context = browser.contexts()[0]; // 利用者のログイン状態を持つ文脈 +const page = await context.newPage(); // 利用者のタブは触らず、新しいタブで操作する +// ... +await browser.close(); // CDP の接続を切るだけ。利用者の Chrome は閉じない +``` + +- 画面を利用者に見せたいときは `await page.bringToFront()` +- **Chrome DevTools の MCP は、手元に Chrome の実行ファイルを探して起動しようとする。** リモートの Chrome へ向けられない構成では使えないので、上の Playwright で操作する + +### Cookie を取り出して headless で回す + +```js +const origin = new URL(process.env.TARGET_URL).origin; +const cookies = await context.cookies(origin); // 対象の origin だけを取る +fs.writeFileSync('.auth/state.json', JSON.stringify({ cookies, origins: [] })); +``` + +Playwright のテストでは `use: { storageState: '.auth/state.json' }` で読ませる。`.auth/` はリポジトリに入れない(`.gitignore` に足す)。セッションの期限が切れたら、利用者に Chrome でログインし直してもらい、取り出し直す。 + +## 安全のための決まり + +- **9222 をネットワークへ開かない。** CDP には認証が無く、届けば誰でもログイン済みのブラウザを操作できる。Chrome の待ち受けは `127.0.0.1` のまま、`--remote-debugging-address=0.0.0.0`・Windows の `netsh portproxy`・Tailscale での公開はしない +- **見るのは対象の origin だけ。** `context.cookies()` を引数なしで呼ぶと、利用者の全サイトの Cookie(名前と値)が取れてしまう。一覧を出力しない。必要な origin を渡す +- **利用者のタブを閉じない・移動しない。** 操作は `newPage()` で開いたタブで行い、終わったら接続だけを切る +- 共有の検証環境では、書き込み(保存・送信・削除)を伴う操作は利用者に確かめてから行う + +## 完了報告に書くこと + +- 使った経路(A / B)と、立てた・使った中継(名前と待ち受けアドレス) +- 取り出した Cookie の origin と件数(値は書かない) +- 利用者に頼んだ設定と、まだ済んでいないもの diff --git a/plugins/ndf/skills/chrome-cdp/references/local-side-setup.md b/plugins/ndf/skills/chrome-cdp/references/local-side-setup.md new file mode 100644 index 000000000..4daae99f9 --- /dev/null +++ b/plugins/ndf/skills/chrome-cdp/references/local-side-setup.md @@ -0,0 +1,67 @@ +# 指示書: 利用者の PC 側の設定(PC で動くエージェントへ渡す) + +この文書は、**利用者の PC で動くエージェント**(Windows の WSL の Claude など)へそのまま渡す。目的は、リモートのホストの上の開発コンテナから、この PC の Chrome を CDP で操作できるようにすること。コンテナ側(中継)の準備は、コンテナ側のエージェントが済ませている。 + +## 全体の形 + +``` +この PC の Chrome(127.0.0.1:9222) + ↑ SSH の RemoteForward(エディタの Remote-SSH の接続に相乗りする) +リモートのホストの 127.0.0.1:9222 + ↑ 中継コンテナ cdp-relay(ブリッジのゲートウェイで受ける) +開発コンテナ +``` + +## やること + +### 1. エディタが使っている SSH の設定ファイルと Host を特定する + +- Windows の VS Code の Remote-SSH は、普通は **Windows の OpenSSH** と `C:\Users\<名前>\.ssh\config` を使う。WSL の `~/.ssh/config` ではない +- VS Code の設定 `remote.SSH.configFile` と `remote.SSH.path` を見る(WSL からは `/mnt/c/Users/<名前>/AppData/Roaming/Code/User/settings.json`)。指定が無ければ既定の場所 +- その設定の中で、リモートのホストを指す `Host` の節(VS Code のリモートエクスプローラーに出ている名前と一致するもの)を探す +- macOS なら `~/.ssh/config` + +### 2. その Host の節に 1 行足す + +``` + RemoteForward 9222 127.0.0.1:9222 +``` + +- 先に `config.bak-<日付>` としてバックアップを取る +- 既存の行は変えない。改行コード(Windows は CRLF)と文字コードを保つ +- 既に `RemoteForward 9222` があれば足さない + +### 3. CDP 用の Chrome を起動する手段を用意する + +Chrome 136 以降は、**既定のプロファイルではリモート操作の口が開かない。** 専用のプロファイルで別に起動する。 + +``` +"C:\Program Files\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9222 --user-data-dir=%LOCALAPPDATA%\chrome-cdp +``` + +- Windows はデスクトップに「Chrome (CDP)」のショートカットを作る。WSL からは `powershell.exe -NoProfile -Command` で `WScript.Shell` の `CreateShortcut` を呼ぶ +- Chrome の場所が違えば `C:\Program Files (x86)\...` や `%LOCALAPPDATA%\Google\Chrome\Application\` を探す +- macOS は `open -na "Google Chrome" --args --remote-debugging-port=9222 --user-data-dir="$HOME/.chrome-cdp"` + +### 4. 確かめる + +1. CDP 用の Chrome を起動する +2. PC の側で 9222 が開いたことを確かめる。Windows は `powershell.exe -NoProfile -Command "(Invoke-WebRequest -UseBasicParsing http://127.0.0.1:9222/json/version).Content"` で `webSocketDebuggerUrl` を含む JSON が返る(WSL の `curl localhost:9222` は WSL のネットワークの設定によっては Windows に届かないので、判定に使わない) +3. 利用者に頼む(このエージェントにはできない) + - **エディタのウィンドウを閉じて、リモートのホストへ接続し直す**(転送は SSH の接続を張り直すまで効かない) + - CDP 用の Chrome で、対象のサイトにログインする +4. 利用者に「コンテナ側のエージェントに、PC 側の設定が済んだと伝えてください」と案内して終える + +## やらないこと + +- 9222 をネットワークへ開く設定(`--remote-debugging-address=0.0.0.0`・`netsh portproxy`・ファイアウォールの許可・Tailscale での公開)。CDP には認証が無く、届けば誰でもログイン済みのブラウザを操作できる +- いつものプロファイルに `--remote-debugging-port` を付けること +- SSH の設定の、`RemoteForward` 以外の行の変更 + +## うまくいかないとき + +| 症状 | 見るところ | +| --- | --- | +| 9222 が開かない | 同じ `--user-data-dir` の Chrome が既に動いていないか。CDP 用だけを閉じて起動し直す | +| 接続のときに `remote port forwarding failed for listen port 9222` | ホストの 9222 を、残っている古い SSH の接続が使っている。古いエディタのウィンドウを閉じる | +| コンテナ側で `socket hang up` | SSH の転送か Chrome のどちらかが無い。2 と 3 を確かめる | diff --git a/scripts/tests/test_agy_distribution.py b/scripts/tests/test_agy_distribution.py index dcc480def..e1faf292f 100644 --- a/scripts/tests/test_agy_distribution.py +++ b/scripts/tests/test_agy_distribution.py @@ -38,7 +38,7 @@ # 4 つとも同じ 5 だけ増えている(#116 / #144)。Google の 2 つの Skill を `google-workspace` # の 1 つへまとめたため、4 つとも同じ 1 だけ減っている(#744)。見積もりの `estimate` を # 4 つとも足したため、同じ 1 だけ増えている(#1856)。 -EXPECTED_COUNTS = {"claude": 48, "codex": 44, "kiro": 45, "agy": 44} +EXPECTED_COUNTS = {"claude": 49, "codex": 45, "kiro": 46, "agy": 45} def manifest_names(runtime: str) -> list[str]: From 5d5aa5f578212bf1e41f12e85fc2539b8bec5fef Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Tue, 27 Oct 2026 15:08:29 +0000 Subject: [PATCH 2/5] =?UTF-8?q?Update:=20chrome-cdp=20=E3=81=AB=E3=80=81?= =?UTF-8?q?=E3=83=96=E3=83=A9=E3=82=A6=E3=82=B6=E5=85=A8=E4=BD=93=E3=81=B8?= =?UTF-8?q?=E3=81=AE=E5=91=BD=E4=BB=A4=E3=81=8C=E6=8B=92=E3=81=BE=E3=82=8C?= =?UTF-8?q?=E3=82=8B=20Chrome=20=E3=81=8B=E3=82=89=E3=82=BF=E3=83=96?= =?UTF-8?q?=E5=8D=98=E4=BD=8D=E3=81=A7=20Cookie=20=E3=82=92=E8=AA=AD?= =?UTF-8?q?=E3=82=80=E6=89=8B=E9=A0=86=E3=82=92=E8=B6=B3=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KYHP7zFtYgAYtprVz6q6a2 --- plugins/ndf/skills/chrome-cdp/SKILL.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/plugins/ndf/skills/chrome-cdp/SKILL.md b/plugins/ndf/skills/chrome-cdp/SKILL.md index 255b5f5ac..684bbfe6a 100644 --- a/plugins/ndf/skills/chrome-cdp/SKILL.md +++ b/plugins/ndf/skills/chrome-cdp/SKILL.md @@ -141,6 +141,23 @@ const cookies = await context.cookies(origin); // 対象の origin だ fs.writeFileSync('.auth/state.json', JSON.stringify({ cookies, origins: [] })); ``` +#### `Browser context management is not supported` で拒まれたとき + +Chrome の起動のしかたによっては、ブラウザ全体への命令が拒まれる。`connectOverCDP` は接続の直後に `Browser.setDownloadBehavior` を送るので、そこで失敗する。`Storage.getCookies` も同じ理由で通らない。 + +**タブ(page)単位につなぎ、`Network.getCookies` に対象の URL を渡す。** この経路なら通る。URL を渡すので、ほかのサイトの Cookie は返らない。 + +```js +const WebSocket = require('ws'); // Node 標準の WebSocket は Host を変えられない +const page = (await getJson('/json/list')).find((t) => t.type === 'page'); // getJson は Host を上書きした http.get +const u = new URL(page.webSocketDebuggerUrl); u.hostname = CDP.host; +const ws = new WebSocket(u.toString(), { headers: { Host: 'localhost:9222' } }); +ws.on('open', () => ws.send(JSON.stringify({ id: 1, method: 'Network.getCookies', params: { urls: [`${origin}/`] } }))); +// 応答の cookies を storageState の形(name・value・domain・path・expires(session なら -1)・httpOnly・secure・sameSite)へ直して保存する +``` + +この状態の Chrome では、Playwright で画面を操作することもできない。目視や操作は利用者に頼み、テストはコンテナの headless で回す。 + Playwright のテストでは `use: { storageState: '.auth/state.json' }` で読ませる。`.auth/` はリポジトリに入れない(`.gitignore` に足す)。セッションの期限が切れたら、利用者に Chrome でログインし直してもらい、取り出し直す。 ## 安全のための決まり From 832b6236ae247b5b104ed7f4a98887c425c0bc28 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 28 Oct 2026 09:40:38 +0000 Subject: [PATCH 3/5] =?UTF-8?q?Fix:=20chrome-cdp=20=E3=81=AE=E7=B5=8C?= =?UTF-8?q?=E8=B7=AF=E5=88=A4=E5=AE=9A=E3=81=AE=20awk=20=E3=81=AB=E3=81=82?= =?UTF-8?q?=E3=81=A3=E3=81=9F=E4=BD=8D=E7=BD=AE=E5=BC=95=E6=95=B0=EF=BC=88?= =?UTF-8?q?$2,=20$3=EF=BC=89=E3=82=92=E3=80=81=E5=90=8D=E5=89=8D=E4=BB=98?= =?UTF-8?q?=E3=81=8D=E3=81=AE=E5=A4=89=E6=95=B0=E3=81=A7=20/proc/net/route?= =?UTF-8?q?=20=E3=82=92=E8=AA=AD=E3=82=80=E5=BD=A2=E3=81=AB=E7=BD=AE?= =?UTF-8?q?=E3=81=8D=E6=8F=9B=E3=81=88=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude Code が Skill 本文の $2 $3 を起動時の引数に置き換えるため、skill-frontmatter-check が落ちていた。 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KYHP82g1gdLBLQ9WcKeiJo --- plugins/ndf/skills/chrome-cdp/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/ndf/skills/chrome-cdp/SKILL.md b/plugins/ndf/skills/chrome-cdp/SKILL.md index 684bbfe6a..af3d73df1 100644 --- a/plugins/ndf/skills/chrome-cdp/SKILL.md +++ b/plugins/ndf/skills/chrome-cdp/SKILL.md @@ -31,7 +31,7 @@ CDP の操作は 1 回ごとに利用者の PC と往復する。リモートの ```bash getent hosts host.docker.internal && echo "経路A の候補" -cat /proc/net/route | awk 'NR>1 && $2=="00000000"{print "既定ゲートウェイ(16進):", $3}' +while read -r iface dest gw _; do if [ "$dest" = 00000000 ]; then echo "既定ゲートウェイ(16進): $gw"; fi; done < /proc/net/route env | grep -qE '^REMOTE_CONTAINERS=' && echo "Dev Containers の中" ``` From 6f072532066d1a6c29fb19ec1f190ed72c0b8ca3 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 28 Oct 2026 09:45:54 +0000 Subject: [PATCH 4/5] =?UTF-8?q?Update:=20playwright-kit=20=E3=81=AE?= =?UTF-8?q?=E5=88=A9=E7=94=A8=E8=80=85=E3=81=AE=20Chrome=20=E3=81=B8?= =?UTF-8?q?=E3=81=AE=E6=8E=A5=E7=B6=9A=E3=82=92=20ndf=20=E3=81=AE=20chrome?= =?UTF-8?q?-cdp=20=E3=81=B8=E5=AF=84=E3=81=9B=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - browser-connection.md から Chrome の起動フラグ・WSL2 / macOS の経路・ネットワーク別の到達手段を外し、/ndf:chrome-cdp を案内する。pytest の cdp-remote 設定と conftest の統合は残す - 既定のプロファイルで CDP を開く手順は Chrome 136 以降で動かず、--remote-allow-origins=* は検査ごと外すため、どちらも使わない - 同じ前提に立つ scripts/start-host-chrome.sh を削除する - cdp_endpoint は IP で書く(名前だと Chrome が Host ヘッダを理由に拒む。IP なら Host を書き換えずに connect_over_cdp が通ることを Chrome 155 で確認)。テンプレートの例も IP に揃える - chrome-cdp に、Host の書き換えが要るのは名前で接続するときだけであることを書き足す Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KYHP82g1gdLBLQ9WcKeiJo --- plugins/ndf/skills/chrome-cdp/SKILL.md | 2 +- .../skills/playwright-authoring/SKILL.md | 6 +- .../references/browser-connection.md | 227 ++---------------- .../scripts/start-host-chrome.sh | 172 ------------- .../templates/scenario.config.yaml | 12 +- 5 files changed, 31 insertions(+), 388 deletions(-) delete mode 100755 plugins/playwright-kit/skills/playwright-authoring/scripts/start-host-chrome.sh diff --git a/plugins/ndf/skills/chrome-cdp/SKILL.md b/plugins/ndf/skills/chrome-cdp/SKILL.md index af3d73df1..8fcef7652 100644 --- a/plugins/ndf/skills/chrome-cdp/SKILL.md +++ b/plugins/ndf/skills/chrome-cdp/SKILL.md @@ -90,7 +90,7 @@ CDP_HOST=host.docker.internal # 経路 B なら上の $GW curl -s -m 5 "http://$CDP_HOST:9222/json/version" -H 'Host: localhost:9222' ``` -**Host ヘッダを `localhost:9222` に上書きする。** Chrome は Host が IP でも localhost でもない接続を拒む(`Host header is specified and is not an IP address or localhost`)。WebSocket の接続も同じ。 +**Host ヘッダを `localhost:9222` に上書きする。** Chrome は Host が IP でも localhost でもない接続を拒む(`Host header is specified and is not an IP address or localhost`)。WebSocket の接続も同じ。上書きが要るのは `host.docker.internal` のような名前で接続するときで、IP(経路 B の中継など)で接続するなら上書きしなくても通る(Chrome 155 で確認)。上書きしても害はないので、下の手順は常に上書きする。 | 応答 | 意味 | | --- | --- | diff --git a/plugins/playwright-kit/skills/playwright-authoring/SKILL.md b/plugins/playwright-kit/skills/playwright-authoring/SKILL.md index 3c4f6267a..7f28a83b3 100644 --- a/plugins/playwright-kit/skills/playwright-authoring/SKILL.md +++ b/plugins/playwright-kit/skills/playwright-authoring/SKILL.md @@ -231,9 +231,9 @@ browser: cdp_endpoint: ${CDP_ENDPOINT:-http://localhost:9222} ``` -WSL2 / macOS / Linux ホストの Chrome へ CDP 接続する手順、`scripts/start-host-chrome.sh` によるホスト -Chrome の起動、`conftest.py` への統合、ネットワーク到達性の確保、トラブルシュートは -[references/browser-connection.md](references/browser-connection.md) を参照。 +`cdp-remote` の `cdp_endpoint` は IP で書く(名前だと Chrome が接続を拒む)。`conftest.py` への統合と +トラブルシュートは [references/browser-connection.md](references/browser-connection.md) を参照。 +利用者の Chrome の起動と、コンテナから届かせる経路は ndf プラグインの `/ndf:chrome-cdp` に従う。 ## MCP でのブラウザ動作確認 diff --git a/plugins/playwright-kit/skills/playwright-authoring/references/browser-connection.md b/plugins/playwright-kit/skills/playwright-authoring/references/browser-connection.md index 29e7d4de0..f452057b1 100644 --- a/plugins/playwright-kit/skills/playwright-authoring/references/browser-connection.md +++ b/plugins/playwright-kit/skills/playwright-authoring/references/browser-connection.md @@ -2,23 +2,6 @@ E2E テスト実行時のブラウザ接続先を構成する手順。概要と設定項目は `SKILL.md` の「ブラウザ接続」節を参照。 -## Chrome 起動フラグ - -CDP 接続に使う Chrome は次のフラグで起動する。OS ごとの差はバイナリパスだけである。 - -| フラグ | 役割 | -|---|---| -| `--remote-debugging-port=9222` | CDP エンドポイントを 9222 で公開 | -| `--remote-allow-origins=*` | CDP WebSocket の Host ヘッダ検証を無効化し、コンテナ等リモートからの接続を許可 (Chrome 106+) | -| `--user-data-dir=/tmp/chrome-debug` | 専用プロファイルで起動し、通常の Chrome と共存させる。任意のパスでよい | -| `--disable-features=DialMediaRouteProvider` | DIAL (Cast) 探索を無効化し、CDP ログのノイズと不要な通信を抑制 | -| `--remote-debugging-address=0.0.0.0` | 全インターフェースで listen する。loopback bind でコンテナから届かない場合のみ付与 | - -既存プロファイルのログイン済み Session をそのまま使う場合は、**全 Chrome プロセスを終了してから** -`--user-data-dir` を外して起動する (デフォルトプロファイルを使用)。 - -> **Security**: `--remote-allow-origins=*` と `--remote-debugging-address=0.0.0.0` は信頼できるネットワーク内でのみ使用する。ファイアウォールでポート 9222 へのアクセスを制限することを推奨。 - ## パターン 1: ローカルコンテナ Chromium (デフォルト) 設定不要。`run.sh` 初回実行時に `playwright install chromium` が自動実行される。 @@ -28,139 +11,39 @@ browser: mode: local ``` -## パターン 2: Windows ホスト Chrome (WSL2 + Docker → CDP) +## パターン 2: 利用者の Chrome (cdp-remote) -``` -Docker container (playwright) - ↓ http://host.docker.internal:9222 -Docker Desktop (WSL2 backend) → Windows host → localhost:9222 - ↓ -Chrome (--remote-debugging-port=9222 --remote-allow-origins=*) -``` +**Chrome の起動と、コンテナから Chrome まで届かせる経路は、ndf プラグインの `chrome-cdp` スキル(`/ndf:chrome-cdp`)に従う。** playwright-kit では扱わない。 -**Step 1: Windows Chrome をリモートデバッグモードで起動** +`chrome-cdp` が扱う範囲: -```powershell -& "C:\Program Files\Google\Chrome\Application\chrome.exe" ` - --remote-debugging-port=9222 ` - --remote-allow-origins=* ` - --user-data-dir="C:\tmp\chrome-debug" -``` +- Chrome を専用プロファイル(`--user-data-dir`)で起動する。Chrome 136 以降は、既定のプロファイルではリモート操作の口が開かない +- 経路の判定。同じ PC の Docker(`host.docker.internal` が引ける)と、EC2 などリモートのホスト(SSH の `RemoteForward` と中継コンテナ) +- つながったかの確かめ方と、うまくいかないときの切り分け +- ログインだけ利用者の Chrome で行い、Cookie を取り出して `local` モードの headless で回す方法 -> Chrome はデフォルトで `127.0.0.1` にバインドするため、`--remote-allow-origins=*` だけでは WSL2/Docker から接続できない場合がある。その場合は後述の「ネットワーク別接続ガイド」で到達性を確保する。 +`--remote-allow-origins=*` や `--remote-debugging-address=0.0.0.0` で Chrome の検査を外す方法は使わない。同じネットワークの誰からでも利用者の Chrome を操作できてしまう。 -**Step 2: WSL2 .wslconfig を NAT mode にする** - -```ini -# %USERPROFILE%\.wslconfig -[wsl2] -networkingMode=NAT -``` +### scenario.config.yaml -**Step 3: scenario.config.yaml** - -Docker Desktop (WSL2 backend) は `host.docker.internal` を標準サポートしている。 +**`cdp_endpoint` は IP アドレスで書く。** Chrome は、Host ヘッダが IP でも localhost でもない接続を拒む。`host.docker.internal` のような名前で書くと、`connect_over_cdp` が `Host header is specified and is not an IP address or localhost` で失敗する。IP で書けば、Host ヘッダを書き換えずに接続できる。 ```yaml browser: mode: cdp-remote - cdp_endpoint: ${CDP_ENDPOINT:-http://host.docker.internal:9222} + cdp_endpoint: ${CDP_ENDPOINT:-http://localhost:9222} ``` -> **Docker Desktop を使わず WSL2 から直接実行する場合**: `host.docker.internal` は Docker Desktop 固有の DNS 名のため使えない。Windows ホストの IP を直接指定する。 -> ```bash -> export CDP_ENDPOINT="http://$(grep nameserver /etc/resolv.conf | awk '{print $2}'):9222" -> ``` - -## パターン 3: macOS ホスト Chrome (Docker → CDP) - -``` -Docker container (playwright) - ↓ http://host.docker.internal:9222 -macOS host → localhost:9222 - ↓ -Chrome (--remote-debugging-port=9222 --remote-allow-origins=*) -``` - -**Step 1: ホスト側で Chrome を起動** - ```bash -"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \ - --remote-debugging-port=9222 \ - --user-data-dir=/tmp/chrome-debug \ - --remote-allow-origins=* \ - --disable-features=DialMediaRouteProvider +# 同じ PC の Docker: 名前を IP に引き直す +export CDP_ENDPOINT="http://$(getent hosts host.docker.internal | cut -d' ' -f1):9222" +# リモートのホスト: chrome-cdp で立てた中継の IP(コンテナのネットワークのゲートウェイ) +export CDP_ENDPOINT="http://172.18.0.1:9222" ``` -> **Note**: 上記は macOS Docker Desktop で動作実績のあるコマンド。macOS Docker Desktop は `host.docker.internal` がホストの loopback (127.0.0.1) バインドのサービスに到達できるため、`--remote-debugging-address=0.0.0.0` は不要で、全インターフェース公開によるセキュリティ低下も避けられる。Linux / WSL2 ホストで loopback bind が問題になる場合のみ `0.0.0.0` を付与する。 +### 大量の操作は local モードで回す -**Step 2: scenario.config.yaml** - -`--remote-allow-origins=*` で Host ヘッダ検証を無効化しているため、WSL2 と異なり **proxy 不要**。 - -```yaml -browser: - mode: cdp-remote - cdp_endpoint: ${CDP_ENDPOINT:-http://host.docker.internal:9222} -``` - -**Step 3 (任意): コンテナからホスト Chrome を起動する** → 次節。 - -## コンテナからホスト Chrome を起動する (SSH 経由) - -### なぜ直接は起動できないのか - -Docker コンテナはホストとプロセス空間が分離されているため、**コンテナ内のプロセスがホスト上に直接プロセスを生成することはできない**。 -特に macOS / Windows の Docker Desktop はコンテナを LinuxKit VM 内で実行するため、`nsenter` やホスト PID namespace を使う Linux 系の回避策も VM 止まりでホストには届かない。 - -したがって「コンテナからホストの Chrome を起動する」には、**ホスト側に起動を受け付ける口** が必要になる。最も導入が容易でスクリプト化しやすいのは **SSH** (macOS の「リモートログイン」= sshd) を使う方法。 - -``` -Docker container ──ssh──▶ host.docker.internal:22 (macOS sshd) - └─▶ Google Chrome --remote-debugging-port=9222 ... (バックグラウンド起動) -Docker container ──CDP──▶ host.docker.internal:9222 (起動後に接続) -``` - -### ホスト側の準備 (一度だけ) - -1. **リモートログインを有効化**: システム設定 > 一般 > 共有 > 「リモートログイン」を ON (CLI: `sudo systemsetup -setremotelogin on`) -2. **SSH 鍵を登録** (パスワードレス実行のため): コンテナ側の公開鍵をホストの `~/.ssh/authorized_keys` に追加 -3. ログインユーザーは **コンソールにログイン中の本人** であること。macOS では GUI アプリ (Chrome) は WindowServer に接続するため、コンソールセッションの所有者として起動する必要がある - -### スクリプト - -`scripts/start-host-chrome.sh` をコンテナ内から実行する。冪等で、既に CDP が起動済みなら何もしない。 - -```bash -# コンテナ内 -HOST_SSH_USER= ./scripts/start-host-chrome.sh -``` - -| 変数 | デフォルト | 説明 | -|---|---|---| -| `HOST_SSH_USER` | (必須) | ホスト (mac) のログインユーザー名 | -| `HOST_SSH_HOST` | `host.docker.internal` | SSH 接続先ホスト | -| `CDP_PORT` | `9222` | リモートデバッグポート | -| `CDP_BIND_ADDRESS` | (空=loopback) | Chrome の listen address。空なら Chrome 既定の loopback bind (macOS Docker Desktop はこれで動作・実証済み)。Linux/WSL2 等で到達できない場合のみ `0.0.0.0` 等を指定 | -| `CHROME_USER_DATA_DIR` | `/tmp/chrome-debug` | 起動プロファイル。空にするとデフォルトプロファイル (ログイン済み Session) を使用 | -| `CHROME_BIN` | `/Applications/Google Chrome.app/Contents/MacOS/Google Chrome` | Chrome バイナリパス | -| `MANUAL_WAIT` | `120` | 手動フォールバック時の起動待ち秒。`0` で待たず即終了 | - -動作: - -1. `http://host.docker.internal:9222/json/version` に疎通すれば **起動済み** とみなし即終了 -2. 未起動なら SSH (`BatchMode=yes`) でホストに接続し、Chrome をバックグラウンド (`nohup ... &`) で起動 -3. CDP エンドポイントが応答するまで最大 30 秒ポーリングして待機 - -### SSH が使えない場合のフォールバック - -`HOST_SSH_USER` 未設定 / コンテナに `ssh` クライアントが無い / SSH 接続・実行に失敗 (鍵未登録・リモートログイン無効・到達不可) のいずれかで、スクリプトは **自動で手動フォールバックに切り替わる**。 - -フォールバック時は、ホスト側で実行すべき起動コマンドをそのまま画面に出力し、`MANUAL_WAIT` 秒 (既定 120s) のあいだ CDP の起動をポーリングして待機する。利用者はその間にホストのターミナルへコマンドを貼り付けて実行すればよい。CI など人手が介在しない環境では `MANUAL_WAIT=0` を指定すれば、案内を出して即座に非ゼロ終了する。 - -> **Note (SSH を使わない代替手段)**: ホスト側に常駐ランチャ (launchd エージェントや FIFO 監視スクリプト、簡易 HTTP エンドポイント等) を置き、コンテナからネットワーク経由でトリガする方法もある。ただし SSH 方式が最も追加実装が少なく確実。X11 forwarding (XQuartz + socat) は「コンテナ内 GUI をホスト画面に表示する」用途であり、本件には不要。 - -> **Note (Linux ホストの場合)**: ホストで sshd が動いていれば同じスクリプトが使える。`CHROME_BIN=google-chrome`、`HOST_SSH_HOST` をホスト IP (`172.17.0.1` 等) に設定する。GUI セッションへの接続には `DISPLAY` 等の追加考慮が必要。 +CDP の操作は 1 回ごとに利用者の PC と往復するので、リモートの開発環境では遅い。テストの本数が多いときは、`chrome-cdp` の手順で Cookie を `storageState` として取り出し、`local` モードの headless の Chromium で回す。`cdp-remote` は、ログイン状態の確認や画面を見せながらの少数の操作に使う。 ## conftest.py への統合 @@ -247,80 +130,14 @@ def page(context, pwk_config): `playwright install` を実行するが、接続先がリモートの場合はスキップして問題ない。 ```bash -# エンドポイントの疎通確認 -curl -s http://host.docker.internal:9222/json/version | python3 -m json.tool -``` - -## ネットワーク別接続ガイド - -Chrome はデフォルトで `127.0.0.1` にバインドするため、同一ホストからしか CDP エンドポイントにアクセスできない。 -Docker コンテナや WSL2 からリモート接続する場合は、次のいずれかで到達性を確保する。 - -### 方法 1: `--remote-debugging-address=0.0.0.0` (推奨) - -Chrome 起動時に全インターフェースでリッスンさせる。最もシンプル。フラグの詳細は冒頭の表を参照。 - -### 方法 2: socat によるポートフォワード (Linux) - -Chrome を `127.0.0.1` バインドのまま維持し、socat でリモートからのアクセスを中継する。 - -```bash -socat TCP-LISTEN:9222,bind=0.0.0.0,reuseaddr,fork TCP:127.0.0.1:9222 -``` - -### 方法 3: netsh portproxy (Windows → WSL2) - -```powershell -# 管理者権限の PowerShell で実行 -netsh interface portproxy add v4tov4 ` - listenaddress=0.0.0.0 listenport=9222 ` - connectaddress=127.0.0.1 connectport=9222 - -netsh interface portproxy show all -netsh interface portproxy delete v4tov4 listenaddress=0.0.0.0 listenport=9222 +# エンドポイントの疎通確認(Browser を含む JSON が返ればよい) +curl -s -m 5 "$CDP_ENDPOINT/json/version" | python3 -m json.tool ``` -### 接続先の早見表 - -| 実行環境 | Chrome の場所 | 推奨方法 | CDP エンドポイント | -|---|---|---|---| -| ローカル (同一ホスト) | 同一ホスト | 設定不要 | `http://localhost:9222` | -| Docker → ホスト (macOS) | macOS ホスト | 方法 1 | `http://host.docker.internal:9222` | -| Docker → ホスト (Linux) | Linux ホスト | 方法 1 or 2 | `http://host.docker.internal:9222` or `http://172.17.0.1:9222` | -| Docker (WSL2) → Windows | Windows ホスト | 方法 1 or 3 | `http://host.docker.internal:9222` | -| WSL2 → Windows | Windows ホスト | 方法 1 or 3 | `http://$(grep nameserver /etc/resolv.conf \| awk '{print $2}'):9222` | - ## トラブルシュート -### 共通 - -| 症状 | 原因 | 対策 | -|---|---|---| -| `connect_over_cdp` で接続拒否 | Chrome が起動していない / ポートが違う | `curl http:///json/version` で確認 | -| WebSocket handshake 失敗 | Host ヘッダ不一致 | Chrome 起動時に `--remote-allow-origins=*` を付与 | -| ページ操作が異常に遅い | VPN / DNS 解決の遅延 | `extra_hosts` で IP 直指定 | - -### Windows (WSL2) 固有 - | 症状 | 原因 | 対策 | |---|---|---| -| `host.docker.internal` 解決不能 | Docker Desktop 未使用 or 古いバージョン | Docker Desktop を使用するか、WSL2 直接の場合は Windows ホスト IP を直接指定 | -| IPv6 でバインドされる | WSL2 が IPv6 優先 | listen address に `0.0.0.0` を明示 | -| `netsh portproxy` で接続ループ | portproxy の自己参照 | `--remote-allow-origins=*` を使い proxy を廃止 | -| mirrored mode で動かない | mirrored は localhost 共有だが CDP の WS 接続でポート競合 | NAT mode に戻す | - -### macOS 固有 - -| 症状 | 原因 | 対策 | -|---|---|---| -| `host.docker.internal` 解決不能 | Docker Desktop が古い / Linux Docker | `--add-host=host.docker.internal:host-gateway` を指定 | -| ファイアウォールでブロック | macOS のアプリファイアウォール | システム設定 > ネットワーク > ファイアウォール で Chrome を許可 | -| SSH 起動で Chrome が表示されない / WindowServer エラー | コンソール非ログインユーザーで SSH した | コンソールにログイン中の本人ユーザーで SSH する | -| `start-host-chrome.sh` が SSH で認証失敗 | リモートログイン未有効 / 鍵未登録 | `sudo systemsetup -setremotelogin on` と `authorized_keys` 登録を確認 | - -## CDP 接続のメリット - -- **GUI Chrome をそのまま操作可能** — OBS 録画可、人間と AI の協調操作が可能 -- **ログイン済み Session の再利用** — Google / AWS / Slack 等の MFA 済み Session をそのまま使える -- **ブラウザ拡張機能が有効** — テスト時にも拡張機能の影響を確認可能 -- **AI Agent との相性** — Claude Code / Browser Use / OpenHands がリアルブラウザを操作 +| `Host header is specified and is not an IP address or localhost` | `cdp_endpoint` を名前で書いた | IP で書く(上の「scenario.config.yaml」) | +| `connect_over_cdp` で接続拒否・空の応答 | Chrome が起動していない / 経路が切れている | `/ndf:chrome-cdp` の「つながったか確かめる」で切り分ける | +| ページ操作が異常に遅い | 利用者の PC との往復が積み重なっている | Cookie を取り出して `local` モードで回す | diff --git a/plugins/playwright-kit/skills/playwright-authoring/scripts/start-host-chrome.sh b/plugins/playwright-kit/skills/playwright-authoring/scripts/start-host-chrome.sh deleted file mode 100755 index 8c7b1d088..000000000 --- a/plugins/playwright-kit/skills/playwright-authoring/scripts/start-host-chrome.sh +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env bash -# コンテナ内から SSH 経由でホスト (macOS / Linux) の Chrome を -# リモートデバッグモードで起動するスクリプト。 -# -# 背景: -# Docker コンテナはホストとプロセス空間が分離されているため、 -# コンテナから直接ホストのプロセスを起動できない。 -# ホストの sshd (macOS: システム設定 > 共有 > リモートログイン) に接続し、 -# 起動コマンドを実行することで間接的にホスト Chrome を起動する。 -# -# SSH が使えない場合 (鍵未登録・リモートログイン無効など) は、 -# ホスト側で手動実行する起動コマンドを案内し、起動されるまで待機する -# フォールバックに切り替わる。 -# -# 前提 (SSH 自動起動を使う場合のみ・ホスト側で一度だけ設定): -# 1) リモートログインを有効化 (macOS: sudo systemsetup -setremotelogin on) -# 2) コンテナの公開鍵を ~/.ssh/authorized_keys に登録 (パスワードレス実行) -# 3) コンソールにログイン中の本人ユーザーで SSH すること -# (GUI アプリは WindowServer 接続のためコンソールセッション所有者が必要) -# -# 使い方 (コンテナ内): -# # SSH 自動起動 -# HOST_SSH_USER= ./scripts/start-host-chrome.sh -# # SSH を使わず手動起動の案内のみ -# ./scripts/start-host-chrome.sh -# -# 環境変数: -# HOST_SSH_USER ホストのログインユーザー名。未設定なら手動フォールバック -# HOST_SSH_HOST SSH 接続先 (default: host.docker.internal) -# CDP_HOST CDP 疎通確認先ホスト (default: HOST_SSH_HOST) -# CDP_PORT リモートデバッグポート (default: 9222) -# CDP_BIND_ADDRESS Chrome の listen address。空 (default) なら付与せず -# Chrome 既定の loopback bind (macOS Docker Desktop は -# host.docker.internal がホスト loopback に到達するため -# これで動作する)。Linux/WSL2 等で loopback bind だと -# コンテナから到達できない場合のみ 0.0.0.0 等を指定する。 -# 0.0.0.0 は全インターフェース公開のためセキュリティ注意。 -# CHROME_USER_DATA_DIR 起動プロファイル (default: /tmp/chrome-debug) -# 空にするとデフォルトプロファイル (ログイン済み Session) を使用 -# CHROME_BIN Chrome バイナリパス -# (default: /Applications/Google Chrome.app/Contents/MacOS/Google Chrome) -# STARTUP_TIMEOUT SSH 起動後の待機タイムアウト秒 (default: 30) -# MANUAL_WAIT 手動フォールバック時の待機秒。0 で待たず即終了 (default: 120) -set -euo pipefail - -HOST_SSH_USER="${HOST_SSH_USER:-}" -HOST_SSH_HOST="${HOST_SSH_HOST:-host.docker.internal}" -CDP_HOST="${CDP_HOST:-$HOST_SSH_HOST}" -CDP_PORT="${CDP_PORT:-9222}" -CDP_BIND_ADDRESS="${CDP_BIND_ADDRESS:-}" -CHROME_USER_DATA_DIR="${CHROME_USER_DATA_DIR-/tmp/chrome-debug}" -CHROME_BIN="${CHROME_BIN:-/Applications/Google Chrome.app/Contents/MacOS/Google Chrome}" -STARTUP_TIMEOUT="${STARTUP_TIMEOUT:-30}" -MANUAL_WAIT="${MANUAL_WAIT:-120}" - -# curl は冪等チェック・起動待機・手動フォールバックの全経路で CDP 疎通確認に -# 使う必須コマンド。無いと終了コード 127 で常に未起動扱いとなり、起動成功時でも -# タイムアウトしてしまうため、ここでフェイルファストする。 -if ! command -v curl >/dev/null 2>&1; then - echo "✗ curl が見つかりません。CDP 疎通確認に必須です。" >&2 - echo " コンテナに curl をインストールしてから再実行してください" >&2 - echo " (例: apt-get install -y curl / apk add curl)。" >&2 - exit 1 -fi - -cdp_up() { - # --max-time でネットワークハング時も待機ループ周期が壊れないようにする - curl -sf --max-time 2 "http://${CDP_HOST}:${CDP_PORT}/json/version" >/dev/null 2>&1 -} - -# --user-data-dir は空文字なら付与しない (デフォルトプロファイル使用) -userdata_arg="" -if [ -n "${CHROME_USER_DATA_DIR}" ]; then - userdata_arg="--user-data-dir='${CHROME_USER_DATA_DIR}'" -fi - -# --remote-debugging-address は CDP_BIND_ADDRESS が空なら付与しない。 -# 既定 (空) では Chrome は loopback (127.0.0.1) のみ listen する。macOS -# Docker Desktop は host.docker.internal がホストの loopback に到達するため -# これで動作する (ユーザー実証済み・0.0.0.0 不要)。 -# Linux/WSL2 等で loopback bind だとコンテナから到達できない場合のみ -# CDP_BIND_ADDRESS=0.0.0.0 等を指定する。0.0.0.0 は CDP を全インターフェース -# へ公開し、CDP は認証なしでブラウザ操作できるためセキュリティに注意すること。 -bind_arg="" -if [ -n "${CDP_BIND_ADDRESS}" ]; then - bind_arg="--remote-debugging-address=${CDP_BIND_ADDRESS} " -fi - -# ホスト側で実行する Chrome 起動コマンド (人間がコピペできる体裁) -host_launch_cmd() { - printf '"%s" --remote-debugging-port=%s %s%s --remote-allow-origins=* --disable-features=DialMediaRouteProvider' \ - "${CHROME_BIN}" "${CDP_PORT}" "${bind_arg}" "${userdata_arg}" -} - -# 手動フォールバック: ホストで実行するコマンドを案内し、起動を待機する -manual_fallback() { - local reason="$1" - echo "" >&2 - echo "──────────────────────────────────────────────────────────────" >&2 - echo "⚠ SSH 自動起動を利用できません (${reason})。" >&2 - echo " ホスト (mac) 側のターミナルで以下を実行してください:" >&2 - echo "" >&2 - echo " $(host_launch_cmd)" >&2 - echo "" >&2 - echo "──────────────────────────────────────────────────────────────" >&2 - - if [ "${MANUAL_WAIT}" -le 0 ]; then - echo "✗ Chrome 未起動のまま終了します (MANUAL_WAIT=0)。" >&2 - exit 1 - fi - - echo "→ ホストでの起動を待機中 (最大 ${MANUAL_WAIT}s, Ctrl-C で中断)..." >&2 - # seq 外部依存も bash 専用 for ((...)) も避け、POSIX 互換 while で待機する - # (最小コンテナ / /bin/sh しかない環境でも動作) - i=0 - while [ "$i" -lt "$MANUAL_WAIT" ]; do - if cdp_up; then - echo "✓ Chrome 起動を検知しました: http://${CDP_HOST}:${CDP_PORT}" - exit 0 - fi - sleep 1 - i=$((i + 1)) - done - echo "✗ 待機タイムアウト。手動起動後に再実行してください。" >&2 - exit 1 -} - -# 1) 既に起動済みなら何もしない (冪等) -if cdp_up; then - echo "✓ Chrome は既に CDP http://${CDP_HOST}:${CDP_PORT} で起動済みです" - exit 0 -fi - -# 2) HOST_SSH_USER 未設定 → 手動フォールバック -if [ -z "${HOST_SSH_USER}" ]; then - manual_fallback "HOST_SSH_USER が未設定" -fi - -# 3) ssh コマンドが無い → 手動フォールバック -if ! command -v ssh >/dev/null 2>&1; then - manual_fallback "コンテナに ssh クライアントが無い" -fi - -# 4) SSH でホストに接続し、Chrome をバックグラウンド起動 -echo "→ ${HOST_SSH_USER}@${HOST_SSH_HOST} で Chrome を起動します..." -# &2 -exit 1 diff --git a/plugins/playwright-kit/skills/playwright-kit-ops/templates/scenario.config.yaml b/plugins/playwright-kit/skills/playwright-kit-ops/templates/scenario.config.yaml index 9c02fb651..2b8c4f193 100644 --- a/plugins/playwright-kit/skills/playwright-kit-ops/templates/scenario.config.yaml +++ b/plugins/playwright-kit/skills/playwright-kit-ops/templates/scenario.config.yaml @@ -14,15 +14,13 @@ # --- ブラウザ接続設定 -------------------------------------------------- # local: playwright install chromium でインストールしたローカルブラウザ (デフォルト) -# cdp-remote: Chrome DevTools Protocol 経由でリモートブラウザに接続 -# → Windows / macOS のホスト Chrome を GUI 付きで操作可能 -# → ログイン済み Session (Google, AWS, Slack 等) をそのまま再利用 -# → 詳細は /playwright-kit:playwright-authoring を参照 +# cdp-remote: Chrome DevTools Protocol 経由で利用者の Chrome に接続 +# → ログイン済み Session をそのまま再利用 +# → Chrome の起動とコンテナから届かせる経路は /ndf:chrome-cdp を参照 browser: mode: local - # cdp-remote 時のみ有効。環境変数で切り替え推奨: - # Windows (WSL2 Docker): export CDP_ENDPOINT=http://host-gateway:9222 - # macOS (Docker Desktop): export CDP_ENDPOINT=http://host.docker.internal:9222 + # cdp-remote 時のみ有効。IP で書く(名前だと Chrome が Host ヘッダを理由に拒む): + # export CDP_ENDPOINT=http://172.18.0.1:9222 # cdp_endpoint: ${CDP_ENDPOINT:-http://localhost:9222} # --- 接続先 ---------------------------------------------------------- From 2ddffd96a8471335d1538d45f85681bd324d4248 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Wed, 28 Oct 2026 09:47:10 +0000 Subject: [PATCH 5/5] =?UTF-8?q?Docs:=20chrome-cdp=20=E3=81=AE=20Host=20?= =?UTF-8?q?=E3=83=98=E3=83=83=E3=83=80=E3=81=AE=E8=AA=AC=E6=98=8E=E3=81=8B?= =?UTF-8?q?=E3=82=89=E7=A2=BA=E8=AA=8D=E3=81=97=E3=81=9F=E7=89=88=E3=81=AE?= =?UTF-8?q?=E8=A8=98=E8=BC=89=E3=82=92=E5=A4=96=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KYHP82g1gdLBLQ9WcKeiJo --- plugins/ndf/skills/chrome-cdp/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/ndf/skills/chrome-cdp/SKILL.md b/plugins/ndf/skills/chrome-cdp/SKILL.md index 8fcef7652..ade43f4bb 100644 --- a/plugins/ndf/skills/chrome-cdp/SKILL.md +++ b/plugins/ndf/skills/chrome-cdp/SKILL.md @@ -90,7 +90,7 @@ CDP_HOST=host.docker.internal # 経路 B なら上の $GW curl -s -m 5 "http://$CDP_HOST:9222/json/version" -H 'Host: localhost:9222' ``` -**Host ヘッダを `localhost:9222` に上書きする。** Chrome は Host が IP でも localhost でもない接続を拒む(`Host header is specified and is not an IP address or localhost`)。WebSocket の接続も同じ。上書きが要るのは `host.docker.internal` のような名前で接続するときで、IP(経路 B の中継など)で接続するなら上書きしなくても通る(Chrome 155 で確認)。上書きしても害はないので、下の手順は常に上書きする。 +**Host ヘッダを `localhost:9222` に上書きする。** Chrome は Host が IP でも localhost でもない接続を拒む(`Host header is specified and is not an IP address or localhost`)。WebSocket の接続も同じ。上書きが要るのは `host.docker.internal` のような名前で接続するときで、IP(経路 B の中継など)で接続するなら上書きしなくても通る。上書きしても害はないので、下の手順は常に上書きする。 | 応答 | 意味 | | --- | --- |