1use crate::{
2 approval_matches, ApprovalRecord, PolicyDecision, V25CitationContext, V25ControlObligationRefs,
3};
4use stack_ids::{
5 ApprovalRecordId, CheckPlanId, ExecutionPermitId, PolicyDecisionId, VerificationCaseId,
6};
7use std::fmt;
8use verification_control::{CheckMethod, CheckPlan, PromotionClass, VerificationCase};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct ExecutionPermitScope {
12 namespace: String,
13 target_key: String,
14 method: CheckMethod,
15 promotion_class: PromotionClass,
16}
17
18impl ExecutionPermitScope {
19 pub fn namespace(&self) -> &str {
21 &self.namespace
22 }
23
24 pub fn target_key(&self) -> &str {
26 &self.target_key
27 }
28
29 pub fn method(&self) -> CheckMethod {
31 self.method
32 }
33
34 pub fn promotion_class(&self) -> PromotionClass {
36 self.promotion_class
37 }
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct CommitToken {
42 execution_permit_id: ExecutionPermitId,
43 decision_id: PolicyDecisionId,
44 seal: seal::RuntimeSeal,
45}
46
47impl CommitToken {
48 pub fn execution_permit_id(&self) -> &ExecutionPermitId {
62 &self.execution_permit_id
63 }
64
65 pub fn decision_id(&self) -> &PolicyDecisionId {
67 &self.decision_id
68 }
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct ExecutionPermit {
73 execution_permit_id: ExecutionPermitId,
74 decision_id: PolicyDecisionId,
75 case_id: VerificationCaseId,
76 plan_id: CheckPlanId,
77 scope: ExecutionPermitScope,
78 approval_record_id: Option<ApprovalRecordId>,
79 issued_at: String,
80 citation: V25CitationContext,
81 obligation_refs: V25ControlObligationRefs,
82 commit_token: CommitToken,
83}
84
85impl ExecutionPermit {
86 pub fn execution_permit_id(&self) -> &ExecutionPermitId {
88 &self.execution_permit_id
89 }
90
91 pub fn decision_id(&self) -> &PolicyDecisionId {
93 &self.decision_id
94 }
95
96 pub fn case_id(&self) -> &VerificationCaseId {
98 &self.case_id
99 }
100
101 pub fn plan_id(&self) -> &CheckPlanId {
103 &self.plan_id
104 }
105
106 pub fn scope(&self) -> &ExecutionPermitScope {
108 &self.scope
109 }
110
111 pub fn approval_record_id(&self) -> Option<&ApprovalRecordId> {
113 self.approval_record_id.as_ref()
114 }
115
116 pub fn issued_at(&self) -> &str {
118 &self.issued_at
119 }
120
121 pub fn citation(&self) -> &V25CitationContext {
123 &self.citation
124 }
125
126 pub fn obligation_refs(&self) -> &V25ControlObligationRefs {
128 &self.obligation_refs
129 }
130
131 pub fn commit_token(&self) -> &CommitToken {
133 &self.commit_token
134 }
135}
136
137#[derive(Debug, Clone, PartialEq, Eq)]
138pub enum PermitIssuanceError {
139 DecisionContextMismatch,
140 MethodDenied,
141 AutonomyDenied,
142 PlanNotPromotionCapable,
143 ApprovalRequired,
144 CitationIncomplete,
145 ObligationRefsIncomplete,
146 PromotionBlocked,
147}
148
149impl PermitIssuanceError {
150 pub fn kind(&self) -> &'static str {
152 match self {
153 Self::DecisionContextMismatch => "decision_context_mismatch",
154 Self::MethodDenied => "method_denied",
155 Self::AutonomyDenied => "autonomy_denied",
156 Self::PlanNotPromotionCapable => "plan_not_promotion_capable",
157 Self::ApprovalRequired => "approval_required",
158 Self::CitationIncomplete => "citation_incomplete",
159 Self::ObligationRefsIncomplete => "obligation_refs_incomplete",
160 Self::PromotionBlocked => "promotion_blocked",
161 }
162 }
163}
164
165impl fmt::Display for PermitIssuanceError {
166 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167 let message = match self {
168 Self::DecisionContextMismatch => {
169 "policy decision lineage does not match the requested case/plan"
170 }
171 Self::MethodDenied => "policy denied the requested method",
172 Self::AutonomyDenied => "policy denied the requested autonomy level",
173 Self::PlanNotPromotionCapable => {
174 "advisory-only or non-promotable plans cannot mint execution permits"
175 }
176 Self::ApprovalRequired => "matching approval is required before execution",
177 Self::CitationIncomplete => "constitutional citation context is incomplete",
178 Self::ObligationRefsIncomplete => "constitutional obligation refs are incomplete",
179 Self::PromotionBlocked => "policy blocked promotion-capable execution",
180 };
181 f.write_str(message)
182 }
183}
184
185impl std::error::Error for PermitIssuanceError {}
186
187impl PolicyDecision {
188 pub fn issue_execution_permit(
191 &self,
192 case: &VerificationCase,
193 plan: &CheckPlan,
194 approvals: &[ApprovalRecord],
195 ) -> Result<ExecutionPermit, PermitIssuanceError> {
196 if self.case_id != case.case_id || self.plan_id != plan.plan_id {
197 return Err(PermitIssuanceError::DecisionContextMismatch);
198 }
199 if !self.method_allowed {
200 return Err(PermitIssuanceError::MethodDenied);
201 }
202 if !self.autonomy_allowed {
203 return Err(PermitIssuanceError::AutonomyDenied);
204 }
205 if plan.advisory_only || !plan.promotable_if_completed {
206 return Err(PermitIssuanceError::PlanNotPromotionCapable);
207 }
208
209 let approval_record_id = if self.approval_required {
210 Some(
211 approvals
212 .iter()
213 .find(|approval| {
214 approval.policy_version == self.policy_version
215 && approval_matches(approval, case, plan, self.evaluated_at.as_str())
216 })
217 .map(|approval| approval.approval_record_id.clone())
218 .ok_or(PermitIssuanceError::ApprovalRequired)?,
219 )
220 } else {
221 None
222 };
223
224 if !self.citation_status.is_complete() {
225 return Err(PermitIssuanceError::CitationIncomplete);
226 }
227 if !self.obligation_refs_status.is_complete() {
228 return Err(PermitIssuanceError::ObligationRefsIncomplete);
229 }
230 if !self.promotion_allowed {
231 return Err(PermitIssuanceError::PromotionBlocked);
232 }
233
234 let execution_permit_id = ExecutionPermitId::generate();
235 let commit_token = CommitToken {
236 execution_permit_id: execution_permit_id.clone(),
237 decision_id: self.decision_id.clone(),
238 seal: seal::RuntimeSeal::new(),
239 };
240
241 Ok(ExecutionPermit {
242 execution_permit_id,
243 decision_id: self.decision_id.clone(),
244 case_id: case.case_id.clone(),
245 plan_id: plan.plan_id.clone(),
246 scope: ExecutionPermitScope {
247 namespace: case.region.namespace.clone(),
248 target_key: case.region.target_key.clone(),
249 method: plan.method,
250 promotion_class: plan.promotion_class,
251 },
252 approval_record_id,
253 issued_at: self.evaluated_at.clone(),
254 citation: self.citation.clone(),
255 obligation_refs: self.obligation_refs.clone(),
256 commit_token,
257 })
258 }
259}
260
261impl From<&ExecutionPermit> for llm_tool_runtime::ToolExecutionPermit {
262 fn from(permit: &ExecutionPermit) -> Self {
263 llm_tool_runtime::ToolExecutionPermit::new(
264 permit.execution_permit_id().clone(),
265 permit.decision_id().clone(),
266 permit.approval_record_id().cloned(),
267 permit.scope().namespace(),
268 permit.scope().target_key(),
269 )
270 }
271}
272
273mod seal {
274 #[derive(Debug, Clone, PartialEq, Eq)]
275 pub struct RuntimeSeal(());
276
277 impl RuntimeSeal {
278 pub(super) fn new() -> Self {
279 Self(())
280 }
281 }
282}