Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion packages/core/src/sandbox/linux/LinuxSandboxManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,6 @@ export class LinuxSandboxManager implements SandboxManager {
workspaceWrite,
networkAccess: mergedAdditional.network ?? false,
maskFilePath: this.getMaskFilePath(),
isWriteCommand: req.command === '__write',
});

const bpfPath = getSeccompBpfPath();
Expand Down
4 changes: 1 addition & 3 deletions packages/core/src/sandbox/linux/bwrapArgsBuilder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@ describe.skipIf(os.platform() === 'win32')('buildBwrapArgs', () => {
workspaceWrite: false,
networkAccess: false,
maskFilePath: '/tmp/mask',
isWriteCommand: false,
};

it('should correctly format the base arguments', async () => {
Expand Down Expand Up @@ -188,7 +187,7 @@ describe.skipIf(os.platform() === 'win32')('buildBwrapArgs', () => {
expect(args[args.indexOf('/opt/tools') - 1]).toBe('--bind-try');
});

it('should bind the parent directory of a non-existent path', async () => {
it('should bind the parent directory of a non-existent path with --bind-try', async () => {
vi.mocked(fs.existsSync).mockImplementation((p) => {
if (p === '/home/user/workspace/new-file.txt') return false;
return true;
Expand All @@ -199,7 +198,6 @@ describe.skipIf(os.platform() === 'win32')('buildBwrapArgs', () => {
resolvedPaths: createResolvedPaths({
policyAllowed: ['/home/user/workspace/new-file.txt'],
}),
isWriteCommand: true,
});

const parentDir = '/home/user/workspace';
Expand Down
16 changes: 4 additions & 12 deletions packages/core/src/sandbox/linux/bwrapArgsBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ export interface BwrapArgsOptions {
workspaceWrite: boolean;
networkAccess: boolean;
maskFilePath: string;
isWriteCommand: boolean;
}

/**
Expand All @@ -32,13 +31,8 @@ export interface BwrapArgsOptions {
export async function buildBwrapArgs(
options: BwrapArgsOptions,
): Promise<string[]> {
const {
resolvedPaths,
workspaceWrite,
networkAccess,
maskFilePath,
isWriteCommand,
} = options;
const { resolvedPaths, workspaceWrite, networkAccess, maskFilePath } =
options;
const { workspace } = resolvedPaths;

const bwrapArgs: string[] = [
Expand Down Expand Up @@ -78,11 +72,9 @@ export async function buildBwrapArgs(
if (fs.existsSync(allowedPath)) {
bwrapArgs.push('--bind-try', allowedPath, allowedPath);
} else {
// If the path doesn't exist, we still want to allow access to its parent
// to enable creating it. Since allowedPath is already resolved by resolveSandboxPaths,
// its parent is also correctly resolved.
// Bind the parent directory as read-write to allow creating the non-existent path.
const parent = dirname(allowedPath);
bwrapArgs.push(isWriteCommand ? '--bind-try' : bindFlag, parent, parent);
bwrapArgs.push('--bind-try', parent, parent);
}
}

Expand Down
18 changes: 15 additions & 3 deletions packages/core/src/sandbox/macos/MacOsSandboxManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { MacOsSandboxManager } from './MacOsSandboxManager.js';
import type { ExecutionPolicy } from '../../services/sandboxManager.js';
import {
type ExecutionPolicy,
GOVERNANCE_FILES,
} from '../../services/sandboxManager.js';
import * as seatbeltArgsBuilder from './seatbeltArgsBuilder.js';
import fs from 'node:fs';
import os from 'node:os';
Expand Down Expand Up @@ -201,8 +204,17 @@ describe('MacOsSandboxManager', () => {
policy: mockPolicy,
});

// The seatbelt builder internally handles governance files, so we simply verify
// it is invoked correctly with the right workspace.
// Verify that governance files were actually created on disk
for (const file of GOVERNANCE_FILES) {
const expectedPath = path.join(mockWorkspace, file.path);
expect(fs.existsSync(expectedPath)).toBe(true);
if (file.isDirectory) {
expect(fs.statSync(expectedPath).isDirectory()).toBe(true);
} else {
expect(fs.statSync(expectedPath).isFile()).toBe(true);
}
}

expect(seatbeltArgsBuilder.buildSeatbeltProfile).toHaveBeenCalledWith(
expect.objectContaining({
resolvedPaths: expect.objectContaining({
Expand Down
30 changes: 30 additions & 0 deletions packages/core/src/sandbox/macos/MacOsSandboxManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
type GlobalSandboxOptions,
type ParsedSandboxDenial,
resolveSandboxPaths,
GOVERNANCE_FILES,
} from '../../services/sandboxManager.js';
import type { ShellExecutionResult } from '../../services/shellExecutionService.js';
import {
Expand All @@ -38,6 +39,27 @@ import {
type SandboxDenialCache,
} from '../utils/sandboxDenialUtils.js';
import { handleReadWriteCommands } from '../utils/sandboxReadWriteUtils.js';
import { assertValidPathString } from '../../utils/paths.js';

/**
* Ensures a file or directory exists.
*/
function touch(filePath: string, isDirectory: boolean) {
assertValidPathString(filePath);
try {
// If it exists (even as a broken symlink), do nothing
if (fs.lstatSync(filePath)) return;
} catch {
// Ignore ENOENT
}

if (isDirectory) {
fs.mkdirSync(filePath, { recursive: true });
} else {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.closeSync(fs.openSync(filePath, 'a'));
}
}

export class MacOsSandboxManager implements SandboxManager {
private readonly denialCache: SandboxDenialCache = createSandboxDenialCache();
Expand Down Expand Up @@ -149,6 +171,14 @@ export class MacOsSandboxManager implements SandboxManager {
mergedAdditional,
);

// Initialize governance files (.git, .gitignore, etc.) on the host.
// These must exist before the sandbox is established to ensure the
// seatbelt profile can correctly target and protect them.
for (const file of GOVERNANCE_FILES) {
const filePath = path.join(this.options.workspace, file.path);
touch(filePath, file.isDirectory);
}

const sandboxArgs = buildSeatbeltProfile({
resolvedPaths,
networkAccess: mergedAdditional.network,
Expand Down
Loading
Loading