Real-time approval modal, settings pages, tool calling auto-detect button, and IPC command wrappers. - Add ShellApprovalModal component for Tier 2 command approvals - Add ShellExecution settings page - Add KubeconfigManager settings page - Update AIProviders page with tool calling detection button - Add shell command wrappers to tauriCommands.ts - Add routes for new settings pages Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
167 lines
5.5 KiB
TypeScript
167 lines
5.5 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { listen, UnlistenFn } from '@tauri-apps/api/event';
|
|
import { Button } from '@/components/ui';
|
|
import { Badge } from '@/components/ui';
|
|
import { AlertTriangle, Shield, Terminal, X } from 'lucide-react';
|
|
import { respondToShellApprovalCmd } from '@/lib/tauriCommands';
|
|
|
|
interface ShellApprovalRequest {
|
|
approval_id: string;
|
|
command: string;
|
|
tier: number;
|
|
reasoning: string;
|
|
risk_factors: string[];
|
|
}
|
|
|
|
export function ShellApprovalModal() {
|
|
const [request, setRequest] = useState<ShellApprovalRequest | null>(null);
|
|
const [isResponding, setIsResponding] = useState(false);
|
|
|
|
useEffect(() => {
|
|
let unlisten: UnlistenFn;
|
|
|
|
const setupListener = async () => {
|
|
unlisten = await listen<ShellApprovalRequest>(
|
|
'shell:approval-needed',
|
|
(event) => {
|
|
setRequest(event.payload);
|
|
}
|
|
);
|
|
};
|
|
|
|
setupListener();
|
|
|
|
return () => {
|
|
if (unlisten) {
|
|
unlisten();
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
const handleResponse = async (decision: string) => {
|
|
if (!request) return;
|
|
|
|
setIsResponding(true);
|
|
try {
|
|
await respondToShellApprovalCmd(request.approval_id, decision);
|
|
setRequest(null);
|
|
} catch (error) {
|
|
console.error('Failed to respond to approval:', error);
|
|
} finally {
|
|
setIsResponding(false);
|
|
}
|
|
};
|
|
|
|
const handleDeny = () => handleResponse('deny');
|
|
const handleAllowOnce = () => handleResponse('allow_once');
|
|
const handleAllowSession = () => handleResponse('allow_session');
|
|
|
|
if (!request) return null;
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
|
<div className="relative bg-card border rounded-lg shadow-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto m-4">
|
|
{/* Header */}
|
|
<div className="sticky top-0 bg-card border-b p-4 flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<Shield className="h-5 w-5 text-yellow-600" />
|
|
<h2 className="text-lg font-semibold">Command Approval Required</h2>
|
|
</div>
|
|
<button
|
|
onClick={() => !isResponding && setRequest(null)}
|
|
disabled={isResponding}
|
|
className="p-1 rounded hover:bg-accent text-muted-foreground"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="p-6 space-y-4">
|
|
<p className="text-sm text-muted-foreground">
|
|
This command requires your approval before execution
|
|
</p>
|
|
|
|
{/* Command Display */}
|
|
<div className="rounded-lg bg-slate-950 p-4 font-mono text-sm">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<Terminal className="h-4 w-4 text-slate-400" />
|
|
<span className="text-slate-400">Command:</span>
|
|
</div>
|
|
<code className="text-green-400">{request.command}</code>
|
|
</div>
|
|
|
|
{/* Tier Badge */}
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm text-muted-foreground">Safety Tier:</span>
|
|
<Badge variant="outline" className="bg-yellow-50 text-yellow-700 border-yellow-300">
|
|
Tier {request.tier}
|
|
</Badge>
|
|
</div>
|
|
|
|
{/* Reasoning */}
|
|
<div className="rounded-lg border border-yellow-300 bg-yellow-50 p-4">
|
|
<div className="flex gap-2">
|
|
<AlertTriangle className="h-4 w-4 text-yellow-700 flex-shrink-0 mt-0.5" />
|
|
<div>
|
|
<div className="font-medium text-yellow-900 mb-1">Why approval is needed:</div>
|
|
<div className="text-sm text-yellow-800">{request.reasoning}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Risk Factors */}
|
|
{request.risk_factors.length > 0 && (
|
|
<div className="space-y-2">
|
|
<div className="text-sm font-medium">Risk Factors:</div>
|
|
<ul className="list-disc list-inside text-sm text-muted-foreground space-y-1">
|
|
{request.risk_factors.map((factor, idx) => (
|
|
<li key={idx}>{factor}</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
|
|
{/* Safety Notice */}
|
|
<div className="rounded-lg bg-muted p-3 text-sm text-muted-foreground">
|
|
<div className="font-medium mb-1">Safety Controls:</div>
|
|
<ul className="list-disc list-inside space-y-1 text-xs">
|
|
<li>Command execution is logged and auditable</li>
|
|
<li>30-second timeout protection</li>
|
|
<li>PII detection before execution</li>
|
|
<li>Output is captured for review</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="sticky bottom-0 bg-card border-t p-4 flex flex-col sm:flex-row gap-2 justify-end">
|
|
<Button
|
|
variant="destructive"
|
|
onClick={handleDeny}
|
|
disabled={isResponding}
|
|
className="w-full sm:w-auto"
|
|
>
|
|
Deny
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
onClick={handleAllowOnce}
|
|
disabled={isResponding}
|
|
className="w-full sm:w-auto"
|
|
>
|
|
Allow Once
|
|
</Button>
|
|
<Button
|
|
onClick={handleAllowSession}
|
|
disabled={isResponding}
|
|
className="w-full sm:w-auto"
|
|
>
|
|
Allow for Session
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|