本文へ移動

セキュリティモデル ​

Agent Boardは、一人の利用者が協力的なエージェントと使うローカルのツールです。このページでは、保護する対象、各保護機能の仕組み、保護の対象外となる範囲を説明します。データを書き込む、内容を配信する、エージェントの設定に触れる変更をする前に読んでください。

前提と目標 ​

  • 利用者は一人です。同じマシンのエージェントは協力的で、規則を読んでそれに従います。悪意のあるプログラムからの完全な防御は目的にしません。
  • 守るのは次の二つです。
    1. タスクの内容を、ネットワーク上の他人や外部のWebサイトから見られず、変えられないこと。
    2. 削除は人だけという線を、エージェントがうっかり越えないこと。

以下の保護機能は、この二つの目標に基づいています。ルールに従うエージェントの誤操作を防ぐだけの制限は、その範囲を明記します。

保護機能の一覧 ​

想定するリスク保護の仕組み詳細
ネットワーク上の誰かがボードを開くserveは127.0.0.1だけで待ち受ける。スマホからの接続は依頼したときだけ始まり、起動ごとに作る4桁の合言葉と、間違いの回数の制限で守る。公開範囲、ngrokの入口
Webサイトがブラウザにボードへ書き込ませるすべての書き込みでHost・Origin・Sec-Fetch-Site・起動ごとのトークンを確かめる。ブラウザからの書き込み
エージェントがデータを削除する削除のコマンドは端末での人のy/Nを必須にし、Web画面はアーカイブ済みのタスクだけを削除する。削除は人だけ
エージェントが下書きや完了したタスクを上書きするCLIが下書きとDoneのタスクへのAIの書き込みを拒否する。下書きとDoneのタスク
タスクの文がブラウザでコードとして動くReactのエスケープ、生のHTMLなし、リンクと画像の許可リスト、CSP、Mermaidのstrictモード。信頼できない文の表示
入力を通じて管理対象外のファイルへアクセスするパスを組み立てる前にIDを検証し、文をパスやシェルのコードにしない。ファイルとパス
ドキュメントのパスがdocs/の外に出るパスを要素ごとに確かめ、シンボリックリンクを拒否し、.mdファイルと4種類の画像だけを返す。プロジェクトのドキュメント
エージェントがプロジェクトを取り違えるすべての書き込みは、不変なProjectIdで一つのプロジェクトを指定する。プロジェクトの境界
setupが必要以上のアクセス権を付与するデータ領域だけを加え、既存の制限は保つ。エージェントのアクセス設定

公開範囲 ​

  • 既定のagboard serveは127.0.0.1だけで待ち受け、すべての要求はHost: 127.0.0.1:<port>かlocalhost:<port>でなければなりません。同じマシンの利用者は認証しません。
  • ngrokのモニター接続を起動したときだけ、サーバーはモニター用の別のループバックの入口を加え、ngrokがそこへ転送します。ローカルのポートは転送しません。入口が配信するのはモニター・その資産・読み取り・回答・表示設定の経路だけで、PCのボードは配信しません。
  • モニターは、読み取り、回答、言語とカラーテーマの保存に限られます。タスク・プロジェクト・接続の設定は変えられません。モニターの前段の検査はngrokの入口で説明します。
  • 接続設定のAPI(/api/monitor-connections)はローカルのリスナーにだけあります。スマホから接続を開始・停止・再設定することはできません。

ngrokの入口 ​

ngrokのアドレスを開ける人が最初に届くのは、ボードではなくngrokのEdgeです。無料のngrokプランはTraffic Policyを拒否するため、エンドポイントはTraffic Policyなしで開き、Edgeはサインインを行わず、何も付けません。要求は次の層を順にすべて通らなければなりません。各層とライフサイクルはモバイルモニターで説明します。

