This commit is contained in:
@@ -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