Unify shell security policy and remove legacy logic (#15770)

This commit is contained in:
Abhi
2026-01-04 00:19:00 -05:00
committed by GitHub
parent f0a039f7c0
commit d3c206c677
14 changed files with 770 additions and 222 deletions

View File

@@ -124,6 +124,12 @@ vi.mock('@google/gemini-cli-core', async () => {
respectGitIgnore: true,
respectGeminiIgnore: true,
},
createPolicyEngineConfig: vi.fn(async () => ({
rules: [],
checkers: [],
defaultDecision: ServerConfig.PolicyDecision.ASK_USER,
approvalMode: ServerConfig.ApprovalMode.DEFAULT,
})),
};
});
@@ -2317,3 +2323,92 @@ describe('Telemetry configuration via environment variables', () => {
expect(config.getTelemetryLogPromptsEnabled()).toBe(false);
});
});
describe('PolicyEngine nonInteractive wiring', () => {
const originalIsTTY = process.stdin.isTTY;
beforeEach(() => {
vi.resetAllMocks();
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([]);
});
afterEach(() => {
process.stdin.isTTY = originalIsTTY;
vi.restoreAllMocks();
});
it('should set nonInteractive to true in one-shot mode', async () => {
process.stdin.isTTY = true;
process.argv = ['node', 'script.js', 'echo hello']; // Positional query makes it one-shot
const argv = await parseArguments({} as Settings);
const config = await loadCliConfig({}, 'test-session', argv);
expect(config.isInteractive()).toBe(false);
expect(
(config.getPolicyEngine() as unknown as { nonInteractive: boolean })
.nonInteractive,
).toBe(true);
});
it('should set nonInteractive to false in interactive mode', async () => {
process.stdin.isTTY = true;
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const config = await loadCliConfig({}, 'test-session', argv);
expect(config.isInteractive()).toBe(true);
expect(
(config.getPolicyEngine() as unknown as { nonInteractive: boolean })
.nonInteractive,
).toBe(false);
});
});
describe('Policy Engine Integration in loadCliConfig', () => {
beforeEach(() => {
vi.resetAllMocks();
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([]);
});
afterEach(() => {
vi.unstubAllEnvs();
vi.restoreAllMocks();
});
it('should pass merged allowed tools from CLI and settings to createPolicyEngineConfig', async () => {
process.argv = ['node', 'script.js', '--allowed-tools', 'cli-tool'];
const settings: Settings = { tools: { allowed: ['settings-tool'] } };
const argv = await parseArguments({} as Settings);
await loadCliConfig(settings, 'test-session', argv);
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
tools: expect.objectContaining({
allowed: expect.arrayContaining(['cli-tool']),
}),
}),
expect.anything(),
);
});
it('should pass merged exclude tools from CLI logic and settings to createPolicyEngineConfig', async () => {
process.stdin.isTTY = false; // Non-interactive to trigger default excludes
process.argv = ['node', 'script.js', '-p', 'test'];
const settings: Settings = { tools: { exclude: ['settings-exclude'] } };
const argv = await parseArguments({} as Settings);
await loadCliConfig(settings, 'test-session', argv);
// In non-interactive mode, ShellTool, etc. are excluded
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
tools: expect.objectContaining({
exclude: expect.arrayContaining([SHELL_TOOL_NAME]),
}),
}),
expect.anything(),
);
});
});

View File

@@ -536,20 +536,16 @@ export async function loadCliConfig(
throw err;
}
const policyEngineConfig = await createPolicyEngineConfig(
settings,
approvalMode,
);
const allowedTools = argv.allowedTools || settings.tools?.allowed || [];
const allowedToolsSet = new Set(allowedTools);
// Interactive mode: explicit -i flag or (TTY + no args + no -p flag)
const hasQuery = !!argv.query;
const interactive =
!!argv.promptInteractive ||
!!argv.experimentalAcp ||
(process.stdin.isTTY && !hasQuery && !argv.prompt);
const allowedTools = argv.allowedTools || settings.tools?.allowed || [];
const allowedToolsSet = new Set(allowedTools);
// In non-interactive mode, exclude tools that require a prompt.
const extraExcludes: string[] = [];
if (!interactive) {
@@ -589,6 +585,26 @@ export async function loadCliConfig(
extraExcludes.length > 0 ? extraExcludes : undefined,
);
// Create a settings object that includes CLI overrides for policy generation
const effectiveSettings: Settings = {
...settings,
tools: {
...settings.tools,
allowed: allowedTools,
exclude: excludeTools,
},
mcp: {
...settings.mcp,
allowed: argv.allowedMcpServerNames ?? settings.mcp?.allowed,
},
};
const policyEngineConfig = await createPolicyEngineConfig(
effectiveSettings,
approvalMode,
);
policyEngineConfig.nonInteractive = !interactive;
const defaultModel = settings.general?.previewFeatures
? PREVIEW_GEMINI_MODEL_AUTO
: DEFAULT_GEMINI_MODEL_AUTO;