use crate::Result;
use serde_json::Value;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
#[cfg(feature = "command-exec")]
mod bash;
mod confirm_latch;
pub(crate) mod context_checkpoint;
mod edit;
mod extension;
mod find;
mod grep;
mod ls;
pub mod memory;
mod multi_edit;
#[cfg(feature = "command-exec")]
mod powershell;
pub(crate) mod read;
mod search;
mod unix_programs;
mod secret_prompt;
mod subagent;
#[cfg(target_os = "linux")]
pub mod sudo_askpass;
pub(crate) use subagent::archive::write_window_transcript;
#[cfg(test)]
pub(crate) use subagent::archive::resume_messages;
pub(crate) use subagent::finalize::build_reload_carry_event;
#[doc(hidden)]
pub use subagent::{legacy_fresh_runtime, spawn_runtime};
mod write;
pub mod activation;
mod agent;
pub mod catalog;
pub mod discovery;
pub mod ledger;
pub mod output;
pub mod prompt_catalog;
mod registry;
pub mod respond;
pub mod send_channel;
pub mod shell;
pub(crate) mod util;
pub(crate) mod utf8_stream;
pub(crate) use util::expand_path;
pub use crate::runtime::subagent::{
SubagentDisplayRow, SubagentHandle, SubagentRegistry, SubagentResult, SubagentState,
SubagentStatus,
};
pub use agent::resolve_agent_prompt;
#[cfg(feature = "command-exec")]
pub use bash::{bash_intermediary_snapshot, BashIntermediarySnapshot, BashTool};
pub use discovery::{ActivateToolsTool, SearchToolsTool};
pub use edit::EditTool;
pub use extension::ExtensionTool;
pub use find::FindTool;
pub use grep::GrepTool;
pub use ls::LsTool;
pub use multi_edit::MultiEditTool;
pub(crate) use multi_edit::{label as multi_edit_label, paths as multi_edit_paths};
#[cfg(feature = "command-exec")]
pub use powershell::PowerShellTool;
pub use read::ReadTool;
pub use registry::{
DeferredToolSplit, DroppedSessionMember, SessionSchemaProjection, ToolRegistry,
};
pub use respond::RespondTool;
pub use confirm_latch::{latch_keys, LatchKey, SessionConfirmLatch};
pub use secret_prompt::SecretPromptQueue;
pub use secret_prompt::{
ConfirmChoice, ConfirmScope, PromptKind, SecretPromptHandle, SecretPromptRequest,
CONFIRM_ANSWER_ALLOW_ALL,
};
pub use send_channel::SendChannelTool;
pub fn run_sudo_askpass_helper_if_invoked() -> Option<i32> {
#[cfg(target_os = "linux")]
{
sudo_askpass::run_helper_if_invoked()
}
#[cfg(not(target_os = "linux"))]
{
None
}
}
pub use shell::{ShellEndTool, ShellSendTool, ShellStartTool};
pub use subagent::{
SubagentCollectTool, SubagentModelAuthorizeTool, SubagentModelsTool, SubagentResumeTool,
SubagentStartTool, SubagentStatusTool, SubagentSteerTool, SubagentTool,
};
pub use write::WriteTool;
pub(crate) use util::{
peek_next_subagent_id, resolve_path_in, seed_next_subagent_id, strip_ansi, NEXT_SUBAGENT_ID,
};
#[doc(hidden)]
pub use subagent::finalize::{build_completion_event, finalize_subagent};
pub struct ToolChannels {
pub tx_delta: Option<crate::tools::output::DeltaSender>,
pub tx_events: Option<tokio::sync::mpsc::UnboundedSender<crate::StreamEvent>>,
}
#[derive(Clone, Default)]
pub struct ToolCapabilities {
pub session_allow_all: Option<Arc<SessionConfirmLatch>>,
pub memory_backend: Option<crate::memory_backend::MemoryBinding>,
pub telemetry: Option<crate::runtime::telemetry::WorkerTelemetry>,
pub tool_register_tx: Option<tokio::sync::mpsc::UnboundedSender<Vec<Arc<dyn Tool>>>>,
pub session_manager: Option<std::sync::Arc<crate::tools::shell::SessionManager>>,
pub subagent_registry: Option<Arc<Mutex<SubagentRegistry>>>,
pub launch_cancel: Option<crate::CancellationToken>,
pub event_queue: Option<Arc<crate::events::EventQueue>>,
pub delegation_parent: Option<String>,
pub codex_parent_plan: Option<crate::runtime::openai::catalog::CodexExecutionPlan>,
pub secret_prompt: Option<SecretPromptHandle>,
pub orchestration: Option<Arc<crate::orchestration::OrchestrationRuntime>>,
pub tool_activation: Option<crate::tools::discovery::ActivationCapability>,
pub mcp_leases: Option<crate::mcp::McpLeaseCapability>,
pub extension_leases: Option<crate::extensions::lease::ExtensionLeaseCapability>,
pub cwd: Option<PathBuf>,
pub env: Option<crate::session::types::ToolEnv>,
pub env_stripped: Vec<String>,
pub env_warned: std::sync::Arc<std::sync::Mutex<std::collections::HashSet<String>>>,
pub session_id: Option<String>,
pub hook_bus: Option<Arc<crate::extensions::hooks::HookBus>>,
pub continuation: Option<crate::runtime::continuation::SharedContinuation>,
}
impl ToolCapabilities {
pub(crate) fn stripped_env_notices(&self, pick: impl Fn(&str) -> bool) -> String {
let mut notice = String::new();
if self.env_stripped.is_empty() {
return notice;
}
let mut warned = self.env_warned.lock().unwrap_or_else(|p| p.into_inner());
for name in self.env_stripped.iter().filter(|n| pick(n)) {
if warned.insert(name.clone()) {
notice.push_str(&format!(
"\nnote: ${name} was stripped from the session env as a secret; \
reattach from a shell that has it \
(synaps --system … from that shell) \
— the daemon never receives credentials."
));
}
}
notice
}
}
#[derive(Clone)]
pub struct ToolLimits {
pub max_tool_output: usize,
pub max_tool_buffer: usize,
pub bash_timeout: u64,
pub bash_max_timeout: u64,
pub subagent_timeout: u64,
}
pub struct ToolContext {
pub channels: ToolChannels,
pub capabilities: ToolCapabilities,
pub limits: ToolLimits,
}
#[derive(Clone)]
pub struct TurnToolContext(Arc<TurnTools>);
struct TurnTools {
capabilities: ToolCapabilities,
limits: ToolLimits,
events: Option<tokio::sync::mpsc::UnboundedSender<crate::StreamEvent>>,
}
impl TurnToolContext {
pub fn new(
mut capabilities: ToolCapabilities,
limits: ToolLimits,
events: Option<tokio::sync::mpsc::UnboundedSender<crate::StreamEvent>>,
) -> Self {
capabilities.tool_activation = None;
capabilities.tool_register_tx = None;
Self(Arc::new(TurnTools {
capabilities,
limits,
events,
}))
}
pub fn no_chat() -> Self {
Self(Arc::new(TurnTools {
capabilities: ToolCapabilities::default(),
limits: ToolLimits {
max_tool_output: 30000,
max_tool_buffer: 256 * 1024,
bash_timeout: 30,
bash_max_timeout: 300,
subagent_timeout: 300,
},
events: None,
}))
}
pub fn for_dispatch(
&self,
tool_register_tx: tokio::sync::mpsc::UnboundedSender<Vec<Arc<dyn Tool>>>,
tool_activation: crate::tools::discovery::ActivationCapability,
) -> Self {
let mut capabilities = self.0.capabilities.clone();
capabilities.tool_register_tx = Some(tool_register_tx);
capabilities.tool_activation = Some(tool_activation);
Self(Arc::new(TurnTools {
capabilities,
limits: self.0.limits.clone(),
events: self.0.events.clone(),
}))
}
pub fn for_call(&self, tx_delta: Option<crate::tools::output::DeltaSender>) -> ToolContext {
ToolContext {
channels: ToolChannels {
tx_delta,
tx_events: self.0.events.clone(),
},
capabilities: self.0.capabilities.clone(),
limits: self.0.limits.clone(),
}
}
pub fn capabilities(&self) -> &ToolCapabilities {
&self.0.capabilities
}
pub fn limits(&self) -> &ToolLimits {
&self.0.limits
}
}
impl std::fmt::Debug for TurnToolContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TurnToolContext")
.field("session_id", &self.0.capabilities.session_id)
.field("cwd", &self.0.capabilities.cwd)
.finish_non_exhaustive()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ToolOrigin {
Builtin,
Extension { extension_id: String },
Mcp {
server_id: String,
server_tool_name: String,
},
Plugin {
plugin_id: String,
tool_name: String,
},
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConcurrencyKey {
Key(String),
Serialize,
}
#[derive(Debug, Clone)]
pub enum ToolOutput {
Text(String),
Blocks { blocks: Vec<Value>, summary: String },
}
impl ToolOutput {
pub fn summary(&self) -> &str {
match self {
Self::Text(s) => s,
Self::Blocks { summary, .. } => summary,
}
}
pub fn into_parts(self) -> (String, Option<Vec<Value>>) {
match self {
Self::Text(s) => (s, None),
Self::Blocks {
mut blocks,
summary,
} => {
let text_first = blocks.first().is_some_and(|b| b["type"] == "text");
if !text_first {
blocks.insert(0, serde_json::json!({"type": "text", "text": summary}));
}
debug_assert!(
blocks[0]["text"] == summary,
"ToolOutput::Blocks invariant: blocks[0] text must equal summary"
);
(summary, Some(blocks))
}
}
}
pub fn into_summary(self) -> String {
self.into_parts().0
}
}
#[async_trait::async_trait]
pub trait Tool: Send + Sync {
fn name(&self) -> &str;
fn description(&self) -> &str;
fn parameters(&self) -> Value;
async fn execute(&self, params: Value, ctx: ToolContext) -> Result<String>;
async fn execute_rich(&self, params: Value, ctx: ToolContext) -> Result<ToolOutput> {
self.execute(params, ctx).await.map(ToolOutput::Text)
}
fn extension_id(&self) -> Option<&str> {
None
}
fn source_operation(&self) -> Option<&str> {
None
}
fn origin(&self) -> ToolOrigin {
match self.extension_id() {
Some(extension_id) => ToolOrigin::Extension {
extension_id: extension_id.to_string(),
},
None => ToolOrigin::Unknown,
}
}
fn effect(&self) -> crate::tools::catalog::ToolEffect {
crate::tools::catalog::ToolEffect::NonIdempotent
}
fn runs_independently(&self) -> bool {
false
}
fn concurrency_key(&self, _validated_input: &Value) -> Option<ConcurrencyKey> {
None
}
fn concurrency_key_in(
&self,
validated_input: &Value,
_cwd: Option<&std::path::Path>,
) -> Option<ConcurrencyKey> {
self.concurrency_key(validated_input)
}
}
#[cfg(test)]
pub(crate) mod test_helpers;
#[cfg(test)]
mod stripped_env_notice_tests {
#[test]
fn notices_name_each_picked_secret_once_per_session() {
let mut caps = super::test_helpers::create_tool_context().capabilities;
assert_eq!(caps.stripped_env_notices(|_| true), "", "nothing stripped: nothing to say");
caps.env_stripped = vec!["GH_TOKEN".into(), "AWS_SECRET_ACCESS_KEY".into()];
let first = caps.stripped_env_notices(|n| n == "GH_TOKEN");
assert!(first.contains("note: $GH_TOKEN was stripped from the session env as a secret"), "{first}");
assert!(!first.contains("AWS_SECRET_ACCESS_KEY"), "{first}");
let all = caps.stripped_env_notices(|_| true);
assert!(all.contains("$AWS_SECRET_ACCESS_KEY") && !all.contains("$GH_TOKEN"), "{all}");
assert_eq!(caps.stripped_env_notices(|_| true), "", "each name once");
}
}
#[cfg(test)]
mod turn_tool_context_tests {
use super::*;
#[test]
fn a_turns_tool_context_is_one_pointer_wide() {
eprintln!(
"ToolCapabilities: {} bytes, ToolContext: {} bytes",
std::mem::size_of::<ToolCapabilities>(),
std::mem::size_of::<ToolContext>(),
);
assert_eq!(
std::mem::size_of::<Option<TurnToolContext>>(),
std::mem::size_of::<usize>()
);
}
#[test]
fn dispatch_capabilities_never_reach_the_turns_context() {
let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
let (events, _events_rx) = tokio::sync::mpsc::unbounded_channel();
let turn = TurnToolContext::new(
ToolCapabilities {
tool_register_tx: Some(tx.clone()),
session_id: Some("chat-A".into()),
env_stripped: vec!["SECRET".into()],
..Default::default()
},
TurnToolContext::no_chat().limits().clone(),
Some(events.clone()),
);
assert!(turn.capabilities().tool_register_tx.is_none());
let set = crate::tools::activation::SessionToolSet::progressive_core_for_catalog(
crate::tools::activation::SessionId::parse("turn-tools").unwrap(),
&crate::tools::catalog::ToolCatalog::empty(),
);
let dispatch = turn.for_dispatch(
tx,
crate::tools::discovery::ActivationCapability::new(
crate::tools::catalog::ToolCatalog::empty(),
Arc::new(std::sync::RwLock::new(set)),
crate::tools::activation::ActivationAuthority::ModelConfirmed,
),
);
let (of_turn, of_dispatch) = (turn.for_call(None), dispatch.for_call(None));
assert!(of_turn.capabilities.tool_register_tx.is_none());
assert!(of_turn.capabilities.tool_activation.is_none());
assert!(of_dispatch.capabilities.tool_register_tx.is_some());
assert!(of_dispatch.capabilities.tool_activation.is_some());
for ctx in [&of_turn, &of_dispatch] {
assert_eq!(ctx.capabilities.session_id.as_deref(), Some("chat-A"));
assert_eq!(ctx.capabilities.env_stripped, ["SECRET"]);
assert!(ctx
.channels
.tx_events
.as_ref()
.is_some_and(|tx| tx.same_channel(&events)));
assert!(ctx.channels.tx_delta.is_none());
}
let none = TurnToolContext::no_chat().for_call(None);
assert!(none.channels.tx_events.is_none());
assert!(none.capabilities.session_id.is_none() && none.capabilities.env.is_none());
assert!(none.capabilities.memory_backend.is_none());
}
}
#[cfg(test)]
mod tool_output_tests {
use super::ToolOutput;
use serde_json::json;
#[test]
fn into_parts_prepends_summary_text_when_blocks_not_text_first() {
let img = json!({"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": "AA=="}});
let out = ToolOutput::Blocks {
blocks: vec![img.clone()],
summary: "[image 1x1]".into(),
};
let (summary, blocks) = out.into_parts();
let blocks = blocks.unwrap();
assert_eq!(summary, "[image 1x1]");
assert_eq!(blocks.len(), 2);
assert_eq!(blocks[0], json!({"type": "text", "text": "[image 1x1]"}));
assert_eq!(blocks[1], img);
}
#[test]
fn into_parts_prepends_summary_text_when_blocks_empty() {
let out = ToolOutput::Blocks {
blocks: vec![],
summary: "s".into(),
};
let (_, blocks) = out.into_parts();
assert_eq!(blocks.unwrap(), vec![json!({"type": "text", "text": "s"})]);
}
#[test]
fn into_parts_leaves_well_formed_blocks_alone() {
let blocks = vec![
json!({"type": "text", "text": "ok"}),
json!({"type": "image"}),
];
let out = ToolOutput::Blocks {
blocks: blocks.clone(),
summary: "ok".into(),
};
let (_, got) = out.into_parts();
assert_eq!(got.unwrap(), blocks);
}
}