feat(settings): add UI for sandbox execution backend configuration (#3265)

This commit is contained in:
Steven Enamakel
2026-06-03 00:43:42 -04:00
committed by GitHub
parent 3eeb6ea8c6
commit 2bb692d9af
25 changed files with 1321 additions and 14 deletions
@@ -47,6 +47,7 @@ export type SettingsRoute =
| 'composio-routing'
| 'mcp-server'
| 'dev-workflow'
| 'sandbox-settings'
| 'devices';
export interface BreadcrumbItem {
@@ -137,6 +138,7 @@ export const useSettingsNavigation = (): SettingsNavigationHook => {
// `agents-settings` (the Agents section page) must be checked before the
// shorter `agents` (the manage-agents registry panel) so it isn't swallowed.
if (path.includes('/settings/agents-settings')) return 'agents-settings';
if (path.includes('/settings/sandbox-settings')) return 'sandbox-settings';
if (path.includes('/settings/agent-access')) return 'agent-access';
if (path.includes('/settings/agents')) return 'agents';
if (path.includes('/settings/mcp-server')) return 'mcp-server';
@@ -230,6 +232,7 @@ export const useSettingsNavigation = (): SettingsNavigationHook => {
// Leaf panels under the Agents section
case 'agents':
case 'agent-access':
case 'sandbox-settings':
case 'autonomy':
case 'persona':
return [settingsCrumb, agentsCrumb];
@@ -0,0 +1,362 @@
import { useEffect, useRef, useState } from 'react';
import { useT } from '../../../lib/i18n/I18nContext';
import {
isTauri,
openhumanGetSandboxSettings,
openhumanUpdateSandboxSettings,
type SandboxBackendId,
} from '../../../utils/tauriCommands';
import SettingsHeader from '../components/SettingsHeader';
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
const BACKEND_OPTIONS: SandboxBackendId[] = [
'auto',
'docker',
'landlock',
'firejail',
'bubblewrap',
'none',
];
const SandboxSettingsPanel = () => {
const { t } = useT();
const { navigateBack, breadcrumbs } = useSettingsNavigation();
const [isLoading, setIsLoading] = useState(isTauri());
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [savedNote, setSavedNote] = useState<string | null>(null);
const [enabled, setEnabled] = useState(true);
const [backend, setBackend] = useState<SandboxBackendId>('auto');
const [dockerImage, setDockerImage] = useState('alpine:3.20');
const [memoryLimitMb, setMemoryLimitMb] = useState('512');
const [cpuLimit, setCpuLimit] = useState('1.0');
const [dockerAvailable, setDockerAvailable] = useState(false);
const [detectedBackend, setDetectedBackend] = useState('');
const [envPassthrough, setEnvPassthrough] = useState<string[]>([]);
const persistSeqRef = useRef(0);
useEffect(() => {
let cancelled = false;
const load = async () => {
if (!isTauri()) return;
try {
const resp = await openhumanGetSandboxSettings();
if (cancelled) return;
const s = resp.result;
setEnabled(s.enabled);
setBackend(s.backend);
setDockerImage(s.docker_image);
setMemoryLimitMb(s.docker_memory_limit_mb != null ? String(s.docker_memory_limit_mb) : '');
setCpuLimit(s.docker_cpu_limit != null ? String(s.docker_cpu_limit) : '');
setDockerAvailable(s.docker_available);
setDetectedBackend(s.detected_backend);
setEnvPassthrough(s.env_passthrough);
} catch (e) {
if (!cancelled) setError(e instanceof Error ? e.message : t('settings.sandbox.loadError'));
} finally {
if (!cancelled) setIsLoading(false);
}
};
void load();
return () => {
cancelled = true;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const persist = async (patch: Parameters<typeof openhumanUpdateSandboxSettings>[0]) => {
const seq = ++persistSeqRef.current;
if (!isTauri()) return;
setError(null);
setSavedNote(null);
setIsSaving(true);
try {
await openhumanUpdateSandboxSettings(patch);
if (seq !== persistSeqRef.current) return;
setSavedNote(t('settings.sandbox.saved'));
} catch (e) {
if (seq !== persistSeqRef.current) return;
setError(e instanceof Error ? e.message : t('settings.sandbox.saveError'));
} finally {
if (seq === persistSeqRef.current) setIsSaving(false);
}
};
const handleBackendChange = (next: SandboxBackendId) => {
setBackend(next);
void persist({ backend: next });
};
const handleEnabledChange = (next: boolean) => {
setEnabled(next);
void persist({ enabled: next });
};
const handleDockerImageBlur = () => {
if (dockerImage.trim()) {
void persist({ docker_image: dockerImage.trim() });
}
};
const handleMemoryBlur = () => {
if (memoryLimitMb.trim() === '') {
void persist({ docker_memory_limit_mb: null });
return;
}
const parsed = parseInt(memoryLimitMb, 10);
if (!isNaN(parsed) && parsed > 0) {
void persist({ docker_memory_limit_mb: parsed });
}
};
const handleCpuBlur = () => {
if (cpuLimit.trim() === '') {
void persist({ docker_cpu_limit: null });
return;
}
const parsed = parseFloat(cpuLimit);
if (!isNaN(parsed) && parsed > 0) {
void persist({ docker_cpu_limit: parsed });
}
};
if (!isTauri()) {
return (
<div className="mx-auto max-w-2xl px-4 py-8">
<SettingsHeader
title={t('settings.sandbox.title')}
onBack={navigateBack}
breadcrumbs={breadcrumbs}
/>
<p className="text-sm text-stone-500 dark:text-stone-400">
{t('settings.sandbox.desktopOnly')}
</p>
</div>
);
}
if (isLoading) {
return (
<div className="mx-auto max-w-2xl px-4 py-8">
<SettingsHeader
title={t('settings.sandbox.title')}
onBack={navigateBack}
breadcrumbs={breadcrumbs}
/>
<p className="text-sm text-stone-500 dark:text-stone-400">
{t('settings.sandbox.loading')}
</p>
</div>
);
}
return (
<div className="mx-auto max-w-2xl px-4 py-8">
<SettingsHeader
title={t('settings.sandbox.title')}
onBack={navigateBack}
breadcrumbs={breadcrumbs}
/>
{error && (
<div
className="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-800 dark:bg-red-950 dark:text-red-300"
role="alert">
{error}
</div>
)}
{savedNote && (
<p className="mb-4 text-sm text-green-600 dark:text-green-400" aria-live="polite">
{savedNote}
</p>
)}
{/* Status */}
<section className="mb-6">
<h2 className="mb-2 text-sm font-semibold text-stone-700 dark:text-stone-200">
{t('settings.sandbox.status')}
</h2>
<div className="space-y-2 rounded-lg border border-stone-200 bg-stone-50 p-4 dark:border-stone-700 dark:bg-stone-800">
<div className="flex items-center justify-between">
<span className="text-sm text-stone-600 dark:text-stone-300">
{t('settings.sandbox.dockerStatus')}
</span>
<span
className={`rounded-full px-2 py-0.5 text-xs font-medium ${
dockerAvailable
? 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300'
: 'bg-stone-200 text-stone-600 dark:bg-stone-700 dark:text-stone-400'
}`}>
{dockerAvailable
? t('settings.sandbox.available')
: t('settings.sandbox.unavailable')}
</span>
</div>
{detectedBackend && (
<div className="flex items-center justify-between">
<span className="text-sm text-stone-600 dark:text-stone-300">
{t('settings.sandbox.detectedBackend')}
</span>
<span className="text-sm font-mono text-stone-700 dark:text-stone-200">
{detectedBackend}
</span>
</div>
)}
</div>
</section>
{/* Enabled toggle */}
<section className="mb-6">
<label className="flex items-center gap-3">
<input
type="checkbox"
checked={enabled}
onChange={e => handleEnabledChange(e.target.checked)}
className="h-4 w-4 rounded border-stone-300 dark:border-stone-600"
aria-label={t('settings.sandbox.enableLabel')}
/>
<div>
<span className="text-sm font-medium text-stone-700 dark:text-stone-200">
{t('settings.sandbox.enableLabel')}
</span>
<p className="text-xs text-stone-500 dark:text-stone-400">
{t('settings.sandbox.enableDesc')}
</p>
</div>
</label>
</section>
{/* Backend selection */}
<section className="mb-6">
<h2 className="mb-2 text-sm font-semibold text-stone-700 dark:text-stone-200">
{t('settings.sandbox.backendLabel')}
</h2>
<p className="mb-2 text-xs text-stone-500 dark:text-stone-400">
{t('settings.sandbox.backendDesc')}
</p>
<select
value={backend}
onChange={e => handleBackendChange(e.target.value as SandboxBackendId)}
className="w-full rounded-lg border border-stone-300 bg-white px-3 py-2 text-sm text-stone-700 dark:border-stone-600 dark:bg-stone-800 dark:text-stone-200"
aria-label={t('settings.sandbox.backendLabel')}>
{BACKEND_OPTIONS.map(opt => (
<option key={opt} value={opt}>
{t(`settings.sandbox.backend.${opt}`)}
</option>
))}
</select>
</section>
{/* Docker settings */}
<section className="mb-6">
<h2 className="mb-2 text-sm font-semibold text-stone-700 dark:text-stone-200">
{t('settings.sandbox.dockerSettings')}
</h2>
<div className="space-y-4 rounded-lg border border-stone-200 p-4 dark:border-stone-700">
{/* Docker image */}
<div>
<label className="mb-1 block text-xs font-medium text-stone-600 dark:text-stone-300">
{t('settings.sandbox.dockerImage')}
</label>
<input
type="text"
value={dockerImage}
onChange={e => setDockerImage(e.target.value)}
onBlur={handleDockerImageBlur}
onKeyDown={e => e.key === 'Enter' && handleDockerImageBlur()}
className="w-full rounded-lg border border-stone-300 bg-white px-3 py-2 font-mono text-sm text-stone-700 dark:border-stone-600 dark:bg-stone-800 dark:text-stone-200"
aria-label={t('settings.sandbox.dockerImage')}
placeholder={t('settings.sandbox.dockerImagePlaceholder')}
/>
</div>
{/* Memory limit */}
<div>
<label className="mb-1 block text-xs font-medium text-stone-600 dark:text-stone-300">
{t('settings.sandbox.memoryLimit')}
</label>
<div className="flex items-center gap-2">
<input
type="number"
value={memoryLimitMb}
onChange={e => setMemoryLimitMb(e.target.value)}
onBlur={handleMemoryBlur}
onKeyDown={e => e.key === 'Enter' && handleMemoryBlur()}
className="w-32 rounded-lg border border-stone-300 bg-white px-3 py-2 text-sm text-stone-700 dark:border-stone-600 dark:bg-stone-800 dark:text-stone-200"
aria-label={t('settings.sandbox.memoryLimit')}
min={64}
/>
<span className="text-xs text-stone-500 dark:text-stone-400">
{t('settings.sandbox.memoryUnit')}
</span>
</div>
</div>
{/* CPU limit */}
<div>
<label className="mb-1 block text-xs font-medium text-stone-600 dark:text-stone-300">
{t('settings.sandbox.cpuLimit')}
</label>
<div className="flex items-center gap-2">
<input
type="number"
value={cpuLimit}
onChange={e => setCpuLimit(e.target.value)}
onBlur={handleCpuBlur}
onKeyDown={e => e.key === 'Enter' && handleCpuBlur()}
className="w-32 rounded-lg border border-stone-300 bg-white px-3 py-2 text-sm text-stone-700 dark:border-stone-600 dark:bg-stone-800 dark:text-stone-200"
aria-label={t('settings.sandbox.cpuLimit')}
min={0.1}
step={0.1}
/>
<span className="text-xs text-stone-500 dark:text-stone-400">
{t('settings.sandbox.cpuUnit')}
</span>
</div>
</div>
</div>
</section>
{/* Environment passthrough */}
<section className="mb-6">
<h2 className="mb-2 text-sm font-semibold text-stone-700 dark:text-stone-200">
{t('settings.sandbox.envPassthrough')}
</h2>
<p className="mb-2 text-xs text-stone-500 dark:text-stone-400">
{t('settings.sandbox.envPassthroughDesc')}
</p>
<div className="rounded-lg border border-stone-200 bg-stone-50 p-3 dark:border-stone-700 dark:bg-stone-800">
{envPassthrough.length > 0 ? (
<div className="flex flex-wrap gap-2">
{envPassthrough.map(v => (
<span
key={v}
className="rounded-md bg-stone-200 px-2 py-0.5 font-mono text-xs text-stone-700 dark:bg-stone-700 dark:text-stone-300">
{v}
</span>
))}
</div>
) : (
<p className="text-xs text-stone-400 dark:text-stone-500">
{t('settings.sandbox.noEnvVars')}
</p>
)}
</div>
</section>
{/* Saving indicator */}
{isSaving && (
<p className="text-xs text-stone-500 dark:text-stone-400" aria-live="polite">
{t('settings.sandbox.saving')}
</p>
)}
</div>
);
};
export default SandboxSettingsPanel;
@@ -0,0 +1,147 @@
import { fireEvent, screen, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { renderWithProviders } from '../../../../test/test-utils';
import {
isTauri,
openhumanGetSandboxSettings,
openhumanUpdateSandboxSettings,
type SandboxSettings,
} from '../../../../utils/tauriCommands';
import SandboxSettingsPanel from '../SandboxSettingsPanel';
const sandboxSettings = (overrides: Partial<SandboxSettings> = {}): SandboxSettings => ({
enabled: true,
backend: 'auto',
docker_image: 'alpine:3.20',
docker_memory_limit_mb: 512,
docker_cpu_limit: 1.0,
docker_available: true,
detected_backend: 'seatbelt',
env_passthrough: ['PATH', 'HOME', 'TERM'],
...overrides,
});
vi.mock('../../hooks/useSettingsNavigation', () => ({
useSettingsNavigation: () => ({
navigateBack: vi.fn(),
navigateToSettings: vi.fn(),
breadcrumbs: [],
}),
}));
vi.mock('../../../../utils/tauriCommands', async () => {
const actual = await vi.importActual<typeof import('../../../../utils/tauriCommands')>(
'../../../../utils/tauriCommands'
);
return {
...actual,
isTauri: vi.fn(() => true),
openhumanGetSandboxSettings: vi.fn(),
openhumanUpdateSandboxSettings: vi.fn(),
};
});
const mockGet = vi.mocked(openhumanGetSandboxSettings);
const mockUpdate = vi.mocked(openhumanUpdateSandboxSettings);
describe('SandboxSettingsPanel', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(isTauri).mockReturnValue(true);
mockGet.mockResolvedValue({ result: sandboxSettings(), logs: [] });
mockUpdate.mockResolvedValue({ result: {} as never, logs: [] });
});
it('loads settings on mount and renders status section', async () => {
renderWithProviders(<SandboxSettingsPanel />);
await waitFor(() => expect(mockGet).toHaveBeenCalledTimes(1));
expect(await screen.findByText('Available')).toBeInTheDocument();
expect(screen.getByText('seatbelt')).toBeInTheDocument();
});
it('shows unavailable status when Docker is not available', async () => {
mockGet.mockResolvedValue({ result: sandboxSettings({ docker_available: false }), logs: [] });
renderWithProviders(<SandboxSettingsPanel />);
expect(await screen.findByText('Unavailable')).toBeInTheDocument();
});
it('renders the backend dropdown with current selection', async () => {
renderWithProviders(<SandboxSettingsPanel />);
await waitFor(() => expect(mockGet).toHaveBeenCalled());
const select = await screen.findByRole('combobox', { name: /backend/i });
expect(select).toHaveValue('auto');
});
it('changing backend persists the selection', async () => {
renderWithProviders(<SandboxSettingsPanel />);
const select = await screen.findByRole('combobox', { name: /backend/i });
fireEvent.change(select, { target: { value: 'docker' } });
await waitFor(() =>
expect(mockUpdate).toHaveBeenCalledWith(expect.objectContaining({ backend: 'docker' }))
);
});
it('toggling the enabled checkbox persists the change', async () => {
renderWithProviders(<SandboxSettingsPanel />);
await waitFor(() => expect(mockGet).toHaveBeenCalled());
const checkbox = await screen.findByRole('checkbox', { name: /enable sandbox/i });
fireEvent.click(checkbox);
await waitFor(() =>
expect(mockUpdate).toHaveBeenCalledWith(expect.objectContaining({ enabled: false }))
);
});
it('renders Docker image input with current value', async () => {
renderWithProviders(<SandboxSettingsPanel />);
const input = await screen.findByRole('textbox', { name: /image/i });
expect(input).toHaveValue('alpine:3.20');
});
it('blurring the Docker image input persists the value', async () => {
renderWithProviders(<SandboxSettingsPanel />);
const input = await screen.findByRole('textbox', { name: /image/i });
fireEvent.change(input, { target: { value: 'node:20-slim' } });
fireEvent.blur(input);
await waitFor(() =>
expect(mockUpdate).toHaveBeenCalledWith(
expect.objectContaining({ docker_image: 'node:20-slim' })
)
);
});
it('renders env passthrough variables as tags', async () => {
renderWithProviders(<SandboxSettingsPanel />);
expect(await screen.findByText('PATH')).toBeInTheDocument();
expect(screen.getByText('HOME')).toBeInTheDocument();
expect(screen.getByText('TERM')).toBeInTheDocument();
});
it('shows desktop-only message when not in Tauri', async () => {
vi.mocked(isTauri).mockReturnValue(false);
renderWithProviders(<SandboxSettingsPanel />);
expect(await screen.findByText(/sandbox settings are only available/i)).toBeInTheDocument();
expect(mockGet).not.toHaveBeenCalled();
});
it('shows error when settings fail to load', async () => {
mockGet.mockRejectedValue(new Error('RPC timeout'));
renderWithProviders(<SandboxSettingsPanel />);
expect(await screen.findByText('RPC timeout')).toBeInTheDocument();
});
it('shows saved note after successful persist', async () => {
renderWithProviders(<SandboxSettingsPanel />);
const select = await screen.findByRole('combobox', { name: /backend/i });
fireEvent.change(select, { target: { value: 'none' } });
expect(await screen.findByText(/applies to new agent sessions/i)).toBeInTheDocument();
});
it('shows error note when persist fails', async () => {
mockUpdate.mockRejectedValue(new Error('Save failed'));
renderWithProviders(<SandboxSettingsPanel />);
const select = await screen.findByRole('combobox', { name: /backend/i });
fireEvent.change(select, { target: { value: 'docker' } });
expect(await screen.findByText('Save failed')).toBeInTheDocument();
});
});
+34 -1
View File
@@ -3600,7 +3600,40 @@ const messages: TranslationMap = {
'settings.agentAccess.approvalHistory': 'Approval history',
'settings.agentAccess.approvalHistoryDesc':
'Review past Approve / Deny decisions the agent requested.',
'settings.agentAccess.viewApprovalHistory': 'View approval history',
'settings.agentAccess.viewApprovalHistory': 'عرض سجل الموافقات',
'settings.sandbox.title': 'تنفيذ Sandbox',
'settings.sandbox.menuDesc': 'تكوين خلفيات Sandbox لعزل أدوات الوكيل.',
'settings.sandbox.loading': 'جارٍ التحميل…',
'settings.sandbox.desktopOnly': 'إعدادات Sandbox متاحة فقط في تطبيق سطح المكتب.',
'settings.sandbox.loadError': 'فشل تحميل إعدادات Sandbox.',
'settings.sandbox.saveError': 'فشل حفظ إعدادات Sandbox.',
'settings.sandbox.saved': 'تم الحفظ — يُطبَّق على جلسات الوكيل الجديدة.',
'settings.sandbox.saving': 'جارٍ الحفظ…',
'settings.sandbox.status': 'الحالة',
'settings.sandbox.dockerStatus': 'Docker',
'settings.sandbox.available': 'متاح',
'settings.sandbox.unavailable': 'غير متاح',
'settings.sandbox.detectedBackend': 'الخلفية التشغيلية',
'settings.sandbox.enableLabel': 'تفعيل تنفيذ Sandbox',
'settings.sandbox.enableDesc': 'تشغيل أدوات الوكيل داخل بيئة Sandbox معزولة.',
'settings.sandbox.backendLabel': 'الخلفية',
'settings.sandbox.backendDesc': 'اختر خلفية العزل المستخدمة للتنفيذ داخل Sandbox.',
'settings.sandbox.backend.auto': 'تلقائي (اكتشاف الأفضل المتاح)',
'settings.sandbox.backend.docker': 'Docker',
'settings.sandbox.backend.landlock': 'Landlock (Linux)',
'settings.sandbox.backend.firejail': 'Firejail (Linux)',
'settings.sandbox.backend.bubblewrap': 'Bubblewrap (Linux)',
'settings.sandbox.backend.none': 'بدون (لا يوجد Sandbox)',
'settings.sandbox.dockerSettings': 'إعدادات Docker',
'settings.sandbox.dockerImage': 'الصورة',
'settings.sandbox.dockerImagePlaceholder': 'alpine:3.20',
'settings.sandbox.memoryLimit': 'حد الذاكرة',
'settings.sandbox.memoryUnit': 'MB',
'settings.sandbox.cpuLimit': 'حد المعالج',
'settings.sandbox.cpuUnit': 'نوى',
'settings.sandbox.envPassthrough': 'تمرير متغيرات البيئة',
'settings.sandbox.envPassthroughDesc': 'متغيرات البيئة المُمرَّرة إلى داخل Sandbox.',
'settings.sandbox.noEnvVars': 'لم يتم تكوين أي متغيرات بيئة.',
'settings.approvalHistory.title': 'Approval history',
'settings.approvalHistory.subtitle': 'Recent tool-approval decisions, newest first.',
'settings.approvalHistory.refresh': 'Refresh',
+35 -1
View File
@@ -3665,7 +3665,41 @@ const messages: TranslationMap = {
'settings.agentAccess.approvalHistory': 'Approval history',
'settings.agentAccess.approvalHistoryDesc':
'Review past Approve / Deny decisions the agent requested.',
'settings.agentAccess.viewApprovalHistory': 'View approval history',
'settings.agentAccess.viewApprovalHistory': 'অনুমোদনের ইতিহাস দেখুন',
'settings.sandbox.title': 'Sandbox কার্যকরী',
'settings.sandbox.menuDesc': 'এজেন্ট টুল আইসোলেশনের জন্য Sandbox ব্যাকএন্ড কনফিগার করুন।',
'settings.sandbox.loading': 'লোড হচ্ছে…',
'settings.sandbox.desktopOnly': 'Sandbox সেটিংস শুধুমাত্র ডেস্কটপ অ্যাপে পাওয়া যায়।',
'settings.sandbox.loadError': 'Sandbox সেটিংস লোড করতে ব্যর্থ।',
'settings.sandbox.saveError': 'Sandbox সেটিংস সংরক্ষণ করতে ব্যর্থ।',
'settings.sandbox.saved': 'সংরক্ষিত — নতুন এজেন্ট সেশনে প্রযোজ্য।',
'settings.sandbox.saving': 'সংরক্ষণ হচ্ছে…',
'settings.sandbox.status': 'অবস্থা',
'settings.sandbox.dockerStatus': 'Docker',
'settings.sandbox.available': 'উপলব্ধ',
'settings.sandbox.unavailable': 'অনুপলব্ধ',
'settings.sandbox.detectedBackend': 'OS ব্যাকএন্ড',
'settings.sandbox.enableLabel': 'Sandbox কার্যকরী সক্ষম করুন',
'settings.sandbox.enableDesc': 'একটি বিচ্ছিন্ন Sandbox পরিবেশের ভিতরে এজেন্ট টুল চালান।',
'settings.sandbox.backendLabel': 'ব্যাকএন্ড',
'settings.sandbox.backendDesc':
'Sandbox কার্যকরীর জন্য কোন আইসোলেশন ব্যাকএন্ড ব্যবহার করবেন তা বেছে নিন।',
'settings.sandbox.backend.auto': 'স্বয়ংক্রিয় (সেরা উপলব্ধ সনাক্ত করুন)',
'settings.sandbox.backend.docker': 'Docker',
'settings.sandbox.backend.landlock': 'Landlock (Linux)',
'settings.sandbox.backend.firejail': 'Firejail (Linux)',
'settings.sandbox.backend.bubblewrap': 'Bubblewrap (Linux)',
'settings.sandbox.backend.none': 'কোনোটি নয় (কোনো Sandbox নেই)',
'settings.sandbox.dockerSettings': 'Docker সেটিংস',
'settings.sandbox.dockerImage': 'ইমেজ',
'settings.sandbox.dockerImagePlaceholder': 'alpine:3.20',
'settings.sandbox.memoryLimit': 'মেমরি সীমা',
'settings.sandbox.memoryUnit': 'MB',
'settings.sandbox.cpuLimit': 'CPU সীমা',
'settings.sandbox.cpuUnit': 'কোর',
'settings.sandbox.envPassthrough': 'পরিবেশ পাসথ্রু',
'settings.sandbox.envPassthroughDesc': 'Sandbox-এ ফরওয়ার্ড করা পরিবেশ ভেরিয়েবল।',
'settings.sandbox.noEnvVars': 'কোনো পরিবেশ ভেরিয়েবল কনফিগার করা হয়নি।',
'settings.approvalHistory.title': 'Approval history',
'settings.approvalHistory.subtitle': 'Recent tool-approval decisions, newest first.',
'settings.approvalHistory.refresh': 'Refresh',
+36 -1
View File
@@ -3760,7 +3760,42 @@ const messages: TranslationMap = {
'settings.agentAccess.approvalHistory': 'Approval history',
'settings.agentAccess.approvalHistoryDesc':
'Review past Approve / Deny decisions the agent requested.',
'settings.agentAccess.viewApprovalHistory': 'View approval history',
'settings.agentAccess.viewApprovalHistory': 'Genehmigungsverlauf anzeigen',
'settings.sandbox.title': 'Sandbox-Ausführung',
'settings.sandbox.menuDesc':
'Sandbox-Backends für die Isolation von Agentenwerkzeugen konfigurieren.',
'settings.sandbox.loading': 'Wird geladen…',
'settings.sandbox.desktopOnly': 'Sandbox-Einstellungen sind nur in der Desktop-App verfügbar.',
'settings.sandbox.loadError': 'Sandbox-Einstellungen konnten nicht geladen werden.',
'settings.sandbox.saveError': 'Sandbox-Einstellungen konnten nicht gespeichert werden.',
'settings.sandbox.saved': 'Gespeichert — gilt für neue Agentensitzungen.',
'settings.sandbox.saving': 'Wird gespeichert…',
'settings.sandbox.status': 'Status',
'settings.sandbox.dockerStatus': 'Docker',
'settings.sandbox.available': 'Verfügbar',
'settings.sandbox.unavailable': 'Nicht verfügbar',
'settings.sandbox.detectedBackend': 'OS-Backend',
'settings.sandbox.enableLabel': 'Sandbox-Ausführung aktivieren',
'settings.sandbox.enableDesc': 'Agentenwerkzeuge in einer isolierten Sandbox-Umgebung ausführen.',
'settings.sandbox.backendLabel': 'Backend',
'settings.sandbox.backendDesc': 'Wählen Sie das Isolierungs-Backend für die Sandbox-Ausführung.',
'settings.sandbox.backend.auto': 'Automatisch (bestes verfügbares erkennen)',
'settings.sandbox.backend.docker': 'Docker',
'settings.sandbox.backend.landlock': 'Landlock (Linux)',
'settings.sandbox.backend.firejail': 'Firejail (Linux)',
'settings.sandbox.backend.bubblewrap': 'Bubblewrap (Linux)',
'settings.sandbox.backend.none': 'Keine (ohne Sandbox)',
'settings.sandbox.dockerSettings': 'Docker-Einstellungen',
'settings.sandbox.dockerImage': 'Image',
'settings.sandbox.dockerImagePlaceholder': 'alpine:3.20',
'settings.sandbox.memoryLimit': 'Speicherlimit',
'settings.sandbox.memoryUnit': 'MB',
'settings.sandbox.cpuLimit': 'CPU-Limit',
'settings.sandbox.cpuUnit': 'Kerne',
'settings.sandbox.envPassthrough': 'Umgebungsvariablen-Durchleitung',
'settings.sandbox.envPassthroughDesc':
'Umgebungsvariablen, die in die Sandbox weitergeleitet werden.',
'settings.sandbox.noEnvVars': 'Keine Umgebungsvariablen konfiguriert.',
'settings.approvalHistory.title': 'Approval history',
'settings.approvalHistory.subtitle': 'Recent tool-approval decisions, newest first.',
'settings.approvalHistory.refresh': 'Refresh',
+36
View File
@@ -4053,6 +4053,42 @@ const en: TranslationMap = {
'settings.agentAccess.approvalHistoryDesc':
'Review past Approve / Deny decisions the agent requested.',
'settings.agentAccess.viewApprovalHistory': 'View approval history',
// ── Sandbox execution backend ─────────────────────────────────────
'settings.sandbox.title': 'Sandbox execution',
'settings.sandbox.menuDesc': 'Configure sandbox backends for agent tool isolation.',
'settings.sandbox.loading': 'Loading…',
'settings.sandbox.desktopOnly': 'Sandbox settings are only available in the desktop app.',
'settings.sandbox.loadError': 'Failed to load sandbox settings.',
'settings.sandbox.saveError': 'Failed to save sandbox settings.',
'settings.sandbox.saved': 'Saved — applies to new agent sessions.',
'settings.sandbox.saving': 'Saving…',
'settings.sandbox.status': 'Status',
'settings.sandbox.dockerStatus': 'Docker',
'settings.sandbox.available': 'Available',
'settings.sandbox.unavailable': 'Unavailable',
'settings.sandbox.detectedBackend': 'OS backend',
'settings.sandbox.enableLabel': 'Enable sandbox execution',
'settings.sandbox.enableDesc': 'Run agent tools inside an isolated sandbox environment.',
'settings.sandbox.backendLabel': 'Backend',
'settings.sandbox.backendDesc': 'Choose which isolation backend to use for sandboxed execution.',
'settings.sandbox.backend.auto': 'Auto (detect best available)',
'settings.sandbox.backend.docker': 'Docker',
'settings.sandbox.backend.landlock': 'Landlock (Linux)',
'settings.sandbox.backend.firejail': 'Firejail (Linux)',
'settings.sandbox.backend.bubblewrap': 'Bubblewrap (Linux)',
'settings.sandbox.backend.none': 'None (no sandbox)',
'settings.sandbox.dockerSettings': 'Docker settings',
'settings.sandbox.dockerImage': 'Image',
'settings.sandbox.dockerImagePlaceholder': 'alpine:3.20',
'settings.sandbox.memoryLimit': 'Memory limit',
'settings.sandbox.memoryUnit': 'MB',
'settings.sandbox.cpuLimit': 'CPU limit',
'settings.sandbox.cpuUnit': 'cores',
'settings.sandbox.envPassthrough': 'Environment passthrough',
'settings.sandbox.envPassthroughDesc': 'Environment variables forwarded into the sandbox.',
'settings.sandbox.noEnvVars': 'No environment variables configured.',
'settings.approvalHistory.title': 'Approval history',
'settings.approvalHistory.subtitle': 'Recent tool-approval decisions, newest first.',
'settings.approvalHistory.refresh': 'Refresh',
+38 -1
View File
@@ -3732,7 +3732,44 @@ const messages: TranslationMap = {
'settings.agentAccess.approvalHistory': 'Approval history',
'settings.agentAccess.approvalHistoryDesc':
'Review past Approve / Deny decisions the agent requested.',
'settings.agentAccess.viewApprovalHistory': 'View approval history',
'settings.agentAccess.viewApprovalHistory': 'Ver historial de aprobaciones',
'settings.sandbox.title': 'Ejecución en sandbox',
'settings.sandbox.menuDesc':
'Configurar backends de sandbox para el aislamiento de herramientas del agente.',
'settings.sandbox.loading': 'Cargando…',
'settings.sandbox.desktopOnly':
'La configuración de sandbox solo está disponible en la aplicación de escritorio.',
'settings.sandbox.loadError': 'Error al cargar la configuración de sandbox.',
'settings.sandbox.saveError': 'Error al guardar la configuración de sandbox.',
'settings.sandbox.saved': 'Guardado — se aplica a las nuevas sesiones del agente.',
'settings.sandbox.saving': 'Guardando…',
'settings.sandbox.status': 'Estado',
'settings.sandbox.dockerStatus': 'Docker',
'settings.sandbox.available': 'Disponible',
'settings.sandbox.unavailable': 'No disponible',
'settings.sandbox.detectedBackend': 'Backend del SO',
'settings.sandbox.enableLabel': 'Habilitar ejecución en sandbox',
'settings.sandbox.enableDesc':
'Ejecutar las herramientas del agente dentro de un entorno sandbox aislado.',
'settings.sandbox.backendLabel': 'Backend',
'settings.sandbox.backendDesc':
'Elige qué backend de aislamiento usar para la ejecución en sandbox.',
'settings.sandbox.backend.auto': 'Automático (detectar el mejor disponible)',
'settings.sandbox.backend.docker': 'Docker',
'settings.sandbox.backend.landlock': 'Landlock (Linux)',
'settings.sandbox.backend.firejail': 'Firejail (Linux)',
'settings.sandbox.backend.bubblewrap': 'Bubblewrap (Linux)',
'settings.sandbox.backend.none': 'Ninguno (sin sandbox)',
'settings.sandbox.dockerSettings': 'Configuración de Docker',
'settings.sandbox.dockerImage': 'Imagen',
'settings.sandbox.dockerImagePlaceholder': 'alpine:3.20',
'settings.sandbox.memoryLimit': 'Límite de memoria',
'settings.sandbox.memoryUnit': 'MB',
'settings.sandbox.cpuLimit': 'Límite de CPU',
'settings.sandbox.cpuUnit': 'núcleos',
'settings.sandbox.envPassthrough': 'Reenvío de variables de entorno',
'settings.sandbox.envPassthroughDesc': 'Variables de entorno reenviadas al sandbox.',
'settings.sandbox.noEnvVars': 'No hay variables de entorno configuradas.',
'settings.approvalHistory.title': 'Approval history',
'settings.approvalHistory.subtitle': 'Recent tool-approval decisions, newest first.',
'settings.approvalHistory.refresh': 'Refresh',
+38 -1
View File
@@ -3746,7 +3746,44 @@ const messages: TranslationMap = {
'settings.agentAccess.approvalHistory': 'Approval history',
'settings.agentAccess.approvalHistoryDesc':
'Review past Approve / Deny decisions the agent requested.',
'settings.agentAccess.viewApprovalHistory': 'View approval history',
'settings.agentAccess.viewApprovalHistory': "Voir l'historique des approbations",
'settings.sandbox.title': 'Exécution en sandbox',
'settings.sandbox.menuDesc':
"Configurer les backends sandbox pour l'isolation des outils de l'agent.",
'settings.sandbox.loading': 'Chargement…',
'settings.sandbox.desktopOnly':
"Les paramètres sandbox sont disponibles uniquement dans l'application de bureau.",
'settings.sandbox.loadError': 'Échec du chargement des paramètres sandbox.',
'settings.sandbox.saveError': 'Échec de la sauvegarde des paramètres sandbox.',
'settings.sandbox.saved': "Enregistré — applicable aux nouvelles sessions de l'agent.",
'settings.sandbox.saving': 'Enregistrement…',
'settings.sandbox.status': 'Statut',
'settings.sandbox.dockerStatus': 'Docker',
'settings.sandbox.available': 'Disponible',
'settings.sandbox.unavailable': 'Indisponible',
'settings.sandbox.detectedBackend': 'Backend OS',
'settings.sandbox.enableLabel': "Activer l'exécution en sandbox",
'settings.sandbox.enableDesc':
"Exécuter les outils de l'agent dans un environnement sandbox isolé.",
'settings.sandbox.backendLabel': 'Backend',
'settings.sandbox.backendDesc':
"Choisissez le backend d'isolation à utiliser pour l'exécution en sandbox.",
'settings.sandbox.backend.auto': 'Automatique (détecter le meilleur disponible)',
'settings.sandbox.backend.docker': 'Docker',
'settings.sandbox.backend.landlock': 'Landlock (Linux)',
'settings.sandbox.backend.firejail': 'Firejail (Linux)',
'settings.sandbox.backend.bubblewrap': 'Bubblewrap (Linux)',
'settings.sandbox.backend.none': 'Aucun (pas de sandbox)',
'settings.sandbox.dockerSettings': 'Paramètres Docker',
'settings.sandbox.dockerImage': 'Image',
'settings.sandbox.dockerImagePlaceholder': 'alpine:3.20',
'settings.sandbox.memoryLimit': 'Limite mémoire',
'settings.sandbox.memoryUnit': 'MB',
'settings.sandbox.cpuLimit': 'Limite CPU',
'settings.sandbox.cpuUnit': 'cœurs',
'settings.sandbox.envPassthrough': "Transfert des variables d'environnement",
'settings.sandbox.envPassthroughDesc': "Variables d'environnement transmises dans le sandbox.",
'settings.sandbox.noEnvVars': "Aucune variable d'environnement configurée.",
'settings.approvalHistory.title': 'Approval history',
'settings.approvalHistory.subtitle': 'Recent tool-approval decisions, newest first.',
'settings.approvalHistory.refresh': 'Refresh',
+35 -1
View File
@@ -3672,7 +3672,41 @@ const messages: TranslationMap = {
'settings.agentAccess.approvalHistory': 'Approval history',
'settings.agentAccess.approvalHistoryDesc':
'Review past Approve / Deny decisions the agent requested.',
'settings.agentAccess.viewApprovalHistory': 'View approval history',
'settings.agentAccess.viewApprovalHistory': 'अनुमोदन इतिहास देखें',
'settings.sandbox.title': 'Sandbox निष्पादन',
'settings.sandbox.menuDesc': 'एजेंट टूल आइसोलेशन के लिए Sandbox बैकएंड कॉन्फ़िगर करें।',
'settings.sandbox.loading': 'लोड हो रहा है…',
'settings.sandbox.desktopOnly': 'Sandbox सेटिंग केवल डेस्कटॉप ऐप में उपलब्ध हैं।',
'settings.sandbox.loadError': 'Sandbox सेटिंग लोड करने में विफल।',
'settings.sandbox.saveError': 'Sandbox सेटिंग सहेजने में विफल।',
'settings.sandbox.saved': 'सहेजा गया — नए एजेंट सत्रों पर लागू होता है।',
'settings.sandbox.saving': 'सहेजा जा रहा है…',
'settings.sandbox.status': 'स्थिति',
'settings.sandbox.dockerStatus': 'Docker',
'settings.sandbox.available': 'उपलब्ध',
'settings.sandbox.unavailable': 'अनुपलब्ध',
'settings.sandbox.detectedBackend': 'OS बैकएंड',
'settings.sandbox.enableLabel': 'Sandbox निष्पादन सक्षम करें',
'settings.sandbox.enableDesc': 'एक पृथक Sandbox वातावरण के अंदर एजेंट टूल चलाएं।',
'settings.sandbox.backendLabel': 'बैकएंड',
'settings.sandbox.backendDesc':
'Sandbox निष्पादन के लिए उपयोग किया जाने वाला आइसोलेशन बैकएंड चुनें।',
'settings.sandbox.backend.auto': 'स्वचालित (सर्वोत्तम उपलब्ध का पता लगाएं)',
'settings.sandbox.backend.docker': 'Docker',
'settings.sandbox.backend.landlock': 'Landlock (Linux)',
'settings.sandbox.backend.firejail': 'Firejail (Linux)',
'settings.sandbox.backend.bubblewrap': 'Bubblewrap (Linux)',
'settings.sandbox.backend.none': 'कोई नहीं (कोई sandbox नहीं)',
'settings.sandbox.dockerSettings': 'Docker सेटिंग',
'settings.sandbox.dockerImage': 'इमेज',
'settings.sandbox.dockerImagePlaceholder': 'alpine:3.20',
'settings.sandbox.memoryLimit': 'मेमोरी सीमा',
'settings.sandbox.memoryUnit': 'MB',
'settings.sandbox.cpuLimit': 'CPU सीमा',
'settings.sandbox.cpuUnit': 'कोर',
'settings.sandbox.envPassthrough': 'पर्यावरण पासथ्रू',
'settings.sandbox.envPassthroughDesc': 'Sandbox में अग्रेषित पर्यावरण चर।',
'settings.sandbox.noEnvVars': 'कोई पर्यावरण चर कॉन्फ़िगर नहीं किए गए।',
'settings.approvalHistory.title': 'Approval history',
'settings.approvalHistory.subtitle': 'Recent tool-approval decisions, newest first.',
'settings.approvalHistory.refresh': 'Refresh',
+35 -1
View File
@@ -3681,7 +3681,41 @@ const messages: TranslationMap = {
'settings.agentAccess.approvalHistory': 'Approval history',
'settings.agentAccess.approvalHistoryDesc':
'Review past Approve / Deny decisions the agent requested.',
'settings.agentAccess.viewApprovalHistory': 'View approval history',
'settings.agentAccess.viewApprovalHistory': 'Lihat riwayat persetujuan',
'settings.sandbox.title': 'Eksekusi sandbox',
'settings.sandbox.menuDesc': 'Konfigurasikan backend sandbox untuk isolasi alat agen.',
'settings.sandbox.loading': 'Memuat…',
'settings.sandbox.desktopOnly': 'Pengaturan sandbox hanya tersedia di aplikasi desktop.',
'settings.sandbox.loadError': 'Gagal memuat pengaturan sandbox.',
'settings.sandbox.saveError': 'Gagal menyimpan pengaturan sandbox.',
'settings.sandbox.saved': 'Tersimpan — berlaku untuk sesi agen baru.',
'settings.sandbox.saving': 'Menyimpan…',
'settings.sandbox.status': 'Status',
'settings.sandbox.dockerStatus': 'Docker',
'settings.sandbox.available': 'Tersedia',
'settings.sandbox.unavailable': 'Tidak tersedia',
'settings.sandbox.detectedBackend': 'Backend OS',
'settings.sandbox.enableLabel': 'Aktifkan eksekusi sandbox',
'settings.sandbox.enableDesc': 'Jalankan alat agen di dalam lingkungan sandbox yang terisolasi.',
'settings.sandbox.backendLabel': 'Backend',
'settings.sandbox.backendDesc':
'Pilih backend isolasi yang akan digunakan untuk eksekusi sandbox.',
'settings.sandbox.backend.auto': 'Otomatis (deteksi yang terbaik tersedia)',
'settings.sandbox.backend.docker': 'Docker',
'settings.sandbox.backend.landlock': 'Landlock (Linux)',
'settings.sandbox.backend.firejail': 'Firejail (Linux)',
'settings.sandbox.backend.bubblewrap': 'Bubblewrap (Linux)',
'settings.sandbox.backend.none': 'Tidak ada (tanpa sandbox)',
'settings.sandbox.dockerSettings': 'Pengaturan Docker',
'settings.sandbox.dockerImage': 'Gambar',
'settings.sandbox.dockerImagePlaceholder': 'alpine:3.20',
'settings.sandbox.memoryLimit': 'Batas memori',
'settings.sandbox.memoryUnit': 'MB',
'settings.sandbox.cpuLimit': 'Batas CPU',
'settings.sandbox.cpuUnit': 'inti',
'settings.sandbox.envPassthrough': 'Terusan variabel lingkungan',
'settings.sandbox.envPassthroughDesc': 'Variabel lingkungan yang diteruskan ke dalam sandbox.',
'settings.sandbox.noEnvVars': 'Tidak ada variabel lingkungan yang dikonfigurasi.',
'settings.approvalHistory.title': 'Approval history',
'settings.approvalHistory.subtitle': 'Recent tool-approval decisions, newest first.',
'settings.approvalHistory.refresh': 'Refresh',
+36 -1
View File
@@ -3725,7 +3725,42 @@ const messages: TranslationMap = {
'settings.agentAccess.approvalHistory': 'Approval history',
'settings.agentAccess.approvalHistoryDesc':
'Review past Approve / Deny decisions the agent requested.',
'settings.agentAccess.viewApprovalHistory': 'View approval history',
'settings.agentAccess.viewApprovalHistory': 'Visualizza cronologia approvazioni',
'settings.sandbox.title': 'Esecuzione in sandbox',
'settings.sandbox.menuDesc':
"Configura i backend sandbox per l'isolamento degli strumenti agente.",
'settings.sandbox.loading': 'Caricamento…',
'settings.sandbox.desktopOnly': "Le impostazioni sandbox sono disponibili solo nell'app desktop.",
'settings.sandbox.loadError': 'Caricamento delle impostazioni sandbox non riuscito.',
'settings.sandbox.saveError': 'Salvataggio delle impostazioni sandbox non riuscito.',
'settings.sandbox.saved': 'Salvato — si applica alle nuove sessioni agente.',
'settings.sandbox.saving': 'Salvataggio…',
'settings.sandbox.status': 'Stato',
'settings.sandbox.dockerStatus': 'Docker',
'settings.sandbox.available': 'Disponibile',
'settings.sandbox.unavailable': 'Non disponibile',
'settings.sandbox.detectedBackend': 'Backend del sistema operativo',
'settings.sandbox.enableLabel': 'Abilita esecuzione in sandbox',
'settings.sandbox.enableDesc': 'Esegui gli strumenti agente in un ambiente sandbox isolato.',
'settings.sandbox.backendLabel': 'Backend',
'settings.sandbox.backendDesc':
"Scegli quale backend di isolamento utilizzare per l'esecuzione in sandbox.",
'settings.sandbox.backend.auto': 'Automatico (rileva il migliore disponibile)',
'settings.sandbox.backend.docker': 'Docker',
'settings.sandbox.backend.landlock': 'Landlock (Linux)',
'settings.sandbox.backend.firejail': 'Firejail (Linux)',
'settings.sandbox.backend.bubblewrap': 'Bubblewrap (Linux)',
'settings.sandbox.backend.none': 'Nessuno (senza sandbox)',
'settings.sandbox.dockerSettings': 'Impostazioni Docker',
'settings.sandbox.dockerImage': 'Immagine',
'settings.sandbox.dockerImagePlaceholder': 'alpine:3.20',
'settings.sandbox.memoryLimit': 'Limite di memoria',
'settings.sandbox.memoryUnit': 'MB',
'settings.sandbox.cpuLimit': 'Limite CPU',
'settings.sandbox.cpuUnit': 'core',
'settings.sandbox.envPassthrough': "Passthrough variabili d'ambiente",
'settings.sandbox.envPassthroughDesc': "Variabili d'ambiente inoltrate nella sandbox.",
'settings.sandbox.noEnvVars': "Nessuna variabile d'ambiente configurata.",
'settings.approvalHistory.title': 'Approval history',
'settings.approvalHistory.subtitle': 'Recent tool-approval decisions, newest first.',
'settings.approvalHistory.refresh': 'Refresh',
+34 -1
View File
@@ -3632,7 +3632,40 @@ const messages: TranslationMap = {
'settings.agentAccess.approvalHistory': 'Approval history',
'settings.agentAccess.approvalHistoryDesc':
'Review past Approve / Deny decisions the agent requested.',
'settings.agentAccess.viewApprovalHistory': 'View approval history',
'settings.agentAccess.viewApprovalHistory': '승인 기록 보기',
'settings.sandbox.title': '샌드박스 실행',
'settings.sandbox.menuDesc': '에이전트 도구 격리를 위한 샌드박스 백엔드를 구성합니다.',
'settings.sandbox.loading': '로딩 중…',
'settings.sandbox.desktopOnly': '샌드박스 설정은 데스크톱 앱에서만 사용할 수 있습니다.',
'settings.sandbox.loadError': '샌드박스 설정을 불러오지 못했습니다.',
'settings.sandbox.saveError': '샌드박스 설정을 저장하지 못했습니다.',
'settings.sandbox.saved': '저장됨 — 새 에이전트 세션에 적용됩니다.',
'settings.sandbox.saving': '저장 중…',
'settings.sandbox.status': '상태',
'settings.sandbox.dockerStatus': 'Docker',
'settings.sandbox.available': '사용 가능',
'settings.sandbox.unavailable': '사용 불가',
'settings.sandbox.detectedBackend': 'OS 백엔드',
'settings.sandbox.enableLabel': '샌드박스 실행 사용',
'settings.sandbox.enableDesc': '격리된 샌드박스 환경에서 에이전트 도구를 실행합니다.',
'settings.sandbox.backendLabel': '백엔드',
'settings.sandbox.backendDesc': '샌드박스 실행에 사용할 격리 백엔드를 선택합니다.',
'settings.sandbox.backend.auto': '자동 (최적 백엔드 자동 감지)',
'settings.sandbox.backend.docker': 'Docker',
'settings.sandbox.backend.landlock': 'Landlock (Linux)',
'settings.sandbox.backend.firejail': 'Firejail (Linux)',
'settings.sandbox.backend.bubblewrap': 'Bubblewrap (Linux)',
'settings.sandbox.backend.none': '없음 (샌드박스 사용 안 함)',
'settings.sandbox.dockerSettings': 'Docker 설정',
'settings.sandbox.dockerImage': '이미지',
'settings.sandbox.dockerImagePlaceholder': 'alpine:3.20',
'settings.sandbox.memoryLimit': '메모리 제한',
'settings.sandbox.memoryUnit': 'MB',
'settings.sandbox.cpuLimit': 'CPU 제한',
'settings.sandbox.cpuUnit': '코어',
'settings.sandbox.envPassthrough': '환경 변수 전달',
'settings.sandbox.envPassthroughDesc': '샌드박스로 전달되는 환경 변수입니다.',
'settings.sandbox.noEnvVars': '구성된 환경 변수가 없습니다.',
'settings.approvalHistory.title': 'Approval history',
'settings.approvalHistory.subtitle': 'Recent tool-approval decisions, newest first.',
'settings.approvalHistory.refresh': 'Refresh',
+36 -1
View File
@@ -3728,7 +3728,42 @@ const messages: TranslationMap = {
'settings.agentAccess.approvalHistory': 'Approval history',
'settings.agentAccess.approvalHistoryDesc':
'Review past Approve / Deny decisions the agent requested.',
'settings.agentAccess.viewApprovalHistory': 'View approval history',
'settings.agentAccess.viewApprovalHistory': 'Wyświetl historię zatwierdzeń',
'settings.sandbox.title': 'Wykonanie w piaskownicy',
'settings.sandbox.menuDesc': 'Konfiguruj backendy piaskownicy do izolacji narzędzi agenta.',
'settings.sandbox.loading': 'Ładowanie…',
'settings.sandbox.desktopOnly':
'Ustawienia piaskownicy są dostępne tylko w aplikacji desktopowej.',
'settings.sandbox.loadError': 'Nie udało się załadować ustawień piaskownicy.',
'settings.sandbox.saveError': 'Nie udało się zapisać ustawień piaskownicy.',
'settings.sandbox.saved': 'Zapisano — dotyczy nowych sesji agenta.',
'settings.sandbox.saving': 'Zapisywanie…',
'settings.sandbox.status': 'Status',
'settings.sandbox.dockerStatus': 'Docker',
'settings.sandbox.available': 'Dostępny',
'settings.sandbox.unavailable': 'Niedostępny',
'settings.sandbox.detectedBackend': 'Backend systemu operacyjnego',
'settings.sandbox.enableLabel': 'Włącz wykonanie w piaskownicy',
'settings.sandbox.enableDesc': 'Uruchamiaj narzędzia agenta w izolowanym środowisku piaskownicy.',
'settings.sandbox.backendLabel': 'Backend',
'settings.sandbox.backendDesc':
'Wybierz backend izolacji do użycia przy wykonaniu w piaskownicy.',
'settings.sandbox.backend.auto': 'Automatyczny (wykryj najlepszy dostępny)',
'settings.sandbox.backend.docker': 'Docker',
'settings.sandbox.backend.landlock': 'Landlock (Linux)',
'settings.sandbox.backend.firejail': 'Firejail (Linux)',
'settings.sandbox.backend.bubblewrap': 'Bubblewrap (Linux)',
'settings.sandbox.backend.none': 'Brak (bez piaskownicy)',
'settings.sandbox.dockerSettings': 'Ustawienia Docker',
'settings.sandbox.dockerImage': 'Obraz',
'settings.sandbox.dockerImagePlaceholder': 'alpine:3.20',
'settings.sandbox.memoryLimit': 'Limit pamięci',
'settings.sandbox.memoryUnit': 'MB',
'settings.sandbox.cpuLimit': 'Limit CPU',
'settings.sandbox.cpuUnit': 'rdzenie',
'settings.sandbox.envPassthrough': 'Przekazywanie zmiennych środowiskowych',
'settings.sandbox.envPassthroughDesc': 'Zmienne środowiskowe przekazywane do piaskownicy.',
'settings.sandbox.noEnvVars': 'Nie skonfigurowano żadnych zmiennych środowiskowych.',
'settings.approvalHistory.title': 'Approval history',
'settings.approvalHistory.subtitle': 'Recent tool-approval decisions, newest first.',
'settings.approvalHistory.refresh': 'Refresh',
+37 -1
View File
@@ -3727,7 +3727,43 @@ const messages: TranslationMap = {
'settings.agentAccess.approvalHistory': 'Approval history',
'settings.agentAccess.approvalHistoryDesc':
'Review past Approve / Deny decisions the agent requested.',
'settings.agentAccess.viewApprovalHistory': 'View approval history',
'settings.agentAccess.viewApprovalHistory': 'Ver histórico de aprovações',
'settings.sandbox.title': 'Execução em sandbox',
'settings.sandbox.menuDesc':
'Configure backends de sandbox para isolamento das ferramentas do agente.',
'settings.sandbox.loading': 'Carregando…',
'settings.sandbox.desktopOnly':
'As definições de sandbox estão disponíveis apenas na aplicação de ambiente de trabalho.',
'settings.sandbox.loadError': 'Falha ao carregar as definições de sandbox.',
'settings.sandbox.saveError': 'Falha ao salvar as configurações de sandbox.',
'settings.sandbox.saved': 'Salvo — aplica-se a novas sessões do agente.',
'settings.sandbox.saving': 'Salvando…',
'settings.sandbox.status': 'Estado',
'settings.sandbox.dockerStatus': 'Docker',
'settings.sandbox.available': 'Disponível',
'settings.sandbox.unavailable': 'Indisponível',
'settings.sandbox.detectedBackend': 'Backend do sistema operativo',
'settings.sandbox.enableLabel': 'Ativar execução em sandbox',
'settings.sandbox.enableDesc': 'Execute as ferramentas do agente num ambiente sandbox isolado.',
'settings.sandbox.backendLabel': 'Backend',
'settings.sandbox.backendDesc':
'Escolha qual backend de isolamento utilizar para a execução em sandbox.',
'settings.sandbox.backend.auto': 'Automático (detetar o melhor disponível)',
'settings.sandbox.backend.docker': 'Docker',
'settings.sandbox.backend.landlock': 'Landlock (Linux)',
'settings.sandbox.backend.firejail': 'Firejail (Linux)',
'settings.sandbox.backend.bubblewrap': 'Bubblewrap (Linux)',
'settings.sandbox.backend.none': 'Nenhum (sem sandbox)',
'settings.sandbox.dockerSettings': 'Definições Docker',
'settings.sandbox.dockerImage': 'Imagem',
'settings.sandbox.dockerImagePlaceholder': 'alpine:3.20',
'settings.sandbox.memoryLimit': 'Limite de memória',
'settings.sandbox.memoryUnit': 'MB',
'settings.sandbox.cpuLimit': 'Limite de CPU',
'settings.sandbox.cpuUnit': 'núcleos',
'settings.sandbox.envPassthrough': 'Passagem de variáveis de ambiente',
'settings.sandbox.envPassthroughDesc': 'Variáveis de ambiente encaminhadas para o sandbox.',
'settings.sandbox.noEnvVars': 'Nenhuma variável de ambiente configurada.',
'settings.approvalHistory.title': 'Approval history',
'settings.approvalHistory.subtitle': 'Recent tool-approval decisions, newest first.',
'settings.approvalHistory.refresh': 'Refresh',
+34 -1
View File
@@ -3695,7 +3695,40 @@ const messages: TranslationMap = {
'settings.agentAccess.approvalHistory': 'Approval history',
'settings.agentAccess.approvalHistoryDesc':
'Review past Approve / Deny decisions the agent requested.',
'settings.agentAccess.viewApprovalHistory': 'View approval history',
'settings.agentAccess.viewApprovalHistory': 'Просмотреть историю утверждений',
'settings.sandbox.title': 'Выполнение в песочнице',
'settings.sandbox.menuDesc': 'Настройте бэкенды песочницы для изоляции инструментов агента.',
'settings.sandbox.loading': 'Загрузка…',
'settings.sandbox.desktopOnly': 'Настройки песочницы доступны только в десктопном приложении.',
'settings.sandbox.loadError': 'Не удалось загрузить настройки песочницы.',
'settings.sandbox.saveError': 'Не удалось сохранить настройки песочницы.',
'settings.sandbox.saved': 'Сохранено — применяется к новым сессиям агента.',
'settings.sandbox.saving': 'Сохранение…',
'settings.sandbox.status': 'Статус',
'settings.sandbox.dockerStatus': 'Docker',
'settings.sandbox.available': 'Доступен',
'settings.sandbox.unavailable': 'Недоступен',
'settings.sandbox.detectedBackend': 'Бэкенд ОС',
'settings.sandbox.enableLabel': 'Включить выполнение в песочнице',
'settings.sandbox.enableDesc': 'Запускать инструменты агента в изолированной среде песочницы.',
'settings.sandbox.backendLabel': 'Бэкенд',
'settings.sandbox.backendDesc': 'Выберите бэкенд изоляции для выполнения в песочнице.',
'settings.sandbox.backend.auto': 'Автоматически (определить лучший доступный)',
'settings.sandbox.backend.docker': 'Docker',
'settings.sandbox.backend.landlock': 'Landlock (Linux)',
'settings.sandbox.backend.firejail': 'Firejail (Linux)',
'settings.sandbox.backend.bubblewrap': 'Bubblewrap (Linux)',
'settings.sandbox.backend.none': 'Нет (без песочницы)',
'settings.sandbox.dockerSettings': 'Настройки Docker',
'settings.sandbox.dockerImage': 'Образ',
'settings.sandbox.dockerImagePlaceholder': 'alpine:3.20',
'settings.sandbox.memoryLimit': 'Ограничение памяти',
'settings.sandbox.memoryUnit': 'MB',
'settings.sandbox.cpuLimit': 'Ограничение CPU',
'settings.sandbox.cpuUnit': 'ядра',
'settings.sandbox.envPassthrough': 'Проброс переменных окружения',
'settings.sandbox.envPassthroughDesc': 'Переменные окружения, передаваемые в песочницу.',
'settings.sandbox.noEnvVars': 'Переменные окружения не настроены.',
'settings.approvalHistory.title': 'Approval history',
'settings.approvalHistory.subtitle': 'Recent tool-approval decisions, newest first.',
'settings.approvalHistory.refresh': 'Refresh',
+34 -1
View File
@@ -3493,7 +3493,40 @@ const messages: TranslationMap = {
'settings.agentAccess.approvalHistory': 'Approval history',
'settings.agentAccess.approvalHistoryDesc':
'Review past Approve / Deny decisions the agent requested.',
'settings.agentAccess.viewApprovalHistory': 'View approval history',
'settings.agentAccess.viewApprovalHistory': '查看审批历史',
'settings.sandbox.title': '沙盒执行',
'settings.sandbox.menuDesc': '配置沙盒后端以隔离代理工具。',
'settings.sandbox.loading': '加载中…',
'settings.sandbox.desktopOnly': '沙盒设置仅在桌面应用中可用。',
'settings.sandbox.loadError': '加载沙盒设置失败。',
'settings.sandbox.saveError': '保存沙盒设置失败。',
'settings.sandbox.saved': '已保存 — 适用于新的代理会话。',
'settings.sandbox.saving': '保存中…',
'settings.sandbox.status': '状态',
'settings.sandbox.dockerStatus': 'Docker',
'settings.sandbox.available': '可用',
'settings.sandbox.unavailable': '不可用',
'settings.sandbox.detectedBackend': '操作系统后端',
'settings.sandbox.enableLabel': '启用沙盒执行',
'settings.sandbox.enableDesc': '在隔离的沙盒环境中运行代理工具。',
'settings.sandbox.backendLabel': '后端',
'settings.sandbox.backendDesc': '选择用于沙盒执行的隔离后端。',
'settings.sandbox.backend.auto': '自动(检测最佳可用选项)',
'settings.sandbox.backend.docker': 'Docker',
'settings.sandbox.backend.landlock': 'Landlock (Linux)',
'settings.sandbox.backend.firejail': 'Firejail (Linux)',
'settings.sandbox.backend.bubblewrap': 'Bubblewrap (Linux)',
'settings.sandbox.backend.none': '无(不使用沙盒)',
'settings.sandbox.dockerSettings': 'Docker 设置',
'settings.sandbox.dockerImage': '镜像',
'settings.sandbox.dockerImagePlaceholder': 'alpine:3.20',
'settings.sandbox.memoryLimit': '内存限制',
'settings.sandbox.memoryUnit': 'MB',
'settings.sandbox.cpuLimit': 'CPU 限制',
'settings.sandbox.cpuUnit': '核心',
'settings.sandbox.envPassthrough': '环境变量透传',
'settings.sandbox.envPassthroughDesc': '转发到沙盒中的环境变量。',
'settings.sandbox.noEnvVars': '未配置任何环境变量。',
'settings.approvalHistory.title': 'Approval history',
'settings.approvalHistory.subtitle': 'Recent tool-approval decisions, newest first.',
'settings.approvalHistory.refresh': 'Refresh',
+9
View File
@@ -38,6 +38,7 @@ import NotificationsTabbedPanel from '../components/settings/panels/Notification
import PersonaPanel from '../components/settings/panels/PersonaPanel';
import PrivacyPanel from '../components/settings/panels/PrivacyPanel';
import RecoveryPhrasePanel from '../components/settings/panels/RecoveryPhrasePanel';
import SandboxSettingsPanel from '../components/settings/panels/SandboxSettingsPanel';
import ScreenAwarenessDebugPanel from '../components/settings/panels/ScreenAwarenessDebugPanel';
import ScreenIntelligencePanel from '../components/settings/panels/ScreenIntelligencePanel';
import SearchPanel from '../components/settings/panels/SearchPanel';
@@ -465,6 +466,13 @@ const Settings = () => {
route: 'activity-level',
icon: LlmIcon,
},
{
id: 'sandbox-settings',
title: t('settings.sandbox.title'),
description: t('settings.sandbox.menuDesc'),
route: 'sandbox-settings',
icon: AgentAccessIcon,
},
];
const composioSettingsItems = [
@@ -596,6 +604,7 @@ const Settings = () => {
<Route path="appearance" element={wrapSettingsPage(<AppearancePanel />)} />
<Route path="agent-access" element={wrapSettingsPage(<AgentAccessPanel />)} />
<Route path="activity-level" element={wrapSettingsPage(<AgentActivityPanel />)} />
<Route path="sandbox-settings" element={wrapSettingsPage(<SandboxSettingsPanel />)} />
<Route path="approval-history" element={wrapSettingsPage(<ApprovalHistoryPanel />)} />
<Route path="agents" element={wrapSettingsPage(<AgentsPanel />)} />
<Route path="agents/new" element={wrapSettingsPage(<AgentEditorPage />)} />
+2
View File
@@ -7,6 +7,7 @@ export const CORE_RPC_METHODS = {
configGetComposioTriggerSettings: 'openhuman.config_get_composio_trigger_settings',
configGetDashboardSettings: 'openhuman.config_get_dashboard_settings',
configGetRuntimeFlags: 'openhuman.config_get_runtime_flags',
configGetSandboxSettings: 'openhuman.config_get_sandbox_settings',
configGetSearchSettings: 'openhuman.config_get_search_settings',
configUpdateSearchSettings: 'openhuman.config_update_search_settings',
configSetBrowserAllowAll: 'openhuman.config_set_browser_allow_all',
@@ -19,6 +20,7 @@ export const CORE_RPC_METHODS = {
configUpdateMemorySettings: 'openhuman.config_update_memory_settings',
configUpdateModelSettings: 'openhuman.config_update_model_settings',
configUpdateRuntimeSettings: 'openhuman.config_update_runtime_settings',
configUpdateSandboxSettings: 'openhuman.config_update_sandbox_settings',
configUpdateScreenIntelligenceSettings: 'openhuman.config_update_screen_intelligence_settings',
configWorkspaceOnboardingFlagExists: 'openhuman.config_workspace_onboarding_flag_exists',
configWorkspaceOnboardingFlagSet: 'openhuman.config_workspace_onboarding_flag_set',
+47
View File
@@ -458,6 +458,53 @@ export async function openhumanUpdateAutonomySettings(
});
}
// ── Sandbox execution backend settings ───────────────────────────────────────
export type SandboxBackendId = 'auto' | 'docker' | 'landlock' | 'firejail' | 'bubblewrap' | 'none';
/** Current sandbox settings returned by config_get_sandbox_settings. */
export interface SandboxSettings {
enabled: boolean;
backend: SandboxBackendId;
docker_image: string;
docker_memory_limit_mb: number | null;
docker_cpu_limit: number | null;
docker_available: boolean;
detected_backend: string;
env_passthrough: string[];
}
/** Partial update — omitted fields are left unchanged. */
export interface SandboxSettingsUpdate {
backend?: SandboxBackendId;
enabled?: boolean;
docker_image?: string;
docker_memory_limit_mb?: number | null;
docker_cpu_limit?: number | null;
env_passthrough?: string[];
}
export async function openhumanGetSandboxSettings(): Promise<CommandResponse<SandboxSettings>> {
if (!isTauri()) {
throw new Error('Not running in Tauri');
}
return await callCoreRpc<CommandResponse<SandboxSettings>>({
method: CORE_RPC_METHODS.configGetSandboxSettings,
});
}
export async function openhumanUpdateSandboxSettings(
update: SandboxSettingsUpdate
): Promise<CommandResponse<ConfigSnapshot>> {
if (!isTauri()) {
throw new Error('Not running in Tauri');
}
return await callCoreRpc<CommandResponse<ConfigSnapshot>>({
method: CORE_RPC_METHODS.configUpdateSandboxSettings,
params: update,
});
}
// ── Agent execution settings (action/tool timeout) ──────────────────────────
/** Agent execution settings as returned by config_get_agent_settings. */
+171
View File
@@ -1976,6 +1976,177 @@ pub async fn get_agent_paths() -> Result<RpcOutcome<serde_json::Value>, String>
))
}
// ── Sandbox settings ─────────────────────────────────────────────────────────
/// Partial update for the `[security.sandbox]` + `[runtime.docker]` blocks.
#[derive(Debug, Clone, Default)]
pub struct SandboxSettingsPatch {
pub backend: Option<String>,
pub enabled: Option<bool>,
pub docker_image: Option<String>,
pub docker_memory_limit_mb: Option<u64>,
pub docker_cpu_limit: Option<f64>,
pub env_passthrough: Option<Vec<String>>,
}
/// Returns the current sandbox and Docker runtime settings merged into a
/// single JSON object, plus a live status probe for Docker availability.
pub async fn get_sandbox_settings() -> Result<RpcOutcome<serde_json::Value>, String> {
let config = load_config_with_timeout().await?;
let sandbox = &config.sandbox;
let docker = &config.runtime.docker;
let docker_available = is_docker_available().await;
let backend_str = match sandbox.backend {
crate::openhuman::config::SandboxBackend::Auto => "auto",
crate::openhuman::config::SandboxBackend::Landlock => "landlock",
crate::openhuman::config::SandboxBackend::Firejail => "firejail",
crate::openhuman::config::SandboxBackend::Bubblewrap => "bubblewrap",
crate::openhuman::config::SandboxBackend::Docker => "docker",
crate::openhuman::config::SandboxBackend::None => "none",
};
let detected_backend = detect_os_sandbox_backend();
let value = json!({
"enabled": sandbox.enabled.unwrap_or(true),
"backend": backend_str,
"docker_image": docker.image,
"docker_memory_limit_mb": docker.memory_limit_mb,
"docker_cpu_limit": docker.cpu_limit,
"docker_available": docker_available,
"detected_backend": detected_backend,
"env_passthrough": crate::openhuman::sandbox::ops::SANDBOX_ENV_PASSTHROUGH,
});
log::debug!("[config][sandbox] get_sandbox_settings: backend={backend_str}, docker_available={docker_available}");
Ok(RpcOutcome::single_log(value, "sandbox settings read"))
}
/// Updates sandbox and Docker runtime settings, persists to disk.
pub async fn apply_sandbox_settings(
config: &mut Config,
update: SandboxSettingsPatch,
) -> Result<RpcOutcome<serde_json::Value>, String> {
if let Some(ref backend) = update.backend {
config.sandbox.backend = match backend.as_str() {
"auto" => crate::openhuman::config::SandboxBackend::Auto,
"landlock" => crate::openhuman::config::SandboxBackend::Landlock,
"firejail" => crate::openhuman::config::SandboxBackend::Firejail,
"bubblewrap" => crate::openhuman::config::SandboxBackend::Bubblewrap,
"docker" => crate::openhuman::config::SandboxBackend::Docker,
"none" => crate::openhuman::config::SandboxBackend::None,
other => {
log::warn!("[config][sandbox] rejected unknown backend: {other}");
return Err(format!(
"unknown sandbox backend '{other}'; valid: auto, landlock, firejail, bubblewrap, docker, none"
));
}
};
}
if let Some(enabled) = update.enabled {
config.sandbox.enabled = Some(enabled);
}
if let Some(ref image) = update.docker_image {
let trimmed = image.trim();
if trimmed.is_empty() {
return Err("docker_image must not be blank".into());
}
config.runtime.docker.image = trimmed.to_string();
}
if let Some(memory) = update.docker_memory_limit_mb {
config.runtime.docker.memory_limit_mb = Some(memory);
}
if let Some(cpu) = update.docker_cpu_limit {
if cpu <= 0.0 {
return Err("docker_cpu_limit must be positive".into());
}
config.runtime.docker.cpu_limit = Some(cpu);
}
if let Some(ref passthrough) = update.env_passthrough {
log::debug!(
"[config][sandbox] env_passthrough update: {} vars",
passthrough.len()
);
}
config.save().await.map_err(|e| e.to_string())?;
log::debug!(
"[config][sandbox] sandbox settings saved to {}",
config.config_path.display()
);
let snapshot = snapshot_config_json(config)?;
Ok(RpcOutcome::new(
snapshot,
vec![format!(
"sandbox settings saved to {}",
config.config_path.display()
)],
))
}
pub async fn load_and_apply_sandbox_settings(
update: SandboxSettingsPatch,
) -> Result<RpcOutcome<serde_json::Value>, String> {
let mut config = load_config_with_timeout().await?;
apply_sandbox_settings(&mut config, update).await
}
/// Probe Docker daemon availability via `docker info` with a 5s timeout.
async fn is_docker_available() -> bool {
let fut = tokio::process::Command::new("docker")
.arg("info")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status();
match tokio::time::timeout(std::time::Duration::from_secs(5), fut).await {
Ok(Ok(status)) => status.success(),
_ => false,
}
}
/// Detect which OS-native sandbox backend is available.
fn detect_os_sandbox_backend() -> &'static str {
#[cfg(target_os = "linux")]
{
if std::path::Path::new("/sys/kernel/security/landlock").exists() {
return "landlock";
}
if std::process::Command::new("firejail")
.arg("--version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok()
{
return "firejail";
}
if std::process::Command::new("bwrap")
.arg("--version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok()
{
return "bubblewrap";
}
"none"
}
#[cfg(target_os = "macos")]
{
"seatbelt"
}
#[cfg(target_os = "windows")]
{
"appcontainer"
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
"none"
}
}
#[cfg(test)]
#[path = "ops_tests.rs"]
mod tests;
+4
View File
@@ -101,6 +101,9 @@ pub struct Config {
#[serde(default)]
pub autonomy: AutonomyConfig,
#[serde(default)]
pub sandbox: SandboxConfig,
#[serde(default)]
pub runtime: RuntimeConfig,
@@ -653,6 +656,7 @@ impl Default for Config {
observability: ObservabilityConfig::default(),
dashboard: DashboardConfig::default(),
autonomy: AutonomyConfig::default(),
sandbox: SandboxConfig::default(),
runtime: RuntimeConfig::default(),
screen_intelligence: ScreenIntelligenceConfig::default(),
autocomplete: AutocompleteConfig::default(),
+75
View File
@@ -274,6 +274,8 @@ pub fn all_controller_schemas() -> Vec<ControllerSchema> {
schemas("get_search_settings"),
schemas("get_activity_level_settings"),
schemas("update_activity_level_settings"),
schemas("get_sandbox_settings"),
schemas("update_sandbox_settings"),
]
}
@@ -431,6 +433,14 @@ pub fn all_registered_controllers() -> Vec<RegisteredController> {
schema: schemas("update_activity_level_settings"),
handler: handle_update_activity_level_settings,
},
RegisteredController {
schema: schemas("get_sandbox_settings"),
handler: handle_get_sandbox_settings,
},
RegisteredController {
schema: schemas("update_sandbox_settings"),
handler: handle_update_sandbox_settings,
},
]
}
@@ -947,6 +957,42 @@ pub fn schemas(function: &str) -> ControllerSchema {
inputs: vec![optional_string("level", "Activity level: off | minimal | moderate | active | always_on (or 04).")],
outputs: vec![json_output("settings", "Updated activity level settings with cost estimates.")],
},
"get_sandbox_settings" => ControllerSchema {
namespace: "config",
function: "get_sandbox_settings",
description: "Get sandbox execution backend settings: selected backend, Docker image/limits, env passthrough, Docker availability, and detected OS backend.",
inputs: vec![],
outputs: vec![json_output("settings", "Sandbox settings with status.")],
},
"update_sandbox_settings" => ControllerSchema {
namespace: "config",
function: "update_sandbox_settings",
description: "Update sandbox execution backend settings: backend selection, Docker image, memory/CPU limits, and env passthrough. Applies to new agent sessions.",
inputs: vec![
optional_string("backend", "Sandbox backend: auto | landlock | firejail | bubblewrap | docker | none."),
optional_bool("enabled", "Enable or disable sandbox execution."),
optional_string("docker_image", "Docker image for sandboxed execution (e.g. alpine:3.20)."),
FieldSchema {
name: "docker_memory_limit_mb",
ty: TypeSchema::Option(Box::new(TypeSchema::U64)),
comment: "Docker container memory limit in MB.",
required: false,
},
FieldSchema {
name: "docker_cpu_limit",
ty: TypeSchema::Option(Box::new(TypeSchema::F64)),
comment: "Docker container CPU limit (e.g. 1.0 = one core).",
required: false,
},
FieldSchema {
name: "env_passthrough",
ty: TypeSchema::Option(Box::new(TypeSchema::Array(Box::new(TypeSchema::String)))),
comment: "Environment variables to pass through into the sandbox.",
required: false,
},
],
outputs: vec![json_output("snapshot", "Updated config snapshot.")],
},
"agent_server_status" => ControllerSchema {
namespace: "config",
function: "agent_server_status",
@@ -1733,6 +1779,35 @@ fn handle_update_activity_level_settings(params: Map<String, Value>) -> Controll
})
}
#[derive(Debug, Deserialize)]
struct SandboxSettingsUpdate {
backend: Option<String>,
enabled: Option<bool>,
docker_image: Option<String>,
docker_memory_limit_mb: Option<u64>,
docker_cpu_limit: Option<f64>,
env_passthrough: Option<Vec<String>>,
}
fn handle_get_sandbox_settings(_params: Map<String, Value>) -> ControllerFuture {
Box::pin(async move { to_json(config_rpc::get_sandbox_settings().await?) })
}
fn handle_update_sandbox_settings(params: Map<String, Value>) -> ControllerFuture {
Box::pin(async move {
let update = deserialize_params::<SandboxSettingsUpdate>(params)?;
let patch = config_rpc::SandboxSettingsPatch {
backend: update.backend,
enabled: update.enabled,
docker_image: update.docker_image,
docker_memory_limit_mb: update.docker_memory_limit_mb,
docker_cpu_limit: update.docker_cpu_limit,
env_passthrough: update.env_passthrough,
};
to_json(config_rpc::load_and_apply_sandbox_settings(patch).await?)
})
}
fn deserialize_params<T: DeserializeOwned>(params: Map<String, Value>) -> Result<T, String> {
serde_json::from_value(Value::Object(params)).map_err(|e| format!("invalid params: {e}"))
}
+1 -1
View File
@@ -15,7 +15,7 @@ use std::process::Command;
use std::time::Duration;
/// Safe environment variables forwarded into sandboxed execution.
const SANDBOX_ENV_PASSTHROUGH: &[&str] = &[
pub const SANDBOX_ENV_PASSTHROUGH: &[&str] = &[
"PATH", "HOME", "TERM", "LANG", "LC_ALL", "LC_CTYPE", "USER", "SHELL", "TMPDIR",
];
@@ -2796,6 +2796,7 @@ async fn worker_a_controller_schemas_are_fully_exposed() {
"openhuman.config_get_meet_settings",
"openhuman.config_get_onboarding_completed",
"openhuman.config_get_runtime_flags",
"openhuman.config_get_sandbox_settings",
"openhuman.config_get_search_settings",
"openhuman.config_get_voice_server_settings",
"openhuman.config_reset_local_data",
@@ -2814,6 +2815,7 @@ async fn worker_a_controller_schemas_are_fully_exposed() {
"openhuman.config_update_memory_settings",
"openhuman.config_update_model_settings",
"openhuman.config_update_runtime_settings",
"openhuman.config_update_sandbox_settings",
"openhuman.config_update_screen_intelligence_settings",
"openhuman.config_update_search_settings",
"openhuman.config_update_voice_server_settings",