mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
Desktop alerts when suspicious unicode characters found in Recipe (#4080)
This commit is contained in:
@@ -389,7 +389,8 @@ impl<'__s> ToSchema<'__s> for AnnotatedSchema {
|
||||
super::routes::schedule::sessions_handler,
|
||||
super::routes::recipe::create_recipe,
|
||||
super::routes::recipe::encode_recipe,
|
||||
super::routes::recipe::decode_recipe
|
||||
super::routes::recipe::decode_recipe,
|
||||
super::routes::recipe::scan_recipe
|
||||
),
|
||||
components(schemas(
|
||||
super::routes::config_management::UpsertConfigQuery,
|
||||
@@ -456,6 +457,8 @@ impl<'__s> ToSchema<'__s> for AnnotatedSchema {
|
||||
super::routes::recipe::EncodeRecipeResponse,
|
||||
super::routes::recipe::DecodeRecipeRequest,
|
||||
super::routes::recipe::DecodeRecipeResponse,
|
||||
super::routes::recipe::ScanRecipeRequest,
|
||||
super::routes::recipe::ScanRecipeResponse,
|
||||
goose::recipe::Recipe,
|
||||
goose::recipe::Author,
|
||||
goose::recipe::Settings,
|
||||
|
||||
@@ -56,6 +56,16 @@ pub struct DecodeRecipeResponse {
|
||||
recipe: Recipe,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct ScanRecipeRequest {
|
||||
recipe: Recipe,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct ScanRecipeResponse {
|
||||
has_security_warnings: bool,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/recipes/create",
|
||||
@@ -164,11 +174,31 @@ async fn decode_recipe(
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/recipes/scan",
|
||||
request_body = ScanRecipeRequest,
|
||||
responses(
|
||||
(status = 200, description = "Recipe scanned successfully", body = ScanRecipeResponse),
|
||||
),
|
||||
tag = "Recipe Management"
|
||||
)]
|
||||
async fn scan_recipe(
|
||||
Json(request): Json<ScanRecipeRequest>,
|
||||
) -> Result<Json<ScanRecipeResponse>, StatusCode> {
|
||||
let has_security_warnings = request.recipe.check_for_security_warnings();
|
||||
|
||||
Ok(Json(ScanRecipeResponse {
|
||||
has_security_warnings,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn routes(state: Arc<AppState>) -> Router {
|
||||
Router::new()
|
||||
.route("/recipes/create", post(create_recipe))
|
||||
.route("/recipes/encode", post(encode_recipe))
|
||||
.route("/recipes/decode", post(decode_recipe))
|
||||
.route("/recipes/scan", post(scan_recipe))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::fmt;
|
||||
|
||||
use crate::agents::extension::ExtensionConfig;
|
||||
use crate::agents::types::RetryConfig;
|
||||
use crate::utils::contains_unicode_tags;
|
||||
use serde::de::Deserializer;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
@@ -253,6 +254,25 @@ pub struct RecipeBuilder {
|
||||
}
|
||||
|
||||
impl Recipe {
|
||||
/// Returns true if harmful content is detected in instructions, prompt, or activities fields
|
||||
pub fn check_for_security_warnings(&self) -> bool {
|
||||
if [self.instructions.as_deref(), self.prompt.as_deref()]
|
||||
.iter()
|
||||
.flatten()
|
||||
.any(|&field| contains_unicode_tags(field))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(activities) = &self.activities {
|
||||
return activities
|
||||
.iter()
|
||||
.any(|activity| contains_unicode_tags(activity));
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Creates a new RecipeBuilder to construct a Recipe instance
|
||||
///
|
||||
/// # Example
|
||||
@@ -746,4 +766,41 @@ isGlobal: true"#;
|
||||
let extensions = recipe.extensions.unwrap();
|
||||
assert_eq!(extensions.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_for_security_warnings() {
|
||||
let mut recipe = Recipe {
|
||||
version: "1.0.0".to_string(),
|
||||
title: "Test".to_string(),
|
||||
description: "Test".to_string(),
|
||||
instructions: Some("clean instructions".to_string()),
|
||||
prompt: Some("clean prompt".to_string()),
|
||||
extensions: None,
|
||||
context: None,
|
||||
settings: None,
|
||||
activities: Some(vec!["clean activity 1".to_string()]),
|
||||
author: None,
|
||||
parameters: None,
|
||||
response: None,
|
||||
sub_recipes: None,
|
||||
retry: None,
|
||||
};
|
||||
|
||||
assert!(!recipe.check_for_security_warnings());
|
||||
|
||||
// Malicious activities
|
||||
recipe.activities = Some(vec![
|
||||
"clean activity".to_string(),
|
||||
format!("malicious{}activity", '\u{E0041}'),
|
||||
]);
|
||||
assert!(recipe.check_for_security_warnings());
|
||||
|
||||
// Malicious instructions
|
||||
recipe.instructions = Some(format!("instructions{}", '\u{E0041}'));
|
||||
assert!(recipe.check_for_security_warnings());
|
||||
|
||||
// Malicious prompt
|
||||
recipe.prompt = Some(format!("prompt{}", '\u{E0042}'));
|
||||
assert!(recipe.check_for_security_warnings());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use unicode_normalization::UnicodeNormalization;
|
||||
|
||||
/// Check if a character is in the Unicode Tags Block range (U+E0000-U+E007F)
|
||||
/// These characters are invisible and can be used for steganographic attacks
|
||||
fn is_in_unicode_tag_range(c: char) -> bool {
|
||||
matches!(c, '\u{E0000}'..='\u{E007F}')
|
||||
}
|
||||
|
||||
pub fn contains_unicode_tags(text: &str) -> bool {
|
||||
text.chars().any(is_in_unicode_tag_range)
|
||||
}
|
||||
|
||||
/// Sanitize Unicode Tags Block characters from text
|
||||
/// Used to prevent Unicode-based prompt injection attacks
|
||||
///
|
||||
/// This function removes invisible Unicode Tags Block characters (U+E0000-U+E007F)
|
||||
/// that can be used for steganographic attacks while preserving legitimate Unicode.
|
||||
pub fn sanitize_unicode_tags(text: &str) -> String {
|
||||
let normalized: String = text.nfc().collect();
|
||||
|
||||
normalized
|
||||
.chars()
|
||||
.filter(|&c| !matches!(c, '\u{E0000}'..='\u{E007F}'))
|
||||
.filter(|&c| !is_in_unicode_tag_range(c))
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -45,6 +51,17 @@ pub fn is_token_cancelled(cancellation_token: &Option<CancellationToken>) -> boo
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_contains_unicode_tags() {
|
||||
// Test detection of Unicode Tags Block characters
|
||||
assert!(contains_unicode_tags("Hello\u{E0041}world"));
|
||||
assert!(contains_unicode_tags("\u{E0000}"));
|
||||
assert!(contains_unicode_tags("\u{E007F}"));
|
||||
assert!(!contains_unicode_tags("Hello world"));
|
||||
assert!(!contains_unicode_tags("Hello 世界 🌍"));
|
||||
assert!(!contains_unicode_tags(""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_unicode_tags() {
|
||||
// Test that Unicode Tags Block characters are removed
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
Reference in New Issue
Block a user