Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
11 changes: 7 additions & 4 deletions packages/core/src/sandbox/linux/bwrapArgsBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,6 @@ export async function buildBwrapArgs(
bwrapArgs.push('--ro-bind-try', p, p);
}

for (const p of resolvedPaths.policyWrite) {
bwrapArgs.push('--bind-try', p, p);
}

for (const file of GOVERNANCE_FILES) {
const filePath = join(workspace.original, file.path);
const realPath = join(workspace.resolved, file.path);
Expand All @@ -115,6 +111,13 @@ export async function buildBwrapArgs(
}
}

// Apply explicit additional write permissions.
// We apply these after GOVERNANCE_FILES and gitWorktree rules so that if a user explicitly requests
// write access to a protected directory (like .git), the explicit allow takes precedence in bwrap.
for (const p of resolvedPaths.policyWrite) {
bwrapArgs.push('--bind-try', p, p);
}
Comment thread
galdawave marked this conversation as resolved.
Resolved

for (const p of resolvedPaths.forbidden) {
if (!fs.existsSync(p)) continue;
try {
Expand Down
56 changes: 54 additions & 2 deletions packages/core/src/sandbox/macos/seatbeltArgsBuilder.ts
Comment thread
galdawave marked this conversation as resolved.
Resolved
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,23 @@ export function escapeSchemeString(str: string): string {
return str.replace(/[\\"]/g, '\\$&');
}

/**
* Checks if a path is explicitly allowed by additional write permissions.
*/
function isPathExplicitlyAllowed(
filePath: string,
realFilePath: string,
policyWrite: string[],
): boolean {
return policyWrite.some(
(p) =>
p === filePath ||
p === realFilePath ||
filePath.startsWith(p + path.sep) ||
Comment thread
galdawave marked this conversation as resolved.
Resolved
realFilePath.startsWith(p + path.sep),
);
}

/**
* Builds a complete macOS Seatbelt profile string using a strict allowlist.
* It embeds paths directly into the profile, properly escaped for Scheme.
Expand Down Expand Up @@ -150,6 +167,17 @@ export function buildSeatbeltProfile(options: SeatbeltArgsOptions): string {
);
const realGovernanceFile = resolveToRealPath(governanceFile);

// Skip deny if explicitly allowed by additional write permissions
const isExplicitlyAllowed = isPathExplicitlyAllowed(
governanceFile,
realGovernanceFile,
resolvedPaths.policyWrite,
);

if (isExplicitlyAllowed) {
continue;
}

// Determine if it should be treated as a directory (subpath) or a file (literal).
// .git is generally a directory, while ignore files are literals.
let isDirectory = GOVERNANCE_FILES[i].isDirectory;
Expand All @@ -175,10 +203,34 @@ export function buildSeatbeltProfile(options: SeatbeltArgsOptions): string {
if (resolvedPaths.gitWorktree) {
const { worktreeGitDir, mainGitDir } = resolvedPaths.gitWorktree;
if (worktreeGitDir) {
profile += `(deny file-write* (subpath "${escapeSchemeString(worktreeGitDir)}"))\n`;
const realWorktreeGitDir = resolveToRealPath(worktreeGitDir);
if (
!isPathExplicitlyAllowed(
worktreeGitDir,
realWorktreeGitDir,
resolvedPaths.policyWrite,
)
) {
profile += `(deny file-write* (subpath "${escapeSchemeString(worktreeGitDir)}"))\n`;
if (realWorktreeGitDir !== worktreeGitDir) {
profile += `(deny file-write* (subpath "${escapeSchemeString(realWorktreeGitDir)}"))\n`;
}
}
}
if (mainGitDir) {
profile += `(deny file-write* (subpath "${escapeSchemeString(mainGitDir)}"))\n`;
const realMainGitDir = resolveToRealPath(mainGitDir);
if (
!isPathExplicitlyAllowed(
mainGitDir,
realMainGitDir,
resolvedPaths.policyWrite,
)
) {
profile += `(deny file-write* (subpath "${escapeSchemeString(mainGitDir)}"))\n`;
if (realMainGitDir !== mainGitDir) {
profile += `(deny file-write* (subpath "${escapeSchemeString(realMainGitDir)}"))\n`;
}
}
}
}

Expand Down
Loading