This commit is contained in:
@@ -291,7 +291,7 @@ export async function runPiece(
|
||||
spawnSubTask?: (params: { title: string; instruction: string; piece?: string }) => Promise<{ jobId: string; subtaskIndex: number; workspacePath: string }>;
|
||||
cancelCheck?: () => boolean;
|
||||
abortController?: AbortController;
|
||||
safetyConfig?: { maxIterations?: number; maxRevisits?: number; bashUnrestricted?: boolean; bashSandbox?: 'auto' | 'always' | 'off' };
|
||||
safetyConfig?: { maxIterations?: number; maxRevisits?: number; bashUnrestricted?: boolean; bashSandbox?: 'auto' | 'always' | 'off'; bashAllowNetwork?: boolean };
|
||||
searchFilter?: SearchFilterConfig;
|
||||
customPiecesDir?: string | string[];
|
||||
contextManager?: ContextManager;
|
||||
@@ -690,7 +690,7 @@ function prepareMovementContext(
|
||||
/** Role for the job owner. */
|
||||
notesUserRole?: 'admin' | 'user';
|
||||
/** Safety config — threaded so prepareMovementContext can propagate bashUnrestricted. */
|
||||
safetyConfig?: { bashUnrestricted?: boolean; bashSandbox?: 'auto' | 'always' | 'off' };
|
||||
safetyConfig?: { bashUnrestricted?: boolean; bashSandbox?: 'auto' | 'always' | 'off'; bashAllowNetwork?: boolean };
|
||||
skillCatalog?: import('./skills.js').SkillCatalog;
|
||||
/** Per-task option: when true, MCP tools are not loaded/dispatched. */
|
||||
mcpDisabled?: boolean;
|
||||
@@ -716,6 +716,7 @@ function prepareMovementContext(
|
||||
allowedCommands: movementDef.allowed_commands,
|
||||
bashUnrestricted: options?.safetyConfig?.bashUnrestricted,
|
||||
bashSandbox: options?.safetyConfig?.bashSandbox,
|
||||
bashAllowNetwork: options?.safetyConfig?.bashAllowNetwork,
|
||||
skillCatalog: options?.skillCatalog,
|
||||
allowedSshConnections: movementDef.allowed_ssh_connections,
|
||||
pieceName: piece.name,
|
||||
|
||||
@@ -62,6 +62,7 @@ export interface ToolContext {
|
||||
allowedCommands?: string[]; // Bash ツールで許可するコマンド名一覧 (省略時は DEFAULT_ALLOWED_COMMANDS)
|
||||
bashUnrestricted?: boolean; // true: skip the command whitelist (bwrap/exec is chosen by bashSandbox, not this)
|
||||
bashSandbox?: 'auto' | 'always' | 'off'; // サンドボックス機構の選択 (config.safety.bashSandbox 由来)
|
||||
bashAllowNetwork?: boolean; // true: bwrap サンドボックスで host network を許可 (--unshare-net を外す。config.safety.bashAllowNetwork 由来)
|
||||
skillCatalog?: import('../skills.js').SkillCatalog;
|
||||
toolsConfig?: ToolsConfig;
|
||||
searchFilter?: SearchFilterConfig; // AppConfig.searchFilter (トップレベル)
|
||||
@@ -1007,6 +1008,7 @@ async function executeBash(input: Record<string, unknown>, ctx: ToolContext): Pr
|
||||
}
|
||||
const result: SandboxedBashResult = await executeSandboxedBash(
|
||||
command, ctx.workspacePath, timeoutSec, BASH_MAX_BUFFER_BYTES, ctx.abortSignal, skillBinds,
|
||||
ctx.bashAllowNetwork === true,
|
||||
);
|
||||
const out = result.isError ? result.output : capOutput(result.output, 'stdout');
|
||||
logBashHistory(ctx.workspacePath, command, result.isError, Date.now() - startedAt, {
|
||||
|
||||
@@ -245,6 +245,27 @@ describe('buildBwrapArgs sandboxing', () => {
|
||||
expect(i).toBeGreaterThan(-1);
|
||||
expect(args).toContain('HOME');
|
||||
});
|
||||
|
||||
it('unshares network by default (allowNetwork omitted)', () => {
|
||||
const args = buildBwrapArgs('echo hi', '/work/ws');
|
||||
expect(args).toContain('--unshare-net');
|
||||
});
|
||||
|
||||
it('keeps host network when allowNetwork=true (drops only --unshare-net)', () => {
|
||||
const args = buildBwrapArgs('echo hi', '/work/ws', undefined, process.env, true);
|
||||
expect(args).not.toContain('--unshare-net');
|
||||
// all other isolations remain
|
||||
expect(args).toContain('--unshare-user');
|
||||
expect(args).toContain('--unshare-ipc');
|
||||
expect(args).toContain('--unshare-pid');
|
||||
expect(args).toContain('--unshare-uts');
|
||||
expect(args).toContain('--unshare-cgroup');
|
||||
});
|
||||
|
||||
it('unshares network when allowNetwork=false (explicit)', () => {
|
||||
const args = buildBwrapArgs('echo hi', '/work/ws', undefined, process.env, false);
|
||||
expect(args).toContain('--unshare-net');
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkBwrapAvailable', () => {
|
||||
|
||||
@@ -36,6 +36,7 @@ export function buildBwrapArgs(
|
||||
workspacePath: string,
|
||||
extraReadOnlyBinds?: ExtraReadOnlyBind[],
|
||||
parentEnv: NodeJS.ProcessEnv = process.env,
|
||||
allowNetwork: boolean = false,
|
||||
): string[] {
|
||||
const args: string[] = [];
|
||||
|
||||
@@ -70,7 +71,13 @@ export function buildBwrapArgs(
|
||||
|
||||
args.push('--chdir', workspacePath);
|
||||
args.push('--die-with-parent');
|
||||
args.push('--unshare-user', '--unshare-ipc', '--unshare-pid', '--unshare-uts', '--unshare-cgroup', '--unshare-net');
|
||||
// Network isolation is the one --unshare-* that's optional. When
|
||||
// allowNetwork is true (config.safety.bashAllowNetwork) the sandbox keeps host
|
||||
// network so pip/npm/curl work; all other namespaces stay unshared.
|
||||
args.push('--unshare-user', '--unshare-ipc', '--unshare-pid', '--unshare-uts', '--unshare-cgroup');
|
||||
if (!allowNetwork) {
|
||||
args.push('--unshare-net');
|
||||
}
|
||||
|
||||
args.push('--clearenv');
|
||||
const sandboxEnv = buildSandboxEnv(parentEnv, workspacePath);
|
||||
@@ -124,8 +131,9 @@ export async function executeSandboxedBash(
|
||||
maxBuffer: number,
|
||||
abortSignal?: AbortSignal,
|
||||
extraReadOnlyBinds?: ExtraReadOnlyBind[],
|
||||
allowNetwork: boolean = false,
|
||||
): Promise<SandboxedBashResult> {
|
||||
const args = buildBwrapArgs(command, workspacePath, extraReadOnlyBinds);
|
||||
const args = buildBwrapArgs(command, workspacePath, extraReadOnlyBinds, process.env, allowNetwork);
|
||||
|
||||
if (abortSignal?.aborted) {
|
||||
return { output: 'Cancelled before sandbox bash launch', isError: true };
|
||||
|
||||
Reference in New Issue
Block a user