mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
feat: external goosed server (#5978)
supports connecting to an external goosed in client/server mode
This commit is contained in:
@@ -100,10 +100,10 @@ export default function MCPUIResourceRenderer({
|
||||
|
||||
const fetchProxyUrl = async () => {
|
||||
try {
|
||||
const baseUrl = await window.electron.getGoosedHostPort();
|
||||
const gooseApiHost = await window.electron.getGoosedHostPort();
|
||||
const secretKey = await window.electron.getSecretKey();
|
||||
if (baseUrl && secretKey) {
|
||||
setProxyUrl(`${baseUrl}/mcp-ui-proxy?secret=${encodeURIComponent(secretKey)}`);
|
||||
if (gooseApiHost && secretKey) {
|
||||
setProxyUrl(`${gooseApiHost}/mcp-ui-proxy?secret=${encodeURIComponent(secretKey)}`);
|
||||
} else {
|
||||
console.error('Failed to get goosed host/port or secret key');
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
|
||||
import { View, ViewOptions } from '../../utils/navigationUtils';
|
||||
import ModelsSection from './models/ModelsSection';
|
||||
import SessionSharingSection from './sessions/SessionSharingSection';
|
||||
import ExternalBackendSection from './app/ExternalBackendSection';
|
||||
import AppSettingsSection from './app/AppSettingsSection';
|
||||
import ConfigSettings from './config/ConfigSettings';
|
||||
import { ExtensionConfig } from '../../api';
|
||||
@@ -127,7 +128,10 @@ export default function SettingsView({
|
||||
value="sharing"
|
||||
className="mt-0 focus-visible:outline-none focus-visible:ring-0"
|
||||
>
|
||||
<SessionSharingSection />
|
||||
<div className="space-y-8">
|
||||
<SessionSharingSection />
|
||||
<ExternalBackendSection />
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Settings, RefreshCw, ExternalLink } from 'lucide-react';
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '../../ui/dialog';
|
||||
import UpdateSection from './UpdateSection';
|
||||
import TunnelSection from '../tunnel/TunnelSection';
|
||||
|
||||
import { COST_TRACKING_ENABLED, UPDATES_ENABLED } from '../../../updates';
|
||||
import { getApiUrl } from '../../../config';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../ui/card';
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Switch } from '../../ui/switch';
|
||||
import { Input } from '../../ui/input';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../ui/card';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
|
||||
interface ExternalGoosedConfig {
|
||||
enabled: boolean;
|
||||
url: string;
|
||||
secret: string;
|
||||
}
|
||||
|
||||
interface Settings {
|
||||
externalGoosed?: Partial<ExternalGoosedConfig>;
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG: ExternalGoosedConfig = {
|
||||
enabled: false,
|
||||
url: '',
|
||||
secret: '',
|
||||
};
|
||||
|
||||
function parseConfig(partial: Partial<ExternalGoosedConfig> | undefined): ExternalGoosedConfig {
|
||||
return {
|
||||
enabled: partial?.enabled ?? DEFAULT_CONFIG.enabled,
|
||||
url: partial?.url ?? DEFAULT_CONFIG.url,
|
||||
secret: partial?.secret ?? DEFAULT_CONFIG.secret,
|
||||
};
|
||||
}
|
||||
|
||||
export default function ExternalBackendSection() {
|
||||
const [config, setConfig] = useState<ExternalGoosedConfig>(DEFAULT_CONFIG);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [urlError, setUrlError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const loadSettings = async () => {
|
||||
const settings = (await window.electron.getSettings()) as Settings | null;
|
||||
setConfig(parseConfig(settings?.externalGoosed));
|
||||
};
|
||||
loadSettings();
|
||||
}, []);
|
||||
|
||||
const validateUrl = (value: string): boolean => {
|
||||
if (!value) {
|
||||
setUrlError(null);
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
const parsed = new URL(value);
|
||||
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
||||
setUrlError('URL must use http or https protocol');
|
||||
return false;
|
||||
}
|
||||
setUrlError(null);
|
||||
return true;
|
||||
} catch {
|
||||
setUrlError('Invalid URL format');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const saveConfig = async (newConfig: ExternalGoosedConfig): Promise<void> => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const currentSettings = ((await window.electron.getSettings()) as Settings) || {};
|
||||
await window.electron.saveSettings({
|
||||
...currentSettings,
|
||||
externalGoosed: newConfig,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to save external backend settings:', error);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const updateField = <K extends keyof ExternalGoosedConfig>(
|
||||
field: K,
|
||||
value: ExternalGoosedConfig[K]
|
||||
) => {
|
||||
const newConfig = { ...config, [field]: value };
|
||||
setConfig(newConfig);
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
const handleUrlChange = (value: string) => {
|
||||
updateField('url', value);
|
||||
validateUrl(value);
|
||||
};
|
||||
|
||||
const handleUrlBlur = async () => {
|
||||
if (validateUrl(config.url)) {
|
||||
await saveConfig(config);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="external-backend" className="space-y-4 pr-4 mt-1">
|
||||
<Card className="pb-2">
|
||||
<CardHeader className="pb-0">
|
||||
<CardTitle>Goose Server</CardTitle>
|
||||
<CardDescription>
|
||||
By default goose launches a server for you, use this to connect to an external goose
|
||||
server
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-4 space-y-4 px-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-text-default text-xs">Use external server</h3>
|
||||
<p className="text-xs text-text-muted max-w-md mt-[2px]">
|
||||
Connect to a goose server running elsewhere (requires app restart)
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Switch
|
||||
checked={config.enabled}
|
||||
onCheckedChange={(checked) => saveConfig(updateField('enabled', checked))}
|
||||
disabled={isSaving}
|
||||
variant="mono"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{config.enabled && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="external-url" className="text-text-default text-xs">
|
||||
Server URL
|
||||
</label>
|
||||
<Input
|
||||
id="external-url"
|
||||
type="url"
|
||||
placeholder="http://127.0.0.1:3000"
|
||||
value={config.url}
|
||||
onChange={(e) => handleUrlChange(e.target.value)}
|
||||
onBlur={handleUrlBlur}
|
||||
disabled={isSaving}
|
||||
className={urlError ? 'border-red-500' : ''}
|
||||
/>
|
||||
{urlError && (
|
||||
<p className="text-xs text-red-500 flex items-center gap-1">
|
||||
<AlertCircle size={12} />
|
||||
{urlError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="external-secret" className="text-text-default text-xs">
|
||||
Secret Key
|
||||
</label>
|
||||
<Input
|
||||
id="external-secret"
|
||||
type="password"
|
||||
placeholder="Enter the server's secret key"
|
||||
value={config.secret}
|
||||
onChange={(e) => updateField('secret', e.target.value)}
|
||||
onBlur={() => saveConfig(config)}
|
||||
disabled={isSaving}
|
||||
/>
|
||||
<p className="text-xs text-text-muted">
|
||||
The secret key configured on the goosed server (GOOSE_SERVER__SECRET_KEY)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-amber-50 dark:bg-amber-950 border border-amber-200 dark:border-amber-800 rounded-md p-3">
|
||||
<p className="text-xs text-amber-800 dark:text-amber-200">
|
||||
<strong>Note:</strong> Changes require restarting Goose to take effect. New chat
|
||||
windows will connect to the external server.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
// Helper to construct API endpoints
|
||||
export const getApiUrl = (endpoint: string): string => {
|
||||
const baseUrl =
|
||||
String(window.appConfig.get('GOOSE_API_HOST') || '') +
|
||||
':' +
|
||||
String(window.appConfig.get('GOOSE_PORT') || '');
|
||||
const gooseApiHost = String(window.appConfig.get('GOOSE_API_HOST') || '');
|
||||
const cleanEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
|
||||
return `${baseUrl}${cleanEndpoint}`;
|
||||
return `${gooseApiHost}${cleanEndpoint}`;
|
||||
};
|
||||
|
||||
+34
-30
@@ -10,6 +10,7 @@ import { Buffer } from 'node:buffer';
|
||||
|
||||
import { status } from './api';
|
||||
import { Client } from './api/client';
|
||||
import { ExternalGoosedConfig } from './utils/settings';
|
||||
|
||||
export const findAvailablePort = (): Promise<number> => {
|
||||
return new Promise((resolve, _reject) => {
|
||||
@@ -53,11 +54,15 @@ export const checkServerStatus = async (client: Client, errorLog: string[]): Pro
|
||||
return false;
|
||||
};
|
||||
|
||||
const connectToExternalBackend = async (
|
||||
workingDir: string,
|
||||
port: number = 3000
|
||||
): Promise<[number, string, ChildProcess, string[]]> => {
|
||||
log.info(`Using external goosed backend on port ${port}`);
|
||||
export interface GoosedResult {
|
||||
baseUrl: string;
|
||||
workingDir: string;
|
||||
process: ChildProcess;
|
||||
errorLog: string[];
|
||||
}
|
||||
|
||||
const connectToExternalBackend = (workingDir: string, url: string): GoosedResult => {
|
||||
log.info(`Using external goosed backend at ${url}`);
|
||||
|
||||
const mockProcess = {
|
||||
pid: undefined,
|
||||
@@ -66,7 +71,7 @@ const connectToExternalBackend = async (
|
||||
},
|
||||
} as ChildProcess;
|
||||
|
||||
return [port, workingDir, mockProcess, []];
|
||||
return { baseUrl: url, workingDir, process: mockProcess, errorLog: [] };
|
||||
};
|
||||
|
||||
interface GooseProcessEnv {
|
||||
@@ -81,18 +86,26 @@ interface GooseProcessEnv {
|
||||
GOOSE_SERVER__SECRET_KEY?: string;
|
||||
}
|
||||
|
||||
export const startGoosed = async (
|
||||
app: App,
|
||||
serverSecret: string,
|
||||
dir: string,
|
||||
env: Partial<GooseProcessEnv> = {}
|
||||
): Promise<[number, string, ChildProcess, string[]]> => {
|
||||
export interface StartGoosedOptions {
|
||||
app: App;
|
||||
serverSecret: string;
|
||||
dir: string;
|
||||
env?: Partial<GooseProcessEnv>;
|
||||
externalGoosed?: ExternalGoosedConfig;
|
||||
}
|
||||
|
||||
export const startGoosed = async (options: StartGoosedOptions): Promise<GoosedResult> => {
|
||||
const { app, serverSecret, dir: inputDir, env = {}, externalGoosed } = options;
|
||||
const isWindows = process.platform === 'win32';
|
||||
const homeDir = os.homedir();
|
||||
dir = path.resolve(path.normalize(dir));
|
||||
const dir = path.resolve(path.normalize(inputDir));
|
||||
|
||||
if (externalGoosed?.enabled && externalGoosed.url) {
|
||||
return connectToExternalBackend(dir, externalGoosed.url);
|
||||
}
|
||||
|
||||
if (process.env.GOOSE_EXTERNAL_BACKEND) {
|
||||
return connectToExternalBackend(dir, 3000);
|
||||
return connectToExternalBackend(dir, 'http://127.0.0.1:3000');
|
||||
}
|
||||
|
||||
let goosedPath = getGoosedBinaryPath(app);
|
||||
@@ -105,25 +118,18 @@ export const startGoosed = async (
|
||||
log.info(`Starting goosed from: ${resolvedGoosedPath} on port ${port} in dir ${dir}`);
|
||||
|
||||
const additionalEnv: GooseProcessEnv = {
|
||||
// Set HOME for UNIX-like systems
|
||||
HOME: homeDir,
|
||||
// Set USERPROFILE for Windows
|
||||
USERPROFILE: homeDir,
|
||||
// Set APPDATA for Windows
|
||||
APPDATA: process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming'),
|
||||
// Set LOCAL_APPDATA for Windows
|
||||
LOCALAPPDATA: process.env.LOCALAPPDATA || path.join(homeDir, 'AppData', 'Local'),
|
||||
// Set PATH to include the binary directory
|
||||
PATH: `${path.dirname(resolvedGoosedPath)}${path.delimiter}${process.env.PATH || ''}`,
|
||||
GOOSE_PORT: String(port),
|
||||
GOOSE_SERVER__SECRET_KEY: serverSecret,
|
||||
// Add any additional environment variables passed in
|
||||
...env,
|
||||
} as GooseProcessEnv;
|
||||
|
||||
const processEnv: GooseProcessEnv = { ...process.env, ...additionalEnv } as GooseProcessEnv;
|
||||
|
||||
// Ensure proper executable path on Windows
|
||||
if (isWindows && !resolvedGoosedPath.toLowerCase().endsWith('.exe')) {
|
||||
goosedPath = resolvedGoosedPath + '.exe';
|
||||
} else {
|
||||
@@ -135,15 +141,11 @@ export const startGoosed = async (
|
||||
cwd: dir,
|
||||
env: processEnv,
|
||||
stdio: ['ignore', 'pipe', 'pipe'] as ['ignore', 'pipe', 'pipe'],
|
||||
// Hide terminal window on Windows
|
||||
windowsHide: true,
|
||||
// Run detached on Windows only to avoid terminal windows
|
||||
detached: isWindows,
|
||||
// Never use shell to avoid command injection - this is critical for security
|
||||
shell: false,
|
||||
};
|
||||
|
||||
// Log spawn options for debugging (excluding sensitive env vars)
|
||||
const safeSpawnOptions = {
|
||||
...spawnOptions,
|
||||
env: Object.keys(spawnOptions.env || {}).reduce(
|
||||
@@ -160,12 +162,10 @@ export const startGoosed = async (
|
||||
};
|
||||
log.info('Spawn options:', JSON.stringify(safeSpawnOptions, null, 2));
|
||||
|
||||
// Security: Use only hardcoded, safe arguments
|
||||
const safeArgs = ['agent'];
|
||||
|
||||
const goosedProcess: ChildProcess = spawn(goosedPath, safeArgs, spawnOptions);
|
||||
|
||||
// Only unref on Windows to allow it to run independently of the parent
|
||||
if (isWindows && goosedProcess.unref) {
|
||||
goosedProcess.unref();
|
||||
}
|
||||
@@ -191,7 +191,7 @@ export const startGoosed = async (
|
||||
|
||||
goosedProcess.on('error', (err: Error) => {
|
||||
log.error(`Failed to start goosed on port ${port} and dir ${dir}`, err);
|
||||
throw err; // Propagate the error
|
||||
throw err;
|
||||
});
|
||||
|
||||
const try_kill_goose = () => {
|
||||
@@ -207,14 +207,18 @@ export const startGoosed = async (
|
||||
}
|
||||
};
|
||||
|
||||
// Ensure goosed is terminated when the app quits
|
||||
app.on('will-quit', () => {
|
||||
log.info('App quitting, terminating goosed server');
|
||||
try_kill_goose();
|
||||
});
|
||||
|
||||
log.info(`Goosed server successfully started on port ${port}`);
|
||||
return [port, dir, goosedProcess, stderrLines];
|
||||
return {
|
||||
baseUrl: `http://127.0.0.1:${port}`,
|
||||
workingDir: dir,
|
||||
process: goosedProcess,
|
||||
errorLog: stderrLines,
|
||||
};
|
||||
};
|
||||
|
||||
const getGoosedBinaryPath = (app: Electron.App): string => {
|
||||
|
||||
+96
-39
@@ -466,16 +466,23 @@ const getBundledConfig = (): BundledConfig => {
|
||||
const { defaultProvider, defaultModel, predefinedModels, baseUrlShare, version } =
|
||||
getBundledConfig();
|
||||
|
||||
const SERVER_SECRET = process.env.GOOSE_EXTERNAL_BACKEND
|
||||
? 'test'
|
||||
: crypto.randomBytes(32).toString('hex');
|
||||
const GENERATED_SECRET = crypto.randomBytes(32).toString('hex');
|
||||
|
||||
const getServerSecret = (settings: ReturnType<typeof loadSettings>): string => {
|
||||
if (settings.externalGoosed?.enabled && settings.externalGoosed.secret) {
|
||||
return settings.externalGoosed.secret;
|
||||
}
|
||||
if (process.env.GOOSE_EXTERNAL_BACKEND) {
|
||||
return 'test';
|
||||
}
|
||||
return GENERATED_SECRET;
|
||||
};
|
||||
|
||||
let appConfig = {
|
||||
GOOSE_DEFAULT_PROVIDER: defaultProvider,
|
||||
GOOSE_DEFAULT_MODEL: defaultModel,
|
||||
GOOSE_PREDEFINED_MODELS: predefinedModels,
|
||||
GOOSE_API_HOST: 'http://127.0.0.1',
|
||||
GOOSE_PORT: 0,
|
||||
GOOSE_WORKING_DIR: '',
|
||||
// If GOOSE_ALLOWLIST_WARNING env var is not set, defaults to false (strict blocking mode)
|
||||
GOOSE_ALLOWLIST_WARNING: process.env.GOOSE_ALLOWLIST_WARNING === 'true',
|
||||
@@ -503,15 +510,18 @@ const createChat = async (
|
||||
) => {
|
||||
updateEnvironmentVariables(envToggles);
|
||||
|
||||
const envVars = {
|
||||
GOOSE_PATH_ROOT: process.env.GOOSE_PATH_ROOT,
|
||||
};
|
||||
const [port, workingDir, goosedProcess, errorLog] = await startGoosed(
|
||||
const settings = loadSettings();
|
||||
const serverSecret = getServerSecret(settings);
|
||||
|
||||
const goosedResult = await startGoosed({
|
||||
app,
|
||||
SERVER_SECRET,
|
||||
dir || os.homedir(),
|
||||
envVars
|
||||
);
|
||||
serverSecret,
|
||||
dir: dir || os.homedir(),
|
||||
env: { GOOSE_PATH_ROOT: process.env.GOOSE_PATH_ROOT },
|
||||
externalGoosed: settings.externalGoosed,
|
||||
});
|
||||
|
||||
const { baseUrl, workingDir, process: goosedProcess, errorLog } = goosedResult;
|
||||
|
||||
const mainWindowState = windowStateKeeper({
|
||||
defaultWidth: 940,
|
||||
@@ -534,14 +544,13 @@ const createChat = async (
|
||||
webPreferences: {
|
||||
spellcheck: true,
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
// Enable features needed for Web Speech API
|
||||
webSecurity: true,
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
additionalArguments: [
|
||||
JSON.stringify({
|
||||
...appConfig,
|
||||
GOOSE_PORT: port,
|
||||
GOOSE_API_HOST: baseUrl,
|
||||
GOOSE_WORKING_DIR: workingDir,
|
||||
REQUEST_DIR: dir,
|
||||
GOOSE_BASE_URL_SHARE: baseUrlShare,
|
||||
@@ -552,7 +561,7 @@ const createChat = async (
|
||||
scheduledJobId: scheduledJobId,
|
||||
}),
|
||||
],
|
||||
partition: 'persist:goose', // Add this line to ensure persistence
|
||||
partition: 'persist:goose',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -567,10 +576,10 @@ const createChat = async (
|
||||
|
||||
const goosedClient = createClient(
|
||||
createConfig({
|
||||
baseUrl: `http://127.0.0.1:${port}`,
|
||||
baseUrl,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Secret-Key': SERVER_SECRET,
|
||||
'X-Secret-Key': serverSecret,
|
||||
},
|
||||
})
|
||||
);
|
||||
@@ -578,13 +587,41 @@ const createChat = async (
|
||||
|
||||
const serverReady = await checkServerStatus(goosedClient, errorLog);
|
||||
if (!serverReady) {
|
||||
dialog.showMessageBoxSync({
|
||||
type: 'error',
|
||||
title: 'Goose Failed to Start',
|
||||
message: 'The backend server failed to start.',
|
||||
detail: errorLog.join('\n'),
|
||||
buttons: ['OK'],
|
||||
});
|
||||
const isUsingExternalBackend = settings.externalGoosed?.enabled;
|
||||
|
||||
if (isUsingExternalBackend) {
|
||||
const response = dialog.showMessageBoxSync({
|
||||
type: 'error',
|
||||
title: 'External Backend Unreachable',
|
||||
message: `Could not connect to external backend at ${settings.externalGoosed?.url}`,
|
||||
detail: 'The external goosed server may not be running.',
|
||||
buttons: ['Disable External Backend & Retry', 'Quit'],
|
||||
defaultId: 0,
|
||||
cancelId: 1,
|
||||
});
|
||||
|
||||
if (response === 0) {
|
||||
const updatedSettings = {
|
||||
...settings,
|
||||
externalGoosed: {
|
||||
enabled: false,
|
||||
url: settings.externalGoosed?.url || '',
|
||||
secret: settings.externalGoosed?.secret || '',
|
||||
},
|
||||
};
|
||||
saveSettings(updatedSettings);
|
||||
mainWindow.destroy();
|
||||
return createChat(app, initialMessage, dir);
|
||||
}
|
||||
} else {
|
||||
dialog.showMessageBoxSync({
|
||||
type: 'error',
|
||||
title: 'Goose Failed to Start',
|
||||
message: 'The backend server failed to start.',
|
||||
detail: errorLog.join('\n'),
|
||||
buttons: ['OK'],
|
||||
});
|
||||
}
|
||||
app.quit();
|
||||
}
|
||||
|
||||
@@ -1166,8 +1203,19 @@ ipcMain.handle('get-settings', () => {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('save-settings', (_event, settings) => {
|
||||
try {
|
||||
saveSettings(settings);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error saving settings:', error);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-secret-key', () => {
|
||||
return SERVER_SECRET;
|
||||
const settings = loadSettings();
|
||||
return getServerSecret(settings);
|
||||
});
|
||||
|
||||
ipcMain.handle('get-goosed-host-port', async (event) => {
|
||||
@@ -1763,6 +1811,28 @@ async function appMain() {
|
||||
}
|
||||
});
|
||||
|
||||
const buildConnectSrc = (): string => {
|
||||
const sources = [
|
||||
"'self'",
|
||||
'http://127.0.0.1:*',
|
||||
'https://api.github.com',
|
||||
'https://github.com',
|
||||
'https://objects.githubusercontent.com',
|
||||
];
|
||||
|
||||
const settings = loadSettings();
|
||||
if (settings.externalGoosed?.enabled && settings.externalGoosed.url) {
|
||||
try {
|
||||
const externalUrl = new URL(settings.externalGoosed.url);
|
||||
sources.push(externalUrl.origin);
|
||||
} catch {
|
||||
console.warn('Invalid external goosed URL in settings, skipping CSP entry');
|
||||
}
|
||||
}
|
||||
|
||||
return sources.join(' ');
|
||||
};
|
||||
|
||||
// Add CSP headers to all sessions
|
||||
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
|
||||
callback({
|
||||
@@ -1770,31 +1840,18 @@ async function appMain() {
|
||||
...details.responseHeaders,
|
||||
'Content-Security-Policy':
|
||||
"default-src 'self';" +
|
||||
// Allow inline styles since we use them in our React components
|
||||
"style-src 'self' 'unsafe-inline';" +
|
||||
// Scripts from our app and inline scripts (for theme initialization)
|
||||
"script-src 'self' 'unsafe-inline';" +
|
||||
// Images from our app and data: URLs (for base64 images)
|
||||
"img-src 'self' data: https:;" +
|
||||
// Connect to our local API and specific external services
|
||||
"connect-src 'self' http://127.0.0.1:* https://api.github.com https://github.com https://objects.githubusercontent.com" +
|
||||
// Don't allow any plugins
|
||||
`connect-src ${buildConnectSrc()};` +
|
||||
"object-src 'none';" +
|
||||
// Allow all frames (iframes)
|
||||
"frame-src 'self' https: http:;" +
|
||||
// Font sources - allow self, data URLs, and external fonts
|
||||
"font-src 'self' data: https:;" +
|
||||
// Media sources - allow microphone
|
||||
"media-src 'self' mediastream:;" +
|
||||
// Form actions
|
||||
"form-action 'none';" +
|
||||
// Base URI restriction
|
||||
"base-uri 'self';" +
|
||||
// Manifest files
|
||||
"manifest-src 'self';" +
|
||||
// Worker sources
|
||||
"worker-src 'self';" +
|
||||
// Upgrade insecure requests
|
||||
'upgrade-insecure-requests;',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -75,6 +75,7 @@ type ElectronAPI = {
|
||||
setDockIcon: (show: boolean) => Promise<boolean>;
|
||||
getDockIconState: () => Promise<boolean>;
|
||||
getSettings: () => Promise<unknown | null>;
|
||||
saveSettings: (settings: unknown) => Promise<boolean>;
|
||||
getSecretKey: () => Promise<string>;
|
||||
getGoosedHostPort: () => Promise<string | null>;
|
||||
setWakelock: (enable: boolean) => Promise<boolean>;
|
||||
@@ -177,6 +178,7 @@ const electronAPI: ElectronAPI = {
|
||||
setDockIcon: (show: boolean) => ipcRenderer.invoke('set-dock-icon', show),
|
||||
getDockIconState: () => ipcRenderer.invoke('get-dock-icon-state'),
|
||||
getSettings: () => ipcRenderer.invoke('get-settings'),
|
||||
saveSettings: (settings: unknown) => ipcRenderer.invoke('save-settings', settings),
|
||||
getSecretKey: () => ipcRenderer.invoke('get-secret-key'),
|
||||
getGoosedHostPort: () => ipcRenderer.invoke('get-goosed-host-port'),
|
||||
setWakelock: (enable: boolean) => ipcRenderer.invoke('set-wakelock', enable),
|
||||
|
||||
@@ -13,14 +13,14 @@ const App = lazy(() => import('./App'));
|
||||
|
||||
if (!isLauncher) {
|
||||
console.log('window created, getting goosed connection info');
|
||||
const baseUrl = await window.electron.getGoosedHostPort();
|
||||
if (baseUrl === null) {
|
||||
const gooseApiHost = await window.electron.getGoosedHostPort();
|
||||
if (gooseApiHost === null) {
|
||||
window.alert('failed to start goose backend process');
|
||||
return;
|
||||
}
|
||||
console.log('connecting at', baseUrl);
|
||||
console.log('connecting at', gooseApiHost);
|
||||
client.setConfig({
|
||||
baseUrl,
|
||||
baseUrl: gooseApiHost,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Secret-Key': await window.electron.getSecretKey(),
|
||||
|
||||
@@ -7,11 +7,18 @@ export interface EnvToggles {
|
||||
GOOSE_SERVER__COMPUTER_CONTROLLER: boolean;
|
||||
}
|
||||
|
||||
export interface ExternalGoosedConfig {
|
||||
enabled: boolean;
|
||||
url: string;
|
||||
secret: string;
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
envToggles: EnvToggles;
|
||||
showMenuBarIcon: boolean;
|
||||
showDockIcon: boolean;
|
||||
enableWakelock: boolean;
|
||||
externalGoosed?: ExternalGoosedConfig;
|
||||
}
|
||||
|
||||
const SETTINGS_FILE = path.join(app.getPath('userData'), 'settings.json');
|
||||
|
||||
Reference in New Issue
Block a user