Skip to main content

a3s_code_core/capability/
ceiling.rs

1use std::collections::BTreeSet;
2
3use serde::Serialize;
4
5use super::{CapabilityId, CapabilityScopeError, CapabilitySet, Sha256Digest, MAX_CAPABILITIES};
6
7pub const CAPABILITY_CEILING_SCHEMA: &str = "a3s.code.capability-ceiling.v1";
8
9/// Workspace operations a scope may expose. A child may only turn flags off.
10#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
11#[serde(rename_all = "camelCase")]
12pub struct WorkspaceCapabilityCeiling {
13    read: bool,
14    write: bool,
15    execute: bool,
16    search: bool,
17    git: bool,
18    code_intelligence: bool,
19}
20
21impl WorkspaceCapabilityCeiling {
22    pub const fn none() -> Self {
23        Self {
24            read: false,
25            write: false,
26            execute: false,
27            search: false,
28            git: false,
29            code_intelligence: false,
30        }
31    }
32
33    pub const fn all() -> Self {
34        Self {
35            read: true,
36            write: true,
37            execute: true,
38            search: true,
39            git: true,
40            code_intelligence: true,
41        }
42    }
43
44    pub const fn with_read(mut self, allowed: bool) -> Self {
45        self.read = allowed;
46        self
47    }
48
49    pub const fn with_write(mut self, allowed: bool) -> Self {
50        self.write = allowed;
51        self
52    }
53
54    pub const fn with_execute(mut self, allowed: bool) -> Self {
55        self.execute = allowed;
56        self
57    }
58
59    pub const fn with_search(mut self, allowed: bool) -> Self {
60        self.search = allowed;
61        self
62    }
63
64    pub const fn with_git(mut self, allowed: bool) -> Self {
65        self.git = allowed;
66        self
67    }
68
69    pub const fn with_code_intelligence(mut self, allowed: bool) -> Self {
70        self.code_intelligence = allowed;
71        self
72    }
73
74    pub const fn read(self) -> bool {
75        self.read
76    }
77
78    pub const fn write(self) -> bool {
79        self.write
80    }
81
82    pub const fn execute(self) -> bool {
83        self.execute
84    }
85
86    pub const fn search(self) -> bool {
87        self.search
88    }
89
90    pub const fn git(self) -> bool {
91        self.git
92    }
93
94    pub const fn code_intelligence(self) -> bool {
95        self.code_intelligence
96    }
97
98    fn expansion_from(self, parent: Self) -> Option<&'static str> {
99        [
100            (self.read, parent.read, "workspace.read"),
101            (self.write, parent.write, "workspace.write"),
102            (self.execute, parent.execute, "workspace.execute"),
103            (self.search, parent.search, "workspace.search"),
104            (self.git, parent.git, "workspace.git"),
105            (
106                self.code_intelligence,
107                parent.code_intelligence,
108                "workspace.code_intelligence",
109            ),
110        ]
111        .into_iter()
112        .find_map(|(child, parent, field)| (child && !parent).then_some(field))
113    }
114}
115
116impl Default for WorkspaceCapabilityCeiling {
117    fn default() -> Self {
118        Self::none()
119    }
120}
121
122/// Parent governance bindings that every child must retain.
123///
124/// These flags do not replace the concrete policy providers. They record which
125/// parent enforcement boundaries must remain composed into a child scope.
126#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)]
127#[serde(rename_all = "camelCase")]
128pub struct GovernanceCapabilityCeiling {
129    permission_guard_required: bool,
130    confirmation_guard_required: bool,
131    security_guard_required: bool,
132    budget_guard_required: bool,
133    active_skill_restrictions_required: bool,
134}
135
136impl GovernanceCapabilityCeiling {
137    pub const fn none_required() -> Self {
138        Self {
139            permission_guard_required: false,
140            confirmation_guard_required: false,
141            security_guard_required: false,
142            budget_guard_required: false,
143            active_skill_restrictions_required: false,
144        }
145    }
146
147    pub const fn require_permission_guard(mut self) -> Self {
148        self.permission_guard_required = true;
149        self
150    }
151
152    pub const fn require_confirmation_guard(mut self) -> Self {
153        self.confirmation_guard_required = true;
154        self
155    }
156
157    pub const fn require_security_guard(mut self) -> Self {
158        self.security_guard_required = true;
159        self
160    }
161
162    pub const fn require_budget_guard(mut self) -> Self {
163        self.budget_guard_required = true;
164        self
165    }
166
167    pub const fn require_active_skill_restrictions(mut self) -> Self {
168        self.active_skill_restrictions_required = true;
169        self
170    }
171
172    pub const fn permission_guard_required(self) -> bool {
173        self.permission_guard_required
174    }
175
176    pub const fn confirmation_guard_required(self) -> bool {
177        self.confirmation_guard_required
178    }
179
180    pub const fn security_guard_required(self) -> bool {
181        self.security_guard_required
182    }
183
184    pub const fn budget_guard_required(self) -> bool {
185        self.budget_guard_required
186    }
187
188    pub const fn active_skill_restrictions_required(self) -> bool {
189        self.active_skill_restrictions_required
190    }
191
192    fn expansion_from(self, parent: Self) -> Option<&'static str> {
193        [
194            (
195                self.permission_guard_required,
196                parent.permission_guard_required,
197                "governance.permission_guard",
198            ),
199            (
200                self.confirmation_guard_required,
201                parent.confirmation_guard_required,
202                "governance.confirmation_guard",
203            ),
204            (
205                self.security_guard_required,
206                parent.security_guard_required,
207                "governance.security_guard",
208            ),
209            (
210                self.budget_guard_required,
211                parent.budget_guard_required,
212                "governance.budget_guard",
213            ),
214            (
215                self.active_skill_restrictions_required,
216                parent.active_skill_restrictions_required,
217                "governance.active_skill_restrictions",
218            ),
219        ]
220        .into_iter()
221        .find_map(|(child_required, parent_required, field)| {
222            (parent_required && !child_required).then_some(field)
223        })
224    }
225}
226
227/// Numeric execution limits. `None` is unbounded and is therefore the widest
228/// value for an optional duration ceiling.
229#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
230#[serde(rename_all = "camelCase")]
231pub struct CapabilityExecutionCeiling {
232    max_tool_rounds: usize,
233    max_parallel_tasks: usize,
234    tool_timeout_ms: Option<u64>,
235    llm_api_timeout_ms: Option<u64>,
236    max_execution_time_ms: Option<u64>,
237}
238
239impl CapabilityExecutionCeiling {
240    pub fn new(
241        max_tool_rounds: usize,
242        max_parallel_tasks: usize,
243        tool_timeout_ms: Option<u64>,
244        llm_api_timeout_ms: Option<u64>,
245        max_execution_time_ms: Option<u64>,
246    ) -> Result<Self, CapabilityScopeError> {
247        validate_positive_usize("max_tool_rounds", max_tool_rounds)?;
248        validate_positive_usize("max_parallel_tasks", max_parallel_tasks)?;
249        validate_optional_positive("tool_timeout_ms", tool_timeout_ms)?;
250        validate_optional_positive("llm_api_timeout_ms", llm_api_timeout_ms)?;
251        validate_optional_positive("max_execution_time_ms", max_execution_time_ms)?;
252        Ok(Self {
253            max_tool_rounds,
254            max_parallel_tasks,
255            tool_timeout_ms,
256            llm_api_timeout_ms,
257            max_execution_time_ms,
258        })
259    }
260
261    pub const fn max_tool_rounds(self) -> usize {
262        self.max_tool_rounds
263    }
264
265    pub const fn max_parallel_tasks(self) -> usize {
266        self.max_parallel_tasks
267    }
268
269    pub const fn tool_timeout_ms(self) -> Option<u64> {
270        self.tool_timeout_ms
271    }
272
273    pub const fn llm_api_timeout_ms(self) -> Option<u64> {
274        self.llm_api_timeout_ms
275    }
276
277    pub const fn max_execution_time_ms(self) -> Option<u64> {
278        self.max_execution_time_ms
279    }
280
281    fn expansion_from(self, parent: Self) -> Option<&'static str> {
282        if self.max_tool_rounds > parent.max_tool_rounds {
283            return Some("execution.max_tool_rounds");
284        }
285        if self.max_parallel_tasks > parent.max_parallel_tasks {
286            return Some("execution.max_parallel_tasks");
287        }
288        for (child, parent, field) in [
289            (
290                self.tool_timeout_ms,
291                parent.tool_timeout_ms,
292                "execution.tool_timeout_ms",
293            ),
294            (
295                self.llm_api_timeout_ms,
296                parent.llm_api_timeout_ms,
297                "execution.llm_api_timeout_ms",
298            ),
299            (
300                self.max_execution_time_ms,
301                parent.max_execution_time_ms,
302                "execution.max_execution_time_ms",
303            ),
304        ] {
305            if optional_limit_expands(child, parent) {
306                return Some(field);
307            }
308        }
309        None
310    }
311}
312
313/// Complete immutable authority ceiling for one catalog generation.
314#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
315#[serde(rename_all = "camelCase")]
316pub struct CapabilityCeiling {
317    schema: &'static str,
318    catalog_digest: Sha256Digest,
319    allowed_capabilities: BTreeSet<CapabilityId>,
320    workspace: WorkspaceCapabilityCeiling,
321    governance: GovernanceCapabilityCeiling,
322    execution: CapabilityExecutionCeiling,
323}
324
325impl CapabilityCeiling {
326    pub fn new(
327        set: &CapabilitySet,
328        allowed_capabilities: impl IntoIterator<Item = CapabilityId>,
329        workspace: WorkspaceCapabilityCeiling,
330        governance: GovernanceCapabilityCeiling,
331        execution: CapabilityExecutionCeiling,
332    ) -> Result<Self, CapabilityScopeError> {
333        let mut allowed = BTreeSet::new();
334        for capability in allowed_capabilities {
335            if allowed.len() >= MAX_CAPABILITIES {
336                return Err(CapabilityScopeError::BoundExceeded {
337                    field: "ceiling_capabilities",
338                    max: MAX_CAPABILITIES,
339                });
340            }
341            if !set.contains(&capability) {
342                return Err(CapabilityScopeError::CapabilityOutsideCatalog {
343                    capability: capability.to_string(),
344                    catalog_digest: set.digest().to_string(),
345                });
346            }
347            if !allowed.insert(capability.clone()) {
348                return Err(CapabilityScopeError::DuplicateCeilingCapability {
349                    capability: capability.to_string(),
350                });
351            }
352        }
353        Ok(Self {
354            schema: CAPABILITY_CEILING_SCHEMA,
355            catalog_digest: set.digest().clone(),
356            allowed_capabilities: allowed,
357            workspace,
358            governance,
359            execution,
360        })
361    }
362
363    pub fn all(
364        set: &CapabilitySet,
365        workspace: WorkspaceCapabilityCeiling,
366        governance: GovernanceCapabilityCeiling,
367        execution: CapabilityExecutionCeiling,
368    ) -> Result<Self, CapabilityScopeError> {
369        Self::new(
370            set,
371            set.iter().map(|(id, _)| id.clone()),
372            workspace,
373            governance,
374            execution,
375        )
376    }
377
378    pub const fn schema(&self) -> &'static str {
379        self.schema
380    }
381
382    pub fn catalog_digest(&self) -> &Sha256Digest {
383        &self.catalog_digest
384    }
385
386    pub fn allows(&self, capability: &CapabilityId) -> bool {
387        self.allowed_capabilities.contains(capability)
388    }
389
390    pub fn len(&self) -> usize {
391        self.allowed_capabilities.len()
392    }
393
394    pub fn is_empty(&self) -> bool {
395        self.allowed_capabilities.is_empty()
396    }
397
398    pub fn iter(&self) -> impl ExactSizeIterator<Item = &CapabilityId> {
399        self.allowed_capabilities.iter()
400    }
401
402    pub const fn workspace(&self) -> WorkspaceCapabilityCeiling {
403        self.workspace
404    }
405
406    pub const fn governance(&self) -> GovernanceCapabilityCeiling {
407        self.governance
408    }
409
410    pub const fn execution(&self) -> CapabilityExecutionCeiling {
411        self.execution
412    }
413
414    pub fn ensure_within(&self, parent: &Self) -> Result<(), CapabilityScopeError> {
415        if self.catalog_digest != parent.catalog_digest {
416            return Err(CapabilityScopeError::CeilingCatalogMismatch);
417        }
418        if !self
419            .allowed_capabilities
420            .is_subset(&parent.allowed_capabilities)
421        {
422            return Err(CapabilityScopeError::CeilingExpansion {
423                dimension: "capabilities",
424            });
425        }
426        if let Some(dimension) = self.workspace.expansion_from(parent.workspace) {
427            return Err(CapabilityScopeError::CeilingExpansion { dimension });
428        }
429        if let Some(dimension) = self.governance.expansion_from(parent.governance) {
430            return Err(CapabilityScopeError::CeilingExpansion { dimension });
431        }
432        if let Some(dimension) = self.execution.expansion_from(parent.execution) {
433            return Err(CapabilityScopeError::CeilingExpansion { dimension });
434        }
435        Ok(())
436    }
437}
438
439fn validate_positive_usize(field: &'static str, value: usize) -> Result<(), CapabilityScopeError> {
440    if value == 0 {
441        return Err(CapabilityScopeError::InvalidExecutionLimit { field });
442    }
443    Ok(())
444}
445
446fn validate_optional_positive(
447    field: &'static str,
448    value: Option<u64>,
449) -> Result<(), CapabilityScopeError> {
450    if value == Some(0) {
451        return Err(CapabilityScopeError::InvalidExecutionLimit { field });
452    }
453    Ok(())
454}
455
456const fn optional_limit_expands(child: Option<u64>, parent: Option<u64>) -> bool {
457    match (child, parent) {
458        (_, None) => false,
459        (None, Some(_)) => true,
460        (Some(child), Some(parent)) => child > parent,
461    }
462}