feat: initial public release (MAESTRO)

This commit is contained in:
oss-sync
2026-06-03 05:08:00 +00:00
commit f5c7666f6b
823 changed files with 184150 additions and 0 deletions
+54
View File
@@ -0,0 +1,54 @@
# AnnotateImage
画像上に矩形枠・矢印・テキストラベルを SVG で重畳描画する。元画像は変更せず `output/` に新しいファイルとして保存される。
## 基本
```js
AnnotateImage({
input_path: "input/screenshot.png",
output_path: "output/annotated.png",
annotations: [
{ type: "rectangle", x: 100, y: 50, width: 200, height: 80, color: "#FF0000", label: "問題箇所" },
{ type: "arrow", from_x: 50, from_y: 200, to_x: 150, to_y: 250, color: "#00FF00", label: "ここに注目" },
{ type: "text", x: 300, y: 100, text: "重要", color: "#0000FF", font_size: 24 }
]
})
```
## annotation の種類
### rectangle(矩形)
- `x`, `y`: 左上の座標(px
- `width`, `height`: 幅・高さ(px
- `color`: 線色(CSS カラー、デフォルト `#FF0000`
- `label`: 矩形の上に表示するラベル(任意)
### arrow(矢印)
- `from_x`, `from_y`: 始点
- `to_x`, `to_y`: 終点(矢印の先)
- `color`: 線・矢じり色
- `label`: 始点付近に表示するラベル(任意)
### text(テキスト)
- `x`, `y`: テキストの基準点
- `text`: 表示文字列
- `color`: 文字色
- `font_size`: フォントサイズ(任意、画像サイズに応じて自動調整)
## 自動スケーリング
線幅・フォントサイズは画像サイズに応じて自動調整される(短辺ベース)。固定値で見栄えが崩れることは少ない。
## 用途
- スクリーンショットへの注釈追加
- 手順書の作成(操作手順を矢印で示す)
- バグ報告での問題箇所のハイライト
- レポート用の図解作成
## 注意
- `output_path``output/` 配下である必要がある
- 元画像は変更されない(非破壊的)
- 日本語ラベル使用時は環境に日本語フォントが必要(prepare.sh で自動チェック)
+87
View File
@@ -0,0 +1,87 @@
# Bash ツール
シェルコマンド実行ツール。**用途は限定されている**。
## 許可される用途
- ファイル操作: `cp`, `mv`, `rm`, `mkdir`, `ls`, `find`
- テキスト処理: `cat`, `grep`, `sed`, `awk`, `head`, `tail`, `sort`, `uniq`, `wc`
- Python スクリプト実行: `python3 script.py``python3 -c "..."` (データ処理・グラフ生成等)
- Git の参照系: `git log`, `git diff`, `git status` (履歴・差分の確認)
- アーカイブ: `tar`, `zip`, `unzip`
## 禁止される用途
**パッケージ・ソフトウェアのインストール一切**
- `apt install`, `apt-get install`
- `pip install`, `pip3 install`
- `npm install`, `yarn add`
- `curl ... | sh`, `wget ... | bash`
- `cargo install`, `go install`
**永続的システム変更**
- `systemctl`, `service` の操作
- `crontab`, `at` の登録
- `chmod` で権限を緩和する操作
**ネットワーク経由のダウンロード(コードや実行可能物)**
- `curl https://.../install.sh`
- 必要なら DownloadFile ツールを使う
## なぜインストールが禁止か
- ジョブ実行環境はサンドボックス化されており、永続化されない
- 必要な機能は専用ツール(`allowed_tools` に列挙されたもの)で提供される
- インストールが必要 = ツールの設計が足りないので、ユーザーに報告して機能追加を依頼する
## 代替
「○○ をインストールしたい」と思ったときの代替策:
| やりたいこと | 専用ツールで代替 |
|-------------|------------------|
| HTTP リクエスト | WebFetch / DownloadFile |
| HTML パース | BrowseWeb |
| 画像加工 | AnnotateImage / ReadImage |
| OCR | OCRTool / BatchOCRTool |
| Office ファイル読み込み | ReadPdf / ReadExcel / ReadDocx / ReadPPTX |
| 音声書き起こし | TranscribeAudio |
| データベース | SQLite |
| 検索 | WebSearch / SearchKnowledge |
## サンドボックス機構 (`safety.bash_sandbox`)
Bash 実行の隔離方式を選ぶ。**2 つの設定は直交する**:
- `safety.bash_sandbox`: 隔離機構を選ぶ — `auto`(既定)/ `always` / `off`
- `safety.bash_unrestricted`: コマンドホワイトリストを適用するか否か(`true` で撤廃)。**bwrap が走るかどうかは制御しない**(それは `bash_sandbox` の役割)
| `bash_sandbox` | 挙動 |
|----------------|------|
| `auto`(既定) | bwrap があれば bwrap サンドボックス、無ければ hardened(whitelist + パススコープ + env スクラブ)にフォールバック |
| `always` | bwrap を強制。bwrap 不在なら**起動失敗**(本番推奨) |
| `off` | bwrap を使わず exec**env スクラブと、unrestricted でなければ whitelist + パススコープは維持**)。デバッグ用・非推奨 |
### bwrap サンドボックスの構成
| マウント | 種別 |
|---------|------|
| タスクの workspace (`{worktreeDir}/local/{taskId}/`) | read-write bind |
| `/usr`, `/bin`, `/sbin`, `/lib`, `/etc` | read-only bind |
| `/lib64` (存在する場合) | read-only bind |
| `/tmp` | private tmpfs |
| `/proc`, `/dev` | proc / dev |
**マウントされないもの**: `/home`, 他タスクの workspace、ホストの `/tmp` など。他ユーザーの workspace にはファイルシステムレベルでアクセス不可。
**環境変数**: `--clearenv` で全消去後、`PATH`/`HOME`/`LANG`/`LC_ALL`/`TZ`/`TERM`/`TMPDIR` の最小 allowlist のみ注入する。`MCP_ENCRYPTION_KEY` 等のシークレットはサンドボックス内から見えない。hardened フォールバック経路も同じ allowlist で exec する。
**ネットワーク**: `--unshare-net` で隔離(ループバックのみ)。bash からの外向き通信は不可 — Web 取得は SSRF ガード付きの WebFetch / DownloadFile / MCP 経由に一本化されている。
**パッケージ**: 各 Bash コールは独立した bwrap サンドボックスで実行され(揮発 `/tmp`・毎回新しい名前空間)、`/usr` は read-only。よって `pip install` / `npm install` は永続せず、全モードで明示的に拒否される。必要な Python パッケージは `runtime/python-requirements.txt` にプリベイクされ、システム pythonread-only bind)から import できる。
### 前提条件 (`always` / bwrap 経路)
- コンテナ/ホストで **user namespace** が有効であること (PVE: `features: nesting=1`)
- `bwrap` バイナリがインストール済みであること
- `bash_sandbox: always`(または `bash_unrestricted: true`)では起動時に bwrap の動作確認を行い、失敗時はエラー終了する。`auto` では失敗時に警告ログを出し hardened へフォールバック
+162
View File
@@ -0,0 +1,162 @@
# Brainstorm 詳細ガイド
着手前または行き詰まり時に、複数アプローチを構造化された形で比較してから 1 つを採用する **思考の checkpoint** ツール。
LLM が「最初に思いついた方法でそのまま突き進む」「同じ tool が失敗してるのに同じ args で呼び直す」といったループに陥ることを防ぐ。
## 何を解決するか
問題のあるパターン:
```
失敗 → リトライ → 失敗 → リトライ → 失敗 → リトライ → ...
```
Brainstorm を挟むと:
```
失敗 → Brainstorm({ context: 失敗内容, approaches: [A, B, C], chosen: B }) → B を試す
```
## 必須フィールド
| フィールド | 説明 |
|---|---|
| `task` | 今解こうとしているサブ問題を **1 文** で。例: `"input/data.xlsx の中身を要約したい"` |
| `approaches` | 検討する解法の配列。**2 個以上**必要 (1 個だと比較にならない) |
| `chosen` | 採用する approach の `name``approaches[].name` のどれかと完全一致させる |
| `rationale` | 採用理由を 1-2 文で |
## 任意フィールド
| フィールド | 説明 |
|---|---|
| `context` | これまで試した手段・失敗内容など。行き詰まり時のリセット用途で記入する |
## approaches[] の各要素
| フィールド | 必須 | 説明 |
|---|---|---|
| `name` | ✓ | 短い名前 (例: `"ReadExcel 直接"`, `"CSV エクスポート経由"`) |
| `description` | ✓ | 1-2 文で具体的な手順 |
| `reliability` | - | `high` / `medium` / `low`。副作用無し・後戻り可能なら high |
| `speed` | - | `fast` / `medium` / `slow` |
| `prerequisites` | - | 前提条件・必要なもの |
| `risks` | - | 想定される失敗パターン |
## 使うべき場面
1. **複雑な依頼の着手前** — レポート生成・複数ファイル処理・多段階の調査など
2. **同じ tool が 2 回以上失敗した時** — エラー内容を `context` に書いて、別アプローチを 2-3 個並べる
3. **存在しないファイルを掴んだ時**`output/foo.xlsx` が無いと分かった時点で「Glob で実在確認 / ユーザーに ASK / 別パスを試す」を比較
4. **方針が複数あって迷った時** — どっちでも動きそうな選択肢がある時に、確実性で選び直す
## 使わなくて良い場面
- 短い質問への即答
- 自明な単一 tool で済む依頼 (例: 「current time を教えて」)
- 1-2 ステップで完結する操作
## 使い方の例
### 例1: ファイル読み込みのアプローチ比較
```js
Brainstorm({
task: "input/data.xlsx の中身を要約したい",
approaches: [
{
name: "ReadExcel 直接",
description: "ReadExcel({ path: 'input/data.xlsx' }) で全シート読む",
reliability: "high",
speed: "fast",
},
{
name: "シート分割→個別 Read",
description: "SplitExcelSheets で .md に分割してから Read で 1 シートずつ",
reliability: "high",
speed: "medium",
prerequisites: "出力ディレクトリの書き込み許可",
},
{
name: "ヘッダーだけ先に確認",
description: "ReadExcel に range: 'A1:Z3' を渡して構造把握 → 範囲拡大",
reliability: "high",
speed: "fast",
risks: "範囲を間違えるとデータを取り逃がす",
},
],
chosen: "ReadExcel 直接",
rationale: "ファイルサイズが小さければ全件読みが最速で確実"
})
```
### 例2: エラー連発時のリセット
```js
Brainstorm({
task: "output/レポート.xlsx を読みたい",
context: "ReadExcel が JSZip エラー、ReadPdf も extension mismatch、Bash cat も file not found。\n直前のターンで Write が成功したという認識だが実際は失敗していた可能性",
approaches: [
{
name: "Glob で実在確認",
description: "Glob({ pattern: 'output/*' }) で実際に存在するファイル一覧を取る",
reliability: "high",
speed: "fast",
},
{
name: "Write をやり直す",
description: "前回の Write 失敗が原因なら、対象ファイルを改めて生成する",
reliability: "medium",
speed: "medium",
risks: "既存ファイルを上書きしてしまう可能性",
},
{
name: "ユーザーに ASK",
description: "complete({ status: 'needs_user_input', missing_info: 'ファイルパスを確認させて' })",
reliability: "high",
speed: "slow",
},
],
chosen: "Glob で実在確認",
rationale: "副作用なしで現状把握できる。実在しないなら次の手も決まる"
})
```
## 出力の形
Brainstorm は以下のような Markdown を返す:
```
# Brainstorm: <task>
## 背景 / これまでの試行
<context があれば>
## 検討した N 個のアプローチ
**A 案**
<description>
[確実性: high / 速度: fast]
✓ **B 案** (採用)
<description>
[確実性: medium / 速度: medium]
**C 案**
...
## 採用: B 案
理由: <rationale>
続けて、採用したアプローチで実装に進んでください。
```
このアウトプットが activity log / tool 履歴に残るので、後から「どのアプローチを比較したか」「なぜそれを選んだか」を追跡できる。
## 注意
- **Brainstorm は思考の checkpoint であって、独立した解答ではない**。Brainstorm 後は採用したアプローチで実際の tool を呼ぶ
- **2 個以上の approaches が必須**。1 個だと「比較」にならない
- `chosen``approaches[].name` と完全一致必要 (大小・空白も含めて)
- 短い質問・自明なタスクで Brainstorm を呼ぶ必要は無い (オーバーヘッドになる)
+48
View File
@@ -0,0 +1,48 @@
# Browser Sessions
Save a logged-in browser session per site so scheduled tasks can scrape authenticated
pages without you being present.
## How to add a session
1. Open Settings → ツール設定 → Browser Sessions.
2. Click **Add site session**.
3. Fill in:
- **Label**: human-readable name (e.g., "My Twitter").
- **Start URL**: the page that proves you're logged in (e.g., `https://twitter.com/home`).
- **Logged-in selector** (optional): a CSS selector that only exists when logged in.
- **Login URL pattern** (optional): a glob that matches the site's login page (e.g., `https://twitter.com/i/flow/login**`).
4. Click **Open login window** — a browser appears inside the dialog.
5. Log in normally. Solve any CAPTCHA / 2FA.
6. Click **Save**. The session is captured, encrypted, and stored.
## How to use a session in a task
When creating a local or scheduled task, pick the saved session from the
**Browser session** dropdown. The agent's `BrowseWeb` calls inside that task
will run with your saved cookies / localStorage.
## Expiry
If the session expires (cookie rotation, password change, etc.) the next task
will fail with `AUTH_SESSION_EXPIRED`, the session will be marked **Expired**
in the settings list, and a comment will be posted on the task notifying you.
Click **Re-login** in the Browser Sessions list to capture a fresh state.
## Security
- Sessions are encrypted with a personal key derived per user. Other users
cannot read them. Admins can revoke and delete sessions, but cannot decrypt them.
- Sessions are not shared with org / public visibility — they are always bound to
the task owner.
- Audit logs record every save / use / decrypt with timestamp, actor, and result.
## Limitations (v1)
- Sessions are read-only snapshots — cookie mutations during a task run are NOT
written back. Sites that rotate refresh tokens on every request may need
re-login periodically.
- IndexedDB and sessionStorage are not captured by `Playwright.context.storageState`,
so sites that depend heavily on them may not work.
- One profile, one site. Cross-domain SSO sessions need every involved origin
visited during the initial login.
+287
View File
@@ -0,0 +1,287 @@
# BrowseWeb 詳細ガイド
ヘッドレスブラウザで Web ページを操作するツール。同一ジョブ内ではブラウザコンテキスト(Cookie・ログイン状態)が永続化される。
## 2 つのモード
### 1. 基本モード — URL を開いてテキスト取得
```js
BrowseWeb({ url: "https://example.com" })
```
ローカルで生成した HTML をブラウザで確認したい場合は、workspace ルートからの **相対パス** をそのまま渡す(推奨)。
```js
BrowseWeb({ url: "output/viewer.html" })
```
例:
- `output/viewer.html` を開く → `BrowseWeb({ url: "output/viewer.html" })`
- `input/sample.html` を開く → `BrowseWeb({ url: "input/sample.html" })`
内部的には実行中ジョブの workspace 絶対パスと結合され `file://` URL に変換される。`../` で workspace 外に出るパスは拒否される。`file:///` で始まる絶対 URL を直接渡すことも可能だが、workspace 外を指すものは拒否される。
オプション:
- `waitFor`: 待機する CSS セレクタ(省略時は load イベント完了まで待機)
- `extractSelector`: 特定要素のテキストだけ抽出する CSS セレクタ
- `screenshot`: スクリーンショットを保存するファイル名(例: `"page.png"``output/page.png`
- `timeout`: タイムアウト(ms、デフォルト 60000)
### 2. アクションモード — 連続操作
```js
BrowseWeb({
actions: [
{ type: "goto", url: "https://example.com/login" },
{ type: "fill", ref: "e3", value: "[email protected]" },
{ type: "click", ref: "e5" },
{ type: "getText" }
]
})
```
利用可能な `type`:
- `goto``url` で指定したページに遷移
- `click``selector` または `ref` で要素をクリック
- `fill``selector` または `ref` の input/textarea に `value` を入力
- `screenshot``value` で指定したファイル名で保存(省略時 `screenshot.png`
- `getText` — 全ページのスナップショット(ref 注釈付き)または `selector` 内のテキストを取得
- `wait``ms` ミリ秒待機(最大 30000
- `dumpHtml``ref` または `selector`(省略時 body)の outerHTML を取得(脱出口、後述)
## 長文ページの取得(preview + ファイル保存)
`getText` (selector 有無問わず) およびスナップショットの戻り値が **5000 文字を超える** 場合、フルテキストはワークスペースの `logs/browse/{ISO-timestamp}-{hash}.txt` に保存され、戻り値は **先頭 5000 文字 + 続きの取得方法案内** になる:
```
(先頭 5000 文字)
... (truncated; full 38214 chars saved to logs/browse/2026-05-07T09-30-12-a1b2c3d4.txt — Read({file_path:"logs/browse/2026-05-07T09-30-12-a1b2c3d4.txt", offset, limit}) で続きを取得可能)
```
続きを読みたい場合は `Read` ツールで `offset` / `limit` を指定:
```js
Read({ file_path: "logs/browse/2026-05-07T09-30-12-a1b2c3d4.txt", offset: 200, limit: 200 })
```
5000 文字以下のページなら従来通り全文が直接返り、ファイルは作成されない。
## ref 注釈の仕組み(重要)
`BrowseWeb({ url })``getText` の出力には、操作可能な要素が以下のような注釈付きで埋め込まれる:
```
ようこそ
{e1 link "ホーム" href="/"} {e2 link "製品" href="/products"}
ログインしてください
{e3 textbox name="email" placeholder="メールアドレス"}
{e4 textbox name="password"}
{e5 button "ログイン"}
```
- `e1`, `e2`, ... の ID(ref)は出現順に自動採番される
- 各 ref は内部的に Playwright で解釈可能なセレクタ(`data-testid` / `id` / `[name]` / `aria-label` / nth-of-type CSS chain の優先順)にマッピングされている
- click/fill アクションで `ref: "e5"` のように指定するだけで操作できる
- **CSS セレクタを自分で組み立てる必要がない**
### 検出される要素の範囲
ref が振られるのは以下の要素:
- 標準 HTML タグ: `<a>` / `<button>` / `<input>` / `<select>` / `<textarea>` / `<label>` / `<summary>` / `<details>` / `<option>`
- ARIA role: `button` / `link` / `menuitem` / `menuitemcheckbox` / `menuitemradio` / `tab` / `option` / `checkbox` / `radio` / `switch` / `combobox` / `listbox` / `slider` / `spinbutton` / `textbox` / `searchbox` / `treeitem`
- `[onclick]` / `[tabindex>=0]` / `[contenteditable=true]` 属性
- JavaScript で `addEventListener('click'|'mousedown'|'pointerdown', ...)` 経由で listener が後付けされた要素(jQuery / vanilla JS / Vue / Svelte の compile 後コードで多用される)
- open shadow DOM 内部の上記要素
- iframe 内の上記要素(同一オリジン / cross-origin 共に対応。Stripe Elements / OAuth / reCAPTCHA など)
検出されないもの: closed shadow DOM、`<canvas>` / WebGL の描画内容、React の `onClick={...}`(ただし React コンポーネントは大抵 `<button>``role="button"` を使うので別経路で拾える)。
### iframe 内の要素
iframe を含むページの `getText` の出力は、メインフレームのテキストの後ろに **フレームごとのセクション** が並ぶ形式になる。メインフレーム本文中には iframe の位置に `[[IFRAME ...]]` プレースホルダーが残るので、フレームの出現順や種別が把握できる:
```
これは決済画面です
[[IFRAME name=card title=Card details src=https://js.stripe.com/v3/elements]]
[ボタン] {e3 button "支払う"}
--- iframe f1 url="https://js.stripe.com/v3/elements/..." name="card" ---
{f1.e1 textbox "Card number"}
{f1.e2 textbox "MM / YY"}
{f1.e3 textbox "CVC"}
--- end iframe f1 ---
```
iframe 内の要素を click / fill / dumpHtml したいときは、frame ID prefix 付きの ref を指定するだけ:
```js
BrowseWeb({
actions: [
{ type: "fill", ref: "f1.e1", value: "4242 4242 4242 4242" },
{ type: "fill", ref: "f1.e2", value: "12 / 30" },
{ type: "fill", ref: "f1.e3", value: "123" },
{ type: "click", ref: "e3" } // メインフレームの「支払う」ボタン
]
})
```
frame ID (`f1`, `f2`, …) は `getText` 取得時の出現順に採番される。同じページに同じ iframe が複数ある場合は src/name で見分けてセクションヘッダーで識別する。
cross-origin iframe (Stripe / OAuth / reCAPTCHA など) でも Playwright が内部で透過的に DOM を取得するので、同じ感覚で操作できる。ただし iframe の中身が完全に読み込まれる前に snapshot を取ると `[empty]``[cannot inspect: ...]` が出ることがあるので、その場合は `wait` を挟んで再取得する。
### 状態属性
ref 注釈の末尾には ARIA 状態が列挙される。エージェントは「いまトグルが開いてるか」「チェック済みか」「無効化されてるか」を判断できる:
```
{e3 tab "設定" selected}
{e7 button "保存" disabled}
{e2 combobox "国" expanded haspopup}
{e9 checkbox "規約に同意" checked}
{e5 button "メニュー" pressed}
```
利用される状態: `expanded` / `collapsed` / `pressed` / `selected` / `checked` / `mixed` / `disabled` / `required` / `haspopup`
### ref はいつリセットされる?
- ページ遷移(`goto` または click でナビゲーションが発生)したとき
- 同一ジョブ内でも、ナビゲーション後は **getText を呼んで新しいスナップショットを取得する**
- 同一ジョブが終わるとブラウザコンテキストごと破棄される
## ワークフロー例
### 例1: ログインしてダッシュボードのデータを取得
```js
// Step 1: ログインページを開いて要素を確認
BrowseWeb({ url: "https://app.example.com/login" })
// → 出力に {e3: input[email]}, {e4: input[password]}, {e5: button "ログイン"} が含まれる
// Step 2: フォーム入力 → 送信 → 遷移後の状態を取得
BrowseWeb({
actions: [
{ type: "fill", ref: "e3", value: "[email protected]" },
{ type: "fill", ref: "e4", value: "p@ssword" },
{ type: "click", ref: "e5" },
{ type: "getText" } // ← ダッシュボードの新 ref を取得
]
})
// Step 3: ダッシュボードでさらにナビゲート(Cookie が維持されているため再ログイン不要)
BrowseWeb({ url: "https://app.example.com/dashboard/orders" })
```
### 例2: 複数ページを順に巡回
```js
// 検索結果ページを開く
BrowseWeb({ url: "https://example.com/search?q=foo" })
// → {e1: link "結果1" href="/item/1"}, {e2: link "結果2" href="/item/2"} ...
// 各リンクの href を確認したら、url 直接指定で各ページへ
BrowseWeb({ url: "https://example.com/item/1" })
BrowseWeb({ url: "https://example.com/item/2" })
```
### 例3: 動的ページの読み込み待ち
```js
BrowseWeb({
url: "https://app.example.com/spa",
waitFor: ".content-loaded" // この CSS セレクタが現れるまで待つ
})
```
## ユーザーに手動操作を委譲する(noVNC 経由のハンドオフ)
BrowseWeb で詰まったとき、エージェントは `InteractiveBrowse` を呼んでブラウザの操作権をユーザーに渡せる。
### 使うべき場面
1. **ログイン / 2FA / SSO 同意画面** — パスワードや TOTP / プッシュ通知を agent に持たせず、ユーザーに直接入力してもらう
2. **CAPTCHA / bot 検証** — reCAPTCHA、画像選択、Cloudflare チャレンジ等
3. **BrowseWeb の click が空振りし続ける**`dumpHtml` でも構造が複雑すぎて selector が組めない、closed shadow DOM、ドラッグ&ドロップが必須等
4. **canvas / WebGL ベースの UI** — 地図ペインや図形エディタなど DOM では addressable でない領域
5. **画面状態を目視確認したい** — agent が想定通りの画面にいるか不安なとき
### フロー
```js
// Step 1: ユーザーに引き継ぐ宣言
InteractiveBrowse({
url: "https://example.com/login",
reason: "ログインが必要です。ID / パスワードを入力して、画面右下の release ボタンを押してください。"
})
// → ジョブが waiting_human に遷移し、UI に noVNC リンクが表示される
// → ユーザーがブラウザ画面で操作 → release を押すとジョブが再開
// → 戻り値に sessionId が含まれる
```
ジョブ再開後、agent は **同じ sessionId で `BrowseWithSession`** を呼んで続きを引き継ぐ:
```js
// Step 2: ユーザーが完了させた状態 (ログイン済み等) で続行
BrowseWithSession({
sessionId: "abc-123", // InteractiveBrowse の戻り値の sessionId
url: "https://example.com/dashboard",
action: "getText" // または click / fill / screenshot
})
```
### `reason` の書き方
ユーザーに何をしてほしいかは `reason` フィールドで明確に伝えること。UI に表示される。良い例:
- 「ログインしてください。完了したら release を押してください」
- 「reCAPTCHA を解いてください。完了したら release を押してください」
- 「カートに入れたい商品を選んでください。完了したら release を押してください」
### 制約
- `InteractiveBrowse`**ローカルタスク経由のジョブ** でのみ使える(`taskId` が必要)。Gitea Issue 直接実行や taskId が立たない subtask root では使えない
- noVNC が orchestrator にインストール / 設定されていない環境ではエラー(Xvfb / x11vnc / websockify が必要、`config.yaml``browser.captcha_solve: novnc` 設定)
- ユーザーが release を押さない限りジョブは進まない。長時間放置すると `browser.auth_timeout`(デフォルト 10 分)で timeout
### 既存の Browser Sessions 機能との違い
| 機能 | 用途 |
|---|---|
| **Browser Sessions** (Settings UI から保存) | スケジュール実行や定期タスクなど **agent しか動いていない時間帯** に、過去にログイン済みの cookie / storageState を再利用 |
| **InteractiveBrowse** | **ジョブ実行中、その場で** ユーザーがブラウザを操作してログインや人間判断を行う |
定期タスクで毎回 InteractiveBrowse を呼ぶのは非効率なので、定常運用のサイトは Browser Sessions として登録するのが正解。「初回ログイン or セッション切れ時だけ InteractiveBrowse」のような使い分けが望ましい。
## トラブルシューティング
- **「ref "e5" not found in current snapshot」と出る**: ページ遷移後で ref がリセットされている。`getText` で新しいスナップショットを取得する
- **テキストが取れない / 空に近い**: ページが SPA で JavaScript で描画されている。`waitFor` で描画完了を待つ
- **ボタンを押せない / click しても何も起きない**:
1. 要素が visible でない可能性。先に getText で本当に存在するか確認
2. ref 注釈に `disabled` が出ていないか確認
3. `dumpHtml({ ref: "..." })` で要素の生 HTML を見て、独自 selector を組む
4. それでもダメなら `InteractiveBrowse` でユーザーに引き継ぐ
- **`<div>` に click 反応する独自 UI が ref に出ない**: addEventListener フックで多くは検出されるが、React の `onClick={...}` (root delegation) や `el.onclick = fn` 直接代入は捕捉できない。`dumpHtml` で構造を見て selector を直接組むか、`InteractiveBrowse` で渡す
- **ログインが維持されない**: 別ジョブから呼んでいる可能性。同一ジョブ内なら維持される。定常運用は Browser Sessions に保存する
## ファイルダウンロード
リンククリック等でブラウザがファイルダウンロードを開始すると、自動的に workspace の `output/` 配下に保存される。戻り値の末尾に以下の形式で通知される:
```
[download] saved output/report.csv (12345 bytes)
```
- ファイル名は server-suggested 名から path traversal 対策と禁則文字置換を経て決定される
- 衝突時は `foo-1.csv`, `foo-2.csv` 形式で番号付与される
- 失敗時は `[download] FAILED <name>: <reason>` と出る
- ダウンロードされたファイルは続く `Read`, `ReadPdf`, `ReadExcel`, `Bash` 等の tool で参照できる
- 履歴は `logs/downloads.jsonl` に追記される (DownloadFile と同じファイル、`source: 'BrowseWeb'` フィールドで区別)
ダウンロードを認証付きで行いたい場合は、Browser Sessions 機能で対象サイトのログインセッションを保存し、タスクで bind した状態で BrowseWeb を呼ぶこと。
## SSRF 保護
ローカル/プライベート IP127.x.x.x, 10.x.x.x, 172.16-31.x.x, 192.168.x.x, ::1, fc00::/7 等)へのアクセスはデフォルトでブロックされる。社内ホストへアクセスする必要がある場合は、Settings UI の「SSRF Allowed Hosts」に追加する。
+42
View File
@@ -0,0 +1,42 @@
# チェックリスト系ツール(CreateChecklist / CheckItem / GetChecklist
複数アイテム(ファイル、ページ、URL等)を順次処理するときの進捗管理に使う。
**「1件処理→即CheckItem」のループを厳守すること。**
## 基本ワークフロー
```
1. CreateChecklist({ name: "ocr-pages", items: ["page-001.png", "page-002.png", ...] })
2. for each item:
a. アイテムを1件処理する(OCR、ダウンロード、加工など)
b. CheckItem({ name: "ocr-pages", item: "page-001.png", status: "done" }) を即呼ぶ
c. 次のアイテムに進む
3. GetChecklist({ name: "ocr-pages" }) で漏れがないか確認
```
## ステータス
- `done` — 正常完了
- `failed` — 処理失敗(後で再試行・スキップ判断)
- `skipped` — 意図的にスキップ(理由を summary に書く)
## 厳禁パターン
**複数アイテムをまとめて処理してから後でまとめて CheckItem を呼ぶ**
- 途中でクラッシュ・中断したときに進捗が失われる
- アイテム順がブレる
- レビュー時に作業順序が追えない
**1件処理 → 即 CheckItem → 次のアイテム** を1件ずつ繰り返す
## ファイルの保存場所
`logs/checklists/{name}.json` に保存される。再開時に GetChecklist で前回の状態を取り出せる。
## いつ使うべきか
- 同種のアイテムを 3 件以上順次処理するとき
- 処理が長時間にわたり、中断・再開がありえるとき
- 後で何が処理済み/失敗かを振り返る必要があるとき
逆に、1〜2件しかない・1ステップで終わる処理には不要。
+50
View File
@@ -0,0 +1,50 @@
# DownloadFile
URL からファイルをダウンロードしてワークスペースに保存する。
## 基本
```js
DownloadFile({
url: "https://example.com/chart.png",
filename: "images/sales-chart.png",
section: "output"
})
```
## パラメータ
- `url`: ダウンロード元 URL
- `filename`: 保存先パス(section 配下からの相対パス)
- `section`: `"input"``"output"` (成果物に使う場合は `"output"`
## ファイル命名規約
### 画像(成果物に埋め込む場合)
- パス: `images/{わかりやすい名前}.png` (または .jpg / .webp / .gif
- section: `"output"`
- 命名は内容を表すスラッグ(kebab-case 推奨): `sales-q3-chart.png`, `product-screenshot-home.png`
### ダウンロード履歴
`logs/downloads.jsonl` に各ダウンロードのメタ情報(URL, 保存先, サイズ)が記録される。
## 成果物への画像埋め込み
ダウンロードした画像は Markdown レポートから相対パスで埋め込める:
```markdown
![Q3 売上推移](./images/sales-q3-chart.png)
```
レポート(output/report.md)と画像(output/images/*.png)が同じ section 配下にあれば `./images/` で参照可能。
## SSRF 保護
WebFetch と同じく、ローカル/プライベート IP はデフォルトブロック。Settings UI の「SSRF Allowed Hosts」で例外設定可能。
## 注意
- 大きすぎるファイル(数百MB以上)はタイムアウトしやすい
- バイナリファイル(PDF, 動画等)も保存可能だが、画像以外の用途では IngestDocument / TranscribeAudio 等の専用ツールも検討
+51
View File
@@ -0,0 +1,51 @@
# YouTube ツール(GetYouTubeTranscript / SearchYouTube
YouTube の動画情報・字幕を取得する。
## SearchYouTube — 動画検索
```js
SearchYouTube({
query: "ローカル LLM ベンチマーク",
limit: 10
})
// → 動画タイトル・URL・チャンネル名・再生回数・投稿日 のリスト
```
検索結果は概要のみ。動画の中身を知りたい場合は GetYouTubeTranscript で字幕を取る。
## GetYouTubeTranscript — 字幕取得
```js
GetYouTubeTranscript({
url: "https://www.youtube.com/watch?v=xxx"
// または video_id: "xxx"
})
// → タイムスタンプ付きの字幕テキスト
```
## 重要: 動画の内容を扱う場合は必ず字幕を取得
調査タスクで「YouTube 動画について書く」場合:
- ❌ 動画タイトルやサムネイルから推測して書く
- ❌ 内部知識や他サイトの情報で代用する
-**必ず GetYouTubeTranscript で実際の字幕を取得して引用する**
字幕がない動画(自動字幕も無い)は「字幕なし」と明記し、内容を書かないか別の情報源を探す。
## 出力フォーマット
タイムスタンプ付き:
```
[00:00] こんにちは、今日は...
[00:15] 最初に説明するのは...
[01:30] 結論として...
```
引用時はタイムスタンプも示すと信頼性が上がる。
## トラブルシューティング
- **字幕が取れない**: 字幕無し動画。動画 ID を確認、または別動画を探す
- **権限エラー**: 一部地域制限がある動画。代替を探す
- **URL 形式**: 短縮 URLyoutu.be/xxx)も対応
+77
View File
@@ -0,0 +1,77 @@
# Piece 編集ツール(ListPieces / GetPiece / CreatePiece / UpdatePiece
Piece(ワークフロー定義 YAML)を CRUD するツール群。`piece-builder` piece で使用。
## ListPieces — 一覧
```js
ListPieces()
// → 全 Piece の名前・説明・トリガーキーワード一覧
```
新規 Piece を作る前に **必ず実行して既存 Piece を確認**する。重複・類似機能の Piece を作らないように。
## GetPiece — 取得
```js
GetPiece({ name: "research" })
// → 指定 Piece の完全な YAML 定義
```
- 既存 Piece の構造を参考にする
- UpdatePiece の前に現状を確認
## CreatePiece — 作成
```js
CreatePiece({
name: "my-new-piece", // 英小文字・数字・ハイフンのみ
yaml_content: `
name: my-new-piece
description: ...
initial_movement: gather
movements:
- name: gather
persona: ...
instruction: ...
allowed_tools: [Read, Write, ...]
rules:
- condition: ...
next: ...
`
})
```
必須要素:
- `name`
- `description`
- `initial_movement`
- `movements`(少なくとも 1 つ)
- 各 movement の `rules`(遷移条件、`next` を明示)
## UpdatePiece — 更新
```js
UpdatePiece({
name: "research",
yaml_content: "..." // 全体を置き換える
})
```
**差分更新ではなく全体置換**。GetPiece で取得 → 編集 → UpdatePiece の流れ。
## 制限
- `general``chat` は削除不可(更新は可能)
- YAML パースエラーは即座にエラー
- movement 構造の検証あり(`rules[].next` が存在する movement か等)
## 設計指針
新しい Piece を作る前に:
1. ListPieces で既存を確認
2. 既存 Piece に少しの調整で対応できないか検討
3. 必要なら GetPiece で類似 Piece を参考にする
4. その上で CreatePiece
「Piece が増えすぎる」のはメンテナンス負債。**Piece は追加よりも既存 Piece の改良が原則**。
+53
View File
@@ -0,0 +1,53 @@
# ListUserAssets
Lists user-authored scripts, templates, and recordings stored in the caller's user folder (`data/users/{userId}/`).
## Input
```ts
{
kind?: 'scripts' | 'templates' | 'recordings' | 'all' // default: 'all'
}
```
## Output
Human-readable text listing each asset category.
**Scripts** (`.js` files in `scripts/`): each entry shows the filename, description, and declared params.
**Templates** and **Recordings**: filename, byte size, and last-modified timestamp.
Example output:
```
User folder for user-abc:
Scripts (2):
- foo.js: "Log into example.com" — params: [date:string]
- bar.js: "Check dashboard" — params: []
Templates (0):
(none)
Recordings (1):
- rec-2026-05-09T12-34-56.json (1234 bytes, 2026-05-09T12:34:56.000Z)
```
## Owner gate
The tool always reads the folder of the **authenticated caller** (`ctx.userId`). There is no way to list another user's assets.
If the caller is unauthenticated (`ctx.userId` missing), the tool returns `isError: true` with a message about authentication.
## Workflow example
```
ListUserAssets({ kind: 'scripts' })
→ see which scripts are available and what params they need
RunUserScript({ name: 'foo', params: { date: '2026-05-01' } })
→ execute the script
```
## Notes
- Scripts without a frontmatter block are listed with an empty description and no params.
- A parse error in one script is reported inline for that entry; other scripts are still listed.
- The tool is a META_TOOL — no need to add it to `allowed_tools` in piece YAML.
+97
View File
@@ -0,0 +1,97 @@
# SearchNotes / ReadNote / WriteNote
ユーザーの共有 knowledge notes`data/users/{userId}/notes/{folder}/{file}.md`)を扱う 3 ツール。
ファイルは YAML frontmatter + Markdown 本文で構成され、DB の `note_index` (FTS5 対応) に mirror される。
## SearchNotes
購読中(`note_subscriptions``mode=search` または `mode=inject``enabled=1` の行がある)の note を FTS5 全文検索する。
### 引数
| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| `query` | string | 必須 | 検索クエリ。ツール内部でフレーズ検索として扱われる |
| `folder` | string | 省略可 | 特定フォルダーのみに絞り込む |
| `limit` | integer | 省略可 | 最大取得件数(デフォルト 10、上限 100) |
### 戻り値
マッチした note のリスト(`owner_id/folder/file_name: title`)。
続いて `ReadNote` で全文を取得できる。
### FTS5 クエリの注意
クエリはフレーズ検索として自動エスケープされる。`kubernetes pod``"kubernetes pod"` に変換。
AND / OR 演算子を使いたい場合は複数回呼び出して結果を手動合算すること。
---
## ReadNote
特定の note の全文(frontmatter + 本文)を取得。可視性チェックあり。
### 引数
| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| `owner_id` | string | 必須 | note の所有者 user ID |
| `folder` | string | 必須 | フォルダー名 |
| `file_name` | string | 必須 | ファイル名(例: `foo.md` |
### 可視性ルール
- 自分の noteowner_id が自分): 常に読める
- `visibility: public` の note: 全ユーザーが読める
- `visibility: org` の note: `scope_org_id` が自分の所属 org に含まれる場合のみ読める
- `visibility: private` の他人の note: 読めない(isError: true
---
## WriteNote
自分の `notes/{folder}/{file}.md` を作成または更新する。
### 引数
| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| `folder` | string | 必須 | フォルダー名(`[a-zA-Z0-9._-]` のみ) |
| `file_name` | string | 必須 | ファイル名(`.md` で終わる) |
| `content` | string | 必須 | YAML frontmatter を含む完全な Markdown 内容 |
### Frontmatter フィールド
```yaml
---
title: "Note のタイトル(省略可)"
visibility: public # private | org | public
scope_org_id: "org-id" # visibility=org の場合に必須(自分の所属 org の ID)
mode_hint: search # search | inject(省略可)
tags:
- kubernetes
- security
---
```
- `visibility` は必須。省略すると `private` として扱われる
- `visibility: org` の場合、`scope_org_id` は自分の所属 org の ID でなければならない
- `mode_hint: inject` にすると、購読者の system prompt に自動注入される
- フォルダーとファイル名は固定の 2 階層(`notes/<folder>/<file>.md`
### 書き込み後の動作
- DB の `note_index` + FTS5 テーブルを即座に更新
- 同フォルダーへの self subscription がなければ自動作成(`mode=search``enabled=1`
- エラー時は `isError: true` を返す(バリデーションエラーや権限エラー)
---
## 使い分け
| 場面 | ツール |
|---|---|
| 「CVE 対象?」「〜の設定は?」など知識検索 | `SearchNotes("CVE")` |
| 検索結果の 1 件の詳細を読む | `ReadNote` |
| スケジュールタスクで取得した情報をチームと共有 | `WriteNote``visibility: org` |
| 自分用のメモ・ログを残す | `WriteNote``visibility: private` |
+150
View File
@@ -0,0 +1,150 @@
# Office ファイル系ツール(ReadPdf / ReadExcel / ReadDocx / ReadPPTX / PdfToImages / SplitExcelSheets / SplitDocxSections
ローカル workspace の Office 文書・PDF を読み込むツール群。
## 読み取り系
### ReadPdf
```js
// 全文抽出 (page 区切りで markdown)
ReadPdf({ path: "input/manual.pdf" })
// ページ範囲を絞る
ReadPdf({ path: "input/spec.pdf", page_range: "10-25" })
// grep -n 風検索 (query mode)
ReadPdf({ path: "input/manual.pdf", query: "保証期間" })
ReadPdf({ path: "input/manual.pdf", query: "保証期間", context_lines: 5 })
ReadPdf({ path: "input/spec.pdf", query: "第\\d+条", query_mode: "regex" })
```
スキャン PDF(テキストレイヤなし)の場合は **自動で PdfToImages + Vision OCR にフォールバック**。手動で `PdfToImages → ReadImage` を呼ぶ必要は通常ない。query を併用すると OCR 結果にも同じフィルタが適用される。
#### 引数
| 引数 | 型 | デフォルト | 説明 |
|---|---|---|---|
| `path` | string | (required) | workspace 相対の PDF パス |
| `page_range` | `"3"` / `"1-5"` | (全ページ) | 抽出ページ範囲 |
| `max_pages` | number | (無制限) | 抽出ページ数の上限 |
| `max_chars` | number | 50,000 | 返却文字数の上限 |
| `query` | string | (なし) | 一致行のみ grep -n 風で返す。trim 後 empty なら全文 mode |
| `query_mode` | `substring` / `regex` / `iregex` | `substring` | 検索モード。substring は大小無視 + metachar auto-escape |
| `context_lines` | number (0..20) | 2 | query マッチ前後の context 行数 |
#### query mode の出力例
```
# foo.pdf, query: "保証期間"
### Summary
- Total pages: 50
- Pages with match: 3
- Total matches: 5
### Matches
## Page 7 — 2 matches
6: 商品は購入日より
> 7: 保証期間内に故障した場合、無償修理対象…
8: ただし、消耗品は対象外です。
> 22: 延長保証期間は最大 3 年まで…
```
`>` が一致行、空白マーカーが context 行。隣接マッチは context window が overlap したら 1 cluster にまとめられ context 重複を回避。
#### gotcha
- **MAX_MATCHES_PER_PAGE = 50**: 1 ページで 50 件超のマッチは打ち切り、page header に `(capped)` を付与。`"the"` のような broad pattern を絞るシグナル
- **`query_mode: "regex"` の invalid pattern** → `isError: true` で friendly メッセージ。substring mode は metachar をエスケープするので絶対に regex error にならない
- **OCR fallback path** でも query 適用。スキャン PDF + キーワード検索 OK
- 上記 PdfToImages の手動呼び出しは **DPI / 出力ファイル個別管理** がしたい時のみ。普通の "PDF を vision で読みたい" は ReadPdf 単発で済む
### ReadExcel
```js
ReadExcel({ file_path: "input/data.xlsx" })
// → 全シートのセル内容をテキスト形式で返す
```
巨大な Excel は token を食うので、シートを絞る場合は SplitExcelSheets を使う。
### ReadDocx
```js
ReadDocx({ file_path: "input/spec.docx" })
// → 本文 + 表を抽出
```
### ReadPPTX
```js
ReadPPTX({ file_path: "input/slides.pptx" })
// → 各スライドのテキスト・表・スピーカーノートを返す
```
## 変換・分割系
### PdfToImages
```js
PdfToImages({ file_path: "input/scan.pdf", dpi: 200 })
// → output/ReadPdf/page-001.png, page-002.png, ... に保存
```
スキャン PDF を ReadImage で扱うときの前段。
### SplitExcelSheets
```js
SplitExcelSheets({ file_path: "input/big.xlsx" })
// → output/excel/{sheetname}.md と manifest.json を生成
```
シート単位で別ファイルにすることで、Read で必要なものだけ取り出せる。
### SplitDocxSections
```js
SplitDocxSections({ file_path: "input/long-spec.docx" })
// → 見出しベースで分割した .md と manifest.json を生成
```
長い Word 文書を構造化して Read で取り回しやすくする。
## ファイル選択の指針
| ファイル | 第一選択 | フォールバック |
|---|---|---|
| PDF(テキストあり) | ReadPdf | - |
| PDF(スキャン画像) | ReadPdf(自動 OCR フォールバック) | 手動 PdfToImages → ReadImage |
| PDF 内をキーワード検索 | ReadPdf + `query` | Read → GrepReadPdf で出力保存後) |
| Excel(小〜中) | ReadExcel | - |
| Excel(巨大) | SplitExcelSheets → Read | - |
| Word(短〜中) | ReadDocx | - |
| Word(長文・章構成) | SplitDocxSections → Read | - |
| PowerPoint | ReadPPTX | - |
## 注意
- すべて workspace 内のローカルファイル(`input/` または `output/`)が対象
- URL 指定不可 → DownloadFile で先にローカル保存
- 全ツール read-only(書き込まない)
## ファイルサイズ上限
Read 系ツールは悪意あるファイル / リソース枯渇対策として入力サイズを検証する。デフォルトは以下の通りで、`config.yaml``tools` セクション、または Settings UI の「Tools → Office ファイルサイズ上限」から変更可能。
| ツール | デフォルト | config キー |
|---|---|---|
| ReadExcel | 10 MB | `tools.office_excel_max_size_mb` |
| ReadDocx | 10 MB | `tools.office_docx_max_size_mb` |
| ReadPdf | 10 MB | `tools.office_pdf_max_size_mb` |
| ReadPPTX | 50 MB | `tools.office_pptx_max_size_mb` |
| ReadPPTX 展開後 | 200 MB | `tools.office_pptx_max_uncompressed_mb` |
PPTX の「展開後」は ZIP bomb 検知用で、ZIP 内の全エントリの非圧縮合計サイズに対する閾値。超過時は `ZIP bomb detected: ...` エラーとなる。
マクロ付きファイル(`.xlsm` / `.docm` / `.pptm` / `.xlsb`)は警告付きで読み込まれる(実行はされない)。
+38
View File
@@ -0,0 +1,38 @@
# ReadImage
画像ファイルを LLM に直接渡して内容を認識・説明させる。VLMVision Language Model)対応 worker でのみ使用可能。
## 基本
```js
ReadImage({ file_path: "input/screenshot.png" })
// → 画像内の文字・図表・物体について自然言語の説明が返る
```
## 動作要件
- 呼び出し時の worker が `vlm: true` で設定されている必要がある
- 設定がない場合、このツールは `allowed_tools` に書いてあっても利用不可(function definition から自動除外される)
## 用途
- スクリーンショットの内容説明
- 図・グラフ・チャートの読み取り
- ページレイアウトの確認
- 写真の被写体・状況把握
## 文字読み取りについて
- ある程度の OCR は可能だが、**精度が要求される文字情報**には別途 OCR ツールの使用を検討
- 数字・記号・固有名詞を厳密に扱う場合は VLM のハルシネーションに注意
- パラメータシート、表、コード等は誤読リスクが高い
## 入力ファイル
- `input/` または `output/` 配下のローカル画像ファイル
- URL 指定は不可(DownloadFile で先にローカル保存する)
- 対応形式: png, jpg, jpeg, gif, webp, bmp
## SearchKnowledge との連携
SearchKnowledge が返したページ画像(`input/knowledge/{ns}/page_xxx.png`)も ReadImage で内容確認できる。
+51
View File
@@ -0,0 +1,51 @@
# ReadUserMemory
Loads a specific memory entry from the caller's personal user folder.
## Overview
Memory entries are stored in `data/users/{userId}/memory/{name}.md`. Each file has YAML frontmatter (`name`, `description`, `type`) and a plain Markdown body.
The MEMORY.md index (injected into system prompt automatically) gives a one-line summary per entry. Use `ReadUserMemory` when you need the full body of a specific fact.
---
## Usage
```json
{
"name": "preferred-language"
}
```
**Response example:**
```
# Memory: preferred-language
**Type**: user
**Description**: User prefers Japanese output
Always respond in Japanese unless the user explicitly asks for another language.
```
---
## Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `name` | string | Yes | Entry identifier to load (no `.md` extension) |
---
## Error cases
- Returns an error if `name` does not exist in the memory folder.
- Returns an error if no user is authenticated.
---
## Related tools
- `UpdateUserMemory` — create, update, or delete memory entries.
- `ReadToolDoc({ name: "UpdateUserMemory" })` — full authoring guide.
+85
View File
@@ -0,0 +1,85 @@
# ReadUserTemplate
Loads a template file from the caller's `templates/` subdir (`data/users/{userId}/templates/`).
## Overview
Templates are plain Markdown files the user stores in their `templates/` folder via the UI.
Unlike `memory/` entries, frontmatter is **optional** — a template can be pure Markdown prose
with no YAML header at all.
Use this tool when the user says "use the weekly-report template" or "follow the api-error-email
boilerplate" — call `ReadUserTemplate`, read the shape, then adapt it to the task at hand.
---
## Usage
```json
{ "name": "weekly-report" }
```
Or with the `.md` extension (both forms work):
```json
{ "name": "weekly-report.md" }
```
**Response example (no frontmatter):**
```
# Template: weekly-report
## Body
# Weekly Report
Fill in this week's highlights here.
```
**Response example (with frontmatter):**
```
# Template: api-error-email
## Frontmatter
title: "API Error Email"
audience: "external"
## Body
Dear customer,
We apologize for the inconvenience.
```
---
## Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `name` | string | Yes | Template filename, with or without `.md` extension (max 128 chars) |
---
## Error cases
- Returns an error if the template does not exist in `templates/`.
- Returns an error if no user is authenticated.
- Returns an error if `name` contains path traversal characters or slashes.
---
## Use cases
- Weekly / monthly report boilerplate: `ReadUserTemplate({ name: "weekly-report" })` → fill in stats.
- Email canned responses: `ReadUserTemplate({ name: "api-error-email" })` → personalise and send.
- Code boilerplate: `ReadUserTemplate({ name: "react-component" })` → generate a new component.
---
## Related tools
- `ListUserAssets({ kind: "templates" })` — see what templates exist before reading one.
- `ReadUserMemory` — for structured facts/preferences (requires frontmatter).
- `RunUserScript` — for executable Node/Playwright scripts stored in `scripts/` / `browser-macros/`.
- This tool is a META_TOOL — no need to add it to `allowed_tools` in piece YAML.
+108
View File
@@ -0,0 +1,108 @@
# RenderUserTemplate
Renders a template from `templates/` by substituting `{{var}}` placeholders with caller-supplied params.
## Overview
Companion to `ReadUserTemplate`. Instead of returning the raw body, this tool:
1. Parses the template's frontmatter `params` spec (same shape as scripts / browser-macros).
2. Validates caller-supplied `params` against the spec (type-check + defaults applied).
3. Replaces every `{{name}}` placeholder in the body with the resolved value.
4. Returns the rendered body (no `# Template:` header, no frontmatter — just the substituted text).
Placeholders **not declared** in `frontmatter.params` are left literal — so prose like
`use {{column}} as the key` survives unchanged when no `column` param exists.
---
## Usage
Template file `templates/weekly-report.md`:
```markdown
---
description: Weekly status report
params:
- name: date
type: string
- name: summary
type: string
default: "(no summary)"
---
# Status — {{date}}
{{summary}}
```
Call:
```json
{ "name": "weekly-report", "params": { "date": "2026-05-11", "summary": "shipped 3 PRs" } }
```
Response:
```
# Status — 2026-05-11
shipped 3 PRs
```
---
## Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `name` | string | Yes | Template filename, with or without `.md` extension (max 128 chars) |
| `params` | object | No | Key-value params matching the template's `frontmatter.params` spec |
---
## Frontmatter `params` schema
Identical to scripts / browser-macros:
```yaml
params:
- name: identifier # must match /^[a-zA-Z_$][a-zA-Z0-9_$]*$/
type: string | number | boolean
description: optional
default: optional # if omitted, the param is required
```
Param values are coerced to string via `String(value)` when substituted.
---
## Error cases
- `name` missing or invalid characters → error.
- Template file does not exist in `templates/` → error.
- Frontmatter is malformed (bad YAML, bad `params` shape) → error.
- A required param (no default) is missing from the call → error: `param X: required but not provided`.
- A param has the wrong type → error: `param X: expected number, got string`.
Templates without any frontmatter render as pure pass-through — any `{{var}}` stays literal.
---
## Use cases
- Weekly / monthly reports: `RenderUserTemplate({ name: "weekly-report", params: { date, summary } })`.
- Email canned responses with variable substitution.
- Code boilerplate with a few configurable parts (component name, props).
For more dynamic logic (conditionals, loops), open `ReadUserTemplate` and do the substitution
inline in your output — there is no Handlebars / Liquid / etc. engine, by design.
---
## Related tools
- `ReadUserTemplate` — returns the raw body + frontmatter. Use this when you want to inspect
the template structure or do substitution yourself.
- `ListUserAssets({ kind: "templates" })` — list available templates.
- `RunUserScript` — for executable scripts (not just text substitution).
- This tool is a META_TOOL — no need to add it to `allowed_tools` in piece YAML.
+130
View File
@@ -0,0 +1,130 @@
# RunUserScript
Executes a user-authored script from the caller's user folder.
Two kinds of scripts are supported:
| kind | directory | runtime | signature | use case |
|------|-----------|---------|-----------|----------|
| `'script'` (default) | `scripts/` | plain Node.js — no Chromium | `main({ params })` | Data processing, API calls, computation, file conversion |
| `'browser-macro'` | `browser-macros/` | Playwright — Chromium | `main({ context, params })` | Web automation with a live browser session |
## Input
```ts
{
name: string, // filename — '.js' is appended if absent
params?: Record<string, unknown>, // runtime values matching the script's param spec
kind?: 'script' | 'browser-macro' // default: 'script'
}
```
## Param validation
Params are validated against the `params:` block in the script's YAML frontmatter:
- Extra params not listed in the spec → error containing "param"
- Wrong type for a declared param → error containing "param"
- Missing required param (no default) → error containing "param"
- Params with defaults are filled in automatically when not supplied
On any param error the tool returns `isError: true` immediately — no subprocess is spawned.
## Session integration (browser-macro only)
If a `browser-macro` script's frontmatter declares `session_profile_id: <N>`, the tool:
1. Loads the profile from the DB (owner-gated — must belong to `ctx.userId`).
2. Decrypts the user's envelope-encrypted DEK using the master key.
3. Decrypts the AES-GCM storageState blob using the DEK.
4. Passes the decrypted Playwright `storageState` object to the child process.
If any step fails the tool returns `isError: true` with a descriptive message.
For `kind: 'script'` (plain runtime), `session_profile_id` in the frontmatter is ignored — no session is loaded.
## Self-healing recorder (browser-macro only)
When a `browser-macro` fails at runtime, the tool automatically enables the BrowseWeb recorder for the current task (if not already enabled). On task completion, `recording-flush` stages a candidate patch as `browser-macros/{name}.next.js` for diff review.
Plain scripts (`kind: 'script'`) do **not** auto-enable the recorder.
## Output format
On success:
```
<result stringified>
[script logs]
<console.log lines from the child process>
```
The result is JSON-stringified if it is an object or array; `String(result)` otherwise. The `[script logs]` section is only appended when the script produced logs.
On failure (plain):
```
RunUserScript "{name}" failed: <error message>
```
On failure (browser-macro):
```
RunUserScript "{name}" failed: <error message>
The recorder is now enabled for this task; subsequent BrowseWeb actions will be captured.
On task complete, a candidate patch will be saved as browser-macros/{name}.next.js for review.
```
## Error cases
| Situation | `isError` | message contains |
|-----------|-----------|-----------------|
| No authenticated user | true | "authenticated" |
| Script file not found | true | "not found" |
| Frontmatter parse error | true | "frontmatter" |
| Param type / missing error | true | "param" |
| Session profile not found / not owned | true | "not found or does not belong" |
| Profile not active | true | "not active" |
| DEK / blob decryption failure | true | "decrypt" |
| Script timeout (60 s) | true | "timeout" |
| Script exits non-zero | true | "exited code" |
| Plain script denied child_process (e.g. spawning python) | true | "exited code" + "use the Bash tool" |
## Notes
- The tool is a META_TOOL — it is available in every movement without listing it in `allowed_tools`.
- Use `kind: 'browser-macro'` for any script that needs a browser (`context`).
- Use `ListUserAssets` first to discover available scripts and their param specs.
- On browser-macro failure, use `BrowseWeb` as a manual fallback.
## Running Python (don't — use Bash)
`RunUserScript` runs **Node only**. There is no Python interpreter path. A
common footgun is to write a Node script that does
`child_process.spawn('python3', ...)` and run it here — that **cannot work**:
plain scripts run under Node's `--permission` model, which denies
`child_process` entirely (you get `ERR_ACCESS_DENIED`). Even if it were
allowed, the child's env is scrubbed, so it would not see the orchestrator's
provisioned Python environment.
To run Python, use the **`Bash` tool** instead: `python3 your_script.py`. The
Bash sandbox has the pip packages pre-baked (pypdf, pdfplumber, python-docx,
python-pptx, openpyxl, pandas, numpy, …). That is the supported, working path.
## Security and trust model
`RunUserScript` is **disabled by default**. To enable it, add to `config.yaml`:
```yaml
tools:
user_scripts_enabled: true
```
**Only enable for trusted users.** User scripts run in a restricted child process:
- Env is scrubbed — only `PATH`, `HOME`, `TMPDIR/TMP`, `LANG`, `NODE_ENV`, and `PLAYWRIGHT_BROWSERS_PATH` are forwarded. API keys, database passwords, and other secrets in the orchestrator's environment are not visible to the script.
- CWD is set to the system tmpdir, not the orchestrator workspace.
- Stdout is capped at 1 MB and stderr at 200 KB; exceeding either limit kills the child.
- On timeout, the entire process group (including Playwright's Chromium for browser-macros) is killed.
**The two runtimes have different capability levels:**
- **Plain scripts (`kind: 'script'`)** run under Node's Permissions Model (`--permission`): `--allow-fs-read` is limited to the child-runner dir and tmpdir, `--allow-fs-write` to tmpdir, and `child_process`, worker threads, and native addons are **denied**. A plain script that tries to spawn a subprocess (e.g. python) fails with `ERR_ACCESS_DENIED`. See "Running Python" above.
- **Browser-macros (`kind: 'browser-macro'`)** cannot use `--permission` — Chromium launch, native bindings, and outbound HTTPS all need unrestricted `child_process`/addons/network. They run with full Node.js capability (env-scrubbed only) and rely on container-level isolation. Treat them as trusted code.
+41
View File
@@ -0,0 +1,41 @@
# SearchAmazon
Amazon.co.jp で商品を検索する。商品画像・価格・Keepa の価格推移グラフ・アフィリエイトリンクを含む整形済み Markdown を返す。
## 基本
```js
SearchAmazon({
query: "ノートPC 16GB",
limit: 5
})
```
## 出力フォーマット
```markdown
## 商品名
![商品画像](https://...)
- 価格: ¥xxx,xxx
- 評価: ★4.5 (1234件)
- [Amazon で見る](アフィリエイトリンク)
![価格推移](Keepa グラフ)
```
## 重要: 出力をそのまま埋め込む
返ってきた Markdown は **必ずそのまま最終回答に含める**
- ❌ 画像要素 `![...](...)` を省略する
- ❌ 画像をテキストリンクに置き換える
- ✅ 商品画像・Keepa グラフを含めて、全部そのまま出力に貼る
これは Amazon ガイドラインへの準拠とユーザー UX の両方の理由から。
## 設定
Settings UI の "Tools" セクション:
- **Amazon Affiliate Tag**: 必須(例: `your-tag-22`)。未設定だとアフィリエイトリンクが正しく生成されない
- **Keepa API Key**: 任意。設定すると価格推移データが詳細化(無くてもグラフ画像リンクは出る)
+103
View File
@@ -0,0 +1,103 @@
# SearchKnowledge / ListNamespaces / ListDocuments / IngestDocument / IngestStatus
DKSDocument Knowledge Service)に取り込んだ社内文書をベクトル検索で参照するツール群。
## 利用可能性チェック
```js
ListNamespaces() // 利用可能な namespace 一覧を返す
```
DKS が設定されていなければ "Knowledge service not configured" を返す。
namespace が空なら何も検索できない。
## 文書一覧の確認
```js
ListDocuments({ namespace: "product-a-support" })
```
その namespace に取り込み済みの文書を表示する。
## 検索
```js
SearchKnowledge({ namespace: "product-a-support", query: "返品ポリシーは何日以内?" })
```
レスポンスには:
1. **sections** — マッチしたツリーノード(タイトル + summary + ページ範囲)
2. **page_image_urls** — 関連ページの画像(PNG
### 自動ダウンロード
検索結果に含まれるページ画像は **自動的にワークスペース** `input/knowledge/{namespace}/page_001.png` などに保存される。
LLM はそのローカルパスを `ReadImage` でそのまま閲覧できる。
```js
// SearchKnowledge の出力例:
// ## 返品ポリシー (manual.pdf, pages: 3, 4)
// 購入後30日以内であれば...
//
// ### ページ画像(ReadImage で閲覧可能)
// - input/knowledge/product-a-support/page_003.png
// - input/knowledge/product-a-support/page_004.png
ReadImage({ file_path: "input/knowledge/product-a-support/page_003.png" })
```
### 生 JSON の保存
DKS の生レスポンス JSON は `logs/raw/searchknowledge-{timestamp}.json` に保存される。doc_id 等の詳細フィールドが必要なときはそちらを Read する。
## 文書の取り込み
```js
// 1. 取り込み開始(非同期)
IngestDocument({ namespace: "product-a-support", file_path: "input/manual.pdf" })
// → "取込を開始しました (job: xxx, 45ページ検出)。完了確認は IngestStatus で可能です。"
// 2. 進捗確認
IngestStatus({ namespace: "product-a-support", job_id: "xxx" })
// → "ジョブ xxx: 処理中: VLM 12/45ページ, ツリー構築: 未完了"
// または "完了 (manual.pdf)" / "失敗: ..."
```
DKS は内部で:
1. PDF → ページ画像化
2. VLM でページごとに記述生成
3. ツリー構造(章・節)構築
4. ベクトル化してインデックス登録
を行う。45 ページで数分かかる規模感。
## ワークフロー例
### 質問応答
```
SearchKnowledge → 関連 sections + ページ画像取得
↓ 必要なら ReadImage で図表確認
回答文に sections の要点を引用、根拠ページを示す
```
### 新文書を取り込んで検索
```
IngestDocument → job_id 取得
↓ 待機(数分後 or 別作業)
IngestStatus → completed まで polling
SearchKnowledge で取り込み済みコンテンツを検索
```
## ログ
`logs/knowledge-history.jsonl` に各ツール呼び出し(クエリ・件数・所要時間・エラー)が記録される。
## 注意
- **検索ヒット件数は DKS 側で制御** されるので、件数上限を心配する必要はない
- DKS サーバーがローカル/プライベート IP でも、API キー認証経由なので SSRF 例外不要
- VLM 処理はバックグラウンドで動くので、IngestDocument 後すぐに SearchKnowledge を呼んでもまだヒットしない可能性あり(IngestStatus で完了確認)
+112
View File
@@ -0,0 +1,112 @@
# Microsoft Learn 検索 / キャッシュツール
`learn.microsoft.com` を検索するための 4 つのツール群。オンライン検索とローカルキャッシュ (魚拓) を統合する。
## ツール一覧
| ツール | 用途 |
|--------|------|
| `SearchMicrosoftLearn` | オンライン検索 + ローカルキャッシュヒットを統合して返す |
| `FetchMicrosoftLearn` | ページを取得し Markdown 化してキャッシュに保存 |
| `SearchMicrosoftLearnCache` | キャッシュ済みページのみ FTS5 全文検索 (オフライン) |
| `RefreshMicrosoftLearnCache` | キャッシュ済みページを強制再取得 |
## 標準フロー
1. `SearchMicrosoftLearn({ query: "azure managed identity" })` で候補 URL を一覧取得
2. 興味のある URL を `FetchMicrosoftLearn({ url })` で取得 (初回はオンライン、2 回目以降はキャッシュ)
3. キャッシュに溜まってきたら `SearchMicrosoftLearnCache({ query })` でオフライン検索可能
## キャッシュ仕様
- 場所: `data/ms-learn-cache/pages.sqlite`
- DB: SQLite + FTS5 (external content)、ロケール (`en-us` / `ja-jp` 等) 横断検索
- TTL: なし (永続)。古さが気になったら `RefreshMicrosoftLearnCache` で個別に再取得
- 1 ページ = HTML から `<main>` / `<article>` を抽出して minimal markdown 化したもの
## SearchMicrosoftLearn
### 引数
| 名前 | 型 | 必須 | 説明 |
|------|----|------|------|
| `query` | string | yes | 自然言語キーワード |
| `locale` | string | no | `en-us` (デフォルト)、`ja-jp` 等 |
| `products` | string[] | no | 製品スコープ (例: `["azure"]``["dotnet"]`)。省略時は Learn 全範囲 |
| `top` | integer | no | 取得件数 (デフォルト 10、最大 25) |
### 出力例
```
## Online results (5)
- [Managed identities for Azure resources](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview) [cached]
Managed identities provide an automatically managed identity in Microsoft Entra ID...
- [Use a managed identity to connect to Azure SQL](https://learn.microsoft.com/en-us/azure/azure-sql/database/authentication-aad-overview)
...
## Cache hits (2)
- [Managed identity types](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview)
... two types of <mark>managed</mark> identities ...
```
`[cached]` マーカーが付いている結果は `FetchMicrosoftLearn` を呼ばなくても直近のキャッシュから即取り出せます。
### 注意
- locale はデフォルト `en-us`。日本語版は遅延・取りこぼしが多いので、特別な理由がない限り `en-us` を推奨
- `products` 絞り込みは Learn 検索 API の仕様に依存。指定しなくても困らない場面が多い
- オンライン検索が失敗した場合 (rate limit / network) はキャッシュ検索のみで結果を返す
## FetchMicrosoftLearn
### 引数
| 名前 | 型 | 必須 | 説明 |
|------|----|------|------|
| `url` | string | yes | `https://learn.microsoft.com/...` で始まる URL |
### 挙動
- URL 正規化: クエリ文字列とハッシュは削除して比較
- キャッシュヒット時は HTTP リクエストを発生させず、保存済みの Markdown を返す
- ヒットしない場合は HTTP 取得 → HTML から `<main>` 抽出 → minimal markdown 変換 → SQLite 保存
### 出力
冒頭にメタデータ行 (`Cached (age=...)` または `Fetched and cached (...)`) + 本文 markdown。
## SearchMicrosoftLearnCache
オフライン専用。FTS5 のクエリ構文をそのまま使えるが、デフォルトはスペース区切りの AND 検索 (各単語をフレーズ扱い)。
### 引数
| 名前 | 型 | 必須 | 説明 |
|------|----|------|------|
| `query` | string | yes | 検索クエリ |
| `top` | integer | no | 取得件数 (デフォルト 10、最大 25) |
### 出力
ヒットしたページ毎に `[title](url)` + ハイライト付きスニペット (`<mark>` タグ)。
## RefreshMicrosoftLearnCache
キャッシュ済みページの内容が古いと判断したときに使う。HTTP 取得を強制し既存レコードを上書き。
### 引数
| 名前 | 型 | 必須 | 説明 |
|------|----|------|------|
| `url` | string | yes | 再取得する URL |
## 設定
`config.yaml` の追加設定は不要。`data/ms-learn-cache/` ディレクトリは初回呼び出し時に自動作成される。
## 制限事項
- HTML→Markdown 変換は Learn の構造に最適化した最小実装。汎用 HTML には使えない
- Learn 以外のドメインは拒否 (`learn.microsoft.com` のみ)
- ページ内の画像は取得しない (テキスト検索のみ用途)
- API レート制限に当たった場合は `SearchMicrosoftLearn` がエラーを返すが、キャッシュ検索は引き続き使える
+49
View File
@@ -0,0 +1,49 @@
# 地図ツール(SearchPlaces / GetDirections / ReverseGeocode
地名・住所・経路情報を扱う。Google Maps API キーがあればそちら、無ければ Nominatim/OSRM(無料)を使用。
## SearchPlaces — 場所検索
```js
SearchPlaces({
query: "東京駅 ラーメン",
location: "35.6812,139.7671", // 任意: 中心座標
limit: 5
})
// → 名称・住所・座標・評価(API キーがあれば)等
```
## GetDirections — 経路検索
```js
GetDirections({
origin: "東京駅",
destination: "羽田空港",
mode: "driving" // driving / walking / transit / bicycling
})
// → 距離・所要時間・経路ステップ
```
## ReverseGeocode — 座標から住所
```js
ReverseGeocode({
lat: 35.6812,
lng: 139.7671
})
// → 住所文字列
```
## API 設定
Settings UI の "Tools" セクション:
- **Google Maps API Key**: 設定すると Google Places/Directions API を使用(高精度・有料)
- 未設定: Nominatim(住所検索)、OSRM(経路)の無料 API を使用
Google Maps API は精度・情報量が多いが、ビジネス要件・無料枠の制約に注意。
## 用途
- 出張・旅程の経路情報
- 店舗・施設の所在確認
- ジオデータの正規化
+115
View File
@@ -0,0 +1,115 @@
# Slide Tools (pptxgenjs)
PowerPoint で再編集可能な .pptx を生成するツール群。
4 ツール:
- `SetTheme` : テーマ (色・フォント・サイズ) を選ぶ。冒頭で 1 回
- `AddSlide` : スライドを 1 枚追加する
- `BuildPptx` : 蓄積した状態から .pptx を書き出す。最後に 1 回
- `ResetSlides` : 全スライドを破棄する (テーマは維持)
中間状態は `output/.slides.json` に保存される。直接編集しないこと。
## SetTheme
```ts
SetTheme({
preset: "corporate-blue" | "minimal-mono" | "vibrant" | "academic" | "dark" | "warm-paper",
overrides?: {
primary?: string, // "#1A5490" 等
accent?: string,
background?: string,
text?: string,
muted?: string,
heading_font?: string,
body_font?: string,
title_size?: number, // pt
heading_size?: number,
body_size?: number,
}
})
```
preset 一覧:
| preset | 雰囲気 |
|---|---|
| corporate-blue | 営業・社内提案 (青基調) |
| minimal-mono | 既定。シンプルな黒白 |
| vibrant | ポップ、LT 向け (赤×ティール) |
| academic | 学術発表 (落ち着いた青、セリフ) |
| dark | 暗背景・明るいテキスト |
| warm-paper | クリーム背景、温かみ |
## AddSlide
```ts
AddSlide({
layout: "title" | "section" | "bullets" | "two-column" |
"image-right" | "image-left" | "image-full" |
"table" | "chart" | "quote" | "closing" | "custom",
content: { /* layout 依存 */ },
notes?: string
})
```
### layout ごとの content
**title**: `{ title, subtitle?, author?, date? }`
**section**: `{ number?: "01", title }`
**bullets**: `{ title, bullets: string[], footnote? }`
**two-column**: `{ title, left: {heading?, bullets?, text?}, right: {...} }`
**image-right** / **image-left**: `{ title, body: string | string[], image: { path, alt? } }`
**image-full**: `{ image: { path }, caption? }`
**table**: `{ title, headers: string[], rows: string[][], col_widths?: number[] }`
- col_widths は比率 (合計 1.0 で全幅、例 `[0.3, 0.5, 0.2]`)。省略時は均等割
**chart**: `{ title, chart_type: "bar"|"line"|"pie"|"doughnut"|"area"|"scatter",
data: { categories: string[], series: [{name, values: number[]}] } }`
- series[].values.length は categories.length と一致必須
**quote**: `{ quote, attribution? }`
**closing**: `{ message?: "Thank you", contact? }`
**custom**: `{ elements: Array<...> }` (escape hatch、詳細下記)
### custom.elements
座標単位は inch。安全領域は x=0.5, y=0.5, w=12.33, h=6.5。
```ts
{ type: "text", text, x, y, w, h, options?: {font_size, bold, color, align} }
{ type: "image", path, x, y, w, h }
{ type: "shape", shape: "rect"|"roundRect"|"arrow"|"oval"|"line",
x, y, w, h, options?: {fill, line, text} }
{ type: "table", headers, rows, x, y, w, h }
{ type: "chart", chart_type, data, x, y, w, h }
```
### よくある失敗
- 画像パスは workspace 相対 (`input/foo.png` 等)。URL は不可 → 事前に DownloadFile
- chart の series.values.length と categories.length の不一致は AddSlide 時点で reject
- table.col_widths を指定するなら headers の長さと同じ要素数
## BuildPptx
```ts
BuildPptx({ output?: string }) // 既定 "output/slides.pptx"
```
- `output` は workspace 相対、`output/` 配下のみ可
- 戻り値に「スライド数 / ファイルサイズ / テーマ / 警告」が含まれる
- スライドが 0 枚なら error
- `.slides.json` が壊れていれば error + `ResetSlides()` を提案
## ResetSlides
```ts
ResetSlides()
```
- slides[] を空にする
- theme は維持
- 全枚やり直すときのみ使う
## PDF が欲しい場合
このツールは PDF 出力に非対応。生成された .pptx を PowerPoint / Keynote / LibreOffice で開いて Export してもらう。
+59
View File
@@ -0,0 +1,59 @@
# SpawnSubTask
タスクを並列サブタスクに分解して実行する。各サブタスクは独立した worker(ジョブ)で動き、完了後に親タスクが結果を集約する。
## 基本
```js
SpawnSubTask({
title: "ローカル LLM 比較調査",
instruction: "Ollama, vLLM, llama.cpp の最新性能ベンチマークを比較する。各ツールについて: 1) 直近6ヶ月の主要ベンチマーク, 2) ハードウェア要件, 3) 対応モデル一覧 を output/report.md にまとめる。",
piece: "research" // 任意。指定しないと自動分類
})
```
呼び出すと `subtasks/{index}/` にサブタスクのワークスペースが作られ、結果はそこに集約される。
## いつ使うか
### 並列分解が効果的なケース
- 2 つ以上の **独立したテーマ**(互いに参照しない)
- 各テーマが軽くなく、調査・処理に時間がかかる
- 分解後の各タスクが単独でも意味を持つ成果物になる
例:
- 「3 つの製品比較レポート」→ 製品ごとに 3 サブタスク
- 「複数 PDF の OCR 処理」→ ファイルごとに分解
- 「複数 SNS の情報収集」→ プラットフォーム別に分解
### 分解しないほうがよいケース
- 単一テーマで論理的に連続する処理(A→B→C のように依存)
- サブタスクが極端に小さい(オーバーヘッドの方が大きい)
- 全体像を見ながら判断する必要がある作業(対話的タスク等)
## instruction の書き方
- **完結した依頼文**で書く(親タスクの文脈を持たないので、サブタスクは instruction だけで判断する)
- 期待する成果物(出力ファイル名・場所)を明示
- 必要な前提情報があれば文中に展開
❌ 「これと同じ調査を別キーワードでやって」
✅ 「キーワード『A』『B』『C』について、各々のメリット・デメリットを比較する独立した調査を行い、output/A-vs-B.md にまとめる」
## piece の指定
- 省略時: 親と同じ classifier ロジックで自動選択
- 明示する場合: `research`, `general`, `office-process` 等の piece 名を指定
## 結果の参照
サブタスク完了後、親タスクは:
- `subtasks/{index}/output/` 以下にサブタスクの成果物がある
- Read で参照して集約レポートを作成する
## 制限
- ネスト深さは `subtasks.maxDepth`(デフォルト 2)まで
- サブタスクが waiting_human 等で停止すると親もブロックされる
+42
View File
@@ -0,0 +1,42 @@
# SQLite
ワークスペース内の SQLite データベースに対してクエリを実行する。
## 基本
```js
SQLite({
db_path: "input/data.db",
query: "SELECT name, price FROM products WHERE category = 'A' LIMIT 10"
})
```
## edit 制御
- **edit: false の movement**: SELECT のみ許可(読み取り専用)
- **edit: true の movement**: INSERT / UPDATE / DELETE / CREATE / ALTER 等の DDL/DML も許可
## 用途
- 既存の SQLite データベースの内容調査
- データ集計(GROUP BY, JOIN
- スキーマ確認(`SELECT name FROM sqlite_master WHERE type='table'`
- 加工後データの新規 DB への書き込み(edit movement のみ)
## クエリの実行結果
- 行は JSON 配列で返る
- 大量行はトークン消費が大きいので **必ず LIMIT を付ける** か WHERE で絞る
- 1 万行を超えるような結果は LIMIT 100 程度から始めて段階的に確認
## 入力ファイルの場所
- workspace 内のパス(`input/`, `output/`, `data/` 等)
- 絶対パスは禁止
- DB ファイルが存在しないときは(edit movement なら)新規作成される
## トラブルシューティング
- **database is locked**: 他プロセスが DB を開いている。暫く待ってリトライ
- **no such table**: スキーマ確認 → テーブル名スペルチェック
- **disk I/O error**: ディスク容量・パーミッション確認
+148
View File
@@ -0,0 +1,148 @@
# SSH Console Tools (SshConsoleEnsure / SshConsoleSend / SshConsoleSnapshot)
AI と人間が共有する SSH PTY セッションを操作する 3 ツール。1 タスクに 1 PTY セッションが対応し、`cd` / 環境変数 / foreground プロセスは job をまたいで維持される。長時間の対話作業 / TUI (vim, top, less, tmux) / 複数ラウンドの調査向け。
単発コマンドだけなら **`SshExec`** (ssh-ops piece) のほうが軽い。本ツール群は対話的シェル + AI が画面を見続ける用途に最適化されている。
## 典型的な flow (まずこれを真似る)
```js
// 1. どの接続が使えるか発見 (タスク本文に UUID が無いとき)
SshListConnections({})
// → {"connections":[{"id":"abcd1234-...","label":"prod-aao","host":"...","host_key_verified":true}]}
// 2. セッション確保 (冪等。何度呼んでも同じセッションを返す)
SshConsoleEnsure({ connection_id: "abcd1234-..." })
// → {"ok":true,"reused":false,"connection_id":"abcd1234-...","cols":120,"rows":32}
// 3. コマンドを送信。改行で実行される
SshConsoleSend({
connection_id: "abcd1234-...",
input: "uptime\n",
wait_ms: 800, // 出力が落ち着くまで待つ ms (default 500, max 5000)
})
// → {"ok":true,"bytes_sent":7,"screen_after":"... load average: 0.05 ...","new_output_bytes":120}
// 4. screen_after で見切れた場合は scrollback を取得
SshConsoleSnapshot({
connection_id: "abcd1234-...",
kind: "scrollback",
max_bytes: 32768,
})
// → {"kind":"scrollback","byte_count":12345,"truncated":false,"text":"..."}
```
## SshConsoleEnsure
セッションを確保する (無ければ open、有れば再利用)。**冪等**。`SshConsoleSend` を呼ぶ前に必須ではない (auto-ensure される) が、最初に明示的に呼んでおくと「セッション開設に成功した」ことを確認できる。
| Param | Required | Description |
|---|---|---|
| `connection_id` | yes | UUID。piece の `allowed_ssh_connections` に含まれている必要がある。**label / hostname / 思い出した文字列で代用してはいけない** — 必ず `SshListConnections``id` を渡すこと |
| `cols` | no | 初回 open 時のターミナル幅。default `ssh.console.default_cols` (120) |
| `rows` | no | 初回 open 時のターミナル高さ。default `ssh.console.default_rows` (32) |
| `force_replace` | no | bool。default `false`。既存 session が**別の** `connection_id` にある場合の挙動を制御 (下記参照) |
Return:
```json
{"ok": true, "reused": <bool>, "connection_id": "...", "cols": 120, "rows": 32, "host_fingerprint": "SHA256:..."}
```
`reused: true` なら過去ターンから引き継いだ既存セッション (cd 等の state あり)。`false` なら今回新規 open。
### connection_id mismatch の挙動 (重要)
同じ task で**別の** `connection_id` を渡した場合:
- `force_replace: false` (default) → エラー返却。レスポンスに **既存セッションの connection_id が含まれる** ので、それをそのまま使うか、本当に切り替えたければ次の呼び出しで `force_replace: true` を渡す
- `force_replace: true` → 旧セッションは `connection_change` 理由で閉じられ、新セッションが開く (旧 shell の state は失われる)
**典型的なバグパターン**: ジョブをまたいで動作するエージェントが `connection_id` を覚えていなくて、
LLM の hallucination で適当な UUID を生成 → mismatch reject される、というケース。エラーメッセージの中に
正しい `connection_id` が出ているのでそれを使うか、Send/Snapshot で `connection_id` を省略する。
## SshConsoleSend
入力を送る。**printable な shell コマンド (改行なし、制御文字なし、2 文字以上) には server が自動で末尾に `\n` を付加して実行する**。例: `input: "ls -la"` でも `input: "ls -la\n"` でも同じ結果。
auto-append が発火した時は response に `auto_newline_appended: true` が載るので、必要なら呼び出し側で検知できる。
raw のまま送りたい (改行を付けない) ケース:
- sudo の password prompt に応答中 (echo OFF — タイプ + 別 Send で `\n`)
- vim の insert mode で文字を順に打鍵
- less / top / htop 等 TUI で 1 キー操作 (`q`, `j`, `k`, space, etc.)
- これらは制御文字を含むか 1 文字なので auto-append は発火しない。
| Param | Required | Description |
|---|---|---|
| `connection_id` | no | UUID。**省略時はこの task の active session を自動採用 (推奨)**。明示する場合は active session の id と一致する必要があり、不一致なら reject (active id が surface される) |
| `input` | yes | raw 文字列。LF / CRLF / control 文字 (`\x03` Ctrl-C, `\x04` Ctrl-D, `\x1b` Esc, `\t` Tab) を透過 |
| `wait_ms` | no | 送信後の screen_after 取得までの待ち時間 (default 500ms, max 5000ms) |
Return:
```json
{
"ok": true,
"bytes_sent": 7,
"screen_after": "user@your-host:~$ uptime\n 12:34 ...",
"new_output_bytes": 120
}
```
### 入力フィルタ
各 line は connection 側の `deny_patterns` / `allow_patterns` (および組み込み deny-list) と照合される。1 行でも NG にひっかかると入力**全体**が reject される (部分実行はしない)。エラー例: `SshConsoleSend: line 2 rejected by builtin_deny (rm\s+-rf).`
### TUI 操作のコツ
- vim 起動: `SshConsoleSend({input: "vim test.txt\n", wait_ms: 1000})` → 待ってから `SshConsoleSnapshot` で画面確認
- vim 抜ける: `SshConsoleSend({input: "\x1b:q!\n"})` (`\x1b` は Esc)
- top/htop 抜ける: `SshConsoleSend({input: "q"})`
- 走行中プロセス中断: `SshConsoleSend({input: "\x03"})` (Ctrl-C)
- パス完成 (Tab): `SshConsoleSend({input: "ls /var/lo\t"})` (Tab だけ送って screen で候補確認)
### よくある間違い
- `wait_ms` が短すぎて screen_after に出力が間に合わない → 再度 `SshConsoleSnapshot` で取り直す
- printable input は server が自動で `\n` を付加するので改行忘れは基本問題ない。raw 入力したい場合 (TUI 操作等) は制御文字を含めること
- 大量出力で screen_after が切れる → `SshConsoleSnapshot({kind: "scrollback"})` で取得
## SshConsoleSnapshot
| Param | Required | Description |
|---|---|---|
| `connection_id` | no | UUID。**省略時はこの task の active session を自動採用 (推奨)**。明示する場合は active session の id と一致する必要があり、不一致なら reject |
| `kind` | no | `screen` (デフォルト) — 現在の表示画面 / `scrollback` — それ以前を含む過去の出力 |
| `max_bytes` | no | scrollback の上限 (default 8192, max 65536)。tail から `max_bytes` バイト返す |
Return (kind=screen):
```json
{"kind":"screen","cols":120,"rows":32,"text":"...","cursor":{"x":0,"y":15}}
```
Return (kind=scrollback):
```json
{"kind":"scrollback","byte_count":123456,"truncated":true,"text":"..."}
```
text は ANSI escape strip 済み (色 / cursor 移動シーケンスを除去)。raw が必要な場合は audit log を参照。
## エラー時のリカバリ
| エラー | 対応 |
|---|---|
| `host_key_*` | UI (Settings → User Folder → SSH Connections) で TOFU 検証してから再試行 |
| `command_rejected (builtin_deny / custom_deny)` | deny-list で reject。admin に許可パターン追加を相談 (ローカルで回避してはいけない) |
| `idle_timeout` / `duration_cap` | 古いセッションが閉じた。`SshConsoleEnsure` を再度呼んで開け直す |
| `connection_change` | 同 task で `force_replace: true` 付き Ensure が呼ばれた → 古いセッションが閉じた |
| `this task already has an active session on connection X (...)` | エラー文の中の **X が正しい id**。X を `connection_id` に使うか、Send/Snapshot で省略する。本当に切り替えたければ `force_replace: true` |
| `this task has an active session on connection X, not Y` | Send/Snapshot 側で id mismatch。X を使う or 省略する |
| `maintenance` | admin の対応を待つ。`complete({status: 'needs_user_input', missing_info: 'SSH maintenance window'})` で停止 |
| `not initialised` | `ssh.enabled` または `ssh.console.enabled` が false / `MCP_ENCRYPTION_KEY` 未設定。admin に依頼 |
| `does not declare allowed_ssh_connections` | piece YAML の movement に `allowed_ssh_connections: ['*']` 等を追加する必要あり |
## deny-list の限界
deny-list は **first line of defense** であって信頼境界ではない。`bash -c "..."``$VAR` 経由の動的展開は通る。多層防御 (audit + abuse lock + admin kill) で運用する。
機密値 (token / password / SSH key) は input 文字列に直接書かない。サーバー側の env / config / secrets manager から読ませる。
+271
View File
@@ -0,0 +1,271 @@
# SSH ツール詳細ガイド (SshExec / SshUpload / SshDownload / SshListConnections)
リモートサーバーで shell コマンドを実行したり、ワークスペースとリモートファイルシステムの間でファイルを転送するためのツール群。同じ前提・同じエラーモデル・同じ監査経路を共有するので、本ドキュメントに統合してある。運用者向けの設計・設定詳細は **[docs/ssh.md](../ssh.md)** を参照。
## 4 ツールの位置づけ
| ツール | 用途 | 入力 |
|--------|------|------|
| `SshListConnections` | この movement で使える接続の UUID + label + host 一覧を取得 | (引数なし) |
| `SshExec` | リモートで shell 単一行を実行 | `connection_id`, `command`, (任意) `timeout_ms` |
| `SshUpload` | workspace → リモートへファイル転送 (SFTP) | `connection_id`, `local_path`, `remote_path`, (任意) `timeout_ms` |
| `SshDownload` | リモート → workspace へファイル取得 (SFTP) | `connection_id`, `remote_path`, `local_path`, (任意) `timeout_ms` |
転送系の 3 ツールは、接続側の `remote_path_prefix` 配下の絶対パスのみを受け付け、`workspace` 外への local パスは reject される。`connection_id` は piece 側の `allowed_ssh_connections` に明示されている UUID のみ使用可能。
タスク本文に `connection_id` が記されていないときは、まず `SshListConnections` を呼んで該当の host / label の UUID を取得すること。
## 共通: 4 つの前提条件
ツール呼び出し前に以下が全て揃っている必要がある。どれか一つでも欠けると即エラー応答 (audit には `denied` で記録される)。
1. **`ssh.enabled: true`** が `config.yaml` で設定されている
2. **`MCP_ENCRYPTION_KEY`** 環境変数が 64 hex 文字 (= 32 バイト) で設定されている
3. **対象 connection の host key が verify 済**。新規作成直後は `host_key_verified_at IS NULL` 状態で SshExec/Upload/Download は `host_key_not_verified` で失敗する。SSH Connections パネル (Settings → User Folder → SSH Connections) で `/test` を実行 → 鍵 fingerprint を確認 → "Verify" ボタンで verify する
4. **piece の現在 movement で `allowed_ssh_connections` に当該 UUID が明示**されている (またはワイルドカード `*`)。空配列 `[]` は「SSH 使用するが許可なし」の deny 宣言とみなされ全 UUID が reject される
不足時のエラーメッセージ例: `SshExec error: piece "ops" movement "exec" does not list connection abcd1234... in allowed_ssh_connections.`
## SshListConnections
```js
SshListConnections({})
```
引数なし。現在の movement の `allowed_ssh_connections` + ジョブ owner の access grant を満たす接続だけを返す (admin 無効化 / piece 除外 / grant 無しは filter out)。
戻り値 (JSON 文字列):
```json
{
"connections": [
{
"id": "abcd1234-5678-90ab-cdef-1234567890ab",
"label": "prod-aao",
"host": "10.0.0.10",
"port": 22,
"username": "deploy",
"host_key_verified": true,
"host_key_pending": false
}
]
}
```
- `host_key_verified: false` の接続は SshExec/Upload/Download/Console* で使う前に UI から TOFU 検証する必要がある (`host_key_pending: true` ならまだ未検証で取り消し可能な状態)
- `connections` が空配列の場合は admin に接続登録 / grant 発行を依頼する
- 通常は **最初に呼ぶ** ことで AI が "どの host か" を発見できる。1 ターンで複数回呼ぶ必要はない (結果は安定)
- 監査 action: `ssh.list_connections` (detail に `count``wildcard` フラグ)
## SshExec
```js
SshExec({
connection_id: "abcd1234-...",
command: "ls -la /srv/agent",
timeout_ms: 30000 // 任意
})
```
戻り値 (JSON 文字列):
```json
{
"exit_code": 0,
"stdout": "total 12\ndrwxr-xr-x 3 agent agent ...",
"stderr": "",
"truncated_stdout": false,
"truncated_stderr": false
}
```
- `exit_code` は remote プロセスの終了コード。0 でない場合も isError=false で返り、LLM が判断する
- 標準出力は `config.yaml``ssh.max_output_bytes` (デフォルト 32 KiB) で truncate される。`truncated_stdout: true` の場合はコマンドを `head` / `tail` / `grep` で絞り込んで再試行する
- 同等以上のサイズが見込まれる出力は SshDownload でファイル取得した上で `Read` で扱うこと
### command フィルタリング (2 段)
- **組み込み deny-list**: `rm -rf /`, `mkfs`, `dd`, `:(){:|:&};:` 系のシステム破壊 / fork bomb 系を unconditional で reject
- **接続側カスタム正規表現**: 接続作成時に `deny_patterns` / `allow_patterns` (改行区切りの正規表現リスト) を設定可能。デフォルトは空 (= 制限なし)。`allow_patterns` を設定した場合、deny を通過した後さらに全 allow パターンに合致しないと reject
エラー: `SshExec error: command rejected by built-in deny-list (matched pattern: rm\s+-rf).` / `command rejected by connection deny-list.`
### timeout
`timeout_ms` 未指定時は `config.yaml``ssh.call_timeout_seconds` (デフォルト 30 秒)。これは TCP 接続 + handshake + 認証 + コマンド実行を全て含む wall-clock。タイムアウトすると `exec_timeout` エラーで終了し、audit row は `failed` outcome + `detail.error = 'exec_timeout'` で記録される (途中で生成された stdout は破棄される)。
## SshUpload
```js
SshUpload({
connection_id: "abcd1234-...",
local_path: "output/report.csv", // workspace 相対
remote_path: "/srv/agent/2026-05/report.csv" // 絶対パス、prefix 配下
})
```
戻り値:
```json
{
"ok": true,
"bytes": 4096,
"remote": "/srv/agent/2026-05/report.csv"
}
```
- `local_path`: workspace ルートからの相対パス。シンボリックリンク経由で workspace 外を指すパスは O_NOFOLLOW + parent lstat で reject される
- `remote_path`: 接続の `remote_path_prefix` (例: `/srv/agent`) 配下の絶対パスのみ。`/srv/agent/../etc/passwd` のような traversal は POSIX 正規化後に prefix 外と判定されて reject
- アップロード先のディレクトリは事前に存在している必要がある (`mkdir -p` 相当を行いたければ先に `SshExec({command: "mkdir -p /srv/agent/2026-05"})` を呼ぶ)
- 既存ファイルへの上書きは現状 reject せず upload する。冪等性が必要な場合は呼び出し側で確認すること
### サイズ上限
`config.yaml``ssh.max_upload_size_mb` (デフォルト 100 MB) を超える local ファイルは `remote_too_large` 相当で reject。
## SshDownload
```js
SshDownload({
connection_id: "abcd1234-...",
remote_path: "/srv/agent/2026-05/log.txt",
local_path: "input/log.txt" // workspace 相対
})
```
戻り値:
```json
{
"ok": true,
"bytes": 8192,
"local": "input/log.txt"
}
```
- `local_path`**既に存在するファイルへの上書きは reject** される (`local_target_exists` エラー)。新規パスを指定するか、既存ファイルを別ツールで削除してから再試行
- 親ディレクトリは呼び出し側で作成済にしておくこと。`Write` 相当の mkdir-p は行わない (e.g. `output/foo/bar.txt` を指定するなら、事前に `Bash({command: "mkdir -p output/foo"})` 等で作成)
- `remote_path` の prefix 配下チェック、サイズ上限 (`ssh.max_download_size_mb`)、SSRF チェックは Upload と同じ
## Host key TOFU フロー (LLM 側で完結しない)
接続を新規作成した直後は host key が観測されていない (`host_key_b64 IS NULL`)。最初の `/test` 呼び出し (または最初の Exec/Upload/Download) で鍵を観測すると、`host_key_first_observe` エラーが返り、`host_key_b64` / `host_key_fingerprint` / `host_key_pending_token` が DB に書き込まれる。
```
Host key first-observe on connection <id> (fingerprint SHA256:...).
Verify via UI (SshConnections panel) before retrying. Pending token: <uuid>
```
LLM ではここで止め、ユーザーに **UI で fingerprint を確認 → Verify** を依頼する。Verify を完了するまで全 SSH ツールは `host_key_not_verified` で失敗する。
サーバー再構築や鍵 rotation で fingerprint が変わると `host_key_mismatch` が返る。これは **既存鍵の上書きにあたるので reason 付きで UI から明示的に replace** する必要がある (`/replace-host-key` エンドポイント)。LLM は自分で replace してはいけない。
```
WARN: Host key MISMATCH on connection <id> (now SHA256:...).
Likely possibilities: server rebuild, key rotation, or MITM.
Verify carefully via UI and supply a reason. Pending token: <uuid>
```
## 共通エラーコード一覧
`isError: true` で返るエラーメッセージは以下のいずれか。LLM は基本的に **retry せず**、メッセージに従って人に判断を仰ぐか、別の手段に切り替えること。
| code | 意味 | 対応 |
|------|------|------|
| `host_key_first_observe` | 初回鍵観測 | UI で verify するようユーザーに依頼 |
| `host_key_mismatch` | 鍵 fingerprint が変化 | UI で replace するようユーザーに依頼 (MITM 可能性) |
| `host_key_not_verified` | 鍵記録済だが未 verify | 同上、UI で verify |
| `host_key_alg_not_allowed` | サーバーが禁止アルゴリズムを提示 | 接続不能、運用者に報告 |
| `auth_failed` | 秘密鍵が認証拒否された | 接続設定 (key/username) を確認 |
| `connect_timeout` | ハンドシェイク前に timeout | network 経路 / SSRF policy 確認 |
| `exec_timeout` | コマンド実行が timeout | `timeout_ms` を増やす、コマンドを軽量化 |
| `transfer_timeout` | SFTP 転送が timeout | ファイルサイズ確認、回線確認 |
| `output_too_large` | stdout が `max_output_bytes` 超過 | フィルタリング、SshDownload に切替 |
| `remote_too_large` | ファイルが `max_(up\|down)load_size_mb` 超過 | サイズ確認、設定変更 |
| `local_target_exists` | download 先が既存 | 別パス選択 |
| `forbidden_address` | SSRF policy で reject | private 接続なら `allow_private_addresses` 設定 |
| `invalid_host` / `dns_failed` / `connect_failed` | 接続 / DNS 失敗 | host 設定、ネットワーク確認 |
`abuse_locked` / `disabled_by_admin` 等の運用上の reject は `SshExec: access denied (...) for connection X.` 形式のエラー (isError=true) で返る。
## abuse counter による自動 lock
連続失敗を 3 つのスコープで集計する:
- **user**: 同一ユーザー × 任意接続
- **host:user**: 同一 (host, username) ペア
- **host (global)**: 同一 host (global connection のみ対象)
`config.yaml``ssh.abuse_window_minutes` (10) 以内に `ssh.abuse_failure_threshold` (5) 回失敗すると、当該スコープが `ssh.abuse_lock_minutes` (30) ロック。ロック解除は時間経過待ち、または **admin が UI から force-unlock** (理由 + 8 字以上必須、レート制限 10 回/時)。
成功すると user scope のカウンターだけクリアされる (他のスコープは時間経過で window から外れる)。
## 監査ログ
3 ツールはすべて以下のライフサイクルを踏む:
```
audit.begin (outcome=pending) → commit (DB)
remote 呼び出し
audit.complete (outcome=success | failed | denied | aborted)
```
途中でプロセスがクラッシュした場合、`pending` 行は次回起動時の recovery sweep で `aborted` に倒される (forensics 用「実行されたが結果不明」)。
action 名:
- `ssh.exec` (SshExec)
- `ssh.upload` (SshUpload)
- `ssh.download` (SshDownload)
- `ssh.connection.host_key.first_observe` / `mismatch` (TOFU 発火時)
`ssh.exec``detail` には command そのものではなく **SHA-256 truncated hex (16 char)**`command_hash` として記録される。command 全文は記録されない (PII / secrets 漏洩防止)。retry 検知やパターン分析は hash 比較で行う。
監査ログの参照経路:
- ユーザー本人の接続: SshConnections パネルの "Audit" タブ
- admin (全接続): Settings → SSH → Audit Log (フィルタ: action / outcome / connection / time range)
## Workflow Recipes
### A. リモートで生成したレポートを workspace に取り込む
```js
// 1. リモートでレポート生成
SshExec({ connection_id: CONN, command: "/srv/agent/build-report.sh > /tmp/report-$(date +%Y%m%d).csv" })
// 2. 生成パスを確認
const ls = SshExec({ connection_id: CONN, command: "ls -1 /tmp/report-*.csv | tail -1" })
const remote = JSON.parse(ls.output).stdout.trim()
// 3. workspace に取り込み
SshDownload({ connection_id: CONN, remote_path: remote, local_path: `input/${remote.split('/').pop()}` })
```
### B. workspace で加工した設定ファイルを反映
```js
// 1. ワークスペースで設定を生成
Write({ file_path: "output/nginx.conf", content: "..." })
// 2. リモートにアップロード
SshUpload({ connection_id: CONN, local_path: "output/nginx.conf", remote_path: "/srv/agent/nginx.conf" })
// 3. validate + reload
SshExec({ connection_id: CONN, command: "nginx -t -c /srv/agent/nginx.conf && systemctl reload nginx" })
```
### C. 大量出力を直接受け取らずファイル経由で扱う
```js
// 直接 SshExec すると max_output_bytes で truncate される
// → 一度ファイルに書いてから Download する
SshExec({ connection_id: CONN, command: "journalctl -u app --since '1 hour ago' > /tmp/app.log" })
SshDownload({ connection_id: CONN, remote_path: "/tmp/app.log", local_path: "input/app.log" })
Read({ file_path: "input/app.log", offset: 0, limit: 200 }) // 必要に応じて
```
## 関連ツール
- `Read` / `Write` / `Edit`: workspace 内のファイルを扱う前後で組み合わせる
- `Bash`: workspace 内でのローカル処理 (mkdir, jq 加工等)
## 参考
- [docs/ssh.md](../ssh.md) — 設定・UI フロー・運用ガイド・セキュリティモデル
+57
View File
@@ -0,0 +1,57 @@
# TranscribeAudio
音声ファイルを文字起こしする。話者分離(ダイアライゼーション)対応。外部の音声認識サーバーに送信して結果を受け取る。
## 基本
```js
TranscribeAudio({
file_path: "input/meeting.mp3",
language: "ja", // 省略時 config の speech_language または "ja"
diarize: true, // 話者分離(デフォルト true)
prompt: "固有名詞: 山田太郎、Project Apollo" // 文字起こしヒント
})
```
## サーバー設定(必須)
Settings UI の "Tools" セクションで:
- **Speech Server URL**: 例 `http://localhost:8000/v1`
- **Speech Timeout**: 秒(デフォルト 300
- **Speech Language**: デフォルト言語コード(`ja`, `en` 等)
サーバー URL が未設定なら "Speech server not configured" で失敗する。
## 入力ファイル
- 対応形式: `mp3`, `wav`
- workspace 内のローカルファイルパス(input/ 配下推奨)
- 大きいファイルはタイムアウトに注意(Speech Timeout を増やす)
## 出力フォーマット
### diarize: false (またはセグメント情報なし)
プレーンテキスト全文:
```
こんにちは。今日の会議を始めます。最初の議題は...
```
### diarize: true
話者ごとに区切られたテキスト:
```
[Speaker_A] こんにちは。今日の会議を始めます。
[Speaker_B] よろしくお願いします。最初の議題なんですが...
[Speaker_A] そうですね、まずは...
```
話者ラベルは `Speaker_A`, `Speaker_B`, ... のような自動採番(実名は出ない)。
## prompt の使い方
固有名詞・専門用語・略語を伝えると認識精度が上がる:
```
prompt: "Project Apollo, MLflow, Kubernetes, 田中部長"
```
短く、対象と関連の深い語だけを列挙。長すぎるとノイズになる。
+43
View File
@@ -0,0 +1,43 @@
# UpdateDashboardWidget
ユーザーの個人ダッシュボード (Side Info Panel) の Markdown widget を upsert するツール。
## いつ使う
- ユーザーから「ダッシュボードにメモして」「news タブを更新して」などと頼まれたとき
- 長期的に残したい情報 (ニュース要約、TODO、参照リンク) を残すよう指示されたとき
- 1 タスク内の一時メモには使わない (それは task のコメントに書く)
## 引数
| name | required | 説明 |
|---|---|---|
| `slug` | yes | Widget の安定 ID。kebab-case (`memo`, `news`, `todo`)、32 文字以内 |
| `content` | yes | Markdown 本文。64KB まで |
| `title` | 新規 slug では必須 | 表示タイトル。既存 slug を更新するときは省略可(既存タイトル維持) |
| `mode` | optional | `replace` (default) または `append` |
## 挙動
- 同じユーザーの `slug` が既に存在 → 更新
- 存在しない → 新規作成 (title 必須)
- `mode='append'` → 既存 content の末尾に `\n\n` 区切りで追記
## ワークフロー例
「最新のテック関連ニュースを news タブにまとめておいて」:
1. `WebFetch` などでニュースを収集
2. Markdown でまとめて
3. `UpdateDashboardWidget({ slug: "news", title: "ニュース", content: "<markdown>" })` を呼ぶ
4. ユーザーには「ダッシュボードの news タブに反映しました」と返す
## gotcha
- `slug` は user スコープでユニーク。他ユーザーの slug と衝突は起きない
- 書き込み先は実行中タスクの owner の dashboard。共有タスクでも他人の dashboard には書かない
- 1 度書いた slug の title は更新できない (新しいタイトルにしたい場合は UI から行うか、新 slug を切る)
- 64KB を超える content は失敗する → 古いログを切り詰めるか、append ではなく replace でローテーション
## 関連
+81
View File
@@ -0,0 +1,81 @@
# UpdateUserMemory
Writes or deletes a persistent memory entry in the caller's personal user folder.
## Overview
Memory entries are stored in `data/users/{userId}/memory/` as individual Markdown files with YAML frontmatter. An index (`MEMORY.md`) is automatically maintained and injected into the LLM system prompt at the start of every movement, giving the agent immediate awareness of what has been stored without reading every fact file.
Use `ReadUserMemory` to load the full body of a specific entry.
---
## Actions
### `upsert`
Creates a new entry or replaces an existing one with the same `name`.
**Required fields:** `action`, `name`, `type`, `description`, `body`
```json
{
"action": "upsert",
"name": "preferred-language",
"type": "user",
"description": "User prefers Japanese output",
"body": "Always respond in Japanese unless the user explicitly asks for another language."
}
```
The index line in MEMORY.md will be:
```
- [preferred-language](preferred-language.md) — User prefers Japanese output
```
### `delete`
Moves the fact file to `trash/` (no hard delete) and removes its index line from MEMORY.md.
**Required fields:** `action`, `name`
```json
{
"action": "delete",
"name": "preferred-language"
}
```
Returns an error if the entry does not exist.
---
## Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `action` | `"upsert" \| "delete"` | Yes | Operation to perform |
| `name` | string | Yes | Entry identifier: alphanumeric, dash, underscore only; no `.md` extension |
| `type` | `"user" \| "feedback" \| "project" \| "reference"` | For upsert | Category of the entry |
| `description` | string | For upsert | One-line summary shown in MEMORY.md index |
| `body` | string | For upsert | Full content of the fact file |
---
## Memory types
| Type | Intended use |
|------|-------------|
| `user` | Long-term user preferences, standing instructions |
| `feedback` | Corrections the user has given (e.g. "don't do X") |
| `project` | Project-specific facts (stack, conventions, key files) |
| `reference` | Reference data (URLs, credentials patterns, external IDs) |
---
## Notes
- **Idempotent upsert:** calling upsert twice with the same `name` replaces the entry; no duplicate index lines are created.
- **Soft delete:** deleted entries land in `trash/` and are never immediately erased.
- **Owner-only:** requires an authenticated user (`ctx.userId`). Cross-user writes are not possible.
- **Name format:** only `[a-zA-Z0-9_-]` are allowed. The `.md` extension is appended automatically.
+69
View File
@@ -0,0 +1,69 @@
# WebFetch
URL を HTTP GET してレスポンス本文を取得するツール。静的ページ向け。
## 基本
```js
WebFetch({ url: "https://example.com/article", timeout: 30 })
```
- HTML はテキスト化されて返る(タグ等は除去)
- JSON / XML / プレーンテキストもそのまま取得可能
- リダイレクトは自動で追従
## いつ使うか
| 状況 | 使うツール |
|------|-----------|
| 静的 HTML ページ | **WebFetch** |
| JS で動的レンダリングされる SPA | BrowseWeb |
| ボタン・フォーム操作が必要 | BrowseWeb |
| ファイルダウンロード | DownloadFile |
| 検索結果を一覧で取得 | WebSearch |
WebFetch は軽量で速い。BrowseWeb はブラウザ起動コストがかかるので、できる限り WebFetch を優先する。
## レスポンス履歴
各 WebFetch 呼び出しは `logs/webfetch-history.jsonl` に記録される。後から「どの URL を取得したか」を振り返れる。
## スクリーンショット添付(vlmEnabled 時のみ)
ワーカーが `vlm=true`(主 LLM が画像入力対応)の場合、WebFetch は成功時に Playwright でファーストビュー(1280×1600 viewport)のスクショを撮り、LLM の文脈に `image_url` として自動添付する。天気・ダッシュボード・地図など、HTML テキスト抽出では情報が欠落しやすいサイトの理解を補う目的。
- 保存先: `logs/webfetch-screenshots/{timestamp}-{url-hash}.png`
- `logs/webfetch-history.jsonl` の各レコードに `screenshotPath` が記録される
- Playwright 未インストール・CAPTCHA・タイムアウト等で失敗しても WebFetch 本体は成功扱い(テキストだけ返る)
- 無効化: `config.yaml``tools.webfetch_screenshot: false`
- タイムアウト: `tools.webfetch_screenshot_timeout_ms`(デフォルト 15,000
## SSRF 保護
ローカル/プライベート IP127.x.x.x, 10.x.x.x, 172.16-31.x.x, 192.168.x.x, ::1, fc00::/7 等)はデフォルトでブロックされる。社内ホストへアクセスする必要がある場合は Settings UI の「SSRF Allowed Hosts」に追加する。
## トラブルシューティング
- **本文がほぼ空**: SPA で JS レンダリングが必要 → BrowseWeb に切り替え
- **タイムアウト**: `timeout` パラメータを増やす(デフォルト 30 秒)
- **403/404**: User-Agent 制限・bot 検出の可能性 → BrowseWeb なら回避できる場合あり
- **SSRF blocked**: ローカル/プライベート IP に向いている → 設定追加またはターゲット見直し
## エラー時のフォールバック方針
WebFetch がエラーを返した場合、以下の原則で `BrowseWeb` にリトライする:
| エラー | BrowseWeb で再試行すべきか |
|---|---|
| HTTP 403 / 429 | **する** — bot 検出・レート制限。ブラウザ User-Agent で回避できる可能性 |
| HTTP 502 / 503 / 504 | **する** — CDN/upstream の一時的エラー。別の HTTP スタックで成功することがある |
| ネットワークエラー / タイムアウト | **する** — 動的ページが静的 fetch に応答しないケースが多い |
| HTTP 404 / 401 / 410 | しない — 永続的なエラー。URL を見直すべき |
| `invalid_url` | しない — URL の記述ミス |
| `ssrf_blocked` | しない — セキュリティ設定。Settings で allowed hosts を追加 |
| `pdf_blocked` | しない — `DownloadFile` + `ReadPdf` の組み合わせを使う |
| `binary_blocked` | しない — `DownloadFile` でバイナリ保存する |
| 本文が極端に短い(< 200 chars | **する** — SPA の空シェルを取得した可能性が高い |
| `Just a moment...` 等 Cloudflare challenge | **する** — ブラウザで JS challenge を通過できる |
リトライ時は同じ URL を `BrowseWeb({ url: "..." })` に渡すだけでよい。`BrowseWeb` はジョブ内で Cookie・セッションを保持するので、複数回呼んでもログイン状態は引き継がれる。
+51
View File
@@ -0,0 +1,51 @@
# WebSearch
Web 検索ツール。SearXNG または Playwright + Google 検索の組み合わせで動作する。
## 基本
```js
WebSearch({ query: "ローカル LLM 比較 2026", limit: 10 })
```
返ってくるのは検索結果のリスト(タイトル・URL・スニペット)。本文は含まれない。
本文が必要なら検索結果の URL に対して WebFetch / BrowseWeb を呼ぶ。
## 使うべき場面
- **最新情報の確認**(モデルの内部知識は学習時点まで)
- **実在性の確認**(人名・製品名・URL の存在チェック)
- **複数情報源の比較**
## 基本原則
### 1. 内部知識に頼らない(厳守)
調査タスクで「思い出して書く」ことは禁止。**必ず WebSearch → WebFetch で一次情報を取得する**。
古い情報、捏造、ハルシネーションのリスクが高い。
### 2. 追加質問への再検証
ユーザーからフォローアップ質問がきたら、関連キーワードで再検索すること。前回の検索結果に依存しない。
### 3. 一次情報の優先
- ブログ記事や要約サイトより、公式ドキュメント・公式発表・論文を優先
- 二次情報を引用する場合は「これは二次情報」と明記
- 動画の内容を扱う場合は GetYouTubeTranscript で字幕を取得してから扱う
### 4. 取得失敗時の振る舞い
一次情報にアクセスできなかった場合:
- 「情報を入手できなかった」と明記する
- Web 検索の断片的なスニペットから推測・捏造してはならない
- 推測を含む場合は「推測」と明示する
## 検索クエリのフィルタリング
機密情報漏洩防止のため、以下が含まれるクエリは自動でブロック・サニタイズされる:
- プライベート IP10.x.x.x, 172.16-31.x.x, 192.168.x.x, 127.x.x.x
- 内部ドメイン(`.local`, `.internal`, `.lan`, `.intranet`, `.corp`, `.home`
- メールアドレス、電話番号
これらを含むクエリは設定で許可されない限り検索エンジンに送られない。
+139
View File
@@ -0,0 +1,139 @@
# WriteUserScript
Creates or overwrites a script in the caller's user folder.
Two destinations are supported:
| kind | directory | runtime | signature |
|------|-----------|---------|-----------|
| `'script'` (default) | `scripts/` | plain Node.js | `main({ params })` |
| `'browser-macro'` | `browser-macros/` | Playwright — Chromium | `main({ context, params })` |
## Input
```ts
{
name: string, // slug — '.js' appended if absent
content: string, // full file text (frontmatter + main())
kind?: 'script' | 'browser-macro', // default: 'script'
overwrite?: boolean // default: false — error if file exists
}
```
## Required file structure
The content must define a `main` function. The following forms are all accepted:
```js
// ES function declaration
async function main({ params }) { }
// Arrow / assigned function
const main = async ({ params }) => { };
// CommonJS export
module.exports = async function main({ params }) { };
exports.main = async function({ params }) { };
```
If none of these patterns is found the tool returns `isError: true` with a
hint to add a `main` definition.
## YAML frontmatter (recommended)
```yaml
---
description: One-line human-readable description shown in ListUserAssets
params:
- name: url
type: string
- name: limit
type: number
default: 10
---
```
Frontmatter is parsed by `RunUserScript` for param validation. Scripts without
frontmatter still run, but param validation is skipped.
Browser macros may additionally declare `session_profile_id: <N>` to auto-load
a saved login session (see `RunUserScript` docs).
## Size limit
256 KB (UTF-8 encoded). Exceeding the limit returns `isError: true`.
## Overwrite semantics
By default (`overwrite: false`) writing to an existing file is an error.
Pass `overwrite: true` to replace the existing file atomically.
## When to use
- You discovered a useful reusable pattern during a task — save it for next time.
- The user asks you to create or update a script they can run later via `RunUserScript`.
- You want to prototype a browser automation without going through the UI.
## Examples
### Plain Node script
```js
WriteUserScript({
name: "fetch-and-clean",
kind: "script",
content: `---
description: Fetch a URL and return cleaned JSON
params:
- name: url
type: string
---
const https = require('https');
async function main({ params }) {
const res = await fetch(params.url);
const json = await res.json();
return { items: json.items ?? [] };
}
`
})
```
### Browser macro
```js
WriteUserScript({
name: "screenshot-dashboard",
kind: "browser-macro",
content: `---
description: Take a screenshot of the dashboard
params:
- name: url
type: string
---
async function main({ context, params }) {
const page = await context.newPage();
await page.goto(params.url);
const buf = await page.screenshot({ fullPage: true });
return { screenshotBase64: buf.toString('base64') };
}
`
})
```
## Error cases
| Situation | `isError` | message contains |
|-----------|-----------|-----------------|
| No authenticated user | true | "authenticated" |
| `name` missing / empty | true | `"name"` |
| `name` contains `/`, space, etc. | true | "alphanumeric" |
| `content` missing `main` | true | "main" |
| Content exceeds 256 KB | true | "bytes" |
| File exists, `overwrite` not set | true | "overwrite" |
## Notes
- `WriteUserScript` is a META_TOOL — available in every movement without listing it in `allowed_tools`.
- After writing, use `RunUserScript` to immediately execute and verify the script.
- Use `ListUserAssets` to see all scripts currently in the folder.
+133
View File
@@ -0,0 +1,133 @@
# WriteUserTemplate
Creates or overwrites a Markdown template in the caller's `templates/` folder.
Templates written here are immediately usable by `ReadUserTemplate` (to inspect them) and `RenderUserTemplate` (to substitute `{{var}}` placeholders and apply defaults).
## Input
```ts
{
name: string, // slug — '.md' appended if absent
content: string, // full file text (optional frontmatter + Markdown body)
overwrite?: boolean // default: false — error if file exists
}
```
## File structure
The content is plain Markdown with an optional YAML frontmatter block:
```markdown
---
description: One-line description shown in ListUserAssets
params:
- name: date
type: string
- name: author
type: string
default: "Team"
---
# Report — {{date}}
Prepared by: {{author}}
## Highlights
...
```
Frontmatter is not required. Templates without `params:` render as-is (no substitution).
## Frontmatter params spec
Each param entry in `params:` supports:
| field | required | description |
|-------|----------|-------------|
| `name` | yes | placeholder name used as `{{name}}` in the body |
| `type` | yes | `string` \| `number` \| `boolean` |
| `default` | no | value applied when the param is omitted |
Required params (no default) must be supplied by the caller of `RenderUserTemplate`. Missing required params produce an error at render time.
## Size limit
128 KB (UTF-8 encoded). Exceeding the limit returns `isError: true`.
## Overwrite semantics
By default (`overwrite: false`) writing to an existing file is an error.
Pass `overwrite: true` to replace the file atomically.
## When to use
- You noticed a recurring structure (email skeleton, weekly report, issue template) — persist it for reuse.
- The user asks you to create or update a template so they can render it later.
- You want to encode a multi-step prompt skeleton with named slots.
## Examples
### Email template
```js
WriteUserTemplate({
name: "api-error-email",
content: `---
description: Customer-facing API error notification email
params:
- name: incident_id
type: string
- name: service
type: string
- name: eta
type: string
default: "unknown"
---
Dear Customer,
We have detected an issue with **{{service}}** (incident {{incident_id}}).
Our team is actively working on a resolution. Estimated resolution time: **{{eta}}**.
We apologize for the inconvenience.
— The Platform Team
`
})
```
### Report skeleton (no params)
```js
WriteUserTemplate({
name: "weekly-retro",
content: `# Weekly Retro
## What went well
-
## What to improve
-
## Action items
- [ ]
`
})
```
## Error cases
| Situation | `isError` | message contains |
|-----------|-----------|-----------------|
| No authenticated user | true | "authenticated" |
| `name` missing / empty | true | `"name"` |
| `name` contains `/`, space, etc. | true | "alphanumeric" |
| Content exceeds 128 KB | true | "bytes" |
| File exists, `overwrite` not set | true | "overwrite" |
## Notes
- `WriteUserTemplate` is a META_TOOL — available in every movement without listing it in `allowed_tools`.
- After writing, use `ReadUserTemplate` to verify the content and `RenderUserTemplate` to test substitution.
- Use `ListUserAssets` to see all templates currently in the folder.
+134
View File
@@ -0,0 +1,134 @@
# X / Twitter ツール(XSearch / XUserPosts / XPostDetail / XFetchCardMedia
twitter-cli を内部で呼び出して X (旧 Twitter) のデータを取得する read-only ツール群。
## 認証設定(必須)
twitter-cli を動かすには Cookie 認証が必要。Settings UI の "Tools" セクションで設定:
- **X Auth Token**: ブラウザの `auth_token` cookie の値
- **X ct0**: ブラウザの `ct0` cookie の値
任意:
- **X Proxy**: `http://proxy:port` 形式
- **X Chrome Profile**: cookie 抽出元のプロファイルパス
設定が無いと「認証エラー」で失敗する。
## XSearch — 投稿検索
```js
XSearch({
query: "ローカル LLM",
limit: 20,
tab: "Latest", // Top / Latest / Photos / Videos
full_text: true, // 長文の省略を避ける
compact: false, // true で token 節約
output_path: "x/local-llm.txt" // 任意: output/x/ 配下に保存
})
```
## XUserPosts — ユーザー投稿一覧
```js
XUserPosts({
username: "elonmusk", // @ なし
limit: 50,
full_text: true
})
```
## XPostDetail — 投稿の詳細+リプライ
```js
XPostDetail({
url: "https://twitter.com/.../status/1234567890",
// または status_id: "1234567890"
})
```
返り値にはリプライツリーが含まれる。議論の流れを追いたいときに使う。
## 出力フォーマット
- デフォルト: 構造化テキスト(投稿者・本文・いいね数等)
- `compact: true`: token 節約版(簡潔表記)
- `output_path` 指定時: ファイルにも保存(パスは output/x/ 相対)
## メディアの自動ダウンロード
X / Twitter ツールは取得した投稿に紐付く画像 / 動画 (poster) を**自動的に
ワークスペースにダウンロード**して `localPath` を返す。LLM はそのパスを
ReadImage / AnnotateImage / Bash 等に直接渡せる。
```yaml
# 出力例 (XPostDetail / XUserPosts / XSearch 共通)
data:
- id: '1234567890'
media:
- type: photo
url: https://pbs.twimg.com/media/AAA.jpg?name=large
localPath: logs/x-media/1234567890/0.jpg # ← 自動付与
bytes: 384172
```
保存先は `{workspace}/logs/x-media/{tweet_id}/{N}.{ext}`。同じ tweet を再取得しても
既存ファイルは上書きしない (idempotent)。
### 動画の扱い
設定で挙動を切り替える (`tools.x_download_video`):
| モード | 挙動 |
| --- | --- |
| `thumbnail` (default) | poster (静止画 jpg) のみ DL。内容把握に十分で軽量 |
| `full` | variants から最高 bitrate の mp4 を DL。サイズ大なので明示的に有効化 |
| `never` | 動画系は完全にスキップ |
### サイズ上限
`tools.x_media_max_mb` (default 25) を超えるメディアはスキップしてログに記録。
content-length ヘッダで判定し、ボディが膨張した場合も DL 後に再度チェックする。
### 完全に無効化したいとき
```yaml
tools:
x_download_media: never # 全 X ツールでメディア DL を無効化
```
## XFetchCardMedia — quiz / poll / link card の画像取得
XSearch / XPostDetail で `media: []` が返るが、tweet が quiz / poll / link card
形式で card 画像があるはずだと LLM が判断した時のみ呼ぶ専用 tool。
```js
XFetchCardMedia({
tweet: "https://x.com/someuser/status/1234567890"
// または tweet: "1234567890" (この場合 screen_name 任意)
})
```
挙動:
- Playwright で X.com を開き、GraphQL response + 対象 article の DOM から
`pbs.twimg.com/(media|card_img)/...` URL を抽出
- 抽出した URL を `logs/x-media/{tweetId}/` に DL
- 成功すれば保存パスを返す
- 0 件なら "no card media found" を返す (LLM は plain text と判断)
**重要**: 1 回の呼び出しに Playwright 起動 + ページ遷移で約 14 秒かかる。
**XSearch / XUserPosts / XPostDetail からは自動発動しない**。LLM が以下の
状況でのみ明示的に呼ぶこと:
- XPostDetail が `media: []` を返した
- かつ tweet 本文が画像クイズ / 投票 / link preview を示唆する
- かつその画像の中身が次の判断に必要
text-only tweet には呼ばないこと (14 秒を無駄にする)。
## トラブルシューティング
- **認証エラー**: cookie の有効期限切れ。ブラウザで取得し直して Settings に再投入
- **rate limit**: しばらく待ってから再実行。検索回数を絞る
- **twitter-cli not found**: `scripts/install-twitter-cli.sh` で導入が必要
- **`media: []` のままで画像が取れない**: card / quiz 形式の投稿は X API 自体に
メディアが乗らない。XFetchCardMedia を呼んで Playwright 経由で取得する