feat(core): populate sandbox forbidden paths with project ignore file contents (#24038)

This commit is contained in:
Emily Hedlund
2026-04-01 12:27:55 -04:00
committed by GitHub
parent 066da2a1d1
commit 6a8a0d4faa
13 changed files with 309 additions and 79 deletions
@@ -38,7 +38,7 @@ describe('WindowsSandboxManager', () => {
manager = new WindowsSandboxManager({
workspace: testCwd,
modeConfig: { readonly: false, allowOverrides: true },
forbiddenPaths: [],
forbiddenPaths: async () => [],
});
});
@@ -107,7 +107,7 @@ describe('WindowsSandboxManager', () => {
const planManager = new WindowsSandboxManager({
workspace: testCwd,
modeConfig: { readonly: true, allowOverrides: false },
forbiddenPaths: [],
forbiddenPaths: async () => [],
});
const req: SandboxRequest = {
command: 'curl',
@@ -139,7 +139,7 @@ describe('WindowsSandboxManager', () => {
workspace: testCwd,
modeConfig: { allowOverrides: true, network: false },
policyManager: mockPolicyManager,
forbiddenPaths: [],
forbiddenPaths: async () => [],
});
const req: SandboxRequest = {
@@ -369,7 +369,7 @@ describe('WindowsSandboxManager', () => {
const managerWithForbidden = new WindowsSandboxManager({
workspace: testCwd,
forbiddenPaths: [missingPath],
forbiddenPaths: async () => [missingPath],
});
const req: SandboxRequest = {
@@ -397,7 +397,7 @@ describe('WindowsSandboxManager', () => {
try {
const managerWithForbidden = new WindowsSandboxManager({
workspace: testCwd,
forbiddenPaths: [forbiddenPath],
forbiddenPaths: async () => [forbiddenPath],
});
const req: SandboxRequest = {
@@ -427,7 +427,7 @@ describe('WindowsSandboxManager', () => {
try {
const managerWithForbidden = new WindowsSandboxManager({
workspace: testCwd,
forbiddenPaths: [conflictPath],
forbiddenPaths: async () => [conflictPath],
});
const req: SandboxRequest = {
@@ -458,12 +458,9 @@ describe('WindowsSandboxManager', () => {
call[1][0] === path.resolve(conflictPath),
);
// Both should have been called
expect(allowCallIndex).toBeGreaterThan(-1);
// Conflict should have been filtered out of allow calls
expect(allowCallIndex).toBe(-1);
expect(denyCallIndex).toBeGreaterThan(-1);
// Verify order: explicitly denying must happen after the explicit allow
expect(allowCallIndex).toBeLessThan(denyCallIndex);
} finally {
fs.rmSync(conflictPath, { recursive: true, force: true });
}
@@ -19,6 +19,7 @@ import {
tryRealpath,
type SandboxPermissions,
type ParsedSandboxDenial,
resolveSandboxPaths,
} from '../../services/sandboxManager.js';
import type { ShellExecutionResult } from '../../services/shellExecutionService.js';
import {
@@ -288,14 +289,16 @@ export class WindowsSandboxManager implements SandboxManager {
await this.grantLowIntegrityAccess(this.options.workspace);
}
const { allowed: allowedPaths, forbidden: forbiddenPaths } =
await resolveSandboxPaths(this.options, req);
// Grant "Low Mandatory Level" access to includeDirectories.
const includeDirs = sanitizePaths(this.options.includeDirectories) || [];
const includeDirs = sanitizePaths(this.options.includeDirectories);
for (const includeDir of includeDirs) {
await this.grantLowIntegrityAccess(includeDir);
}
// Grant "Low Mandatory Level" read/write access to allowedPaths.
const allowedPaths = sanitizePaths(req.policy?.allowedPaths) || [];
for (const allowedPath of allowedPaths) {
const resolved = await tryRealpath(allowedPath);
if (!fs.existsSync(resolved)) {
@@ -308,8 +311,9 @@ export class WindowsSandboxManager implements SandboxManager {
}
// Grant "Low Mandatory Level" write access to additional permissions write paths.
const additionalWritePaths =
sanitizePaths(mergedAdditional.fileSystem?.write) || [];
const additionalWritePaths = sanitizePaths(
mergedAdditional.fileSystem?.write,
);
for (const writePath of additionalWritePaths) {
const resolved = await tryRealpath(writePath);
if (!fs.existsSync(resolved)) {
@@ -358,7 +362,6 @@ export class WindowsSandboxManager implements SandboxManager {
// is restricted to avoid host corruption. External commands rely on
// Low Integrity read/write restrictions, while internal commands
// use the manifest for enforcement.
const forbiddenPaths = sanitizePaths(this.options.forbiddenPaths) || [];
for (const forbiddenPath of forbiddenPaths) {
try {
await this.denyLowIntegrityAccess(forbiddenPath);