88from __future__ import annotations
99
1010from collections .abc import Awaitable , Callable
11- from dataclasses import asdict
12- from typing import Any , Literal
11+ from typing import Any
1312
14- from pydantic import BaseModel , ConfigDict , Field
1513from starlette .requests import Request
16- from starlette .responses import JSONResponse , Response
1714from starlette .routing import Route
1815
1916from agentnet .approval .service import IndependentApprovalVerifier
2017from agentnet .authorization .admin_http import create_authority_admin_routes
2118from agentnet .authorization .evidence import IssuanceAuthority
2219from agentnet .authorization .policy import AuthorizationRequest
2320from agentnet .core .app import CommunicationCore
24- from agentnet .errors import GateBlocked , ValidationError
21+ from agentnet .errors import GateBlocked
22+ from agentnet .identity .recovery import OIDCCredentialRecoveryCoordinator
23+ from agentnet .identity .recovery_http import create_credential_recovery_routes
2524from agentnet .identity .revocation import HarnessRevocationService
2625from agentnet .identity .revocation_http import create_harness_revocation_routes
27- from agentnet .identity .recovery import OIDCCredentialRecoveryCoordinator
2826from agentnet .identity .workload_http import create_workload_admin_routes
2927
3028
@@ -40,24 +38,6 @@ def _headers() -> dict[str, str]:
4038 }
4139
4240
43- class RecoveryBeginBody (BaseModel ):
44- model_config = ConfigDict (extra = "forbid" )
45-
46- old_harness_id : str = Field (min_length = 1 , max_length = 256 )
47- new_harness_kind : str = Field (min_length = 1 , max_length = 64 )
48- new_harness_name : str = Field (min_length = 1 , max_length = 128 )
49- new_binding_assurance : Literal ["os_bound" , "hardware_bound" ]
50- new_public_key_pem : str = Field (min_length = 128 , max_length = 16_384 )
51-
52-
53- class RecoveryCompleteBody (BaseModel ):
54- model_config = ConfigDict (extra = "forbid" )
55-
56- recovery_transaction_id : str = Field (min_length = 16 , max_length = 128 )
57- possession_signature : str = Field (min_length = 1 , max_length = 2_048 )
58- independent_approvals : tuple [dict [str , Any ], ...] = Field (min_length = 1 , max_length = 5 )
59-
60-
6141def _decision (
6242 core : CommunicationCore ,
6343 * ,
@@ -101,51 +81,6 @@ def create_identity_admin_routes(
10181 task_grants = core .grants ,
10282 )
10383
104- async def begin_recovery (request : Request ) -> Response :
105- if recovery_coordinator is None :
106- raise GateBlocked ("credential_recovery" , "OIDC credential recovery is not configured" )
107- peer = "unavailable" if request .client is None else request .client .host
108- core .quotas .consume (
109- scope = f"public-credential-recovery:{ peer } " ,
110- metric = "recovery_attempts" ,
111- amount = 1 ,
112- limit = 20 ,
113- )
114- body = await request .body ()
115- if len (body ) > core .config .max_request_bytes :
116- raise ValidationError ("credential recovery request exceeds the configured limit" )
117- parsed = RecoveryBeginBody .model_validate_json (body )
118- authorization = recovery_coordinator .begin_authorization (
119- domain_id = core .config .domain_id ,
120- old_harness_id = parsed .old_harness_id ,
121- new_harness_kind = parsed .new_harness_kind ,
122- new_harness_name = parsed .new_harness_name ,
123- new_binding_assurance = parsed .new_binding_assurance ,
124- new_public_key_pem = parsed .new_public_key_pem ,
125- )
126- return JSONResponse (asdict (authorization ), status_code = 201 , headers = _headers ())
127-
128- async def complete_recovery (request : Request ) -> Response :
129- if recovery_coordinator is None :
130- raise GateBlocked ("credential_recovery" , "OIDC credential recovery is not configured" )
131- peer = "unavailable" if request .client is None else request .client .host
132- core .quotas .consume (
133- scope = f"public-credential-recovery:{ peer } " ,
134- metric = "recovery_attempts" ,
135- amount = 1 ,
136- limit = 20 ,
137- )
138- body = await request .body ()
139- if len (body ) > core .config .max_request_bytes :
140- raise ValidationError ("credential recovery request exceeds the configured limit" )
141- parsed = RecoveryCompleteBody .model_validate_json (body )
142- result = recovery_coordinator .complete_recovery (
143- transaction_id = parsed .recovery_transaction_id ,
144- possession_signature = parsed .possession_signature ,
145- approvals = parsed .independent_approvals ,
146- )
147- return JSONResponse (result .model_dump (mode = "json" ), status_code = 201 , headers = _headers ())
148-
14984 routes = create_authority_admin_routes (
15085 core ,
15186 body_and_actor ,
@@ -172,10 +107,11 @@ async def complete_recovery(request: Request) -> Response:
172107 )
173108 if recovery_coordinator is not None :
174109 routes .extend (
175- [
176- Route ("/v1/credential-recovery/oidc/begin" , begin_recovery , methods = ["POST" ]),
177- Route ("/v1/credential-recovery/complete" , complete_recovery , methods = ["POST" ]),
178- ]
110+ create_credential_recovery_routes (
111+ core ,
112+ recovery_coordinator ,
113+ _headers (),
114+ )
179115 )
180116 return routes
181117
0 commit comments