From b1c903b847d91cbc0f0498e4c4c38694e389ad38 Mon Sep 17 00:00:00 2001 From: Chen Yuanrun Date: Thu, 19 Feb 2026 19:00:27 +0800 Subject: [PATCH] feat: add allowedUnrestricted mode for tool calling Co-Authored-By: DeepSeek-V3.2 --- core/tools/policies/fileAccess.ts | 5 ++ .../config/components/ToolPolicyItem.tsx | 11 ++-- gui/src/redux/slices/uiSlice.ts | 7 ++- .../src/evaluateTerminalCommandSecurity.ts | 17 +++++-- packages/terminal-security/src/types.ts | 3 +- .../test/terminalCommandSecurity.test.ts | 51 ++++++++++++++++++- 6 files changed, 83 insertions(+), 11 deletions(-) diff --git a/core/tools/policies/fileAccess.ts b/core/tools/policies/fileAccess.ts index 2d6801f5438..4e3e3af3332 100644 --- a/core/tools/policies/fileAccess.ts +++ b/core/tools/policies/fileAccess.ts @@ -16,6 +16,11 @@ export function evaluateFileAccessPolicy( return "disabled"; } + // If tool is in unrestricted mode, skip workspace boundary check + if (basePolicy === "allowedUnrestricted") { + return "allowedUnrestricted"; + } + // Files within workspace use the base policy (typically "allowedWithoutPermission") if (isWithinWorkspace) { return basePolicy; diff --git a/gui/src/pages/config/components/ToolPolicyItem.tsx b/gui/src/pages/config/components/ToolPolicyItem.tsx index 5d81c5908f2..98e891b2355 100644 --- a/gui/src/pages/config/components/ToolPolicyItem.tsx +++ b/gui/src/pages/config/components/ToolPolicyItem.tsx @@ -141,14 +141,19 @@ export function ToolPolicyItem(props: ToolPolicyItemProps) { {disabled || policy === "disabled" ? "Excluded" - : policy === "allowedWithoutPermission" - ? "Automatic" - : "Ask First"} + : policy === "allowedUnrestricted" + ? "Unrestricted" + : policy === "allowedWithoutPermission" + ? "Automatic" + : "Ask First"} {!disabled && ( + + Unrestricted + Automatic diff --git a/gui/src/redux/slices/uiSlice.ts b/gui/src/redux/slices/uiSlice.ts index 415f927c728..20948a311da 100644 --- a/gui/src/redux/slices/uiSlice.ts +++ b/gui/src/redux/slices/uiSlice.ts @@ -97,14 +97,17 @@ export const uiSlice = createSlice({ const setting = state.toolSettings[action.payload]; switch (setting) { - case "allowedWithPermission": + case "allowedUnrestricted": state.toolSettings[action.payload] = "allowedWithoutPermission"; break; case "allowedWithoutPermission": + state.toolSettings[action.payload] = "allowedWithPermission"; + break; + case "allowedWithPermission": state.toolSettings[action.payload] = "disabled"; break; case "disabled": - state.toolSettings[action.payload] = "allowedWithPermission"; + state.toolSettings[action.payload] = "allowedUnrestricted"; break; default: state.toolSettings[action.payload] = DEFAULT_TOOL_SETTING; diff --git a/packages/terminal-security/src/evaluateTerminalCommandSecurity.ts b/packages/terminal-security/src/evaluateTerminalCommandSecurity.ts index 693eef25f9c..7a05c942396 100644 --- a/packages/terminal-security/src/evaluateTerminalCommandSecurity.ts +++ b/packages/terminal-security/src/evaluateTerminalCommandSecurity.ts @@ -27,7 +27,7 @@ type ParsedToken = string | ShellOperator | GlobPattern | CommentToken; * * @param basePolicy The base policy configured for the tool * @param command The command string to evaluate - * @returns The security policy to apply: 'disabled', 'allowedWithPermission', or 'allowedWithoutPermission' + * @returns The security policy to apply: 'disabled', 'allowedWithPermission', 'allowedWithoutPermission', or 'allowedUnrestricted' */ export function evaluateTerminalCommandSecurity( basePolicy: ToolPolicy, @@ -38,6 +38,11 @@ export function evaluateTerminalCommandSecurity( return "disabled"; } + // If tool is in unrestricted mode, skip all security checks + if (basePolicy === "allowedUnrestricted") { + return "allowedUnrestricted"; + } + // Handle null/undefined/empty commands if (!command || typeof command !== "string") { return basePolicy; @@ -263,7 +268,10 @@ function evaluateTokens( } // Check for obfuscation patterns - if (hasObfuscationPatterns(originalCommand)) { + if ( + hasObfuscationPatterns(originalCommand) && + mostRestrictivePolicy !== "allowedUnrestricted" + ) { mostRestrictivePolicy = getMostRestrictive( mostRestrictivePolicy, "allowedWithPermission", @@ -283,7 +291,10 @@ function getMostRestrictive(...policies: ToolPolicy[]): ToolPolicy { if (policies.some((p) => p === "allowedWithPermission")) { return "allowedWithPermission"; } - return "allowedWithoutPermission"; + if (policies.some((p) => p === "allowedWithoutPermission")) { + return "allowedWithoutPermission"; + } + return "allowedUnrestricted"; } /** diff --git a/packages/terminal-security/src/types.ts b/packages/terminal-security/src/types.ts index cf3fad0c704..2a830c25cd0 100644 --- a/packages/terminal-security/src/types.ts +++ b/packages/terminal-security/src/types.ts @@ -4,4 +4,5 @@ export type ToolPolicy = | "allowedWithPermission" | "allowedWithoutPermission" - | "disabled"; + | "disabled" + | "allowedUnrestricted"; diff --git a/packages/terminal-security/test/terminalCommandSecurity.test.ts b/packages/terminal-security/test/terminalCommandSecurity.test.ts index 7d2e484765e..169ebd2a5e6 100644 --- a/packages/terminal-security/test/terminalCommandSecurity.test.ts +++ b/packages/terminal-security/test/terminalCommandSecurity.test.ts @@ -1,5 +1,5 @@ -import { describe, it, expect } from "vitest"; -import { evaluateTerminalCommandSecurity, ToolPolicy } from "../src/index.js"; +import { describe, expect, it } from "vitest"; +import { evaluateTerminalCommandSecurity } from "../src/index.js"; describe("evaluateTerminalCommandSecurity", () => { describe("Critical Risk - Always Disabled", () => { @@ -1864,4 +1864,51 @@ describe("evaluateTerminalCommandSecurity", () => { }); }); }); + + describe("Unrestricted Mode", () => { + it("should always return allowedUnrestricted when base policy is allowedUnrestricted", () => { + const result = evaluateTerminalCommandSecurity( + "allowedUnrestricted", + "rm -rf /", + ); + expect(result).toBe("allowedUnrestricted"); + }); + + it("should return allowedUnrestricted for dangerous commands in unrestricted mode", () => { + const result = evaluateTerminalCommandSecurity( + "allowedUnrestricted", + "sudo apt-get update", + ); + expect(result).toBe("allowedUnrestricted"); + }); + + it("should return allowedUnrestricted for command substitution in unrestricted mode", () => { + const result = evaluateTerminalCommandSecurity( + "allowedUnrestricted", + "echo $(rm -rf /)", + ); + expect(result).toBe("allowedUnrestricted"); + }); + + it("should return allowedUnrestricted for obfuscated commands in unrestricted mode", () => { + const result = evaluateTerminalCommandSecurity( + "allowedUnrestricted", + "echo 'cm0gLXJmIC8=' | base64 -d | sh", + ); + expect(result).toBe("allowedUnrestricted"); + }); + + it("should return allowedUnrestricted for multi-line commands in unrestricted mode", () => { + const result = evaluateTerminalCommandSecurity( + "allowedUnrestricted", + "ls\nrm -rf /", + ); + expect(result).toBe("allowedUnrestricted"); + }); + + it("should handle empty command in unrestricted mode", () => { + const result = evaluateTerminalCommandSecurity("allowedUnrestricted", ""); + expect(result).toBe("allowedUnrestricted"); + }); + }); });