This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"title": "ファイル・ノート",
|
||||
"description": "output/ のファイルを一覧・閲覧し、メモを output/note.md に保存するサンプルアプリ。",
|
||||
"entry": "index.html"
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
<!doctype html>
|
||||
<html lang="ja">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>ファイル・ノート</title>
|
||||
<style>
|
||||
/* 自己完結: 外部 CSS/フォント禁止(CSP で遮断される)。インラインのみ。 */
|
||||
:root { color-scheme: light dark; }
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0; padding: 16px;
|
||||
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
line-height: 1.5; color: #1e293b; background: #f8fafc;
|
||||
}
|
||||
h1 { font-size: 16px; margin: 0 0 12px; }
|
||||
.row { display: flex; gap: 16px; align-items: flex-start; flex-wrap: wrap; }
|
||||
.panel { flex: 1 1 280px; min-width: 240px; }
|
||||
.card { background: #fff; border: 1px solid #e2e8f0; border-radius: 8px; padding: 12px; }
|
||||
ul { list-style: none; margin: 0; padding: 0; max-height: 220px; overflow: auto; }
|
||||
li { margin: 0; }
|
||||
.file-btn {
|
||||
width: 100%; text-align: left; border: 0; background: transparent;
|
||||
padding: 6px 8px; border-radius: 6px; cursor: pointer; font-size: 13px; color: #334155;
|
||||
}
|
||||
.file-btn:hover { background: #f1f5f9; }
|
||||
.file-btn[aria-current="true"] { background: #e0e7ff; font-weight: 600; }
|
||||
pre {
|
||||
margin: 8px 0 0; padding: 8px; background: #f1f5f9; border-radius: 6px;
|
||||
font-size: 12px; max-height: 200px; overflow: auto; white-space: pre-wrap; word-break: break-word;
|
||||
}
|
||||
textarea {
|
||||
width: 100%; min-height: 120px; padding: 8px; border: 1px solid #cbd5e1;
|
||||
border-radius: 6px; font-family: inherit; font-size: 13px; resize: vertical;
|
||||
}
|
||||
button.primary {
|
||||
margin-top: 8px; border: 0; background: #4f46e5; color: #fff; font-weight: 700;
|
||||
padding: 8px 14px; border-radius: 6px; cursor: pointer; font-size: 13px;
|
||||
}
|
||||
button.primary:disabled { opacity: .5; cursor: default; }
|
||||
.muted { color: #64748b; font-size: 12px; }
|
||||
.status { margin-top: 8px; font-size: 12px; min-height: 1.2em; }
|
||||
.status.ok { color: #047857; }
|
||||
.status.err { color: #dc2626; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>ファイル・ノート</h1>
|
||||
<p class="muted">このワークスペースの <code>output/</code> を一覧・閲覧し、メモを <code>output/note.md</code> に保存します。</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="panel">
|
||||
<div class="card">
|
||||
<strong style="font-size:13px;">output/ のファイル</strong>
|
||||
<ul id="files"><li class="muted" style="padding:6px 8px;">読み込み中…</li></ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="card">
|
||||
<strong style="font-size:13px;">プレビュー</strong>
|
||||
<pre id="preview" class="muted">左の一覧からファイルを選んでください。</pre>
|
||||
</div>
|
||||
<div class="card" style="margin-top:12px;">
|
||||
<strong style="font-size:13px;">メモ(output/note.md に保存)</strong>
|
||||
<textarea id="note" placeholder="ここにメモを書いて「保存」を押すと output/note.md に保存されます。"></textarea>
|
||||
<button id="save" class="primary">保存</button>
|
||||
<div id="status" class="status"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ── postMessage ブリッジ(自己完結 / 外部ネットワーク無し)─────────────────
|
||||
// 親 (AppRunner) が、ユーザーのセッションでファイル I/O を代理する。
|
||||
// 詳細仕様: docs/workspace-apps-bridge.md
|
||||
let seq = 0;
|
||||
const pending = new Map();
|
||||
function call(req) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const id = ++seq;
|
||||
pending.set(id, { resolve, reject });
|
||||
window.parent.postMessage({ ...req, id }, '*');
|
||||
});
|
||||
}
|
||||
window.addEventListener('message', (e) => {
|
||||
const r = e.data;
|
||||
const p = r && pending.get(r.id);
|
||||
if (!p) return;
|
||||
pending.delete(r.id);
|
||||
r.ok ? p.resolve(r.data) : p.reject(new Error(r.error || 'request failed'));
|
||||
});
|
||||
|
||||
const $files = document.getElementById('files');
|
||||
const $preview = document.getElementById('preview');
|
||||
const $note = document.getElementById('note');
|
||||
const $save = document.getElementById('save');
|
||||
const $status = document.getElementById('status');
|
||||
|
||||
function setStatus(msg, kind) {
|
||||
$status.textContent = msg;
|
||||
$status.className = 'status' + (kind ? ' ' + kind : '');
|
||||
}
|
||||
|
||||
async function loadFiles() {
|
||||
try {
|
||||
const { entries } = await call({ type: 'listFiles', dir: 'output' });
|
||||
const files = (entries || []).filter((e) => e.kind !== 'directory');
|
||||
$files.innerHTML = '';
|
||||
if (files.length === 0) {
|
||||
$files.innerHTML = '<li class="muted" style="padding:6px 8px;">output/ は空です。</li>';
|
||||
return;
|
||||
}
|
||||
for (const f of files) {
|
||||
const li = document.createElement('li');
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'file-btn';
|
||||
btn.textContent = f.name;
|
||||
btn.onclick = () => selectFile(f, btn);
|
||||
li.appendChild(btn);
|
||||
$files.appendChild(li);
|
||||
}
|
||||
} catch (err) {
|
||||
// output/ が無い等。空表示にして致命化しない。
|
||||
$files.innerHTML = '<li class="muted" style="padding:6px 8px;">output/ がまだありません。</li>';
|
||||
}
|
||||
}
|
||||
|
||||
async function selectFile(file, btn) {
|
||||
for (const b of $files.querySelectorAll('.file-btn')) b.removeAttribute('aria-current');
|
||||
btn.setAttribute('aria-current', 'true');
|
||||
$preview.className = 'muted';
|
||||
$preview.textContent = '読み込み中…';
|
||||
try {
|
||||
const { content } = await call({ type: 'readFile', path: 'output/' + file.name });
|
||||
$preview.className = '';
|
||||
$preview.textContent = content.length > 4000 ? content.slice(0, 4000) + '\n…(省略)' : content;
|
||||
} catch (err) {
|
||||
$preview.className = 'muted';
|
||||
$preview.textContent = '読み込めませんでした(テキストファイルのみ対応)。';
|
||||
}
|
||||
}
|
||||
|
||||
// 既存の note.md があれば編集欄に読み込む(無ければ空のまま)。
|
||||
async function loadExistingNote() {
|
||||
try {
|
||||
const { content } = await call({ type: 'readFile', path: 'output/note.md' });
|
||||
$note.value = content;
|
||||
} catch {
|
||||
/* 無ければ何もしない */
|
||||
}
|
||||
}
|
||||
|
||||
$save.onclick = async () => {
|
||||
$save.disabled = true;
|
||||
setStatus('保存中…');
|
||||
try {
|
||||
// output/ への書き込みは確認不要(親のサイレント許可ポリシー)。
|
||||
await call({ type: 'writeFile', path: 'output/note.md', content: $note.value });
|
||||
setStatus('output/note.md に保存しました。', 'ok');
|
||||
await loadFiles();
|
||||
} catch (err) {
|
||||
setStatus('保存に失敗しました: ' + err.message, 'err');
|
||||
} finally {
|
||||
$save.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
loadFiles();
|
||||
loadExistingNote();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -134,6 +134,19 @@ docker compose logs | grep "setup token"
|
||||
認証を有効にするまでは信頼できないネットワークへ公開しないこと。外部公開時は TLS
|
||||
対応のリバースプロキシも使用する。運用上の注意は [../SECURITY.md](../SECURITY.md) を参照。
|
||||
|
||||
### ワークスペースを共有する(招待リンク)
|
||||
|
||||
認証を有効にすると、案件ワークスペースを複数ユーザーで共有できる。メンバー追加のピッカーは
|
||||
プライバシー保護のため同じ組織のユーザーしか出ないため、組織を持たないユーザー(例: 管理者が
|
||||
ローカル組織を割り当てていない Google ログインユーザー)はピッカーが空になることがある。
|
||||
|
||||
その場合は **招待リンク** を使う。オーナーまたは管理者がワークスペースの **設定 → メンバー** で
|
||||
リンクを発行し、付与する役割(編集者 / 閲覧者)と有効期限(無期限 / 7日 / 30日)を選ぶ。リンクを
|
||||
受け取ったログインユーザーは `/ui/invite/<トークン>` を開いて参加する。リンクで付与できるのは
|
||||
編集者・閲覧者のみ(オーナー権限は付与不可)。発行・無効化はオーナー / 管理者だけが行える。
|
||||
ワークスペースごとに有効なリンクは1本で、再生成すると旧リンクは即座に失効する。無効・期限切れの
|
||||
リンクはワークスペース情報を一切返さない。認証なしモードでは招待リンクは無効。
|
||||
|
||||
## 8. Bash サンドボックスを有効にする(任意・マルチユーザー推奨)
|
||||
|
||||
エージェントの Bash 実行をタスク単位で隔離する。本番では:
|
||||
|
||||
@@ -134,6 +134,21 @@ By default it runs without authentication. To use Google / Gitea OAuth, configur
|
||||
Do not expose it to an untrusted network until authentication is enabled. When exposing it externally, also use a TLS-enabled
|
||||
reverse proxy. For operational caveats, see [../SECURITY.md](../SECURITY.md).
|
||||
|
||||
### Share a workspace (invite links)
|
||||
|
||||
With authentication enabled, a case workspace can be shared with several users. The
|
||||
member picker only lists users in the same organization (to avoid leaking the user
|
||||
list), so a user with no organization — for example a Google login the admin has not
|
||||
assigned a local org to — may see an empty picker.
|
||||
|
||||
For that case, use an **invite link**. An owner or admin generates the link from the
|
||||
workspace's **Settings → Members** and picks the granted role (editor / viewer) and an
|
||||
optional expiry (never / 7 days / 30 days). Any logged-in user who has the link joins by
|
||||
opening `/ui/invite/<token>`. Links can grant editor or viewer only (never owner), and
|
||||
only an owner or admin can create or revoke them. Each workspace has at most one active
|
||||
link; regenerating it immediately invalidates the old one, and an invalid or expired link
|
||||
returns no workspace information. Invite links are disabled in no-auth mode.
|
||||
|
||||
## 8. Enable the Bash sandbox (optional, recommended for multi-user)
|
||||
|
||||
Isolates the agent's Bash execution per task. In production:
|
||||
|
||||
@@ -117,6 +117,22 @@ grep -A 20 'LEGACY_SECTION_REDIRECT' ui/src/components/settings/SettingsSidebar.
|
||||
|
||||
---
|
||||
|
||||
## 4-C. TopBar のトップレベルタブ (page) を追加・削除した場合
|
||||
|
||||
`PageId` は複数ファイルに分散しているので、追加・削除のどちらでも下記をすべて触る。
|
||||
|
||||
**対象ファイル (すべて必須):**
|
||||
- `ui/src/lib/urlState.ts` — `PAGES` 配列 (PageId の唯一の定義源)
|
||||
- `ui/src/components/layout/TopBar.tsx` — `NAV_ITEMS` (タブ表示順 + adminOnly / requiresAuth)
|
||||
- `ui/src/components/layout/NavDrawer.tsx` — `NAV_ICONS` は `Record<PageId, ReactNode>` の網羅型。**PageId を削除したら同名キーも消さないと TS が excess-property で落ちる**
|
||||
- `ui/src/App.tsx` — `{page === '...' && <... />}` のレンダー分岐 (+ 不要になった import の削除)
|
||||
- 外部からそのページへ飛ばす導線 (`url.searchParams.set('page', '...')` 等) の付け替え
|
||||
- i18n `layout.nav.<id>` (`ui/src/i18n/locales/{en,ja}/layout.json`)
|
||||
|
||||
**履歴 (2026-06-19):** 旧「ユーザーフォルダ」(`userfolder`) タブは廃止。中身は個人ワークスペースの **設定** タブ (AGENTS.md / メモリ / Pieces / スキル / MCP / SSH / ブラウザ。`SpaceSettings.tsx`) に集約し、孤立していた **Pets** はグローバル設定の `pets` セクション (`settings/PetsForm.tsx`) へ移設した。`userfolder/` 配下のパネル (AgentsMdPanel / MemoryPanel / McpPanel / SshConnectionsPanel / SkillsPanel / AddBrowserSessionDialog / MonacoFileEditor 等) は SpaceSettings / SpaceBrowserPanel / 設定フォームが再利用するため残置。
|
||||
|
||||
---
|
||||
|
||||
## 5. SSRF 保護に関わる変更をした場合
|
||||
|
||||
**対象ファイル:**
|
||||
@@ -138,7 +154,7 @@ grep -A 20 'LEGACY_SECTION_REDIRECT' ui/src/components/settings/SettingsSidebar.
|
||||
|
||||
**なぜ必要か:**
|
||||
新ツールを追加したとき、`RAW_SAVE_TOOLS` への追加を忘れると `logs/raw/` に出力が保存されない。
|
||||
knowledge ツールのように独自で raw 保存する場合は `RAW_SAVE_TOOLS` に含めない(二重保存回避)。
|
||||
ツール側で独自に raw 保存する場合は `RAW_SAVE_TOOLS` に含めない(二重保存回避)。
|
||||
|
||||
**保存先の整理:**
|
||||
|
||||
@@ -147,7 +163,6 @@ knowledge ツールのように独自で raw 保存する場合は `RAW_SAVE_TOO
|
||||
| 生データ | `logs/raw/{tool}-{timestamp}.txt/.json` | ツール実行結果の生出力 |
|
||||
| WebFetch 履歴 | `logs/webfetch-history.jsonl` | URL, ステータス, サイズ |
|
||||
| ダウンロード履歴 | `logs/downloads.jsonl` | 保存パス, サイズ |
|
||||
| ナレッジ履歴 | `logs/knowledge-history.jsonl` | クエリ, ヒット数, 所要時間 |
|
||||
| チェックリスト | `logs/checklists/{name}.json` | チェックリスト状態 |
|
||||
| rawdata インデックス | `logs/rawdata-history.jsonl` | raw 保存のメタデータ |
|
||||
|
||||
@@ -491,45 +506,13 @@ grep -rn "/continue\b\|continueTaskWithPiece" src/scheduler.ts src/bridge/schedu
|
||||
|
||||
---
|
||||
|
||||
## 14. Knowledge Notes 追加・変更時
|
||||
## 14. Knowledge Notes 機能 (削除済み)
|
||||
|
||||
Knowledge Notes は `data/users/{userId}/notes/` 以下のマークダウンファイルで管理される共有ナレッジ機能。
|
||||
FM (frontmatter) の `visibility` / `scope_org_id` で公開範囲を制御し、他ユーザーが検索・購読・inject できる。
|
||||
|
||||
**対象ファイル:**
|
||||
- `src/db/schema.sql` と `src/db/migrate.ts` — dual path で両方更新 (知識テーブル追加時)
|
||||
- `src/engine/tools/index.ts` — 新ツールの dynamic import
|
||||
- `pieces/*.yaml` — `allowed_tools` に必要なツール名を追加
|
||||
- `src/engine/tools/docs.ts` の `TOOL_DOC_ALIASES` — 関連ツールを同じ doc にまップ
|
||||
- `ui/src/components/userfolder/FileTree.tsx` の `FILE_SUBDIRS` — サイドバーに新ディレクトリを追加
|
||||
- `config.yaml.example` — `notes.inject` セクションのサンプル設定を更新
|
||||
|
||||
**セキュリティ・可視性チェックリスト:**
|
||||
|
||||
- [ ] `src/db/schema.sql` と `src/db/migrate.ts` の両方を更新 (dual path)
|
||||
- [ ] 新ツールを `src/engine/tools/index.ts` の dynamic import に追加
|
||||
- [ ] `pieces/*.yaml` の `allowed_tools` に必要なツール名を追加
|
||||
- [ ] `src/engine/tools/docs.ts` の `TOOL_DOC_ALIASES` にエイリアスを追加
|
||||
- [ ] `ui/src/components/userfolder/FileTree.tsx` の `FILE_SUBDIRS` を更新
|
||||
- [ ] `config.yaml.example` の `notes.inject` セクションを更新
|
||||
- [ ] FM の `visibility=org` チェックは publisher の所属 org で行う (`getUserOrgIds`)
|
||||
- [ ] inject 注入経路 (`agent-loop.ts` `buildSystemPrompt`) は visibility WHERE を必ず通る
|
||||
- [ ] cross-user read は `audit_log` に記録される
|
||||
|
||||
**なぜ必要か:**
|
||||
`visibility=org` の org チェックを publisher 側の org で行わないと、異なる org のユーザーが他 org のノートを閲覧できる。inject 経路で visibility チェックを省略すると、private なノートがシステムプロンプト経由で漏洩する。cross-user read の監査記録が欠けると、後からアクセス追跡ができなくなる。
|
||||
|
||||
**確認方法:**
|
||||
```bash
|
||||
# visibility WHERE が inject 経路にあるか
|
||||
grep -n "visibility\|getUserOrgIds" src/engine/agent-loop.ts
|
||||
|
||||
# audit_log への記録が行われているか
|
||||
grep -n "audit_log\|read_note" src/engine/tools/knowledge.ts
|
||||
|
||||
# SearchNotes / ListNotes ツールが tools-api.ts に登録されているか
|
||||
grep -n "knowledge\|notes" src/bridge/tools-api.ts
|
||||
```
|
||||
共有 Markdown ノート + 購読 + `SearchNotes` / `ReadNote` / `WriteNote` ツールからなる
|
||||
Knowledge Notes 機能は 2026-06 に削除された (共有ワークスペースと役割が重複したため)。
|
||||
`src/notes/`・`src/bridge/notes-api.ts`・`src/engine/notes-inject.ts`・`NotesForm.tsx`・
|
||||
`note_index` / `note_subscriptions` / `pending_reindex` テーブルはすべて撤去済み。
|
||||
既存 DB に残る孤立テーブルは無害なので DROP していない。再導入の予定はない。
|
||||
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@
|
||||
| Office ファイル読み込み | ReadPdf / ReadExcel / ReadDocx / ReadPPTX |
|
||||
| 音声書き起こし | TranscribeAudio |
|
||||
| データベース | SQLite |
|
||||
| 検索 | WebSearch / SearchKnowledge |
|
||||
| 検索 | WebSearch / WebFetch |
|
||||
|
||||
## サンドボックス機構 (`safety.bash_sandbox`)
|
||||
|
||||
|
||||
@@ -282,6 +282,16 @@ BrowseWithSession({
|
||||
|
||||
ダウンロードを認証付きで行いたい場合は、Browser Sessions 機能で対象サイトのログインセッションを保存し、タスクで bind した状態で BrowseWeb を呼ぶこと。
|
||||
|
||||
## ソースライブラリへの蓄積(スペースタスク)
|
||||
|
||||
スペースのタスクで BrowseWeb がページ本文(スナップショット / セレクタ抽出 / dumpHtml 等の抽出テキスト)を取得すると、その本文が Markdown として `source/` に保存され、出典メタ(URL・取得日時・サイズ・`tool: "BrowseWeb"`)が `source/index.jsonl` に追記される。WebFetch / DownloadFile と同じソースライブラリに合流するので、「そのスペースのソース(資料)」が自動で溜まり、後から追加調査やグラウンディングに使える。
|
||||
|
||||
- 同一 URL は重複保存しない(既に index にあればスキップ)。同じ URL を 2 回閲覧しても 1 件のまま
|
||||
- スペース文脈でないタスク(ephemeral / gitea issue 等)では従来どおり蓄積しない(`ctx.spaceId` が無いとき)
|
||||
- 空・エラー結果、`http(s)` 以外(`file://` 等)の URL はソース化しない
|
||||
- スクリーンショットはソースではないので `output/` に残り、`source/` には入らない(ソースはあくまでテキスト)
|
||||
- これは純粋な追記サイドライトで、BrowseWeb の戻り値・スクショ・`logs/` 出力は一切変わらない
|
||||
|
||||
## SSRF 保護
|
||||
|
||||
ローカル/プライベート IP(127.x.x.x, 10.x.x.x, 172.16-31.x.x, 192.168.x.x, ::1, fc00::/7 等)へのアクセスはデフォルトでブロックされる。社内ホストへアクセスする必要がある場合は、Settings UI の「SSRF Allowed Hosts」に追加する。
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# カレンダーツール(AddCalendarEvent / ListCalendarEvents)
|
||||
|
||||
スペース別カレンダーに「予定」を読み書きするツール。予定は受動的なエントリで、
|
||||
cron の定期タスク(`scheduled_tasks`)とは別概念(予定は実行しない)。
|
||||
|
||||
予定は実行中タスクが属するスペース(`ctx.spaceId`)のカレンダーに記録される。
|
||||
スペースに属していないタスク(旧 local タスク)では使えず、明確なエラーを返す。
|
||||
|
||||
## AddCalendarEvent
|
||||
|
||||
実行中タスクのスペースのカレンダーに予定を1件追加する。
|
||||
`created_by` は常に `agent`、`source_task_id` には実行中タスク ID が記録される。
|
||||
|
||||
引数:
|
||||
- `title`(必須): 予定のタイトル
|
||||
- `date`(必須): 開始日 `YYYY-MM-DD`(ローカル日付)
|
||||
- `end_date`(任意): 終了日 `YYYY-MM-DD`。複数日にまたがる予定のとき指定。省略すると単日。`date` より前は不可
|
||||
- `time`(任意): `HH:MM`。省略すると終日扱い
|
||||
- `description`(任意): 補足説明
|
||||
|
||||
例:
|
||||
```
|
||||
AddCalendarEvent({ title: "定例MTG", date: "2026-07-01", time: "10:00" })
|
||||
AddCalendarEvent({ title: "提出締切", date: "2026-07-10" }) // 終日
|
||||
AddCalendarEvent({ title: "現地出張", date: "2026-07-10", end_date: "2026-07-12" }) // 複数日
|
||||
```
|
||||
|
||||
使いどころ: ユーザーが「○日に△△を予定に入れて」と依頼したとき、調査の結果として
|
||||
期限や予定日が判明したとき。日付は必ずユーザーのローカル暦日で指定する。
|
||||
|
||||
## ListCalendarEvents
|
||||
|
||||
スペースの予定を日付範囲で一覧する(読み取り専用)。
|
||||
|
||||
引数:
|
||||
- `from`(任意): 範囲開始 `YYYY-MM-DD`
|
||||
- `to`(任意): 範囲終了 `YYYY-MM-DD`
|
||||
|
||||
範囲を省略すると全期間。結果は日付→時刻順(終日は各日の先頭)で返る。
|
||||
|
||||
## gotcha
|
||||
|
||||
- `date` / `from` / `to` は `YYYY-MM-DD`、`time` は `HH:MM` 厳守。形式違反はエラー。
|
||||
- スペース未所属のタスクからは呼べない(`ctx.spaceId` 必須)。
|
||||
- 予定はスペースの可視性に従う。イベント単独の可視性は持たない。
|
||||
@@ -16,7 +16,10 @@ DownloadFile({
|
||||
|
||||
- `url`: ダウンロード元 URL
|
||||
- `filename`: 保存先パス(section 配下からの相対パス)
|
||||
- `section`: `"input"` か `"output"` (成果物に使う場合は `"output"`)
|
||||
- `section`: `"input"` / `"output"` / `"source"`
|
||||
- `"input"`: 後続処理の入力(既定)
|
||||
- `"output"`: 成果物
|
||||
- `"source"`: 調べ物・参照資料。スペースのソースライブラリ(`source/`)に蓄積され、出典付きで Files タブに一覧される。research 系のダウンロードはこれを推奨
|
||||
|
||||
## ファイル命名規約
|
||||
|
||||
@@ -30,6 +33,13 @@ DownloadFile({
|
||||
|
||||
`logs/downloads.jsonl` に各ダウンロードのメタ情報(URL, 保存先, サイズ)が記録される。
|
||||
|
||||
### ソースライブラリ(section: "source")
|
||||
|
||||
スペースのタスクで `section: "source"` を指定すると、ファイルは `source/` に保存され、
|
||||
出典メタ(ファイル名・URL・取得日時・サイズ・ツール名)が `source/index.jsonl` に 1 行追記される。
|
||||
これらは「そのスペースのソース(資料)」として蓄積され、後から追加調査やグラウンディングに使える。
|
||||
WebFetch がスペース文脈でページ本文を取得した場合や、BrowseWeb がブラウザでページを閲覧した場合も、同じ `source/index.jsonl` に自動で記録される。
|
||||
|
||||
## 成果物への画像埋め込み
|
||||
|
||||
ダウンロードした画像は Markdown レポートから相対パスで埋め込める:
|
||||
@@ -47,4 +57,4 @@ WebFetch と同じく、ローカル/プライベート IP はデフォルトブ
|
||||
## 注意
|
||||
|
||||
- 大きすぎるファイル(数百MB以上)はタイムアウトしやすい
|
||||
- バイナリファイル(PDF, 動画等)も保存可能だが、画像以外の用途では IngestDocument / TranscribeAudio 等の専用ツールも検討
|
||||
- バイナリファイル(PDF, 動画等)も保存可能だが、画像以外の用途では ReadPdf / TranscribeAudio 等の専用ツールも検討
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
# 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`) |
|
||||
|
||||
### 可視性ルール
|
||||
|
||||
- 自分の note(owner_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`) |
|
||||
@@ -32,7 +32,3 @@ ReadImage({ file_path: "input/screenshot.png" })
|
||||
- `input/` または `output/` 配下のローカル画像ファイル
|
||||
- URL 指定は不可(DownloadFile で先にローカル保存する)
|
||||
- 対応形式: png, jpg, jpeg, gif, webp, bmp
|
||||
|
||||
## SearchKnowledge との連携
|
||||
|
||||
SearchKnowledge が返したページ画像(`input/knowledge/{ns}/page_xxx.png`)も ReadImage で内容確認できる。
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# RequestTool
|
||||
|
||||
この movement で提示されていないツールがどうしても必要なときに、その要求を**記録**するためのメタツール(`allowed_tools` に書かなくても常時利用可能)。
|
||||
|
||||
## いつ使うか
|
||||
|
||||
- 依頼を達成するのに必要なツールが、現在の movement の `allowed_tools` に無いと気づいたとき。
|
||||
- まず「本当にそのツールが要るか」を検討すること。多くの作業は既存のツール(`Bash` / `Read` / `WebSearch` 等)で代替できる。
|
||||
|
||||
## 引数
|
||||
|
||||
| 引数 | 必須 | 説明 |
|
||||
|------|------|------|
|
||||
| `name` | はい | 必要なツール名(例: `WebSearch`, `Bash`, `mcp__foo__bar`) |
|
||||
| `reason` | はい | なぜそのツールが必要かを具体的に。これがピース作者への記録に残る |
|
||||
|
||||
## 重要: これは「要求の記録」であって「即時付与」ではない
|
||||
|
||||
RequestTool を呼んでも、そのツールが**その場で使えるようにはならない**。要求は記録され、タスク詳細とピース集計に表示される。ピース作者が `allowed_tools` / `shared_tools` に追加すれば次回から使える。
|
||||
|
||||
要求したあとの進め方:
|
||||
|
||||
1. そのツール無しで達成できないか、もう一度考える。
|
||||
2. どうしても無理なら `complete({status: "needs_user_input", missing_info: "...", why_no_default: "..."})` でユーザーに依頼する。
|
||||
3. 勝手に諦めて中途半端な結果を返さない。
|
||||
|
||||
## 分類(記録される `category`)
|
||||
|
||||
- **既に利用可能**: そのツールはこの movement で使える → 記録せず「そのまま呼んでください」と返る。
|
||||
- **`requested`**: カタログに存在するがこの movement では未許可 → 設定漏れ候補として記録。
|
||||
- **`unknown`**: そんなツールは存在しない(名前の誤り・能力ギャップ)→ 記録するが付与対象外。
|
||||
|
||||
## 関連
|
||||
|
||||
足りないツールを呼んで弾かれた場合も、同じ記録に「受動捕捉(`blocked`)」として残る。ピース側の `shared_tools`(全 movement 共通ツール)も参照(`pieces/SCHEMA.md`)。
|
||||
@@ -1,103 +0,0 @@
|
||||
# SearchKnowledge / ListNamespaces / ListDocuments / IngestDocument / IngestStatus
|
||||
|
||||
DKS(Document 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 で完了確認)
|
||||
@@ -0,0 +1,38 @@
|
||||
# Skills(InstallSkill / ReadSkill / ListSkills)
|
||||
|
||||
Skill は「参照知識」(手順書・ガイド・規約)。実行テンプレートの Piece とは別物。
|
||||
Piece の取得・編集には GetPiece / CreatePiece / UpdatePiece を使う。
|
||||
|
||||
利用可能なスキル一覧はシステムプロンプトの **Skills Index** に出る。本文を読むには
|
||||
`ReadSkill({ name: "..." })` を呼ぶ。
|
||||
|
||||
## ツール
|
||||
|
||||
- **InstallSkill** — スキルを保存する。通常は `content` に SKILL.md 全文(YAML frontmatter + 本文)を渡す。workspace 内に `SKILL.md` と `scripts/` 等を含むディレクトリを組み立て済みなら `sourcePath`(workspace 内の絶対パス)を渡す。`scope` は `user`(個人 or 共有ワークスペース)か `system`(全ユーザー共有・admin のみ)。
|
||||
- **ReadSkill** — スキル本文を取得する。ディレクトリ型スキルは workspace の `skills/{name}/` に展開され、その相対パスでスクリプトを実行できる。
|
||||
- **ListSkills** — インストール済みスキルの一覧を返す。
|
||||
|
||||
## スコープと可視性(重要)
|
||||
|
||||
`scope: 'user'` で保存したスキルの置き場所は、**いま作業しているワークスペースで決まる**。
|
||||
AGENTS.md・メモリ・Piece と同じ考え方。
|
||||
|
||||
| 作業中のワークスペース | 保存先 | 見える人 |
|
||||
|---|---|---|
|
||||
| 共有ワークスペース(案件など) | `data/spaces/{spaceId}/skills/` | そのワークスペースのメンバー全員 |
|
||||
| 個人ワークスペース | `data/users/{userId}/skills/` | 自分だけ |
|
||||
|
||||
つまり、共有ワークスペースで作ったスキルはメンバー間で共有され、Skills Index にも
|
||||
全メンバー分が出る。個人ワークスペースで作ったスキルは自分専用で、他人には見えない。
|
||||
|
||||
`scope: 'system'` は作業中のワークスペースに関係なく常に全ユーザー共有(admin 専用)。
|
||||
この挙動はワークスペーススコープの影響を受けない。
|
||||
|
||||
### 共有時の注意
|
||||
|
||||
共有ワークスペースのスキルは他メンバーのジョブからも実行され得る(共有 Piece・
|
||||
共有 AGENTS.md と同じ信頼境界)。秘匿情報や自分専用の手順は、共有ワークスペースでは
|
||||
保存しないこと。個人ワークスペースで作れば自分だけのものになる。
|
||||
|
||||
InstallSkill のセキュリティスキャン(高深刻度をブロック、中深刻度を警告)と
|
||||
sourcePath の workspace 外参照拒否・サイズ/深さ上限は、スコープに関係なく常に適用される。
|
||||
@@ -1,43 +0,0 @@
|
||||
# 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 でローテーション
|
||||
|
||||
## 関連
|
||||
|
||||
+34
-1
@@ -8,10 +8,12 @@ URL を HTTP GET してレスポンス本文を取得するツール。静的ペ
|
||||
WebFetch({ url: "https://example.com/article", timeout: 30 })
|
||||
```
|
||||
|
||||
- HTML はテキスト化されて返る(タグ等は除去)
|
||||
- HTML は **readability 抽出**でクリーンな Markdown に変換されて返る(ナビ・広告・ヘッダー・フッターを除去し、見出し・リスト・テーブル・リンクは保持)
|
||||
- JSON / XML / プレーンテキストもそのまま取得可能
|
||||
- リダイレクトは自動で追従
|
||||
|
||||
返却テキストは context 節約のため約 10,000 文字で切り詰められる。**スペースタスクでは全文(未切り詰め)が `source/` に保存される**ので、切り詰めが起きると末尾に `... (truncated; full version saved to source/<file>)` という案内が付く。全文を読みたいときは保存された `source/` のファイルを `Read` で開けばよい。
|
||||
|
||||
## いつ使うか
|
||||
|
||||
| 状況 | 使うツール |
|
||||
@@ -24,6 +26,27 @@ WebFetch({ url: "https://example.com/article", timeout: 30 })
|
||||
|
||||
WebFetch は軽量で速い。BrowseWeb はブラウザ起動コストがかかるので、できる限り WebFetch を優先する。
|
||||
|
||||
## クリーン本文抽出(readability)
|
||||
|
||||
取得した生 HTML は、記事本文だけを取り出す readability エンジンで処理してから返す。
|
||||
内部パイプラインは `HTML → linkedom で DOM 化 → @mozilla/readability で記事抽出 → turndown(+gfm) で Markdown 化`。
|
||||
|
||||
- **除去されるもの**: ナビゲーション、広告バナー、ヘッダー、フッター、サイドバー等のボイラープレート
|
||||
- **保持されるもの**: 見出し(`#` 記法)、箇条書き、番号付きリスト、テーブル(GFM 記法)、リンク、コードブロック
|
||||
- 相対リンクは取得 URL を基準に絶対 URL へ変換される
|
||||
- `selector` を指定すると、その CSS セレクタにマッチする要素のサブツリーだけをクリーン抽出する(見つからなければページ全体の抽出にフォールバック)
|
||||
|
||||
抽出に失敗するページ(記事構造のない LP、JS で本文を生成する SPA、空ページ等)では、
|
||||
従来の素朴なタグ除去にフォールバックするので WebFetch が失敗扱いになることはない。
|
||||
本文がほぼ空のときは SPA の可能性が高いので `BrowseWeb` に切り替える。
|
||||
|
||||
### 全文は source へ、切り詰め版は agent へ
|
||||
|
||||
- agent のコンテキストに返るのは約 10,000 文字に切り詰めた版(context 膨張を防ぐため)
|
||||
- **スペースタスクでは、切り詰める前の全文クリーン Markdown が `source/` に保存される**
|
||||
- 切り詰めが起きた場合、返却テキスト末尾に `... (truncated; full version saved to source/<file>)` が付く
|
||||
- 病的に巨大なページ対策として、`source/` 保存も 2MB で打ち切る(その旨ノートを付す)
|
||||
|
||||
## レスポンス履歴
|
||||
|
||||
各 WebFetch 呼び出しは `logs/webfetch-history.jsonl` に記録される。後から「どの URL を取得したか」を振り返れる。
|
||||
@@ -75,3 +98,13 @@ WebFetch は取得 body の先頭 8KB を sniff し(magic byte / NUL / 不正
|
||||
(例: `.xls` を `text/plain`)していても実バイトで検出する。バイナリを処理したい場合は
|
||||
`DownloadFile` で `input/` に保存し、`ReadExcel` / `ReadPdf` 等を使う。テキスト body は
|
||||
5MB で打ち切られる(末尾に `[truncated: body exceeded 5MB]`)。
|
||||
|
||||
## ソースライブラリへの蓄積(スペースタスク)
|
||||
|
||||
スペースのタスクで WebFetch がページ本文を取得すると、**クリーン抽出した全文 Markdown**(agent への
|
||||
返却で切り詰める前のもの)が `source/` に保存され、出典メタ(URL・タイトル・取得日時・サイズ)が
|
||||
`source/index.jsonl` に追記される。
|
||||
これにより「そのスペースのソース(資料)」が自動で溜まり、後から追加調査やグラウンディングに使える。
|
||||
同一 URL は重複保存しない(既に index にあればスキップ)。スペース文脈でないタスク
|
||||
(ephemeral / gitea issue 等)では従来どおり蓄積しない。`logs/raw/` への生データ保存は従来どおり。
|
||||
BrowseWeb(ブラウザ閲覧)や DownloadFile(`section: "source"`)も同じ `source/index.jsonl` に合流する。
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# ワークスペース・アプリの作り方(エージェント向け)
|
||||
|
||||
ユーザーから「ワークスペース・アプリを作って」「○○用の GUI を作って」などと頼まれたときの
|
||||
手順です。専用ツールは不要 — `edit: true` の movement で **Write/Edit を使って HTML を書く**
|
||||
だけで成立します。
|
||||
|
||||
## 何を作るか
|
||||
|
||||
ワークスペースの `apps/{name}/index.html` に置く、**自己完結した 1 枚の HTML**(インライン
|
||||
JS/CSS、`data:` 画像のみ)。ユーザーはワークスペースの「アプリ」タブから「開く」で起動します。
|
||||
アプリはサンドボックス iframe(opaque origin、`allow-same-origin` 無し)で動き、ファイル I/O は
|
||||
親への **postMessage ブリッジ経由のみ**です。
|
||||
|
||||
## 守るべき制約(重要)
|
||||
|
||||
- **外部ネットワーク禁止**: `default-src 'none'; connect-src 'none'` の CSP が自動注入される。
|
||||
外部 CDN・外部 API・別ファイルの `<script src>`・Web フォントは読み込めない。すべてインライン化する。
|
||||
- **資格情報は渡らない**: I/O は親がユーザーのセッションで代理する。ユーザーの権限を超えられない。
|
||||
- **パスはワークスペース相対**: `output/...`・`apps/{name}/data/...` への書き込みは確認不要。
|
||||
それ以外への書き込み・削除はユーザー確認が出る。`..`・絶対パスは拒否される。
|
||||
|
||||
## ブリッジ API(要点)
|
||||
|
||||
```js
|
||||
// 一意 id を付けて postMessage、message イベントで応答を id 突き合わせ。
|
||||
const { entries } = await call({ type: 'listFiles', dir: 'output' }); // 一覧
|
||||
const { content } = await call({ type: 'readFile', path: 'output/x.md' }); // 読み取り
|
||||
await call({ type: 'writeFile', path: 'output/note.md', content: '...' }); // 書き込み
|
||||
await call({ type: 'deleteFile', path: 'output/old.txt' }); // 削除(常に確認)
|
||||
```
|
||||
|
||||
完全なプロトコル・最小ヘルパー実装・応答形式は **`docs/workspace-apps-bridge.md`** を参照。
|
||||
|
||||
## ひな型をコピーする
|
||||
|
||||
動くサンプルが `docs/examples/workspace-apps/file-note/` にある(`index.html` + `app.json`)。
|
||||
`output/` を一覧・閲覧し、メモを `output/note.md` に保存する自己完結アプリ。これを土台に、
|
||||
依頼に合わせて改変して `apps/{name}/index.html` に Write するのが最短。
|
||||
|
||||
`apps/{name}/app.json`(任意)で一覧の表示名・説明・エントリを指定できる(無ければフォルダ名)。
|
||||
|
||||
## 仕上げ
|
||||
|
||||
- 書き込んだら、ユーザーに「アプリ」タブの「開く」で起動できることを一言伝える。
|
||||
- アプリ名(フォルダ名)は内容が分かる短い英小文字+ハイフンにする(例: `invoice-gen`)。
|
||||
@@ -0,0 +1,141 @@
|
||||
# ワークスペース・アプリ ブリッジ仕様(アプリ作者=エージェント向け)
|
||||
|
||||
ワークスペース・アプリは、ワークスペースの `apps/{name}/index.html` に置く HTML 製の小さな
|
||||
GUI です。ワークスペースの **「アプリ」タブ**に一覧表示され、「開く」を押すと `AppRunner` が
|
||||
サンドボックス iframe で実行します(ファイルタブの「アプリとして実行」からも起動できます)。
|
||||
アプリはワークスペースのファイルを **postMessage ブリッジ経由でのみ** 読み書きできます。
|
||||
|
||||
このドキュメントは、生成する HTML(アプリ)が従うべきブリッジ API を定めます。
|
||||
|
||||
> **エージェントへ**: ユーザーから「ワークスペース・アプリを作って」と頼まれたら、この仕様に
|
||||
> 従った**自己完結 HTML** を `apps/{name}/index.html` に Write してください(外部リソース禁止・
|
||||
> インライン JS/CSS のみ)。動くひな型は `docs/examples/workspace-apps/file-note/`(`index.html`
|
||||
> + `app.json`)。この仕様自体は `ReadToolDoc({ name: "workspace-apps" })` でいつでも読めます。
|
||||
|
||||
## 実行環境(重要な制約)
|
||||
|
||||
- アプリは `<iframe sandbox="allow-scripts">` で動きます。**`allow-same-origin` は付きません。**
|
||||
そのため不透明オリジン(opaque origin)で動作し、親ウィンドウの DOM・Cookie・セッション・
|
||||
localStorage には一切アクセスできません。
|
||||
- **資格情報は渡されません。** ファイル I/O は親が代理します。代理は実行中ユーザーのセッションで
|
||||
行われるため、アプリはそのユーザーの権限を超えられません(メンバーシップ・編集権限・パス
|
||||
ガードはサーバ側でそのまま効きます)。
|
||||
- **外部ネットワークは使えません。** `fetch` / `XMLHttpRequest` による外部通信は想定外です
|
||||
(配信はサンドボックス + CSP で絞られます)。データの入出力は必ずブリッジ経由で行ってください。
|
||||
- V1 の主対象は **単一の自己完結 `index.html`**(JS/CSS をインライン化したもの)。複数ファイル
|
||||
構成(`apps/{name}/` 配下の相対参照)はベストエフォートで、相対 `src`/`href` は親が raw URL に
|
||||
書き換えます。確実に動かしたいなら 1 ファイルにまとめてください。
|
||||
|
||||
## マニフェスト `app.json`(任意)
|
||||
|
||||
アプリフォルダに `apps/{name}/app.json` を置くと、「アプリ」タブの表示情報を指定できます。
|
||||
**任意**で、無ければフォルダ名と `index.html` がそのまま使われます。形式が壊れていても無視され、
|
||||
フォールバックされます(アプリ一覧は壊れません)。
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "ファイル・ノート",
|
||||
"description": "output/ のファイルを閲覧してメモを保存するアプリ",
|
||||
"entry": "index.html"
|
||||
}
|
||||
```
|
||||
|
||||
| キー | 必須 | 説明 |
|
||||
|---|---|---|
|
||||
| `title` | 任意 | 一覧に出す表示名(無ければフォルダ名) |
|
||||
| `description` | 任意 | 一覧に出す短い説明 |
|
||||
| `entry` | 任意 | アプリフォルダ基準の HTML エントリ(既定 `index.html`)。**必ず `apps/{name}/` 配下**に収めること(`..` や絶対パスは拒否され、`index.html` にフォールバックされる) |
|
||||
|
||||
## ブリッジ・プロトコル
|
||||
|
||||
要求は `window.parent.postMessage(req, '*')` で送り、応答は `window` の `message` イベントで
|
||||
受け取ります。各要求には一意の `id` を付け、応答の `id` と突き合わせてください。
|
||||
|
||||
### 要求
|
||||
|
||||
```js
|
||||
// 読み取り
|
||||
window.parent.postMessage({ id, type: 'readFile', path: 'output/data.json' }, '*');
|
||||
// 一覧(dir 省略でワークスペース直下)
|
||||
window.parent.postMessage({ id, type: 'listFiles', dir: 'output' }, '*');
|
||||
// 書き込み(content は文字列)
|
||||
window.parent.postMessage({ id, type: 'writeFile', path: 'output/請求書.html', content: '<html>…' }, '*');
|
||||
// 削除(常に確認ダイアログが出る)
|
||||
window.parent.postMessage({ id, type: 'deleteFile', path: 'output/old.txt' }, '*');
|
||||
```
|
||||
|
||||
### 応答
|
||||
|
||||
```js
|
||||
window.addEventListener('message', (e) => {
|
||||
const r = e.data; // { id, ok, data } または { id, ok:false, error }
|
||||
if (r.id !== myRequestId) return;
|
||||
if (r.ok) { /* r.data を使う */ } else { /* r.error を表示 */ }
|
||||
});
|
||||
```
|
||||
|
||||
応答ペイロード:
|
||||
|
||||
| type | 成功時 `data` |
|
||||
|-------------|-------------------------------------------------|
|
||||
| `readFile` | `{ path, content }`(content は UTF-8 テキスト)|
|
||||
| `listFiles` | `{ dir, entries }`(entries はファイル/フォルダ)|
|
||||
| `writeFile` | `{ path, bytes }` |
|
||||
| `deleteFile`| `{ deleted, skipped }` |
|
||||
|
||||
失敗時は `{ id, ok:false, error }`。
|
||||
|
||||
## パスの規則
|
||||
|
||||
- パスは必ず **ワークスペース files 直下からの相対パス**。先頭 `/`・`..`・ドライブ文字
|
||||
(`C:`)・バックスラッシュは拒否されます(クライアントとサーバの二重ガード)。
|
||||
- アプリは自分のワークスペースの外には出られません。
|
||||
|
||||
## 書き込みの確認ポリシー
|
||||
|
||||
- **確認なしで書ける場所**: `output/` 配下、および自分のアプリの `apps/{name}/data/` 配下。
|
||||
- それ以外のパスへの `writeFile` は、親が確認ダイアログ(「アプリ『{name}』が {path} に書き込もう
|
||||
としています。許可しますか?」)を出し、ユーザーが許可したときだけ書き込みます。
|
||||
- `deleteFile` は場所に関わらず常に確認ダイアログが出ます。
|
||||
|
||||
データを残すアプリは、原則として `output/`(ユーザーに見せる成果物)か
|
||||
`apps/{name}/data/`(アプリ内部の状態)に書いてください。そうすればユーザーを確認で煩わせません。
|
||||
|
||||
## 最小サンプル(自己完結 index.html)
|
||||
|
||||
```html
|
||||
<!doctype html><meta charset="utf-8">
|
||||
<button id="save">保存</button>
|
||||
<script>
|
||||
let seq = 0;
|
||||
const pending = new Map();
|
||||
function call(req) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const id = ++seq;
|
||||
pending.set(id, { resolve, reject });
|
||||
window.parent.postMessage({ ...req, id }, '*');
|
||||
});
|
||||
}
|
||||
window.addEventListener('message', (e) => {
|
||||
const r = e.data;
|
||||
const p = pending.get(r && r.id);
|
||||
if (!p) return;
|
||||
pending.delete(r.id);
|
||||
r.ok ? p.resolve(r.data) : p.reject(new Error(r.error));
|
||||
});
|
||||
document.getElementById('save').onclick = async () => {
|
||||
await call({ type: 'writeFile', path: 'output/note.txt', content: 'hello' });
|
||||
alert('保存しました');
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
## ネットワーク制約(重要)
|
||||
|
||||
アプリ HTML には `default-src 'none'; connect-src 'none'; ...` の CSP が自動注入され、
|
||||
**外部ネットワーク(fetch/XHR/WebSocket/sendBeacon/フォーム外部 POST)はすべて遮断**される。
|
||||
アプリのファイル I/O は postMessage ブリッジ(親が代理)だけが経路。これは、ユーザーから
|
||||
渡されたワークスペースのファイル内容を外部へ持ち出されないための exfil 対策。
|
||||
|
||||
V1 のアプリは **自己完結 HTML**(インライン JS/CSS、`data:` 画像)で書くこと。
|
||||
外部 CDN・外部 API・別ファイルの `<script src>` は CSP で読み込めない。
|
||||
Reference in New Issue
Block a user