feat: external goosed server (#5978)

supports connecting to an external goosed in client/server mode
This commit is contained in:
Michael Neale
2025-12-15 14:04:39 +11:00
committed by GitHub
parent 108ffad4a4
commit 208417e751
10 changed files with 334 additions and 83 deletions
@@ -100,10 +100,10 @@ export default function MCPUIResourceRenderer({
const fetchProxyUrl = async () => { const fetchProxyUrl = async () => {
try { try {
const baseUrl = await window.electron.getGoosedHostPort(); const gooseApiHost = await window.electron.getGoosedHostPort();
const secretKey = await window.electron.getSecretKey(); const secretKey = await window.electron.getSecretKey();
if (baseUrl && secretKey) { if (gooseApiHost && secretKey) {
setProxyUrl(`${baseUrl}/mcp-ui-proxy?secret=${encodeURIComponent(secretKey)}`); setProxyUrl(`${gooseApiHost}/mcp-ui-proxy?secret=${encodeURIComponent(secretKey)}`);
} else { } else {
console.error('Failed to get goosed host/port or secret key'); 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 { View, ViewOptions } from '../../utils/navigationUtils';
import ModelsSection from './models/ModelsSection'; import ModelsSection from './models/ModelsSection';
import SessionSharingSection from './sessions/SessionSharingSection'; import SessionSharingSection from './sessions/SessionSharingSection';
import ExternalBackendSection from './app/ExternalBackendSection';
import AppSettingsSection from './app/AppSettingsSection'; import AppSettingsSection from './app/AppSettingsSection';
import ConfigSettings from './config/ConfigSettings'; import ConfigSettings from './config/ConfigSettings';
import { ExtensionConfig } from '../../api'; import { ExtensionConfig } from '../../api';
@@ -127,7 +128,10 @@ export default function SettingsView({
value="sharing" value="sharing"
className="mt-0 focus-visible:outline-none focus-visible:ring-0" className="mt-0 focus-visible:outline-none focus-visible:ring-0"
> >
<SessionSharingSection /> <div className="space-y-8">
<SessionSharingSection />
<ExternalBackendSection />
</div>
</TabsContent> </TabsContent>
<TabsContent <TabsContent
@@ -5,6 +5,7 @@ import { Settings, RefreshCw, ExternalLink } from 'lucide-react';
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '../../ui/dialog'; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '../../ui/dialog';
import UpdateSection from './UpdateSection'; import UpdateSection from './UpdateSection';
import TunnelSection from '../tunnel/TunnelSection'; import TunnelSection from '../tunnel/TunnelSection';
import { COST_TRACKING_ENABLED, UPDATES_ENABLED } from '../../../updates'; import { COST_TRACKING_ENABLED, UPDATES_ENABLED } from '../../../updates';
import { getApiUrl } from '../../../config'; import { getApiUrl } from '../../../config';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../ui/card'; 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>
);
}
+2 -6
View File
@@ -1,9 +1,5 @@
// Helper to construct API endpoints
export const getApiUrl = (endpoint: string): string => { export const getApiUrl = (endpoint: string): string => {
const baseUrl = const gooseApiHost = String(window.appConfig.get('GOOSE_API_HOST') || '');
String(window.appConfig.get('GOOSE_API_HOST') || '') +
':' +
String(window.appConfig.get('GOOSE_PORT') || '');
const cleanEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`; const cleanEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
return `${baseUrl}${cleanEndpoint}`; return `${gooseApiHost}${cleanEndpoint}`;
}; };
+34 -30
View File
@@ -10,6 +10,7 @@ import { Buffer } from 'node:buffer';
import { status } from './api'; import { status } from './api';
import { Client } from './api/client'; import { Client } from './api/client';
import { ExternalGoosedConfig } from './utils/settings';
export const findAvailablePort = (): Promise<number> => { export const findAvailablePort = (): Promise<number> => {
return new Promise((resolve, _reject) => { return new Promise((resolve, _reject) => {
@@ -53,11 +54,15 @@ export const checkServerStatus = async (client: Client, errorLog: string[]): Pro
return false; return false;
}; };
const connectToExternalBackend = async ( export interface GoosedResult {
workingDir: string, baseUrl: string;
port: number = 3000 workingDir: string;
): Promise<[number, string, ChildProcess, string[]]> => { process: ChildProcess;
log.info(`Using external goosed backend on port ${port}`); errorLog: string[];
}
const connectToExternalBackend = (workingDir: string, url: string): GoosedResult => {
log.info(`Using external goosed backend at ${url}`);
const mockProcess = { const mockProcess = {
pid: undefined, pid: undefined,
@@ -66,7 +71,7 @@ const connectToExternalBackend = async (
}, },
} as ChildProcess; } as ChildProcess;
return [port, workingDir, mockProcess, []]; return { baseUrl: url, workingDir, process: mockProcess, errorLog: [] };
}; };
interface GooseProcessEnv { interface GooseProcessEnv {
@@ -81,18 +86,26 @@ interface GooseProcessEnv {
GOOSE_SERVER__SECRET_KEY?: string; GOOSE_SERVER__SECRET_KEY?: string;
} }
export const startGoosed = async ( export interface StartGoosedOptions {
app: App, app: App;
serverSecret: string, serverSecret: string;
dir: string, dir: string;
env: Partial<GooseProcessEnv> = {} env?: Partial<GooseProcessEnv>;
): Promise<[number, string, ChildProcess, string[]]> => { externalGoosed?: ExternalGoosedConfig;
}
export const startGoosed = async (options: StartGoosedOptions): Promise<GoosedResult> => {
const { app, serverSecret, dir: inputDir, env = {}, externalGoosed } = options;
const isWindows = process.platform === 'win32'; const isWindows = process.platform === 'win32';
const homeDir = os.homedir(); 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) { if (process.env.GOOSE_EXTERNAL_BACKEND) {
return connectToExternalBackend(dir, 3000); return connectToExternalBackend(dir, 'http://127.0.0.1:3000');
} }
let goosedPath = getGoosedBinaryPath(app); let goosedPath = getGoosedBinaryPath(app);
@@ -105,25 +118,18 @@ export const startGoosed = async (
log.info(`Starting goosed from: ${resolvedGoosedPath} on port ${port} in dir ${dir}`); log.info(`Starting goosed from: ${resolvedGoosedPath} on port ${port} in dir ${dir}`);
const additionalEnv: GooseProcessEnv = { const additionalEnv: GooseProcessEnv = {
// Set HOME for UNIX-like systems
HOME: homeDir, HOME: homeDir,
// Set USERPROFILE for Windows
USERPROFILE: homeDir, USERPROFILE: homeDir,
// Set APPDATA for Windows
APPDATA: process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming'), APPDATA: process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming'),
// Set LOCAL_APPDATA for Windows
LOCALAPPDATA: process.env.LOCALAPPDATA || path.join(homeDir, 'AppData', 'Local'), 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 || ''}`, PATH: `${path.dirname(resolvedGoosedPath)}${path.delimiter}${process.env.PATH || ''}`,
GOOSE_PORT: String(port), GOOSE_PORT: String(port),
GOOSE_SERVER__SECRET_KEY: serverSecret, GOOSE_SERVER__SECRET_KEY: serverSecret,
// Add any additional environment variables passed in
...env, ...env,
} as GooseProcessEnv; } as GooseProcessEnv;
const processEnv: GooseProcessEnv = { ...process.env, ...additionalEnv } as GooseProcessEnv; const processEnv: GooseProcessEnv = { ...process.env, ...additionalEnv } as GooseProcessEnv;
// Ensure proper executable path on Windows
if (isWindows && !resolvedGoosedPath.toLowerCase().endsWith('.exe')) { if (isWindows && !resolvedGoosedPath.toLowerCase().endsWith('.exe')) {
goosedPath = resolvedGoosedPath + '.exe'; goosedPath = resolvedGoosedPath + '.exe';
} else { } else {
@@ -135,15 +141,11 @@ export const startGoosed = async (
cwd: dir, cwd: dir,
env: processEnv, env: processEnv,
stdio: ['ignore', 'pipe', 'pipe'] as ['ignore', 'pipe', 'pipe'], stdio: ['ignore', 'pipe', 'pipe'] as ['ignore', 'pipe', 'pipe'],
// Hide terminal window on Windows
windowsHide: true, windowsHide: true,
// Run detached on Windows only to avoid terminal windows
detached: isWindows, detached: isWindows,
// Never use shell to avoid command injection - this is critical for security
shell: false, shell: false,
}; };
// Log spawn options for debugging (excluding sensitive env vars)
const safeSpawnOptions = { const safeSpawnOptions = {
...spawnOptions, ...spawnOptions,
env: Object.keys(spawnOptions.env || {}).reduce( env: Object.keys(spawnOptions.env || {}).reduce(
@@ -160,12 +162,10 @@ export const startGoosed = async (
}; };
log.info('Spawn options:', JSON.stringify(safeSpawnOptions, null, 2)); log.info('Spawn options:', JSON.stringify(safeSpawnOptions, null, 2));
// Security: Use only hardcoded, safe arguments
const safeArgs = ['agent']; const safeArgs = ['agent'];
const goosedProcess: ChildProcess = spawn(goosedPath, safeArgs, spawnOptions); const goosedProcess: ChildProcess = spawn(goosedPath, safeArgs, spawnOptions);
// Only unref on Windows to allow it to run independently of the parent
if (isWindows && goosedProcess.unref) { if (isWindows && goosedProcess.unref) {
goosedProcess.unref(); goosedProcess.unref();
} }
@@ -191,7 +191,7 @@ export const startGoosed = async (
goosedProcess.on('error', (err: Error) => { goosedProcess.on('error', (err: Error) => {
log.error(`Failed to start goosed on port ${port} and dir ${dir}`, err); log.error(`Failed to start goosed on port ${port} and dir ${dir}`, err);
throw err; // Propagate the error throw err;
}); });
const try_kill_goose = () => { const try_kill_goose = () => {
@@ -207,14 +207,18 @@ export const startGoosed = async (
} }
}; };
// Ensure goosed is terminated when the app quits
app.on('will-quit', () => { app.on('will-quit', () => {
log.info('App quitting, terminating goosed server'); log.info('App quitting, terminating goosed server');
try_kill_goose(); try_kill_goose();
}); });
log.info(`Goosed server successfully started on port ${port}`); 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 => { const getGoosedBinaryPath = (app: Electron.App): string => {
+96 -39
View File
@@ -466,16 +466,23 @@ const getBundledConfig = (): BundledConfig => {
const { defaultProvider, defaultModel, predefinedModels, baseUrlShare, version } = const { defaultProvider, defaultModel, predefinedModels, baseUrlShare, version } =
getBundledConfig(); getBundledConfig();
const SERVER_SECRET = process.env.GOOSE_EXTERNAL_BACKEND const GENERATED_SECRET = crypto.randomBytes(32).toString('hex');
? 'test'
: 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 = { let appConfig = {
GOOSE_DEFAULT_PROVIDER: defaultProvider, GOOSE_DEFAULT_PROVIDER: defaultProvider,
GOOSE_DEFAULT_MODEL: defaultModel, GOOSE_DEFAULT_MODEL: defaultModel,
GOOSE_PREDEFINED_MODELS: predefinedModels, GOOSE_PREDEFINED_MODELS: predefinedModels,
GOOSE_API_HOST: 'http://127.0.0.1', GOOSE_API_HOST: 'http://127.0.0.1',
GOOSE_PORT: 0,
GOOSE_WORKING_DIR: '', GOOSE_WORKING_DIR: '',
// If GOOSE_ALLOWLIST_WARNING env var is not set, defaults to false (strict blocking mode) // If GOOSE_ALLOWLIST_WARNING env var is not set, defaults to false (strict blocking mode)
GOOSE_ALLOWLIST_WARNING: process.env.GOOSE_ALLOWLIST_WARNING === 'true', GOOSE_ALLOWLIST_WARNING: process.env.GOOSE_ALLOWLIST_WARNING === 'true',
@@ -503,15 +510,18 @@ const createChat = async (
) => { ) => {
updateEnvironmentVariables(envToggles); updateEnvironmentVariables(envToggles);
const envVars = { const settings = loadSettings();
GOOSE_PATH_ROOT: process.env.GOOSE_PATH_ROOT, const serverSecret = getServerSecret(settings);
};
const [port, workingDir, goosedProcess, errorLog] = await startGoosed( const goosedResult = await startGoosed({
app, app,
SERVER_SECRET, serverSecret,
dir || os.homedir(), dir: dir || os.homedir(),
envVars env: { GOOSE_PATH_ROOT: process.env.GOOSE_PATH_ROOT },
); externalGoosed: settings.externalGoosed,
});
const { baseUrl, workingDir, process: goosedProcess, errorLog } = goosedResult;
const mainWindowState = windowStateKeeper({ const mainWindowState = windowStateKeeper({
defaultWidth: 940, defaultWidth: 940,
@@ -534,14 +544,13 @@ const createChat = async (
webPreferences: { webPreferences: {
spellcheck: true, spellcheck: true,
preload: path.join(__dirname, 'preload.js'), preload: path.join(__dirname, 'preload.js'),
// Enable features needed for Web Speech API
webSecurity: true, webSecurity: true,
nodeIntegration: false, nodeIntegration: false,
contextIsolation: true, contextIsolation: true,
additionalArguments: [ additionalArguments: [
JSON.stringify({ JSON.stringify({
...appConfig, ...appConfig,
GOOSE_PORT: port, GOOSE_API_HOST: baseUrl,
GOOSE_WORKING_DIR: workingDir, GOOSE_WORKING_DIR: workingDir,
REQUEST_DIR: dir, REQUEST_DIR: dir,
GOOSE_BASE_URL_SHARE: baseUrlShare, GOOSE_BASE_URL_SHARE: baseUrlShare,
@@ -552,7 +561,7 @@ const createChat = async (
scheduledJobId: scheduledJobId, scheduledJobId: scheduledJobId,
}), }),
], ],
partition: 'persist:goose', // Add this line to ensure persistence partition: 'persist:goose',
}, },
}); });
@@ -567,10 +576,10 @@ const createChat = async (
const goosedClient = createClient( const goosedClient = createClient(
createConfig({ createConfig({
baseUrl: `http://127.0.0.1:${port}`, baseUrl,
headers: { headers: {
'Content-Type': 'application/json', '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); const serverReady = await checkServerStatus(goosedClient, errorLog);
if (!serverReady) { if (!serverReady) {
dialog.showMessageBoxSync({ const isUsingExternalBackend = settings.externalGoosed?.enabled;
type: 'error',
title: 'Goose Failed to Start', if (isUsingExternalBackend) {
message: 'The backend server failed to start.', const response = dialog.showMessageBoxSync({
detail: errorLog.join('\n'), type: 'error',
buttons: ['OK'], 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(); 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', () => { ipcMain.handle('get-secret-key', () => {
return SERVER_SECRET; const settings = loadSettings();
return getServerSecret(settings);
}); });
ipcMain.handle('get-goosed-host-port', async (event) => { 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 // Add CSP headers to all sessions
session.defaultSession.webRequest.onHeadersReceived((details, callback) => { session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
callback({ callback({
@@ -1770,31 +1840,18 @@ async function appMain() {
...details.responseHeaders, ...details.responseHeaders,
'Content-Security-Policy': 'Content-Security-Policy':
"default-src 'self';" + "default-src 'self';" +
// Allow inline styles since we use them in our React components
"style-src 'self' 'unsafe-inline';" + "style-src 'self' 'unsafe-inline';" +
// Scripts from our app and inline scripts (for theme initialization)
"script-src 'self' 'unsafe-inline';" + "script-src 'self' 'unsafe-inline';" +
// Images from our app and data: URLs (for base64 images)
"img-src 'self' data: https:;" + "img-src 'self' data: https:;" +
// Connect to our local API and specific external services `connect-src ${buildConnectSrc()};` +
"connect-src 'self' http://127.0.0.1:* https://api.github.com https://github.com https://objects.githubusercontent.com" +
// Don't allow any plugins
"object-src 'none';" + "object-src 'none';" +
// Allow all frames (iframes)
"frame-src 'self' https: http:;" + "frame-src 'self' https: http:;" +
// Font sources - allow self, data URLs, and external fonts
"font-src 'self' data: https:;" + "font-src 'self' data: https:;" +
// Media sources - allow microphone
"media-src 'self' mediastream:;" + "media-src 'self' mediastream:;" +
// Form actions
"form-action 'none';" + "form-action 'none';" +
// Base URI restriction
"base-uri 'self';" + "base-uri 'self';" +
// Manifest files
"manifest-src 'self';" + "manifest-src 'self';" +
// Worker sources
"worker-src 'self';" + "worker-src 'self';" +
// Upgrade insecure requests
'upgrade-insecure-requests;', 'upgrade-insecure-requests;',
}, },
}); });
+2
View File
@@ -75,6 +75,7 @@ type ElectronAPI = {
setDockIcon: (show: boolean) => Promise<boolean>; setDockIcon: (show: boolean) => Promise<boolean>;
getDockIconState: () => Promise<boolean>; getDockIconState: () => Promise<boolean>;
getSettings: () => Promise<unknown | null>; getSettings: () => Promise<unknown | null>;
saveSettings: (settings: unknown) => Promise<boolean>;
getSecretKey: () => Promise<string>; getSecretKey: () => Promise<string>;
getGoosedHostPort: () => Promise<string | null>; getGoosedHostPort: () => Promise<string | null>;
setWakelock: (enable: boolean) => Promise<boolean>; setWakelock: (enable: boolean) => Promise<boolean>;
@@ -177,6 +178,7 @@ const electronAPI: ElectronAPI = {
setDockIcon: (show: boolean) => ipcRenderer.invoke('set-dock-icon', show), setDockIcon: (show: boolean) => ipcRenderer.invoke('set-dock-icon', show),
getDockIconState: () => ipcRenderer.invoke('get-dock-icon-state'), getDockIconState: () => ipcRenderer.invoke('get-dock-icon-state'),
getSettings: () => ipcRenderer.invoke('get-settings'), getSettings: () => ipcRenderer.invoke('get-settings'),
saveSettings: (settings: unknown) => ipcRenderer.invoke('save-settings', settings),
getSecretKey: () => ipcRenderer.invoke('get-secret-key'), getSecretKey: () => ipcRenderer.invoke('get-secret-key'),
getGoosedHostPort: () => ipcRenderer.invoke('get-goosed-host-port'), getGoosedHostPort: () => ipcRenderer.invoke('get-goosed-host-port'),
setWakelock: (enable: boolean) => ipcRenderer.invoke('set-wakelock', enable), setWakelock: (enable: boolean) => ipcRenderer.invoke('set-wakelock', enable),
+4 -4
View File
@@ -13,14 +13,14 @@ const App = lazy(() => import('./App'));
if (!isLauncher) { if (!isLauncher) {
console.log('window created, getting goosed connection info'); console.log('window created, getting goosed connection info');
const baseUrl = await window.electron.getGoosedHostPort(); const gooseApiHost = await window.electron.getGoosedHostPort();
if (baseUrl === null) { if (gooseApiHost === null) {
window.alert('failed to start goose backend process'); window.alert('failed to start goose backend process');
return; return;
} }
console.log('connecting at', baseUrl); console.log('connecting at', gooseApiHost);
client.setConfig({ client.setConfig({
baseUrl, baseUrl: gooseApiHost,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-Secret-Key': await window.electron.getSecretKey(), 'X-Secret-Key': await window.electron.getSecretKey(),
+7
View File
@@ -7,11 +7,18 @@ export interface EnvToggles {
GOOSE_SERVER__COMPUTER_CONTROLLER: boolean; GOOSE_SERVER__COMPUTER_CONTROLLER: boolean;
} }
export interface ExternalGoosedConfig {
enabled: boolean;
url: string;
secret: string;
}
export interface Settings { export interface Settings {
envToggles: EnvToggles; envToggles: EnvToggles;
showMenuBarIcon: boolean; showMenuBarIcon: boolean;
showDockIcon: boolean; showDockIcon: boolean;
enableWakelock: boolean; enableWakelock: boolean;
externalGoosed?: ExternalGoosedConfig;
} }
const SETTINGS_FILE = path.join(app.getPath('userData'), 'settings.json'); const SETTINGS_FILE = path.join(app.getPath('userData'), 'settings.json');