mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53:36 +00:00
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:
@@ -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 & 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 & 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>
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user