summaryrefslogtreecommitdiff
path: root/src/Agent.Windows/Tools
diff options
context:
space:
mode:
Diffstat (limited to 'src/Agent.Windows/Tools')
-rw-r--r--src/Agent.Windows/Tools/WindowsToolBackend.cs46
1 files changed, 35 insertions, 11 deletions
diff --git a/src/Agent.Windows/Tools/WindowsToolBackend.cs b/src/Agent.Windows/Tools/WindowsToolBackend.cs
index 6e62d3d..5c1fe49 100644
--- a/src/Agent.Windows/Tools/WindowsToolBackend.cs
+++ b/src/Agent.Windows/Tools/WindowsToolBackend.cs
@@ -22,7 +22,8 @@ public sealed record WindowsSandboxSettings(
long MemoryBackupLimitBytes,
long WholeReadLimitBytes,
int SearchResultLimit,
- int TailLineLimit);
+ int TailLineLimit,
+ int ProcessTimeoutSeconds = 300);
public sealed class WindowsToolBackend(WindowsSandboxSettings settings, ISecurityAuditSink audit, ITaskUpdateHandler? taskUpdates = null) : IInternalToolBackend
{
@@ -37,7 +38,7 @@ public sealed class WindowsToolBackend(WindowsSandboxSettings settings, ISecurit
AppContainerProfile profile = AppContainerProfile.EnsureFixed();
TaskResourceJournal journal = TaskResourceJournal.Create(settings.JournalDirectory, taskId);
var acl = new TaskAclManager(profile.Sid, audit, journal);
- var context = new TaskContext(journal, acl);
+ var context = new TaskContext(journal, acl, sessionId);
_taskUpdates.BeginTask(taskId, sessionId);
try
{
@@ -57,8 +58,9 @@ public sealed class WindowsToolBackend(WindowsSandboxSettings settings, ISecurit
IReadOnlyList<NetworkEndpoint> allowances = settings.NetworkMode == "allow"
? await NetworkAllowanceSnapshot.ResolveAsync(settings.NetworkAllowances, cancellationToken).ConfigureAwait(false)
: [];
- context.Network = TaskNetworkPolicy.Apply(journal, profile.Sid, allowances, audit);
+ context.Network = TaskNetworkPolicy.Apply(journal, profile.Sid, allowances, audit, sessionId);
if (!_contexts.TryAdd(taskId, context)) throw new InvalidOperationException("Task context already exists.");
+ audit.Write(new(DateTimeOffset.UtcNow, taskId, "task.start", "running", new Dictionary<string, string>(), SessionId: sessionId));
}
catch
{
@@ -71,6 +73,8 @@ public sealed class WindowsToolBackend(WindowsSandboxSettings settings, ISecurit
public Task<ToolExecutionResult> ExecuteAsync(Guid taskId, string toolName, JsonElement arguments, CancellationToken cancellationToken)
{
TaskContext context = Get(taskId);
+ audit.Write(new(DateTimeOffset.UtcNow, taskId, "tool.call", "attempted",
+ new Dictionary<string, string> { ["tool"] = toolName }, SessionId: context.SessionId));
try
{
return toolName switch
@@ -78,7 +82,7 @@ public sealed class WindowsToolBackend(WindowsSandboxSettings settings, ISecurit
"file_search" => Task.FromResult(Search(arguments)),
"file_read" => Task.FromResult(Read(arguments)),
"file_edit" => Task.FromResult(Edit(context, arguments)),
- "process_run" => Task.FromResult(RunProcess(taskId, arguments)),
+ "process_run" => Task.FromResult(RunProcess(context, arguments)),
"task_update" => Task.FromResult(_taskUpdates.Execute(taskId, arguments)),
"dir" => Task.FromResult(DirectoryOp(context, arguments)),
_ => throw new ToolValidationException($"Unknown internal tool '{toolName}'.")
@@ -86,14 +90,28 @@ public sealed class WindowsToolBackend(WindowsSandboxSettings settings, ISecurit
}
catch (SandboxPolicyException ex)
{
- return Task.FromResult(new ToolExecutionResult(false, ex.Message, false, $"{toolName}:SandboxPolicyException:{ex.Message}", RetryAllowed: false));
+ audit.Write(new(DateTimeOffset.UtcNow, taskId, "policy.deny", "denied",
+ new Dictionary<string, string> { ["tool"] = toolName, ["message"] = ex.Message },
+ SessionId: context.SessionId, ErrorCode: ex.Code));
+ return Task.FromResult(new ToolExecutionResult(false, ex.Message, false, $"{toolName}:{ex.Code}:{ex.Message}", RetryAllowed: false));
}
}
public async Task EndTaskAsync(Guid taskId, bool success, CancellationToken cancellationToken)
{
if (!_contexts.TryRemove(taskId, out TaskContext? context)) return;
- try { await CleanupAsync(taskId, context).ConfigureAwait(false); }
+ try
+ {
+ await CleanupAsync(taskId, context).ConfigureAwait(false);
+ audit.Write(new(DateTimeOffset.UtcNow, taskId, "task.end", success ? "completed" : "stopped",
+ new Dictionary<string, string>(), SessionId: context.SessionId));
+ }
+ catch
+ {
+ audit.Write(new(DateTimeOffset.UtcNow, taskId, "task.end", "cleanup_failed",
+ new Dictionary<string, string>(), SessionId: context.SessionId));
+ throw;
+ }
finally { _taskUpdates.EndTask(taskId); }
}
@@ -188,7 +206,9 @@ public sealed class WindowsToolBackend(WindowsSandboxSettings settings, ISecurit
string auditPath = operation == "rename"
? $"{Path.GetFullPath(path)} -> {Path.GetFullPath(arguments.GetProperty("new_path").GetString()!)}"
: Path.GetFullPath(path);
- audit.Write(new(DateTimeOffset.UtcNow, context.Journal.TaskId, "file." + operation, "completed", new Dictionary<string, string> { ["path"] = auditPath }));
+ string eventName = operation == "delete" ? "file.delete" : "file.write";
+ audit.Write(new(DateTimeOffset.UtcNow, context.Journal.TaskId, eventName, "completed",
+ new Dictionary<string, string> { ["path"] = auditPath, ["operation"] = operation }, SessionId: context.SessionId));
return Success($"{operation} completed for {auditPath}", externalEffect: true);
}
@@ -225,8 +245,9 @@ public sealed class WindowsToolBackend(WindowsSandboxSettings settings, ISecurit
throw new ToolValidationException($"Unsupported dir operation '{operation}'.");
}
- private ToolExecutionResult RunProcess(Guid taskId, JsonElement arguments)
+ private ToolExecutionResult RunProcess(TaskContext context, JsonElement arguments)
{
+ Guid taskId = context.Journal.TaskId;
string executable = arguments.GetProperty("executable").GetString()!;
string cwd = arguments.GetProperty("cwd").GetString()!;
_paths.Demand(executable, FileCapability.Execute);
@@ -234,10 +255,12 @@ public sealed class WindowsToolBackend(WindowsSandboxSettings settings, ISecurit
string[] args = arguments.GetProperty("args").EnumerateArray().Select(static item => item.GetString()!).ToArray();
string? managedEntryPoint = ExecutablePolicy.ManagedEntryPoint(executable, args);
if (managedEntryPoint is not null) _paths.Demand(managedEntryPoint, FileCapability.Execute);
- int seconds = arguments.TryGetProperty("timeout_seconds", out JsonElement timeout) ? timeout.GetInt32() : 300;
+ int seconds = arguments.TryGetProperty("timeout_seconds", out JsonElement timeout) ? timeout.GetInt32() : settings.ProcessTimeoutSeconds;
+ if (seconds > settings.ProcessTimeoutSeconds)
+ throw new ToolValidationException($"timeout_seconds exceeds configured sandbox.resources.process_timeout_seconds ({settings.ProcessTimeoutSeconds}).");
string? stdin = arguments.TryGetProperty("stdin", out JsonElement stdinElement) && stdinElement.ValueKind == JsonValueKind.String ? stdinElement.GetString() : null;
RestrictedProcessResult result = new RestrictedProcessLauncher(_executables, audit, AppContainerProfile.EnsureFixed().Sid, settings.JobLimits)
- .Run(taskId, executable, args, cwd, TimeSpan.FromSeconds(seconds), stdin);
+ .Run(taskId, executable, args, cwd, TimeSpan.FromSeconds(seconds), stdin, context.SessionId);
string output = $"Process exited with code {result.ExitCode} (PID {result.ProcessId}).\nstdout:\n{result.StandardOutput}\nstderr:\n{result.StandardError}";
return new(result.ExitCode == 0, output, true,
result.ExitCode == 0 ? null : $"process:{executable}:{result.ExitCode}");
@@ -276,9 +299,10 @@ public sealed class WindowsToolBackend(WindowsSandboxSettings settings, ISecurit
return current[..first] + newText + current[(first + oldText.Length)..];
}
- private sealed class TaskContext(TaskResourceJournal journal, TaskAclManager acl)
+ private sealed class TaskContext(TaskResourceJournal journal, TaskAclManager acl, Guid sessionId)
{
public TaskResourceJournal Journal { get; } = journal;
+ public Guid SessionId { get; } = sessionId;
public TaskAclManager Acl { get; } = acl;
public List<AclSnapshot> Acls { get; } = [];
public TaskNetworkPolicy? Network { get; set; }