mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-24 20:14:44 -07:00
fix(core): enhance sandbox usability and fix build error (#24460)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
@@ -6,7 +6,7 @@ allowOverrides = false
|
||||
|
||||
[modes.default]
|
||||
network = false
|
||||
readonly = true
|
||||
readonly = false
|
||||
approvedTools = ['cat', 'ls', 'grep', 'head', 'tail', 'less', 'Get-Content', 'dir', 'type', 'findstr', 'Get-ChildItem', 'echo']
|
||||
allowOverrides = true
|
||||
|
||||
|
||||
@@ -3630,4 +3630,150 @@ describe('PolicyEngine', () => {
|
||||
).toBe(PolicyDecision.ALLOW);
|
||||
});
|
||||
});
|
||||
|
||||
describe('additional_permissions', () => {
|
||||
const workspace = '/workspace';
|
||||
let mockSandboxManager: SandboxManager;
|
||||
let engine: PolicyEngine;
|
||||
|
||||
beforeEach(() => {
|
||||
mockSandboxManager = {
|
||||
prepareCommand: vi.fn(),
|
||||
isKnownSafeCommand: vi.fn().mockReturnValue(false),
|
||||
isDangerousCommand: vi.fn().mockReturnValue(false),
|
||||
parseDenials: vi.fn(),
|
||||
getWorkspace: vi.fn().mockReturnValue(workspace),
|
||||
} as never as SandboxManager;
|
||||
|
||||
engine = new PolicyEngine({
|
||||
rules: [
|
||||
{
|
||||
toolName: 'run_shell_command',
|
||||
decision: PolicyDecision.ALLOW,
|
||||
modes: [ApprovalMode.AUTO_EDIT],
|
||||
},
|
||||
],
|
||||
approvalMode: ApprovalMode.AUTO_EDIT,
|
||||
sandboxManager: mockSandboxManager,
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow permissions exactly at the workspace root', async () => {
|
||||
const call = {
|
||||
name: 'run_shell_command',
|
||||
args: {
|
||||
command: 'ls',
|
||||
additional_permissions: {
|
||||
fileSystem: {
|
||||
read: [workspace],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect((await engine.check(call, undefined)).decision).toBe(
|
||||
PolicyDecision.ALLOW,
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow permissions for subpaths of the workspace', async () => {
|
||||
const call = {
|
||||
name: 'run_shell_command',
|
||||
args: {
|
||||
command: 'ls',
|
||||
additional_permissions: {
|
||||
fileSystem: {
|
||||
read: [`${workspace}/subdir/file.txt`],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect((await engine.check(call, undefined)).decision).toBe(
|
||||
PolicyDecision.ALLOW,
|
||||
);
|
||||
});
|
||||
|
||||
it('should downgrade ALLOW to ASK_USER if a read path is outside workspace', async () => {
|
||||
const call = {
|
||||
name: 'run_shell_command',
|
||||
args: {
|
||||
command: 'ls',
|
||||
additional_permissions: {
|
||||
fileSystem: {
|
||||
read: ['/outside'],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect((await engine.check(call, undefined)).decision).toBe(
|
||||
PolicyDecision.ASK_USER,
|
||||
);
|
||||
});
|
||||
|
||||
it('should downgrade ALLOW to ASK_USER if a write path is outside workspace', async () => {
|
||||
const call = {
|
||||
name: 'run_shell_command',
|
||||
args: {
|
||||
command: 'ls',
|
||||
additional_permissions: {
|
||||
fileSystem: {
|
||||
write: ['/outside/secret.txt'],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect((await engine.check(call, undefined)).decision).toBe(
|
||||
PolicyDecision.ASK_USER,
|
||||
);
|
||||
});
|
||||
|
||||
it('should downgrade ALLOW to ASK_USER if any path in a list is outside workspace', async () => {
|
||||
const call = {
|
||||
name: 'run_shell_command',
|
||||
args: {
|
||||
command: 'ls',
|
||||
additional_permissions: {
|
||||
fileSystem: {
|
||||
read: [`${workspace}/safe`, '/outside'],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect((await engine.check(call, undefined)).decision).toBe(
|
||||
PolicyDecision.ASK_USER,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle missing or empty fileSystem permissions gracefully (ALLOW)', async () => {
|
||||
const call = {
|
||||
name: 'run_shell_command',
|
||||
args: {
|
||||
command: 'ls',
|
||||
additional_permissions: {
|
||||
network: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
expect((await engine.check(call, undefined)).decision).toBe(
|
||||
PolicyDecision.ALLOW,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle non-array fileSystem paths gracefully', async () => {
|
||||
const call = {
|
||||
name: 'run_shell_command',
|
||||
args: {
|
||||
command: 'ls',
|
||||
additional_permissions: {
|
||||
fileSystem: {
|
||||
read: '/not/an/array' as never as string[],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
// It should just ignore the non-array and keep ALLOW if no other rules trigger
|
||||
expect((await engine.check(call, undefined)).decision).toBe(
|
||||
PolicyDecision.ALLOW,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
extractStringFromParseEntry,
|
||||
} from '../utils/shell-utils.js';
|
||||
import { parse as shellParse } from 'shell-quote';
|
||||
import { isSubpath } from '../utils/paths.js';
|
||||
import {
|
||||
PolicyDecision,
|
||||
type PolicyEngineConfig,
|
||||
@@ -28,6 +29,7 @@ import { debugLogger } from '../utils/debugLogger.js';
|
||||
import type { CheckerRunner } from '../safety/checker-runner.js';
|
||||
import { SafetyCheckDecision } from '../safety/protocol.js';
|
||||
import { getToolAliases } from '../tools/tool-names.js';
|
||||
import { PARAM_ADDITIONAL_PERMISSIONS } from '../tools/definitions/base-declarations.js';
|
||||
import {
|
||||
MCP_TOOL_PREFIX,
|
||||
isMcpToolAnnotation,
|
||||
@@ -38,6 +40,7 @@ import {
|
||||
import {
|
||||
type SandboxManager,
|
||||
NoopSandboxManager,
|
||||
type SandboxPermissions,
|
||||
} from '../services/sandboxManager.js';
|
||||
|
||||
function isWildcardPattern(name: string): boolean {
|
||||
@@ -647,6 +650,36 @@ export class PolicyEngine {
|
||||
}
|
||||
}
|
||||
|
||||
if (decision === PolicyDecision.ALLOW) {
|
||||
const args = toolCall.args;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const additionalPermissions = args?.[PARAM_ADDITIONAL_PERMISSIONS] as
|
||||
| SandboxPermissions
|
||||
| undefined;
|
||||
|
||||
const fsPerms = additionalPermissions?.fileSystem;
|
||||
if (fsPerms) {
|
||||
const workspace = this.sandboxManager.getWorkspace();
|
||||
const readPaths = Array.isArray(fsPerms.read) ? fsPerms.read : [];
|
||||
const writePaths = Array.isArray(fsPerms.write) ? fsPerms.write : [];
|
||||
const allPaths = [...readPaths, ...writePaths];
|
||||
|
||||
for (const p of allPaths) {
|
||||
if (
|
||||
typeof p === 'string' &&
|
||||
!isSubpath(workspace, p) &&
|
||||
workspace !== p
|
||||
) {
|
||||
debugLogger.debug(
|
||||
`[PolicyEngine.check] Additional permission path '${p}' is outside workspace '${workspace}'. Downgrading to ASK_USER.`,
|
||||
);
|
||||
decision = PolicyDecision.ASK_USER;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Safety checks
|
||||
if (decision !== PolicyDecision.DENY && this.checkerRunner) {
|
||||
for (const checkerRule of this.checkers) {
|
||||
|
||||
@@ -19,6 +19,7 @@ export const SandboxModeConfigSchema = z.object({
|
||||
readonly: z.boolean(),
|
||||
approvedTools: z.array(z.string()),
|
||||
allowOverrides: z.boolean().optional(),
|
||||
yolo: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const PersistentCommandConfigSchema = z.object({
|
||||
@@ -66,7 +67,7 @@ export class SandboxPolicyManager {
|
||||
},
|
||||
default: {
|
||||
network: false,
|
||||
readonly: true,
|
||||
readonly: false,
|
||||
approvedTools: [],
|
||||
allowOverrides: true,
|
||||
},
|
||||
@@ -132,8 +133,17 @@ export class SandboxPolicyManager {
|
||||
}
|
||||
|
||||
getModeConfig(
|
||||
mode: 'plan' | 'accepting_edits' | 'default' | string,
|
||||
mode: 'plan' | 'accepting_edits' | 'default' | 'yolo' | string,
|
||||
): SandboxModeConfig {
|
||||
if (mode === 'yolo') {
|
||||
return {
|
||||
network: true,
|
||||
readonly: false,
|
||||
approvedTools: [],
|
||||
allowOverrides: true,
|
||||
yolo: true,
|
||||
};
|
||||
}
|
||||
if (mode === 'plan') return this.config.modes.plan;
|
||||
if (mode === 'accepting_edits' || mode === 'autoEdit')
|
||||
return this.config.modes.accepting_edits;
|
||||
|
||||
Reference in New Issue
Block a user