層確かめること防ぐもの
HostエンドポイントはTraffic Policyなしで開くため、Edgeは何も付けない。Hostヘッダーは公開HTTPSアドレスとちょうど一致する必要があり、それ以外は同じ403を返す。ループバックのポートに届くブラウザやWebサイト(Hostがループバックのアドレスか別の名前)、別のHostへの要求、偽のngrok-auth-user-*やX-Forwarded-*のヘッダー。
合言葉とsession起動ごとに作る4桁の合言葉が、この起動とアドレスに結び付いたメモリー上のアプリsession(agboard_ngrok_session、Secure; HttpOnly; SameSite=Lax; Path=/monitor/、最長12時間)を開く。合言葉は定時間で比較し、連続5回の間違いで、次の起動までのすべての試行を止める。合言葉を知らないままのモニターAPIの読み取り。起動ごとに5回を超える当て推量。停止や再起動のあとでのsessionの再利用。
CSRFとOrigin回答・表示設定の保存・切断では、公開Originとの完全一致、Sec-Fetch-Site: same-origin、sessionから導くCSRFトークン。Webサイトがスマホのブラウザに回答や表示設定の保存を送らせること。
  • モニターを守るのはGoogleでもアカウントでもありません。合言葉はPCの画面とスマホの間の共有の秘密なので、サインインより弱い守りです。10,000通りしかなく、当て推量の制限と短い寿命で守ります。アプリはEdgeの本人確認ヘッダー、Forwarded、X-Forwarded-*を何かの証明として受け付けず、Hostの確認のあとで取り除きます。
  • 合言葉を見られるのはPCだけです。readyの間のローカルの状態の応答、接続のダイアログ、serveの起動行に表示します。ファイル、ログ、serve.json、ページ、エラーには書かず、間違いの回数も報告しません。
  • 合言葉、session、CSRFトークン、ngrokのトークンはメモリーだけに置きます。停止、開始のやり直し、サーバーの再起動で破棄され、開いているイベントストリームも終わり、新しい合言葉と数え直した回数になります。
  • ngrokのトークンは、ローカルの開始要求の本文でだけ受け取り、返したり設定ファイルに書いたりしません。利用者が保存を選んだ場合は、このPCの安全な保管場所(OSの資格情報ストア)に保存します。環境変数NGROK_AUTHTOKENは保存済みのトークンより優先されます。
  • Traffic Inspector。 ngrokはHTTPSを終端するため通信を処理でき、アカウントでFull Captureが有効だと、Traffic Inspectorがリクエストとレスポンスの詳細を保持します。Agent Boardはこれを無効にできません。利用者が、ngrokのダッシュボードのSettings → Observabilityで無効であることを確認します。
  • モニターは、PCの画面の経路、編集やアーカイブの経路、ファイルの一覧を配信しません。未知の経路は404、許可されないメソッドは405で、対象を探す前に認証を評価します。

保存した言語とカラーテーマ ​

言語とカラーテーマは、PCの<データ領域>/.internal/preferences.jsonに保存します。書き込む経路は二つです。PCのPUT /api/preferencesは、後述のブラウザからの書き込みの保護を通ります。モニターのPUT /monitor/api/preferencesは、前述のngrokの入口の確認を通ります。

  • モニターの保存には、回答と同じ確認が必要です。Hostの確認、有効な合言葉のsession、公開Originとの完全一致、Sec-Fetch-Site: same-origin、CSRFトークンを求めます。
  • 受け付けるのはlocaleとthemeだけです。正しいJSONオブジェクトに含まれるほかの項目や許可されない値は、invalid_preferencesで拒否します。JSONとして壊れた本文は400のinvalid_json、JSON以外のcontent typeは415で拒否します。いずれの場合も何も保存しません。
  • 保存する値は秘密ではありません。言語と配色の二つだけです。ファイルは所有者だけが読み書きでき、トークン・合言葉・sessionは含みません。
  • 守られているのは、モニターが変えられる設定がこの二つだけであるという制限です。
  • 守れないこと: 入ることを許された端末、またはそのsessionを奪った人は、PCの言語とテーマを変えられます。開いているすべてのページに、その選択が反映されます。タスク・プロジェクト・接続の設定は変えられません。

ブラウザからの書き込み ​

