diff --git a/app/src/components/settings/hooks/useSettingsNavigation.ts b/app/src/components/settings/hooks/useSettingsNavigation.ts index 9b04ae3c8..72a6206ae 100644 --- a/app/src/components/settings/hooks/useSettingsNavigation.ts +++ b/app/src/components/settings/hooks/useSettingsNavigation.ts @@ -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]; diff --git a/app/src/components/settings/panels/SandboxSettingsPanel.tsx b/app/src/components/settings/panels/SandboxSettingsPanel.tsx new file mode 100644 index 000000000..4c970d535 --- /dev/null +++ b/app/src/components/settings/panels/SandboxSettingsPanel.tsx @@ -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(null); + const [savedNote, setSavedNote] = useState(null); + + const [enabled, setEnabled] = useState(true); + const [backend, setBackend] = useState('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([]); + + 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[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 ( +
+ +

+ {t('settings.sandbox.desktopOnly')} +

+
+ ); + } + + if (isLoading) { + return ( +
+ +

+ {t('settings.sandbox.loading')} +

+
+ ); + } + + return ( +
+ + + {error && ( +
+ {error} +
+ )} + + {savedNote && ( +

+ {savedNote} +

+ )} + + {/* Status */} +
+

+ {t('settings.sandbox.status')} +

+
+
+ + {t('settings.sandbox.dockerStatus')} + + + {dockerAvailable + ? t('settings.sandbox.available') + : t('settings.sandbox.unavailable')} + +
+ {detectedBackend && ( +
+ + {t('settings.sandbox.detectedBackend')} + + + {detectedBackend} + +
+ )} +
+
+ + {/* Enabled toggle */} +
+ +
+ + {/* Backend selection */} +
+

+ {t('settings.sandbox.backendLabel')} +

+

+ {t('settings.sandbox.backendDesc')} +

+ +
+ + {/* Docker settings */} +
+

+ {t('settings.sandbox.dockerSettings')} +

+
+ {/* Docker image */} +
+ + 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')} + /> +
+ + {/* Memory limit */} +
+ +
+ 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} + /> + + {t('settings.sandbox.memoryUnit')} + +
+
+ + {/* CPU limit */} +
+ +
+ 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} + /> + + {t('settings.sandbox.cpuUnit')} + +
+
+
+
+ + {/* Environment passthrough */} +
+

+ {t('settings.sandbox.envPassthrough')} +

+

+ {t('settings.sandbox.envPassthroughDesc')} +

