1use crate::capability::{
2 CapabilityDecision, CapabilityLedgerEntry, CapabilityScope, capabilities_from_tool_metadata,
3};
4use crate::effect::PostDecision;
5use crate::event::AgentEvent;
6use crate::runtime_components::{DefaultToolSecurityPolicy, ToolSecurityPolicy};
7use crate::security::{SecurityDecision, SecurityPolicy};
8use anyhow::Result;
9use async_trait::async_trait;
10use serde::{Deserialize, Serialize};
11use serde_json::{Map, Value, json};
12use std::collections::HashMap;
13use std::path::Path;
14use std::sync::Arc;
15use tokio::sync::mpsc;
16
17pub mod background;
18pub(crate) mod builtin;
19pub mod metadata;
20pub mod registry;
21#[cfg(test)]
22mod tests;
23
24#[cfg(feature = "browser")]
25use builtin::BrowserTool;
26use builtin::{
27 AppendNoteTool, BashTool, CodeExecTool, ContextRemainingTool, CurrentTimeTool, EditTool,
28 HistoryOpsTool, InitSessionTool, MarkFeatureDoneTool, MemoryTool, MultiEditTool,
29 NewContextWindowTool, PackageManagerTool, PlanTool, QuestionTool, ReadTool,
30 RepoIntelligenceAction, RepoIntelligenceTool, RequestUserInputTool, RuntimeInfoTool,
31 SandboxTool, SearchTool, SleepTool, ToolSearchTool, ViewImageTool, WriteTool, builtin_metadata,
32 truncate_tool_result,
33};
34#[cfg(feature = "code-vfs")]
35use builtin::{CodeEditTool, CodeReadTool};
36
37pub use builtin::{
38 AgentBackend, AgentProfile, ApprovalMode, MockAgentBackend, PolicyAgentBackend,
39 ProviderBuilderFn, RepoExploreTool, SubagentBridgeBackend, SubagentTool, WorkerProbeBackend,
40 WorkflowTool, workflow_tool_description,
41};
42pub use metadata::{ToolExposure, ToolMetadata, ToolRisk, capabilities};
43pub use registry::{ToolRegistry, ToolSet, phases};
44
45#[async_trait]
46pub trait Tool: Send + Sync {
47 fn definition(&self) -> ToolDefinition;
48 async fn invoke(&self, invocation: ToolInvocation) -> Result<ToolResult>;
49 async fn invoke_with_context(
50 &self,
51 invocation: ToolInvocation,
52 context: ToolInvocationContext,
53 ) -> Result<ToolResult> {
54 let _ = context;
55 self.invoke(invocation).await
56 }
57}
58
59#[derive(Clone, Default)]
60pub struct ToolInvocationContext {
61 pub event_tx: Option<mpsc::UnboundedSender<AgentEvent>>,
62 pub sudo_password_resolver: Option<crate::runtime::SudoPasswordResolver>,
64 pub cancel_token: Option<crate::cancel::CancelToken>,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct ToolDefinition {
69 pub name: String,
70 pub description: String,
71 pub kind: ToolKind,
72 #[serde(default)]
73 pub input_schema: Value,
74 #[serde(default)]
77 pub metadata: ToolMetadata,
78}
79
80impl Default for ToolDefinition {
81 fn default() -> Self {
82 Self {
83 name: String::new(),
84 description: String::new(),
85 kind: ToolKind::Custom,
86 input_schema: Value::Object(Default::default()),
87 metadata: ToolMetadata::default(),
88 }
89 }
90}
91
92impl ToolDefinition {
93 pub fn new(
95 name: impl Into<String>,
96 description: impl Into<String>,
97 kind: ToolKind,
98 input_schema: Value,
99 ) -> Self {
100 Self {
101 name: name.into(),
102 description: description.into(),
103 kind,
104 input_schema,
105 metadata: ToolMetadata::default(),
106 }
107 }
108
109 pub fn with_metadata(
111 name: impl Into<String>,
112 description: impl Into<String>,
113 kind: ToolKind,
114 input_schema: Value,
115 metadata: ToolMetadata,
116 ) -> Self {
117 Self {
118 name: name.into(),
119 description: description.into(),
120 kind,
121 input_schema,
122 metadata,
123 }
124 }
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
128pub enum ToolKind {
129 Read,
130 Write,
131 Command,
132 Custom,
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
136pub enum ToolParallelism {
137 Shared,
139 Exclusive,
141}
142
143#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
144pub struct ToolInvocation {
145 pub id: String,
146 pub tool_name: String,
147 pub input: Value,
148}
149
150#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
151pub struct ToolResult {
152 pub invocation_id: String,
153 pub ok: bool,
154 pub output: Value,
155}
156
157pub const NAVI_CONTENT_PARTS_KEY: &str = "_navi_content_parts";
160
161pub fn take_tool_content_parts(result: &mut ToolResult) -> Vec<crate::model::ContentPart> {
167 let Some(obj) = result.output.as_object_mut() else {
168 return Vec::new();
169 };
170 let Some(raw) = obj.remove(NAVI_CONTENT_PARTS_KEY) else {
171 return Vec::new();
172 };
173 match serde_json::from_value::<Vec<crate::model::ContentPart>>(raw) {
174 Ok(parts) => parts,
175 Err(err) => {
176 tracing::warn!(error = %err, "failed to deserialize tool content_parts");
177 Vec::new()
178 }
179 }
180}
181
182pub struct ToolExecutor {
183 tools: HashMap<String, Arc<dyn Tool>>,
184 validators: HashMap<String, Arc<jsonschema::Validator>>,
185 invalid_schemas: HashMap<String, String>,
186 policy: SecurityPolicy,
187 security: Arc<dyn ToolSecurityPolicy>,
188 harness_profile: String,
189 registry: ToolRegistry,
190 rewind_store: Option<Arc<std::sync::Mutex<crate::rewind::RewindStore>>>,
192}
193
194#[derive(Debug, Clone, PartialEq, Eq)]
195pub enum ToolCallInvalid {
196 UnknownTool {
197 tool_name: String,
198 available_tools: Vec<String>,
199 },
200 InvalidSchema {
201 tool_name: String,
202 message: String,
203 },
204 MalformedArguments {
205 tool_name: String,
206 raw_arguments_preview: String,
207 example: Value,
208 },
209 InvalidArguments {
210 tool_name: String,
211 problems: Vec<String>,
212 example: Value,
213 },
214}
215
216const EXCLUSIVE_BATCH_TOOL_NAMES: &[&str] = &[
217 "plan", "question",
218 "subagent", "workflow",
221 ];
223
224impl ToolExecutor {
225 pub fn new(policy: SecurityPolicy) -> Self {
226 Self::with_security_policy(policy, Arc::new(DefaultToolSecurityPolicy))
227 }
228
229 pub fn empty(policy: SecurityPolicy) -> Self {
230 Self::empty_with_security_policy(policy, Arc::new(DefaultToolSecurityPolicy))
231 }
232
233 pub fn empty_with_security_policy(
234 policy: SecurityPolicy,
235 security: Arc<dyn ToolSecurityPolicy>,
236 ) -> Self {
237 Self {
238 tools: HashMap::new(),
239 validators: HashMap::new(),
240 invalid_schemas: HashMap::new(),
241 policy,
242 security,
243 harness_profile: "medium".to_string(),
244 registry: ToolRegistry::new(),
245 rewind_store: None,
246 }
247 }
248
249 pub fn set_rewind_store(
251 &mut self,
252 store: Option<Arc<std::sync::Mutex<crate::rewind::RewindStore>>>,
253 ) {
254 self.rewind_store = store;
255 }
256
257 pub fn rewind_store(&self) -> Option<Arc<std::sync::Mutex<crate::rewind::RewindStore>>> {
259 self.rewind_store.clone()
260 }
261
262 pub fn with_security_policy(
263 policy: SecurityPolicy,
264 security: Arc<dyn ToolSecurityPolicy>,
265 ) -> Self {
266 let mut executor = Self::empty_with_security_policy(policy, security);
267 executor.register_builtin_tools();
268 executor
269 }
270
271 pub fn registry(&self) -> &ToolRegistry {
272 &self.registry
273 }
274
275 pub fn registry_mut(&mut self) -> &mut ToolRegistry {
276 &mut self.registry
277 }
278
279 pub fn search_tools(&self, query: &str, max_results: usize) -> Vec<ToolDefinition> {
281 self.registry.search(query, max_results)
282 }
283
284 pub fn set_harness_profile(&mut self, profile: String) {
285 self.harness_profile = profile;
286 self.register(RuntimeInfoTool::new(
287 self.policy.clone(),
288 self.harness_profile.clone(),
289 ));
290 }
291
292 pub fn set_security_policy(&mut self, policy: SecurityPolicy) {
294 self.policy = policy;
295 self.register(RuntimeInfoTool::new(
296 self.policy.clone(),
297 self.harness_profile.clone(),
298 ));
299 }
300
301 pub fn security_policy(&self) -> &SecurityPolicy {
303 &self.policy
304 }
305
306 pub fn register_skill_loader(
307 &mut self,
308 project_dir: std::path::PathBuf,
309 data_dir: std::path::PathBuf,
310 config: std::sync::Arc<std::sync::RwLock<crate::config::NaviConfig>>,
311 ) {
312 let loader = crate::tool::builtin::SkillTool::new(
314 project_dir.clone(),
315 data_dir.clone(),
316 config.clone(),
317 );
318 self.register_tool(std::sync::Arc::new(loader));
319 self.register_tool(std::sync::Arc::new(
320 crate::tool::builtin::SkillListTool::new(
321 project_dir.clone(),
322 data_dir.clone(),
323 config.clone(),
324 ),
325 ));
326 self.register_tool(std::sync::Arc::new(
327 crate::tool::builtin::SkillGetTool::new(project_dir.clone(), data_dir.clone(), config),
328 ));
329 self.register_tool(std::sync::Arc::new(
330 crate::tool::builtin::SkillSaveTool::new(project_dir.clone(), data_dir.clone()),
331 ));
332 self.register_tool(std::sync::Arc::new(
333 crate::tool::builtin::SkillDeleteTool::new(project_dir, data_dir),
334 ));
335 }
336
337 pub(crate) fn new_code_exec_host(policy: SecurityPolicy) -> Self {
338 let pr = policy.project_root().to_path_buf();
339 let mut executor = Self {
340 tools: HashMap::new(),
341 validators: HashMap::new(),
342 invalid_schemas: HashMap::new(),
343 policy: policy.clone(),
344 security: Arc::new(DefaultToolSecurityPolicy),
345 harness_profile: "medium".to_string(),
346 registry: ToolRegistry::new(),
347 rewind_store: None,
348 };
349 executor.register(ReadTool::new(pr.clone()));
350 executor.register(ReadTool::alias(pr.clone(), "read"));
351 executor.register(SearchTool::new(pr.clone()));
352 executor.register(SearchTool::grep(pr.clone()));
353 executor.register(SearchTool::fs_browser(pr.clone()));
354 executor.register(WriteTool::apply_patch(pr.clone()));
355 executor.register(BashTool::new(pr.clone()));
356 executor.register(RepoIntelligenceTool::new(
357 policy.clone(),
358 RepoIntelligenceAction::AstSearch,
359 ));
360 executor.register(RepoIntelligenceTool::new(
361 policy,
362 RepoIntelligenceAction::TestDiscovery,
363 ));
364 executor
365 }
366
367 pub fn definitions(&self) -> Vec<ToolDefinition> {
368 let visible_names: std::collections::HashSet<String> =
373 self.registry.visible_tool_names().into_iter().collect();
374
375 let mut result: Vec<ToolDefinition> = self
376 .tools
377 .values()
378 .filter(|tool| {
379 let def = tool.definition();
380 visible_names.contains(&def.name)
381 })
382 .map(|tool| {
383 let mut def = model_friendly_definition(tool.definition());
384 if let Some(registered) = self.registry.get(&def.name) {
386 def.metadata = registered.definition.metadata.clone();
387 }
388 def
389 })
390 .collect();
391 result.sort_by(|a, b| a.name.cmp(&b.name));
392 result
393 }
394
395 pub fn all_definitions(&self) -> Vec<ToolDefinition> {
396 let mut result = self
397 .tools
398 .values()
399 .map(|tool| model_friendly_definition(self.enriched_definition(tool.as_ref())))
400 .collect::<Vec<_>>();
401 result.sort_by(|a, b| a.name.cmp(&b.name));
402 result
403 }
404
405 pub fn definition(&self, name: &str) -> Option<ToolDefinition> {
406 self.tools
407 .get(name)
408 .map(|tool| self.enriched_definition(tool.as_ref()))
409 }
410
411 pub fn parallelism_for(&self, tool_name: &str) -> ToolParallelism {
412 if EXCLUSIVE_BATCH_TOOL_NAMES.contains(&tool_name) {
413 return ToolParallelism::Exclusive;
414 }
415
416 let Some(definition) = self.definition(tool_name) else {
417 return ToolParallelism::Shared;
418 };
419
420 if definition.metadata.is_read_only && definition.metadata.is_concurrency_safe {
421 ToolParallelism::Shared
422 } else {
423 ToolParallelism::Exclusive
424 }
425 }
426
427 pub fn register_tool(&mut self, tool: Arc<dyn Tool>) -> Option<Arc<dyn Tool>> {
428 let mut def = tool.definition();
429 let name = def.name.clone();
430
431 if def.metadata.is_default() {
433 let builtin = builtin_metadata(&name, def.kind);
434 def.metadata = builtin;
435 }
436
437 self.registry.register(def.clone());
439
440 match jsonschema::validator_for(&def.input_schema) {
441 Ok(v) => {
442 self.validators.insert(name.clone(), Arc::new(v));
443 self.invalid_schemas.remove(&name);
444 }
445 Err(e) => {
446 self.validators.remove(&name);
447 self.invalid_schemas.insert(name.clone(), e.to_string());
448 tracing::warn!(tool = %name, error = %e, "invalid schema");
449 }
450 }
451 self.tools.insert(name, tool)
452 }
453
454 pub fn validate_arguments(
455 &self,
456 inv: &ToolInvocation,
457 ) -> std::result::Result<(), ToolCallInvalid> {
458 let Some(_) = self.definition(&inv.tool_name) else {
459 return Err(ToolCallInvalid::UnknownTool {
460 tool_name: inv.tool_name.clone(),
461 available_tools: self.tool_names(),
462 });
463 };
464 if let Some(e) = self.invalid_schemas.get(&inv.tool_name) {
465 return Err(ToolCallInvalid::InvalidSchema {
466 tool_name: inv.tool_name.clone(),
467 message: e.clone(),
468 });
469 }
470 if let Some(raw) = inv.input.get("raw_arguments").and_then(Value::as_str) {
471 return Err(ToolCallInvalid::MalformedArguments {
472 tool_name: inv.tool_name.clone(),
473 raw_arguments_preview: raw.chars().take(200).collect(),
474 example: self
475 .definition(&inv.tool_name)
476 .map(|d| example_from_schema(&d.input_schema))
477 .unwrap_or(json!({})),
478 });
479 }
480 let Some(v) = self.validators.get(&inv.tool_name) else {
481 return Err(ToolCallInvalid::InvalidSchema {
482 tool_name: inv.tool_name.clone(),
483 message: "missing validator".into(),
484 });
485 };
486 let errors: Vec<String> = v
487 .iter_errors(&inv.input)
488 .take(4)
489 .map(|e| {
490 let p = e.instance_path().to_string();
491 if p.is_empty() {
492 e.to_string()
493 } else {
494 format!("{e} at {p}")
495 }
496 })
497 .collect();
498 if !errors.is_empty() {
499 return Err(ToolCallInvalid::InvalidArguments {
500 tool_name: inv.tool_name.clone(),
501 problems: errors,
502 example: self
503 .definition(&inv.tool_name)
504 .map(|d| example_from_schema(&d.input_schema))
505 .unwrap_or(json!({})),
506 });
507 }
508 Ok(())
509 }
510
511 pub fn tool_names(&self) -> Vec<String> {
512 let mut n: Vec<String> = self.tools.keys().cloned().collect();
513 n.sort();
514 n
515 }
516
517 pub fn retain_tools<F>(&mut self, mut pred: F)
520 where
521 F: FnMut(&str) -> bool,
522 {
523 self.tools.retain(|n, _| pred(n));
524 self.validators.retain(|n, _| pred(n));
525 self.invalid_schemas.retain(|n, _| pred(n));
526 self.registry.retain_tools(|n| pred(n));
527 }
528
529 pub fn clear_tools(&mut self) {
531 self.tools.clear();
532 self.validators.clear();
533 self.invalid_schemas.clear();
534 self.registry.clear();
535 }
536
537 pub fn unregister_plugin_tools(&mut self) {
538 self.tools.retain(|n, _| !n.starts_with("plugin__"));
539 self.validators.retain(|n, _| !n.starts_with("plugin__"));
540 self.invalid_schemas
541 .retain(|n, _| !n.starts_with("plugin__"));
542 self.registry.unregister_prefix("plugin__");
543 }
544
545 pub fn invalid_tool_result(&self, inv: &ToolInvocation, err: ToolCallInvalid) -> ToolResult {
546 ToolResult {
547 invocation_id: inv.id.clone(),
548 ok: false,
549 output: tool_call_advice(err),
550 }
551 }
552
553 pub fn validate(&self, inv: &ToolInvocation) -> SecurityDecision {
554 if let Err(e) = self.validate_arguments(inv) {
555 return SecurityDecision::Deny(tool_call_advice_message(&e));
556 }
557 let Some(def) = self.definition(&inv.tool_name) else {
558 return SecurityDecision::Deny(format!("unknown `{}`", inv.tool_name));
559 };
560 self.security.validate_tool(&self.policy, &def, inv)
561 }
562
563 pub fn fork_with_policy_and_tools(
566 &self,
567 policy: SecurityPolicy,
568 allowed_tool_names: &[String],
569 ) -> Self {
570 let mut forked = Self::empty_with_security_policy(policy, self.security.clone());
571 forked.harness_profile = self.harness_profile.clone();
572 forked.rewind_store = self.rewind_store.clone();
573 for name in allowed_tool_names {
574 if let Some(tool) = self.tools.get(name) {
575 forked.register_tool(tool.clone());
576 }
577 }
578 forked.retain_tools(|n| n != "subagent" && n != "workflow");
580 forked
581 }
582
583 pub fn policy(&self) -> &SecurityPolicy {
584 &self.policy
585 }
586
587 pub async fn invoke(&self, invocation: ToolInvocation) -> ToolResult {
588 self.invoke_with_event_tx(invocation, None).await
589 }
590
591 pub async fn invoke_with_event_tx(
592 &self,
593 invocation: ToolInvocation,
594 event_tx: Option<mpsc::UnboundedSender<AgentEvent>>,
595 ) -> ToolResult {
596 self.invoke_with_context_inner(
597 invocation,
598 ToolInvocationContext {
599 event_tx,
600 ..Default::default()
601 },
602 false,
603 )
604 .await
605 }
606
607 pub async fn invoke_approved_with_event_tx(
608 &self,
609 invocation: ToolInvocation,
610 event_tx: Option<mpsc::UnboundedSender<AgentEvent>>,
611 ) -> ToolResult {
612 self.invoke_with_context_inner(
613 invocation,
614 ToolInvocationContext {
615 event_tx,
616 ..Default::default()
617 },
618 true,
619 )
620 .await
621 }
622
623 pub async fn invoke_with_full_context(
624 &self,
625 invocation: ToolInvocation,
626 context: ToolInvocationContext,
627 approval_granted: bool,
628 ) -> ToolResult {
629 self.invoke_with_context_inner(invocation, context, approval_granted)
630 .await
631 }
632
633 async fn invoke_with_context_inner(
634 &self,
635 invocation: ToolInvocation,
636 context: ToolInvocationContext,
637 approval_granted: bool,
638 ) -> ToolResult {
639 let event_tx = context.event_tx.clone();
640 let inv_id = invocation.id.clone();
641 let started = std::time::Instant::now();
642 let invocation = self.policy.normalize_invocation_paths(&invocation);
643 let invocation =
647 recover_misnamed_tool_invocation(&invocation, |name| self.definition(name).is_some())
648 .unwrap_or(invocation);
649 let tool_name = invocation.tool_name.clone();
650
651 let tool_def = self.definition(&invocation.tool_name);
653 let tool_kind = tool_def.as_ref().map(|d| d.kind);
654 let tool_verifier_hint = tool_def
655 .as_ref()
656 .and_then(|d| d.metadata.verifier.as_deref())
657 .map(|v| v.to_string());
658 let capability_event_tx = event_tx.clone();
659 emit_capability_events(
660 capability_event_tx.as_ref(),
661 &invocation,
662 tool_def.as_ref(),
663 CapabilityDecision::Requested,
664 "tool invocation requested",
665 );
666
667 if let Err(e) = self.validate_arguments(&invocation) {
668 return self.invalid_tool_result(&invocation, e);
669 }
670 match self.validate(&invocation) {
671 SecurityDecision::Allow => {}
672 SecurityDecision::NeedsApproval(risk) if approval_granted => {
673 tracing::debug!(tool = %invocation.tool_name, ?risk, "tool approval already granted");
674 }
675 SecurityDecision::NeedsApproval(risk) => {
676 let message = format!(
677 "approval required for tool `{}`: {:?}",
678 invocation.tool_name, risk
679 );
680 emit_capability_events(
681 capability_event_tx.as_ref(),
682 &invocation,
683 tool_def.as_ref(),
684 CapabilityDecision::Denied,
685 &message,
686 );
687 return ToolResult {
688 invocation_id: inv_id,
689 ok: false,
690 output: json!({
691 "error_code": "approval_required",
692 "error": message,
693 "message": message,
694 "recoverable": true,
695 "hint": "Approve the tool request or switch permission mode (AcceptEdits/Auto/Yolo) if this should not require approval.",
696 }),
697 };
698 }
699 SecurityDecision::Deny(r) => {
700 emit_capability_events(
701 capability_event_tx.as_ref(),
702 &invocation,
703 tool_def.as_ref(),
704 CapabilityDecision::Denied,
705 &r,
706 );
707 return ToolResult {
708 invocation_id: inv_id,
709 ok: false,
710 output: json!({
711 "error_code": "security_denied",
712 "error": r,
713 "message": r,
714 "recoverable": true,
715 "hint": "Adjust the path/command or permission mode. Restricted mode keeps a project path jail; YOLO/AcceptEdits allow broader agency.",
716 }),
717 };
718 }
719 }
720 let Some(tool) = self.tools.get(&invocation.tool_name).cloned() else {
721 let message = format!("unknown `{}`", invocation.tool_name);
722 return ToolResult {
723 invocation_id: inv_id,
724 ok: false,
725 output: json!({
726 "error_code": "unknown_tool",
727 "error": message,
728 "message": message,
729 "recoverable": true,
730 "hint": "Use a registered tool name, or call tool_search to discover tools.",
731 }),
732 };
733 };
734 if invocation.tool_name == "tool_search" {
735 return self.invoke_tool_search(invocation);
736 }
737
738 let pre_execution_snapshot = if tool_kind == Some(crate::tool::ToolKind::Write) {
739 let paths = self.snapshot_paths_for_invocation(&invocation);
740 if let Some(store) = &self.rewind_store {
742 if !paths.is_empty() {
743 if let Ok(mut store) = store.lock() {
744 if let Err(e) = store.ensure_pre_write_capture(paths.iter().cloned()) {
745 tracing::debug!(error = %e, "rewind: pre-write capture failed");
746 }
747 }
748 }
749 }
750 if paths.is_empty() {
751 None
752 } else {
753 Some(crate::sandbox::SandboxManager::create_snapshot(&paths))
754 }
755 } else {
756 None
757 };
758
759 let inv_input = invocation.input.clone();
762
763 let mut result = match tool.invoke_with_context(invocation, context).await {
764 Ok(r) => truncate_tool_result(r),
765 Err(e) => ToolResult {
766 invocation_id: inv_id.clone(),
767 ok: false,
768 output: json!({"error": format!("{e:#}")}),
769 },
770 };
771
772 if result.ok && tool_kind == Some(crate::tool::ToolKind::Write) {
774 if let Some(store) = &self.rewind_store {
775 let paths = crate::effect::extract_paths(
776 &result,
777 &ToolInvocation {
778 id: inv_id.clone(),
779 tool_name: tool_name.clone(),
780 input: inv_input.clone(),
781 },
782 );
783 let abs_paths: Vec<std::path::PathBuf> = paths
784 .into_iter()
785 .map(|p| self.policy.resolve_project_path(&p))
786 .collect();
787 if !abs_paths.is_empty() {
788 if let Ok(mut store) = store.lock() {
789 store.note_written_paths(abs_paths);
790 }
791 }
792 }
793 }
794
795 if result.ok {
797 let should_check = match tool_kind {
798 Some(crate::tool::ToolKind::Write) => true,
799 Some(crate::tool::ToolKind::Command) => true,
800 _ => false,
801 };
802
803 if should_check {
804 let paths = crate::effect::extract_paths(
805 &result,
806 &ToolInvocation {
807 id: inv_id.clone(),
808 tool_name: tool_name.clone(),
809 input: inv_input,
810 },
811 );
812
813 if !paths.is_empty() {
814 let command = None;
815 let decision = self
816 .policy
817 .post_execution_effect_check(&tool_name, &paths, command);
818
819 match decision {
820 PostDecision::Allow => {
821 }
823 PostDecision::Ask(reason) => {
824 tracing::warn!(
825 tool = %tool_name,
826 reason = %reason,
827 "post-execution effect check: ask user"
828 );
829 if let Value::Object(ref mut map) = result.output {
830 map.insert(
831 "effect_warning".to_string(),
832 json!({
833 "decision": "ask",
834 "message": reason,
835 }),
836 );
837 }
838 }
839 PostDecision::Deny(reason) => {
840 tracing::warn!(
841 tool = %tool_name,
842 reason = %reason,
843 "post-execution effect check: denied"
844 );
845 let rollback = pre_execution_snapshot
846 .as_ref()
847 .map(crate::sandbox::SandboxManager::rollback);
848 let (rolled_back, rollback_error) = rollback_outcome(rollback);
849 emit_capability_events(
850 capability_event_tx.as_ref(),
851 &ToolInvocation {
852 id: inv_id.clone(),
853 tool_name: tool_name.clone(),
854 input: json!({}),
855 },
856 tool_def.as_ref(),
857 CapabilityDecision::Violated,
858 &reason,
859 );
860 return ToolResult {
861 invocation_id: inv_id,
862 ok: false,
863 output: json!({
864 "error": reason,
865 "error_code": "effect_denied",
866 "rolled_back": rolled_back,
867 "rollback_error": rollback_error,
868 }),
869 };
870 }
871 PostDecision::Rollback(reason) => {
872 tracing::warn!(
873 tool = %tool_name,
874 reason = %reason,
875 "post-execution effect check: rollback recommended"
876 );
877 let rollback = pre_execution_snapshot
878 .as_ref()
879 .map(crate::sandbox::SandboxManager::rollback);
880 let (rolled_back, rollback_error) = rollback_outcome(rollback);
881 emit_capability_events(
882 capability_event_tx.as_ref(),
883 &ToolInvocation {
884 id: inv_id.clone(),
885 tool_name: tool_name.clone(),
886 input: json!({}),
887 },
888 tool_def.as_ref(),
889 CapabilityDecision::Violated,
890 &reason,
891 );
892 return ToolResult {
893 invocation_id: inv_id,
894 ok: false,
895 output: json!({
896 "error": reason,
897 "error_code": "effect_rollback",
898 "rolled_back": rolled_back,
899 "rollback_error": rollback_error,
900 }),
901 };
902 }
903 }
904 }
905 }
906 }
907
908 emit_capability_events(
909 capability_event_tx.as_ref(),
910 &ToolInvocation {
911 id: inv_id.clone(),
912 tool_name: tool_name.clone(),
913 input: json!({}),
914 },
915 tool_def.as_ref(),
916 if result.ok {
917 CapabilityDecision::Consumed
918 } else {
919 CapabilityDecision::Violated
920 },
921 if result.ok {
922 "tool invocation completed"
923 } else {
924 "tool invocation failed"
925 },
926 );
927
928 if result.ok && tool_kind == Some(crate::tool::ToolKind::Write) {
932 if let Some(verifier_cmd) = tool_verifier_hint {
933 if let Value::Object(ref mut map) = result.output {
934 map.insert(
935 "verifier_hint".to_string(),
936 json!({
937 "suggested": true,
938 "command": verifier_cmd,
939 "message": format!(
940 "After writing, verify with: verifier(action='run', verifier='command', command='{}')",
941 verifier_cmd
942 ),
943 }),
944 );
945 }
946 }
947 }
948
949 tracing::info!(tool = %tool_name, ok = result.ok, dur_ms = started.elapsed().as_millis() as u64, "invoke finished");
950 result
951 }
952
953 fn invoke_tool_search(&self, invocation: ToolInvocation) -> ToolResult {
954 let query = invocation
955 .input
956 .get("query")
957 .and_then(Value::as_str)
958 .unwrap_or_default()
959 .to_string();
960 let max_results = invocation
961 .input
962 .get("max_results")
963 .and_then(Value::as_u64)
964 .unwrap_or(10)
965 .min(50) as usize;
966 let results = self.registry.search(&query, max_results);
967 let results = results
968 .into_iter()
969 .map(model_friendly_definition)
970 .map(|def| {
971 json!({
972 "name": def.name,
973 "description": def.description,
974 "kind": def.kind,
975 "metadata": def.metadata,
976 "input_schema": def.input_schema,
977 })
978 })
979 .collect::<Vec<_>>();
980
981 ToolResult {
982 invocation_id: invocation.id,
983 ok: true,
984 output: json!({
985 "query": query,
986 "results": results,
987 "total": results.len(),
988 "hint": if results.is_empty() {
989 "No tools found. Try broader terms like: code, browser, package, memory, subagent, sandbox, goal."
990 } else {
991 "These tools may be deferred (not always in the schema). Call a returned tool by its `name` with arguments matching `input_schema`."
992 },
993 "power_catalog": [
994 "code / code_edit / ast_search / symbol_*: symbols and structured code nav",
995 "repo_explore: BM25 semantic search",
996 "package_manager: dependency install/add/update",
997 "browser: headless UI testing",
998 "subagent: nested agent",
999 "apply_patch / sandbox / set_goal / history_ops: advanced workflows",
1000 ],
1001 }),
1002 }
1003 }
1004
1005 fn snapshot_paths_for_invocation(
1006 &self,
1007 invocation: &ToolInvocation,
1008 ) -> Vec<std::path::PathBuf> {
1009 let mut paths = Vec::new();
1010 for key in ["path", "file", "file_path"] {
1011 if let Some(path) = invocation.input.get(key).and_then(Value::as_str) {
1012 push_unique_snapshot_path(
1013 &mut paths,
1014 self.policy.resolve_project_path(Path::new(path)),
1015 );
1016 }
1017 }
1018
1019 if let Some(patch) = invocation.input.get("patch").and_then(Value::as_str) {
1020 for path in crate::security::extract_apply_patch_paths(patch) {
1021 push_unique_snapshot_path(
1022 &mut paths,
1023 self.policy.resolve_project_path(Path::new(&path)),
1024 );
1025 }
1026 }
1027 if let Some(patches) = invocation.input.get("patches").and_then(Value::as_array) {
1028 for patch in patches.iter().filter_map(Value::as_str) {
1029 for path in crate::security::extract_apply_patch_paths(patch) {
1030 push_unique_snapshot_path(
1031 &mut paths,
1032 self.policy.resolve_project_path(Path::new(&path)),
1033 );
1034 }
1035 }
1036 }
1037
1038 paths
1039 }
1040
1041 pub async fn list_background_commands(&self) -> Vec<background::BackgroundCommandSnapshot> {
1043 let r = self
1044 .invoke(ToolInvocation {
1045 id: "bg-list".into(),
1046 tool_name: "bash".into(),
1047 input: json!({"action": "list"}),
1048 })
1049 .await;
1050 r.output
1051 .get("tasks")
1052 .and_then(|v| v.as_array())
1053 .map(|a| {
1054 a.iter()
1055 .filter_map(background::BackgroundCommandSnapshot::from_json)
1056 .collect()
1057 })
1058 .unwrap_or_default()
1059 }
1060
1061 pub async fn poll_background_command(
1063 &self,
1064 task_id: &str,
1065 ) -> Option<background::BackgroundCommandSnapshot> {
1066 let r = self
1067 .invoke(ToolInvocation {
1068 id: "bg-poll".into(),
1069 tool_name: "bash".into(),
1070 input: json!({"task_id": task_id}),
1071 })
1072 .await;
1073 if r.ok {
1074 background::BackgroundCommandSnapshot::from_json(&r.output)
1075 } else {
1076 None
1077 }
1078 }
1079
1080 pub async fn cancel_background_command(
1082 &self,
1083 task_id: &str,
1084 ) -> Option<background::BackgroundCommandSnapshot> {
1085 let r = self
1086 .invoke(ToolInvocation {
1087 id: "bg-cancel".into(),
1088 tool_name: "bash".into(),
1089 input: json!({"task_id": task_id, "action": "cancel"}),
1090 })
1091 .await;
1092 if r.ok {
1093 background::BackgroundCommandSnapshot::from_json(&r.output)
1094 } else {
1095 None
1096 }
1097 }
1098
1099 fn register(&mut self, tool: impl Tool + 'static) {
1100 self.register_tool(Arc::new(tool));
1101 }
1102
1103 fn enriched_definition(&self, tool: &dyn Tool) -> ToolDefinition {
1104 let mut def = tool.definition();
1105 if let Some(registered) = self.registry.get(&def.name) {
1106 def.metadata = registered.definition.metadata.clone();
1107 }
1108 def
1109 }
1110
1111 fn register_builtin_tools(&mut self) {
1112 let pr = self.policy.project_root().to_path_buf();
1113 self.register(ReadTool::new(pr.clone())); self.register(ReadTool::alias(pr.clone(), "read")); self.register(SearchTool::new(pr.clone()));
1116 self.register(SearchTool::grep(pr.clone()));
1117 self.register(SearchTool::fs_browser(pr.clone()));
1118 self.register(SearchTool::list_dir(pr.clone()));
1119 self.register(SearchTool::glob(pr.clone()));
1120 self.register(EditTool::new(pr.clone()));
1121 self.register(MultiEditTool::new(pr.clone()));
1122 self.register(WriteTool::new(pr.clone()));
1123 self.register(WriteTool::write_file(pr.clone()));
1124 self.register(WriteTool::apply_patch(pr.clone()));
1125 self.register(BashTool::new(pr.clone()));
1126 self.register(QuestionTool);
1127 self.register(PlanTool::new(self.policy.clone()));
1128 self.register(PackageManagerTool::new(pr.clone()));
1129 self.register(RuntimeInfoTool::new(
1130 self.policy.clone(),
1131 self.harness_profile.clone(),
1132 ));
1133 #[cfg(feature = "code-vfs")]
1134 {
1135 self.register(CodeReadTool::new(self.policy.clone()));
1136 self.register(CodeEditTool::new(self.policy.clone()));
1137 }
1138 self.register(CodeExecTool::new(self.policy.clone()));
1139 self.register(RepoIntelligenceTool::new(
1140 self.policy.clone(),
1141 RepoIntelligenceAction::AstSearch,
1142 ));
1143 self.register(RepoIntelligenceTool::new(
1144 self.policy.clone(),
1145 RepoIntelligenceAction::SymbolGoto,
1146 ));
1147 self.register(RepoIntelligenceTool::new(
1148 self.policy.clone(),
1149 RepoIntelligenceAction::SymbolReferences,
1150 ));
1151 self.register(RepoIntelligenceTool::new(
1152 self.policy.clone(),
1153 RepoIntelligenceAction::DependencyGraph,
1154 ));
1155 self.register(RepoIntelligenceTool::new(
1156 self.policy.clone(),
1157 RepoIntelligenceAction::TestDiscovery,
1158 ));
1159 self.register(RepoIntelligenceTool::new(
1160 self.policy.clone(),
1161 RepoIntelligenceAction::OwnershipChurn,
1162 ));
1163 self.register(InitSessionTool::new(self.policy.clone()));
1164 self.register(MarkFeatureDoneTool::new(self.policy.clone()));
1165 self.register(AppendNoteTool::new(pr.clone()));
1166 self.register(MemoryTool::new(pr.clone()));
1167 self.register(HistoryOpsTool::new(pr.clone()));
1168 self.register(CurrentTimeTool::new());
1169 self.register(SleepTool::new());
1170 self.register(ContextRemainingTool::new(pr.clone()));
1173 self.register(RequestUserInputTool::new());
1174 self.register(SandboxTool::new(pr.clone()));
1175 let data_dir = self.policy.data_dir().to_path_buf();
1176 self.register(ViewImageTool::new(pr.clone(), data_dir.clone()));
1177 self.register(ViewImageTool::inspect_image(pr.clone(), data_dir));
1178 #[cfg(feature = "browser")]
1179 self.register(BrowserTool::new(pr.clone()));
1180 self.register(NewContextWindowTool::new());
1181 self.register(ToolSearchTool::new(Arc::new(self.registry.clone())));
1182 self.register(WorkflowTool::new(
1185 self.policy.clone(),
1186 crate::config::WorkflowConfig::default(),
1187 ));
1188 }
1189}
1190
1191fn tool_call_advice(err: ToolCallInvalid) -> Value {
1192 match err {
1194 ToolCallInvalid::UnknownTool {
1195 tool_name,
1196 available_tools,
1197 } => {
1198 let message = "Requested tool is not registered. Use one of the available tool names."
1199 .to_string();
1200 json!({
1201 "error_code": "unknown_tool",
1202 "error_kind": "unknown_tool",
1203 "tool": tool_name,
1204 "error": message,
1205 "message": message,
1206 "hint": "Call tool_search or use a name from available_tools.",
1207 "recoverable": true,
1208 "suggestions": suggest_tool_replacements(&tool_name, &available_tools),
1209 "available_tools": available_tools.into_iter().take(20).collect::<Vec<_>>(),
1210 })
1211 }
1212 ToolCallInvalid::InvalidSchema { tool_name, message } => {
1213 let message = format!("Tool schema is invalid: {message}");
1214 json!({
1215 "error_code": "invalid_schema",
1216 "error_kind": "invalid_schema",
1217 "tool": tool_name,
1218 "error": message,
1219 "message": message,
1220 "recoverable": false,
1221 })
1222 }
1223 ToolCallInvalid::MalformedArguments {
1224 tool_name,
1225 raw_arguments_preview,
1226 example,
1227 } => {
1228 let message = "Tool arguments were not valid JSON. Emit one complete JSON object matching the schema before calling the tool again.".to_string();
1229 json!({
1230 "error_code": "invalid_arguments",
1231 "error_kind": "malformed_arguments",
1232 "tool": tool_name,
1233 "error": message,
1234 "message": message,
1235 "hint": "Emit a single complete JSON object; do not wrap arguments in markdown fences.",
1236 "recoverable": true,
1237 "raw_arguments_preview": raw_arguments_preview,
1238 "example": example,
1239 })
1240 }
1241 ToolCallInvalid::InvalidArguments {
1242 tool_name,
1243 problems,
1244 example,
1245 } => {
1246 let message = "Tool arguments do not match the JSON schema. Fix the arguments and call the tool again.".to_string();
1247 json!({
1248 "error_code": "invalid_arguments",
1249 "error_kind": "invalid_arguments",
1250 "tool": tool_name,
1251 "error": message,
1252 "message": message,
1253 "hint": "Compare your arguments to the tool schema and the example.",
1254 "recoverable": true,
1255 "problems": problems,
1256 "example": example,
1257 })
1258 }
1259 }
1260}
1261
1262fn tool_call_advice_message(err: &ToolCallInvalid) -> String {
1263 match err {
1264 ToolCallInvalid::UnknownTool { tool_name, .. } => format!("unknown tool `{tool_name}`"),
1265 ToolCallInvalid::InvalidSchema { tool_name, message } => {
1266 format!("invalid schema for `{tool_name}`: {message}")
1267 }
1268 ToolCallInvalid::MalformedArguments { tool_name, .. } => {
1269 format!("malformed args for `{tool_name}`")
1270 }
1271 ToolCallInvalid::InvalidArguments {
1272 tool_name,
1273 problems,
1274 ..
1275 } => format!("invalid args for `{tool_name}`: {}", problems.join("; ")),
1276 }
1277}
1278
1279fn model_friendly_definition(mut definition: ToolDefinition) -> ToolDefinition {
1280 definition.input_schema = simplify_schema_for_model(&definition.input_schema);
1281 definition
1282}
1283
1284fn simplify_schema_for_model(schema: &Value) -> Value {
1285 match schema {
1286 Value::Object(object) => simplify_schema_object_for_model(object),
1287 Value::Array(values) => {
1288 Value::Array(values.iter().map(simplify_schema_for_model).collect())
1289 }
1290 value => value.clone(),
1291 }
1292}
1293
1294fn simplify_schema_object_for_model(object: &Map<String, Value>) -> Value {
1295 let mut simplified = Map::new();
1296
1297 for keyword in ["oneOf", "anyOf", "allOf"] {
1298 let Some(branches) = object.get(keyword).and_then(Value::as_array) else {
1299 continue;
1300 };
1301 if let Some(Value::Object(branch)) = branches.first().map(simplify_schema_for_model) {
1302 simplified.extend(branch);
1303 }
1304 break;
1305 }
1306
1307 for (key, value) in object {
1308 if matches!(key.as_str(), "oneOf" | "anyOf" | "allOf" | "const") {
1309 continue;
1310 }
1311 simplified.insert(key.clone(), simplify_schema_for_model(value));
1312 }
1313
1314 Value::Object(simplified)
1315}
1316
1317fn suggest_tool_replacements(tool_name: &str, available_tools: &[String]) -> Vec<String> {
1318 let lower = tool_name.trim().to_ascii_lowercase();
1319 let candidates: &[&str] = match lower.as_str() {
1320 "list_files" | "ls" | "listdir" | "dir" | "list" | "list_dir" | "find" | "find_files"
1321 | "grep" | "glob" | "fs_browser" => &["search"],
1322 "cat" | "type" | "open" | "view_file" | "read" | "view" => &["read_file"],
1323 "patch" | "str_replace" | "search_replace" | "searchreplace" | "multiedit"
1324 | "multi_edit" | "multi-edit" | "batched_edit" | "apply_patch" => &["edit", "write_file"],
1325 "request_user_input" | "ask_user" | "ask" => &["question"],
1326 "shell" | "run" | "terminal" | "exec" | "sh" | "cmd" | "process" => &["bash"],
1327 "rg" | "ripgrep" | "search_code" => &["search", "code"],
1328 "symbols" | "symbol" | "symbols_overview" | "find_symbol" | "find_references"
1329 | "code_diagnostics" => &["code", "ast_search", "symbol_goto"],
1330 "replace_symbol_body"
1331 | "insert_before_symbol"
1332 | "insert_after_symbol"
1333 | "rename_symbol" => &["code_edit"],
1334 _ if looks_like_filesystem_path(tool_name) => &["read_file", "fs_browser", "grep", "bash"],
1335 _ => &[],
1336 };
1337 candidates
1338 .iter()
1339 .filter(|candidate| available_tools.iter().any(|tool| tool == **candidate))
1340 .map(|candidate| (*candidate).to_string())
1341 .collect()
1342}
1343
1344fn recover_misnamed_tool_invocation(
1347 inv: &ToolInvocation,
1348 has_tool: impl Fn(&str) -> bool,
1349) -> Option<ToolInvocation> {
1350 if has_tool(&inv.tool_name) {
1351 return None;
1352 }
1353
1354 let name = inv.tool_name.trim();
1355 if name.is_empty() {
1356 return None;
1357 }
1358
1359 let input = inv.input.as_object();
1360 let action = input
1361 .and_then(|obj| obj.get("action"))
1362 .and_then(Value::as_str)
1363 .map(|s| s.to_ascii_lowercase());
1364 let has_path_field = input
1365 .and_then(|obj| obj.get("path"))
1366 .and_then(Value::as_str)
1367 .is_some_and(|p| !p.is_empty());
1368 let path_from_input = input
1369 .and_then(|obj| obj.get("path"))
1370 .and_then(Value::as_str)
1371 .map(str::to_string);
1372 let path_like_name = looks_like_filesystem_path(name);
1373
1374 if matches!(
1377 action.as_deref(),
1378 Some("list" | "tree" | "find" | "stat" | "read" | "search")
1379 ) && has_tool("fs_browser")
1380 {
1381 let mut recovered = inv.clone();
1382 recovered.tool_name = "fs_browser".to_string();
1383 if !has_path_field && path_like_name {
1384 if let Some(obj) = recovered.input.as_object_mut() {
1385 obj.insert("path".to_string(), Value::String(name.to_string()));
1386 }
1387 }
1388 tracing::info!(
1389 from = %inv.tool_name,
1390 to = "fs_browser",
1391 "recovered misnamed tool invocation"
1392 );
1393 return Some(recovered);
1394 }
1395
1396 if path_like_name && has_tool("read_file") {
1399 let keys: Vec<&str> = input
1400 .map(|obj| obj.keys().map(String::as_str).collect())
1401 .unwrap_or_default();
1402 let readish = keys.is_empty()
1403 || keys.iter().all(|k| {
1404 matches!(
1405 *k,
1406 "path"
1407 | "offset"
1408 | "limit"
1409 | "start_line"
1410 | "end_line"
1411 | "max_bytes"
1412 | "encoding"
1413 )
1414 });
1415 let writeish = keys
1417 .iter()
1418 .any(|k| matches!(*k, "content" | "patch" | "patches" | "edits" | "new_string"));
1419 if readish && !writeish {
1420 let mut recovered = inv.clone();
1421 recovered.tool_name = "read_file".to_string();
1422 if !has_path_field {
1423 let mut obj = serde_json::Map::new();
1424 obj.insert("path".to_string(), Value::String(name.to_string()));
1425 if let Some(input_obj) = input {
1426 for (k, v) in input_obj {
1427 if k != "path" {
1428 obj.insert(k.clone(), v.clone());
1429 }
1430 }
1431 }
1432 recovered.input = Value::Object(obj);
1433 } else if path_from_input.as_deref() != Some(name)
1434 && path_from_input
1435 .as_deref()
1436 .is_some_and(|p| p == "." || p.is_empty())
1437 {
1438 if let Some(obj) = recovered.input.as_object_mut() {
1440 obj.insert("path".to_string(), Value::String(name.to_string()));
1441 }
1442 }
1443 tracing::info!(
1444 from = %inv.tool_name,
1445 to = "read_file",
1446 "recovered misnamed tool invocation"
1447 );
1448 return Some(recovered);
1449 }
1450 }
1451
1452 None
1453}
1454
1455fn looks_like_filesystem_path(name: &str) -> bool {
1456 let name = name.trim();
1457 if name.is_empty() {
1458 return false;
1459 }
1460 if name == "." || name == ".." {
1461 return true;
1462 }
1463 if name.contains('/') || name.contains('\\') {
1464 return true;
1465 }
1466 if let Some((_, ext)) = name.rsplit_once('.') {
1468 let ext = ext.trim();
1469 if !ext.is_empty()
1470 && ext.len() <= 12
1471 && ext
1472 .chars()
1473 .all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '-')
1474 && !name.starts_with('.')
1475 && (ext.len() <= 8)
1478 {
1479 return true;
1480 }
1481 }
1482 false
1483}
1484
1485fn push_unique_snapshot_path(paths: &mut Vec<std::path::PathBuf>, path: std::path::PathBuf) {
1486 if !paths.contains(&path) {
1487 paths.push(path);
1488 }
1489}
1490
1491fn rollback_outcome(rollback: Option<std::result::Result<(), String>>) -> (bool, Option<String>) {
1492 match rollback {
1493 Some(Ok(())) => (true, None),
1494 Some(Err(error)) => (false, Some(error)),
1495 None => (false, None),
1496 }
1497}
1498
1499fn emit_capability_events(
1500 event_tx: Option<&mpsc::UnboundedSender<AgentEvent>>,
1501 invocation: &ToolInvocation,
1502 definition: Option<&ToolDefinition>,
1503 decision: CapabilityDecision,
1504 justification: &str,
1505) {
1506 let Some(event_tx) = event_tx else {
1507 return;
1508 };
1509 let Some(definition) = definition else {
1510 return;
1511 };
1512 for capability in capabilities_from_tool_metadata(&definition.metadata.capabilities) {
1513 let _ = event_tx.send(AgentEvent::CapabilityRecorded(CapabilityLedgerEntry {
1514 capability,
1515 scope: CapabilityScope::SingleCall(invocation.id.clone()),
1516 decision: decision.clone(),
1517 at_ms: tool_unix_millis(),
1518 justification: format!("{}: {justification}", invocation.tool_name),
1519 }));
1520 }
1521}
1522
1523fn tool_unix_millis() -> u64 {
1524 std::time::SystemTime::now()
1525 .duration_since(std::time::UNIX_EPOCH)
1526 .map(|duration| duration.as_millis() as u64)
1527 .unwrap_or(0)
1528}
1529
1530pub fn example_from_schema(schema: &Value) -> Value {
1531 if let Some(ex) = schema
1532 .get("examples")
1533 .and_then(Value::as_array)
1534 .and_then(|a| a.first())
1535 {
1536 return ex.clone();
1537 }
1538 let Some(properties) = schema.get("properties").and_then(Value::as_object) else {
1539 return json!({});
1540 };
1541 let required: Vec<&str> = schema
1542 .get("required")
1543 .and_then(Value::as_array)
1544 .into_iter()
1545 .flatten()
1546 .filter_map(Value::as_str)
1547 .collect();
1548 let mut ex = serde_json::Map::new();
1549 for field in required {
1550 let v = properties
1551 .get(field)
1552 .and_then(|p| p.get("type"))
1553 .and_then(Value::as_str)
1554 .map(|k| match k {
1555 "integer" => json!(1),
1556 "number" => json!(1.0),
1557 "boolean" => json!(true),
1558 "array" => json!([]),
1559 "object" => json!({}),
1560 _ => json!("example"),
1561 })
1562 .unwrap_or(json!("example"));
1563 ex.insert(field.to_string(), v);
1564 }
1565 Value::Object(ex)
1566}