dcp_ai.fastapi — FastAPI のミドルウェア¶
英語版の参考訳です(28/10/2026 更新)。内容が異なる場合は英語版が優先されます。識別子、コマンド、コードブロックは翻訳せずにそのまま掲載しています。
FastAPI 向けの DCP の検証ミドルウェアです。Signed Bundle を自動で検証し、保護するルートのための依存性注入を提供します。
サポートする DCP の仕様¶
DCP-01 から DCP-09。部分的なもので、以下に述べるバンドルの検証とアドミッション、そして DCP-05 と DCP-09 のいくつかのフィールド(lifecycle_state、mandate_id)です。これらの仕様の完全な実装ではありません。
インストール¶
PyPI 公開済み(26/10/2026)。 dcp-ai 2.10.0 はここで説明する実装を含みます。2.9.0 から更新し、環境またはロックファイルを更新してください。Python のリリースノートを参照してください。
2.9.0 は V2 署名を検証しないため、V2 検証には使用しないでください。2.10.0 は修正済み検証器、プリンシパルの信頼、strict-v1、本文の予算、信頼・失効ソースの監督、切断処理を含みます。受け入れ判定は行為を認可しません。
ソース開発の場合:
git clone https://github.com/dcp-ai-protocol/dcp-ai.git
pip install -e "/path/to/dcp-ai/sdks/python[fastapi]"
クイックスタート¶
from fastapi import FastAPI, Depends
from dcp_ai.fastapi import DCPVerifyMiddleware, require_dcp, DCPAgentContext
app = FastAPI()
# Add verification middleware
app.add_middleware(DCPVerifyMiddleware, require_bundle=False)
# Public route (no bundle required)
@app.get("/health")
async def health():
return {"ok": True}
# Protected route (requires a verified bundle)
@app.post("/api/action")
async def agent_action(agent: DCPAgentContext = Depends(require_dcp)):
return {
"agent_id": agent.agent_id,
"human_id": agent.human_id,
"capabilities": agent.capabilities,
"risk_tier": agent.risk_tier,
}
バンドルを送る¶
ハイブリッドの V2 のバンドルは約 33 KB で、ほとんどのサーバーやプロキシのヘッダーの上限を超えるため、JSON のボディで送ってください。ヘッダーは、経路上のすべての上限に収まるバンドルのためのものです。以下のボディを使うリクエストはテンプレートで、{...} は実際の署名付きバンドルを表します。
# Via header
curl -X POST http://localhost:8000/api/action \
-H "Content-Type: application/json" \
-H "x-dcp-bundle: $(cat signed_bundle.json)" \
-d '{}'
# Via JSON body
curl -X POST http://localhost:8000/api/action \
-H "Content-Type: application/json" \
-d '{"signed_bundle": {...}}'
API リファレンス¶
DCPVerifyMiddleware¶
リクエストを捕捉して DCP のバンドルを検証する Starlette/FastAPI のミドルウェアです。
app.add_middleware(
DCPVerifyMiddleware,
require_bundle=False, # If True, rejects requests without a bundle
header_name="x-dcp-bundle", # Header where the bundle is sent
verifier_policy={"default_mode": "hybrid_required"}, # Optional, over DEFAULT_VERIFIER_POLICY
)
コンストラクターのパラメーター¶
| パラメーター | 型 | 既定値 | 説明 |
|---|---|---|---|
app |
Any |
(必須) | ASGI のアプリケーション |
require_bundle |
bool |
False |
バンドルのないリクエストを 403 で拒否する |
header_name |
str |
"x-dcp-bundle" |
HTTP ヘッダーの名前 |
verifier_policy |
dict \| VerifierPolicy \| None |
None |
検証器のポリシー。dict は DEFAULT_VERIFIER_POLICY の上に VerifierPolicy のフィールドを設定する。risk_overrides なしの default_mode はすべてのリスクティアに適用され、risk_overrides の dict はほかより優先してそのティアを設定し、risk_overrides=None は既定のティアを保つ。VerifierPolicy はもう一度検証され、その risk_overrides が含まないリスクティア(None ならすべてのティア)は、V2 の検証器が解決するのと同じく、その default_mode になる。未知のフィールド、リスクティア、モードは、ミドルウェアの構築時に例外を投げる。 |
dcp_version |
str |
"2.0" |
保存されるが強制されない。V1 のバンドルを受け入れるかどうかは verifier_policy 次第で(下記参照)、require_dcp_v2 はルートを V2 に限定する。 |
trusted_principals |
dict \| callable \| None |
None |
キーワード専用。あなたが信頼するプリンシパルとその鍵。信頼セット(schemas/v2/principal_trust_set.schema.json、ミドルウェアの構築時に検証)、またはリクエストのたびに呼ばれる (tenant) -> trust set(同期または非同期)。これを使うと、アドミッションを通った V2 のバンドルだけがルートに届き、require_bundle=True が必要になる(信頼するプリンシパルと strict-v1 を参照)。 |
profile |
dict \| None |
None |
キーワード専用。strict-v1 のプロファイル(schemas/v2/admission_profile.schema.json)で、ミドルウェアの構築時に読み込まれる。trusted_principals と revocation_source が必要で、検証器のポリシーは自ら設定する。 |
revocation_source |
callable \| None |
None |
キーワード専用、profile と併用。(subjects, tenant) -> revocation snapshot \| None、同期(ワーカースレッドで実行)または非同期で、検証済みのバンドルのアドミッションのたびに呼ばれる。deny_list_revocation_source(url) で作れる。 |
revocation_timeout |
float |
5.0 |
キーワード専用。失効のソースが応答するまでの秒数。{"revocation": True} のもとでは監督された期限で、0 より大きく 2147483.647 以下。 |
tenant |
callable \| None |
None |
キーワード専用。(request) -> str、同期または非同期で、あなた自身のコード(たとえば、認証が request.state に設定した呼び出し元のテナント)。信頼と失効の呼び出し可能オブジェクトに渡される。既定では常に ""。 |
clock |
callable \| None |
None |
キーワード専用、テスト用:エポックからのミリ秒単位の時刻。既定はシステムの時計。 |
source_supervisor |
SourceSupervisor \| None |
None |
キーワード専用、supervise と併用。信頼のソースの監督を参照。 |
supervise |
dict \| None |
None |
キーワード専用:非同期の trusted_principals には {"trust_timeout": seconds}(0 より大きく 2147483.647 以下)、非同期の revocation_source には revocation_timeout のもとで {"revocation": True}、またはその両方。失効のソースの監督を参照。 |
cancel_on_disconnect |
bool |
False |
キーワード専用。アドミッションの最中にクライアントが本当に去ったとき、その監督されたソースの呼び出しを取り消す。その応答は、サーバーが届けない空の 499 で終わる。max_body_bytes が必要。去るクライアントを参照。 |
max_body_bytes |
int \| None |
None |
キーワード専用。正の整数(bool、float、文字列、0、負の数は不可)で、ミドルウェアが受け付けるリクエストのボディのバイト数。Starlette 0.28.0 以降が必要。既定では以前と同じく予算なし。ボディの予算を参照。 |
動作¶
max_body_bytesを使うと、まず予算の範囲内でボディを読みます(ボディの予算を参照)。- ヘッダーまたは JSON のボディ(
signed_bundle)から署名付きバンドルを取り出します。JSON でないヘッダー、Python の 4300 桁の上限を超える整数を含むヘッダー、入れ子が深すぎるヘッダーは、バンドルがないものとして扱われ(require_bundle=Trueでは403)、決して500にはなりません。 - Python SDK で検証します。
- V2(
bundle.dcp_bundle_version: "2.0"):Ed25519 / ML-DSA-65 / SLH-DSA-192f のレジストリと上記のポリシーで、dcp_ai.v2のawait verify_signed_bundle_v2(signed_bundle, registry, policy)を使います。ペイロードのハッシュとマニフェスト、セッションの結び付けと期限、バンドルと各アーティファクトの複合署名、検証モード(パスポートのリスクティアに対するポリシーのモードと、意図のセキュリティティアのうち厳しい方)、監査のハッシュチェーン(dcp-ai2.10.0 では SHA-256 のものだけ。2.10.1 以降は、存在する V2-11 の二次の SHA3-256 のフィールドも確認します)、鍵の有効性を確認します。composite_sig_validは、バンドルの署名についての検証器の結果です。 - V1(
bundle.responsible_principal_record.dcp_version: "1.0"):V1 のバンドルにはポスト量子の署名がないため、VERIFICATION.md の V2-10 がクラシカルのみのバンドルを受け入れる場合にだけverify_signed_bundle()を使います。つまり、allow_v1_bundlesが真で、ポリシーが ed25519 を受け入れ(accepted_classical_algsにあり、advisory_rejected_algsにない)、オーディエンスの結び付け、セッションの期限、セッションの最大期間を求めず(V1 のバンドルにはintended_verifierもセッションの期間もありません)、バンドルのresponsible_principal_record、agent_passport、intent、policy_decisionがオブジェクトで、そのaudit_entriesがオブジェクトのリストであり、パスポートのagent_idと RPR のhuman_idが文字列で(パスポートのpublic_keyとstatusは、あれば文字列、そのcapabilitiesは文字列のリスト。エージェントのコンテキストに入るため)、V1 のパスポートのrisk_tierに対するポリシーのモード(それがなければdefault_mode)と、V1 の意図とポリシー判断のセキュリティティアのモード(V1 のrisk_scoreは 0 から 1 で、1000 倍される。ない場合はelevatedとして扱う)のうち厳しい方がclassical_onlyまたはhybrid_preferredの場合です。DEFAULT_VERIFIER_POLICYでは、routine または standard の V1 の意図は通り、支払い、機密データ、高い推定影響、0.5 以上のリスクスコアは拒否されます。 - バージョンを判別できないバンドルは拒否されます。
- 有効なら:検証器の
warningsとともに、DCPAgentContextをrequest.state.dcp_agentに保存します - 失敗したら:
403と検証器のerrors(V2 ではwarningsも)で応答します。それぞれ最大 20 件、各最大 500 文字で、バンドルが提供したテキストの制御文字と書式文字はエスケープされます。コンテキストのwarningsも同じように制限されます。 - バンドルがなく
require_bundle=Falseなら:検証せずに続行します
ボディの予算¶
max_body_bytes はオプトインです。これがなければ、ミドルウェアは以前と同じく、独自の上限なしでボディを読みます。これを使うと、ミドルウェアはほかの何よりも先に(バンドルのヘッダー、JSON の解析、テナントと信頼セット、検証、失効、ルートより前に)リクエストのボディ全体を読みます。バンドルがヘッダーで届く場合や require_bundle=False の場合も同様です。
- 予算を超える
Content-Lengthは、ボディのバイトを受け取る前に413{"verified": false, "errors": ["Request body exceeds max_body_bytes"]}を受け取ります。 - 実際に受け取ったバイトも数えるため、
Content-Lengthのないリクエスト、チャンク形式のボディ、小さすぎるContent-Lengthは、予算を超えた最初の ASGI のメッセージで同じ413を受け取り(そのメッセージはすでに届いています)、その後は何も受け取りません。 - ASCII の数字の単一の並びでない
Content-Length(符号、小数点、空白、リスト)や 2 回現れるものは400["Invalid Content-Length"]を受け取り、ボディの送信中に切断したクライアントは400["Request body could not be read"]を受け取ります。どちらもルートには届きません。 - 予算内のボディは、空や不正な形式のものも含め、このオプションがない場合とまったく同じに扱われ、ルートはいつもどおりそれを読みます(
request.body()、request.json()、request.stream())。Starlette のBaseHTTPMiddlewareは、Starlette 0.28.0 以降、ミドルウェアで読んだボディを引き渡します。 - そのため、このオプションには Starlette 0.28.0 以降が必要です。それより古い Starlette、またはバージョンを読めない Starlette では、
max_body_bytesはミドルウェアの構築時にAdmissionConfigError(CONFIG_INVALID_VALUE)を投げます。FastAPI/Starlette を更新するか、設定しないでおいてください。このオプションがなければ何も確認されず、パッケージ自体の最低バージョンも変わりません。
これは、あなたのデプロイが設定する、受け取るボディのバイト数の上限であり、DCP のプロトコルの一部ではありません。展開(ASGI サーバーが届けるバイトを数えます)、JSON の深さ、時間、読み取り後の切断は制限しません。サーバー自身の上限とあわせて設定してください。
信頼のソースの監督¶
これらのオプションがなければ、非同期または同期の trusted_principals は以前と同じく期限なしで実行されます。これらを使うと、非同期のものに期限と容量の枠が与えられます。
from dcp_ai.fastapi import DCPVerifyMiddleware, SourceSupervisor
supervisor = SourceSupervisor(max_in_flight=64, max_in_flight_per_tenant=8) # one per event loop / worker
async def trust_set_for(tenant):
return await load_trust_set(tenant)
app.add_middleware(DCPVerifyMiddleware, require_bundle=True, trusted_principals=trust_set_for,
source_supervisor=supervisor, supervise={"trust_timeout": 2.0})
- 期限。 ソースは監督者が所有する Task として実行されます。
trust_timeoutの後に届いた信頼セットは使われず、それは届いたときと使われるときにloop.time()で測られます。リクエストは失敗したソースと同じ503を受け取り、Task には取り消しが要求されます。協力を待つものは何もありません。 - 容量。 同時に実行されるソースの呼び出しは最大
max_in_flightで、キューはありません。max_in_flight_per_tenantは 1 つのテナントを制限し、これがなければ 1 つのテナントが容量全体を使えてしまいます。空きの枠がない呼び出しは、ソースを呼ばずにただちに503を受け取ります。枠が解放されるのは、ソースの Task が本当に終わったときだけで、リクエストに応答したときではありません。取り消しを無視するソースは枠を持ち続けます。このようにしてすべての枠が埋まると、それらのソースが終わるかプロセスが再起動するまで、すべてのリクエストが503を受け取ります。 - 失敗。 自ら取り消すソース、取り消された呼び出し元(そのソースは終わるまで枠を持ち続けます)、無効な信頼セットは、すべてフェイルクローズします。これらのオプションとともに静的または同期の
trusted_principalsを使うとAdmissionConfigErrorを投げます。同期のものはイベントループの上で実行され、そこではどの期限も守られないからです。 - 範囲。 容量は
SourceSupervisorのオブジェクトごとで、それを渡したミドルウェアが共有します。最初に使ったイベントループに結び付くため、ワーカープロセスやループごとに個別のものを与えてください。ほかのループやスレッドからの呼び出しはフェイルクローズし、ほかのループからのclose()は何も変えずにRuntimeErrorを投げます。 await supervisor.close(drain_timeout=0)は新しい呼び出しを拒否し、保留中の呼び出しに503で応答し、すべてのソースの Task に取り消しを要求し、最大drain_timeout秒待ちます。{"in_flight": n}を返し、それらの枠は埋まったままで、再開はできません。supervisor.stats()はテナントを含まないカウンターを返します:open、in_flight、tenants、timeouts、saturated、closed、wrong_loop、late_results、late_errors。2**53 - 1 で止まります。- しないこと。 取り消しを無視する処理を止めることも、ソースが自ら開始する処理に上限を設けることも、CPU を隔離することもしません。イベントループをブロックする
async defはすべての期限を遅らせます。シャットダウン中のサーバーやasyncio.runは、取り消しを無視するソースの Task を待ち続けることがあります。{"revocation": True}がなければ、失効は以前と同じくrevocation_timeoutを使います。信頼のソースへの問い合わせは、以前と同じく、バンドルの検証の前に行われます。
失効のソースの監督¶
strict-v1 のプロファイルでは、supervise={"revocation": True} が、非同期の revocation_source を、同じ種類の監督者の Task として、revocation_timeout を期限に実行します。
app.add_middleware(DCPVerifyMiddleware, require_bundle=True, trusted_principals=TRUST_SET, # static, or async with trust_timeout
profile={"profile": "strict-v1", "verifier_id": "verifier:acme-api"},
revocation_source=deny_list_revocation_source("https://revocations.example.com/.well-known/dcp-revocations.json"),
revocation_timeout=2.0, source_supervisor=supervisor,
supervise={"revocation": True}) # or {"trust_timeout": 2.0, "revocation": True}
- オプトイン。
superviseはtrust_timeout、"revocation": True、またはその両方を受け取ります。"revocation": Trueには非同期のrevocation_source(async def、非同期の__call__を持つオブジェクト、またはfunctools.partialに包まれたそのいずれか)が必要で、静的な信頼セットでも動きます。同期のものはAdmissionConfigErrorを投げます。監督者が所有しないワーカースレッドで実行されることになるからです。このオプションがなければ何も変わらず、同期のソースは引き続きasyncio.wait_forのもとでワーカースレッドで実行されます。 - 同じ応答。 それぞれの答えは監督なしの場合と同じ応答になります。スナップショットは以前と同じく評価されます。
None、例外、時間切れ、自ら取り消すソース、空きの枠がないこと、閉じた監督者、ほかのループに結び付いた監督者はcodes: ["REVOCATION_UNAVAILABLE"]とともに403になり、決して失効していないとはされません。codeがREVOCATION_INVALIDの例外は、期限の前に届いた場合にだけREVOCATION_INVALIDになります。 - 変わる場合。 このオプションがないと、
asyncio.wait_forは自身の取り消しを抑え込むソースを待ち、そのとき返されたスナップショットを使います。このオプションを使うと、リクエストは期限の時点でREVOCATION_UNAVAILABLEを受け取り、その遅れたスナップショットは決して使われません。 - 1 つの容量。 信頼と失効の呼び出しは同じ監督者の枠を使い、それぞれ Task が本当に終わるまで保持されます。
deny_list_revocation_sourceの制限。 既定ではasyncio.to_threadでダウンロードします。その Task を取り消すと Task は終わって枠も解放されますが、ワーカースレッドは解放されません。そのスレッドは urllib が戻るまで接続を保持します。監督者はそれらのスレッドを制限せず、その I/O も取り消しません。それらを制限するには、ソースにDenyListWorkersの予算を与えてください(拒否リストのダウンロードの制限を参照)。
去るクライアント¶
cancel_on_disconnect=True を使うと、リクエストのアドミッションの最中に去ったクライアントは、そのリクエストの処理を止めます。
- どう検出するか。 ミドルウェアはまずボディ全体を読みます。このオプションには
max_body_bytes、したがって Starlette 0.28 以降が必要です。その後、リクエストのアドミッションの間、保留中のreceive()を 1 つ保持します。ボディを読み終えた後、サーバーがそこに送れるのはhttp.disconnectだけです。 - クライアントが去ったとき:
- アドミッションは取り消されます。その監督された呼び出しも取り消されます。
SourceSupervisorは Task を待たずに取り消し、枠は Task が本当に終わるまで埋まったままです。DenyListWorkersのスレッドも同じように枠を保持します。 - どのルートも実行されず、そのアドミッションのものは何も
request.stateに公開されません。そのdcp_agentはクライアントがまだいるとわかったときにだけ設定され、あなた自身のコードが設定した値は変更されません。 - そのリクエストの ASGI の応答は、
Content-Length: 0とCache-Control: no-storeを伴う空の499(nginx の「client closed request」。登録されたものではなく、DCP のステータスでもありません)で終わります。これは DCP の応答がその外側の層に送る 2 つの ASGI のメッセージで、クライアントへの答えではありません。http.disconnectを受け取った後にだけ送られ、残っているクライアントには決して送られず、応答を待つ DCP の外側のBaseHTTPMiddlewareが正常に終われるようにします。そのため、DCP はミドルウェアのどの順序でも動きます。 - ネットワークに何が届くかはサーバー次第です。Uvicorn は、もう存在しない接続については何も書かず、何もログに残さず、
500も送りません(uvicorn 0.54 と h11 で測定)。ASGI の仕様では、ほかのサーバーは、閉じた接続で応答が送られたときにOSErrorのサブクラスを投げることがあります。 - DCP の外側の層はその 499 を見ます。ログの層は、成功ではなく、クライアントがリクエストを閉じたことを記録します。応答を変換する層や ASGI に違反する層はそれを変えることがありますが、それはこの保証の範囲外です。どの層の本当のエラーも引き続き伝播します。
- ボディの途中で去ったクライアントは、このオプションなしで送られる 400 ではなく、同じ 499 の終わり方になります。
- ルートから何も奪いません。 ルートは同じボディを読み、その保留中の
receive()は次に読むもの(ルート、request.is_disconnected()、Starlette)に引き渡され、二度読まれることも失われることもありません。解釈されるのはアドミッションの最中に起きる読み取りだけで、完全な応答の後にサーバーが送るhttp.disconnectは、クライアントが去ったものとは見なされません。 - 要件。 ミドルウェアの構築時に確認されます。
max_body_bytes- 監督されたすべてのソースの呼び出し可能オブジェクト(呼び出し可能な
trusted_principalsにはtrust_timeout、revocation_sourceには{"revocation": True}) - 同期の
tenantの呼び出し可能オブジェクト。非同期のものは、もう去ったリクエストを待たせ続けることがあるからです。
そうでなければ AdmissionConfigError(CONFIG_PROFILE_CONTRADICTION)です。このオプションを使うと、awaitable を返す tenant の呼び出し可能オブジェクトは、待たれることなく、解決できないテナントの 503 を受け取ります。このオプションがなければ何も変わりません。
- しないこと。 バンドルの検証やテナントの呼び出し可能オブジェクトは中断せず、取り消しを無視するソースも止めません。飛ばすのは、切断を見た後に始まる段階だけです。外側の層の後ろでは、切断が届くのはループの数回後になるため、ソースがすでに始まっていることがあります(そのときは取り消され、終わるまで枠を保持します)。asyncio でのみ動きます。接続が失われたときに http.disconnect を送るサーバーが必要です。uvicorn は送りますが、プロセス内の httpx のトランスポートは応答が完了したときにしか送りません。
「検証済み」の意味¶
検証済みのバンドルは、内部で一貫しており、それ自身が列挙する鍵(パスポートの鍵と RPR の結び付けの鍵)で署名されています。プリンシパルが誰かは証明しません。誰でも鍵を作り、任意の human_id を名乗る RPR を作れるからです。そのためには trusted_principals を設定してください(次の節)。検証済みのバンドルは、セッションが切れるまで再送されることもあります。verifier_policy={"require_audience_binding": True, "verifier_id": "my-service"}(バンドルの manifest.intended_verifier がこの検証器を指していなければなりません)、require_session_expiry、max_session_duration_seconds がそれを制限し、strict-v1 のプロファイルも同様です。どれもバンドルを一回限りにするものではありません。それが必要なら、使用済みの nonce の記録を自分で保持してください。
信頼するプリンシパルと strict-v1¶
trusted_principals を使うと、リクエストがルートに届くのは、そのバンドルがアドミッションを通った場合だけです(docs/ADMISSION.md、spec/profiles/admission)。つまり、プリンシパルの署名(RPR とバンドルのマニフェスト、クラシカルとポスト量子)が、あなたの信頼セットがその human_id について挙げる、有効で有効期間内の鍵で作られた V2 のバンドルです。profile を使うと、strict-v1 の要件も適用されます。すべてのリスクティアでのハイブリッドな署名、あなたの verifier_id と等しい署名された intended_verifier、最大 300 秒の署名されたセッション、そしてエージェントとすべての署名鍵が失効していないと報告する、最大 300 秒前の失効のスナップショットです。
import os
from dcp_ai.fastapi import DCPVerifyMiddleware, deny_list_revocation_source
app.add_middleware(
DCPVerifyMiddleware,
require_bundle=True,
trusted_principals=trust_set, # or a callable: lambda tenant: trust_sets[tenant]
profile={"profile": "strict-v1", "verifier_id": "api.acme.example"},
revocation_source=deny_list_revocation_source(
"https://revocation.acme.example/.well-known/dcp-revocations.json",
headers={"Authorization": f"Bearer {os.environ['REVOCATION_TOKEN']}"},
),
tenant=lambda request: request.state.tenant, # set by your own authentication
)
- 拒否されたリクエストは
{"verified", "admitted": false, "principal_trust", "profile", "profile_compliance", "codes", "errors"}(codesは整列され、errorsは"CODE: message"の形で、制限されエスケープされます)とともに403を受け取り、ルートには届きません。V1 のバンドルは拒否されます。アンカーするプリンシパルの署名を持たないからです。テナントの呼び出し可能オブジェクトや信頼セットのソースが失敗するか、無効なものを返した場合、リクエストは503を受け取り、やはりルートには届きません。 - アドミッションを通ったリクエストは、上と同じ
request.state.dcp_agentを受け取り、そこにはadmitted=True、principal_trust、profile、profile_compliance、recorded_decision(バンドルのapprove、escalate、block)、trust_set_versionがあります。action_authorizedは常にFalseです。アドミッションが示すのは、証拠が本物で信頼するプリンシパルから来たことであり、行動を実行してよいことではありません。 - リクエストのたびに改めて評価されます。信頼の呼び出し可能オブジェクトと失効のソースはアドミッションのたびに呼ばれるため、信頼セットから外されたプリンシパル、そこで
revokedとされた鍵、ソースが挙げる失効は、次のリクエストで同じバンドルを拒否します。 - 失効はフェイルクローズします。 例外を投げる、時間切れになる(
revocation_timeout)、Noneを返すソースはREVOCATION_UNAVAILABLEになり、codeがREVOCATION_INVALIDの例外や、schemas/v2/revocation_snapshot.schema.jsonに合わないスナップショットはREVOCATION_INVALIDになります。どれも失効していないとは見なされません。 deny_list_revocation_source(url, *, headers=None, timeout=5.0, workers=None)は、リファレンスの失効サービスの/.well-known/dcp-revocations.jsonの形式(updated_at、revocations[].agent_id、kid_revocations[].revoked_kid)の完全な拒否リストを、標準ライブラリで読みます。スナップショットのchecked_atはupdated_atで、挙げられたエージェントや鍵は失効し、それ以外のすべての対象は失効していません。ネットワークのエラー、時間切れ、2xx 以外の応答はREVOCATION_UNAVAILABLE、ほかの形のボディはREVOCATION_INVALIDです。リダイレクトをたどらず(あなたのヘッダーはあなたの URL にしか送られません)、HTTPS には TLS 1.2 以降が必要です。httpsを使い、認証情報は環境変数か vault に保管してください。timeoutは、各ソケット操作、ボディの読み取り、呼び出しが待つ時間を制限しますが、DNS の解決や、少しずつ届くステータス行とヘッダーは制限しません。workersは後述します。- 設定はあなたのコードからだけ来ます。 信頼セット、プロファイル、ポリシー、失効のソース、テナントは、リクエストのヘッダー、クエリ、ボディから読まれることはありません。ほかのテナントを名乗るリクエストは何も変えません。
- 設定のエラーは、ミドルウェアの構築時に(
code付きの)AdmissionConfigErrorを投げます。require_bundle=Trueなしのtrusted_principalsやprofile、trusted_principalsまたはrevocation_sourceのないプロファイル、あるいはverifier_policyを伴うプロファイル(CONFIG_PROFILE_CONTRADICTION)、無効な信頼セットやプロファイル(それぞれのコード)、型の誤った引数、profileのないrevocation_source(CONFIG_INVALID_VALUE)です。Starlette はミドルウェアのスタックをadd_middlewareではなく最初のリクエストで構築するため、このようなエラーはまずすべてのリクエストで500として現れます(ルートには決して届きません)。起動時に失敗させるには、信頼セットとプロファイルを自分で読み込み(create_principal_trust_store、resolve_strict_v1_profile)、読み込んだオブジェクトを渡してください。 - 同期の失効のソースはワーカースレッドで実行されます。時間切れになるとリクエストはただちに拒否されますが、スレッドはソースが戻るまで実行を続けます。非同期のソース、または
deny_list_revocation_source(url, timeout=...)のように独自のタイムアウトを持つものを選んでください。それは環境のプロキシを使わず、リダイレクトをたどらず、16 MiB を超えるリストを拒否します。そのtimeoutは各ソケット操作とボディの読み取りを制限しますが、DNS の解決や少しずつ届くステータス行は制限しません。
共有のアドミッションのベクターを使ってオフラインで実行できます:python integrations/fastapi/examples/strict_v1.py。
拒否リストのダウンロードの制限¶
既定では、deny_list_revocation_source の各呼び出しは asyncio.to_thread でダウンロードし、呼び出しが待つのをやめた後のそれらのスレッドを数えるものはありません。DenyListWorkers の予算を使うと、それらを数えます。
from dcp_ai.v2 import DenyListWorkers, deny_list_revocation_source
workers = DenyListWorkers(max_workers=8, max_workers_per_tenant=2) # one per process
source = deny_list_revocation_source(
"https://revocations.example.com/.well-known/dcp-revocations.json",
timeout=5.0,
workers=workers,
)
- 容量。 同時に実行されるダウンロードは最大
max_workers(1 から 64 の整数)で、1 つのテナントについては最大max_workers_per_tenantです。これがなければ、1 つのテナントがすべてを使えてしまいます。 - 各ダウンロードは、その枠を予約した後にだけ開始される、専用のスレッドで実行されます。
- 空きの枠がない呼び出しは、接続せずにただちに
REVOCATION_UNAVAILABLEを受け取ります。キューはありません。 - 容量はオブジェクトごとです。それを共有すべきすべてのソースに同じものを与えてください。
- 枠は、ダウンロードが本当に終わるまで保持されます。 呼び出しが待つのをやめたときではありません。時間切れ、取り消されたリクエスト、C3 の監督者の期限、閉じたイベントループでは解放されません。
- 予算が数えるのは進行中のダウンロードです。ダウンロードを終えたばかりのスレッドは、結果を引き渡して終了するまでの短い間、まだ存在します。
- 同じ答え。
- ダウンロードは、同じ TLS、プロキシ、リダイレクト、サイズの規則による同じ GET です。
- ボディは引き続きイベントループの上で確認され、解析されます。
- エラーはこのオプションなしの場合と同じです。
timeoutの後に届いた、または使われた結果は、ループが忙しくてタイマーを実行できなかった場合でもREVOCATION_UNAVAILABLEです。workers.close()は新しいダウンロードを拒否し、{"in_flight": n}を返します。実行中のものを待つことも止めることもせず、それらの枠は終わるまで埋まったままで、再開はできません。workers.stats()は、URL、テナント、ヘッダーを含まないカウンターを返します:open、in_flight、tenants、saturated、closed、start_failed、late_results、dropped。- プロセスの終了は待ちます。 スレッドはデーモンスレッドではないため、プロセスの終了時、インタープリターはまだ実行中のダウンロードを待ちます。これは
asyncio.to_threadですでに起きていることで、長くかかることがあります。ここでは DNS、TLS、urllib を止めるものは何もなく、呼び出しを取り消しても、それを待つのをやめるだけです。待ち続けるプロセスを終わらせるには、猶予期間の後のプロセスマネージャーによる強制終了に頼ってください。 - プロセスごとに 1 つ。 作成した後にフォークしたプロセスでは、
stats()とclose()はRuntimeErrorを投げ、ソースは親のロックやカウンターに触れずにREVOCATION_UNAVAILABLEを返します。ワーカープロセスごとに 1 つ作ってください。 - しないこと。 実行するダウンロードの数を制限するのであって、1 つにかかる時間ではありません。ボディの期限はブロックする読み取りの間で確認されるため、すでに進行中の読み取りはそれを超えることがあります。
require_dcp(request)¶
検証済みの DCP のエージェントを求める FastAPI の依存関係です。
from fastapi import Depends
from dcp_ai.fastapi import require_dcp, DCPAgentContext
@app.post("/protected")
async def protected_route(agent: DCPAgentContext = Depends(require_dcp)):
return {"agent_id": agent.agent_id}
- 戻り値:
DCPAgentContext - 例外: 検証済みのエージェントがいない場合は
HTTPException(403)
DCPAgentContext¶
検証済みのエージェントのデータを持つデータクラスです。
@dataclass
class DCPAgentContext:
dcp_version: str = "1.0"
agent_id: str = ""
human_id: str = ""
session_nonce: str = "" # V2: verified session nonce
public_key: str = ""
capabilities: list[str] = field(default_factory=list)
risk_tier: str = "medium"
status: str = "active"
passport: dict[str, Any] = field(default_factory=dict)
rpr: dict[str, Any] = field(default_factory=dict)
intent: dict[str, Any] = field(default_factory=dict)
policy_decision: dict[str, Any] = field(default_factory=dict)
composite_sig_valid: bool = False # V2: the bundle signature verified
session_binding_valid: bool = False # V2: session binding verified
blinded_rpr: bool = False
lifecycle_state: str = "active" # DCP-05
mandate_id: str = "" # DCP-09
mandate_valid: bool = False # DCP-09
warnings: list[str] = field(default_factory=list) # V2: verifier warnings
# Admission: set only with trusted_principals (admitted stays False otherwise)
admitted: bool = False
principal_trust: str = "not_evaluated" # "trusted" when admitted with trusted_principals
profile: str | None = None # "strict-v1" under the profile
profile_compliance: str = "not_evaluated"
recorded_decision: str | None = None # approve, escalate or block
trust_set_version: str | None = None
action_authorized: bool = False # always False
require_dcp_commissioned(request)¶
lifecycle_state が decommissioned のエージェントを拒否する FastAPI の依存関係です。ミドルウェアは lifecycle_state をパスポートの status から設定し、DCP-05 の稼働開始の証明書やライフサイクルの記録は読みません。
@app.post("/active-only")
async def active_only(agent: DCPAgentContext = Depends(require_dcp_commissioned)):
return {"agent_id": agent.agent_id}
require_dcp_mandated(request)¶
委任マンデート(DCP-09 §3.1)を求める FastAPI の依存関係ですが、確認するのは mandate_id が空でないことだけです。マンデートを取得することも、その署名を検証することも、mandate_valid を確認することもしません。ミドルウェアは mandate_id を自ら設定しないため、あなた自身のコードがそれを設定しない限り、すべてのリクエストが 403 を受け取ります(設定した場合、マンデートを確認するのはあなたの責任です)。
@app.post("/delegated")
async def delegated(agent: DCPAgentContext = Depends(require_dcp_mandated)):
return {"agent_id": agent.agent_id, "mandate_id": agent.mandate_id}
高度な例 — グローバルなミドルウェア + 選択的なルート¶
from fastapi import FastAPI, Depends, HTTPException, Request
from dcp_ai.fastapi import DCPVerifyMiddleware, require_dcp, DCPAgentContext
app = FastAPI()
# Global middleware (non-blocking, only verifies if a bundle is present)
app.add_middleware(DCPVerifyMiddleware, require_bundle=False)
# Public route
@app.get("/health")
async def health():
return {"ok": True}
# Route that REQUIRES a verified agent
@app.post("/agent/execute")
async def execute(agent: DCPAgentContext = Depends(require_dcp)):
if "api_call" not in agent.capabilities:
raise HTTPException(403, "Capability 'api_call' required")
return {"executed_by": agent.agent_id, "risk": agent.risk_tier}
# Route that uses the agent if available
@app.get("/info")
async def info(request: Request):
agent = getattr(request.state, "dcp_agent", None)
if agent:
return {"mode": "verified", "agent": agent.agent_id}
return {"mode": "anonymous"}
開発¶
チェックアウトから:
依存関係¶
dcp-ai— DCP の検証の SDKfastapi— Web のフレームワークstarlette— ASGI のツールキット(FastAPI に含まれる)
ライセンス¶
Apache-2.0