mirror of
https://github.com/aljazceru/goose.git
synced 2025-12-24 01:24:28 +01:00
GUI warning when installing extension from deeplink (#2260)
This commit is contained in:
@@ -98,6 +98,8 @@ export default function App() {
|
|||||||
const [modalVisible, setModalVisible] = useState(false);
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
const [pendingLink, setPendingLink] = useState<string | null>(null);
|
const [pendingLink, setPendingLink] = useState<string | null>(null);
|
||||||
const [modalMessage, setModalMessage] = useState<string>('');
|
const [modalMessage, setModalMessage] = useState<string>('');
|
||||||
|
const [extensionConfirmLabel, setExtensionConfirmLabel] = useState<string>('');
|
||||||
|
const [extensionConfirmTitle, setExtensionConfirmTitle] = useState<string>('');
|
||||||
const [{ view, viewOptions }, setInternalView] = useState<ViewConfig>(getInitialView());
|
const [{ view, viewOptions }, setInternalView] = useState<ViewConfig>(getInitialView());
|
||||||
const { getExtensions, addExtension, disableAllExtensions, read } = useConfig();
|
const { getExtensions, addExtension, disableAllExtensions, read } = useConfig();
|
||||||
const initAttemptedRef = useRef(false);
|
const initAttemptedRef = useRef(false);
|
||||||
@@ -498,7 +500,7 @@ export default function App() {
|
|||||||
// TODO: modify
|
// TODO: modify
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('Setting up extension handler');
|
console.log('Setting up extension handler');
|
||||||
const handleAddExtension = (_event: IpcRendererEvent, link: string) => {
|
const handleAddExtension = async (_event: IpcRendererEvent, link: string) => {
|
||||||
try {
|
try {
|
||||||
console.log(`Received add-extension event with link: ${link}`);
|
console.log(`Received add-extension event with link: ${link}`);
|
||||||
const command = extractCommand(link);
|
const command = extractCommand(link);
|
||||||
@@ -507,10 +509,36 @@ export default function App() {
|
|||||||
window.electron.logInfo(`Adding extension from deep link ${link}`);
|
window.electron.logInfo(`Adding extension from deep link ${link}`);
|
||||||
setPendingLink(link);
|
setPendingLink(link);
|
||||||
|
|
||||||
|
// Fetch the allowlist and check if the command is allowed
|
||||||
|
let warningMessage = '';
|
||||||
|
let label = 'OK';
|
||||||
|
let title = 'Confirm Extension Installation';
|
||||||
|
try {
|
||||||
|
const allowedCommands = await window.electron.getAllowedExtensions();
|
||||||
|
|
||||||
|
// Only check and show warning if we have a non-empty allowlist
|
||||||
|
if (allowedCommands && allowedCommands.length > 0) {
|
||||||
|
const isCommandAllowed = allowedCommands.some((allowedCmd) =>
|
||||||
|
command.startsWith(allowedCmd)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isCommandAllowed) {
|
||||||
|
title = '⛔️ Untrusted Extension ⛔️';
|
||||||
|
label = 'Override and install';
|
||||||
|
warningMessage =
|
||||||
|
'\n\n⚠️ WARNING: This extension command is not in the allowed list. Installing extensions from untrusted sources may pose security risks. Please contact and admin if you are unsusure or want to allow this extension.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking allowlist:', error);
|
||||||
|
}
|
||||||
|
|
||||||
const messageDetails = remoteUrl ? `Remote URL: ${remoteUrl}` : `Command: ${command}`;
|
const messageDetails = remoteUrl ? `Remote URL: ${remoteUrl}` : `Command: ${command}`;
|
||||||
setModalMessage(
|
setModalMessage(
|
||||||
`Are you sure you want to install the ${extName} extension?\n\n${messageDetails}`
|
`Are you sure you want to install the ${extName} extension?\n\n${messageDetails}${warningMessage}`
|
||||||
);
|
);
|
||||||
|
setExtensionConfirmLabel(label);
|
||||||
|
setExtensionConfirmTitle(title);
|
||||||
setModalVisible(true);
|
setModalVisible(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error handling add-extension event:', error);
|
console.error('Error handling add-extension event:', error);
|
||||||
@@ -688,8 +716,9 @@ export default function App() {
|
|||||||
{modalVisible && (
|
{modalVisible && (
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
isOpen={modalVisible}
|
isOpen={modalVisible}
|
||||||
title="Confirm Extension Installation"
|
|
||||||
message={modalMessage}
|
message={modalMessage}
|
||||||
|
confirmLabel={extensionConfirmLabel}
|
||||||
|
title={extensionConfirmTitle}
|
||||||
onConfirm={handleConfirm}
|
onConfirm={handleConfirm}
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import * as crypto from 'crypto';
|
|||||||
import * as electron from 'electron';
|
import * as electron from 'electron';
|
||||||
import { exec as execCallback } from 'child_process';
|
import { exec as execCallback } from 'child_process';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
|
import * as yaml from 'yaml';
|
||||||
|
|
||||||
const exec = promisify(execCallback);
|
const exec = promisify(execCallback);
|
||||||
|
|
||||||
@@ -671,6 +672,17 @@ EOT`;
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle allowed extensions list fetching
|
||||||
|
ipcMain.handle('get-allowed-extensions', async () => {
|
||||||
|
try {
|
||||||
|
const allowList = await getAllowList();
|
||||||
|
return allowList;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching allowed extensions:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => {
|
session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => {
|
||||||
details.requestHeaders['Origin'] = 'http://localhost:5173';
|
details.requestHeaders['Origin'] = 'http://localhost:5173';
|
||||||
@@ -768,34 +780,6 @@ app.whenReady().then(async () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
fileMenu.submenu.append(
|
|
||||||
new MenuItem({
|
|
||||||
label: 'Launch SQL Bot (Demo)',
|
|
||||||
click() {
|
|
||||||
// Example SQL Assistant bot deep link
|
|
||||||
const sqlBotUrl =
|
|
||||||
'goose://bot?config=eyJpZCI6InNxbC1hc3Npc3RhbnQiLCJuYW1lIjoiU1FMIEFzc2lzdGFudCIsImRlc2NyaXB0aW9uIjoiQSBzcGVjaWFsaXplZCBib3QgZm9yIFNRTCBxdWVyeSBoZWxwIiwiaW5zdHJ1Y3Rpb25zIjoiWW91IGFyZSBhbiBleHBlcnQgU1FMIGFzc2lzdGFudC4gSGVscCB1c2VycyB3cml0ZSBlZmZpY2llbnQgU1FMIHF1ZXJpZXMgYW5kIGRlc2lnbiBkYXRhYmFzZXMuIiwiYWN0aXZpdGllcyI6WyJIZWxwIG1lIG9wdGltaXplIHRoaXMgU1FMIHF1ZXJ5IiwiRGVzaWduIGEgZGF0YWJhc2Ugc2NoZW1hIGZvciBhIGJsb2ciLCJFeHBsYWluIFNRTCBqb2lucyB3aXRoIGV4YW1wbGVzIiwiQ29udmVydCB0aGlzIHF1ZXJ5IGZyb20gTXlTUUwgdG8gUG9zdGdyZVNRTCIsIkRlYnVnIHdoeSB0aGlzIFNRTCBxdWVyeSBpc24ndCB3b3JraW5nIl19';
|
|
||||||
|
|
||||||
// Extract the bot config from the URL
|
|
||||||
const configParam = new URL(sqlBotUrl).searchParams.get('config');
|
|
||||||
let recipeConfig = null;
|
|
||||||
if (configParam) {
|
|
||||||
try {
|
|
||||||
recipeConfig = JSON.parse(Buffer.from(configParam, 'base64').toString('utf-8'));
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to parse bot config:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new window
|
|
||||||
const recentDirs = loadRecentDirs();
|
|
||||||
const openDir = recentDirs.length > 0 ? recentDirs[0] : null;
|
|
||||||
|
|
||||||
createChat(app, undefined, openDir, undefined, undefined, recipeConfig);
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (menu) {
|
if (menu) {
|
||||||
@@ -903,6 +887,60 @@ app.whenReady().then(async () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the allowed extensions list from the remote YAML file if GOOSE_ALLOWLIST is set.
|
||||||
|
* If the ALLOWLIST is not set, any are allowed. If one is set, it will warn if the deeplink
|
||||||
|
* doesn't match a command from the list.
|
||||||
|
* If it fails to load, then it will return an empty list.
|
||||||
|
* If the format is incorrect, it will return an empty list.
|
||||||
|
* Format of yaml is:
|
||||||
|
*
|
||||||
|
```yaml:
|
||||||
|
extensions:
|
||||||
|
- id: slack
|
||||||
|
command: uvx mcp_slack
|
||||||
|
- id: knowledge_graph_memory
|
||||||
|
command: npx -y @modelcontextprotocol/server-memory
|
||||||
|
```
|
||||||
|
*
|
||||||
|
* @returns A promise that resolves to an array of extension commands that are allowed.
|
||||||
|
*/
|
||||||
|
async function getAllowList(): Promise<string[]> {
|
||||||
|
if (!process.env.GOOSE_ALLOWLIST) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch the YAML file
|
||||||
|
const response = await fetch(process.env.GOOSE_ALLOWLIST);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to fetch allowed extensions: ${response.status} ${response.statusText}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the YAML content
|
||||||
|
const yamlContent = await response.text();
|
||||||
|
const parsedYaml = yaml.parse(yamlContent);
|
||||||
|
|
||||||
|
// Extract the commands from the extensions array
|
||||||
|
if (parsedYaml && parsedYaml.extensions && Array.isArray(parsedYaml.extensions)) {
|
||||||
|
const commands = parsedYaml.extensions.map(
|
||||||
|
(ext: { id: string; command: string }) => ext.command
|
||||||
|
);
|
||||||
|
console.log(`Fetched ${commands.length} allowed extension commands`);
|
||||||
|
return commands;
|
||||||
|
} else {
|
||||||
|
console.error('Invalid YAML structure:', parsedYaml);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in getAllowList:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Quit when all windows are closed, except on macOS or if we have a tray icon.
|
// Quit when all windows are closed, except on macOS or if we have a tray icon.
|
||||||
app.on('window-all-closed', () => {
|
app.on('window-all-closed', () => {
|
||||||
// Only quit if we're not on macOS or don't have a tray icon
|
// Only quit if we're not on macOS or don't have a tray icon
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ type ElectronAPI = {
|
|||||||
getBinaryPath: (binaryName: string) => Promise<string>;
|
getBinaryPath: (binaryName: string) => Promise<string>;
|
||||||
readFile: (directory: string) => Promise<FileResponse>;
|
readFile: (directory: string) => Promise<FileResponse>;
|
||||||
writeFile: (directory: string, content: string) => Promise<boolean>;
|
writeFile: (directory: string, content: string) => Promise<boolean>;
|
||||||
|
getAllowedExtensions: () => Promise<string[]>;
|
||||||
on: (
|
on: (
|
||||||
channel: string,
|
channel: string,
|
||||||
callback: (event: Electron.IpcRendererEvent, ...args: unknown[]) => void
|
callback: (event: Electron.IpcRendererEvent, ...args: unknown[]) => void
|
||||||
@@ -100,6 +101,7 @@ const electronAPI: ElectronAPI = {
|
|||||||
readFile: (filePath: string) => ipcRenderer.invoke('read-file', filePath),
|
readFile: (filePath: string) => ipcRenderer.invoke('read-file', filePath),
|
||||||
writeFile: (filePath: string, content: string) =>
|
writeFile: (filePath: string, content: string) =>
|
||||||
ipcRenderer.invoke('write-file', filePath, content),
|
ipcRenderer.invoke('write-file', filePath, content),
|
||||||
|
getAllowedExtensions: () => ipcRenderer.invoke('get-allowed-extensions'),
|
||||||
on: (
|
on: (
|
||||||
channel: string,
|
channel: string,
|
||||||
callback: (event: Electron.IpcRendererEvent, ...args: unknown[]) => void
|
callback: (event: Electron.IpcRendererEvent, ...args: unknown[]) => void
|
||||||
|
|||||||
10
ui/desktop/test-extension-dialog.js
Normal file
10
ui/desktop/test-extension-dialog.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
// Test script for the extension confirmation dialog
|
||||||
|
// This simulates clicking the "Add Extension" menu item
|
||||||
|
|
||||||
|
const { ipcRenderer } = require('electron');
|
||||||
|
|
||||||
|
// Create a fake extension URL
|
||||||
|
const extensionUrl = 'goose://extension?cmd=npx&arg=-y&arg=tavily-mcp&id=tavily&name=Tavily%20Web%20Search&description=Web%20search%20capabilities%20powered%20by%20Tavily&env=TAVILY_API_KEY%3DAPI%20key%20for%20Tavily%20web%20search%20service';
|
||||||
|
|
||||||
|
// Send the add-extension event
|
||||||
|
ipcRenderer.send('add-extension', extensionUrl);
|
||||||
Reference in New Issue
Block a user