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#[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#[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#[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#[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}