mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
feat(settings): add UI for sandbox execution backend configuration (#3265)
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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',
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 />)} />
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 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<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}"))
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user