mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-23 03:24:42 -07:00
feat(security): add disableAlwaysAllow setting to disable auto-approvals (#21941)
This commit is contained in:
@@ -127,6 +127,7 @@ they appear in the UI.
|
|||||||
| ------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- |
|
| ------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- |
|
||||||
| Tool Sandboxing | `security.toolSandboxing` | Experimental tool-level sandboxing (implementation in progress). | `false` |
|
| Tool Sandboxing | `security.toolSandboxing` | Experimental tool-level sandboxing (implementation in progress). | `false` |
|
||||||
| Disable YOLO Mode | `security.disableYoloMode` | Disable YOLO mode, even if enabled by a flag. | `false` |
|
| Disable YOLO Mode | `security.disableYoloMode` | Disable YOLO mode, even if enabled by a flag. | `false` |
|
||||||
|
| Disable Always Allow | `security.disableAlwaysAllow` | Disable "Always allow" options in tool confirmation dialogs. | `false` |
|
||||||
| Allow Permanent Tool Approval | `security.enablePermanentToolApproval` | Enable the "Allow for all future sessions" option in tool confirmation dialogs. | `false` |
|
| Allow Permanent Tool Approval | `security.enablePermanentToolApproval` | Enable the "Allow for all future sessions" option in tool confirmation dialogs. | `false` |
|
||||||
| Auto-add to Policy by Default | `security.autoAddToPolicyByDefault` | When enabled, the "Allow for all future sessions" option becomes the default choice for low-risk tools in trusted workspaces. | `false` |
|
| Auto-add to Policy by Default | `security.autoAddToPolicyByDefault` | When enabled, the "Allow for all future sessions" option becomes the default choice for low-risk tools in trusted workspaces. | `false` |
|
||||||
| Blocks extensions from Git | `security.blockGitExtensions` | Blocks installing and loading extensions from Git. | `false` |
|
| Blocks extensions from Git | `security.blockGitExtensions` | Blocks installing and loading extensions from Git. | `false` |
|
||||||
|
|||||||
@@ -901,6 +901,12 @@ their corresponding top-level category object in your `settings.json` file.
|
|||||||
- **Default:** `false`
|
- **Default:** `false`
|
||||||
- **Requires restart:** Yes
|
- **Requires restart:** Yes
|
||||||
|
|
||||||
|
- **`security.disableAlwaysAllow`** (boolean):
|
||||||
|
- **Description:** Disable "Always allow" options in tool confirmation
|
||||||
|
dialogs.
|
||||||
|
- **Default:** `false`
|
||||||
|
- **Requires restart:** Yes
|
||||||
|
|
||||||
- **`security.enablePermanentToolApproval`** (boolean):
|
- **`security.enablePermanentToolApproval`** (boolean):
|
||||||
- **Description:** Enable the "Allow for all future sessions" option in tool
|
- **Description:** Enable the "Allow for all future sessions" option in tool
|
||||||
confirmation dialogs.
|
confirmation dialogs.
|
||||||
@@ -1191,7 +1197,8 @@ their corresponding top-level category object in your `settings.json` file.
|
|||||||
#### `admin`
|
#### `admin`
|
||||||
|
|
||||||
- **`admin.secureModeEnabled`** (boolean):
|
- **`admin.secureModeEnabled`** (boolean):
|
||||||
- **Description:** If true, disallows yolo mode from being used.
|
- **Description:** If true, disallows YOLO mode and "Always allow" options
|
||||||
|
from being used.
|
||||||
- **Default:** `false`
|
- **Default:** `false`
|
||||||
|
|
||||||
- **`admin.extensions.enabled`** (boolean):
|
- **`admin.extensions.enabled`** (boolean):
|
||||||
|
|||||||
@@ -176,6 +176,7 @@ describe('GeminiAgent', () => {
|
|||||||
getGemini31LaunchedSync: vi.fn().mockReturnValue(false),
|
getGemini31LaunchedSync: vi.fn().mockReturnValue(false),
|
||||||
getHasAccessToPreviewModel: vi.fn().mockReturnValue(false),
|
getHasAccessToPreviewModel: vi.fn().mockReturnValue(false),
|
||||||
getCheckpointingEnabled: vi.fn().mockReturnValue(false),
|
getCheckpointingEnabled: vi.fn().mockReturnValue(false),
|
||||||
|
getDisableAlwaysAllow: vi.fn().mockReturnValue(false),
|
||||||
} as unknown as Mocked<Awaited<ReturnType<typeof loadCliConfig>>>;
|
} as unknown as Mocked<Awaited<ReturnType<typeof loadCliConfig>>>;
|
||||||
mockSettings = {
|
mockSettings = {
|
||||||
merged: {
|
merged: {
|
||||||
@@ -654,6 +655,7 @@ describe('Session', () => {
|
|||||||
getCheckpointingEnabled: vi.fn().mockReturnValue(false),
|
getCheckpointingEnabled: vi.fn().mockReturnValue(false),
|
||||||
getGitService: vi.fn().mockResolvedValue({} as GitService),
|
getGitService: vi.fn().mockResolvedValue({} as GitService),
|
||||||
waitForMcpInit: vi.fn(),
|
waitForMcpInit: vi.fn(),
|
||||||
|
getDisableAlwaysAllow: vi.fn().mockReturnValue(false),
|
||||||
} as unknown as Mocked<Config>;
|
} as unknown as Mocked<Config>;
|
||||||
mockConnection = {
|
mockConnection = {
|
||||||
sessionUpdate: vi.fn(),
|
sessionUpdate: vi.fn(),
|
||||||
@@ -947,6 +949,61 @@ describe('Session', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should exclude always allow options when disableAlwaysAllow is true', async () => {
|
||||||
|
mockConfig.getDisableAlwaysAllow = vi.fn().mockReturnValue(true);
|
||||||
|
const confirmationDetails = {
|
||||||
|
type: 'info',
|
||||||
|
onConfirm: vi.fn(),
|
||||||
|
};
|
||||||
|
mockTool.build.mockReturnValue({
|
||||||
|
getDescription: () => 'Test Tool',
|
||||||
|
toolLocations: () => [],
|
||||||
|
shouldConfirmExecute: vi.fn().mockResolvedValue(confirmationDetails),
|
||||||
|
execute: vi.fn().mockResolvedValue({ llmContent: 'Tool Result' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
mockConnection.requestPermission.mockResolvedValue({
|
||||||
|
outcome: {
|
||||||
|
outcome: 'selected',
|
||||||
|
optionId: ToolConfirmationOutcome.ProceedOnce,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const stream1 = createMockStream([
|
||||||
|
{
|
||||||
|
type: StreamEventType.CHUNK,
|
||||||
|
value: {
|
||||||
|
functionCalls: [{ name: 'test_tool', args: {} }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const stream2 = createMockStream([
|
||||||
|
{
|
||||||
|
type: StreamEventType.CHUNK,
|
||||||
|
value: { candidates: [] },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
mockChat.sendMessageStream
|
||||||
|
.mockResolvedValueOnce(stream1)
|
||||||
|
.mockResolvedValueOnce(stream2);
|
||||||
|
|
||||||
|
await session.prompt({
|
||||||
|
sessionId: 'session-1',
|
||||||
|
prompt: [{ type: 'text', text: 'Call tool' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockConnection.requestPermission).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
options: expect.not.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
optionId: ToolConfirmationOutcome.ProceedAlways,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should use filePath for ACP diff content in permission request', async () => {
|
it('should use filePath for ACP diff content in permission request', async () => {
|
||||||
const confirmationDetails = {
|
const confirmationDetails = {
|
||||||
type: 'edit',
|
type: 'edit',
|
||||||
|
|||||||
@@ -908,7 +908,7 @@ export class Session {
|
|||||||
|
|
||||||
const params: acp.RequestPermissionRequest = {
|
const params: acp.RequestPermissionRequest = {
|
||||||
sessionId: this.id,
|
sessionId: this.id,
|
||||||
options: toPermissionOptions(confirmationDetails),
|
options: toPermissionOptions(confirmationDetails, this.config),
|
||||||
toolCall: {
|
toolCall: {
|
||||||
toolCallId: callId,
|
toolCallId: callId,
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
@@ -1457,60 +1457,76 @@ const basicPermissionOptions = [
|
|||||||
|
|
||||||
function toPermissionOptions(
|
function toPermissionOptions(
|
||||||
confirmation: ToolCallConfirmationDetails,
|
confirmation: ToolCallConfirmationDetails,
|
||||||
|
config: Config,
|
||||||
): acp.PermissionOption[] {
|
): acp.PermissionOption[] {
|
||||||
switch (confirmation.type) {
|
const disableAlwaysAllow = config.getDisableAlwaysAllow();
|
||||||
case 'edit':
|
const options: acp.PermissionOption[] = [];
|
||||||
return [
|
|
||||||
{
|
if (!disableAlwaysAllow) {
|
||||||
|
switch (confirmation.type) {
|
||||||
|
case 'edit':
|
||||||
|
options.push({
|
||||||
optionId: ToolConfirmationOutcome.ProceedAlways,
|
optionId: ToolConfirmationOutcome.ProceedAlways,
|
||||||
name: 'Allow All Edits',
|
name: 'Allow All Edits',
|
||||||
kind: 'allow_always',
|
kind: 'allow_always',
|
||||||
},
|
});
|
||||||
...basicPermissionOptions,
|
break;
|
||||||
];
|
case 'exec':
|
||||||
case 'exec':
|
options.push({
|
||||||
return [
|
|
||||||
{
|
|
||||||
optionId: ToolConfirmationOutcome.ProceedAlways,
|
optionId: ToolConfirmationOutcome.ProceedAlways,
|
||||||
name: `Always Allow ${confirmation.rootCommand}`,
|
name: `Always Allow ${confirmation.rootCommand}`,
|
||||||
kind: 'allow_always',
|
kind: 'allow_always',
|
||||||
},
|
});
|
||||||
...basicPermissionOptions,
|
break;
|
||||||
];
|
case 'mcp':
|
||||||
case 'mcp':
|
options.push(
|
||||||
return [
|
{
|
||||||
{
|
optionId: ToolConfirmationOutcome.ProceedAlwaysServer,
|
||||||
optionId: ToolConfirmationOutcome.ProceedAlwaysServer,
|
name: `Always Allow ${confirmation.serverName}`,
|
||||||
name: `Always Allow ${confirmation.serverName}`,
|
kind: 'allow_always',
|
||||||
kind: 'allow_always',
|
},
|
||||||
},
|
{
|
||||||
{
|
optionId: ToolConfirmationOutcome.ProceedAlwaysTool,
|
||||||
optionId: ToolConfirmationOutcome.ProceedAlwaysTool,
|
name: `Always Allow ${confirmation.toolName}`,
|
||||||
name: `Always Allow ${confirmation.toolName}`,
|
kind: 'allow_always',
|
||||||
kind: 'allow_always',
|
},
|
||||||
},
|
);
|
||||||
...basicPermissionOptions,
|
break;
|
||||||
];
|
case 'info':
|
||||||
case 'info':
|
options.push({
|
||||||
return [
|
|
||||||
{
|
|
||||||
optionId: ToolConfirmationOutcome.ProceedAlways,
|
optionId: ToolConfirmationOutcome.ProceedAlways,
|
||||||
name: `Always Allow`,
|
name: `Always Allow`,
|
||||||
kind: 'allow_always',
|
kind: 'allow_always',
|
||||||
},
|
});
|
||||||
...basicPermissionOptions,
|
break;
|
||||||
];
|
case 'ask_user':
|
||||||
|
case 'exit_plan_mode':
|
||||||
|
// askuser and exit_plan_mode don't need "always allow" options
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// No "always allow" options for other types
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
options.push(...basicPermissionOptions);
|
||||||
|
|
||||||
|
// Exhaustive check
|
||||||
|
switch (confirmation.type) {
|
||||||
|
case 'edit':
|
||||||
|
case 'exec':
|
||||||
|
case 'mcp':
|
||||||
|
case 'info':
|
||||||
case 'ask_user':
|
case 'ask_user':
|
||||||
// askuser doesn't need "always allow" options since it's asking questions
|
|
||||||
return [...basicPermissionOptions];
|
|
||||||
case 'exit_plan_mode':
|
case 'exit_plan_mode':
|
||||||
// exit_plan_mode doesn't need "always allow" options since it's a plan approval flow
|
break;
|
||||||
return [...basicPermissionOptions];
|
|
||||||
default: {
|
default: {
|
||||||
const unreachable: never = confirmation;
|
const unreachable: never = confirmation;
|
||||||
throw new Error(`Unexpected: ${unreachable}`);
|
throw new Error(`Unexpected: ${unreachable}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -785,6 +785,9 @@ export async function loadCliConfig(
|
|||||||
approvalMode,
|
approvalMode,
|
||||||
disableYoloMode:
|
disableYoloMode:
|
||||||
settings.security?.disableYoloMode || settings.admin?.secureModeEnabled,
|
settings.security?.disableYoloMode || settings.admin?.secureModeEnabled,
|
||||||
|
disableAlwaysAllow:
|
||||||
|
settings.security?.disableAlwaysAllow ||
|
||||||
|
settings.admin?.secureModeEnabled,
|
||||||
showMemoryUsage: settings.ui?.showMemoryUsage || false,
|
showMemoryUsage: settings.ui?.showMemoryUsage || false,
|
||||||
accessibility: {
|
accessibility: {
|
||||||
...settings.ui?.accessibility,
|
...settings.ui?.accessibility,
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ export async function createPolicyEngineConfig(
|
|||||||
policyPaths: settings.policyPaths,
|
policyPaths: settings.policyPaths,
|
||||||
adminPolicyPaths: settings.adminPolicyPaths,
|
adminPolicyPaths: settings.adminPolicyPaths,
|
||||||
workspacePoliciesDir,
|
workspacePoliciesDir,
|
||||||
|
disableAlwaysAllow:
|
||||||
|
settings.security?.disableAlwaysAllow ||
|
||||||
|
settings.admin?.secureModeEnabled,
|
||||||
};
|
};
|
||||||
|
|
||||||
return createCorePolicyEngineConfig(policySettings, approvalMode);
|
return createCorePolicyEngineConfig(policySettings, approvalMode);
|
||||||
|
|||||||
@@ -524,16 +524,19 @@ describe('Settings Loading and Merging', () => {
|
|||||||
const userSettingsContent = {
|
const userSettingsContent = {
|
||||||
security: {
|
security: {
|
||||||
disableYoloMode: false,
|
disableYoloMode: false,
|
||||||
|
disableAlwaysAllow: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const workspaceSettingsContent = {
|
const workspaceSettingsContent = {
|
||||||
security: {
|
security: {
|
||||||
disableYoloMode: false, // This should be ignored
|
disableYoloMode: false, // This should be ignored
|
||||||
|
disableAlwaysAllow: false, // This should be ignored
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const systemSettingsContent = {
|
const systemSettingsContent = {
|
||||||
security: {
|
security: {
|
||||||
disableYoloMode: true,
|
disableYoloMode: true,
|
||||||
|
disableAlwaysAllow: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -551,6 +554,7 @@ describe('Settings Loading and Merging', () => {
|
|||||||
|
|
||||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||||
expect(settings.merged.security?.disableYoloMode).toBe(true); // System setting should be used
|
expect(settings.merged.security?.disableYoloMode).toBe(true); // System setting should be used
|
||||||
|
expect(settings.merged.security?.disableAlwaysAllow).toBe(true); // System setting should be used
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
|
|||||||
@@ -1541,6 +1541,16 @@ const SETTINGS_SCHEMA = {
|
|||||||
description: 'Disable YOLO mode, even if enabled by a flag.',
|
description: 'Disable YOLO mode, even if enabled by a flag.',
|
||||||
showInDialog: true,
|
showInDialog: true,
|
||||||
},
|
},
|
||||||
|
disableAlwaysAllow: {
|
||||||
|
type: 'boolean',
|
||||||
|
label: 'Disable Always Allow',
|
||||||
|
category: 'Security',
|
||||||
|
requiresRestart: true,
|
||||||
|
default: false,
|
||||||
|
description:
|
||||||
|
'Disable "Always allow" options in tool confirmation dialogs.',
|
||||||
|
showInDialog: true,
|
||||||
|
},
|
||||||
enablePermanentToolApproval: {
|
enablePermanentToolApproval: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
label: 'Allow Permanent Tool Approval',
|
label: 'Allow Permanent Tool Approval',
|
||||||
@@ -2267,7 +2277,8 @@ const SETTINGS_SCHEMA = {
|
|||||||
category: 'Admin',
|
category: 'Admin',
|
||||||
requiresRestart: false,
|
requiresRestart: false,
|
||||||
default: false,
|
default: false,
|
||||||
description: 'If true, disallows yolo mode from being used.',
|
description:
|
||||||
|
'If true, disallows YOLO mode and "Always allow" options from being used.',
|
||||||
showInDialog: false,
|
showInDialog: false,
|
||||||
mergeStrategy: MergeStrategy.REPLACE,
|
mergeStrategy: MergeStrategy.REPLACE,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
|
|||||||
getBannerTextNoCapacityIssues: vi.fn().mockResolvedValue(''),
|
getBannerTextNoCapacityIssues: vi.fn().mockResolvedValue(''),
|
||||||
getBannerTextCapacityIssues: vi.fn().mockResolvedValue(''),
|
getBannerTextCapacityIssues: vi.fn().mockResolvedValue(''),
|
||||||
isInteractiveShellEnabled: vi.fn().mockReturnValue(false),
|
isInteractiveShellEnabled: vi.fn().mockReturnValue(false),
|
||||||
|
getDisableAlwaysAllow: vi.fn().mockReturnValue(false),
|
||||||
isSkillsSupportEnabled: vi.fn().mockReturnValue(false),
|
isSkillsSupportEnabled: vi.fn().mockReturnValue(false),
|
||||||
reloadSkills: vi.fn().mockResolvedValue(undefined),
|
reloadSkills: vi.fn().mockResolvedValue(undefined),
|
||||||
reloadAgents: vi.fn().mockResolvedValue(undefined),
|
reloadAgents: vi.fn().mockResolvedValue(undefined),
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ describe('ToolConfirmationQueue', () => {
|
|||||||
const mockConfig = {
|
const mockConfig = {
|
||||||
isTrustedFolder: () => true,
|
isTrustedFolder: () => true,
|
||||||
getIdeMode: () => false,
|
getIdeMode: () => false,
|
||||||
|
getDisableAlwaysAllow: () => false,
|
||||||
getModel: () => 'gemini-pro',
|
getModel: () => 'gemini-pro',
|
||||||
getDebugMode: () => false,
|
getDebugMode: () => false,
|
||||||
getTargetDir: () => '/mock/target/dir',
|
getTargetDir: () => '/mock/target/dir',
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ describe('ToolConfirmationMessage Redirection', () => {
|
|||||||
const mockConfig = {
|
const mockConfig = {
|
||||||
isTrustedFolder: () => true,
|
isTrustedFolder: () => true,
|
||||||
getIdeMode: () => false,
|
getIdeMode: () => false,
|
||||||
|
getDisableAlwaysAllow: () => false,
|
||||||
} as unknown as Config;
|
} as unknown as Config;
|
||||||
|
|
||||||
it('should display redirection warning and tip for redirected commands', async () => {
|
it('should display redirection warning and tip for redirected commands', async () => {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ describe('ToolConfirmationMessage', () => {
|
|||||||
const mockConfig = {
|
const mockConfig = {
|
||||||
isTrustedFolder: () => true,
|
isTrustedFolder: () => true,
|
||||||
getIdeMode: () => false,
|
getIdeMode: () => false,
|
||||||
|
getDisableAlwaysAllow: () => false,
|
||||||
} as unknown as Config;
|
} as unknown as Config;
|
||||||
|
|
||||||
it('should not display urls if prompt and url are the same', async () => {
|
it('should not display urls if prompt and url are the same', async () => {
|
||||||
@@ -331,8 +332,8 @@ describe('ToolConfirmationMessage', () => {
|
|||||||
const mockConfig = {
|
const mockConfig = {
|
||||||
isTrustedFolder: () => true,
|
isTrustedFolder: () => true,
|
||||||
getIdeMode: () => false,
|
getIdeMode: () => false,
|
||||||
|
getDisableAlwaysAllow: () => false,
|
||||||
} as unknown as Config;
|
} as unknown as Config;
|
||||||
|
|
||||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||||
<ToolConfirmationMessage
|
<ToolConfirmationMessage
|
||||||
callId="test-call-id"
|
callId="test-call-id"
|
||||||
@@ -353,6 +354,7 @@ describe('ToolConfirmationMessage', () => {
|
|||||||
const mockConfig = {
|
const mockConfig = {
|
||||||
isTrustedFolder: () => false,
|
isTrustedFolder: () => false,
|
||||||
getIdeMode: () => false,
|
getIdeMode: () => false,
|
||||||
|
getDisableAlwaysAllow: () => false,
|
||||||
} as unknown as Config;
|
} as unknown as Config;
|
||||||
|
|
||||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||||
@@ -388,8 +390,8 @@ describe('ToolConfirmationMessage', () => {
|
|||||||
const mockConfig = {
|
const mockConfig = {
|
||||||
isTrustedFolder: () => true,
|
isTrustedFolder: () => true,
|
||||||
getIdeMode: () => false,
|
getIdeMode: () => false,
|
||||||
|
getDisableAlwaysAllow: () => false,
|
||||||
} as unknown as Config;
|
} as unknown as Config;
|
||||||
|
|
||||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||||
<ToolConfirmationMessage
|
<ToolConfirmationMessage
|
||||||
callId="test-call-id"
|
callId="test-call-id"
|
||||||
@@ -415,8 +417,8 @@ describe('ToolConfirmationMessage', () => {
|
|||||||
const mockConfig = {
|
const mockConfig = {
|
||||||
isTrustedFolder: () => true,
|
isTrustedFolder: () => true,
|
||||||
getIdeMode: () => false,
|
getIdeMode: () => false,
|
||||||
|
getDisableAlwaysAllow: () => false,
|
||||||
} as unknown as Config;
|
} as unknown as Config;
|
||||||
|
|
||||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||||
<ToolConfirmationMessage
|
<ToolConfirmationMessage
|
||||||
callId="test-call-id"
|
callId="test-call-id"
|
||||||
@@ -457,8 +459,8 @@ describe('ToolConfirmationMessage', () => {
|
|||||||
const mockConfig = {
|
const mockConfig = {
|
||||||
isTrustedFolder: () => true,
|
isTrustedFolder: () => true,
|
||||||
getIdeMode: () => false,
|
getIdeMode: () => false,
|
||||||
|
getDisableAlwaysAllow: () => false,
|
||||||
} as unknown as Config;
|
} as unknown as Config;
|
||||||
|
|
||||||
vi.mocked(useToolActions).mockReturnValue({
|
vi.mocked(useToolActions).mockReturnValue({
|
||||||
confirm: vi.fn(),
|
confirm: vi.fn(),
|
||||||
cancel: vi.fn(),
|
cancel: vi.fn(),
|
||||||
@@ -485,8 +487,8 @@ describe('ToolConfirmationMessage', () => {
|
|||||||
const mockConfig = {
|
const mockConfig = {
|
||||||
isTrustedFolder: () => true,
|
isTrustedFolder: () => true,
|
||||||
getIdeMode: () => true,
|
getIdeMode: () => true,
|
||||||
|
getDisableAlwaysAllow: () => false,
|
||||||
} as unknown as Config;
|
} as unknown as Config;
|
||||||
|
|
||||||
vi.mocked(useToolActions).mockReturnValue({
|
vi.mocked(useToolActions).mockReturnValue({
|
||||||
confirm: vi.fn(),
|
confirm: vi.fn(),
|
||||||
cancel: vi.fn(),
|
cancel: vi.fn(),
|
||||||
@@ -513,8 +515,8 @@ describe('ToolConfirmationMessage', () => {
|
|||||||
const mockConfig = {
|
const mockConfig = {
|
||||||
isTrustedFolder: () => true,
|
isTrustedFolder: () => true,
|
||||||
getIdeMode: () => true,
|
getIdeMode: () => true,
|
||||||
|
getDisableAlwaysAllow: () => false,
|
||||||
} as unknown as Config;
|
} as unknown as Config;
|
||||||
|
|
||||||
vi.mocked(useToolActions).mockReturnValue({
|
vi.mocked(useToolActions).mockReturnValue({
|
||||||
confirm: vi.fn(),
|
confirm: vi.fn(),
|
||||||
cancel: vi.fn(),
|
cancel: vi.fn(),
|
||||||
|
|||||||
@@ -86,12 +86,14 @@ export const ToolConfirmationMessage: React.FC<
|
|||||||
|
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
const allowPermanentApproval =
|
const allowPermanentApproval =
|
||||||
settings.merged.security.enablePermanentToolApproval;
|
settings.merged.security.enablePermanentToolApproval &&
|
||||||
|
!config.getDisableAlwaysAllow();
|
||||||
|
|
||||||
const handlesOwnUI =
|
const handlesOwnUI =
|
||||||
confirmationDetails.type === 'ask_user' ||
|
confirmationDetails.type === 'ask_user' ||
|
||||||
confirmationDetails.type === 'exit_plan_mode';
|
confirmationDetails.type === 'exit_plan_mode';
|
||||||
const isTrustedFolder = config.isTrustedFolder();
|
const isTrustedFolder =
|
||||||
|
config.isTrustedFolder() && !config.getDisableAlwaysAllow();
|
||||||
|
|
||||||
const handleConfirm = useCallback(
|
const handleConfirm = useCallback(
|
||||||
(outcome: ToolConfirmationOutcome, payload?: ToolConfirmationPayload) => {
|
(outcome: ToolConfirmationOutcome, payload?: ToolConfirmationPayload) => {
|
||||||
|
|||||||
@@ -606,6 +606,7 @@ export interface ConfigParameters {
|
|||||||
recordResponses?: string;
|
recordResponses?: string;
|
||||||
ptyInfo?: string;
|
ptyInfo?: string;
|
||||||
disableYoloMode?: boolean;
|
disableYoloMode?: boolean;
|
||||||
|
disableAlwaysAllow?: boolean;
|
||||||
rawOutput?: boolean;
|
rawOutput?: boolean;
|
||||||
acceptRawOutputRisk?: boolean;
|
acceptRawOutputRisk?: boolean;
|
||||||
modelConfigServiceConfig?: ModelConfigServiceConfig;
|
modelConfigServiceConfig?: ModelConfigServiceConfig;
|
||||||
@@ -805,6 +806,7 @@ export class Config implements McpContext, AgentLoopContext {
|
|||||||
readonly fakeResponses?: string;
|
readonly fakeResponses?: string;
|
||||||
readonly recordResponses?: string;
|
readonly recordResponses?: string;
|
||||||
private readonly disableYoloMode: boolean;
|
private readonly disableYoloMode: boolean;
|
||||||
|
private readonly disableAlwaysAllow: boolean;
|
||||||
private readonly rawOutput: boolean;
|
private readonly rawOutput: boolean;
|
||||||
private readonly acceptRawOutputRisk: boolean;
|
private readonly acceptRawOutputRisk: boolean;
|
||||||
private pendingIncludeDirectories: string[];
|
private pendingIncludeDirectories: string[];
|
||||||
@@ -1045,11 +1047,13 @@ export class Config implements McpContext, AgentLoopContext {
|
|||||||
this.policyUpdateConfirmationRequest =
|
this.policyUpdateConfirmationRequest =
|
||||||
params.policyUpdateConfirmationRequest;
|
params.policyUpdateConfirmationRequest;
|
||||||
|
|
||||||
|
this.disableAlwaysAllow = params.disableAlwaysAllow ?? false;
|
||||||
this.policyEngine = new PolicyEngine(
|
this.policyEngine = new PolicyEngine(
|
||||||
{
|
{
|
||||||
...params.policyEngineConfig,
|
...params.policyEngineConfig,
|
||||||
approvalMode:
|
approvalMode:
|
||||||
params.approvalMode ?? params.policyEngineConfig?.approvalMode,
|
params.approvalMode ?? params.policyEngineConfig?.approvalMode,
|
||||||
|
disableAlwaysAllow: this.disableAlwaysAllow,
|
||||||
},
|
},
|
||||||
checkerRunner,
|
checkerRunner,
|
||||||
);
|
);
|
||||||
@@ -2203,6 +2207,10 @@ export class Config implements McpContext, AgentLoopContext {
|
|||||||
return this.disableYoloMode || !this.isTrustedFolder();
|
return this.disableYoloMode || !this.isTrustedFolder();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getDisableAlwaysAllow(): boolean {
|
||||||
|
return this.disableAlwaysAllow;
|
||||||
|
}
|
||||||
|
|
||||||
getRawOutput(): boolean {
|
getRawOutput(): boolean {
|
||||||
return this.rawOutput;
|
return this.rawOutput;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
type PolicyRule,
|
type PolicyRule,
|
||||||
type PolicySettings,
|
type PolicySettings,
|
||||||
type SafetyCheckerRule,
|
type SafetyCheckerRule,
|
||||||
|
ALWAYS_ALLOW_PRIORITY_OFFSET,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import type { PolicyEngine } from './policy-engine.js';
|
import type { PolicyEngine } from './policy-engine.js';
|
||||||
import { loadPoliciesFromToml, type PolicyFileError } from './toml-loader.js';
|
import { loadPoliciesFromToml, type PolicyFileError } from './toml-loader.js';
|
||||||
@@ -66,19 +67,6 @@ export const WORKSPACE_POLICY_TIER = 3;
|
|||||||
export const USER_POLICY_TIER = 4;
|
export const USER_POLICY_TIER = 4;
|
||||||
export const ADMIN_POLICY_TIER = 5;
|
export const ADMIN_POLICY_TIER = 5;
|
||||||
|
|
||||||
/**
|
|
||||||
* The fractional priority of "Always allow" rules (e.g., 950/1000).
|
|
||||||
* Higher fraction within a tier wins.
|
|
||||||
*/
|
|
||||||
export const ALWAYS_ALLOW_PRIORITY_FRACTION = 950;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The fractional priority offset for "Always allow" rules (e.g., 0.95).
|
|
||||||
* This ensures consistency between in-memory rules and persisted rules.
|
|
||||||
*/
|
|
||||||
export const ALWAYS_ALLOW_PRIORITY_OFFSET =
|
|
||||||
ALWAYS_ALLOW_PRIORITY_FRACTION / 1000;
|
|
||||||
|
|
||||||
// Specific priority offsets and derived priorities for dynamic/settings rules.
|
// Specific priority offsets and derived priorities for dynamic/settings rules.
|
||||||
|
|
||||||
export const MCP_EXCLUDED_PRIORITY = USER_POLICY_TIER + 0.9;
|
export const MCP_EXCLUDED_PRIORITY = USER_POLICY_TIER + 0.9;
|
||||||
@@ -535,6 +523,7 @@ export async function createPolicyEngineConfig(
|
|||||||
checkers,
|
checkers,
|
||||||
defaultDecision: PolicyDecision.ASK_USER,
|
defaultDecision: PolicyDecision.ASK_USER,
|
||||||
approvalMode,
|
approvalMode,
|
||||||
|
disableAlwaysAllow: settings.disableAlwaysAllow,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
InProcessCheckerType,
|
InProcessCheckerType,
|
||||||
ApprovalMode,
|
ApprovalMode,
|
||||||
PRIORITY_SUBAGENT_TOOL,
|
PRIORITY_SUBAGENT_TOOL,
|
||||||
|
ALWAYS_ALLOW_PRIORITY_FRACTION,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import type { FunctionCall } from '@google/genai';
|
import type { FunctionCall } from '@google/genai';
|
||||||
import { SafetyCheckDecision } from '../safety/protocol.js';
|
import { SafetyCheckDecision } from '../safety/protocol.js';
|
||||||
@@ -3229,4 +3230,116 @@ describe('PolicyEngine', () => {
|
|||||||
expect(hookCheckers[1].priority).toBe(5);
|
expect(hookCheckers[1].priority).toBe(5);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('disableAlwaysAllow', () => {
|
||||||
|
it('should ignore "Always Allow" rules when disableAlwaysAllow is true', async () => {
|
||||||
|
const alwaysAllowRule: PolicyRule = {
|
||||||
|
toolName: 'test-tool',
|
||||||
|
decision: PolicyDecision.ALLOW,
|
||||||
|
priority: 3 + ALWAYS_ALLOW_PRIORITY_FRACTION / 1000, // 3.95
|
||||||
|
source: 'Dynamic (Confirmed)',
|
||||||
|
};
|
||||||
|
|
||||||
|
const engine = new PolicyEngine({
|
||||||
|
rules: [alwaysAllowRule],
|
||||||
|
disableAlwaysAllow: true,
|
||||||
|
defaultDecision: PolicyDecision.ASK_USER,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await engine.check(
|
||||||
|
{ name: 'test-tool', args: {} },
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
expect(result.decision).toBe(PolicyDecision.ASK_USER);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respect "Always Allow" rules when disableAlwaysAllow is false', async () => {
|
||||||
|
const alwaysAllowRule: PolicyRule = {
|
||||||
|
toolName: 'test-tool',
|
||||||
|
decision: PolicyDecision.ALLOW,
|
||||||
|
priority: 3 + ALWAYS_ALLOW_PRIORITY_FRACTION / 1000, // 3.95
|
||||||
|
source: 'Dynamic (Confirmed)',
|
||||||
|
};
|
||||||
|
|
||||||
|
const engine = new PolicyEngine({
|
||||||
|
rules: [alwaysAllowRule],
|
||||||
|
disableAlwaysAllow: false,
|
||||||
|
defaultDecision: PolicyDecision.ASK_USER,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await engine.check(
|
||||||
|
{ name: 'test-tool', args: {} },
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
expect(result.decision).toBe(PolicyDecision.ALLOW);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT ignore other rules when disableAlwaysAllow is true', async () => {
|
||||||
|
const normalRule: PolicyRule = {
|
||||||
|
toolName: 'test-tool',
|
||||||
|
decision: PolicyDecision.ALLOW,
|
||||||
|
priority: 1.5, // Not a .950 fraction
|
||||||
|
source: 'Normal Rule',
|
||||||
|
};
|
||||||
|
|
||||||
|
const engine = new PolicyEngine({
|
||||||
|
rules: [normalRule],
|
||||||
|
disableAlwaysAllow: true,
|
||||||
|
defaultDecision: PolicyDecision.ASK_USER,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await engine.check(
|
||||||
|
{ name: 'test-tool', args: {} },
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
expect(result.decision).toBe(PolicyDecision.ALLOW);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getExcludedTools with disableAlwaysAllow', () => {
|
||||||
|
it('should exclude tool if an Always Allow rule says ALLOW but disableAlwaysAllow is true (falling back to DENY)', async () => {
|
||||||
|
// To prove the ALWAYS_ALLOW rule is ignored, we set the default decision to DENY.
|
||||||
|
// If the rule was honored, the decision would be ALLOW (tool not excluded).
|
||||||
|
// Since it's ignored, it falls back to the default DENY (tool is excluded).
|
||||||
|
// In the real app, it usually falls back to ASK_USER, but ASK_USER also doesn't
|
||||||
|
// exclude the tool, so we use DENY here purely to make the test observable.
|
||||||
|
const alwaysAllowRule: PolicyRule = {
|
||||||
|
toolName: 'test-tool',
|
||||||
|
decision: PolicyDecision.ALLOW,
|
||||||
|
priority: 3 + ALWAYS_ALLOW_PRIORITY_FRACTION / 1000,
|
||||||
|
};
|
||||||
|
|
||||||
|
const engine = new PolicyEngine({
|
||||||
|
rules: [alwaysAllowRule],
|
||||||
|
disableAlwaysAllow: true,
|
||||||
|
defaultDecision: PolicyDecision.DENY,
|
||||||
|
});
|
||||||
|
|
||||||
|
const excluded = engine.getExcludedTools(
|
||||||
|
undefined,
|
||||||
|
new Set(['test-tool']),
|
||||||
|
);
|
||||||
|
expect(excluded.has('test-tool')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT exclude tool if ALWAYS_ALLOW is enabled and rule says ALLOW', async () => {
|
||||||
|
const alwaysAllowRule: PolicyRule = {
|
||||||
|
toolName: 'test-tool',
|
||||||
|
decision: PolicyDecision.ALLOW,
|
||||||
|
priority: 3 + ALWAYS_ALLOW_PRIORITY_FRACTION / 1000,
|
||||||
|
};
|
||||||
|
|
||||||
|
const engine = new PolicyEngine({
|
||||||
|
rules: [alwaysAllowRule],
|
||||||
|
disableAlwaysAllow: false,
|
||||||
|
defaultDecision: PolicyDecision.DENY,
|
||||||
|
});
|
||||||
|
|
||||||
|
const excluded = engine.getExcludedTools(
|
||||||
|
undefined,
|
||||||
|
new Set(['test-tool']),
|
||||||
|
);
|
||||||
|
expect(excluded.has('test-tool')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
type HookCheckerRule,
|
type HookCheckerRule,
|
||||||
ApprovalMode,
|
ApprovalMode,
|
||||||
type CheckResult,
|
type CheckResult,
|
||||||
|
ALWAYS_ALLOW_PRIORITY_FRACTION,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import { stableStringify } from './stable-stringify.js';
|
import { stableStringify } from './stable-stringify.js';
|
||||||
import { debugLogger } from '../utils/debugLogger.js';
|
import { debugLogger } from '../utils/debugLogger.js';
|
||||||
@@ -154,6 +155,7 @@ export class PolicyEngine {
|
|||||||
private hookCheckers: HookCheckerRule[];
|
private hookCheckers: HookCheckerRule[];
|
||||||
private readonly defaultDecision: PolicyDecision;
|
private readonly defaultDecision: PolicyDecision;
|
||||||
private readonly nonInteractive: boolean;
|
private readonly nonInteractive: boolean;
|
||||||
|
private readonly disableAlwaysAllow: boolean;
|
||||||
private readonly checkerRunner?: CheckerRunner;
|
private readonly checkerRunner?: CheckerRunner;
|
||||||
private approvalMode: ApprovalMode;
|
private approvalMode: ApprovalMode;
|
||||||
|
|
||||||
@@ -169,6 +171,7 @@ export class PolicyEngine {
|
|||||||
);
|
);
|
||||||
this.defaultDecision = config.defaultDecision ?? PolicyDecision.ASK_USER;
|
this.defaultDecision = config.defaultDecision ?? PolicyDecision.ASK_USER;
|
||||||
this.nonInteractive = config.nonInteractive ?? false;
|
this.nonInteractive = config.nonInteractive ?? false;
|
||||||
|
this.disableAlwaysAllow = config.disableAlwaysAllow ?? false;
|
||||||
this.checkerRunner = checkerRunner;
|
this.checkerRunner = checkerRunner;
|
||||||
this.approvalMode = config.approvalMode ?? ApprovalMode.DEFAULT;
|
this.approvalMode = config.approvalMode ?? ApprovalMode.DEFAULT;
|
||||||
}
|
}
|
||||||
@@ -187,6 +190,13 @@ export class PolicyEngine {
|
|||||||
return this.approvalMode;
|
return this.approvalMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isAlwaysAllowRule(rule: PolicyRule): boolean {
|
||||||
|
return (
|
||||||
|
rule.priority !== undefined &&
|
||||||
|
Math.round((rule.priority % 1) * 1000) === ALWAYS_ALLOW_PRIORITY_FRACTION
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private shouldDowngradeForRedirection(
|
private shouldDowngradeForRedirection(
|
||||||
command: string,
|
command: string,
|
||||||
allowRedirection?: boolean,
|
allowRedirection?: boolean,
|
||||||
@@ -422,6 +432,10 @@ export class PolicyEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const rule of this.rules) {
|
for (const rule of this.rules) {
|
||||||
|
if (this.disableAlwaysAllow && this.isAlwaysAllowRule(rule)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const match = toolCallsToTry.some((tc) =>
|
const match = toolCallsToTry.some((tc) =>
|
||||||
ruleMatches(
|
ruleMatches(
|
||||||
rule,
|
rule,
|
||||||
@@ -684,6 +698,10 @@ export class PolicyEngine {
|
|||||||
|
|
||||||
// Evaluate rules in priority order (they are already sorted in constructor)
|
// Evaluate rules in priority order (they are already sorted in constructor)
|
||||||
for (const rule of this.rules) {
|
for (const rule of this.rules) {
|
||||||
|
if (this.disableAlwaysAllow && this.isAlwaysAllowRule(rule)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Create a copy of the rule without argsPattern to see if it targets the tool
|
// Create a copy of the rule without argsPattern to see if it targets the tool
|
||||||
// regardless of the runtime arguments it might receive.
|
// regardless of the runtime arguments it might receive.
|
||||||
const ruleWithoutArgs: PolicyRule = { ...rule, argsPattern: undefined };
|
const ruleWithoutArgs: PolicyRule = { ...rule, argsPattern: undefined };
|
||||||
|
|||||||
@@ -285,6 +285,11 @@ export interface PolicyEngineConfig {
|
|||||||
*/
|
*/
|
||||||
nonInteractive?: boolean;
|
nonInteractive?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to ignore "Always Allow" rules.
|
||||||
|
*/
|
||||||
|
disableAlwaysAllow?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to allow hooks to execute.
|
* Whether to allow hooks to execute.
|
||||||
* When false, all hooks are denied.
|
* When false, all hooks are denied.
|
||||||
@@ -314,6 +319,7 @@ export interface PolicySettings {
|
|||||||
// Admin provided policies that will supplement the ADMIN level policies
|
// Admin provided policies that will supplement the ADMIN level policies
|
||||||
adminPolicyPaths?: string[];
|
adminPolicyPaths?: string[];
|
||||||
workspacePoliciesDir?: string;
|
workspacePoliciesDir?: string;
|
||||||
|
disableAlwaysAllow?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CheckResult {
|
export interface CheckResult {
|
||||||
@@ -326,3 +332,16 @@ export interface CheckResult {
|
|||||||
* Effective priority matching Tier 1 (Default) read-only tools.
|
* Effective priority matching Tier 1 (Default) read-only tools.
|
||||||
*/
|
*/
|
||||||
export const PRIORITY_SUBAGENT_TOOL = 1.05;
|
export const PRIORITY_SUBAGENT_TOOL = 1.05;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The fractional priority of "Always allow" rules (e.g., 950/1000).
|
||||||
|
* Higher fraction within a tier wins.
|
||||||
|
*/
|
||||||
|
export const ALWAYS_ALLOW_PRIORITY_FRACTION = 950;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The fractional priority offset for "Always allow" rules (e.g., 0.95).
|
||||||
|
* This ensures consistency between in-memory rules and persisted rules.
|
||||||
|
*/
|
||||||
|
export const ALWAYS_ALLOW_PRIORITY_OFFSET =
|
||||||
|
ALWAYS_ALLOW_PRIORITY_FRACTION / 1000;
|
||||||
|
|||||||
@@ -102,6 +102,32 @@ describe('policy.ts', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should respect disableAlwaysAllow from config', async () => {
|
||||||
|
const mockPolicyEngine = {
|
||||||
|
check: vi.fn().mockResolvedValue({ decision: PolicyDecision.ALLOW }),
|
||||||
|
} as unknown as Mocked<PolicyEngine>;
|
||||||
|
|
||||||
|
const mockConfig = {
|
||||||
|
getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine),
|
||||||
|
getDisableAlwaysAllow: vi.fn().mockReturnValue(true),
|
||||||
|
} as unknown as Mocked<Config>;
|
||||||
|
|
||||||
|
(mockConfig as unknown as { config: Config }).config =
|
||||||
|
mockConfig as Config;
|
||||||
|
|
||||||
|
const toolCall = {
|
||||||
|
request: { name: 'test-tool', args: {} },
|
||||||
|
tool: { name: 'test-tool' },
|
||||||
|
} as ValidatingToolCall;
|
||||||
|
|
||||||
|
// Note: checkPolicy calls config.getPolicyEngine().check()
|
||||||
|
// The PolicyEngine itself is already configured with disableAlwaysAllow
|
||||||
|
// when created in Config. Here we are just verifying that checkPolicy
|
||||||
|
// doesn't somehow bypass it.
|
||||||
|
await checkPolicy(toolCall, mockConfig);
|
||||||
|
expect(mockPolicyEngine.check).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('should throw if ASK_USER is returned in non-interactive mode', async () => {
|
it('should throw if ASK_USER is returned in non-interactive mode', async () => {
|
||||||
const mockPolicyEngine = {
|
const mockPolicyEngine = {
|
||||||
check: vi.fn().mockResolvedValue({ decision: PolicyDecision.ASK_USER }),
|
check: vi.fn().mockResolvedValue({ decision: PolicyDecision.ASK_USER }),
|
||||||
|
|||||||
@@ -1495,6 +1495,13 @@
|
|||||||
"default": false,
|
"default": false,
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"disableAlwaysAllow": {
|
||||||
|
"title": "Disable Always Allow",
|
||||||
|
"description": "Disable \"Always allow\" options in tool confirmation dialogs.",
|
||||||
|
"markdownDescription": "Disable \"Always allow\" options in tool confirmation dialogs.\n\n- Category: `Security`\n- Requires restart: `yes`\n- Default: `false`",
|
||||||
|
"default": false,
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"enablePermanentToolApproval": {
|
"enablePermanentToolApproval": {
|
||||||
"title": "Allow Permanent Tool Approval",
|
"title": "Allow Permanent Tool Approval",
|
||||||
"description": "Enable the \"Allow for all future sessions\" option in tool confirmation dialogs.",
|
"description": "Enable the \"Allow for all future sessions\" option in tool confirmation dialogs.",
|
||||||
@@ -2027,8 +2034,8 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"secureModeEnabled": {
|
"secureModeEnabled": {
|
||||||
"title": "Secure Mode Enabled",
|
"title": "Secure Mode Enabled",
|
||||||
"description": "If true, disallows yolo mode from being used.",
|
"description": "If true, disallows YOLO mode and \"Always allow\" options from being used.",
|
||||||
"markdownDescription": "If true, disallows yolo mode from being used.\n\n- Category: `Admin`\n- Requires restart: `no`\n- Default: `false`",
|
"markdownDescription": "If true, disallows YOLO mode and \"Always allow\" options from being used.\n\n- Category: `Admin`\n- Requires restart: `no`\n- Default: `false`",
|
||||||
"default": false,
|
"default": false,
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user