1use std::{fs, io::Read, sync::Arc};
7
8use agent_sdk_core::{
9 AgentError, ExecutorRef, PolicyRef, ToolExecutionOutput, ToolExecutionRequest, ToolExecutor,
10 ToolPackId, ToolPackKind, ToolPackSnapshot,
11 policy::{CapabilityPermission, EffectClass, RiskClass},
12};
13use serde::{Deserialize, Serialize};
14
15use super::{
16 anchor::HashLineAnchor,
17 bounds::BoundedWorkspace,
18 policy::WorkspacePolicy,
19 read_pipeline::{
20 WorkspaceArchiveMetadata, WorkspaceDocumentMetadata, WorkspaceMediaMetadata,
21 WorkspaceReadDetection, WorkspaceReaderStep, WorkspaceResourceMetadata,
22 WorkspaceSqliteMetadata, detect_workspace_file,
23 },
24 readers::{render_bounded_prefix_read, render_workspace_read, render_workspace_uri},
25 util::{content_ref_for, first_arg_ref, hash_bytes, tool_failure},
26};
27use crate::{
28 packs::{ToolkitPackBundle, tool_snapshot},
29 testing::{InMemoryJsonArgumentStore, InMemoryToolkitContentStore},
30};
31
32#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
33pub struct WorkspaceReadRequest {
36 pub path: String,
38 pub max_bytes: Option<u64>,
41}
42
43#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
44pub struct WorkspaceReadOutput {
47 pub path: String,
49 pub mime_type: String,
52 pub detected: WorkspaceReadDetection,
54 pub reader_pipeline: Vec<WorkspaceReaderStep>,
58 pub byte_len: u64,
60 pub content_hash: String,
63 pub truncated: bool,
66 pub binary: bool,
69 pub anchors: Vec<HashLineAnchor>,
72 pub content: String,
75 pub content_summary: Option<String>,
78 pub media: Option<WorkspaceMediaMetadata>,
80 pub document: Option<WorkspaceDocumentMetadata>,
83 pub archive: Option<WorkspaceArchiveMetadata>,
85 pub sqlite: Option<WorkspaceSqliteMetadata>,
87 pub resource: Option<WorkspaceResourceMetadata>,
89 pub warnings: Vec<String>,
92}
93
94impl BoundedWorkspace {
95 pub fn read(&self, request: &WorkspaceReadRequest) -> Result<WorkspaceReadOutput, AgentError> {
99 let max_output_bytes = request
100 .max_bytes
101 .unwrap_or(self.policy.max_output_bytes)
102 .min(self.policy.max_output_bytes);
103 if is_uri_read(&request.path) {
104 let uri_read =
105 render_workspace_uri(&request.path, self.policy.max_file_bytes, max_output_bytes)?;
106 return Ok(WorkspaceReadOutput {
107 path: request.path.clone(),
108 mime_type: uri_read.detection.mime_type.clone(),
109 detected: uri_read.detection,
110 reader_pipeline: uri_read.rendered.reader_pipeline,
111 byte_len: uri_read.byte_len,
112 content_hash: uri_read.content_hash,
113 truncated: uri_read.rendered.truncated,
114 binary: uri_read.rendered.binary,
115 anchors: uri_read.rendered.anchors,
116 content: uri_read.rendered.content,
117 content_summary: uri_read.rendered.content_summary,
118 media: uri_read.rendered.media,
119 document: uri_read.rendered.document,
120 archive: uri_read.rendered.archive,
121 sqlite: uri_read.rendered.sqlite,
122 resource: uri_read.rendered.resource,
123 warnings: uri_read.rendered.warnings,
124 });
125 }
126
127 let path = self.resolve_existing_file(&request.path)?;
128 let byte_len = fs::metadata(&path).map_err(tool_failure)?.len();
129 if byte_len > self.policy.max_file_bytes {
130 let prefix_limit = self
131 .policy
132 .max_file_bytes
133 .min(max_output_bytes.max(1))
134 .max(1);
135 let mut bytes = Vec::new();
136 fs::File::open(&path)
137 .map_err(tool_failure)?
138 .take(prefix_limit)
139 .read_to_end(&mut bytes)
140 .map_err(tool_failure)?;
141 let detected = detect_workspace_file(&path, &bytes);
142 let rendered =
143 render_bounded_prefix_read(&bytes, &detected, byte_len, max_output_bytes)?;
144 return Ok(WorkspaceReadOutput {
145 path: request.path.clone(),
146 mime_type: detected.mime_type.clone(),
147 detected,
148 reader_pipeline: rendered.reader_pipeline,
149 byte_len,
150 content_hash: hash_bytes(&bytes),
151 truncated: true,
152 binary: rendered.binary,
153 anchors: rendered.anchors,
154 content: rendered.content,
155 content_summary: rendered.content_summary,
156 media: rendered.media,
157 document: rendered.document,
158 archive: rendered.archive,
159 sqlite: rendered.sqlite,
160 resource: rendered.resource,
161 warnings: rendered.warnings,
162 });
163 }
164 let bytes = fs::read(&path).map_err(tool_failure)?;
165 let detected = detect_workspace_file(&path, &bytes);
166 let rendered = render_workspace_read(&path, &bytes, &detected, max_output_bytes)?;
167 Ok(WorkspaceReadOutput {
168 path: request.path.clone(),
169 mime_type: detected.mime_type.clone(),
170 detected,
171 reader_pipeline: rendered.reader_pipeline,
172 byte_len: bytes.len() as u64,
173 content_hash: hash_bytes(&bytes),
174 truncated: rendered.truncated,
175 binary: rendered.binary,
176 anchors: rendered.anchors,
177 content: rendered.content,
178 content_summary: rendered.content_summary,
179 media: rendered.media,
180 document: rendered.document,
181 archive: rendered.archive,
182 sqlite: rendered.sqlite,
183 resource: rendered.resource,
184 warnings: rendered.warnings,
185 })
186 }
187}
188
189fn is_uri_read(path: &str) -> bool {
190 path.starts_with("data:") || path.contains("://")
191}
192
193#[derive(Clone)]
194pub struct WorkspaceReadExecutor {
197 executor_ref: ExecutorRef,
198 workspace: Arc<BoundedWorkspace>,
199 arguments: InMemoryJsonArgumentStore,
200 content: InMemoryToolkitContentStore,
201}
202
203impl WorkspaceReadExecutor {
204 pub fn new(
208 workspace: Arc<BoundedWorkspace>,
209 arguments: InMemoryJsonArgumentStore,
210 content: InMemoryToolkitContentStore,
211 ) -> Self {
212 Self {
213 executor_ref: ExecutorRef::new("executor.toolkit.workspace_read.v1"),
214 workspace,
215 arguments,
216 content,
217 }
218 }
219
220 pub fn pack_bundle(
224 source: agent_sdk_core::SourceRef,
225 policy_ref: PolicyRef,
226 workspace: &WorkspacePolicy,
227 ) -> Result<ToolkitPackBundle, AgentError> {
228 let snapshot = ToolPackSnapshot::new(
229 ToolPackId::new("toolpack.workspace_readonly.v1"),
230 ToolPackKind::WorkspaceReadOnly,
231 "v1",
232 source.clone(),
233 )
234 .with_workspace_bounds(workspace.bounds_snapshot(policy_ref.clone()))
235 .with_tool(tool_snapshot(
236 "cap.toolkit.workspace_read",
237 "workspace_read",
238 "executor.toolkit.workspace_read.v1",
239 "schema.toolkit.workspace_read.v1",
240 vec![policy_ref],
241 vec![CapabilityPermission::FilesystemRead],
242 EffectClass::Read,
243 RiskClass::Low,
244 &source,
245 ));
246 ToolkitPackBundle::from_snapshot(snapshot)
247 }
248}
249
250impl ToolExecutor for WorkspaceReadExecutor {
251 fn executor_ref(&self) -> &ExecutorRef {
252 &self.executor_ref
253 }
254
255 fn execute(&self, request: &ToolExecutionRequest) -> Result<ToolExecutionOutput, AgentError> {
256 let args_ref = first_arg_ref(request)?;
257 let read_request: WorkspaceReadRequest = self.arguments.get(args_ref)?;
258 let output = self.workspace.read(&read_request)?;
259 let content_ref = content_ref_for(request, "workspace_read");
260 self.content.put(content_ref.clone(), &output)?;
261 let mut envelope = ToolExecutionOutput::completed("workspace read returned content ref");
262 envelope.content_refs.push(content_ref);
263 Ok(envelope)
264 }
265}