1pub mod a2a;
9pub mod agent;
10pub mod agent_defs;
11pub mod audit;
12pub mod checkpoint;
13pub mod client_fs;
14pub mod count_lines;
15pub mod dispatch;
16#[cfg(feature = "cloud-runtime")]
17pub mod docker_provider;
18#[cfg(feature = "cloud-runtime")]
19pub mod docker_sandbox;
20#[cfg(feature = "e2b-sandbox")]
21pub mod e2b_provider;
22pub mod edit;
23pub mod episodic_recall;
24pub mod estimate_tokens;
25pub mod facts;
26pub mod fs;
27pub mod glob;
28#[cfg(feature = "skill-hub")]
29pub mod install_skill;
30pub mod load_skill;
31pub mod memory;
32pub mod permission_pipeline;
33pub mod plan_mode;
34pub mod policy;
35pub mod policy_sandbox;
36pub mod registry;
37pub mod run_background;
38pub mod schedule_wakeup;
39pub mod search;
40pub mod send_message;
41pub mod shell;
42pub mod stop_loop;
43#[cfg(feature = "coordinator-mode")]
44pub mod task_create;
45#[cfg(feature = "coordinator-mode")]
46pub mod task_get;
47#[cfg(feature = "coordinator-mode")]
48pub mod task_list;
49#[cfg(feature = "coordinator-mode")]
50pub mod task_output;
51#[cfg(feature = "coordinator-mode")]
52pub mod task_stop;
53#[cfg(feature = "coordinator-mode")]
54pub mod task_update;
55#[cfg(feature = "coordinator-mode")]
56pub mod team_create;
57#[cfg(feature = "coordinator-mode")]
58pub mod team_delete;
59pub mod todo;
60pub mod tool_search;
61pub mod transport;
62pub(crate) mod url_guard;
63pub mod watch_file;
64#[cfg(feature = "web_fetch")]
65pub mod web_fetch;
66#[cfg(feature = "web_search")]
67pub mod web_search;
68
69pub use registry::{
72 build_standard_tools, build_standard_tools_with_roots, PermissionHook, SpecWithHint, Tool,
73 ToolRegistry,
74};
75
76pub use audit::{
79 AuditKey, AuditMeta, ExitStatus, ToolSideEffect, TouchedFiles, AUDIT_ERR_MAX_BYTES,
80};
81
82pub use dispatch::resolve_within;
85pub use dispatch::{args_preview_for_permission, ToolDispatch};
86pub use dispatch::{new_shared_sandbox_roots, resolve_within_any, AccessTier, SharedSandboxRoots};
87
88pub use a2a::{A2aCallTool, A2aCardTool, A2aTaskCheckTool};
91pub use agent::{AgentTool, SharedMemoryRead, SharedMemoryWrite};
92pub use agent_defs::{AgentDefinition, AgentDefinitions};
93pub use checkpoint::{
94 build_checkpoint_save_tool, build_checkpoint_tools, CheckpointDiff, CheckpointList,
95 CheckpointSave, CheckpointSaveCtx, CheckpointToolCtx,
96};
97pub use client_fs::{AcpClientFsState, ClientReadFile, ClientWriteFile};
98pub use count_lines::CountLines;
99pub use edit::EditTool;
100pub use episodic_recall::{episodic_recall_summary, EpisodicRecall};
101pub use estimate_tokens::EstimateTokens;
102pub use facts::{
103 facts_path, facts_summary, load_facts, search_facts, Fact, FactStore, ForgetFact, RecallFact,
104 RememberFact, ScoredFact, UpdateFact,
105};
106pub use fs::{ReadFile, ReadFileState, WriteFile};
107pub use glob::GlobTool;
108#[cfg(feature = "skill-hub")]
109pub use install_skill::InstallSkill;
110pub use load_skill::LoadSkill;
111pub use memory::{
112 load_scratchpad, scratchpad_path, scratchpad_summary, Scratchpad, ScratchpadDelete,
113 ScratchpadGet, ScratchpadList, WorkingMemoryTool,
114};
115pub use memory::{Forget, Recall, Remember};
116pub use permission_pipeline::{CheckOutcome, PermissionPipeline};
117pub use plan_mode::{
118 EnterPlanModeTool, ExitPlanModeTool, PlanApprovalGate, PlanApprovalResult, PlanModeRequestGate,
119 PlanModeRequestResult, RequestPlanModeTool,
120};
121pub use policy_sandbox::{FsPolicy, PolicyConfig, ShellPolicy};
122pub use run_background::LoopControl;
123pub use run_background::{
124 BackgroundJobManager, CheckBackground, Job, JobState, RunBackground, WatchTarget,
125};
126pub use schedule_wakeup::{ScheduleWakeup, WakeupRequest, WakeupSlot};
127pub use search::SearchFiles;
128pub use send_message::{ListWorkersTool, SendMessageTool, WorkerMailbox, WorkerRegistry};
129pub use shell::RunShell;
130pub use stop_loop::StopLoop;
131pub use watch_file::WatchFile;
132
133#[cfg(feature = "coordinator-mode")]
134pub use task_create::TaskCreateTool;
135#[cfg(feature = "coordinator-mode")]
136pub use task_get::TaskGetTool;
137#[cfg(feature = "coordinator-mode")]
138pub use task_list::TaskListTool;
139#[cfg(feature = "coordinator-mode")]
140pub use task_output::TaskOutputTool;
141#[cfg(feature = "coordinator-mode")]
142pub use task_stop::TaskStopTool;
143#[cfg(feature = "coordinator-mode")]
144pub use task_update::TaskUpdateTool;
145#[cfg(feature = "coordinator-mode")]
146pub use team_create::TeamCreateTool;
147#[cfg(feature = "coordinator-mode")]
148pub use team_delete::TeamDeleteTool;
149pub use todo::{TodoItem, TodoStatus, TodoWriteTool};
150pub use tool_search::{DeferredCatalog, ToolSearchTool, TOOL_SEARCH_TOOL_NAME};
151pub use transport::{DirEntry, ExecResult, LocalTransport, ReadResult, ToolTransport};
152#[cfg(feature = "web_fetch")]
153pub use web_fetch::WebFetch;
154#[cfg(feature = "web_search")]
155pub use web_search::WebSearch;
156
157#[cfg(test)]
160mod tests {
161 use super::registry::{PermissionHook, Tool, ToolRegistry};
162 use crate::error::{Error, Result};
163 use crate::llm::ToolSpec;
164 use crate::permissions::PermissionMode;
165 use async_trait::async_trait;
166 use serde_json::Value;
167 use std::sync::Arc;
168
169 struct Echo;
170
171 #[async_trait]
172 impl Tool for Echo {
173 fn spec(&self) -> ToolSpec {
174 ToolSpec {
175 name: "echo".into(),
176 description: "echo".into(),
177 parameters: serde_json::json!({"type":"object","properties":{"msg":{"type":"string"}}}),
178 }
179 }
180 async fn execute(&self, args: Value) -> Result<String> {
181 Ok(args["msg"].as_str().unwrap_or("").into())
182 }
183 }
184
185 #[tokio::test]
186 async fn registry_dispatches_and_errors_on_unknown() {
187 let reg = ToolRegistry::local().register(Arc::new(Echo));
188 let out = reg
189 .invoke("echo", serde_json::json!({"msg":"hi"}))
190 .await
191 .unwrap();
192 assert_eq!(out, "hi");
193 let err = reg.invoke("nope", serde_json::json!({})).await.unwrap_err();
194 assert!(matches!(err, Error::UnknownTool(_)));
195 }
196
197 #[test]
198 fn resolve_within_rejects_escape() {
199 let root = std::path::Path::new("/work");
200 assert!(super::dispatch::resolve_within(root, "../etc/passwd").is_err());
201 assert!(super::dispatch::resolve_within(root, "/elsewhere").is_err());
202 assert!(super::dispatch::resolve_within(root, "src/lib.rs").is_ok());
203 }
204
205 #[test]
206 fn resolve_within_handles_relative_root() {
207 let cwd = std::env::current_dir().unwrap();
209 let resolved =
210 super::dispatch::resolve_within(std::path::Path::new("."), "src/lib.rs").unwrap();
211 assert!(resolved.starts_with(&cwd));
212 assert!(resolved.ends_with("src/lib.rs"));
213 }
214
215 #[tokio::test]
216 async fn test_permission_deny_blocks_invoke() {
217 let config = crate::permissions::LayeredPermissionsConfig {
218 mode: PermissionMode::Default,
219 layers: vec![crate::permissions::PermissionLayer {
220 source: crate::permissions::RuleSource::User,
221 allow: vec!["echo".into()],
222 deny: vec!["echo".into()],
223 ..Default::default()
224 }],
225 };
226 let reg = ToolRegistry::local()
227 .with_permissions(config)
228 .register(Arc::new(Echo));
229 let err = reg
230 .invoke("echo", serde_json::json!({"msg":"hi"}))
231 .await
232 .unwrap_err();
233 assert!(matches!(err, Error::PermissionDenied { .. }));
234 }
235
236 struct AllowHook;
239 struct DenyHook;
240
241 #[async_trait]
242 impl PermissionHook for AllowHook {
243 async fn check(
244 &self,
245 _name: &str,
246 _args: &serde_json::Value,
247 ) -> crate::agent::PermissionDecision {
248 crate::agent::PermissionDecision::Allow
249 }
250 }
251
252 #[async_trait]
253 impl PermissionHook for DenyHook {
254 async fn check(
255 &self,
256 _name: &str,
257 _args: &serde_json::Value,
258 ) -> crate::agent::PermissionDecision {
259 crate::agent::PermissionDecision::Deny("denied by test hook".to_string())
260 }
261 }
262
263 #[tokio::test]
264 async fn permission_hook_allow_lets_tool_run() {
265 let reg = ToolRegistry::local()
266 .with_permission_hook(Arc::new(AllowHook))
267 .register(Arc::new(Echo));
268 let result = reg
269 .invoke("echo", serde_json::json!({"msg": "hello"}))
270 .await
271 .unwrap();
272 assert_eq!(result, "hello");
273 }
274
275 #[tokio::test]
276 async fn permission_hook_deny_blocks_invoke() {
277 let reg = ToolRegistry::local()
278 .with_permission_hook(Arc::new(DenyHook))
279 .register(Arc::new(Echo));
280 let err = reg
281 .invoke("echo", serde_json::json!({"msg": "blocked"}))
282 .await
283 .unwrap_err();
284 assert!(matches!(err, Error::PermissionDenied { .. }));
285 }
286
287 #[test]
288 fn args_preview_truncates_long_strings() {
289 let big_val = "x".repeat(200);
290 let args = serde_json::json!({"command": big_val});
291 let preview = super::dispatch::args_preview_for_permission(&args);
292 assert!(preview.chars().count() <= 81); }
294
295 #[tokio::test]
299 async fn headless_interactive_tool_denied_without_hooks() {
300 let config = crate::permissions::LayeredPermissionsConfig {
301 mode: PermissionMode::Default,
302 layers: vec![crate::permissions::PermissionLayer {
303 source: crate::permissions::RuleSource::User,
304 interactive: vec!["echo".into()],
305 ..Default::default()
306 }],
307 };
308 let reg = ToolRegistry::local()
309 .with_permissions(config)
310 .with_headless(true)
311 .register(Arc::new(Echo));
312 let err = reg
313 .invoke("echo", serde_json::json!({"msg": "hi"}))
314 .await
315 .unwrap_err();
316 assert!(matches!(err, Error::PermissionDenied { .. }));
317 }
318
319 #[tokio::test]
321 async fn headless_interactive_tool_allowed_by_hook() {
322 use tempfile::tempdir;
323 let tmp = tempdir().unwrap();
324 let hook_path = tmp.path().join("allow.sh");
325 let script = "#!/bin/sh\nread -r _\necho '{\"action\":\"continue\"}'\n";
326 std::fs::write(&hook_path, script).unwrap();
327 #[cfg(unix)]
328 {
329 use std::os::unix::fs::PermissionsExt;
330 let mut perms = std::fs::metadata(&hook_path).unwrap().permissions();
331 perms.set_mode(0o755);
332 std::fs::set_permissions(&hook_path, perms).unwrap();
333 }
334 let hook_runner = crate::hooks::ExternalHookRunner::discover(&[tmp.path().to_path_buf()]);
335
336 let config = crate::permissions::LayeredPermissionsConfig {
337 mode: PermissionMode::Default,
338 layers: vec![crate::permissions::PermissionLayer {
339 source: crate::permissions::RuleSource::User,
340 interactive: vec!["echo".into()],
341 ..Default::default()
342 }],
343 };
344 let reg = ToolRegistry::local()
345 .with_permissions(config)
346 .with_headless(true)
347 .with_hook_runner(hook_runner)
348 .register(Arc::new(Echo));
349
350 #[cfg(unix)]
351 {
352 let result = reg
353 .invoke("echo", serde_json::json!({"msg": "allowed"}))
354 .await
355 .unwrap();
356 assert_eq!(result, "allowed");
357 }
358 #[cfg(not(unix))]
359 {
360 let err = reg
361 .invoke("echo", serde_json::json!({"msg": "blocked"}))
362 .await
363 .unwrap_err();
364 assert!(matches!(err, Error::PermissionDenied { .. }));
365 }
366 }
367
368 #[tokio::test]
370 async fn non_headless_interactive_not_auto_denied() {
371 let config = crate::permissions::LayeredPermissionsConfig {
372 mode: PermissionMode::Default,
373 layers: vec![crate::permissions::PermissionLayer {
374 source: crate::permissions::RuleSource::User,
375 interactive: vec!["echo".into()],
376 ..Default::default()
377 }],
378 };
379 let reg = ToolRegistry::local()
380 .with_permissions(config)
381 .with_headless(false)
382 .register(Arc::new(Echo));
383 let result = reg
384 .invoke("echo", serde_json::json!({"msg": "hello"}))
385 .await
386 .unwrap();
387 assert_eq!(result, "hello");
388 }
389
390 #[test]
394 fn permission_unknown_variant_exists() {
395 use crate::permissions::Permission;
396 let u = Permission::Unknown;
397 assert!(!u.is_allowed());
398 assert!(!u.is_denied());
399 }
400
401 #[tokio::test]
404 async fn unknown_interactive_tool_deny_hook_blocks_invoke_with_audit() {
405 let config = crate::permissions::LayeredPermissionsConfig {
406 mode: PermissionMode::Default,
407 layers: vec![crate::permissions::PermissionLayer {
408 source: crate::permissions::RuleSource::User,
409 interactive: vec!["echo".into()],
410 ..Default::default()
411 }],
412 };
413 let reg = ToolRegistry::local()
414 .with_permissions(config)
415 .with_permission_hook(Arc::new(DenyHook))
416 .with_headless(false)
417 .register(Arc::new(Echo));
418 let dispatch = reg
419 .invoke_with_audit("echo", serde_json::json!({"msg": "hi"}))
420 .await;
421 assert!(
422 matches!(dispatch.result, Err(Error::PermissionDenied { .. })),
423 "hook-deny should block interactive Unknown tool via invoke_with_audit"
424 );
425 }
426
427 #[tokio::test]
429 async fn unknown_interactive_tool_no_hook_is_allowed() {
430 let config = crate::permissions::LayeredPermissionsConfig {
431 mode: PermissionMode::Default,
432 layers: vec![crate::permissions::PermissionLayer {
433 source: crate::permissions::RuleSource::User,
434 interactive: vec!["echo".into()],
435 ..Default::default()
436 }],
437 };
438 let reg = ToolRegistry::local()
439 .with_permissions(config)
440 .with_headless(false)
441 .register(Arc::new(Echo));
442 let dispatch = reg
443 .invoke_with_audit("echo", serde_json::json!({"msg": "ok"}))
444 .await;
445 assert_eq!(
446 dispatch.result.unwrap(),
447 "ok",
448 "no hook = allow for Unknown interactive tool"
449 );
450 }
451
452 #[tokio::test]
454 async fn unknown_non_interactive_tool_hook_not_consulted() {
455 let config = crate::permissions::LayeredPermissionsConfig {
457 mode: PermissionMode::Default,
458 layers: vec![],
459 };
460 let reg = ToolRegistry::local()
461 .with_permissions(config)
462 .with_permission_hook(Arc::new(DenyHook))
463 .with_headless(false)
464 .register(Arc::new(Echo));
465 let dispatch = reg
466 .invoke_with_audit("echo", serde_json::json!({"msg": "pass"}))
467 .await;
468 assert_eq!(
469 dispatch.result.unwrap(),
470 "pass",
471 "DenyHook must not fire for non-interactive Unknown tools"
472 );
473 }
474
475 #[tokio::test]
478 async fn allowed_interactive_tool_hook_not_consulted() {
479 let config = crate::permissions::LayeredPermissionsConfig {
480 mode: PermissionMode::Default,
481 layers: vec![crate::permissions::PermissionLayer {
482 source: crate::permissions::RuleSource::User,
483 allow: vec!["echo".into()],
484 interactive: vec!["echo".into()],
485 ..Default::default()
486 }],
487 };
488 let reg = ToolRegistry::local()
489 .with_permissions(config)
490 .with_permission_hook(Arc::new(DenyHook))
491 .with_headless(false)
492 .register(Arc::new(Echo));
493 let dispatch = reg
496 .invoke_with_audit("echo", serde_json::json!({"msg": "explicit-allow"}))
497 .await;
498 assert_eq!(
499 dispatch.result.unwrap(),
500 "explicit-allow",
501 "Explicitly Allowed tools must not be re-checked via hook"
502 );
503 }
504
505 #[test]
508 fn default_registry_has_no_plan_mode_tools() {
509 let workspace = std::path::PathBuf::from(".");
512 let registry = super::registry::build_standard_tools(&workspace, &[], 30);
513 assert!(
514 registry.get("enter_plan_mode").is_none(),
515 "enter_plan_mode must not be in the default registry"
516 );
517 assert!(
518 registry.get("exit_plan_mode").is_none(),
519 "exit_plan_mode must not be in the default registry"
520 );
521 }
522
523 #[test]
526 fn fork_returns_usable_registry() {
527 let reg = ToolRegistry::local().register(Arc::new(Echo));
528 let forked = reg.fork();
529 assert!(
531 forked.get("echo").is_some(),
532 "forked registry should contain 'echo'"
533 );
534 assert!(
535 forked.get("nope").is_none(),
536 "forked registry should not contain unknown tools"
537 );
538 }
539}