Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions apps/mesh/src/api/routes/decopilot/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,4 +380,73 @@ describe("toolNeedsApproval", () => {
expect(toolNeedsApproval(level, undefined)).toBe(true);
});
});

describe("destructiveHint always requires approval", () => {
test("returns true even when level is auto", () => {
expect(toolNeedsApproval("auto", false, { destructiveHint: true })).toBe(
true,
);
});

test("returns true even when readOnlyHint is true", () => {
expect(toolNeedsApproval("auto", true, { destructiveHint: true })).toBe(
true,
);
});

test("returns true for readonly level", () => {
expect(
toolNeedsApproval("readonly", false, { destructiveHint: true }),
).toBe(true);
});

test("does not affect non-destructive tools", () => {
expect(toolNeedsApproval("auto", false, { destructiveHint: false })).toBe(
false,
);
});

test("does not affect when destructiveHint is undefined", () => {
expect(
toolNeedsApproval("auto", false, { destructiveHint: undefined }),
).toBe(false);
});

test("plan mode hard-block takes precedence over destructiveHint for non-readOnly tools", () => {
expect(
toolNeedsApproval("auto", false, {
isPlanMode: true,
destructiveHint: true,
}),
).toBe("hard-block");
});
});

describe('approval level: "trust-all"', () => {
const level: ToolApprovalLevel = "trust-all";

test("returns false for read-only tools", () => {
expect(toolNeedsApproval(level, true)).toBe(false);
});

test("returns false for non-read-only tools", () => {
expect(toolNeedsApproval(level, false)).toBe(false);
});

test("returns false even for destructive tools", () => {
expect(toolNeedsApproval(level, false, { destructiveHint: true })).toBe(
false,
);
});

test("plan mode still hard-blocks non-read-only tools", () => {
expect(toolNeedsApproval(level, false, { isPlanMode: true })).toBe(
"hard-block",
);
});

test("plan mode allows read-only tools", () => {
expect(toolNeedsApproval(level, true, { isPlanMode: true })).toBe(false);
});
});
});
12 changes: 9 additions & 3 deletions apps/mesh/src/api/routes/decopilot/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,25 +31,30 @@ import {
/**
* Tool approval levels determine which tools require user approval before executing
*/
export type ToolApprovalLevel = "auto" | "readonly";
export type ToolApprovalLevel = "auto" | "readonly" | "trust-all";

/**
* Determine if a tool needs approval based on approval level and readOnlyHint
* Determine if a tool needs approval based on approval level and annotations
*
* @param level - The approval level setting
* @param readOnlyHint - Optional hint from MCP tool annotations
* @param options.isPlanMode - When true (chat `mode: "plan"`), non-read-only tools are hard-blocked
* @param options.destructiveHint - When true, the tool always requires approval regardless of level
* @returns true if the tool requires approval, false if auto-approved
*/
export function toolNeedsApproval(
level: ToolApprovalLevel,
readOnlyHint?: boolean,
options?: { isPlanMode?: boolean },
options?: { isPlanMode?: boolean; destructiveHint?: boolean },
): boolean | "hard-block" {
if (options?.isPlanMode) {
if (readOnlyHint === true) return false;
return "hard-block";
}
// "trust-all": auto-approve everything, including destructive tools
if (level === "trust-all") return false;
// Destructive tools always require approval (unless yolo)
if (options?.destructiveHint === true) return true;
if (level === "auto") return false;
// "readonly": auto-approve only if explicitly marked readOnly
return readOnlyHint !== true;
Expand Down Expand Up @@ -165,6 +170,7 @@ export async function toolsFromMCP(
needsApproval:
toolNeedsApproval(toolApprovalLevel, annotations?.readOnlyHint, {
isPlanMode: options?.isPlanMode,
destructiveHint: annotations?.destructiveHint,
}) !== false,
execute: async (input, callOptions) => {
const startTime = performance.now();
Expand Down
3 changes: 2 additions & 1 deletion apps/mesh/src/web/components/chat/highlight/approval.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const APPROVAL_LEVEL_OPTIONS: {
}[] = [
{ value: "readonly", label: "Ask before edit" },
{ value: "auto", label: "Auto approve" },
{ value: "trust-all", label: "Trust all" },
];

// ============================================================================
Expand All @@ -55,7 +56,7 @@ function ApprovalLevelSelect({ onYolo }: { onYolo: () => void }) {
const handleLevelChange = (value: string) => {
const newLevel = value as ToolApprovalLevel;
setPreferences({ ...preferences, toolApprovalLevel: newLevel });
if (newLevel === "auto") {
if (newLevel === "auto" || newLevel === "trust-all") {
onYolo();
}
};
Expand Down
8 changes: 6 additions & 2 deletions apps/mesh/src/web/hooks/use-preferences.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useLocalStorage } from "./use-local-storage.ts";
import { LOCALSTORAGE_KEYS } from "@/web/lib/localstorage-keys.ts";

export type ToolApprovalLevel = "auto" | "readonly";
export type ToolApprovalLevel = "auto" | "readonly" | "trust-all";
export type ThemeMode = "light" | "dark" | "system";
interface Preferences {
toolApprovalLevel: ToolApprovalLevel;
Expand All @@ -19,7 +19,11 @@ const DEFAULT_PREFERENCES: Preferences = {
experimental_vibecode: false,
};

const VALID_TOOL_APPROVAL_LEVELS: ToolApprovalLevel[] = ["auto", "readonly"];
const VALID_TOOL_APPROVAL_LEVELS: ToolApprovalLevel[] = [
"auto",
"readonly",
"trust-all",
];

const VALID_THEME_MODES: ThemeMode[] = ["light", "dark", "system"];

Expand Down
9 changes: 9 additions & 0 deletions apps/mesh/src/web/views/settings/profile-preferences.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ function PreferencesSection() {
{{
readonly: "Ask before edit",
auto: "Auto approve",
"trust-all": "Trust all",
}[preferences.toolApprovalLevel] ?? "Ask before edit"}
</span>
</SelectTrigger>
Expand All @@ -259,6 +260,14 @@ function PreferencesSection() {
<SelectItem value="auto" textValue="Auto approve">
<div className="flex flex-col gap-0.5">
<span className="font-medium">Auto approve</span>
<span className="text-xs text-muted-foreground">
Ask before destructive tools
</span>
</div>
</SelectItem>
<SelectItem value="trust-all" textValue="Trust all">
<div className="flex flex-col gap-0.5">
<span className="font-medium">Trust all</span>
<span className="text-xs text-muted-foreground">
Execute all without approval
</span>
Expand Down
Loading