4949 SidecarConfig ,
5050)
5151from predicate_authority .sidecar_store import LocalCredentialStore
52- from predicate_contracts import PolicyRule , TraceEmitter
52+ from predicate_contracts import (
53+ ActionRequest ,
54+ ActionSpec ,
55+ AuthorizationDecision ,
56+ PolicyRule ,
57+ PrincipalRef ,
58+ StateEvidence ,
59+ TraceEmitter ,
60+ VerificationEvidence ,
61+ VerificationSignal ,
62+ VerificationStatus ,
63+ )
5364
5465
5566@dataclass (frozen = True )
@@ -185,6 +196,8 @@ def do_GET(self) -> None: # noqa: N802
185196 def do_POST (self ) -> None : # noqa: N802
186197 parsed = urlparse (self .path )
187198 handlers : dict [str , Any ] = {
199+ "/v1/authorize" : self ._handle_authorize ,
200+ "/authorize" : self ._handle_authorize ,
188201 "/policy/reload" : self ._handle_policy_reload ,
189202 "/revoke/principal" : self ._handle_revoke_principal ,
190203 "/revoke/intent" : self ._handle_revoke_intent ,
@@ -201,6 +214,137 @@ def do_POST(self) -> None: # noqa: N802
201214 return
202215 handler ()
203216
217+ def _handle_authorize (self ) -> None :
218+ payload = self ._read_json_body ()
219+ try :
220+ request = self ._build_action_request (payload )
221+ except ValueError as exc :
222+ self ._send_json (400 , {"error" : str (exc )})
223+ return
224+ decision = self .server .daemon_ref .authorize_request (request ) # type: ignore[attr-defined]
225+ response = {
226+ "allowed" : decision .allowed ,
227+ "reason" : decision .reason .value ,
228+ "mandate_id" : (
229+ decision .mandate .claims .mandate_id if decision .mandate is not None else None
230+ ),
231+ "violated_rule" : decision .violated_rule ,
232+ "missing_labels" : list (decision .missing_labels ),
233+ }
234+ self ._send_json (200 if decision .allowed else 403 , response )
235+
236+ def _build_action_request (self , payload : dict [str , Any ]) -> ActionRequest :
237+ principal_id = payload .get ("principal" )
238+ if not isinstance (principal_id , str ) or principal_id .strip () == "" :
239+ raise ValueError ("principal is required" )
240+ action = payload .get ("action" )
241+ if not isinstance (action , str ) or action .strip () == "" :
242+ raise ValueError ("action is required" )
243+ resource = payload .get ("resource" )
244+ if not isinstance (resource , str ) or resource .strip () == "" :
245+ raise ValueError ("resource is required" )
246+
247+ intent_hash = payload .get ("intent_hash" )
248+ if isinstance (intent_hash , str ) and intent_hash .strip () != "" :
249+ intent = intent_hash .strip ()
250+ else :
251+ intent = f"{ action .strip ()} :{ resource .strip ()} "
252+
253+ context_raw = payload .get ("context" )
254+ context = context_raw if isinstance (context_raw , dict ) else {}
255+ tenant_id = context .get ("tenant_id" )
256+ session_id = context .get ("session_id" )
257+ state = self ._parse_state_evidence (payload = payload , context = context , intent = intent )
258+ verification = self ._parse_verification_evidence (payload = payload )
259+
260+ return ActionRequest (
261+ principal = PrincipalRef (
262+ principal_id = principal_id .strip (),
263+ tenant_id = tenant_id if isinstance (tenant_id , str ) else None ,
264+ session_id = session_id if isinstance (session_id , str ) else None ,
265+ ),
266+ action_spec = ActionSpec (
267+ action = action .strip (),
268+ resource = resource .strip (),
269+ intent = intent ,
270+ ),
271+ state_evidence = state ,
272+ verification_evidence = verification ,
273+ )
274+
275+ def _parse_state_evidence (
276+ self ,
277+ payload : dict [str , Any ],
278+ context : dict [str , Any ],
279+ intent : str ,
280+ ) -> StateEvidence :
281+ state_raw = payload .get ("state_evidence" )
282+ if not isinstance (state_raw , dict ):
283+ state_raw = {}
284+ source = state_raw .get ("source" )
285+ state_hash = state_raw .get ("state_hash" )
286+ schema_version = state_raw .get ("schema_version" )
287+ confidence = state_raw .get ("confidence" )
288+
289+ source_value = (
290+ source .strip () if isinstance (source , str ) and source .strip () != "" else "external"
291+ )
292+ if isinstance (state_hash , str ) and state_hash .strip () != "" :
293+ state_hash_value = state_hash .strip ()
294+ else :
295+ context_state_hash = context .get ("state_hash" )
296+ if isinstance (context_state_hash , str ) and context_state_hash .strip () != "" :
297+ state_hash_value = context_state_hash .strip ()
298+ else :
299+ state_hash_value = intent
300+ schema_value = (
301+ schema_version .strip ()
302+ if isinstance (schema_version , str ) and schema_version .strip () != ""
303+ else "v1"
304+ )
305+ confidence_value = float (confidence ) if isinstance (confidence , (float , int )) else None
306+ return StateEvidence (
307+ source = source_value ,
308+ state_hash = state_hash_value ,
309+ schema_version = schema_value ,
310+ confidence = confidence_value ,
311+ )
312+
313+ def _parse_verification_evidence (self , payload : dict [str , Any ]) -> VerificationEvidence :
314+ verification_raw = payload .get ("verification_evidence" )
315+ if not isinstance (verification_raw , dict ):
316+ return VerificationEvidence ()
317+ signals_raw = verification_raw .get ("signals" )
318+ if not isinstance (signals_raw , list ):
319+ return VerificationEvidence ()
320+ parsed_signals : list [VerificationSignal ] = []
321+ for item in signals_raw :
322+ if not isinstance (item , dict ):
323+ continue
324+ label = item .get ("label" )
325+ status = item .get ("status" )
326+ if not isinstance (label , str ) or label .strip () == "" :
327+ continue
328+ if not isinstance (status , str ):
329+ continue
330+ try :
331+ parsed_status = VerificationStatus (status .strip ())
332+ except ValueError :
333+ continue
334+ required_raw = item .get ("required" )
335+ required = bool (required_raw ) if isinstance (required_raw , bool ) else True
336+ reason_raw = item .get ("reason" )
337+ reason = reason_raw if isinstance (reason_raw , str ) else None
338+ parsed_signals .append (
339+ VerificationSignal (
340+ label = label .strip (),
341+ status = parsed_status ,
342+ required = required ,
343+ reason = reason ,
344+ )
345+ )
346+ return VerificationEvidence (signals = tuple (parsed_signals ))
347+
204348 def _handle_policy_reload (self ) -> None :
205349 reloaded = self .server .daemon_ref .reload_policy_now () # type: ignore[attr-defined]
206350 self ._send_json (200 , {"reloaded" : reloaded })
@@ -447,6 +591,9 @@ def revoke_mandate(self, mandate_id: str) -> None:
447591 def max_request_body_bytes (self ) -> int :
448592 return max (0 , int (self ._config .max_request_body_bytes ))
449593
594+ def authorize_request (self , request : ActionRequest ) -> AuthorizationDecision :
595+ return self ._sidecar .issue_mandate (request )
596+
450597 def issue_task_identity (
451598 self ,
452599 principal_id : str ,
0 commit comments