+
+ {envPassthrough.length > 0 ? ( +
+ {envPassthrough.map(v => ( + + {v} + + ))} +
+ ) : ( +

+ {t('settings.sandbox.noEnvVars')} +

+ )} +
+
+ + {/* Saving indicator */} + {isSaving && ( +

+ {t('settings.sandbox.saving')} +

+ )} +
+ ); +}; + +export default SandboxSettingsPanel; diff --git a/app/src/components/settings/panels/__tests__/SandboxSettingsPanel.test.tsx b/app/src/components/settings/panels/__tests__/SandboxSettingsPanel.test.tsx new file mode 100644 index 000000000..6599f995d --- /dev/null +++ b/app/src/components/settings/panels/__tests__/SandboxSettingsPanel.test.tsx @@ -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 => ({ + 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( + '../../../../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(); + 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(); + expect(await screen.findByText('Unavailable')).toBeInTheDocument(); + }); + + it('renders the backend dropdown with current selection', async () => { + renderWithProviders(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + expect(await screen.findByText('RPC timeout')).toBeInTheDocument(); + }); + + it('shows saved note after successful persist', async () => { + renderWithProviders(); + 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(); + const select = await screen.findByRole('combobox', { name: /backend/i }); + fireEvent.change(select, { target: { value: 'docker' } }); + expect(await screen.findByText('Save failed')).toBeInTheDocument(); + }); +}); diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts index a94fe0f9f..100e11edb 100644 --- a/app/src/lib/i18n/ar.ts +++ b/app/src/lib/i18n/ar.ts @@ -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', diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts index c9de2e69b..efb20d17f 100644 --- a/app/src/lib/i18n/bn.ts +++ b/app/src/lib/i18n/bn.ts @@ -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', diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts index bed2ab324..617f3a776 100644 --- a/app/src/lib/i18n/de.ts +++ b/app/src/lib/i18n/de.ts @@ -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', diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts index 371720059..a7c16a740 100644 --- a/app/src/lib/i18n/en.ts +++ b/app/src/lib/i18n/en.ts @@ -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', diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts index 728727d5e..f7fe2a20c 100644 --- a/app/src/lib/i18n/es.ts +++ b/app/src/lib/i18n/es.ts @@ -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', diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts index 31b97f082..4d2137cee 100644 --- a/app/src/lib/i18n/fr.ts +++ b/app/src/lib/i18n/fr.ts @@ -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', diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts index bc07953c3..f4578e1a5 100644 --- a/app/src/lib/i18n/hi.ts +++ b/app/src/lib/i18n/hi.ts @@ -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', diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts index 5b2359f4b..9e843d467 100644 --- a/app/src/lib/i18n/id.ts +++ b/app/src/lib/i18n/id.ts @@ -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', diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts index 8e7af0cc5..a67e54694 100644 --- a/app/src/lib/i18n/it.ts +++ b/app/src/lib/i18n/it.ts @@ -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', diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts index 72fa35107..f9765b655 100644 --- a/app/src/lib/i18n/ko.ts +++ b/app/src/lib/i18n/ko.ts @@ -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', diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts index 93c86280e..5359b126b 100644 --- a/app/src/lib/i18n/pl.ts +++ b/app/src/lib/i18n/pl.ts @@ -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', diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts index 19d77b4a7..8f0c1be81 100644 --- a/app/src/lib/i18n/pt.ts +++ b/app/src/lib/i18n/pt.ts @@ -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', diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts index dde525f88..296dab923 100644 --- a/app/src/lib/i18n/ru.ts +++ b/app/src/lib/i18n/ru.ts @@ -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', diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts index 7363adde3..a01c54488 100644 --- a/app/src/lib/i18n/zh-CN.ts +++ b/app/src/lib/i18n/zh-CN.ts @@ -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', diff --git a/app/src/pages/Settings.tsx b/app/src/pages/Settings.tsx index 1fb485267..38de5d436 100644 --- a/app/src/pages/Settings.tsx +++ b/app/src/pages/Settings.tsx @@ -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 = () => { )} /> )} /> )} /> + )} /> )} /> )} /> )} /> diff --git a/app/src/services/rpcMethods.ts b/app/src/services/rpcMethods.ts index 0d04cbbbc..9e26550c4 100644 --- a/app/src/services/rpcMethods.ts +++ b/app/src/services/rpcMethods.ts @@ -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', diff --git a/app/src/utils/tauriCommands/config.ts b/app/src/utils/tauriCommands/config.ts index bc72df193..f044fb3f3 100644 --- a/app/src/utils/tauriCommands/config.ts +++ b/app/src/utils/tauriCommands/config.ts @@ -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> { + if (!isTauri()) { + throw new Error('Not running in Tauri'); + } + return await callCoreRpc>({ + method: CORE_RPC_METHODS.configGetSandboxSettings, + }); +} + +export async function openhumanUpdateSandboxSettings( + update: SandboxSettingsUpdate +): Promise> { + if (!isTauri()) { + throw new Error('Not running in Tauri'); + } + return await callCoreRpc>({ + method: CORE_RPC_METHODS.configUpdateSandboxSettings, + params: update, + }); +} + // ── Agent execution settings (action/tool timeout) ────────────────────────── /** Agent execution settings as returned by config_get_agent_settings. */ diff --git a/src/openhuman/config/ops.rs b/src/openhuman/config/ops.rs index 390a4b6b9..d5d178733 100644 --- a/src/openhuman/config/ops.rs +++ b/src/openhuman/config/ops.rs @@ -1976,6 +1976,177 @@ pub async fn get_agent_paths() -> Result, String> )) } +// ── Sandbox settings ───────────────────────────────────────────────────────── + +/// Partial update for the `[security.sandbox]` + `[runtime.docker]` blocks. +#[derive(Debug, Clone, Default)] +pub struct SandboxSettingsPatch { + pub backend: Option, + pub enabled: Option, + pub docker_image: Option, + pub docker_memory_limit_mb: Option, + pub docker_cpu_limit: Option, + pub env_passthrough: Option>, +} + +/// 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, 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, 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, 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; diff --git a/src/openhuman/config/schema/types.rs b/src/openhuman/config/schema/types.rs index 988ffc9e1..0badcd8a8 100644 --- a/src/openhuman/config/schema/types.rs +++ b/src/openhuman/config/schema/types.rs @@ -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(), diff --git a/src/openhuman/config/schemas.rs b/src/openhuman/config/schemas.rs index 46e4aaea7..b2eaad8a4 100644 --- a/src/openhuman/config/schemas.rs +++ b/src/openhuman/config/schemas.rs @@ -274,6 +274,8 @@ pub fn all_controller_schemas() -> Vec { 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 { 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 0–4).")], 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) -> Controll }) } +#[derive(Debug, Deserialize)] +struct SandboxSettingsUpdate { + backend: Option, + enabled: Option, + docker_image: Option, + docker_memory_limit_mb: Option, + docker_cpu_limit: Option, + env_passthrough: Option>, +} + +fn handle_get_sandbox_settings(_params: Map) -> ControllerFuture { + Box::pin(async move { to_json(config_rpc::get_sandbox_settings().await?) }) +} + +fn handle_update_sandbox_settings(params: Map) -> ControllerFuture { + Box::pin(async move { + let update = deserialize_params::(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(params: Map) -> Result { serde_json::from_value(Value::Object(params)).map_err(|e| format!("invalid params: {e}")) } diff --git a/src/openhuman/sandbox/ops.rs b/src/openhuman/sandbox/ops.rs index 65e1a1ac4..4758f1ab5 100644 --- a/src/openhuman/sandbox/ops.rs +++ b/src/openhuman/sandbox/ops.rs @@ -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", ]; diff --git a/tests/config_auth_app_state_connectivity_e2e.rs b/tests/config_auth_app_state_connectivity_e2e.rs index 195e92ed9..3bb783f56 100644 --- a/tests/config_auth_app_state_connectivity_e2e.rs +++ b/tests/config_auth_app_state_connectivity_e2e.rs @@ -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",