From 6c3458a5ec37a5b69957ad17c84de58984698f25 Mon Sep 17 00:00:00 2001 From: tanaka takashi Date: Tue, 11 Aug 2026 21:44:53 +0900 Subject: Audit and align v1 runtime design --- src/Agent.Windows/Tools/WindowsToolBackend.cs | 46 ++++++++++++++++++++------- 1 file changed, 35 insertions(+), 11 deletions(-) (limited to 'src/Agent.Windows/Tools/WindowsToolBackend.cs') 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 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(), SessionId: sessionId)); } catch { @@ -71,6 +73,8 @@ public sealed class WindowsToolBackend(WindowsSandboxSettings settings, ISecurit public Task ExecuteAsync(Guid taskId, string toolName, JsonElement arguments, CancellationToken cancellationToken) { TaskContext context = Get(taskId); + audit.Write(new(DateTimeOffset.UtcNow, taskId, "tool.call", "attempted", + new Dictionary { ["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 { ["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(), SessionId: context.SessionId)); + } + catch + { + audit.Write(new(DateTimeOffset.UtcNow, taskId, "task.end", "cleanup_failed", + new Dictionary(), 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 { ["path"] = auditPath })); + string eventName = operation == "delete" ? "file.delete" : "file.write"; + audit.Write(new(DateTimeOffset.UtcNow, context.Journal.TaskId, eventName, "completed", + new Dictionary { ["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 Acls { get; } = []; public TaskNetworkPolicy? Network { get; set; } -- cgit v1.2.3