ローカルのリスナーは、書き込み(作成・編集・下書きの切り替え・エピックの作成と更新・エピックの所属・画像のアップロードと削除・移動・回答・アーカイブ・復元・削除・保存した言語とテーマ)を、次のすべてを満たすときだけ受け付けます。満たさなければ403を返し、何も変えません。

  1. Hostがローカルのループバックの条件を満たす。
  2. Originがhttp://127.0.0.1:<port>かhttp://localhost:<port>。
  3. Sec-Fetch-Siteがsame-origin。
  4. X-Agboard-Tokenが、このserveの起動時に生成したトークン(32バイトの乱数の16進表記)と一致する。トークンはGET /のHTMLにだけ埋め込み、serve.json・APIの応答・ログには出さない。

保護は対象を探す前に行い、actorは常にhumanとして記録します。POST /api/notifyはクライアントに再取得を促すだけなので、トークンは要りません。

これらの検証は、要求がブラウザのページから来たことを確かめます。ヘッダーを偽装し、HTMLからトークンを抜き出すローカルのプログラムは止められません。そのため、agboard --helpはエージェントに削除を人に頼むよう伝え、APIを説明しません。

削除は人だけ ​

  • rm・project rm・epic rmと、空でないプロジェクトへのinitは、端末での人のy/Nの確認を必須にします(CLIのガイドを参照)。端末がないとき、または--jsonのときは、確認を出さずに失敗します。確認を省くオプションはありません。
  • プロジェクトの削除では、名前と、すべてのタスクとその履歴が消えることを示します。
  • Web画面が物理削除するのは、書き込みの保護を通ったアーカイブ済みのタスクだけです。アーカイブはデータを保ち、物理削除はタスクのフォルダ(本文・状態・履歴・画像)を消します。利用者がほかの場所に作ったコピーには触れません。
  • 最初のリリースまで破壊的な変更を許すのは、開発中の形式の変更だけです。これらの確認を免除するものではありません。

下書きとDoneのタスク ​

  • CLIのedit・mv・state・history addは下書きのタスクを拒否し、人が書いているタスクを協力的なエージェントが上書きしないようにします。
  • DoneのタスクへのAIの変更(編集・状態・エピック・履歴・画像)は拒否します。
  • アーカイブ済みのタスクへの書き込み(編集・下書きの変更・移動・状態・回答・添付)は書き込みロックの中で拒否し、画像は読めるままにします。
  • 下書きの切り替えと編集のAPIは、ブラウザの書き込みの保護を通った要求だけを受け付けます。
  • actor(aiかhuman)は操作の区分で、認証ではありません。CLIでの明示の入力か、Web側の固定のhumanから決め、担当からは推測しません。

信頼できない文の表示 ​

タイトル・説明・理由・回答はエージェントと利用者が書くので、画面はこれらを信頼できないものとして扱います。

  • 文はReactで表示し、HTMLとして解釈しません(dangerouslySetInnerHTMLは使わない)。Markdownは生のHTMLを文字として表示します。外部のスクリプトやフォントは読み込みません。
  • 有効なリンクはhttpとhttpsだけで、noopener noreferrer付きで新しいタブに開きます。javascript:などほかのスキームは文字として表示します。
  • 本文の画像は、同じタスクまたはエピックが所有するassets/<id>.<ext>(またはattachment:<id>)だけを、ProjectId・所有者・画像IDを確かめる画像APIで解決します。../・絶対パス・file:・data:・javascript:・別の所有者・別のプロジェクト・シンボリックリンクは表示しません。外部の画像はhttpとhttpsのURLからだけ、Refererなしで読み込みます。
  • 多層防御として、HTMLの応答にContent-Security-Policy(スクリプトと接続は自分自身だけ、画像は自分自身・data:・http・https)を付け、描画の不具合があっても外部のスクリプトを実行しません。分割したスクリプトもサーバー自身の/assets/から読み込みます。
  • MermaidはsecurityLevel: 'strict'で描画し、図の中のHTML・クリックの処理・スクリプトを無効にします。

