agent_sdk_toolkit/shell/
executor.rs1use agent_sdk_core::{
7 AgentError, EffectTerminalStatus, ExecutorRef, PolicyKind, PolicyRef, ToolExecutionOutput,
8 ToolExecutionRequest, ToolExecutor, ToolPackId, ToolPackKind, ToolPackSnapshot,
9 domain::ContentRef,
10 policy::{CapabilityPermission, EffectClass, RiskClass},
11};
12
13use crate::{
14 packs::{ToolkitPackBundle, tool_snapshot},
15 testing::{InMemoryJsonArgumentStore, InMemoryToolkitContentStore},
16};
17
18use super::{command::run_command, policy::ShellExecutionPolicy, types::ShellRequest};
19
20#[derive(Clone)]
21pub struct ShellExecutor {
24 executor_ref: ExecutorRef,
25 policy: ShellExecutionPolicy,
26 arguments: InMemoryJsonArgumentStore,
27 content: InMemoryToolkitContentStore,
28}
29
30impl ShellExecutor {
31 pub fn new(
35 policy: ShellExecutionPolicy,
36 arguments: InMemoryJsonArgumentStore,
37 content: InMemoryToolkitContentStore,
38 ) -> Self {
39 Self {
40 executor_ref: ExecutorRef::new("executor.toolkit.shell.v1"),
41 policy,
42 arguments,
43 content,
44 }
45 }
46
47 pub fn pack_bundle(
51 source: agent_sdk_core::SourceRef,
52 policy_ref: PolicyRef,
53 ) -> Result<ToolkitPackBundle, AgentError> {
54 let snapshot = ToolPackSnapshot::new(
55 ToolPackId::new("toolpack.shell.v1"),
56 ToolPackKind::Shell,
57 "v1",
58 source.clone(),
59 )
60 .with_tool(tool_snapshot(
61 "cap.toolkit.shell",
62 "shell",
63 "executor.toolkit.shell.v1",
64 "schema.toolkit.shell.v1",
65 vec![
66 policy_ref,
67 PolicyRef::with_kind(PolicyKind::Sandbox, "policy.sandbox.shell.required"),
68 ],
69 vec![CapabilityPermission::Shell],
70 EffectClass::Process,
71 RiskClass::High,
72 &source,
73 ));
74 ToolkitPackBundle::from_snapshot(snapshot)
75 }
76}
77
78impl ToolExecutor for ShellExecutor {
79 fn executor_ref(&self) -> &ExecutorRef {
80 &self.executor_ref
81 }
82
83 fn execute(&self, request: &ToolExecutionRequest) -> Result<ToolExecutionOutput, AgentError> {
84 let args_ref = request
85 .effect_intent
86 .content_refs
87 .first()
88 .ok_or_else(|| AgentError::missing_required_field("shell.argument_content_ref"))?;
89 let shell_request: ShellRequest = self.arguments.get(args_ref)?;
90 if shell_request.cancel_before_start {
91 return Ok(ToolExecutionOutput {
92 terminal_status: EffectTerminalStatus::Cancelled,
93 content_refs: Vec::new(),
94 redacted_summary: "shell execution cancelled before process start".to_string(),
95 external_operation_id: None,
96 reconciliation_ref: None,
97 error_ref: None,
98 });
99 }
100 if shell_request.argv.is_empty() {
101 return Ok(ToolExecutionOutput::failed(
102 "shell argv must be structured and non-empty",
103 "shell.argv.empty",
104 ));
105 }
106 if shell_request.timeout_ms == 0 || shell_request.timeout_ms > self.policy.max_timeout_ms {
107 return Ok(ToolExecutionOutput::failed(
108 "shell requires timeout within sandbox policy",
109 "shell.timeout.policy",
110 ));
111 }
112 if shell_request.network && !self.policy.network_enabled {
113 return Ok(ToolExecutionOutput::failed(
114 "shell network access denied by sandbox policy",
115 "shell.network.policy",
116 ));
117 }
118 if !self.policy.allow_host_execution {
119 return Ok(ToolExecutionOutput::failed(
120 "shell host execution denied by sandbox policy",
121 self.policy.sandbox_policy_ref.as_str(),
122 ));
123 }
124
125 let result = run_command(&shell_request)?;
126 let content_ref = ContentRef::new(format!(
127 "content.{}.shell",
128 request.resolved_call.request.tool_call_id.as_str()
129 ));
130 self.content.put(content_ref.clone(), &result)?;
131 let mut output = ToolExecutionOutput::completed("shell process completed with output refs");
132 if result.exit_code != Some(0) {
133 output.terminal_status = EffectTerminalStatus::Failed;
134 }
135 output.content_refs.push(content_ref);
136 output.external_operation_id = Some("process.agent_owned".to_string());
137 output.reconciliation_ref = Some("process.exit_status.recorded".to_string());
138 Ok(output)
139 }
140}