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 TurnPolicy {
238 pub require_action_or_yield: bool,
242 pub max_prose_chars: Option<usize>,
246}
247
248#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
249#[serde(default)]
250pub struct ModelPolicy {
251 pub provider: Option<String>,
252 pub model: Option<String>,
253 pub model_tier: Option<String>,
254 pub temperature: Option<f64>,
255 pub max_tokens: Option<i64>,
256 pub max_iterations: Option<usize>,
258 pub max_nudges: Option<usize>,
260 pub nudge: Option<String>,
263 pub tool_examples: Option<String>,
267 #[serde(skip)]
272 pub post_turn_callback: Option<EqIgnored<VmValue>>,
273 pub stop_after_successful_tools: Option<Vec<String>>,
278 pub turn_policy: Option<TurnPolicy>,
280}
281
282#[derive(Clone, Debug, Default)]
284pub struct EqIgnored<T>(pub T);
285
286impl<T> PartialEq for EqIgnored<T> {
287 fn eq(&self, _: &Self) -> bool {
288 true
289 }
290}
291
292impl<T> std::ops::Deref for EqIgnored<T> {
293 type Target = T;
294 fn deref(&self) -> &T {
295 &self.0
296 }
297}
298
299#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
300#[serde(default)]
301pub struct TranscriptPolicy {
302 pub mode: Option<String>,
303 pub visibility: Option<String>,
304 pub summarize: bool,
305 pub compact: bool,
306 pub keep_last: Option<usize>,
307 pub auto_compact: bool,
309 pub compact_threshold: Option<usize>,
311 pub tool_output_max_chars: Option<usize>,
313 pub compact_strategy: Option<String>,
315 pub hard_limit_tokens: Option<usize>,
317 pub hard_limit_strategy: Option<String>,
319}
320
321#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
322#[serde(default)]
323pub struct ContextPolicy {
324 pub max_artifacts: Option<usize>,
325 pub max_tokens: Option<usize>,
326 pub reserve_tokens: Option<usize>,
327 pub include_kinds: Vec<String>,
328 pub exclude_kinds: Vec<String>,
329 pub prioritize_kinds: Vec<String>,
330 pub pinned_ids: Vec<String>,
331 pub include_stages: Vec<String>,
332 pub prefer_recent: bool,
333 pub prefer_fresh: bool,
334 pub render: Option<String>,
335}
336
337#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
338#[serde(default)]
339pub struct RetryPolicy {
340 pub max_attempts: usize,
341 pub verify: bool,
342 pub repair: bool,
343 #[serde(default)]
346 pub backoff_ms: Option<u64>,
347 #[serde(default)]
350 pub backoff_multiplier: Option<f64>,
351}
352
353#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
354#[serde(default)]
355pub struct StageContract {
356 pub input_kinds: Vec<String>,
357 pub output_kinds: Vec<String>,
358 pub min_inputs: Option<usize>,
359 pub max_inputs: Option<usize>,
360 pub require_transcript: bool,
361 pub schema: Option<serde_json::Value>,
362}
363
364#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
365#[serde(default)]
366pub struct BranchSemantics {
367 pub success: Option<String>,
368 pub failure: Option<String>,
369 pub verify_pass: Option<String>,
370 pub verify_fail: Option<String>,
371 pub condition_true: Option<String>,
372 pub condition_false: Option<String>,
373 pub loop_continue: Option<String>,
374 pub loop_exit: Option<String>,
375 pub escalation: Option<String>,
376}
377
378#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
379#[serde(default)]
380pub struct MapPolicy {
381 pub items: Vec<serde_json::Value>,
382 pub item_artifact_kind: Option<String>,
383 pub output_kind: Option<String>,
384 pub max_items: Option<usize>,
385 pub max_concurrent: Option<usize>,
386}
387
388#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
389#[serde(default)]
390pub struct JoinPolicy {
391 pub strategy: String,
392 pub require_all_inputs: bool,
393 pub min_completed: Option<usize>,
394}
395
396#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
397#[serde(default)]
398pub struct ReducePolicy {
399 pub strategy: String,
400 pub separator: Option<String>,
401 pub output_kind: Option<String>,
402}
403
404#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
405#[serde(default)]
406pub struct EscalationPolicy {
407 pub level: Option<String>,
408 pub queue: Option<String>,
409 pub reason: Option<String>,
410}
411
412pub fn push_execution_policy(policy: CapabilityPolicy) {
415 EXECUTION_POLICY_STACK.with(|stack| stack.borrow_mut().push(policy));
416}
417
418pub fn pop_execution_policy() {
419 EXECUTION_POLICY_STACK.with(|stack| {
420 stack.borrow_mut().pop();
421 });
422}
423
424pub fn current_execution_policy() -> Option<CapabilityPolicy> {
425 EXECUTION_POLICY_STACK.with(|stack| stack.borrow().last().cloned())
426}
427
428pub fn current_tool_metadata(tool: &str) -> Option<ToolRuntimePolicyMetadata> {
429 current_execution_policy().and_then(|policy| policy.tool_metadata.get(tool).cloned())
430}
431
432fn policy_allows_tool(policy: &CapabilityPolicy, tool: &str) -> bool {
433 policy.tools.is_empty() || policy.tools.iter().any(|allowed| allowed == tool)
434}
435
436fn policy_allows_capability(policy: &CapabilityPolicy, capability: &str, op: &str) -> bool {
437 policy.capabilities.is_empty()
438 || policy
439 .capabilities
440 .get(capability)
441 .is_some_and(|ops| ops.is_empty() || ops.iter().any(|allowed| allowed == op))
442}
443
444fn policy_allows_side_effect(policy: &CapabilityPolicy, requested: &str) -> bool {
445 fn rank(v: &str) -> usize {
446 match v {
447 "none" => 0,
448 "read_only" => 1,
449 "workspace_write" => 2,
450 "process_exec" => 3,
451 "network" => 4,
452 _ => 5,
453 }
454 }
455 policy
456 .side_effect_level
457 .as_ref()
458 .map(|allowed| rank(allowed) >= rank(requested))
459 .unwrap_or(true)
460}
461
462fn reject_policy(reason: String) -> Result<(), VmError> {
463 Err(VmError::CategorizedError {
464 message: reason,
465 category: crate::value::ErrorCategory::ToolRejected,
466 })
467}
468
469fn fallback_mutation_classification(tool_name: &str) -> String {
470 let lower = tool_name.to_ascii_lowercase();
471 if lower.starts_with("mcp_") {
472 return "host_defined".to_string();
473 }
474 if lower == "exec"
475 || lower == "shell"
476 || lower == "exec_at"
477 || lower == "shell_at"
478 || lower == "run"
479 || lower.starts_with("run_")
480 {
481 return "ambient_side_effect".to_string();
482 }
483 if lower.starts_with("delete")
484 || lower.starts_with("remove")
485 || lower.starts_with("move")
486 || lower.starts_with("rename")
487 {
488 return "destructive".to_string();
489 }
490 if lower.contains("write")
491 || lower.contains("edit")
492 || lower.contains("patch")
493 || lower.contains("create")
494 || lower.contains("scaffold")
495 || lower.starts_with("insert")
496 || lower.starts_with("replace")
497 || lower == "add_import"
498 {
499 return "apply_workspace".to_string();
500 }
501 "read_only".to_string()
502}
503
504pub fn current_tool_mutation_classification(tool_name: &str) -> String {
505 current_tool_metadata(tool_name)
506 .and_then(|metadata| metadata.mutation_classification)
507 .unwrap_or_else(|| fallback_mutation_classification(tool_name))
508}
509
510pub fn current_tool_declared_paths(tool_name: &str, args: &serde_json::Value) -> Vec<String> {
511 let Some(map) = args.as_object() else {
512 return Vec::new();
513 };
514 let path_keys = current_tool_metadata(tool_name)
515 .map(|metadata| metadata.path_params)
516 .filter(|keys| !keys.is_empty())
517 .unwrap_or_else(|| {
518 vec![
519 "path".to_string(),
520 "file".to_string(),
521 "cwd".to_string(),
522 "repo".to_string(),
523 "target".to_string(),
524 "destination".to_string(),
525 ]
526 });
527 let mut paths = Vec::new();
528 for key in path_keys {
529 if let Some(value) = map.get(&key).and_then(|value| value.as_str()) {
530 if !value.is_empty() {
531 paths.push(value.to_string());
532 }
533 }
534 }
535 if let Some(items) = map.get("paths").and_then(|value| value.as_array()) {
536 for item in items {
537 if let Some(value) = item.as_str() {
538 if !value.is_empty() {
539 paths.push(value.to_string());
540 }
541 }
542 }
543 }
544 paths.sort();
545 paths.dedup();
546 paths
547}
548
549pub fn enforce_current_policy_for_builtin(name: &str, args: &[VmValue]) -> Result<(), VmError> {
550 let Some(policy) = current_execution_policy() else {
551 return Ok(());
552 };
553 match name {
554 "read" | "read_file" => {
555 if !policy_allows_tool(&policy, name)
556 || !policy_allows_capability(&policy, "workspace", "read_text")
557 {
558 return reject_policy(format!(
559 "builtin '{name}' exceeds workspace.read_text ceiling"
560 ));
561 }
562 }
563 "search" | "list_dir" => {
564 if !policy_allows_tool(&policy, name)
565 || !policy_allows_capability(&policy, "workspace", "list")
566 {
567 return reject_policy(format!("builtin '{name}' exceeds workspace.list ceiling"));
568 }
569 }
570 "file_exists" | "stat" => {
571 if !policy_allows_capability(&policy, "workspace", "exists") {
572 return reject_policy(format!("builtin '{name}' exceeds workspace.exists ceiling"));
573 }
574 }
575 "edit" | "write_file" | "append_file" | "mkdir" | "copy_file" => {
576 if !policy_allows_tool(&policy, "edit")
577 || !policy_allows_capability(&policy, "workspace", "write_text")
578 || !policy_allows_side_effect(&policy, "workspace_write")
579 {
580 return reject_policy(format!("builtin '{name}' exceeds workspace write ceiling"));
581 }
582 }
583 "delete_file" => {
584 if !policy_allows_capability(&policy, "workspace", "delete")
585 || !policy_allows_side_effect(&policy, "workspace_write")
586 {
587 return reject_policy(
588 "builtin 'delete_file' exceeds workspace.delete ceiling".to_string(),
589 );
590 }
591 }
592 "apply_edit" => {
593 if !policy_allows_capability(&policy, "workspace", "apply_edit")
594 || !policy_allows_side_effect(&policy, "workspace_write")
595 {
596 return reject_policy(
597 "builtin 'apply_edit' exceeds workspace.apply_edit ceiling".to_string(),
598 );
599 }
600 }
601 "exec" | "exec_at" | "shell" | "shell_at" | "run_command" => {
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 "http_get" | "http_post" | "http_put" | "http_patch" | "http_delete" | "http_request" => {
610 if !policy_allows_side_effect(&policy, "network") {
611 return reject_policy(format!("builtin '{name}' exceeds network ceiling"));
612 }
613 }
614 "mcp_connect"
615 | "mcp_call"
616 | "mcp_list_tools"
617 | "mcp_list_resources"
618 | "mcp_list_resource_templates"
619 | "mcp_read_resource"
620 | "mcp_list_prompts"
621 | "mcp_get_prompt"
622 | "mcp_server_info"
623 | "mcp_disconnect" => {
624 if !policy_allows_tool(&policy, "run")
625 || !policy_allows_capability(&policy, "process", "exec")
626 || !policy_allows_side_effect(&policy, "process_exec")
627 {
628 return reject_policy(format!("builtin '{name}' exceeds process.exec ceiling"));
629 }
630 }
631 "host_call" => {
632 let name = args.first().map(|v| v.display()).unwrap_or_default();
633 let Some((capability, op)) = name.split_once('.') else {
634 return reject_policy(format!(
635 "host_call '{name}' must use capability.operation naming"
636 ));
637 };
638 if !policy_allows_capability(&policy, capability, op) {
639 return reject_policy(format!(
640 "host_call {capability}.{op} exceeds capability ceiling"
641 ));
642 }
643 let requested_side_effect = match (capability, op) {
644 ("workspace", "write_text" | "apply_edit" | "delete") => "workspace_write",
645 ("process", "exec") => "process_exec",
646 _ => "read_only",
647 };
648 if !policy_allows_side_effect(&policy, requested_side_effect) {
649 return reject_policy(format!(
650 "host_call {capability}.{op} exceeds side-effect ceiling"
651 ));
652 }
653 }
654 _ => {}
655 }
656 Ok(())
657}
658
659pub fn enforce_current_policy_for_bridge_builtin(name: &str) -> Result<(), VmError> {
660 if current_execution_policy().is_some() {
661 return reject_policy(format!(
662 "bridged builtin '{name}' exceeds execution policy; declare an explicit capability/tool surface instead"
663 ));
664 }
665 Ok(())
666}
667
668pub fn enforce_current_policy_for_tool(tool_name: &str) -> Result<(), VmError> {
669 let Some(policy) = current_execution_policy() else {
670 return Ok(());
671 };
672 if !policy_allows_tool(&policy, tool_name) {
673 return reject_policy(format!("tool '{tool_name}' exceeds tool ceiling"));
674 }
675 if let Some(metadata) = policy.tool_metadata.get(tool_name) {
676 for (capability, ops) in &metadata.capabilities {
677 for op in ops {
678 if !policy_allows_capability(&policy, capability, op) {
679 return reject_policy(format!(
680 "tool '{tool_name}' exceeds capability ceiling: {capability}.{op}"
681 ));
682 }
683 }
684 }
685 if let Some(side_effect_level) = metadata.side_effect_level.as_deref() {
686 if !policy_allows_side_effect(&policy, side_effect_level) {
687 return reject_policy(format!(
688 "tool '{tool_name}' exceeds side-effect ceiling: {side_effect_level}"
689 ));
690 }
691 }
692 }
693 Ok(())
694}
695
696fn compact_transcript(transcript: &VmValue, keep_last: usize) -> Option<VmValue> {
699 let dict = transcript.as_dict()?;
700 let messages = match dict.get("messages") {
701 Some(VmValue::List(list)) => list.iter().cloned().collect::<Vec<_>>(),
702 _ => Vec::new(),
703 };
704 let retained = messages
705 .into_iter()
706 .rev()
707 .take(keep_last)
708 .collect::<Vec<_>>()
709 .into_iter()
710 .rev()
711 .collect::<Vec<_>>();
712 let mut compacted = dict.clone();
713 compacted.insert(
714 "messages".to_string(),
715 VmValue::List(Rc::new(retained.clone())),
716 );
717 compacted.insert(
718 "events".to_string(),
719 VmValue::List(Rc::new(
720 crate::llm::helpers::transcript_events_from_messages(&retained),
721 )),
722 );
723 Some(VmValue::Dict(Rc::new(compacted)))
724}
725
726fn redact_transcript_visibility(transcript: &VmValue, visibility: Option<&str>) -> Option<VmValue> {
727 let Some(visibility) = visibility else {
728 return Some(transcript.clone());
729 };
730 if visibility != "public" && visibility != "public_only" {
731 return Some(transcript.clone());
732 }
733 let dict = transcript.as_dict()?;
734 let public_messages = match dict.get("messages") {
735 Some(VmValue::List(list)) => list
736 .iter()
737 .filter(|message| {
738 message
739 .as_dict()
740 .and_then(|d| d.get("role"))
741 .map(|v| v.display())
742 .map(|role| role != "tool_result")
743 .unwrap_or(true)
744 })
745 .cloned()
746 .collect::<Vec<_>>(),
747 _ => Vec::new(),
748 };
749 let public_events = match dict.get("events") {
750 Some(VmValue::List(list)) => list
751 .iter()
752 .filter(|event| {
753 event
754 .as_dict()
755 .and_then(|d| d.get("visibility"))
756 .map(|v| v.display())
757 .map(|value| value == "public")
758 .unwrap_or(true)
759 })
760 .cloned()
761 .collect::<Vec<_>>(),
762 _ => Vec::new(),
763 };
764 let mut redacted = dict.clone();
765 redacted.insert(
766 "messages".to_string(),
767 VmValue::List(Rc::new(public_messages)),
768 );
769 redacted.insert("events".to_string(), VmValue::List(Rc::new(public_events)));
770 Some(VmValue::Dict(Rc::new(redacted)))
771}
772
773pub(crate) fn apply_input_transcript_policy(
774 transcript: Option<VmValue>,
775 policy: &TranscriptPolicy,
776) -> Option<VmValue> {
777 let mut transcript = transcript;
778 match policy.mode.as_deref() {
779 Some("reset") => return None,
780 Some("fork") => {
781 if let Some(VmValue::Dict(dict)) = transcript.as_ref() {
782 let mut forked = dict.as_ref().clone();
783 forked.insert(
784 "id".to_string(),
785 VmValue::String(Rc::from(new_id("transcript"))),
786 );
787 transcript = Some(VmValue::Dict(Rc::new(forked)));
788 }
789 }
790 _ => {}
791 }
792 if policy.compact {
793 let keep_last = policy.keep_last.unwrap_or(6);
794 transcript = transcript.and_then(|value| compact_transcript(&value, keep_last));
795 }
796 transcript
797}
798
799pub(crate) fn apply_output_transcript_policy(
800 transcript: Option<VmValue>,
801 policy: &TranscriptPolicy,
802) -> Option<VmValue> {
803 let mut transcript = transcript;
804 if policy.compact {
805 let keep_last = policy.keep_last.unwrap_or(6);
806 transcript = transcript.and_then(|value| compact_transcript(&value, keep_last));
807 }
808 transcript.and_then(|value| redact_transcript_visibility(&value, policy.visibility.as_deref()))
809}
810
811pub fn builtin_ceiling() -> CapabilityPolicy {
812 CapabilityPolicy {
813 tools: Vec::new(),
817 capabilities: BTreeMap::new(),
818 workspace_roots: Vec::new(),
819 side_effect_level: Some("network".to_string()),
820 recursion_limit: Some(8),
821 tool_arg_constraints: Vec::new(),
822 tool_metadata: BTreeMap::new(),
823 }
824}