Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
20 changes: 14 additions & 6 deletions packages/runtime/src/__tests__/builtin-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@ import { expect } from '../test-helpers.js';
import { buildBuiltinTools } from '../builtin-tools.js';
import { SandboxManager } from '../sandbox/sandbox-manager.js';
import { LinuxBubblewrapBackend } from '../sandbox/linux-sandbox.js';
import { MacosSeatbeltBackend } from '../sandbox/macos-seatbelt.js';
import {
MacosSeatbeltBackend,
macosBashExecutableRoots,
resolveMacosDeveloperToolchainRoot,
} from '../sandbox/macos-seatbelt.js';
import { WindowsBrokerSandboxBackend } from '../sandbox/windows-sandbox.js';
import { SandboxCommandError } from '../sandbox/errors.js';
import type { ShellRunLauncher } from '../shell-tools.js';
Expand Down Expand Up @@ -1465,12 +1469,16 @@ describe('builtin Bash streaming output', () => {
(argument) => /^-DEXECUTABLE_ROOT_\d+=/u.test(argument) && argument.endsWith(`=${root}`),
);
assert.ok(hasExecutableRoot(executableRoot));
if (process.execPath.startsWith('/opt/homebrew/')) {
assert.ok(hasExecutableRoot('/opt/homebrew'));
}
if (process.execPath.startsWith('/usr/local/')) {
assert.ok(hasExecutableRoot('/usr/local'));
const expectedRoots = macosBashExecutableRoots({
execPath: process.execPath,
path: process.env.PATH,
developerRoot: resolveMacosDeveloperToolchainRoot(process.env.DEVELOPER_DIR),
});
for (const expectedRoot of expectedRoots) {
assert.ok(hasExecutableRoot(expectedRoot), `missing executable root ${expectedRoot}`);
}
assert.equal(input?.env?.GIT_CONFIG_GLOBAL, process.env.GIT_CONFIG_GLOBAL ?? '/dev/null');
Comment thread
hqhq1025 marked this conversation as resolved.
assert.equal(input?.env?.GIT_CONFIG_SYSTEM, process.env.GIT_CONFIG_SYSTEM ?? '/dev/null');
} finally {
await rm(root, { recursive: true, force: true });
}
Expand Down
121 changes: 119 additions & 2 deletions packages/runtime/src/__tests__/macos-seatbelt-smoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ import {
type PermissionProfile,
} from '@maka/core/permission-profile';

import { MACOS_SEATBELT_EXECUTABLE, MacosSeatbeltBackend } from '../sandbox/macos-seatbelt.js';
import {
MACOS_SEATBELT_EXECUTABLE,
MacosSeatbeltBackend,
macosBashExecutableRoots,
resolveMacosDeveloperToolchainRoot,
} from '../sandbox/macos-seatbelt.js';
import { SandboxManager } from '../sandbox/sandbox-manager.js';

const canRunSeatbelt = process.platform === 'darwin' && existsSync(MACOS_SEATBELT_EXECUTABLE);
Expand Down Expand Up @@ -69,6 +74,8 @@ function runSeatbeltCommand(
command: string,
profile: PermissionProfile = createWorkspaceWritePermissionProfile(),
includeTempRoots = false,
executableRoots: readonly string[] = [],
env: NodeJS.ProcessEnv = process.env,
) {
const manager = new SandboxManager([new MacosSeatbeltBackend()]);
const result = manager.transform({
Expand All @@ -81,6 +88,7 @@ function runSeatbeltCommand(
pathContext: {
workspaceRoots: [workspaceRoot],
...(includeTempRoots ? { tmpdir: tmpdir(), slashTmp: '/tmp' } : {}),
...(executableRoots.length > 0 ? { executableRoots } : {}),
},
},
});
Expand All @@ -90,7 +98,7 @@ function runSeatbeltCommand(

return spawnSync(result.exec.argv[0], result.exec.argv.slice(1), {
cwd: result.exec.cwd,
env: { ...process.env, ...result.exec.env },
env: { ...env, ...result.exec.env },
encoding: 'utf8',
});
}
Expand Down Expand Up @@ -146,6 +154,115 @@ describe('macOS Seatbelt smoke', { skip: !canRunSeatbelt }, () => {
assert.equal(child.status, 0, child.stderr);
});

it('runs a repository-local Homebrew Git command with its runtime dependencies', {
skip: !existsSync('/opt/homebrew/bin/git'),
}, async () => {
const workspaceRoot = await makeWorkspace();
cleanup.push(workspaceRoot);
const gitEnvironment = {
...process.env,
GIT_CONFIG_GLOBAL: '/dev/null',
GIT_CONFIG_SYSTEM: '/dev/null',
};
for (const args of [
['init'],
['config', 'user.name', 'Maka Test'],
['config', 'user.email', 'maka@example.test'],
]) {
const setup = spawnSync('/opt/homebrew/bin/git', args, {
cwd: workspaceRoot,
env: gitEnvironment,
encoding: 'utf8',
});
assert.equal(setup.status, 0, setup.stderr);
}
await writeFile(join(workspaceRoot, 'fixture.txt'), 'fixture\n');
for (const args of [
['add', 'fixture.txt'],
['commit', '-m', 'fixture commit'],
]) {
const setup = spawnSync('/opt/homebrew/bin/git', args, {
cwd: workspaceRoot,
env: gitEnvironment,
encoding: 'utf8',
});
assert.equal(setup.status, 0, setup.stderr);
}
const executableRoots = macosBashExecutableRoots({
execPath: process.execPath,
path: '/opt/homebrew/bin:/usr/bin:/bin',
});

const child = runSeatbeltCommand(
workspaceRoot,
'/opt/homebrew/bin/git log -1 --pretty=format:"%s"',
createWorkspaceWritePermissionProfile(),
true,
executableRoots,
gitEnvironment,
);

assert.equal(child.status, 0, child.stderr);
assert.equal(child.stdout, 'fixture commit');
});

it('allows Apple Git to load the selected developer toolchain', async () => {
const workspaceRoot = await makeWorkspace();
cleanup.push(workspaceRoot);
const executableRoots = macosBashExecutableRoots({
execPath: process.execPath,
path: '/usr/bin:/bin',
});

const child = runSeatbeltCommand(
workspaceRoot,
'/usr/bin/git --version',
createWorkspaceWritePermissionProfile(),
true,
executableRoots,
{
...process.env,
GIT_CONFIG_GLOBAL: '/dev/null',
GIT_CONFIG_SYSTEM: '/dev/null',
},
);

assert.equal(child.status, 0, child.stderr);
assert.match(child.stdout, /^git version /);
});

it('admits an alternate selected Xcode application root', async () => {
const workspaceRoot = await makeWorkspace();
const alternateRoot = await realpath(
await mkdtemp(join(tmpdir(), 'maka-seatbelt-xcode-beta-')),
);
cleanup.push(workspaceRoot, alternateRoot);
const contents = join(alternateRoot, 'Xcode-beta.app', 'Contents');
const developer = join(contents, 'Developer');
const marker = join(contents, 'SharedFrameworks', 'marker.txt');
await mkdir(developer, { recursive: true });
await mkdir(join(contents, 'SharedFrameworks'), { recursive: true });
await writeFile(marker, 'alternate developer root\n');
const developerRoot = resolveMacosDeveloperToolchainRoot(undefined, () => developer);
assert.ok(developerRoot);
const executableRoots = macosBashExecutableRoots({
execPath: process.execPath,
path: '/usr/bin:/bin',
developerRoot,
});

const child = runSeatbeltCommand(
workspaceRoot,
`/bin/cat ${JSON.stringify(marker)}`,
createWorkspaceWritePermissionProfile(),
false,
executableRoots,
);

assert.equal(child.status, 0, child.stderr);
assert.equal(child.stdout, 'alternate developer root\n');
});

it('denies writes outside the workspace root', async () => {
const workspaceRoot = await makeWorkspace();
const outsideRoot = await realpath(await mkdtemp(join(tmpdir(), 'maka-seatbelt-outside-')));
Expand Down
81 changes: 81 additions & 0 deletions packages/runtime/src/__tests__/macos-seatbelt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import {
buildSeatbeltPolicy,
createSeatbeltExecArgs,
escapeSeatbeltRegex,
macosBashExecutableRoots,
resolveMacosDeveloperToolchainRoot,
} from '../sandbox/macos-seatbelt.js';
import type { SandboxTransformRequest } from '../sandbox/types.js';

Expand Down Expand Up @@ -136,6 +138,85 @@ describe('escapeSeatbeltRegex', () => {
});
});

describe('macosBashExecutableRoots', () => {
it('provides fixed Homebrew and Apple developer toolchain roots', () => {
assert.deepEqual(
macosBashExecutableRoots({
execPath: '/Applications/Maka.app/Contents/MacOS/Maka',
path: '/Users/test/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin',
developerRoot: '/Applications/Xcode-beta.app/Contents',
}),
[
'/Applications/Maka.app/Contents/MacOS',
'/opt/homebrew/bin',
'/opt/homebrew/sbin',
'/opt/homebrew/Cellar',
'/opt/homebrew/opt',
'/opt/homebrew/lib',
'/opt/homebrew/libexec',
'/opt/homebrew/share',
'/usr/local/bin',
'/usr/local/sbin',
'/usr/local/Cellar',
'/usr/local/opt',
'/usr/local/lib',
'/usr/local/libexec',
'/usr/local/share',
'/Applications/Xcode-beta.app/Contents',
],
);
});

it('does not expose unrelated PATH directories as executable roots', () => {
const roots = macosBashExecutableRoots({
execPath: '/Applications/Maka.app/Contents/MacOS/Maka',
path: '/Users/test/private:/custom/toolchain/bin:/usr/bin:/bin',
});

assert.equal(roots.includes('/Users/test/private'), false);
assert.equal(roots.includes('/custom/toolchain/bin'), false);
assert.equal(roots.includes('/opt/homebrew/etc'), false);
assert.equal(roots.includes('/usr/local/etc'), false);
});

it('keeps Homebrew Cellar Node on the fixed package-manager subroots', () => {
const roots = macosBashExecutableRoots({
execPath: '/opt/homebrew/Cellar/node/24.0.0/bin/node',
path: '/opt/homebrew/bin:/usr/bin:/bin',
developerRoot: '/Applications/Xcode.app/Contents',
});

assert.equal(roots.includes('/opt/homebrew'), false);
assert.equal(roots.includes('/opt/homebrew/Cellar'), true);
assert.equal(roots.includes('/opt/homebrew/opt'), true);
assert.equal(roots.includes('/opt/homebrew/lib'), true);
});

it('resolves the configured or selected developer directory to the Xcode toolchain root', () => {
const root = mkdtempSync(join(realpathSync('/tmp'), 'maka-xcode-root-'));
try {
const contents = join(root, 'Xcode-beta.app', 'Contents');
const developer = join(contents, 'Developer');
const alias = join(root, 'selected-xcode');
mkdirSync(developer, { recursive: true });
symlinkSync(developer, alias);

assert.equal(
resolveMacosDeveloperToolchainRoot(alias, () => {
throw new Error('DEVELOPER_DIR must take precedence');
}),
realpathSync(contents),
);
assert.equal(
resolveMacosDeveloperToolchainRoot(undefined, () => developer),
realpathSync(contents),
);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
});

describe('buildSeatbeltPolicy', () => {
it('builds read-only policy with readable workspace roots and no writable workspace roots', () => {
const result = buildSeatbeltPolicy({
Expand Down
26 changes: 17 additions & 9 deletions packages/runtime/src/builtin-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ import { profileRequiresSandbox, type SandboxManager } from './sandbox/sandbox-m
import { SandboxCommandError } from './sandbox/errors.js';
import { isLikelySandboxDenial } from './sandbox/detect.js';
import { linuxExecutableRoots } from './sandbox/linux-sandbox.js';
import {
macosBashExecutableRoots,
resolveMacosDeveloperToolchainRoot,
} from './sandbox/macos-seatbelt.js';
import { pinExistingLinuxProfilePath } from './sandbox/linux-profile-path.js';
import type { SandboxPlatform, SandboxType } from './sandbox/types.js';
import type { ChildFdInput } from './child-fd-input.js';
Expand Down Expand Up @@ -747,6 +751,14 @@ function sandboxCommand(
? { profile: boundary.profile, workspaceRoots: [cwd] }
: effectivePermissionProfile(explicitProfile, ctx.permissionMode ?? 'ask', cwd);
const env = { ...process.env };
if (platform === 'darwin' && env.GIT_CONFIG_GLOBAL === undefined) {
// Restricted Seatbelt profiles cannot read ~/.gitconfig. Treat it as absent
// by default so ordinary repository-local Git commands do not fail closed.
env.GIT_CONFIG_GLOBAL = '/dev/null';
}
if (platform === 'darwin' && env.GIT_CONFIG_SYSTEM === undefined) {
env.GIT_CONFIG_SYSTEM = '/dev/null';
}
if (pty) {
if (profileRequiresSandbox(effective.profile)) {
throw new SandboxCommandError({
Expand Down Expand Up @@ -833,7 +845,11 @@ function sandboxCommand(
...(platform === 'win32' ? {} : { slashTmp: '/tmp' }),
...(platform === 'darwin'
? {
executableRoots: macosRuntimeExecutableRoots(process.execPath),
executableRoots: macosBashExecutableRoots({
execPath: process.execPath,
path: env.PATH,
developerRoot: resolveMacosDeveloperToolchainRoot(env.DEVELOPER_DIR),
}),
}
: {}),
...(platform === 'linux'
Expand Down Expand Up @@ -1084,14 +1100,6 @@ function canonicalExistingPath(path: string): string {
}
}

function macosRuntimeExecutableRoots(execPath: string): readonly string[] {
return [
...linuxExecutableRoots({ execPath }),
...(execPath.startsWith('/opt/homebrew/') ? ['/opt/homebrew'] : []),
...(execPath.startsWith('/usr/local/') ? ['/usr/local'] : []),
];
}

function effectivePermissionProfile(
explicitProfile: PermissionProfile | undefined,
permissionMode: NonNullable<MakaToolContext['permissionMode']>,
Expand Down
1 change: 1 addition & 0 deletions packages/runtime/src/sandbox/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export {
buildSeatbeltPolicy,
createSeatbeltExecArgs,
escapeSeatbeltRegex,
macosBashExecutableRoots,
} from './macos-seatbelt.js';
export type {
BuildSeatbeltPolicyInput,
Expand Down
Loading