1use std::cell::RefCell;
4use std::collections::BTreeMap;
5use std::rc::Rc;
6use std::thread_local;
7
8use serde::{Deserialize, Serialize};
9
10use super::{glob_match, new_id};
11use crate::value::{VmError, VmValue};
12
13thread_local! {
14 static EXECUTION_POLICY_STACK: RefCell<Vec<CapabilityPolicy>> = const { RefCell::new(Vec::new()) };
15}
16
17#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
21#[serde(default)]
22pub struct ToolArgConstraint {
23 pub tool: String,
25 pub arg_patterns: Vec<String>,
28}
29
30pub fn enforce_tool_arg_constraints(
32 policy: &CapabilityPolicy,
33 tool_name: &str,
34 args: &serde_json::Value,
35) -> Result<(), VmError> {
36 for constraint in &policy.tool_arg_constraints {
37 if !glob_match(&constraint.tool, tool_name) {
38 continue;
39 }
40 if constraint.arg_patterns.is_empty() {
41 continue;
42 }
43 let first_arg = args
45 .as_object()
46 .and_then(|o| o.values().next())
47 .and_then(|v| v.as_str())
48 .or_else(|| args.as_str())
49 .unwrap_or("");
50 let matches = constraint
51 .arg_patterns
52 .iter()
53 .any(|pattern| glob_match(pattern, first_arg));
54 if !matches {
55 return reject_policy(format!(
56 "tool '{tool_name}' argument '{first_arg}' does not match allowed patterns: {:?}",
57 constraint.arg_patterns
58 ));
59 }
60 }
61 Ok(())
62}
63
64#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
65#[serde(default)]
66pub struct ToolRuntimePolicyMetadata {
67 pub capabilities: BTreeMap<String, Vec<String>>,
68 pub side_effect_level: Option<String>,
69 pub path_params: Vec<String>,
70 pub mutation_classification: Option<String>,
71}
72
73#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
74#[serde(default)]
75pub struct CapabilityPolicy {
76 pub tools: Vec<String>,
77 pub capabilities: BTreeMap<String, Vec<String>>,
78 pub workspace_roots: Vec<String>,
79 pub side_effect_level: Option<String>,
80 pub recursion_limit: Option<usize>,
81 #[serde(default)]
83 pub tool_arg_constraints: Vec<ToolArgConstraint>,
84 #[serde(default)]
85 pub tool_metadata: BTreeMap<String, ToolRuntimePolicyMetadata>,
86}
87
88impl CapabilityPolicy {
89 pub fn intersect(&self, requested: &CapabilityPolicy) -> Result<CapabilityPolicy, String> {
90 let side_effect_level = match (&self.side_effect_level, &requested.side_effect_level) {
91 (Some(a), Some(b)) => Some(min_side_effect(a, b).to_string()),
92 (Some(a), None) => Some(a.clone()),
93 (None, Some(b)) => Some(b.clone()),
94 (None, None) => None,
95 };
96
97 if !self.tools.is_empty() {
98 let denied: Vec<String> = requested
99 .tools
100 .iter()
101 .filter(|tool| !self.tools.contains(*tool))
102 .cloned()
103 .collect();
104 if !denied.is_empty() {
105 return Err(format!(
106 "requested tools exceed host ceiling: {}",
107 denied.join(", ")
108 ));
109 }
110 }
111
112 for (capability, requested_ops) in &requested.capabilities {
113 if let Some(allowed_ops) = self.capabilities.get(capability) {
114 let denied: Vec<String> = requested_ops
115 .iter()
116 .filter(|op| !allowed_ops.contains(*op))
117 .cloned()
118 .collect();
119 if !denied.is_empty() {
120 return Err(format!(
121 "requested capability operations exceed host ceiling: {}.{}",
122 capability,
123 denied.join(",")
124 ));
125 }
126 } else if !self.capabilities.is_empty() {
127 return Err(format!(
128 "requested capability exceeds host ceiling: {capability}"
129 ));
130 }
131 }
132
133 let tools = if self.tools.is_empty() {
134 requested.tools.clone()
135 } else if requested.tools.is_empty() {
136 self.tools.clone()
137 } else {
138 requested
139 .tools
140 .iter()
141 .filter(|tool| self.tools.contains(*tool))
142 .cloned()
143 .collect()
144 };
145
146 let capabilities = if self.capabilities.is_empty() {
147 requested.capabilities.clone()
148 } else if requested.capabilities.is_empty() {
149 self.capabilities.clone()
150 } else {
151 requested
152 .capabilities
153 .iter()
154 .filter_map(|(capability, requested_ops)| {
155 self.capabilities.get(capability).map(|allowed_ops| {
156 (
157 capability.clone(),
158 requested_ops
159 .iter()
160 .filter(|op| allowed_ops.contains(*op))
161 .cloned()
162 .collect::<Vec<_>>(),
163 )
164 })
165 })
166 .collect()
167 };
168
169 let workspace_roots = if self.workspace_roots.is_empty() {
170 requested.workspace_roots.clone()
171 } else if requested.workspace_roots.is_empty() {
172 self.workspace_roots.clone()
173 } else {
174 requested
175 .workspace_roots
176 .iter()
177 .filter(|root| self.workspace_roots.contains(*root))
178 .cloned()
179 .collect()
180 };
181
182 let recursion_limit = match (self.recursion_limit, requested.recursion_limit) {
183 (Some(a), Some(b)) => Some(a.min(b)),
184 (Some(a), None) => Some(a),
185 (None, Some(b)) => Some(b),
186 (None, None) => None,
187 };
188
189 let mut tool_arg_constraints = self.tool_arg_constraints.clone();
191 tool_arg_constraints.extend(requested.tool_arg_constraints.clone());
192
193 let tool_metadata = tools
194 .iter()
195 .filter_map(|tool| {
196 requested
197 .tool_metadata
198 .get(tool)
199 .or_else(|| self.tool_metadata.get(tool))
200 .cloned()
201 .map(|metadata| (tool.clone(), metadata))
202 })
203 .collect();
204
205 Ok(CapabilityPolicy {
206 tools,
207 capabilities,
208 workspace_roots,
209 side_effect_level,
210 recursion_limit,
211 tool_arg_constraints,
212 tool_metadata,
213 })
214 }
215}
216
217fn min_side_effect<'a>(a: &'a str, b: &'a str) -> &'a str {
218 fn rank(v: &str) -> usize {
219 match v {
220 "none" => 0,
221 "read_only" => 1,
222 "workspace_write" => 2,
223 "process_exec" => 3,
224 "network" => 4,
225 _ => 5,
226 }
227 }
228 if rank(a) <= rank(b) {
229 a
230 } else {
231 b
232 }
233}
234
235#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
236#[serde(default)]
237pub struct ModelPolicy {
238 pub provider: Option<String>,
239 pub model: Option<String>,
240 pub model_tier: Option<String>,
241 pub temperature: Option<f64>,
242 pub max_tokens: Option<i64>,
243 pub max_iterations: Option<usize>,
245 pub max_nudges: Option<usize>,
247 pub nudge: Option<String>,
250 pub tool_examples: Option<String>,
254 #[serde(skip)]
258 pub post_turn_callback: Option<EqIgnored<VmValue>>,
259}
260
261#[derive(Clone, Debug, Default)]
263pub struct EqIgnored<T>(pub T);
264
265impl<T> PartialEq for EqIgnored<T> {
266 fn eq(&self, _: &Self) -> bool {
267 true
268 }
269}
270
271impl<T> std::ops::Deref for EqIgnored<T> {
272 type Target = T;
273 fn deref(&self) -> &T {
274 &self.0
275 }
276}
277
278#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
279#[serde(default)]
280pub struct TranscriptPolicy {
281 pub mode: Option<String>,
282 pub visibility: Option<String>,
283 pub summarize: bool,
284 pub compact: bool,
285 pub keep_last: Option<usize>,
286 pub auto_compact: bool,
288 pub compact_threshold: Option<usize>,
290 pub tool_output_max_chars: Option<usize>,
292 pub compact_strategy: Option<String>,
294 pub hard_limit_tokens: Option<usize>,
296 pub hard_limit_strategy: Option<String>,
298}
299
300#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
301#[serde(default)]
302pub struct ContextPolicy {
303 pub max_artifacts: Option<usize>,
304 pub max_tokens: Option<usize>,
305 pub reserve_tokens: Option<usize>,
306 pub include_kinds: Vec<String>,
307 pub exclude_kinds: Vec<String>,
308 pub prioritize_kinds: Vec<String>,
309 pub pinned_ids: Vec<String>,
310 pub include_stages: Vec<String>,
311 pub prefer_recent: bool,
312 pub prefer_fresh: bool,
313 pub render: Option<String>,
314}
315
316#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
317#[serde(default)]
318pub struct RetryPolicy {
319 pub max_attempts: usize,
320 pub verify: bool,
321 pub repair: bool,
322 #[serde(default)]
325 pub backoff_ms: Option<u64>,
326 #[serde(default)]
329 pub backoff_multiplier: Option<f64>,
330}
331
332#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
333#[serde(default)]
334pub struct StageContract {
335 pub input_kinds: Vec<String>,
336 pub output_kinds: Vec<String>,
337 pub min_inputs: Option<usize>,
338 pub max_inputs: Option<usize>,
339 pub require_transcript: bool,
340 pub schema: Option<serde_json::Value>,
341}
342
343#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
344#[serde(default)]
345pub struct BranchSemantics {
346 pub success: Option<String>,
347 pub failure: Option<String>,
348 pub verify_pass: Option<String>,
349 pub verify_fail: Option<String>,
350 pub condition_true: Option<String>,
351 pub condition_false: Option<String>,
352 pub loop_continue: Option<String>,
353 pub loop_exit: Option<String>,
354 pub escalation: Option<String>,
355}
356
357#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
358#[serde(default)]
359pub struct MapPolicy {
360 pub items: Vec<serde_json::Value>,
361 pub item_artifact_kind: Option<String>,
362 pub output_kind: Option<String>,
363 pub max_items: Option<usize>,
364}
365
366#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
367#[serde(default)]
368pub struct JoinPolicy {
369 pub strategy: String,
370 pub require_all_inputs: bool,
371 pub min_completed: Option<usize>,
372}
373
374#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
375#[serde(default)]
376pub struct ReducePolicy {
377 pub strategy: String,
378 pub separator: Option<String>,
379 pub output_kind: Option<String>,
380}
381
382#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
383#[serde(default)]
384pub struct EscalationPolicy {
385 pub level: Option<String>,
386 pub queue: Option<String>,
387 pub reason: Option<String>,
388}
389
390pub fn push_execution_policy(policy: CapabilityPolicy) {
393 EXECUTION_POLICY_STACK.with(|stack| stack.borrow_mut().push(policy));
394}
395
396pub fn pop_execution_policy() {
397 EXECUTION_POLICY_STACK.with(|stack| {
398 stack.borrow_mut().pop();
399 });
400}
401
402pub fn current_execution_policy() -> Option<CapabilityPolicy> {
403 EXECUTION_POLICY_STACK.with(|stack| stack.borrow().last().cloned())
404}
405
406pub fn current_tool_metadata(tool: &str) -> Option<ToolRuntimePolicyMetadata> {
407 current_execution_policy().and_then(|policy| policy.tool_metadata.get(tool).cloned())
408}
409
410fn policy_allows_tool(policy: &CapabilityPolicy, tool: &str) -> bool {
411 policy.tools.is_empty() || policy.tools.iter().any(|allowed| allowed == tool)
412}
413
414fn policy_allows_capability(policy: &CapabilityPolicy, capability: &str, op: &str) -> bool {
415 policy.capabilities.is_empty()
416 || policy
417 .capabilities
418 .get(capability)
419 .is_some_and(|ops| ops.is_empty() || ops.iter().any(|allowed| allowed == op))
420}
421
422fn policy_allows_side_effect(policy: &CapabilityPolicy, requested: &str) -> bool {
423 fn rank(v: &str) -> usize {
424 match v {
425 "none" => 0,
426 "read_only" => 1,
427 "workspace_write" => 2,
428 "process_exec" => 3,
429 "network" => 4,
430 _ => 5,
431 }
432 }
433 policy
434 .side_effect_level
435 .as_ref()
436 .map(|allowed| rank(allowed) >= rank(requested))
437 .unwrap_or(true)
438}
439
440fn reject_policy(reason: String) -> Result<(), VmError> {
441 Err(VmError::CategorizedError {
442 message: reason,
443 category: crate::value::ErrorCategory::ToolRejected,
444 })
445}
446
447fn fallback_mutation_classification(tool_name: &str) -> String {
448 let lower = tool_name.to_ascii_lowercase();
449 if lower.starts_with("mcp_") {
450 return "host_defined".to_string();
451 }
452 if lower == "exec"
453 || lower == "shell"
454 || lower == "exec_at"
455 || lower == "shell_at"
456 || lower == "run"
457 || lower.starts_with("run_")
458 {
459 return "ambient_side_effect".to_string();
460 }
461 if lower.starts_with("delete")
462 || lower.starts_with("remove")
463 || lower.starts_with("move")
464 || lower.starts_with("rename")
465 {
466 return "destructive".to_string();
467 }
468 if lower.contains("write")
469 || lower.contains("edit")
470 || lower.contains("patch")
471 || lower.contains("create")
472 || lower.contains("scaffold")
473 || lower.starts_with("insert")
474 || lower.starts_with("replace")
475 || lower == "add_import"
476 {
477 return "apply_workspace".to_string();
478 }
479 "read_only".to_string()
480}
481
482pub fn current_tool_mutation_classification(tool_name: &str) -> String {
483 current_tool_metadata(tool_name)
484 .and_then(|metadata| metadata.mutation_classification)
485 .unwrap_or_else(|| fallback_mutation_classification(tool_name))
486}
487
488pub fn current_tool_declared_paths(tool_name: &str, args: &serde_json::Value) -> Vec<String> {
489 let Some(map) = args.as_object() else {
490 return Vec::new();
491 };
492 let path_keys = current_tool_metadata(tool_name)
493 .map(|metadata| metadata.path_params)
494 .filter(|keys| !keys.is_empty())
495 .unwrap_or_else(|| {
496 vec![
497 "path".to_string(),
498 "file".to_string(),
499 "cwd".to_string(),
500 "repo".to_string(),
501 "target".to_string(),
502 "destination".to_string(),
503 ]
504 });
505 let mut paths = Vec::new();
506 for key in path_keys {
507 if let Some(value) = map.get(&key).and_then(|value| value.as_str()) {
508 if !value.is_empty() {
509 paths.push(value.to_string());
510 }
511 }
512 }
513 if let Some(items) = map.get("paths").and_then(|value| value.as_array()) {
514 for item in items {
515 if let Some(value) = item.as_str() {
516 if !value.is_empty() {
517 paths.push(value.to_string());
518 }
519 }
520 }
521 }
522 paths.sort();
523 paths.dedup();
524 paths
525}
526
527pub fn enforce_current_policy_for_builtin(name: &str, args: &[VmValue]) -> Result<(), VmError> {
528 let Some(policy) = current_execution_policy() else {
529 return Ok(());
530 };
531 match name {
532 "read" | "read_file" => {
533 if !policy_allows_tool(&policy, name)
534 || !policy_allows_capability(&policy, "workspace", "read_text")
535 {
536 return reject_policy(format!(
537 "builtin '{name}' exceeds workspace.read_text ceiling"
538 ));
539 }
540 }
541 "search" | "list_dir" => {
542 if !policy_allows_tool(&policy, name)
543 || !policy_allows_capability(&policy, "workspace", "list")
544 {
545 return reject_policy(format!("builtin '{name}' exceeds workspace.list ceiling"));
546 }
547 }
548 "file_exists" | "stat" => {
549 if !policy_allows_capability(&policy, "workspace", "exists") {
550 return reject_policy(format!("builtin '{name}' exceeds workspace.exists ceiling"));
551 }
552 }
553 "edit" | "write_file" | "append_file" | "mkdir" | "copy_file" => {
554 if !policy_allows_tool(&policy, "edit")
555 || !policy_allows_capability(&policy, "workspace", "write_text")
556 || !policy_allows_side_effect(&policy, "workspace_write")
557 {
558 return reject_policy(format!("builtin '{name}' exceeds workspace write ceiling"));
559 }
560 }
561 "delete_file" => {
562 if !policy_allows_capability(&policy, "workspace", "delete")
563 || !policy_allows_side_effect(&policy, "workspace_write")
564 {
565 return reject_policy(
566 "builtin 'delete_file' exceeds workspace.delete ceiling".to_string(),
567 );
568 }
569 }
570 "apply_edit" => {
571 if !policy_allows_capability(&policy, "workspace", "apply_edit")
572 || !policy_allows_side_effect(&policy, "workspace_write")
573 {
574 return reject_policy(
575 "builtin 'apply_edit' exceeds workspace.apply_edit ceiling".to_string(),
576 );
577 }
578 }
579 "exec" | "exec_at" | "shell" | "shell_at" | "run_command" => {
580 if !policy_allows_tool(&policy, "run")
581 || !policy_allows_capability(&policy, "process", "exec")
582 || !policy_allows_side_effect(&policy, "process_exec")
583 {
584 return reject_policy(format!("builtin '{name}' exceeds process.exec ceiling"));
585 }
586 }
587 "http_get" | "http_post" | "http_put" | "http_patch" | "http_delete" | "http_request" => {
588 if !policy_allows_side_effect(&policy, "network") {
589 return reject_policy(format!("builtin '{name}' exceeds network ceiling"));
590 }
591 }
592 "mcp_connect"
593 | "mcp_call"
594 | "mcp_list_tools"
595 | "mcp_list_resources"
596 | "mcp_list_resource_templates"
597 | "mcp_read_resource"
598 | "mcp_list_prompts"
599 | "mcp_get_prompt"
600 | "mcp_server_info"
601 | "mcp_disconnect" => {
602 if !policy_allows_tool(&policy, "run")
603 || !policy_allows_capability(&policy, "process", "exec")
604 || !policy_allows_side_effect(&policy, "process_exec")
605 {
606 return reject_policy(format!("builtin '{name}' exceeds process.exec ceiling"));
607 }
608 }
609 "host_call" => {
610 let name = args.first().map(|v| v.display()).unwrap_or_default();
611 let Some((capability, op)) = name.split_once('.') else {
612 return reject_policy(format!(
613 "host_call '{name}' must use capability.operation naming"
614 ));
615 };
616 if !policy_allows_capability(&policy, capability, op) {
617 return reject_policy(format!(
618 "host_call {capability}.{op} exceeds capability ceiling"
619 ));
620 }
621 let requested_side_effect = match (capability, op) {
622 ("workspace", "write_text" | "apply_edit" | "delete") => "workspace_write",
623 ("process", "exec") => "process_exec",
624 _ => "read_only",
625 };
626 if !policy_allows_side_effect(&policy, requested_side_effect) {
627 return reject_policy(format!(
628 "host_call {capability}.{op} exceeds side-effect ceiling"
629 ));
630 }
631 }
632 _ => {}
633 }
634 Ok(())
635}
636
637pub fn enforce_current_policy_for_bridge_builtin(name: &str) -> Result<(), VmError> {
638 if current_execution_policy().is_some() {
639 return reject_policy(format!(
640 "bridged builtin '{name}' exceeds execution policy; declare an explicit capability/tool surface instead"
641 ));
642 }
643 Ok(())
644}
645
646pub fn enforce_current_policy_for_tool(tool_name: &str) -> Result<(), VmError> {
647 let Some(policy) = current_execution_policy() else {
648 return Ok(());
649 };
650 if !policy_allows_tool(&policy, tool_name) {
651 return reject_policy(format!("tool '{tool_name}' exceeds tool ceiling"));
652 }
653 if let Some(metadata) = policy.tool_metadata.get(tool_name) {
654 for (capability, ops) in &metadata.capabilities {
655 for op in ops {
656 if !policy_allows_capability(&policy, capability, op) {
657 return reject_policy(format!(
658 "tool '{tool_name}' exceeds capability ceiling: {capability}.{op}"
659 ));
660 }
661 }
662 }
663 if let Some(side_effect_level) = metadata.side_effect_level.as_deref() {
664 if !policy_allows_side_effect(&policy, side_effect_level) {
665 return reject_policy(format!(
666 "tool '{tool_name}' exceeds side-effect ceiling: {side_effect_level}"
667 ));
668 }
669 }
670 }
671 Ok(())
672}
673
674fn compact_transcript(transcript: &VmValue, keep_last: usize) -> Option<VmValue> {
677 let dict = transcript.as_dict()?;
678 let messages = match dict.get("messages") {
679 Some(VmValue::List(list)) => list.iter().cloned().collect::<Vec<_>>(),
680 _ => Vec::new(),
681 };
682 let retained = messages
683 .into_iter()
684 .rev()
685 .take(keep_last)
686 .collect::<Vec<_>>()
687 .into_iter()
688 .rev()
689 .collect::<Vec<_>>();
690 let mut compacted = dict.clone();
691 compacted.insert(
692 "messages".to_string(),
693 VmValue::List(Rc::new(retained.clone())),
694 );
695 compacted.insert(
696 "events".to_string(),
697 VmValue::List(Rc::new(
698 crate::llm::helpers::transcript_events_from_messages(&retained),
699 )),
700 );
701 Some(VmValue::Dict(Rc::new(compacted)))
702}
703
704fn redact_transcript_visibility(transcript: &VmValue, visibility: Option<&str>) -> Option<VmValue> {
705 let Some(visibility) = visibility else {
706 return Some(transcript.clone());
707 };
708 if visibility != "public" && visibility != "public_only" {
709 return Some(transcript.clone());
710 }
711 let dict = transcript.as_dict()?;
712 let public_messages = match dict.get("messages") {
713 Some(VmValue::List(list)) => list
714 .iter()
715 .filter(|message| {
716 message
717 .as_dict()
718 .and_then(|d| d.get("role"))
719 .map(|v| v.display())
720 .map(|role| role != "tool_result")
721 .unwrap_or(true)
722 })
723 .cloned()
724 .collect::<Vec<_>>(),
725 _ => Vec::new(),
726 };
727 let public_events = match dict.get("events") {
728 Some(VmValue::List(list)) => list
729 .iter()
730 .filter(|event| {
731 event
732 .as_dict()
733 .and_then(|d| d.get("visibility"))
734 .map(|v| v.display())
735 .map(|value| value == "public")
736 .unwrap_or(true)
737 })
738 .cloned()
739 .collect::<Vec<_>>(),
740 _ => Vec::new(),
741 };
742 let mut redacted = dict.clone();
743 redacted.insert(
744 "messages".to_string(),
745 VmValue::List(Rc::new(public_messages)),
746 );
747 redacted.insert("events".to_string(), VmValue::List(Rc::new(public_events)));
748 Some(VmValue::Dict(Rc::new(redacted)))
749}
750
751pub(crate) fn apply_input_transcript_policy(
752 transcript: Option<VmValue>,
753 policy: &TranscriptPolicy,
754) -> Option<VmValue> {
755 let mut transcript = transcript;
756 match policy.mode.as_deref() {
757 Some("reset") => return None,
758 Some("fork") => {
759 if let Some(VmValue::Dict(dict)) = transcript.as_ref() {
760 let mut forked = dict.as_ref().clone();
761 forked.insert(
762 "id".to_string(),
763 VmValue::String(Rc::from(new_id("transcript"))),
764 );
765 transcript = Some(VmValue::Dict(Rc::new(forked)));
766 }
767 }
768 _ => {}
769 }
770 if policy.compact {
771 let keep_last = policy.keep_last.unwrap_or(6);
772 transcript = transcript.and_then(|value| compact_transcript(&value, keep_last));
773 }
774 transcript
775}
776
777pub(crate) fn apply_output_transcript_policy(
778 transcript: Option<VmValue>,
779 policy: &TranscriptPolicy,
780) -> Option<VmValue> {
781 let mut transcript = transcript;
782 if policy.compact {
783 let keep_last = policy.keep_last.unwrap_or(6);
784 transcript = transcript.and_then(|value| compact_transcript(&value, keep_last));
785 }
786 transcript.and_then(|value| redact_transcript_visibility(&value, policy.visibility.as_deref()))
787}
788
789pub fn builtin_ceiling() -> CapabilityPolicy {
790 CapabilityPolicy {
791 tools: Vec::new(),
795 capabilities: BTreeMap::new(),
796 workspace_roots: Vec::new(),
797 side_effect_level: Some("network".to_string()),
798 recursion_limit: Some(8),
799 tool_arg_constraints: Vec::new(),
800 tool_metadata: BTreeMap::new(),
801 }
802}