Skip to main content

verification_policy/
permit.rs

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    /// Returns the namespace covered by the permit.
20    pub fn namespace(&self) -> &str {
21        &self.namespace
22    }
23
24    /// Returns the concrete target key covered by the permit.
25    pub fn target_key(&self) -> &str {
26        &self.target_key
27    }
28
29    /// Returns the verification method covered by the permit.
30    pub fn method(&self) -> CheckMethod {
31        self.method
32    }
33
34    /// Returns the promotion class covered by the permit.
35    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    /// Runtime-only commit tokens cannot be forged outside this crate.
49    ///
50    /// ```compile_fail
51    /// use stack_ids::{ExecutionPermitId, PolicyDecisionId};
52    /// use verification_policy::CommitToken;
53    ///
54    /// let _forged = CommitToken {
55    ///     execution_permit_id: ExecutionPermitId::new("permit-1"),
56    ///     decision_id: PolicyDecisionId::new("decision-1"),
57    /// };
58    /// ```
59    ///
60    /// Returns the permit identifier bound to this runtime token.
61    pub fn execution_permit_id(&self) -> &ExecutionPermitId {
62        &self.execution_permit_id
63    }
64
65    /// Returns the policy decision that minted this runtime token.
66    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    /// Returns the permit identifier.
87    pub fn execution_permit_id(&self) -> &ExecutionPermitId {
88        &self.execution_permit_id
89    }
90
91    /// Returns the policy decision that issued the permit.
92    pub fn decision_id(&self) -> &PolicyDecisionId {
93        &self.decision_id
94    }
95
96    /// Returns the case covered by this permit.
97    pub fn case_id(&self) -> &VerificationCaseId {
98        &self.case_id
99    }
100
101    /// Returns the verification plan covered by this permit.
102    pub fn plan_id(&self) -> &CheckPlanId {
103        &self.plan_id
104    }
105
106    /// Returns the effect scope covered by this permit.
107    pub fn scope(&self) -> &ExecutionPermitScope {
108        &self.scope
109    }
110
111    /// Returns the approval record lineage, when human approval was required.
112    pub fn approval_record_id(&self) -> Option<&ApprovalRecordId> {
113        self.approval_record_id.as_ref()
114    }
115
116    /// Returns the issuance timestamp recorded on the decision.
117    pub fn issued_at(&self) -> &str {
118        &self.issued_at
119    }
120
121    /// Returns the constitutional citation lineage bound to this permit.
122    pub fn citation(&self) -> &V25CitationContext {
123        &self.citation
124    }
125
126    /// Returns the obligation references bound to this permit.
127    pub fn obligation_refs(&self) -> &V25ControlObligationRefs {
128        &self.obligation_refs
129    }
130
131    /// Returns the sealed runtime commit token.
132    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    /// Returns a stable string discriminant for this permit issuance error kind.
151    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    /// Mints a sealed execution permit when the policy decision still authorizes
189    /// one promotable execution for the supplied case and plan.
190    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}