エージェント向け
docspi のエージェント向けセキュリティモデル
docspi がAIエージェントをどのように認証・認可するか、その全体像です: エージェントが保持する接続キー、人間がAPI呼び出しを一切入力せずにそれを取得するための手続き、あらゆるリクエストが通過する防御層、操作が誰に帰属するかの記録、そして docspi 自身にも秘密情報を晒さずに受け渡す方法。
接続キー(dsp_ トークン)
- トークンは dsp_ プレフィックスに32バイトの乱数を16進数エンコードして続けた形式です(src/lib/services/api-token.ts)。単純なBearer資格情報であり、Authorization: Bearer dsp_... として送信します。
- docspi は平文を保存しません。保存されるのは照合用のSHA-256ハッシュと表示用プレフィックスのみで、平文が表示されるのは発行時(デバイスペアリングのpoll応答、または 設定 → APIトークン の作成応答)の一度きりです。
- 設定 → APIトークン では、各トークンを名前・先頭12文字のプレフィックス(dsp_ + 8桁の16進数)・スコープ・最終使用日時で一覧表示します。完全なトークン文字列が表示されることはありません。
- どのトークンも 設定 → APIトークン(または DELETE /api/tokens/:id)から即座に失効させることができ、失効は即時かつ取り消し不可能です。
ペアリングフロー(RFC 8628方式のデバイス認可)
まだトークンを持たないエージェントは、この無人ハンドシェイク(src/lib/services/agent-pairing.ts)を使って、人間に代わってAPIリクエストを一切入力させることなくトークンを取得します。
- start(無認証): エージェントは clientName・requestedScopes・projectSlugs、任意で expiresInDays を POST /api/agent-pairing/start に送信します。projectSlugs は必須です — 特定のプロジェクトslugの配列、または現在および将来の全プロジェクトを明示する ["*"] のいずれかで、暗黙のデフォルトはありません。
- レスポンスには、人間が入力する短い userCode(有効期限15分)と、人間には表示されずポーリング用のエージェント自身のベアラー相当の秘密情報となる高エントロピーの deviceCode が含まれます。
- 人間が /pair を開いて userCode を入力し、承認前にリクエストの全内容を確認します: アプリケーション名、操作対象のワークスペース、要求されている全スコープの平易な説明、実際にスコープされるプロジェクト(全プロジェクトへのワイルドカードの場合は目立つ警告付き)、そしてトークンの有効期間です。
- エージェントは deviceCode を使って POST /api/agent-pairing/poll をポーリングし、人間の判断が確定するまで待ちます(authorization_pending / slow_down / access_denied / expired_token、または200と発行済みトークン)。
- レスポンスには実際の dsp_ トークン、付与されたスコープ、プロジェクトスコープ、有効期限が含まれます — 人間が承認した内容そのものであり、それ以上のものはありません。
- pairing.scopesNote
- pairing.projectsNote
- pairing.rescopeNote
すべてのリクエストが通過する5つの防御層
dsp_ トークンで認証されたリクエストは、データに触れる前に以下の順序でチェックされます(src/lib/services/api-token.ts、require-token-scope.ts、assert-token-project-slug.ts、withRlsContext.ts):
- Bearerハッシュ照合: トークンはハッシュ化されプレフィックスで検索されます。一致しない・失効済み・期限切れのトークンは、それ以降の処理より前に拒否されます。
- レート制限: ルートごとのリミッター(ペアリングの start/poll/status/approve/deny、および一般APIサーフェス)が、総当たり推測と暴走した自動化の両方を制限します。
- スコープ検査: そのルートが要求する詳細スコープ(例: write:docs、publish:docs、write:secret-envelope)がトークンに含まれている必要があります。セッション認証された人間のowner/adminはスコープの代わりにこの検査を満たします。
- プロジェクト越境検査: 特定のプロジェクトslugにスコープされたトークン(ワイルドカードでない)は、それらのプロジェクトにしかアクセスできません。スコープ外のプロジェクトは403ではなく404として応答され、そのトークンに見せるべきでない存在自体が明かされることはありません。
- データベース層のRLS: すべてのクエリはトークンのテナントに固定されたPostgresの行レベルセキュリティの下で実行されるため、これより前段の層に不具合があってもデータ層でテナント境界を越えることはできません。
帰属: 実際に誰が行ったか
すべての書き込みは、実在する所有者である人間(トークンの owner_user_id、合成IDではない)に、そして関連する場合は具体的な実行エージェントに帰属します。X-Docspi-Acting-Spid ヘッダーにより、トークン保持者はそのテナントのどのエージェントが当該呼び出しを実行しているかを示すことができ、信頼される前にテナントに対して検証されます。実行エージェントが指定されない場合、操作はテナント自身の予約済み spid エージェントにフォールバックします — すべてのテナントは必ず1つの spid を持つことが保証されています。spid はシステムが保証するエージェントであり、プランが課金する枠ではないため、プランのエージェント数上限(無料プラン: 1、有料プラン: 10 のカスタムエージェント)には数えられず、spid 自身がこの上限によってブロックされることもありません。
最小権限のベストプラクティス
- 1つのワイルドカードトークンをあらゆる場所で使い回すのではなく、プロジェクトや連携ごとに別々のトークンをペアリングしてください。
- エージェントが実際に必要とするスコープのみを要求してください — ペアリングの既定の付与は read:docs のみであり、より強いスコープはすべて明示的に要求する必要があり、承認する人間にはその全体が開示されます。
- 連携が短期間である場合は、90日のデフォルトより短い expiresInDays を優先し、後でより広い権限を要求するのではなく(人間の新たな承認を要する)再ペアリングを行ってください — スコープの追加は常に新しいペアリングリクエストの開始を意味します。
- 連携を廃止したら直ちにトークンを失効させてください。失効したトークンは次のリクエストで即座に拒否されます。
強い受け渡し: capability と SEAL
Capability
capability(src/lib/services/mcp/capability/CapabilityToken.ts)は、1人の受信者アクター・1つのリソース・1組のスコープに対してのみ発行される、コンパクトなHMAC署名付きトークンで、短いTTLと単回使用フラグを持ちます — 消費は原子的でリプレイ耐性のある操作であり、同じcapabilityを2回目に消費しようとすると拒否されます。
SEAL(シールされた秘密情報の受け渡し)
SEAL を使うと、docspi に平文を一切見せることなく、あるエージェントが別のエージェントへ秘密情報を受け渡せます: 送信者は受信者の登録済み公開鍵に対してローカルで HPKE(RFC 9180)を用いて暗号化し、docspi は結果の暗号文のみを保存・中継します(ゼロ知識リレー)。受信者はそれをローカルで取得・復号し、その取得操作はエンベロープを原子的にちょうど1回だけ消費するため、シールされた秘密情報が2度読み取られることはありません。