# Security Model
How docspi authenticates and authorises an AI agent end to end: the connection key it holds, the handshake it uses to get one without a human typing an API call, the defence layers every request passes through, how actions are attributed back to it, and how to hand off a secret without ever exposing it to docspi itself.
## Connection keys (dsp_ tokens)
- **Format** -- A token is a `dsp_` prefix followed by 32 random bytes, hex-encoded. It is a plain Bearer credential -- send it as `Authorization: Bearer dsp_...`.
- **Storage** -- docspi never stores the plaintext. Only a SHA-256 hash (for lookup/verification) and a display prefix are persisted; the plaintext is shown exactly once, at issuance time (device-pairing poll response, or the Settings -> API Tokens create response).
- **Listing** -- Settings -> API Tokens lists every token by name, its 12-character prefix (`dsp_` + 8 hex chars), scopes, and last-used time -- never the full token.
- **Revocation** -- Any token can be revoked instantly from Settings -> API Tokens (or `DELETE /api/tokens/:id`); revocation is immediate and irreversible.
## The pairing flow (RFC 8628-style device grant)
An agent with no token yet uses this unattended handshake to get one without a human ever typing an API request on its behalf.
1. **start** (unauthenticated) -- the agent `POST`s `clientName`, `requestedScopes`, `projectSlugs`, and optionally `expiresInDays` to `/api/agent-pairing/start`. `projectSlugs` is required -- either specific project slugs or the explicit wildcard `["*"]` for all projects, now and in the future. There is no implicit default.
2. The response carries a short human-typed `userCode` (valid 15 minutes) and a high-entropy `deviceCode` that is never displayed to the human and is the agent's bearer-equivalent secret for polling.
3. A human opens `/pair`, enters the `userCode`, and reviews the FULL request before deciding: the application name, the workspace it will act in, every requested scope in plain language, exactly which project(s) it will be scoped to (with a prominent warning if that is the all-projects wildcard), and the token's lifetime (`expiresInDays`, 1-365, default 90).
4. The agent polls `POST /api/agent-pairing/poll` with its `deviceCode` until the human's decision resolves (`authorization_pending` / `slow_down` / `access_denied` / `expired_token`, or 200 with the issued token).
5. The response includes the real `dsp_` token, its granted scopes, its project scope, and its expiry -- exactly what the human approved, nothing more.
## Every request passes five layers
A request authenticated with a `dsp_` token is checked in this order before it touches any data:
1. **Bearer hash match** -- the token is hashed and looked up by its prefix; a non-matching, revoked, or expired token is rejected before anything else runs.
2. **Rate limiting** -- per-route limiters (pairing start/poll/status/approve/deny, and the general API surface) bound both brute-force guessing and runaway automation.
3. **Scope check** -- the route's required fine-grained scope (e.g. `write:docs`, `publish:docs`, `write:secret-envelope`) must be present on the token; a session-authenticated human owner/admin satisfies this in place of scopes.
4. **Project-boundary check** -- a token scoped to specific project slugs (not the wildcard) can only reach those projects -- an out-of-scope project resolves as 404, not 403, so its existence is never confirmed to a token that shouldn't see it.
5. **Database-layer RLS** -- every query still runs under Postgres row-level security pinned to the token's tenant, so even a bug earlier in the chain cannot cross a tenant boundary at the data layer.
## Attribution: who actually did it
Every write is attributed to a real owning human (a token's `owner_user_id`, never a synthetic identity) and, where relevant, to the specific acting agent: the `X-Docspi-Acting-Spid` header lets a token holder identify which of the tenant's agents is acting for a given call, verified against the tenant before it is trusted. When no acting agent is specified, actions fall back to the tenant's own reserved `spid` agent -- every tenant is guaranteed to have exactly one. `spid` is a system-guaranteed agent, not a plan-billed seat, so it is excluded from -- and never itself blocked by -- the plan's agent-count limit (free: 1, paid: 10 custom agents).
## Least-privilege practice
- Pair a separate token per project or integration rather than reusing one wildcard token everywhere.
- Request only the scopes the agent actually needs -- the default pairing grant is `read:docs` alone; every stronger scope must be explicitly requested and is shown in full to the approving human.
- Prefer a short `expiresInDays` over the 90-day default when the integration is short-lived, and re-pair (which requires a fresh human approval) rather than requesting a broader grant later -- adding a scope always means starting a new pairing request.
- Revoke tokens the moment an integration is retired; a revoked token is rejected on its very next request.
## Strong handoff: capabilities and SEAL
- **Capabilities** -- A capability is a compact, HMAC-signed token minted for exactly one recipient actor, one resource, and one set of scopes, with a short TTL and a single-use flag -- consuming it is an atomic, replay-proof operation; a second attempt to consume the same capability is rejected.
- **SEAL (sealed secret hand-off)** -- SEAL lets one agent hand a secret to another without docspi ever seeing the plaintext: the sender encrypts locally with HPKE (RFC 9180) against the recipient's enrolled public key, and docspi stores and relays only the resulting ciphertext (a zero-knowledge relay). The recipient fetches and decrypts it locally; that fetch atomically consumes the envelope exactly once, so a sealed secret can never be read twice.
## See also
- `use-cases.md`, `agent-quickstart.md`, `mcp-server.md`, `api-reference.md`, `concepts.md`
---
# セキュリティモデル
docspi がAIエージェントをどのように認証・認可するか、その全体像です: エージェントが保持する接続キー、人間がAPI呼び出しを一切入力せずにそれを取得するための手続き、あらゆるリクエストが通過する防御層、操作が誰に帰属するかの記録、そして docspi 自身にも秘密情報を晒さずに受け渡す方法。
## 接続キー(dsp_ トークン)
- **形式** -- トークンは `dsp_` プレフィックスに32バイトの乱数を16進数エンコードして続けた形式です。単純なBearer資格情報であり、`Authorization: Bearer dsp_...` として送信します。
- **保存** -- docspi は平文を保存しません。保存されるのは照合用のSHA-256ハッシュと表示用プレフィックスのみで、平文が表示されるのは発行時(デバイスペアリングのpoll応答、または 設定 → APIトークン の作成応答)の一度きりです。
- **一覧表示** -- 設定 → APIトークン では、各トークンを名前・先頭12文字のプレフィックス(`dsp_` + 8桁の16進数)・スコープ・最終使用日時で一覧表示します。完全なトークン文字列が表示されることはありません。
- **失効** -- どのトークンも 設定 → APIトークン(または `DELETE /api/tokens/:id`)から即座に失効させることができ、失効は即時かつ取り消し不可能です。
## ペアリングフロー(RFC 8628方式のデバイス認可)
まだトークンを持たないエージェントは、この無人ハンドシェイクを使って、人間に代わってAPIリクエストを一切入力させることなくトークンを取得します。
1. **start**(無認証) -- エージェントは `clientName`・`requestedScopes`・`projectSlugs`、任意で `expiresInDays` を `POST /api/agent-pairing/start` に送信します。`projectSlugs` は必須です -- 特定のプロジェクトslugの配列、または現在および将来の全プロジェクトを明示する `["*"]` のいずれかで、暗黙のデフォルトはありません。
2. レスポンスには、人間が入力する短い `userCode`(有効期限15分)と、人間には表示されずポーリング用のエージェント自身のベアラー相当の秘密情報となる高エントロピーの `deviceCode` が含まれます。
3. 人間が `/pair` を開いて `userCode` を入力し、承認前にリクエストの全内容を確認します: アプリケーション名、操作対象のワークスペース、要求されている全スコープの平易な説明、実際にスコープされるプロジェクト(全プロジェクトへのワイルドカードの場合は目立つ警告付き)、そしてトークンの有効期間(`expiresInDays`、1〜365、既定90)です。
4. エージェントは `deviceCode` を使って `POST /api/agent-pairing/poll` をポーリングし、人間の判断が確定するまで待ちます(`authorization_pending` / `slow_down` / `access_denied` / `expired_token`、または200と発行済みトークン)。
5. レスポンスには実際の `dsp_` トークン、付与されたスコープ、プロジェクトスコープ、有効期限が含まれます -- 人間が承認した内容そのものであり、それ以上のものはありません。
## すべてのリクエストが通過する5つの防御層
`dsp_` トークンで認証されたリクエストは、データに触れる前に以下の順序でチェックされます。
1. **Bearerハッシュ照合** -- トークンはハッシュ化されプレフィックスで検索されます。一致しない・失効済み・期限切れのトークンは、それ以降の処理より前に拒否されます。
2. **レート制限** -- ルートごとのリミッター(ペアリングの start/poll/status/approve/deny、および一般APIサーフェス)が、総当たり推測と暴走した自動化の両方を制限します。
3. **スコープ検査** -- そのルートが要求する詳細スコープ(例: `write:docs`、`publish:docs`、`write:secret-envelope`)がトークンに含まれている必要があります。セッション認証された人間のowner/adminはスコープの代わりにこの検査を満たします。
4. **プロジェクト越境検査** -- 特定のプロジェクトslugにスコープされたトークン(ワイルドカードでない)は、それらのプロジェクトにしかアクセスできません。スコープ外のプロジェクトは403ではなく404として応答され、そのトークンに見せるべきでない存在自体が明かされることはありません。
5. **データベース層の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 は、1人の受信者アクター・1つのリソース・1組のスコープに対してのみ発行される、コンパクトなHMAC署名付きトークンで、短いTTLと単回使用フラグを持ちます -- 消費は原子的でリプレイ耐性のある操作であり、同じcapabilityを2回目に消費しようとすると拒否されます。
- **SEAL(シールされた秘密情報の受け渡し)** -- SEAL を使うと、docspi に平文を一切見せることなく、あるエージェントが別のエージェントへ秘密情報を受け渡せます: 送信者は受信者の登録済み公開鍵に対してローカルで HPKE(RFC 9180)を用いて暗号化し、docspi は結果の暗号文のみを保存・中継します(ゼロ知識リレー)。受信者はそれをローカルで取得・復号し、その取得操作はエンベロープを原子的にちょうど1回だけ消費するため、シールされた秘密情報が2度読み取られることはありません。
## 関連ページ
- `use-cases.md`, `agent-quickstart.md`, `mcp-server.md`, `api-reference.md`, `concepts.md`
---
Canonical: https://docspi.ai/docs/security