142 lines
7.4 KiB
Markdown
142 lines
7.4 KiB
Markdown
# ワークスペース・アプリ ブリッジ仕様(アプリ作者=エージェント向け)
|
||
|
||
ワークスペース・アプリは、ワークスペースの `apps/{name}/index.html` に置く HTML 製の小さな
|
||
GUI です。ワークスペースの **「アプリ」タブ**に一覧表示され、「開く」を押すと `AppRunner` が
|
||
サンドボックス iframe で実行します(ファイルタブの「アプリとして実行」からも起動できます)。
|
||
アプリはワークスペースのファイルを **postMessage ブリッジ経由でのみ** 読み書きできます。
|
||
|
||
このドキュメントは、生成する HTML(アプリ)が従うべきブリッジ API を定めます。
|
||
|
||
> **エージェントへ**: ユーザーから「ワークスペース・アプリを作って」と頼まれたら、この仕様に
|
||
> 従った**自己完結 HTML** を `apps/{name}/index.html` に Write してください(外部リソース禁止・
|
||
> インライン JS/CSS のみ)。動くひな型は `docs/examples/workspace-apps/note-editor/`(`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 で読み込めない。
|