1mod approval_rules;
4mod effects;
5mod nested_budget;
6mod operator_grant;
7mod types;
8
9use crate::value::VmDictExt;
10use std::cell::RefCell;
11use std::collections::BTreeMap;
12use std::thread_local;
13
14use serde::{Deserialize, Serialize};
15
16use crate::runtime_limits::RuntimeLimits;
17use crate::tool_annotations::{SideEffectLevel, ToolAnnotations};
18use crate::value::{VmError, VmValue};
19use crate::workspace_path::{classify_workspace_path, WorkspacePathInfo};
20
21pub use crate::tool_annotations::{ToolArgSchema, ToolKind};
22pub use approval_rules::{
23 clear_all_approval_policy_repeat_counts, clear_approval_policy_repeat_counts,
24 next_approval_policy_repeat_count, next_approval_unavailable_class_repeat_count, ApprovalShape,
25 PolicyAction, PolicyEvaluation, PolicyMatchedRule, PolicyRule, PolicyRuleMatch,
26 ToolApprovalRequest,
27};
28pub use effects::{
29 compute_handoff_effects, effect_kind_label, effect_record_summary, effect_subset_violations,
30 effects_from_metadata, EffectKind, EffectRecord, EffectScope,
31};
32pub use nested_budget::{
33 annotate_nested_execution_options, enter_nested_execution_policy, NestedExecutionGuard,
34 NestedExecutionKind, NESTED_KIND_OPTION_KEY, NESTED_LABEL_OPTION_KEY,
35};
36pub(crate) use operator_grant::{
37 clear_operator_approval_grants, swap_operator_approval_grant_stack,
38};
39pub use operator_grant::{
40 current_operator_approval_grant, install_operator_approval_grant, OperatorApprovalGrant,
41 OperatorApprovalGrantGuard,
42};
43pub use types::{
44 enforce_tool_arg_constraints, AutoCompactPolicy, BranchSemantics, CapabilityPolicy,
45 ContextPolicy, EqIgnored, EscalationPolicy, FeedbackBounds, FeedbackPolicy, JoinPolicy,
46 MapPolicy, ModelPolicy, NativeToolFallbackPolicy, ProcessSandboxPolicy, ProcessSandboxPreset,
47 ReducePolicy, RequiredSuccessfulTool, RetryPolicy, SandboxProfile, StageContract,
48 ToolArgConstraint, TurnPolicy,
49};
50
51thread_local! {
52 static EXECUTION_POLICY_STACK: RefCell<Vec<CapabilityPolicy>> = const { RefCell::new(Vec::new()) };
53 static EXECUTION_APPROVAL_POLICY_STACK: RefCell<Vec<ToolApprovalPolicy>> = const { RefCell::new(Vec::new()) };
54 static TRUSTED_BRIDGE_CALL_DEPTH: RefCell<usize> = const { RefCell::new(0) };
55}
56
57pub fn push_execution_policy(policy: CapabilityPolicy) {
58 EXECUTION_POLICY_STACK.with(|stack| stack.borrow_mut().push(policy));
59}
60
61pub fn pop_execution_policy() {
62 EXECUTION_POLICY_STACK.with(|stack| {
63 stack.borrow_mut().pop();
64 });
65}
66
67pub fn clear_execution_policy_stacks() {
68 EXECUTION_POLICY_STACK.with(|stack| stack.borrow_mut().clear());
69 EXECUTION_APPROVAL_POLICY_STACK.with(|stack| stack.borrow_mut().clear());
70 clear_operator_approval_grants();
71 TRUSTED_BRIDGE_CALL_DEPTH.with(|depth| *depth.borrow_mut() = 0);
72}
73
74pub fn current_execution_policy() -> Option<CapabilityPolicy> {
75 EXECUTION_POLICY_STACK.with(|stack| stack.borrow().last().cloned())
76}
77
78pub fn execution_policy_active() -> bool {
83 EXECUTION_POLICY_STACK.with(|stack| !stack.borrow().is_empty())
84}
85
86pub fn push_approval_policy(policy: ToolApprovalPolicy) {
87 EXECUTION_APPROVAL_POLICY_STACK.with(|stack| stack.borrow_mut().push(policy));
88}
89
90pub fn pop_approval_policy() {
91 EXECUTION_APPROVAL_POLICY_STACK.with(|stack| {
92 stack.borrow_mut().pop();
93 });
94}
95
96pub fn current_approval_policy() -> Option<ToolApprovalPolicy> {
97 EXECUTION_APPROVAL_POLICY_STACK.with(|stack| stack.borrow().last().cloned())
98}
99
100pub(crate) fn swap_execution_policy_stack(next: Vec<CapabilityPolicy>) -> Vec<CapabilityPolicy> {
113 EXECUTION_POLICY_STACK.with(|stack| std::mem::replace(&mut *stack.borrow_mut(), next))
114}
115
116pub(crate) fn swap_approval_policy_stack(next: Vec<ToolApprovalPolicy>) -> Vec<ToolApprovalPolicy> {
117 EXECUTION_APPROVAL_POLICY_STACK.with(|stack| std::mem::replace(&mut *stack.borrow_mut(), next))
118}
119
120pub(crate) fn swap_trusted_bridge_depth(next: usize) -> usize {
121 TRUSTED_BRIDGE_CALL_DEPTH.with(|depth| std::mem::replace(&mut *depth.borrow_mut(), next))
122}
123
124pub fn current_tool_annotations(tool: &str) -> Option<ToolAnnotations> {
125 current_execution_policy().and_then(|policy| policy.tool_annotations.get(tool).cloned())
126}
127
128pub fn current_allowed_tool_names() -> Vec<String> {
136 let Some(policy) = current_execution_policy() else {
137 return Vec::new();
138 };
139 if policy.tools_are_restricted() {
140 return policy.allowed_tool_patterns().map(str::to_string).collect();
141 }
142 policy.tool_annotations.keys().cloned().collect()
143}
144
145pub(super) fn tool_kind_participates_in_write_allowlist(tool_name: &str) -> bool {
146 current_tool_annotations(tool_name)
147 .map(|annotations| !annotations.kind.is_read_only())
148 .unwrap_or(true)
149}
150
151pub struct TrustedBridgeCallGuard;
152
153pub fn allow_trusted_bridge_calls() -> TrustedBridgeCallGuard {
154 TRUSTED_BRIDGE_CALL_DEPTH.with(|depth| {
155 *depth.borrow_mut() += 1;
156 });
157 TrustedBridgeCallGuard
158}
159
160impl Drop for TrustedBridgeCallGuard {
161 fn drop(&mut self) {
162 TRUSTED_BRIDGE_CALL_DEPTH.with(|depth| {
163 let mut depth = depth.borrow_mut();
164 *depth = depth.saturating_sub(1);
165 });
166 }
167}
168
169fn policy_allows_tool(policy: &CapabilityPolicy, tool: &str) -> bool {
170 policy.tool_pattern_allows(tool)
171}
172
173fn policy_grants_capability(policy: &CapabilityPolicy, capability: &str, op: &str) -> bool {
174 policy
175 .capabilities
176 .get(capability)
177 .is_some_and(|ops| ops.is_empty() || ops.iter().any(|allowed| allowed == op))
178}
179
180fn policy_allows_capability(policy: &CapabilityPolicy, capability: &str, op: &str) -> bool {
181 if !policy.capabilities_are_restricted() {
182 return true;
184 }
185 if policy.capabilities_deny_all() {
186 return false;
187 }
188 if policy_grants_capability(policy, capability, op) {
189 return true;
190 }
191 if capability == "workspace" && op == "exists" {
203 return policy_grants_capability(policy, "workspace", "read_text")
204 || policy_grants_capability(policy, "workspace", "list");
205 }
206 false
207}
208
209fn policy_allows_side_effect(policy: &CapabilityPolicy, requested: &str) -> bool {
210 let requested_rank = SideEffectLevel::rank_str(requested);
216 policy
217 .side_effect_level
218 .as_ref()
219 .map(|allowed| SideEffectLevel::rank_str(allowed) >= requested_rank)
220 .unwrap_or(true)
221}
222
223pub(super) fn reject_policy(reason: String) -> Result<(), VmError> {
224 Err(VmError::CategorizedError {
225 message: reason,
226 category: crate::value::ErrorCategory::ToolRejected,
227 })
228}
229
230#[derive(Clone, Debug, PartialEq, Eq)]
237pub struct PolicyDenial {
238 pub gate: crate::agent_events::DenialGate,
239 pub capability: Option<String>,
240 pub reason: String,
241 pub side_effect_ceiling: Option<SideEffectCeilingViolation>,
246}
247
248#[derive(Clone, Copy, Debug, Eq, PartialEq)]
252pub struct SideEffectCeilingViolation {
253 pub ceiling: SideEffectLevel,
254 pub required_level: SideEffectLevel,
255}
256
257#[derive(Clone, Debug, Eq, PartialEq)]
261pub(crate) struct SideEffectCeilingGrant {
262 tool_name: String,
263 violation: SideEffectCeilingViolation,
264}
265
266impl PolicyDenial {
267 pub(crate) fn side_effect_grant_for(&self, tool_name: &str) -> Option<SideEffectCeilingGrant> {
269 self.side_effect_ceiling
270 .map(|violation| SideEffectCeilingGrant {
271 tool_name: tool_name.to_string(),
272 violation,
273 })
274 }
275}
276
277impl SideEffectCeilingGrant {
278 fn matches(&self, tool_name: &str, violation: SideEffectCeilingViolation) -> bool {
279 self.tool_name == tool_name && self.violation == violation
280 }
281}
282
283impl From<PolicyDenial> for VmError {
284 fn from(denial: PolicyDenial) -> Self {
285 VmError::CategorizedError {
286 message: denial.reason,
287 category: crate::value::ErrorCategory::ToolRejected,
288 }
289 }
290}
291
292pub(super) fn reject_tool(
293 gate: crate::agent_events::DenialGate,
294 capability: Option<String>,
295 particulars: String,
296) -> Result<(), PolicyDenial> {
297 Err(PolicyDenial {
298 gate,
299 capability,
300 reason: gate.render_reason(particulars),
301 side_effect_ceiling: None,
302 })
303}
304
305pub fn current_tool_mutation_classification(tool_name: &str) -> String {
310 current_tool_annotations(tool_name)
311 .map(|annotations| annotations.kind.mutation_class().to_string())
312 .unwrap_or_else(|| "other".to_string())
313}
314
315pub fn current_tool_declared_paths(tool_name: &str, args: &serde_json::Value) -> Vec<String> {
319 current_tool_declared_path_entries(tool_name, args)
320 .into_iter()
321 .map(|entry| entry.display_path().to_string())
322 .collect()
323}
324
325pub fn current_tool_declared_path_entries(
330 tool_name: &str,
331 args: &serde_json::Value,
332) -> Vec<WorkspacePathInfo> {
333 let Some(annotations) = current_tool_annotations(tool_name) else {
334 return Vec::new();
335 };
336 tool_declared_path_entries(&annotations, args)
337}
338
339pub fn tool_declared_path_entries(
345 annotations: &crate::tool_annotations::ToolAnnotations,
346 args: &serde_json::Value,
347) -> Vec<WorkspacePathInfo> {
348 let Some(map) = args.as_object() else {
349 return Vec::new();
350 };
351 let workspace_root = crate::stdlib::process::execution_root_path();
352 let mut entries = Vec::new();
353 for key in &annotations.arg_schema.path_params {
354 if let Some(value) = map.get(key) {
355 match value {
356 serde_json::Value::String(path) if !path.is_empty() => {
357 entries.push(classify_workspace_path(path, Some(&workspace_root)));
358 }
359 serde_json::Value::Array(items) => {
360 for item in items.iter().filter_map(|item| item.as_str()) {
361 if !item.is_empty() {
362 entries.push(classify_workspace_path(item, Some(&workspace_root)));
363 }
364 }
365 }
366 _ => {}
367 }
368 }
369 }
370 entries.sort_by(|a, b| a.display_path().cmp(b.display_path()));
371 entries.dedup_by(|left, right| left.policy_candidates() == right.policy_candidates());
372 entries
373}
374
375pub fn enforce_current_policy_for_builtin(name: &str, args: &[VmValue]) -> Result<(), VmError> {
376 let Some(policy) = current_execution_policy() else {
377 return Ok(());
378 };
379 if effects::builtin_has_network_effect(name)
380 && (!policy_allows_capability(&policy, "network", "http")
381 || !policy_allows_side_effect(&policy, "network"))
382 {
383 return reject_policy(format!("builtin '{name}' exceeds network.http ceiling"));
384 }
385 match name {
386 "find_text" | "find_evidence"
387 if !policy_allows_capability(&policy, "workspace", "read_text")
388 || !policy_allows_capability(&policy, "workspace", "list") =>
389 {
390 return reject_policy(format!(
391 "builtin '{name}' exceeds workspace.read_text/workspace.list ceiling"
392 ));
393 }
394 "read_file"
395 | "read_file_result"
396 | "read_file_bytes"
397 | "package_snapshot_open"
398 | "render"
399 | "render_prompt"
400 | "render_with_provenance"
401 | "read_lines"
402 if !policy_allows_capability(&policy, "workspace", "read_text") =>
403 {
404 return reject_policy(format!(
405 "builtin '{name}' exceeds workspace.read_text ceiling"
406 ));
407 }
408 "list_dir" | "walk_dir" | "glob"
409 if !policy_allows_capability(&policy, "workspace", "list") =>
410 {
411 return reject_policy(format!("builtin '{name}' exceeds workspace.list ceiling"));
412 }
413 "file_exists" | "path_status" | "stat"
414 if !policy_allows_capability(&policy, "workspace", "exists") =>
415 {
416 return reject_policy(format!("builtin '{name}' exceeds workspace.exists ceiling"));
417 }
418 "write_file"
419 | "write_file_bytes"
420 | "replace_file"
421 | "replace_file_result"
422 | "replace_file_bytes"
423 | "replace_file_bytes_result"
424 | "append_file"
425 | "append_file_locked"
426 | "mkdir"
427 | "copy_file"
428 | "move_file"
429 if !policy_allows_capability(&policy, "workspace", "write_text")
430 || !policy_allows_side_effect(&policy, "workspace_write") =>
431 {
432 return reject_policy(format!("builtin '{name}' exceeds workspace write ceiling"));
433 }
434 "delete_file"
435 if !policy_allows_capability(&policy, "workspace", "delete")
436 || !policy_allows_side_effect(&policy, "workspace_write") =>
437 {
438 return reject_policy(
439 "builtin 'delete_file' exceeds workspace.delete ceiling".to_string(),
440 );
441 }
442 "apply_edit"
443 if !policy_allows_capability(&policy, "workspace", "apply_edit")
444 || !policy_allows_side_effect(&policy, "workspace_write") =>
445 {
446 return reject_policy(
447 "builtin 'apply_edit' exceeds workspace.apply_edit ceiling".to_string(),
448 );
449 }
450 "exec"
451 | "exec_at"
452 | "shell"
453 | "shell_at"
454 | "git.repo.discover"
455 | "git.worktree.create"
456 | "git.worktree.remove"
457 | "git.fetch"
458 | "git.rebase"
459 | "git.status"
460 | "git.conflicts"
461 | "git.push"
462 | "git.diff"
463 | "git.merge_base"
464 | "git.tag_list"
465 | "git.describe"
466 | "git.ls_remote"
467 if !policy_allows_capability(&policy, "process", "exec")
468 || !policy_allows_side_effect(&policy, "process_exec") =>
469 {
470 return reject_policy(format!("builtin '{name}' exceeds process.exec ceiling"));
471 }
472 "__files_upload" if !policy_allows_capability(&policy, "workspace", "read_text") => {
477 return reject_policy(
478 "builtin '__files_upload' exceeds workspace.read_text/network ceiling".to_string(),
479 );
480 }
481 "llm_call" | "llm_call_safe" | "llm_completion" | "llm_stream" | "llm_stream_call"
482 | "llm_healthcheck" | "agent_loop"
483 if !policy_allows_capability(&policy, "llm", "call") =>
484 {
485 return reject_policy(format!("builtin '{name}' exceeds llm.call ceiling"));
486 }
487 "connector_call"
488 if !policy_allows_capability(&policy, "connector", "call")
489 || !policy_allows_side_effect(&policy, "network") =>
490 {
491 return reject_policy(
492 "builtin 'connector_call' exceeds connector.call/network ceiling".to_string(),
493 );
494 }
495 "secret_get" if !policy_allows_capability(&policy, "connector", "secret_get") => {
496 return reject_policy(
497 "builtin 'secret_get' exceeds connector.secret_get ceiling".to_string(),
498 );
499 }
500 "event_log_emit" if !policy_allows_capability(&policy, "connector", "event_log_emit") => {
501 return reject_policy(
502 "builtin 'event_log_emit' exceeds connector.event_log_emit ceiling".to_string(),
503 );
504 }
505 "metrics_inc" if !policy_allows_capability(&policy, "connector", "metrics_inc") => {
506 return reject_policy(
507 "builtin 'metrics_inc' exceeds connector.metrics_inc ceiling".to_string(),
508 );
509 }
510 "project_fingerprint"
511 | "project_context_profile_native"
512 | "project_scan_native"
513 | "project_scan_tree_native"
514 | "project_walk_tree_native"
515 | "project_catalog_native"
516 if !policy_allows_capability(&policy, "workspace", "list")
517 || !policy_allows_side_effect(&policy, "read_only") =>
518 {
519 return reject_policy(format!("builtin '{name}' exceeds workspace.list ceiling"));
520 }
521 "__agent_state_init"
522 | "__agent_state_resume"
523 | "__agent_state_write"
524 | "__agent_state_read"
525 | "__agent_state_list"
526 | "__agent_state_delete"
527 | "__agent_state_handoff"
528 if !policy_allows_capability(&policy, "agent_state", "access") =>
529 {
530 return reject_policy(format!(
531 "builtin '{name}' exceeds agent_state.access ceiling"
532 ));
533 }
534 "vision_ocr"
535 if !policy_allows_capability(&policy, "vision", "ocr")
536 || !policy_allows_side_effect(&policy, "process_exec") =>
537 {
538 return reject_policy(format!(
539 "builtin '{name}' exceeds vision.ocr/process ceiling"
540 ));
541 }
542 "mcp_connect"
543 | "mcp_ensure_active"
544 | "mcp_call"
545 | "mcp_list_tools"
546 | "mcp_list_resources"
547 | "mcp_list_resource_templates"
548 | "mcp_read_resource"
549 | "mcp_list_prompts"
550 | "mcp_get_prompt"
551 | "mcp_server_info"
552 | "mcp_disconnect"
553 if !policy_allows_capability(&policy, "process", "exec")
554 || !policy_allows_side_effect(&policy, "process_exec") =>
555 {
556 return reject_policy(format!("builtin '{name}' exceeds process.exec ceiling"));
557 }
558 "host_call" => {
559 let name = args.first().map(|v| v.display()).unwrap_or_default();
560 let Some((capability, op)) = name.split_once('.') else {
561 return reject_policy(format!(
562 "host_call '{name}' must use capability.operation naming"
563 ));
564 };
565 if !policy_allows_capability(&policy, capability, op) {
566 return reject_policy(format!(
567 "host_call {capability}.{op} exceeds capability ceiling"
568 ));
569 }
570 let requested_side_effect = match (capability, op) {
571 ("workspace", "write_text" | "apply_edit" | "delete") => "workspace_write",
572 ("process", "exec") => "process_exec",
573 _ => "read_only",
574 };
575 if !policy_allows_side_effect(&policy, requested_side_effect) {
576 return reject_policy(format!(
577 "host_call {capability}.{op} exceeds side-effect ceiling"
578 ));
579 }
580 }
581 "host_tool_list" | "host_tool_call"
582 if !policy_allows_capability(&policy, "host", "tool_call") =>
583 {
584 return reject_policy(format!("builtin '{name}' exceeds host.tool_call ceiling"));
585 }
586 _ => {}
587 }
588 Ok(())
589}
590
591pub fn enforce_current_policy_for_bridge_builtin(name: &str) -> Result<(), VmError> {
592 let trusted = TRUSTED_BRIDGE_CALL_DEPTH.with(|depth| *depth.borrow() > 0);
593 if trusted {
594 return Ok(());
595 }
596 if current_execution_policy().is_some() {
597 return reject_policy(format!(
598 "bridged builtin '{name}' exceeds execution policy; declare an explicit capability/tool surface instead"
599 ));
600 }
601 Ok(())
602}
603
604pub fn enforce_current_policy_for_tool(tool_name: &str) -> Result<(), PolicyDenial> {
605 enforce_current_policy_for_tool_with_side_effect_grant(tool_name, None)
606}
607
608pub(crate) fn enforce_current_policy_for_tool_with_side_effect_grant(
613 tool_name: &str,
614 side_effect_grant: Option<&SideEffectCeilingGrant>,
615) -> Result<(), PolicyDenial> {
616 use crate::agent_events::DenialGate;
617 let Some(policy) = current_execution_policy() else {
618 return Ok(());
619 };
620 if !policy_allows_tool(&policy, tool_name) {
621 return reject_tool(
622 DenialGate::ToolCeiling,
623 None,
624 format!("tool '{tool_name}' is not in the active allowed-tool list"),
625 );
626 }
627 if let Some(annotations) = policy.tool_annotations.get(tool_name) {
628 for (capability, ops) in &annotations.capabilities {
629 for op in ops {
630 if !policy_allows_capability(&policy, capability, op) {
631 return reject_tool(
632 DenialGate::CapabilityCeiling,
633 Some(format!("{capability}.{op}")),
634 format!("tool '{tool_name}' requires {capability}.{op}"),
635 );
636 }
637 }
638 }
639 let requested_level = annotations.side_effect_level;
640 if requested_level != SideEffectLevel::None
641 && !policy_allows_side_effect(&policy, requested_level.as_str())
642 {
643 let ceiling = policy
644 .side_effect_level
645 .as_deref()
646 .map(SideEffectLevel::parse)
647 .expect("a side-effect refusal requires an active policy ceiling");
648 let violation = SideEffectCeilingViolation {
649 ceiling,
650 required_level: requested_level,
651 };
652 if side_effect_grant.is_some_and(|grant| grant.matches(tool_name, violation)) {
653 return Ok(());
654 }
655 return Err(PolicyDenial {
656 gate: DenialGate::SideEffectCeiling,
657 capability: None,
658 reason: DenialGate::SideEffectCeiling.render_reason(format!(
659 "tool '{tool_name}' requires side-effect level '{}' but the active ceiling is '{}'",
660 requested_level.as_str(),
661 ceiling.as_str(),
662 )),
663 side_effect_ceiling: Some(violation),
664 });
665 }
666 }
667 Ok(())
668}
669
670pub fn redact_transcript_visibility(
682 transcript: &VmValue,
683 visibility: Option<&str>,
684) -> Option<VmValue> {
685 let Some(visibility) = visibility else {
686 return Some(transcript.clone());
687 };
688 if visibility != "public" && visibility != "public_only" {
689 return Some(transcript.clone());
690 }
691 let dict = transcript.as_dict()?;
692 let public_messages = match dict.get("messages") {
693 Some(VmValue::List(list)) => list
694 .iter()
695 .filter_map(redact_public_message)
696 .collect::<Vec<_>>(),
697 _ => Vec::new(),
698 };
699 let public_events = match dict.get("events") {
700 Some(VmValue::List(list)) => list
701 .iter()
702 .filter(|event| {
703 event
704 .as_dict()
705 .and_then(|d| d.get("visibility"))
706 .map(|v| v.display())
707 .map(|value| value == "public")
708 .unwrap_or(true)
709 })
710 .cloned()
711 .collect::<Vec<_>>(),
712 _ => Vec::new(),
713 };
714 let mut redacted = dict.clone();
715 redacted.insert(
716 crate::value::intern_key("messages"),
717 VmValue::List(std::sync::Arc::new(public_messages)),
718 );
719 redacted.insert(
720 crate::value::intern_key("events"),
721 VmValue::List(std::sync::Arc::new(public_events)),
722 );
723 Some(VmValue::dict(redacted))
724}
725
726fn redact_public_message(message: &VmValue) -> Option<VmValue> {
727 let Some(dict) = message.as_dict() else {
728 return Some(message.clone());
729 };
730 if dict.get("role").map(|value| value.display()).as_deref() == Some("tool_result") {
731 return None;
732 }
733 if dict
734 .get("visibility")
735 .map(|value| value.display())
736 .is_some_and(|visibility| visibility != "public")
737 {
738 return None;
739 }
740
741 let mut redacted = dict.clone();
742 let mut saw_structured_blocks = false;
743 let mut public_text = Vec::new();
744 for key in ["content", "blocks"] {
745 if let Some(VmValue::List(blocks)) = dict.get(key) {
746 saw_structured_blocks = true;
747 let public_blocks = blocks
748 .iter()
749 .filter_map(redact_public_block)
750 .collect::<Vec<_>>();
751 if key == "blocks" || public_text.is_empty() {
752 public_text = text_fragments_from_blocks(&public_blocks);
753 }
754 redacted.insert(
755 crate::value::intern_key(key),
756 VmValue::List(std::sync::Arc::new(public_blocks)),
757 );
758 }
759 }
760 if saw_structured_blocks {
761 if public_text.is_empty() {
762 redacted.remove("text");
763 } else {
764 redacted.put_str("text", public_text.join("\n"));
765 }
766 }
767 Some(VmValue::dict(redacted))
768}
769
770fn redact_public_block(block: &VmValue) -> Option<VmValue> {
771 let Some(dict) = block.as_dict() else {
772 return Some(block.clone());
773 };
774 if dict
775 .get("visibility")
776 .map(|value| value.display())
777 .is_some_and(|visibility| visibility != "public")
778 {
779 return None;
780 }
781 Some(block.clone())
782}
783
784fn text_fragments_from_blocks(blocks: &[VmValue]) -> Vec<String> {
785 blocks
786 .iter()
787 .filter_map(|block| block.as_dict())
788 .filter_map(|dict| dict.get("text"))
789 .filter_map(|text| match text {
790 VmValue::String(value) if !value.is_empty() => Some(value.to_string()),
791 _ => None,
792 })
793 .collect()
794}
795
796pub fn builtin_ceiling() -> CapabilityPolicy {
797 CapabilityPolicy {
798 tools: Vec::new(),
802 capabilities: BTreeMap::new(),
803 workspace_roots: Vec::new(),
804 read_only_roots: Vec::new(),
805 side_effect_level: Some(SideEffectLevel::MAX.as_str().to_string()),
815 recursion_limit: Some(RuntimeLimits::DEFAULT.max_nested_execution_depth),
816 tool_arg_constraints: Vec::new(),
817 tool_annotations: BTreeMap::new(),
818 sandbox_profile: SandboxProfile::Worktree,
819 process_sandbox: Default::default(),
820 }
821}
822
823#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
827#[serde(default)]
828pub struct ToolApprovalPolicy {
829 #[serde(default)]
832 pub rules: Vec<PolicyRule>,
833 #[serde(default)]
835 pub auto_approve: Vec<String>,
836 #[serde(default)]
838 pub auto_deny: Vec<String>,
839 #[serde(default)]
841 pub require_approval: Vec<String>,
842 #[serde(default)]
844 pub write_path_allowlist: Vec<String>,
845 #[serde(default)]
847 pub allow_sensitive_paths: bool,
848 #[serde(default)]
851 pub sensitive_path_patterns: Vec<String>,
852 #[serde(default)]
854 pub allow_external_paths: bool,
855 #[serde(default)]
857 pub external_roots: Vec<String>,
858 #[serde(default, alias = "repeated_call_limit")]
860 pub repeat_limit: Option<u64>,
861 #[serde(default, alias = "repeated_call_action")]
863 pub repeat_action: Option<PolicyAction>,
864}
865
866#[derive(Debug, Clone, PartialEq, Eq)]
868pub enum ToolApprovalDecision {
869 AutoApproved,
871 AutoDenied { reason: String },
873 RequiresHostApproval,
876}
877
878impl ToolApprovalPolicy {
879 pub fn evaluate_detailed(&self, tool_name: &str, args: &serde_json::Value) -> PolicyEvaluation {
880 approval_rules::evaluate_tool_approval_policy(self, tool_name, args, None)
881 }
882
883 pub fn evaluate_detailed_with_repeat(
884 &self,
885 tool_name: &str,
886 args: &serde_json::Value,
887 repeat_count: u64,
888 ) -> PolicyEvaluation {
889 approval_rules::evaluate_tool_approval_policy(self, tool_name, args, Some(repeat_count))
890 }
891
892 pub fn evaluate(&self, tool_name: &str, args: &serde_json::Value) -> ToolApprovalDecision {
895 let decision = self.evaluate_detailed(tool_name, args);
896 if decision.is_deny() {
897 return ToolApprovalDecision::AutoDenied {
898 reason: decision.reason,
899 };
900 }
901 if decision.is_ask() {
902 return ToolApprovalDecision::RequiresHostApproval;
903 }
904 ToolApprovalDecision::AutoApproved
905 }
906
907 pub fn intersect(&self, other: &ToolApprovalPolicy) -> ToolApprovalPolicy {
913 let auto_approve = if self.auto_approve.is_empty() {
914 other.auto_approve.clone()
915 } else if other.auto_approve.is_empty() {
916 self.auto_approve.clone()
917 } else {
918 self.auto_approve
919 .iter()
920 .filter(|p| other.auto_approve.contains(p))
921 .cloned()
922 .collect()
923 };
924 let mut auto_deny = self.auto_deny.clone();
925 auto_deny.extend(other.auto_deny.iter().cloned());
926 let mut require_approval = self.require_approval.clone();
927 require_approval.extend(other.require_approval.iter().cloned());
928 let write_path_allowlist = if self.write_path_allowlist.is_empty() {
929 other.write_path_allowlist.clone()
930 } else if other.write_path_allowlist.is_empty() {
931 self.write_path_allowlist.clone()
932 } else {
933 self.write_path_allowlist
934 .iter()
935 .filter(|p| other.write_path_allowlist.contains(p))
936 .cloned()
937 .collect()
938 };
939 let mut rules = self.rules.clone();
940 rules.extend(other.rules.iter().cloned());
941 let mut sensitive_path_patterns = self.sensitive_path_patterns.clone();
942 sensitive_path_patterns.extend(other.sensitive_path_patterns.iter().cloned());
943 sensitive_path_patterns.sort();
944 sensitive_path_patterns.dedup();
945 let external_roots = if self.external_roots.is_empty() {
946 other.external_roots.clone()
947 } else if other.external_roots.is_empty() {
948 self.external_roots.clone()
949 } else {
950 self.external_roots
951 .iter()
952 .filter(|root| other.external_roots.contains(root))
953 .cloned()
954 .collect()
955 };
956 ToolApprovalPolicy {
957 rules,
958 auto_approve,
959 auto_deny,
960 require_approval,
961 write_path_allowlist,
962 allow_sensitive_paths: self.allow_sensitive_paths && other.allow_sensitive_paths,
963 sensitive_path_patterns,
964 allow_external_paths: self.allow_external_paths && other.allow_external_paths,
965 external_roots,
966 repeat_limit: match (self.repeat_limit, other.repeat_limit) {
967 (Some(left), Some(right)) => Some(left.min(right)),
968 (Some(left), None) => Some(left),
969 (None, Some(right)) => Some(right),
970 (None, None) => None,
971 },
972 repeat_action: match (self.repeat_action, other.repeat_action) {
973 (Some(PolicyAction::Deny), _) | (_, Some(PolicyAction::Deny)) => {
974 Some(PolicyAction::Deny)
975 }
976 (Some(PolicyAction::Ask), _) | (_, Some(PolicyAction::Ask)) => {
977 Some(PolicyAction::Ask)
978 }
979 (Some(PolicyAction::Allow), Some(PolicyAction::Allow)) => Some(PolicyAction::Allow),
980 (Some(action), None) | (None, Some(action)) => Some(action),
981 (None, None) => None,
982 },
983 }
984 }
985}
986
987#[cfg(test)]
988mod approval_policy_tests {
989 use super::*;
990 use crate::orchestration::{pop_execution_policy, push_execution_policy, CapabilityPolicy};
991 use crate::tool_annotations::{ToolAnnotations, ToolArgSchema, ToolKind};
992
993 fn workspace_caps(ops: &[&str]) -> CapabilityPolicy {
994 CapabilityPolicy {
995 capabilities: std::collections::BTreeMap::from([(
996 "workspace".to_string(),
997 ops.iter().map(|s| s.to_string()).collect(),
998 )]),
999 ..Default::default()
1000 }
1001 }
1002
1003 #[test]
1004 fn builtin_ceiling_permits_desktop_control_but_a_lower_ceiling_denies_it() {
1005 let builtin = builtin_ceiling();
1009 assert!(policy_allows_side_effect(
1010 &builtin,
1011 SideEffectLevel::DesktopControl.as_str()
1012 ));
1013
1014 let network_ceiling = CapabilityPolicy {
1018 side_effect_level: Some(SideEffectLevel::Network.as_str().to_string()),
1019 ..Default::default()
1020 };
1021 assert!(!policy_allows_side_effect(
1022 &network_ceiling,
1023 SideEffectLevel::DesktopControl.as_str()
1024 ));
1025 assert!(policy_allows_side_effect(
1027 &network_ceiling,
1028 SideEffectLevel::ProcessExec.as_str()
1029 ));
1030 }
1031
1032 #[test]
1033 fn read_text_subsumes_exists_probe() {
1034 push_execution_policy(workspace_caps(&[
1042 "read_text",
1043 "list",
1044 "write_text",
1045 "apply_edit",
1046 ]));
1047 assert!(enforce_current_policy_for_builtin("file_exists", &[]).is_ok());
1048 assert!(enforce_current_policy_for_builtin("path_status", &[]).is_ok());
1049 assert!(enforce_current_policy_for_builtin("stat", &[]).is_ok());
1050 pop_execution_policy();
1051 }
1052
1053 #[test]
1054 fn list_alone_subsumes_exists_probe() {
1055 push_execution_policy(workspace_caps(&["list"]));
1057 assert!(enforce_current_policy_for_builtin("file_exists", &[]).is_ok());
1058 assert!(enforce_current_policy_for_builtin("path_status", &[]).is_ok());
1059 pop_execution_policy();
1060 }
1061
1062 #[test]
1063 fn exists_probe_rejected_without_any_read_grant() {
1064 push_execution_policy(workspace_caps(&["write_text", "apply_edit"]));
1067 assert!(enforce_current_policy_for_builtin("file_exists", &[]).is_err());
1068 assert!(enforce_current_policy_for_builtin("path_status", &[]).is_err());
1069 pop_execution_policy();
1070 }
1071
1072 #[test]
1073 fn auto_deny_takes_precedence_over_auto_approve() {
1074 let policy = ToolApprovalPolicy {
1075 auto_approve: vec!["*".to_string()],
1076 auto_deny: vec!["dangerous_*".to_string()],
1077 ..Default::default()
1078 };
1079 assert_eq!(
1080 policy.evaluate("dangerous_rm", &serde_json::json!({})),
1081 ToolApprovalDecision::AutoDenied {
1082 reason: "tool 'dangerous_rm' matches deny pattern 'dangerous_*'".to_string()
1083 }
1084 );
1085 }
1086
1087 #[test]
1088 fn auto_approve_matches_glob() {
1089 let policy = ToolApprovalPolicy {
1090 auto_approve: vec!["read*".to_string(), "search*".to_string()],
1091 ..Default::default()
1092 };
1093 assert_eq!(
1094 policy.evaluate("read_file", &serde_json::json!({})),
1095 ToolApprovalDecision::AutoApproved
1096 );
1097 assert_eq!(
1098 policy.evaluate("search", &serde_json::json!({})),
1099 ToolApprovalDecision::AutoApproved
1100 );
1101 }
1102
1103 #[test]
1104 fn require_approval_emits_decision() {
1105 let policy = ToolApprovalPolicy {
1106 require_approval: vec!["edit*".to_string()],
1107 ..Default::default()
1108 };
1109 let decision = policy.evaluate("edit_file", &serde_json::json!({"path": "foo.rs"}));
1110 assert!(matches!(
1111 decision,
1112 ToolApprovalDecision::RequiresHostApproval
1113 ));
1114 }
1115
1116 #[test]
1117 fn unmatched_tool_defaults_to_approved() {
1118 let policy = ToolApprovalPolicy {
1119 auto_approve: vec!["read*".to_string()],
1120 require_approval: vec!["edit*".to_string()],
1121 ..Default::default()
1122 };
1123 assert_eq!(
1124 policy.evaluate("unknown_tool", &serde_json::json!({})),
1125 ToolApprovalDecision::AutoApproved
1126 );
1127 }
1128
1129 #[test]
1130 fn intersect_merges_deny_lists() {
1131 let a = ToolApprovalPolicy {
1132 auto_deny: vec!["rm*".to_string()],
1133 ..Default::default()
1134 };
1135 let b = ToolApprovalPolicy {
1136 auto_deny: vec!["drop*".to_string()],
1137 ..Default::default()
1138 };
1139 let merged = a.intersect(&b);
1140 assert_eq!(merged.auto_deny.len(), 2);
1141 }
1142
1143 #[test]
1144 fn intersect_restricts_auto_approve_to_common_patterns() {
1145 let a = ToolApprovalPolicy {
1146 auto_approve: vec!["read*".to_string(), "search*".to_string()],
1147 ..Default::default()
1148 };
1149 let b = ToolApprovalPolicy {
1150 auto_approve: vec!["read*".to_string(), "write*".to_string()],
1151 ..Default::default()
1152 };
1153 let merged = a.intersect(&b);
1154 assert_eq!(merged.auto_approve, vec!["read*".to_string()]);
1155 }
1156
1157 #[test]
1158 fn intersect_defers_auto_approve_when_one_side_empty() {
1159 let a = ToolApprovalPolicy {
1160 auto_approve: vec!["read*".to_string()],
1161 ..Default::default()
1162 };
1163 let b = ToolApprovalPolicy::default();
1164 let merged = a.intersect(&b);
1165 assert_eq!(merged.auto_approve, vec!["read*".to_string()]);
1166 }
1167
1168 #[test]
1169 fn write_path_allowlist_matches_recovered_workspace_relative_path() {
1170 let temp = tempfile::tempdir().unwrap();
1171 std::fs::create_dir_all(temp.path().join("packages/demo")).unwrap();
1172 std::fs::write(temp.path().join("packages/demo/file.txt"), "ok").unwrap();
1173 crate::stdlib::process::set_thread_execution_context(Some(
1174 crate::orchestration::RunExecutionRecord {
1175 cwd: Some(temp.path().to_string_lossy().into_owned()),
1176 project_root: None,
1177 source_dir: Some(temp.path().to_string_lossy().into_owned()),
1178 env: BTreeMap::new(),
1179 adapter: None,
1180 repo_path: None,
1181 worktree_path: None,
1182 branch: None,
1183 base_ref: None,
1184 cleanup: None,
1185 environment_policy: Default::default(),
1186 grants: Vec::new(),
1187 },
1188 ));
1189
1190 let mut tool_annotations = BTreeMap::new();
1191 tool_annotations.insert(
1192 "write_file".to_string(),
1193 ToolAnnotations {
1194 kind: ToolKind::Edit,
1195 arg_schema: ToolArgSchema {
1196 path_params: vec!["path".to_string()],
1197 ..Default::default()
1198 },
1199 ..Default::default()
1200 },
1201 );
1202 push_execution_policy(CapabilityPolicy {
1203 tool_annotations,
1204 ..Default::default()
1205 });
1206
1207 let policy = ToolApprovalPolicy {
1208 write_path_allowlist: vec!["packages/demo/file.txt".to_string()],
1209 ..Default::default()
1210 };
1211 let decision = policy.evaluate(
1212 "write_file",
1213 &serde_json::json!({"path": "/packages/demo/file.txt"}),
1214 );
1215 assert_eq!(decision, ToolApprovalDecision::AutoApproved);
1216
1217 pop_execution_policy();
1218 crate::stdlib::process::set_thread_execution_context(None);
1219 }
1220
1221 #[test]
1222 fn write_path_allowlist_does_not_block_read_only_tools() {
1223 let temp = tempfile::tempdir().unwrap();
1224 std::fs::create_dir_all(temp.path().join("packages/demo")).unwrap();
1225 std::fs::write(temp.path().join("packages/demo/context.txt"), "ok").unwrap();
1226 crate::stdlib::process::set_thread_execution_context(Some(
1227 crate::orchestration::RunExecutionRecord {
1228 cwd: Some(temp.path().to_string_lossy().into_owned()),
1229 project_root: None,
1230 source_dir: Some(temp.path().to_string_lossy().into_owned()),
1231 env: BTreeMap::new(),
1232 adapter: None,
1233 repo_path: None,
1234 worktree_path: None,
1235 branch: None,
1236 base_ref: None,
1237 cleanup: None,
1238 environment_policy: Default::default(),
1239 grants: Vec::new(),
1240 },
1241 ));
1242
1243 let mut tool_annotations = BTreeMap::new();
1244 tool_annotations.insert(
1245 "read_file".to_string(),
1246 ToolAnnotations {
1247 kind: ToolKind::Read,
1248 arg_schema: ToolArgSchema {
1249 path_params: vec!["path".to_string()],
1250 ..Default::default()
1251 },
1252 ..Default::default()
1253 },
1254 );
1255 push_execution_policy(CapabilityPolicy {
1256 tool_annotations,
1257 ..Default::default()
1258 });
1259
1260 let policy = ToolApprovalPolicy {
1261 write_path_allowlist: vec!["packages/demo/file.txt".to_string()],
1262 ..Default::default()
1263 };
1264 let decision = policy.evaluate(
1265 "read_file",
1266 &serde_json::json!({"path": "/packages/demo/context.txt"}),
1267 );
1268 assert_eq!(decision, ToolApprovalDecision::AutoApproved);
1269
1270 pop_execution_policy();
1271 crate::stdlib::process::set_thread_execution_context(None);
1272 }
1273
1274 #[test]
1275 fn builtin_policy_covers_fs_read_and_list_helpers() {
1276 clear_execution_policy_stacks();
1277 push_execution_policy(CapabilityPolicy {
1278 capabilities: BTreeMap::from([("workspace".to_string(), vec!["exists".to_string()])]),
1279 side_effect_level: Some("read_only".to_string()),
1280 ..CapabilityPolicy::default()
1281 });
1282
1283 for name in [
1284 "read_lines",
1285 "find_text",
1286 "find_evidence",
1287 "walk_dir",
1288 "glob",
1289 "project_context_profile_native",
1290 ] {
1291 assert!(
1292 enforce_current_policy_for_builtin(name, &[]).is_err(),
1293 "{name} should be rejected when the matching workspace capability is absent"
1294 );
1295 }
1296
1297 pop_execution_policy();
1298 }
1299
1300 #[test]
1301 fn move_file_requires_workspace_write_side_effect() {
1302 clear_execution_policy_stacks();
1303 push_execution_policy(CapabilityPolicy {
1304 capabilities: BTreeMap::from([(
1305 "workspace".to_string(),
1306 vec!["write_text".to_string()],
1307 )]),
1308 side_effect_level: Some("read_only".to_string()),
1309 ..CapabilityPolicy::default()
1310 });
1311
1312 let error = enforce_current_policy_for_builtin("move_file", &[]).unwrap_err();
1313 assert!(
1314 error.to_string().contains("workspace write ceiling"),
1315 "unexpected error: {error}"
1316 );
1317
1318 pop_execution_policy();
1319 }
1320
1321 #[test]
1322 fn unix_socket_json_request_requires_network_side_effect() {
1323 clear_execution_policy_stacks();
1324 push_execution_policy(CapabilityPolicy {
1325 side_effect_level: Some("read_only".to_string()),
1326 ..CapabilityPolicy::default()
1327 });
1328
1329 let error =
1330 enforce_current_policy_for_builtin("__net_unix_socket_json_request", &[]).unwrap_err();
1331 assert!(
1332 error.to_string().contains("network.http ceiling"),
1333 "unexpected error: {error}"
1334 );
1335
1336 pop_execution_policy();
1337 }
1338
1339 #[test]
1340 fn files_upload_requires_workspace_read_and_network_side_effect() {
1341 clear_execution_policy_stacks();
1342 push_execution_policy(CapabilityPolicy {
1343 capabilities: BTreeMap::from([
1344 ("workspace".to_string(), vec!["read_text".to_string()]),
1345 ("network".to_string(), vec!["http".to_string()]),
1346 ]),
1347 side_effect_level: Some("read_only".to_string()),
1348 ..CapabilityPolicy::default()
1349 });
1350
1351 let network_error = enforce_current_policy_for_builtin("__files_upload", &[]).unwrap_err();
1352 assert!(
1353 network_error.to_string().contains("network.http ceiling"),
1354 "unexpected error: {network_error}"
1355 );
1356 pop_execution_policy();
1357
1358 push_execution_policy(CapabilityPolicy {
1359 capabilities: BTreeMap::from([
1360 ("workspace".to_string(), vec!["exists".to_string()]),
1361 ("network".to_string(), vec!["http".to_string()]),
1362 ]),
1363 side_effect_level: Some("network".to_string()),
1364 ..CapabilityPolicy::default()
1365 });
1366 let read_error = enforce_current_policy_for_builtin("__files_upload", &[]).unwrap_err();
1367 assert!(
1368 read_error.to_string().contains("workspace.read_text"),
1369 "unexpected error: {read_error}"
1370 );
1371
1372 pop_execution_policy();
1373 }
1374}
1375
1376#[cfg(test)]
1377mod turn_policy_tests {
1378 use super::TurnPolicy;
1379
1380 #[test]
1381 fn default_allows_done_sentinel() {
1382 let policy = TurnPolicy::default();
1383 assert!(policy.allow_done_sentinel);
1384 assert!(!policy.require_action_or_yield);
1385 assert!(policy.max_prose_chars.is_none());
1386 }
1387
1388 #[test]
1389 fn deserializing_partial_dict_preserves_done_sentinel_pathway() {
1390 let policy: TurnPolicy =
1395 serde_json::from_value(serde_json::json!({ "require_action_or_yield": true }))
1396 .expect("deserialize");
1397 assert!(policy.require_action_or_yield);
1398 assert!(policy.allow_done_sentinel);
1399 }
1400
1401 #[test]
1402 fn deserializing_explicit_false_disables_done_sentinel() {
1403 let policy: TurnPolicy = serde_json::from_value(serde_json::json!({
1404 "require_action_or_yield": true,
1405 "allow_done_sentinel": false,
1406 }))
1407 .expect("deserialize");
1408 assert!(policy.require_action_or_yield);
1409 assert!(!policy.allow_done_sentinel);
1410 }
1411}
1412
1413#[cfg(test)]
1414mod visibility_redaction_tests {
1415 use super::*;
1416 use crate::value::VmValue;
1417
1418 fn mock_transcript() -> VmValue {
1419 let messages = vec![
1420 serde_json::json!({"role": "user", "content": "hi"}),
1421 serde_json::json!({"role": "assistant", "content": "hello"}),
1422 serde_json::json!({"role": "tool_result", "content": "internal tool output"}),
1423 ];
1424 crate::llm::helpers::transcript_to_vm_with_events(
1425 Some("test-id".to_string()),
1426 None,
1427 None,
1428 &messages,
1429 Vec::new(),
1430 Vec::new(),
1431 Some("active"),
1432 )
1433 }
1434
1435 fn message_count(transcript: &VmValue) -> usize {
1436 transcript
1437 .as_dict()
1438 .and_then(|d| d.get("messages"))
1439 .and_then(|v| match v {
1440 VmValue::List(list) => Some(list.len()),
1441 _ => None,
1442 })
1443 .unwrap_or(0)
1444 }
1445
1446 #[test]
1447 fn visibility_none_returns_unchanged() {
1448 let t = mock_transcript();
1449 let result = redact_transcript_visibility(&t, None).unwrap();
1450 assert_eq!(message_count(&result), 3);
1451 }
1452
1453 #[test]
1454 fn visibility_public_drops_tool_results() {
1455 let t = mock_transcript();
1456 let result = redact_transcript_visibility(&t, Some("public")).unwrap();
1457 assert_eq!(message_count(&result), 2);
1458 }
1459
1460 #[test]
1461 fn visibility_public_drops_private_content_blocks() {
1462 let t = crate::schema::json_to_vm_value(&serde_json::json!({
1463 "messages": [
1464 {
1465 "role": "assistant",
1466 "visibility": "public",
1467 "text": "visible answer\nsecret chain",
1468 "content": [
1469 {"type": "output_text", "text": "visible answer", "visibility": "public"},
1470 {"type": "reasoning", "text": "secret chain", "visibility": "private"}
1471 ],
1472 "blocks": [
1473 {"type": "output_text", "text": "visible block", "visibility": "public"},
1474 {"type": "tool_call", "text": "internal args", "visibility": "internal"}
1475 ]
1476 }
1477 ],
1478 "events": []
1479 }));
1480
1481 let result = redact_transcript_visibility(&t, Some("public")).unwrap();
1482 let rendered = result.display();
1483 assert!(rendered.contains("visible answer"));
1484 assert!(rendered.contains("visible block"));
1485 assert!(!rendered.contains("secret chain"));
1486 assert!(!rendered.contains("internal args"));
1487 }
1488
1489 #[test]
1490 fn visibility_unknown_string_is_pass_through() {
1491 let t = mock_transcript();
1492 let result = redact_transcript_visibility(&t, Some("internal")).unwrap();
1493 assert_eq!(message_count(&result), 3);
1494 }
1495}