feat: add sandbox environment checks and user confirmation dialogs

- Introduced a new endpoint to check if the application is running in a containerized environment, allowing the UI to display appropriate risk warnings.
- Added a confirmation dialog for users when running outside a sandbox, requiring acknowledgment of potential risks before proceeding.
- Implemented a rejection screen for users who deny sandbox risk confirmation, providing options to restart in a container or reload the application.
- Updated the main application logic to handle sandbox status checks and user responses effectively, enhancing security and user experience.
This commit is contained in:
Test User
2025-12-31 21:00:23 -05:00
parent 2828431cca
commit b9a6e29ee8
12 changed files with 388 additions and 3 deletions

View File

@@ -8,10 +8,21 @@ import {
} from '@/contexts/file-browser-context';
import { useAppStore } from '@/store/app-store';
import { useSetupStore } from '@/store/setup-store';
import { getElectronAPI } from '@/lib/electron';
import { initApiKey, isElectronMode, verifySession } from '@/lib/http-api-client';
import { getElectronAPI, isElectron } from '@/lib/electron';
import {
initApiKey,
isElectronMode,
verifySession,
checkSandboxEnvironment,
} from '@/lib/http-api-client';
import { Toaster } from 'sonner';
import { ThemeOption, themeOptions } from '@/config/theme-options';
import { SandboxRiskDialog } from '@/components/dialogs/sandbox-risk-dialog';
import { SandboxRejectionScreen } from '@/components/dialogs/sandbox-rejection-screen';
// Session storage key for sandbox risk acknowledgment
const SANDBOX_RISK_ACKNOWLEDGED_KEY = 'automaker-sandbox-risk-acknowledged';
const SANDBOX_DENIED_KEY = 'automaker-sandbox-denied';
function RootLayoutContent() {
const location = useLocation();
@@ -27,6 +38,20 @@ function RootLayoutContent() {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const { openFileBrowser } = useFileBrowser();
// Sandbox environment check state
type SandboxStatus = 'pending' | 'containerized' | 'needs-confirmation' | 'denied' | 'confirmed';
const [sandboxStatus, setSandboxStatus] = useState<SandboxStatus>(() => {
// Check if user previously denied in this session
if (sessionStorage.getItem(SANDBOX_DENIED_KEY)) {
return 'denied';
}
// Check if user previously acknowledged in this session
if (sessionStorage.getItem(SANDBOX_RISK_ACKNOWLEDGED_KEY)) {
return 'confirmed';
}
return 'pending';
});
// Hidden streamer panel - opens with "\" key
const handleStreamerPanelShortcut = useCallback((event: KeyboardEvent) => {
const activeElement = document.activeElement;
@@ -73,6 +98,63 @@ function RootLayoutContent() {
setIsMounted(true);
}, []);
// Check sandbox environment on mount
useEffect(() => {
// Skip if already decided
if (sandboxStatus !== 'pending') {
return;
}
const checkSandbox = async () => {
try {
const result = await checkSandboxEnvironment();
if (result.isContainerized) {
// Running in a container, no warning needed
setSandboxStatus('containerized');
} else {
// Not containerized, show warning dialog
setSandboxStatus('needs-confirmation');
}
} catch (error) {
console.error('[Sandbox] Failed to check environment:', error);
// On error, assume not containerized and show warning
setSandboxStatus('needs-confirmation');
}
};
checkSandbox();
}, [sandboxStatus]);
// Handle sandbox risk confirmation
const handleSandboxConfirm = useCallback(() => {
sessionStorage.setItem(SANDBOX_RISK_ACKNOWLEDGED_KEY, 'true');
setSandboxStatus('confirmed');
}, []);
// Handle sandbox risk denial
const handleSandboxDeny = useCallback(async () => {
sessionStorage.setItem(SANDBOX_DENIED_KEY, 'true');
if (isElectron()) {
// In Electron mode, quit the application
// Use window.electronAPI directly since getElectronAPI() returns the HTTP client
try {
const electronAPI = window.electronAPI;
if (electronAPI?.quit) {
await electronAPI.quit();
} else {
console.error('[Sandbox] quit() not available on electronAPI');
}
} catch (error) {
console.error('[Sandbox] Failed to quit app:', error);
}
} else {
// In web mode, show rejection screen
setSandboxStatus('denied');
}
}, []);
// Initialize authentication
// - Electron mode: Uses API key from IPC (header-based auth)
// - Web mode: Uses HTTP-only session cookie
@@ -201,11 +283,31 @@ function RootLayoutContent() {
const isSetupRoute = location.pathname === '/setup';
const isLoginRoute = location.pathname === '/login';
// Show rejection screen if user denied sandbox risk (web mode only)
if (sandboxStatus === 'denied' && !isElectron()) {
return <SandboxRejectionScreen />;
}
// Show loading while checking sandbox environment
if (sandboxStatus === 'pending') {
return (
<main className="flex h-screen items-center justify-center" data-testid="app-container">
<div className="text-muted-foreground">Checking environment...</div>
</main>
);
}
// Show login page (full screen, no sidebar)
if (isLoginRoute) {
return (
<main className="h-screen overflow-hidden" data-testid="app-container">
<Outlet />
{/* Show sandbox dialog on top of login page if needed */}
<SandboxRiskDialog
open={sandboxStatus === 'needs-confirmation'}
onConfirm={handleSandboxConfirm}
onDeny={handleSandboxDeny}
/>
</main>
);
}
@@ -228,6 +330,12 @@ function RootLayoutContent() {
return (
<main className="h-screen overflow-hidden" data-testid="app-container">
<Outlet />
{/* Show sandbox dialog on top of setup page if needed */}
<SandboxRiskDialog
open={sandboxStatus === 'needs-confirmation'}
onConfirm={handleSandboxConfirm}
onDeny={handleSandboxDeny}
/>
</main>
);
}
@@ -249,6 +357,13 @@ function RootLayoutContent() {
}`}
/>
<Toaster richColors position="bottom-right" />
{/* Show sandbox dialog if needed */}
<SandboxRiskDialog
open={sandboxStatus === 'needs-confirmation'}
onConfirm={handleSandboxConfirm}
onDeny={handleSandboxDeny}
/>
</main>
);
}