feat: enhance sandbox risk dialog and settings management

- Updated the SandboxRiskDialog to include a checkbox for users to opt-out of future warnings, passing the state to the onConfirm callback.
- Modified SettingsView to manage the skipSandboxWarning state, allowing users to reset the warning preference.
- Enhanced DangerZoneSection to display a message when the sandbox warning is disabled and provide an option to reset this setting.
- Updated RootLayoutContent to respect the user's choice regarding the sandbox warning, auto-confirming if the user opts to skip it.
- Added skipSandboxWarning state management to the app store for persistent user preferences.
This commit is contained in:
webdevcody
2026-01-01 16:49:35 -05:00
parent 59d47928a7
commit fb46c0c9ea
5 changed files with 151 additions and 48 deletions

View File

@@ -16,10 +16,12 @@ import {
DialogTitle, DialogTitle,
} from '@/components/ui/dialog'; } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Label } from '@/components/ui/label';
interface SandboxRiskDialogProps { interface SandboxRiskDialogProps {
open: boolean; open: boolean;
onConfirm: () => void; onConfirm: (skipInFuture: boolean) => void;
onDeny: () => void; onDeny: () => void;
} }
@@ -27,6 +29,13 @@ const DOCKER_COMMAND = 'npm run dev:docker';
export function SandboxRiskDialog({ open, onConfirm, onDeny }: SandboxRiskDialogProps) { export function SandboxRiskDialog({ open, onConfirm, onDeny }: SandboxRiskDialogProps) {
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const [skipInFuture, setSkipInFuture] = useState(false);
const handleConfirm = () => {
onConfirm(skipInFuture);
// Reset checkbox state after confirmation
setSkipInFuture(false);
};
const handleCopy = async () => { const handleCopy = async () => {
try { try {
@@ -93,18 +102,34 @@ export function SandboxRiskDialog({ open, onConfirm, onDeny }: SandboxRiskDialog
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<DialogFooter className="gap-2 sm:gap-2 pt-4"> <DialogFooter className="flex-col gap-4 sm:flex-col pt-4">
<Button variant="outline" onClick={onDeny} className="px-4" data-testid="sandbox-deny"> <div className="flex items-center space-x-2 self-start">
Deny &amp; Exit <Checkbox
</Button> id="skip-sandbox-warning"
<Button checked={skipInFuture}
variant="destructive" onCheckedChange={(checked) => setSkipInFuture(checked === true)}
onClick={onConfirm} data-testid="sandbox-skip-checkbox"
className="px-4" />
data-testid="sandbox-confirm" <Label
> htmlFor="skip-sandbox-warning"
<ShieldAlert className="w-4 h-4 mr-2" />I Accept the Risks className="text-sm text-muted-foreground cursor-pointer"
</Button> >
Do not show this warning again
</Label>
</div>
<div className="flex gap-2 sm:gap-2 w-full sm:justify-end">
<Button variant="outline" onClick={onDeny} className="px-4" data-testid="sandbox-deny">
Deny &amp; Exit
</Button>
<Button
variant="destructive"
onClick={handleConfirm}
className="px-4"
data-testid="sandbox-confirm"
>
<ShieldAlert className="w-4 h-4 mr-2" />I Accept the Risks
</Button>
</div>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@@ -55,6 +55,8 @@ export function SettingsView() {
setAutoLoadClaudeMd, setAutoLoadClaudeMd,
enableSandboxMode, enableSandboxMode,
setEnableSandboxMode, setEnableSandboxMode,
skipSandboxWarning,
setSkipSandboxWarning,
promptCustomization, promptCustomization,
setPromptCustomization, setPromptCustomization,
} = useAppStore(); } = useAppStore();
@@ -184,6 +186,8 @@ export function SettingsView() {
<DangerZoneSection <DangerZoneSection
project={settingsProject} project={settingsProject}
onDeleteClick={() => setShowDeleteDialog(true)} onDeleteClick={() => setShowDeleteDialog(true)}
skipSandboxWarning={skipSandboxWarning}
onResetSandboxWarning={() => setSkipSandboxWarning(false)}
/> />
); );
default: default:

View File

@@ -1,16 +1,21 @@
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Trash2, Folder, AlertTriangle } from 'lucide-react'; import { Trash2, Folder, AlertTriangle, Shield, RotateCcw } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import type { Project } from '../shared/types'; import type { Project } from '../shared/types';
interface DangerZoneSectionProps { interface DangerZoneSectionProps {
project: Project | null; project: Project | null;
onDeleteClick: () => void; onDeleteClick: () => void;
skipSandboxWarning: boolean;
onResetSandboxWarning: () => void;
} }
export function DangerZoneSection({ project, onDeleteClick }: DangerZoneSectionProps) { export function DangerZoneSection({
if (!project) return null; project,
onDeleteClick,
skipSandboxWarning,
onResetSandboxWarning,
}: DangerZoneSectionProps) {
return ( return (
<div <div
className={cn( className={cn(
@@ -28,35 +33,75 @@ export function DangerZoneSection({ project, onDeleteClick }: DangerZoneSectionP
<h2 className="text-lg font-semibold text-foreground tracking-tight">Danger Zone</h2> <h2 className="text-lg font-semibold text-foreground tracking-tight">Danger Zone</h2>
</div> </div>
<p className="text-sm text-muted-foreground/80 ml-12"> <p className="text-sm text-muted-foreground/80 ml-12">
Permanently remove this project from Automaker. Destructive actions and reset options.
</p> </p>
</div> </div>
<div className="p-6"> <div className="p-6 space-y-4">
<div className="flex items-center justify-between gap-4 p-4 rounded-xl bg-destructive/5 border border-destructive/10"> {/* Sandbox Warning Reset */}
<div className="flex items-center gap-3.5 min-w-0"> {skipSandboxWarning && (
<div className="w-11 h-11 rounded-xl bg-gradient-to-br from-brand-500/15 to-brand-600/10 border border-brand-500/20 flex items-center justify-center shrink-0"> <div className="flex items-center justify-between gap-4 p-4 rounded-xl bg-destructive/5 border border-destructive/10">
<Folder className="w-5 h-5 text-brand-500" /> <div className="flex items-center gap-3.5 min-w-0">
</div> <div className="w-11 h-11 rounded-xl bg-gradient-to-br from-destructive/15 to-destructive/10 border border-destructive/20 flex items-center justify-center shrink-0">
<div className="min-w-0"> <Shield className="w-5 h-5 text-destructive" />
<p className="font-medium text-foreground truncate">{project.name}</p> </div>
<p className="text-xs text-muted-foreground/70 truncate mt-0.5">{project.path}</p> <div className="min-w-0">
<p className="font-medium text-foreground">Sandbox Warning Disabled</p>
<p className="text-xs text-muted-foreground/70 mt-0.5">
The sandbox environment warning is hidden on startup
</p>
</div>
</div> </div>
<Button
variant="outline"
onClick={onResetSandboxWarning}
data-testid="reset-sandbox-warning-button"
className={cn(
'shrink-0 gap-2',
'transition-all duration-200 ease-out',
'hover:scale-[1.02] active:scale-[0.98]'
)}
>
<RotateCcw className="w-4 h-4" />
Reset
</Button>
</div> </div>
<Button )}
variant="destructive"
onClick={onDeleteClick} {/* Project Delete */}
data-testid="delete-project-button" {project && (
className={cn( <div className="flex items-center justify-between gap-4 p-4 rounded-xl bg-destructive/5 border border-destructive/10">
'shrink-0', <div className="flex items-center gap-3.5 min-w-0">
'shadow-md shadow-destructive/20 hover:shadow-lg hover:shadow-destructive/25', <div className="w-11 h-11 rounded-xl bg-gradient-to-br from-brand-500/15 to-brand-600/10 border border-brand-500/20 flex items-center justify-center shrink-0">
'transition-all duration-200 ease-out', <Folder className="w-5 h-5 text-brand-500" />
'hover:scale-[1.02] active:scale-[0.98]' </div>
)} <div className="min-w-0">
> <p className="font-medium text-foreground truncate">{project.name}</p>
<Trash2 className="w-4 h-4 mr-2" /> <p className="text-xs text-muted-foreground/70 truncate mt-0.5">{project.path}</p>
Delete Project </div>
</Button> </div>
</div> <Button
variant="destructive"
onClick={onDeleteClick}
data-testid="delete-project-button"
className={cn(
'shrink-0',
'shadow-md shadow-destructive/20 hover:shadow-lg hover:shadow-destructive/25',
'transition-all duration-200 ease-out',
'hover:scale-[1.02] active:scale-[0.98]'
)}
>
<Trash2 className="w-4 h-4 mr-2" />
Delete Project
</Button>
</div>
)}
{/* Empty state when nothing to show */}
{!skipSandboxWarning && !project && (
<p className="text-sm text-muted-foreground/60 text-center py-4">
No danger zone actions available.
</p>
)}
</div> </div>
</div> </div>
); );

View File

@@ -25,7 +25,13 @@ import { SandboxRejectionScreen } from '@/components/dialogs/sandbox-rejection-s
function RootLayoutContent() { function RootLayoutContent() {
const location = useLocation(); const location = useLocation();
const { setIpcConnected, currentProject, getEffectiveTheme } = useAppStore(); const {
setIpcConnected,
currentProject,
getEffectiveTheme,
skipSandboxWarning,
setSkipSandboxWarning,
} = useAppStore();
const { setupComplete } = useSetupStore(); const { setupComplete } = useSetupStore();
const navigate = useNavigate(); const navigate = useNavigate();
const [isMounted, setIsMounted] = useState(false); const [isMounted, setIsMounted] = useState(false);
@@ -106,6 +112,9 @@ function RootLayoutContent() {
if (result.isContainerized) { if (result.isContainerized) {
// Running in a container, no warning needed // Running in a container, no warning needed
setSandboxStatus('containerized'); setSandboxStatus('containerized');
} else if (skipSandboxWarning) {
// User opted to skip the warning, auto-confirm
setSandboxStatus('confirmed');
} else { } else {
// Not containerized, show warning dialog // Not containerized, show warning dialog
setSandboxStatus('needs-confirmation'); setSandboxStatus('needs-confirmation');
@@ -113,17 +122,27 @@ function RootLayoutContent() {
} catch (error) { } catch (error) {
console.error('[Sandbox] Failed to check environment:', error); console.error('[Sandbox] Failed to check environment:', error);
// On error, assume not containerized and show warning // On error, assume not containerized and show warning
setSandboxStatus('needs-confirmation'); if (skipSandboxWarning) {
setSandboxStatus('confirmed');
} else {
setSandboxStatus('needs-confirmation');
}
} }
}; };
checkSandbox(); checkSandbox();
}, [sandboxStatus]); }, [sandboxStatus, skipSandboxWarning]);
// Handle sandbox risk confirmation // Handle sandbox risk confirmation
const handleSandboxConfirm = useCallback(() => { const handleSandboxConfirm = useCallback(
setSandboxStatus('confirmed'); (skipInFuture: boolean) => {
}, []); if (skipInFuture) {
setSkipSandboxWarning(true);
}
setSandboxStatus('confirmed');
},
[setSkipSandboxWarning]
);
// Handle sandbox risk denial // Handle sandbox risk denial
const handleSandboxDeny = useCallback(async () => { const handleSandboxDeny = useCallback(async () => {

View File

@@ -487,6 +487,7 @@ export interface AppState {
// Claude Agent SDK Settings // Claude Agent SDK Settings
autoLoadClaudeMd: boolean; // Auto-load CLAUDE.md files using SDK's settingSources option autoLoadClaudeMd: boolean; // Auto-load CLAUDE.md files using SDK's settingSources option
enableSandboxMode: boolean; // Enable sandbox mode for bash commands (may cause issues on some systems) enableSandboxMode: boolean; // Enable sandbox mode for bash commands (may cause issues on some systems)
skipSandboxWarning: boolean; // Skip the sandbox environment warning dialog on startup
// MCP Servers // MCP Servers
mcpServers: MCPServerConfig[]; // List of configured MCP servers for agent use mcpServers: MCPServerConfig[]; // List of configured MCP servers for agent use
@@ -775,6 +776,7 @@ export interface AppActions {
// Claude Agent SDK Settings actions // Claude Agent SDK Settings actions
setAutoLoadClaudeMd: (enabled: boolean) => Promise<void>; setAutoLoadClaudeMd: (enabled: boolean) => Promise<void>;
setEnableSandboxMode: (enabled: boolean) => Promise<void>; setEnableSandboxMode: (enabled: boolean) => Promise<void>;
setSkipSandboxWarning: (skip: boolean) => Promise<void>;
setMcpAutoApproveTools: (enabled: boolean) => Promise<void>; setMcpAutoApproveTools: (enabled: boolean) => Promise<void>;
setMcpUnrestrictedTools: (enabled: boolean) => Promise<void>; setMcpUnrestrictedTools: (enabled: boolean) => Promise<void>;
@@ -976,6 +978,7 @@ const initialState: AppState = {
validationModel: 'opus', // Default to opus for GitHub issue validation validationModel: 'opus', // Default to opus for GitHub issue validation
autoLoadClaudeMd: false, // Default to disabled (user must opt-in) autoLoadClaudeMd: false, // Default to disabled (user must opt-in)
enableSandboxMode: false, // Default to disabled (can be enabled for additional security) enableSandboxMode: false, // Default to disabled (can be enabled for additional security)
skipSandboxWarning: false, // Default to disabled (show sandbox warning dialog)
mcpServers: [], // No MCP servers configured by default mcpServers: [], // No MCP servers configured by default
mcpAutoApproveTools: true, // Default to enabled - bypass permission prompts for MCP tools mcpAutoApproveTools: true, // Default to enabled - bypass permission prompts for MCP tools
mcpUnrestrictedTools: true, // Default to enabled - don't filter allowedTools when MCP enabled mcpUnrestrictedTools: true, // Default to enabled - don't filter allowedTools when MCP enabled
@@ -1623,6 +1626,12 @@ export const useAppStore = create<AppState & AppActions>()(
const { syncSettingsToServer } = await import('@/hooks/use-settings-migration'); const { syncSettingsToServer } = await import('@/hooks/use-settings-migration');
await syncSettingsToServer(); await syncSettingsToServer();
}, },
setSkipSandboxWarning: async (skip) => {
set({ skipSandboxWarning: skip });
// Sync to server settings file
const { syncSettingsToServer } = await import('@/hooks/use-settings-migration');
await syncSettingsToServer();
},
setMcpAutoApproveTools: async (enabled) => { setMcpAutoApproveTools: async (enabled) => {
set({ mcpAutoApproveTools: enabled }); set({ mcpAutoApproveTools: enabled });
// Sync to server settings file // Sync to server settings file
@@ -2921,6 +2930,7 @@ export const useAppStore = create<AppState & AppActions>()(
validationModel: state.validationModel, validationModel: state.validationModel,
autoLoadClaudeMd: state.autoLoadClaudeMd, autoLoadClaudeMd: state.autoLoadClaudeMd,
enableSandboxMode: state.enableSandboxMode, enableSandboxMode: state.enableSandboxMode,
skipSandboxWarning: state.skipSandboxWarning,
// MCP settings // MCP settings
mcpServers: state.mcpServers, mcpServers: state.mcpServers,
mcpAutoApproveTools: state.mcpAutoApproveTools, mcpAutoApproveTools: state.mcpAutoApproveTools,