Desktop alerts when suspicious unicode characters found in Recipe (#4080)

This commit is contained in:
Amed Rodriguez
2025-08-19 14:05:46 -07:00
committed by GitHub
parent 33bf5a44ed
commit 4f4e8ace33
11 changed files with 256 additions and 15 deletions
+52
View File
@@ -780,6 +780,36 @@
}
}
},
"/recipes/scan": {
"post": {
"tags": [
"Recipe Management"
],
"operationId": "scan_recipe",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ScanRecipeRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Recipe scanned successfully",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ScanRecipeResponse"
}
}
}
}
}
}
},
"/schedule/create": {
"post": {
"tags": [
@@ -2795,6 +2825,28 @@
}
}
},
"ScanRecipeRequest": {
"type": "object",
"required": [
"recipe"
],
"properties": {
"recipe": {
"$ref": "#/components/schemas/Recipe"
}
}
},
"ScanRecipeResponse": {
"type": "object",
"required": [
"has_security_warnings"
],
"properties": {
"has_security_warnings": {
"type": "boolean"
}
}
},
"ScheduledJob": {
"type": "object",
"required": [
+12 -1
View File
@@ -1,7 +1,7 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { Options as ClientOptions, TDataShape, Client } from './client';
import type { AddSubRecipesData, AddSubRecipesResponses, AddSubRecipesErrors, ExtendPromptData, ExtendPromptResponses, ExtendPromptErrors, UpdateSessionConfigData, UpdateSessionConfigResponses, UpdateSessionConfigErrors, GetToolsData, GetToolsResponses, GetToolsErrors, UpdateAgentProviderData, UpdateAgentProviderResponses, UpdateAgentProviderErrors, UpdateRouterToolSelectorData, UpdateRouterToolSelectorResponses, UpdateRouterToolSelectorErrors, ReadAllConfigData, ReadAllConfigResponses, BackupConfigData, BackupConfigResponses, BackupConfigErrors, GetExtensionsData, GetExtensionsResponses, GetExtensionsErrors, AddExtensionData, AddExtensionResponses, AddExtensionErrors, RemoveExtensionData, RemoveExtensionResponses, RemoveExtensionErrors, InitConfigData, InitConfigResponses, InitConfigErrors, UpsertPermissionsData, UpsertPermissionsResponses, UpsertPermissionsErrors, ProvidersData, ProvidersResponses, ReadConfigData, ReadConfigResponses, ReadConfigErrors, RecoverConfigData, RecoverConfigResponses, RecoverConfigErrors, RemoveConfigData, RemoveConfigResponses, RemoveConfigErrors, UpsertConfigData, UpsertConfigResponses, UpsertConfigErrors, ValidateConfigData, ValidateConfigResponses, ValidateConfigErrors, ConfirmPermissionData, ConfirmPermissionResponses, ConfirmPermissionErrors, ManageContextData, ManageContextResponses, ManageContextErrors, CreateRecipeData, CreateRecipeResponses, CreateRecipeErrors, DecodeRecipeData, DecodeRecipeResponses, DecodeRecipeErrors, EncodeRecipeData, EncodeRecipeResponses, EncodeRecipeErrors, CreateScheduleData, CreateScheduleResponses, CreateScheduleErrors, DeleteScheduleData, DeleteScheduleResponses, DeleteScheduleErrors, ListSchedulesData, ListSchedulesResponses, ListSchedulesErrors, UpdateScheduleData, UpdateScheduleResponses, UpdateScheduleErrors, InspectRunningJobData, InspectRunningJobResponses, InspectRunningJobErrors, KillRunningJobData, KillRunningJobResponses, PauseScheduleData, PauseScheduleResponses, PauseScheduleErrors, RunNowHandlerData, RunNowHandlerResponses, RunNowHandlerErrors, SessionsHandlerData, SessionsHandlerResponses, SessionsHandlerErrors, UnpauseScheduleData, UnpauseScheduleResponses, UnpauseScheduleErrors, ListSessionsData, ListSessionsResponses, ListSessionsErrors, GetSessionHistoryData, GetSessionHistoryResponses, GetSessionHistoryErrors } from './types.gen';
import type { AddSubRecipesData, AddSubRecipesResponses, AddSubRecipesErrors, ExtendPromptData, ExtendPromptResponses, ExtendPromptErrors, UpdateSessionConfigData, UpdateSessionConfigResponses, UpdateSessionConfigErrors, GetToolsData, GetToolsResponses, GetToolsErrors, UpdateAgentProviderData, UpdateAgentProviderResponses, UpdateAgentProviderErrors, UpdateRouterToolSelectorData, UpdateRouterToolSelectorResponses, UpdateRouterToolSelectorErrors, ReadAllConfigData, ReadAllConfigResponses, BackupConfigData, BackupConfigResponses, BackupConfigErrors, GetExtensionsData, GetExtensionsResponses, GetExtensionsErrors, AddExtensionData, AddExtensionResponses, AddExtensionErrors, RemoveExtensionData, RemoveExtensionResponses, RemoveExtensionErrors, InitConfigData, InitConfigResponses, InitConfigErrors, UpsertPermissionsData, UpsertPermissionsResponses, UpsertPermissionsErrors, ProvidersData, ProvidersResponses, ReadConfigData, ReadConfigResponses, ReadConfigErrors, RecoverConfigData, RecoverConfigResponses, RecoverConfigErrors, RemoveConfigData, RemoveConfigResponses, RemoveConfigErrors, UpsertConfigData, UpsertConfigResponses, UpsertConfigErrors, ValidateConfigData, ValidateConfigResponses, ValidateConfigErrors, ConfirmPermissionData, ConfirmPermissionResponses, ConfirmPermissionErrors, ManageContextData, ManageContextResponses, ManageContextErrors, CreateRecipeData, CreateRecipeResponses, CreateRecipeErrors, DecodeRecipeData, DecodeRecipeResponses, DecodeRecipeErrors, EncodeRecipeData, EncodeRecipeResponses, EncodeRecipeErrors, ScanRecipeData, ScanRecipeResponses, CreateScheduleData, CreateScheduleResponses, CreateScheduleErrors, DeleteScheduleData, DeleteScheduleResponses, DeleteScheduleErrors, ListSchedulesData, ListSchedulesResponses, ListSchedulesErrors, UpdateScheduleData, UpdateScheduleResponses, UpdateScheduleErrors, InspectRunningJobData, InspectRunningJobResponses, InspectRunningJobErrors, KillRunningJobData, KillRunningJobResponses, PauseScheduleData, PauseScheduleResponses, PauseScheduleErrors, RunNowHandlerData, RunNowHandlerResponses, RunNowHandlerErrors, SessionsHandlerData, SessionsHandlerResponses, SessionsHandlerErrors, UnpauseScheduleData, UnpauseScheduleResponses, UnpauseScheduleErrors, ListSessionsData, ListSessionsResponses, ListSessionsErrors, GetSessionHistoryData, GetSessionHistoryResponses, GetSessionHistoryErrors } from './types.gen';
import { client as _heyApiClient } from './client.gen';
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = ClientOptions<TData, ThrowOnError> & {
@@ -245,6 +245,17 @@ export const encodeRecipe = <ThrowOnError extends boolean = false>(options: Opti
});
};
export const scanRecipe = <ThrowOnError extends boolean = false>(options: Options<ScanRecipeData, ThrowOnError>) => {
return (options.client ?? _heyApiClient).post<ScanRecipeResponses, unknown, ThrowOnError>({
url: '/recipes/scan',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
};
export const createSchedule = <ThrowOnError extends boolean = false>(options: Options<CreateScheduleData, ThrowOnError>) => {
return (options.client ?? _heyApiClient).post<CreateScheduleResponses, CreateScheduleErrors, ThrowOnError>({
url: '/schedule/create',
+24
View File
@@ -588,6 +588,14 @@ export type RunNowResponse = {
session_id: string;
};
export type ScanRecipeRequest = {
recipe: Recipe;
};
export type ScanRecipeResponse = {
has_security_warnings: boolean;
};
export type ScheduledJob = {
cron: string;
current_session_id?: string | null;
@@ -1434,6 +1442,22 @@ export type EncodeRecipeResponses = {
export type EncodeRecipeResponse2 = EncodeRecipeResponses[keyof EncodeRecipeResponses];
export type ScanRecipeData = {
body: ScanRecipeRequest;
path?: never;
query?: never;
url: '/recipes/scan';
};
export type ScanRecipeResponses = {
/**
* Recipe scanned successfully
*/
200: ScanRecipeResponse;
};
export type ScanRecipeResponse2 = ScanRecipeResponses[keyof ScanRecipeResponses];
export type CreateScheduleData = {
body: CreateScheduleRequest;
path?: never;
+2
View File
@@ -200,6 +200,7 @@ function BaseChatContent({
recipeAccepted,
handleRecipeAccept,
handleRecipeCancel,
hasSecurityWarnings,
} = useRecipeManager(messages, location.state);
// Reset recipe usage tracking when recipe changes
@@ -573,6 +574,7 @@ function BaseChatContent({
description: recipeConfig?.description,
instructions: recipeConfig?.instructions || undefined,
}}
hasSecurityWarnings={hasSecurityWarnings}
/>
{/* Recipe Parameter Modal */}
@@ -18,6 +18,7 @@ interface RecipeWarningModalProps {
description?: string;
instructions?: string;
};
hasSecurityWarnings?: boolean;
}
export function RecipeWarningModal({
@@ -25,18 +26,39 @@ export function RecipeWarningModal({
onConfirm,
onCancel,
recipeDetails,
hasSecurityWarnings = false,
}: RecipeWarningModalProps) {
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && onCancel()}>
<DialogContent className="sm:max-w-[80vw] max-h-[80vh] flex flex-col p-0">
<DialogHeader className="flex-shrink-0 p-6 pb-0">
<DialogTitle> New Recipe Warning</DialogTitle>
<DialogTitle>
{hasSecurityWarnings ? '⚠️ Security Warning' : '⚠️ New Recipe Warning'}
</DialogTitle>
<DialogDescription>
You are about to execute a recipe that you haven't run before. Only proceed if you trust
the source of this recipe.
{!hasSecurityWarnings &&
"You are about to execute a recipe that you haven't run before. "}
Only proceed if you trust the source of this recipe.
</DialogDescription>
</DialogHeader>
{hasSecurityWarnings && (
<div className="px-6">
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4">
<div className="flex items-start">
<div className="ml-3">
<div className="mt-2 text-sm text-yellow-700 dark:text-yellow-300">
<p>
This recipe contains hidden characters that will be ignored for your safety,
as they could be used for malicious purposes.
</p>
</div>
</div>
</div>
</div>
</div>
)}
<div className="flex-1 overflow-y-auto p-6 pt-4">
<div className="bg-background-muted p-4 rounded-lg">
<h3 className="font-medium mb-3 text-text-standard">Recipe Preview:</h3>
+10 -5
View File
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState, useRef } from 'react';
import { createRecipe, Recipe } from '../recipe';
import { createRecipe, Recipe, scanRecipe } from '../recipe';
import { Message, createUserMessage } from '../types/message';
import { updateSystemPromptWithParameters } from '../utils/providerUtils';
import { useChatContext } from '../contexts/ChatContext';
@@ -17,6 +17,7 @@ export const useRecipeManager = (messages: Message[], locationState?: LocationSt
const [recipeError, setRecipeError] = useState<string | null>(null);
const [isRecipeWarningModalOpen, setIsRecipeWarningModalOpen] = useState(false);
const [recipeAccepted, setRecipeAccepted] = useState(false);
const [hasSecurityWarnings, setHasSecurityWarnings] = useState(false);
// Get chat context to access persisted recipe and parameters
const chatContext = useChatContext();
@@ -78,20 +79,23 @@ export const useRecipeManager = (messages: Message[], locationState?: LocationSt
}
}, [chatContext, locationState]);
// Check if recipe has been accepted before
// Check if recipe has been accepted before and scan for security warnings
useEffect(() => {
const checkRecipeAcceptance = async () => {
if (recipeConfig) {
try {
const hasAccepted = await window.electron.hasAcceptedRecipeBefore(recipeConfig);
if (!hasAccepted) {
const securityScanResult = await scanRecipe(recipeConfig);
setHasSecurityWarnings(securityScanResult.has_security_warnings);
setIsRecipeWarningModalOpen(true);
} else {
setRecipeAccepted(true);
}
} catch (error) {
console.error('Error checking recipe acceptance:', error);
// If there's an error, assume the recipe hasn't been accepted
} catch {
setHasSecurityWarnings(false);
setIsRecipeWarningModalOpen(true);
}
}
@@ -311,5 +315,6 @@ export const useRecipeManager = (messages: Message[], locationState?: LocationSt
recipeAccepted,
handleRecipeAccept,
handleRecipeCancel,
hasSecurityWarnings,
};
};
+18
View File
@@ -2,6 +2,7 @@ import {
createRecipe as apiCreateRecipe,
encodeRecipe as apiEncodeRecipe,
decodeRecipe as apiDecodeRecipe,
scanRecipe as apiScanRecipe,
} from '../api';
import type {
CreateRecipeRequest as ApiCreateRecipeRequest,
@@ -133,6 +134,23 @@ export async function decodeRecipe(deeplink: string): Promise<Recipe> {
}
}
export async function scanRecipe(recipe: Recipe): Promise<{ has_security_warnings: boolean }> {
try {
const response = await apiScanRecipe({
body: { recipe },
});
if (!response.data) {
throw new Error('No data returned from API');
}
return response.data;
} catch (error) {
console.error('Failed to scan recipe:', error);
throw error;
}
}
export async function generateDeepLink(recipe: Recipe): Promise<string> {
const encoded = await encodeRecipe(recipe);
return `goose://recipe?config=${encoded}`;