summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Agent.Core/Tools/InternalToolCatalog.cs3
-rw-r--r--src/Agent.Core/Tools/InternalToolDispatcher.cs2
-rw-r--r--src/Agent.Core/Tools/InternalToolValidator.cs2
-rw-r--r--src/Agent.Windows/Recovery/RecoveryManager.cs16
-rw-r--r--src/Agent.Windows/Recovery/TransactionalFileEditor.cs51
-rw-r--r--src/Agent.Windows/Tools/WindowsToolBackend.cs34
6 files changed, 106 insertions, 2 deletions
diff --git a/src/Agent.Core/Tools/InternalToolCatalog.cs b/src/Agent.Core/Tools/InternalToolCatalog.cs
index 1563c16..3ab17b1 100644
--- a/src/Agent.Core/Tools/InternalToolCatalog.cs
+++ b/src/Agent.Core/Tools/InternalToolCatalog.cs
@@ -19,7 +19,8 @@ public static class InternalToolCatalog
Define("file_read", "Read an approved file.", """{"type":"object","additionalProperties":false,"required":["path","mode"],"properties":{"path":{"type":"string"},"mode":{"type":"string","enum":["whole","range","metadata"]},"start":{"type":"integer","minimum":0},"length":{"type":"integer","minimum":0}}}"""),
Define("file_edit", "Edit an approved file transactionally.", """{"type":"object","additionalProperties":false,"required":["path","operation"],"properties":{"path":{"type":"string"},"operation":{"type":"string","enum":["overwrite","patch","atomic_replace","delete"]},"content":{"type":["string","null"]},"patch":{"type":["string","null"]},"expected_sha256":{"type":["string","null"]}}}"""),
Define("process_run", "Run one approved executable with separated arguments.", """{"type":"object","additionalProperties":false,"required":["executable","args","cwd"],"properties":{"executable":{"type":"string"},"args":{"type":"array","items":{"type":"string"}},"cwd":{"type":"string"},"timeout_seconds":{"type":"integer","minimum":1},"stdin":{"type":["string","null"]}}}"""),
- Define("task_update", "Update the deterministic task graph.", """{"type":"object","additionalProperties":false,"required":["operation"],"properties":{"operation":{"type":"string","enum":["add","add_dependency","set_state","set_estimate"]},"task_id":{"type":["string","null"]},"description":{"type":["string","null"]},"depends_on":{"type":"array","items":{"type":"string"}},"budget_class":{"type":["string","null"],"enum":["1K","2K","4K","8K","16K","32K","64K","128K",null]},"optimistic_tokens":{"type":"integer","minimum":0},"most_likely_tokens":{"type":"integer","minimum":0},"pessimistic_tokens":{"type":"integer","minimum":0},"state":{"type":["string","null"],"enum":["queued","planning","running","completed","failed","cancelled",null]}}}""")
+ Define("task_update", "Update the deterministic task graph.", """{"type":"object","additionalProperties":false,"required":["operation"],"properties":{"operation":{"type":"string","enum":["add","add_dependency","set_state","set_estimate"]},"task_id":{"type":["string","null"]},"description":{"type":["string","null"]},"depends_on":{"type":"array","items":{"type":"string"}},"budget_class":{"type":["string","null"],"enum":["1K","2K","4K","8K","16K","32K","64K","128K",null]},"optimistic_tokens":{"type":"integer","minimum":0},"most_likely_tokens":{"type":"integer","minimum":0},"pessimistic_tokens":{"type":"integer","minimum":0},"state":{"type":["string","null"],"enum":["queued","planning","running","completed","failed","cancelled",null]}}}"""),
+ Define("dir", "Create, remove, or list directories under an approved root. Only empty directories can be removed.", """{"type":"object","additionalProperties":false,"required":["path","operation"],"properties":{"path":{"type":"string"},"operation":{"type":"string","enum":["mkdir","rmdir","list"]}}}""")
];
return definitions.ToDictionary(static definition => definition.Name, StringComparer.Ordinal);
}
diff --git a/src/Agent.Core/Tools/InternalToolDispatcher.cs b/src/Agent.Core/Tools/InternalToolDispatcher.cs
index 0dca5df..c888705 100644
--- a/src/Agent.Core/Tools/InternalToolDispatcher.cs
+++ b/src/Agent.Core/Tools/InternalToolDispatcher.cs
@@ -23,7 +23,7 @@ public sealed class FakeInternalToolBackend : IInternalToolBackend
public Task<ToolExecutionResult> ExecuteAsync(Guid taskId, string toolName, JsonElement arguments, CancellationToken cancellationToken)
{
_calls.Add((toolName, arguments.Clone()));
- bool effect = toolName is "file_edit" or "process_run";
+ bool effect = toolName is "file_edit" or "process_run" or "dir";
return Task.FromResult(new ToolExecutionResult(true, "fake-ok", effect));
}
}
diff --git a/src/Agent.Core/Tools/InternalToolValidator.cs b/src/Agent.Core/Tools/InternalToolValidator.cs
index c48719e..091b790 100644
--- a/src/Agent.Core/Tools/InternalToolValidator.cs
+++ b/src/Agent.Core/Tools/InternalToolValidator.cs
@@ -75,6 +75,8 @@ public static class InternalToolValidator
if (operation == "set_state") Require(arguments, "task_id", "state");
if (operation == "set_estimate") Require(arguments, "task_id", "budget_class", "optimistic_tokens", "most_likely_tokens", "pessimistic_tokens");
}
+ // dir: path and operation are already required by the schema; no operation-specific
+ // additional arguments are needed since mkdir/rmdir/list only use the path field.
static void Require(JsonElement value, params string[] properties)
{
diff --git a/src/Agent.Windows/Recovery/RecoveryManager.cs b/src/Agent.Windows/Recovery/RecoveryManager.cs
index df69a5e..8fd7368 100644
--- a/src/Agent.Windows/Recovery/RecoveryManager.cs
+++ b/src/Agent.Windows/Recovery/RecoveryManager.cs
@@ -49,6 +49,8 @@ public sealed class RecoveryManager(string journalDirectory, ISecurityAuditSink
{
case "file.replace": RecoverFile(entry); break;
case "file.delete": RecoverDeletedFile(entry); break;
+ case "dir.mkdir": RecoverMkdir(entry); break;
+ case "dir.rmdir": RecoverRmdir(entry); break;
case "acl": RecoverAcl(entry); break;
case "wfp.dynamic": break; // BFE removes every object when the originating dynamic session ends.
case "network.loopback_exemption": RecoverLoopback(entry); break;
@@ -94,6 +96,20 @@ public sealed class RecoveryManager(string journalDirectory, ISecurityAuditSink
File.WriteAllBytes(entry.Target, original);
}
+ private static void RecoverMkdir(JournalEntry entry)
+ {
+ if (!Directory.Exists(entry.Target)) return;
+ if (Directory.EnumerateFileSystemEntries(entry.Target, "*", SearchOption.TopDirectoryOnly).Any())
+ throw new InvalidOperationException($"Directory '{entry.Target}' created by mkdir is not empty; automatic rollback refused.");
+ Directory.Delete(entry.Target, recursive: false);
+ }
+
+ private static void RecoverRmdir(JournalEntry entry)
+ {
+ if (Directory.Exists(entry.Target)) throw new InvalidOperationException($"Directory '{entry.Target}' was recreated after crash; automatic recovery refused.");
+ Directory.CreateDirectory(entry.Target);
+ }
+
private static void CleanupPrepared(JournalEntry entry)
{
if (entry.Kind == "network.loopback_exemption" && !bool.Parse(entry.Data["originally_present"]))
diff --git a/src/Agent.Windows/Recovery/TransactionalFileEditor.cs b/src/Agent.Windows/Recovery/TransactionalFileEditor.cs
index 52963a0..805a069 100644
--- a/src/Agent.Windows/Recovery/TransactionalFileEditor.cs
+++ b/src/Agent.Windows/Recovery/TransactionalFileEditor.cs
@@ -1,5 +1,6 @@
using System.Security.Cryptography;
using System.Text;
+using Agent.Core.Tools;
using Agent.Windows.Security;
namespace Agent.Windows.Recovery;
@@ -68,6 +69,30 @@ public sealed class TransactionalFileEditor(TaskResourceJournal journal, string
return new(journal, entryId, canonical, originalHash, backupPath, inline);
}
+ public DirectoryMkdirTransaction Mkdir(string path)
+ {
+ string canonical = CanonicalPath.Resolve(path, requireExisting: false);
+ bool existed = Directory.Exists(canonical);
+ if (existed) throw new InvalidOperationException($"Directory already exists: '{canonical}'.");
+ if (File.Exists(canonical)) throw new InvalidOperationException($"A file exists at the target path: '{canonical}'.");
+ Guid entryId = journal.Prepare("dir.mkdir", canonical, new Dictionary<string, string> { ["existed"] = existed.ToString() });
+ Directory.CreateDirectory(canonical);
+ journal.MarkApplied(entryId);
+ return new(journal, entryId, canonical);
+ }
+
+ public DirectoryRmdirTransaction Rmdir(string path)
+ {
+ string canonical = CanonicalPath.Resolve(path);
+ if (!Directory.Exists(canonical)) throw new DirectoryNotFoundException($"Directory does not exist: '{canonical}'.");
+ if (Directory.EnumerateFileSystemEntries(canonical, "*", SearchOption.TopDirectoryOnly).Any())
+ throw new ToolValidationException("Directory is not empty. Remove contents first before removing the directory.");
+ Guid entryId = journal.Prepare("dir.rmdir", canonical, new Dictionary<string, string>());
+ Directory.Delete(canonical, recursive: false);
+ journal.MarkApplied(entryId);
+ return new(journal, entryId, canonical);
+ }
+
private static string NormalizeHash(string hash) => hash.Replace("-", string.Empty, StringComparison.Ordinal).Trim().ToUpperInvariant();
}
@@ -93,6 +118,32 @@ public sealed class FileDeleteTransaction(TaskResourceJournal journal, Guid entr
}
}
+public sealed class DirectoryMkdirTransaction(TaskResourceJournal journal, Guid entryId, string path)
+{
+ private bool _finished;
+ public void Commit() { if (_finished) return; journal.MarkCleaned(entryId); _finished = true; }
+ public void Rollback()
+ {
+ if (_finished) return;
+ if (Directory.Exists(path)) Directory.Delete(path, recursive: false);
+ journal.MarkCleaned(entryId);
+ _finished = true;
+ }
+}
+
+public sealed class DirectoryRmdirTransaction(TaskResourceJournal journal, Guid entryId, string path)
+{
+ private bool _finished;
+ public void Commit() { if (_finished) return; journal.MarkCleaned(entryId); _finished = true; }
+ public void Rollback()
+ {
+ if (_finished) return;
+ if (!Directory.Exists(path)) Directory.CreateDirectory(path);
+ journal.MarkCleaned(entryId);
+ _finished = true;
+ }
+}
+
public sealed class FileEditTransaction(
TaskResourceJournal journal, Guid entryId, string path, string originalHash, string replacementHash,
string backupPath, string originalBase64, bool originallyExisted)
diff --git a/src/Agent.Windows/Tools/WindowsToolBackend.cs b/src/Agent.Windows/Tools/WindowsToolBackend.cs
index c0d1561..bb54c6e 100644
--- a/src/Agent.Windows/Tools/WindowsToolBackend.cs
+++ b/src/Agent.Windows/Tools/WindowsToolBackend.cs
@@ -78,6 +78,7 @@ public sealed class WindowsToolBackend(WindowsSandboxSettings settings, ISecurit
"file_edit" => Task.FromResult(Edit(context, arguments)),
"process_run" => Task.FromResult(RunProcess(taskId, arguments)),
"task_update" => Task.FromResult(_taskUpdates.Execute(taskId, arguments)),
+ "dir" => Task.FromResult(DirectoryOp(context, arguments)),
_ => throw new ToolValidationException($"Unknown internal tool '{toolName}'.")
};
}
@@ -172,6 +173,39 @@ public sealed class WindowsToolBackend(WindowsSandboxSettings settings, ISecurit
return Success($"{operation} completed for {Path.GetFullPath(path)}", externalEffect: true);
}
+ private ToolExecutionResult DirectoryOp(TaskContext context, JsonElement arguments)
+ {
+ string path = arguments.GetProperty("path").GetString()!;
+ string operation = arguments.GetProperty("operation").GetString()!;
+ var editor = new TransactionalFileEditor(context.Journal, settings.BackupDirectory, settings.MemoryBackupLimitBytes);
+ if (operation == "mkdir")
+ {
+ _paths.Demand(path, FileCapability.Write);
+ DirectoryMkdirTransaction transaction = editor.Mkdir(path);
+ transaction.Commit();
+ audit.Write(new(DateTimeOffset.UtcNow, context.Journal.TaskId, "dir.mkdir", "completed", new Dictionary<string, string> { ["path"] = Path.GetFullPath(path) }));
+ return Success($"mkdir completed for {Path.GetFullPath(path)}", externalEffect: true);
+ }
+ if (operation == "rmdir")
+ {
+ _paths.Demand(path, FileCapability.Delete);
+ DirectoryRmdirTransaction transaction = editor.Rmdir(path);
+ transaction.Commit();
+ audit.Write(new(DateTimeOffset.UtcNow, context.Journal.TaskId, "dir.rmdir", "completed", new Dictionary<string, string> { ["path"] = Path.GetFullPath(path) }));
+ return Success($"rmdir completed for {Path.GetFullPath(path)}", externalEffect: true);
+ }
+ if (operation == "list")
+ {
+ _paths.Demand(path, FileCapability.Read);
+ string canonical = CanonicalPath.Resolve(path);
+ var entries = Directory.EnumerateFileSystemEntries(canonical, "*", SearchOption.TopDirectoryOnly)
+ .Select(Path.GetFullPath)
+ .ToArray();
+ return Success(JsonSerializer.Serialize(entries));
+ }
+ throw new ToolValidationException($"Unsupported dir operation '{operation}'.");
+ }
+
private ToolExecutionResult RunProcess(Guid taskId, JsonElement arguments)
{
string executable = arguments.GetProperty("executable").GetString()!;