summaryrefslogtreecommitdiff
path: root/src/Agent.Windows/Tools
diff options
context:
space:
mode:
authortanaka takashi <isazaka@gmail.com>2026-08-10 02:26:09 +0900
committertanaka takashi <isazaka@gmail.com>2026-08-10 02:26:09 +0900
commitf2001b0c5a0b35e052ba62ef4f2e19bcc64fd870 (patch)
treed9ed012a2a2dfacc24e5b7e765b3919a2ae6033f /src/Agent.Windows/Tools
parent60d558e9a7eb1944fc58ee88e121a5db5248a332 (diff)
perf(sandbox): apply ACLs at approved roots
Replace per-descendant ACL enumeration, security descriptor snapshots, journal entries, audit events, and cleanup rewrites with one inheritable ACE and one snapshot per approved directory root. Exact file approvals continue to receive non-inheritable file ACEs, and nested roots retain independent least-privilege scopes cleaned in reverse order. Keep protected descendant DACLs protected instead of disabling inheritance protection. Such paths remain OS-denied, preserving fail-safe behavior without silently expanding the SandboxPolicy. Remove the obsolete descendant normalization pass. Restoring the root security descriptor removes inherited Application SID ACEs from existing and Task-created descendants; crash recovery performs the same root descriptor restoration from the journal. Add tests for existing-child inheritance, newly-created-child inheritance, protected descendants, one journal entry per root, exact descriptor restoration, no residual Application SID ACE, and independent-process crash recovery. Extend the existing elevated recovery and backend tests to validate inherited child cleanup and one ACL apply/restore event per root. Verification: Release build passed with 0 warnings and 0 errors; Core 50/50 passed; Planner 10/10 passed; focused ACL/recovery 3/3 passed; elevated Windows Security 28/28 passed; all reported skip 0. Operational evidence: the updated Release Bundle completed MCP Task 9b4a59a9-1f9b-4362-85f4-7b858899b488. ACL apply/restore decreased from 3,583/3,583 events taking 72/57 seconds to 3/3 root_inheritance events completing within the same second. WFP residual filters were 0 and the Task journal was removed. Release: refreshed the win-x64 self-contained folder bundle, clean ZIP, manifest, and checksum while preserving the local agent.yaml. Agent.Windows.dll SHA-256 is 24FDDD5727D5455170B6742B1D21E023A0BD70F92C225A2681BB5832C7AC0824; ZIP SHA-256 is EF7684C1A5A6155B774F4DF23CB08E9D1CF676351BF2B7D0F57915304495F456. No dependencies were added.
Diffstat (limited to 'src/Agent.Windows/Tools')
-rw-r--r--src/Agent.Windows/Tools/WindowsToolBackend.cs32
1 files changed, 4 insertions, 28 deletions
diff --git a/src/Agent.Windows/Tools/WindowsToolBackend.cs b/src/Agent.Windows/Tools/WindowsToolBackend.cs
index a21544b..c0d1561 100644
--- a/src/Agent.Windows/Tools/WindowsToolBackend.cs
+++ b/src/Agent.Windows/Tools/WindowsToolBackend.cs
@@ -49,8 +49,10 @@ public sealed class WindowsToolBackend(WindowsSandboxSettings settings, ISecurit
// Do not rewrite a file ACL when execute is the only requested capability; CreateProcess remains
// constrained by AppContainer access plus canonical path/SHA-256 approval.
if (File.Exists(path) && capabilities == FileCapability.Execute) continue;
- context.Roots.Add(path);
- foreach (string target in ExistingAclTargets(path)) context.Acls.Add(acl.Grant(taskId, target, capabilities));
+ // A single inheritable ACE on an approved directory covers existing and newly-created
+ // descendants. Snapshotting and rewriting every child ACL is both redundant and
+ // prohibitively expensive for runtime trees such as Python installations.
+ context.Acls.Add(acl.Grant(taskId, path, capabilities));
}
IReadOnlyList<NetworkEndpoint> allowances = settings.NetworkMode == "allow"
? await NetworkAllowanceSnapshot.ResolveAsync(settings.NetworkAllowances, cancellationToken).ConfigureAwait(false)
@@ -193,11 +195,6 @@ public sealed class WindowsToolBackend(WindowsSandboxSettings settings, ISecurit
List<Exception> failures = [];
try { context.Network?.Dispose(); } catch (Exception ex) { failures.Add(ex); }
foreach (AclSnapshot snapshot in context.Acls.AsEnumerable().Reverse()) try { context.Acl.Restore(taskId, snapshot); } catch (Exception ex) { failures.Add(ex); }
- var existingSnapshots = context.Acls.Select(static snapshot => snapshot.Path).ToHashSet(StringComparer.OrdinalIgnoreCase);
- foreach (string entry in context.Roots.SelectMany(ExistingAclTargets).Distinct(StringComparer.OrdinalIgnoreCase)
- .Where(entry => !existingSnapshots.Contains(entry))
- .OrderBy(static entry => entry.Count(character => character == Path.DirectorySeparatorChar)))
- try { context.Acl.NormalizeNewEntry(taskId, entry); } catch (Exception ex) { failures.Add(ex); }
if (failures.Count == 0) context.Journal.CompleteAndDelete();
await Task.CompletedTask;
if (failures.Count > 0) throw new AggregateException("Task resource cleanup failed.", failures);
@@ -215,26 +212,6 @@ public sealed class WindowsToolBackend(WindowsSandboxSettings settings, ISecurit
var regex = new Regex(query, RegexOptions.CultureInvariant, TimeSpan.FromSeconds(1));
return files.Where(path => IsTextMatch(path, regex.IsMatch));
}
- private static IEnumerable<string> ExistingAclTargets(string path)
- {
- if (File.Exists(path))
- {
- yield return path;
- yield break;
- }
- if (!Directory.Exists(path)) yield break;
- var options = new EnumerationOptions
- {
- RecurseSubdirectories = true,
- IgnoreInaccessible = false,
- ReturnSpecialDirectories = false,
- AttributesToSkip = FileAttributes.ReparsePoint
- };
- foreach (string entry in Directory.EnumerateFileSystemEntries(path, "*", options)
- .OrderByDescending(static entry => entry.Count(character => character == Path.DirectorySeparatorChar)))
- yield return entry;
- yield return path;
- }
private static string ApplyPatch(string path, string patch)
{
string current = File.Exists(path) ? File.ReadAllText(path) : string.Empty;
@@ -251,7 +228,6 @@ public sealed class WindowsToolBackend(WindowsSandboxSettings settings, ISecurit
public TaskResourceJournal Journal { get; } = journal;
public TaskAclManager Acl { get; } = acl;
public List<AclSnapshot> Acls { get; } = [];
- public HashSet<string> Roots { get; } = new(StringComparer.OrdinalIgnoreCase);
public TaskNetworkPolicy? Network { get; set; }
}
}