Skip to content

Commit 005d226

Browse files
refactor: reduce complexity of process_agent_memory in cache_memory/execute.rs (#531)
1 parent 8cfcdc6 commit 005d226

1 file changed

Lines changed: 113 additions & 95 deletions

File tree

src/tools/cache_memory/execute.rs

Lines changed: 113 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,100 @@ async fn collect_files(base: &Path, current: &Path) -> Result<Vec<PathBuf>> {
150150
Ok(files)
151151
}
152152

153+
/// Outcome of processing a single memory file.
154+
enum FileOutcome {
155+
Copied(u64),
156+
Skipped(String),
157+
}
158+
159+
/// Validate containment, safety, extension, size, and content for one file;
160+
/// copy it to `dest_file` if all checks pass.
161+
///
162+
/// All validation failures return `Ok(FileOutcome::Skipped(reason))` so the
163+
/// caller can keep a tally without complex nested control flow.
164+
async fn process_memory_file(
165+
relative_path: &Path,
166+
source_file: &Path,
167+
canonical_base: &Path,
168+
dest_file: &Path,
169+
config: &MemoryConfig,
170+
total_size: u64,
171+
) -> Result<FileOutcome> {
172+
// Defense-in-depth: verify the fully-resolved source path is still within
173+
// the memory base directory, guarding against TOCTOU symlink races.
174+
match tokio::fs::canonicalize(source_file).await {
175+
Ok(canonical_source) => {
176+
if !canonical_source.starts_with(canonical_base) {
177+
warn!(
178+
"Skipping path that escapes memory directory (possible symlink attack): {}",
179+
relative_path.display()
180+
);
181+
return Ok(FileOutcome::Skipped(format!(
182+
"{}: resolved path escapes memory directory",
183+
relative_path.display()
184+
)));
185+
}
186+
}
187+
Err(e) => {
188+
warn!(
189+
"Skipping unresolvable path '{}': {}",
190+
relative_path.display(),
191+
e
192+
);
193+
return Ok(FileOutcome::Skipped(format!(
194+
"{}: cannot resolve path ({})",
195+
relative_path.display(),
196+
e
197+
)));
198+
}
199+
}
200+
201+
if let Err(e) = validate_memory_path(relative_path) {
202+
warn!("Skipping unsafe path '{}': {}", relative_path.display(), e);
203+
return Ok(FileOutcome::Skipped(format!(
204+
"{}: {}",
205+
relative_path.display(),
206+
e
207+
)));
208+
}
209+
210+
if !config.is_extension_allowed(relative_path) {
211+
warn!("Skipping disallowed extension: {}", relative_path.display());
212+
return Ok(FileOutcome::Skipped(format!(
213+
"{}: extension not in allowed list",
214+
relative_path.display()
215+
)));
216+
}
217+
218+
let metadata = tokio::fs::metadata(source_file).await?;
219+
if total_size + metadata.len() > MAX_MEMORY_SIZE_BYTES {
220+
warn!(
221+
"Memory size limit ({} bytes) would be exceeded, skipping: {}",
222+
MAX_MEMORY_SIZE_BYTES,
223+
relative_path.display()
224+
);
225+
return Ok(FileOutcome::Skipped(format!(
226+
"{}: would exceed {} byte size limit",
227+
relative_path.display(),
228+
MAX_MEMORY_SIZE_BYTES
229+
)));
230+
}
231+
232+
if !validate_memory_file_content(source_file).await? {
233+
return Ok(FileOutcome::Skipped(format!(
234+
"{}: contains ##vso[ command",
235+
relative_path.display()
236+
)));
237+
}
238+
239+
if let Some(parent) = dest_file.parent() {
240+
tokio::fs::create_dir_all(parent).await?;
241+
}
242+
tokio::fs::copy(source_file, dest_file).await?;
243+
debug!("Copied memory file: {}", relative_path.display());
244+
Ok(FileOutcome::Copied(metadata.len()))
245+
}
246+
153247
/// Process agent memory from the safe output directory.
154248
///
155249
/// Validates and copies sanitized memory files from `source_dir/agent_memory/`
@@ -192,9 +286,7 @@ pub async fn process_agent_memory(
192286

193287
if files.is_empty() {
194288
info!("Agent memory directory is empty");
195-
return Ok(ExecutionResult::success(
196-
"Agent memory directory is empty",
197-
));
289+
return Ok(ExecutionResult::success("Agent memory directory is empty"));
198290
}
199291

200292
info!("Found {} file(s) in agent_memory", files.len());
@@ -217,109 +309,35 @@ pub async fn process_agent_memory(
217309

218310
for relative_path in &files {
219311
let source_file = memory_source.join(relative_path);
220-
221-
// Defense-in-depth: verify the fully-resolved source path is still within
222-
// the memory base directory, guarding against TOCTOU symlink races.
223-
match tokio::fs::canonicalize(&source_file).await {
224-
Ok(canonical_source) => {
225-
if !canonical_source.starts_with(&canonical_base) {
226-
warn!(
227-
"Skipping path that escapes memory directory (possible symlink attack): {}",
228-
relative_path.display()
229-
);
230-
skipped_count += 1;
231-
skipped_reasons.push(format!(
232-
"{}: resolved path escapes memory directory",
233-
relative_path.display()
234-
));
235-
continue;
236-
}
312+
let dest_file = memory_output.join(relative_path);
313+
match process_memory_file(
314+
relative_path,
315+
&source_file,
316+
&canonical_base,
317+
&dest_file,
318+
config,
319+
total_size,
320+
)
321+
.await?
322+
{
323+
FileOutcome::Copied(size) => {
324+
total_size += size;
325+
copied_count += 1;
237326
}
238-
Err(e) => {
239-
warn!(
240-
"Skipping unresolvable path '{}': {}",
241-
relative_path.display(),
242-
e
243-
);
327+
FileOutcome::Skipped(reason) => {
244328
skipped_count += 1;
245-
skipped_reasons.push(format!(
246-
"{}: cannot resolve path ({})",
247-
relative_path.display(),
248-
e
249-
));
250-
continue;
329+
skipped_reasons.push(reason);
251330
}
252331
}
253-
254-
// Validate path safety
255-
if let Err(e) = validate_memory_path(relative_path) {
256-
warn!("Skipping unsafe path '{}': {}", relative_path.display(), e);
257-
skipped_count += 1;
258-
skipped_reasons.push(format!("{}: {}", relative_path.display(), e));
259-
continue;
260-
}
261-
262-
// Validate file extension
263-
if !config.is_extension_allowed(relative_path) {
264-
warn!(
265-
"Skipping disallowed extension: {}",
266-
relative_path.display()
267-
);
268-
skipped_count += 1;
269-
skipped_reasons.push(format!(
270-
"{}: extension not in allowed list",
271-
relative_path.display()
272-
));
273-
continue;
274-
}
275-
276-
// Check file size contribution
277-
let metadata = tokio::fs::metadata(&source_file).await?;
278-
if total_size + metadata.len() > MAX_MEMORY_SIZE_BYTES {
279-
warn!(
280-
"Memory size limit ({} bytes) would be exceeded, skipping: {}",
281-
MAX_MEMORY_SIZE_BYTES,
282-
relative_path.display()
283-
);
284-
skipped_count += 1;
285-
skipped_reasons.push(format!(
286-
"{}: would exceed {} byte size limit",
287-
relative_path.display(),
288-
MAX_MEMORY_SIZE_BYTES
289-
));
290-
continue;
291-
}
292-
293-
// Validate content for ##vso[ injection
294-
if !validate_memory_file_content(&source_file).await? {
295-
skipped_count += 1;
296-
skipped_reasons.push(format!(
297-
"{}: contains ##vso[ command",
298-
relative_path.display()
299-
));
300-
continue;
301-
}
302-
303-
// Copy the file
304-
let dest_file = memory_output.join(relative_path);
305-
if let Some(parent) = dest_file.parent() {
306-
tokio::fs::create_dir_all(parent).await?;
307-
}
308-
tokio::fs::copy(&source_file, &dest_file).await?;
309-
total_size += metadata.len();
310-
copied_count += 1;
311-
debug!("Copied memory file: {}", relative_path.display());
312332
}
313333

314334
let message = format!(
315335
"Agent memory processed: {} file(s) copied ({} bytes), {} skipped",
316336
copied_count, total_size, skipped_count
317337
);
318338

319-
if !skipped_reasons.is_empty() {
320-
for reason in &skipped_reasons {
321-
info!("Skipped: {}", reason);
322-
}
339+
for reason in &skipped_reasons {
340+
info!("Skipped: {}", reason);
323341
}
324342

325343
info!("{}", message);

0 commit comments

Comments
 (0)