ファイルとパス ​

  • 共有ファイルはOSの既定の権限で作り、秘密情報は持ちません。
  • 所有パスは、ProjectId(32桁の16進)・タスクID・EPIC-ID・画像IDを検証してから組み立てます。名前や本文の文字列をパスやシェルのコードにすることはありません。
  • 共有領域の絶対パスを返すのはworkspace --jsonだけで、Hook・スナップショット・エラーは相対パスだけを示します。フォルダの紐付けは端末ローカルのデータ領域にとどめ、EPIC.md・TASK.mdのパスはローカルのCLIとHookの出力にだけ現れ、リモート向けのJSONには出ません。
  • ボード内の文章は作業データで、Hookの設定・コマンドの引数・シェルのコードとして使うことはありません。Hookとwaitは読み取りだけです。セッショントークン・環境変数・設定の内容はスナップショットに現れません。

プロジェクトのドキュメント ​

ドキュメントのルートは、要求に含まれるパスでファイルを選んで読みます。ファイルをコンピュータのアプリで開くルートもあります。そのため、ファイルに触れる前にパスを確かめます(src/docs/paths.rs)。ボードがドキュメントに書き込むことはありません。

拒否するもの:

  • パスの文字列。 空のパス、絶対パス、空の要素(a//b)、ドットで始まる要素(..・.git)、バックスラッシュ、NULなどの制御文字、255バイトを超える要素、1,024バイトを超えるパス、17を超える要素数はdocs_path_invalidです。ファイルのルートでは、最後の要素が.mdか.mdxで終わることも求めます(docs_not_markdown)。
  • シンボリックリンク。 プロジェクトのdocs/の下で、パスを1要素ずつたどり、どの段階でもシンボリックリンク(ファイル自体とその上のフォルダ)を拒否します。そのうえでUnixではO_NOFOLLOWでファイルを開き、開いたものが通常のファイルであることを確かめます。ツリーはシンボリックリンクを含めないので、画面がリンクを示すことはありません。
  • 内容の種類。 ファイルのルートが読むのは、2 MiB以下のUTF-8のテキストだけです。画像のルートが返すのは、10 MiB以下のPNG・JPEG・WebP・GIF・AVIF・SVGだけです。Content-Typeは拡張子から決め、X-Content-Type-Options: nosniffを付けるので、HTMLのファイルがブラウザで実行できる形で返ることはありません。SVGはスクリプトを含められるため、応答にContent-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; sandboxも付け、画面では<img>で表示します。
  • 開く。 POST docs/openはコンピュータでプログラムを起動するため、ボードのほかの書き込みと同じガード(Host・Origin・Sec-Fetch-Site・トークン)を通ります。?path=があるときは、読み込みと同じ方法でパスを確かめ、存在する通常のファイルだけを、OSの開くコマンド(open・explorer・xdg-open)に一つの引数として渡します。シェルは通しません。開くアプリはOSが選び、利用者がファイルをダブルクリックしたときと同じ権限で動きます。
  • 書き込みなし。 ドキュメントのファイルを書く・作る・名前を変える・消すルートはありません。ファイルのルートへのPUTは拒否します(405)。
  • 公開範囲。 ルートはボードのルーターにだけあります。ngrokが転送するモニターの入口にはドキュメントのルートがないので、スマホからドキュメントを読んだり変えたりはできません。

ルートのフォルダは、サーバー自身のデータ領域とURLのProjectIdから決めます。要求の本文から決めることはありません。フォルダは利用者のものです。Agent Boardが書くのは、フォルダを作るときのREADMEだけです。

ドキュメントのMDX ​

.mdxにはコードを書けます。{1 + 2}のような式は、描画するときに実行されます。ボードのページはこれを実行しません。ボードのポリシーにunsafe-evalはなく、MDXのコンパイラもボードのスクリプトに入っていません。

本文は、別のページGET /docs-sandboxが実行します。このページはsandbox="allow-scripts"でallow-same-originなしのiframeに表示します。iframeは不透明なオリジンになるため、中のコードはボードのDOM・localStorage・セッショントークン・Cookieを読めず、ボードのAPIも呼べません。ページは固定で、トークンもボードのデータも持ちません。ボードはMDXの文字列をpostMessageで渡します。ページは、自分を埋め込んだウィンドウからのメッセージだけを受け付け、ボードはそのiframeからの返信だけを受け付けます。ページはリンクをたどりません。クリックを取り消して、hrefを{type: 'link', href}として送ります。ボードはこの文字列を信頼しません。開くのは、Docsのツリーにあるファイルか、noopener,noreferrerを付けて新しいタブで開くhttp・httpsのアドレスだけで、ほかのスキームは何もしません。

このページには専用のポリシー(connect-src 'none'、フレーム埋め込みなし、フォームなし、base URLなし)を付けます。'unsafe-eval'を持つのはこのページだけです。MDXのコンパイラが実行時にコードを組み立てるためです。ボード自身のポリシーに加えたのは、このページを埋め込むためのframe-src 'self'だけです。

守れるもの: 文書のコードから、トークンとファイルには手が届きません。コードは何も外へ送れません。守れないもの: iframeの中の無限ループや重い処理は止められず、利用者が別のファイルを選ぶかドキュメントを閉じるまで、ブラウザのタブが重くなることがあります。部品は名前で登録する方式で、登録してあるのは標準のHTML要素だけです。それ以外の部品は「未登録の部品」と表示します。モニターにはこのルートがないので、スマホからこのページは開けません。

プロジェクトの境界 ​

  • 毎回対象を指定することと不変なProjectIdは、協力的なエージェントがプロジェクト・タスク番号・名前を取り違えるのを防ぎます。プロジェクトごとの認証や認可ではありません。
  • 次タスク待ちのセレクターは機械的な絞り込みで、承認や認可の判定ではありません。対象タスク一件を返しますが、占有せず、複数のリスナーが同じタスクを取得することも防ぎません。作業前に最新のEPIC.mdとTASK.mdを読み、既存の承認境界を守って状態を再確認します。共有ファイルは同じOSユーザーで動くプログラムから書き込めます。
  • URLのプロジェクト名やIDを、身元や権限として扱うことはありません。プロジェクトを管理するHTTP APIもありません。
  • ls --all-projectsは読み取り専用で、全プロジェクトのタスクと履歴を返します。すべて同じOSユーザーで動くので、読む範囲を狭めません。書き込み・Hook・waitは、これまでどおり明示した一つのプロジェクトだけを対象にします。
  • Hookはcwdに紐付いたプロジェクトの進行表だけを渡し、全プロジェクトを暗黙に選ぶことはありません。テンプレートと汎用のスキルは、プロジェクト名や絶対パスを埋め込みません。
  • パスのProjectIdや、モニターの?project=は、読む対象またはストリームが運ぶ対象を選ぶだけで、アクセス権ではありません。モニターにはプロジェクトごとの権限がありません。合言葉を通過したsessionは、PCにあるすべてのプロジェクトのSummaryとDetailを読めます。アドレスのプロジェクトは、ページが表示するものを決めるだけです。

エージェントのアクセス設定 ​

setupが加えるのは、解決したAgent Boardのデータ領域だけで、ホームディレクトリ全体やほかのフォルダは加えません。これはエージェントに利用者のすべてのボードのデータへのアクセスを与えるので、エージェントのユーザー設定に適用します。

  • 既存の許可・拒否・確認の規則、許可モード、モデル、Hook、ネットワークの設定を保ちます。フルアクセス・bypassPermissions・Bashの包括的な許可・管理ポリシーの緩和は加えません。現在の方式でフォルダを加えられなければ、成功として報告しません。
  • 退避や一時ファイルにも同じ機微な設定が含まれうるので、既存のアクセスの制限は広げません。
  • プロジェクト・アプリ・組織の設定がユーザー設定より優先されることがあるので、ファイルを更新してもアクセスは保証されません。利用者はセッションで確かめます。
  • 解決した絶対パスは利用者のローカルの設定にだけ保存し、共有のテンプレートやプロジェクトの文書には書きません。

防げないこと ​

以下は、このセキュリティモデルで保護の対象外としている範囲です。

  • ngrokとそのアカウントの所有者。 トンネルはエンドツーエンドで暗号化されません。ngrokがHTTPSを終端するため通信の内容を見ることができ、ngrokアカウントの管理者は、アドレスの提供方法を変更できます。
  • モニターのループバックのポートへ直接HTTPを送るローカルのプロセス。 モニターの入口は127.0.0.1のランダムなポートで待ち受けており、要求がngrok経由で届いたことを証明するものはありません。PC上のプログラムがHost: <公開アドレス>をそのポートへ送れば、Hostの確認は通ります。そこから先に進む手段は合言葉とそのsessionだけなので、合言葉がなければボードのデータは読めません。ただし、このプログラムは当て推量でき(起動ごとに5回)、その5回を使い切ることで、接続を開始し直すまでスマホを締め出せます。PCを使う利用者は信頼する前提です。同じプログラムは共有ファイルを直接読めるため、この経路への追加の防御は設けません。Webサイトはこれを行えません。ブラウザは別のHostを送れないためです。
  • 入ることを許された端末、またはそのsessionを奪った人。 モニターを通じてPCの言語とテーマを変えられます。モニターが変えられる設定はこれだけで、タスク・プロジェクト・接続の設定は届きません。
  • アドレスと合言葉を知っている人。 合言葉を入力した人は、最長12時間、すべてのプロジェクトのモニターのデータを読め(プロジェクトごとの権限はありません)、未着手か進行中のタスクの現在の質問に回答でき、言語とテーマも変えられます。4桁の合言葉は10,000通りです。連続5回の間違いで止める制限と、起動ごとに変わる合言葉で当て推量を難しくしていますが、不可能にはなりません。アドレスを見つけた攻撃者には起動ごとに5回の当て推量ができ、一度当たれば開きます。PCの画面や起動行で合言葉を見た人は、それを使えます。アドレスを秘密にし、使わないときは接続を停止してください。
  • 同じOSユーザーで動くプログラムは、共有ファイルを直接読み書き・削除でき、CLIとWebのすべての確認を迂回できます。フォルダの書き込み権限はtask.jsonを物理的には守りません。
  • 下書きのTASK.mdの直接編集は止められず、READMEとエージェントのスキルで編集しないよう伝えます。タスクやエピックのフォルダの削除、task.json・epic.json・project.jsonの編集も同じです。
  • トークンを抜き出すローカルのプログラムからの偽装されたブラウザの要求は、書き込みの保護を通れます。
  • 外部画像の表示に伴う情報の送信。 外部の画像を表示するたびにそのサーバーと通信し、閲覧したこととIPアドレスが伝わります。画像のURLに、タスクの内容を載せて外部のサーバーに送ることもできます。悪意のある、または乗っ取られたエージェントが書いた場合も含みます。この制限を踏まえ、利用者自身が見慣れない画像URLに注意する必要があります。
  • Done・下書き・アーカイブ済みのタスクへのAIの変更の拒否は、協力的なエージェントの抑止で、直接のファイル編集やactorの偽装に対する認証ではありません。
  • プロジェクト横断の読み込みに基づく書き込みは、エージェント自身が名前を書くので、--projectの必須化では防げません。
  • 確認と開く操作の間に差し替えられたフォルダ。 パスの確認とファイルを開く操作は別の段階で、標準ライブラリだけを使っています。確認の後、開く前に、docs/の中のフォルダをシンボリックリンクに差し替えるプログラムがあると、サーバーはそのリンクの先を読み書きしてしまいます。最後のO_NOFOLLOWが守るのは、パスの最後の要素だけです。このようなプログラムはすでに同じ利用者として動いており、ファイルを直接変更できます。
  • ドキュメントのファイルを開く。 エディターで開くは、OSがそのファイルの種類に割り当てたアプリを、利用者の権限で起動します。docs/に誰かが置いたファイルも、ファイルマネージャでダブルクリックしたときと同じように開けます。
  • docs/フォルダそのもの。 確認はdocs/より下から始まります。データ領域、プロジェクトのフォルダ、docs/自体は、ほかの共有ファイルと同じく信頼します。
  • ライフサイクルのロックは、協力的なプロセスの間で二つ目のserveを防ぐだけで、何かを認可するものではありません。