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
32 changes: 17 additions & 15 deletions extensions/cli/src/stream/streamChatResponse.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ import { PreprocessedToolCall, ToolCall } from "../tools/types.js";
import { logger } from "../util/logger.js";

import { StreamCallbacks } from "./streamChatResponse.types.js";
import {
getPermissionDeniedMessage,
resolveUserPermissionResult,
type ToolPermissionApprovalResult,
type ToolPermissionDenialReason,
} from "./toolPermissionResult.js";

export interface ToolResultWithStatus extends ChatCompletionToolMessageParam {
status: ToolStatus;
Expand All @@ -38,12 +44,9 @@ export function handlePermissionDenied(
toolCall: PreprocessedToolCall,
chatHistoryEntries: ChatCompletionToolMessageParam[],
callbacks?: StreamCallbacks,
reason: "user" | "policy" = "user",
reason: ToolPermissionDenialReason = "user",
): void {
const deniedMessage =
reason === "policy"
? `Command blocked by security policy`
: `Permission denied by user`;
const deniedMessage = getPermissionDeniedMessage(reason);

logger.info("Tool call denied", {
name: toolCall.name,
Expand All @@ -65,9 +68,13 @@ export function handlePermissionDenied(
export async function requestUserPermission(
toolCall: PreprocessedToolCall,
callbacks?: StreamCallbacks,
): Promise<boolean> {
): Promise<boolean | undefined> {
if (!callbacks?.onToolPermissionRequest) {
return false;
logger.error("Cannot request interactive tool permission", {
name: toolCall.name,
reason: "onToolPermissionRequest callback is missing",
});
return undefined;
}

const toolCallRequest: ToolCallRequest = {
Expand Down Expand Up @@ -115,7 +122,7 @@ export async function checkToolPermissionApproval(
toolCall: PreprocessedToolCall,
callbacks?: StreamCallbacks,
isHeadless?: boolean,
): Promise<{ approved: boolean; denialReason?: "user" | "policy" }> {
): Promise<ToolPermissionApprovalResult> {
const permissionCheck = checkToolPermission(toolCall, permissions);

if (permissionCheck.permission === "allow") {
Expand All @@ -126,9 +133,7 @@ export async function checkToolPermissionApproval(
return { approved: false, denialReason: "policy" };
}
const userApproved = await requestUserPermission(toolCall, callbacks);
return userApproved
? { approved: true }
: { approved: false, denialReason: "user" };
return resolveUserPermissionResult(userApproved);
} else if (permissionCheck.permission === "exclude") {
// Tool blocked by security policy
return { approved: false, denialReason: "policy" };
Expand Down Expand Up @@ -520,10 +525,7 @@ export async function executeStreamedToolCalls(
if (!permissionResult.approved) {
// Permission denied: create entry with canceled status
const denialReason = permissionResult.denialReason || "user";
const deniedMessage =
denialReason === "policy"
? `Command blocked by security policy`
: `Permission denied by user`;
const deniedMessage = getPermissionDeniedMessage(denialReason);

const deniedEntry: ToolResultWithStatus = {
role: "tool",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { describe, expect, it } from "vitest";

import {
getPermissionDeniedMessage,
resolveUserPermissionResult,
} from "./toolPermissionResult.js";

describe("tool permission result", () => {
it("reports a missing interactive callback instead of a user denial", () => {
const result = resolveUserPermissionResult(undefined);

expect(result).toEqual({
approved: false,
denialReason: "callback_missing",
});
expect(getPermissionDeniedMessage(result.denialReason!)).toBe(
"Interactive permission prompt unavailable",
);
});
});
29 changes: 29 additions & 0 deletions extensions/cli/src/stream/toolPermissionResult.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export type ToolPermissionDenialReason = "user" | "policy" | "callback_missing";

export interface ToolPermissionApprovalResult {
approved: boolean;
denialReason?: ToolPermissionDenialReason;
}

export function resolveUserPermissionResult(
userApproved: boolean | undefined,
): ToolPermissionApprovalResult {
if (userApproved === undefined) {
return { approved: false, denialReason: "callback_missing" };
}
return userApproved
? { approved: true }
: { approved: false, denialReason: "user" };
}

export function getPermissionDeniedMessage(
reason: ToolPermissionDenialReason,
): string {
if (reason === "policy") {
return "Command blocked by security policy";
}
if (reason === "callback_missing") {
return "Interactive permission prompt unavailable";
}
return "Permission denied by user";
}
Loading