From dd9e6c0d3d9184ce09d871e7a013f1bf90a9ff9b Mon Sep 17 00:00:00 2001 From: Shaun Arman Date: Fri, 5 Jun 2026 08:14:03 -0500 Subject: [PATCH] feat: add shell execution and kubeconfig management UI 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 --- src/App.tsx | 6 + src/components/ShellApprovalModal.tsx | 166 +++++++++++++++ src/lib/tauriCommands.ts | 55 +++++ src/pages/Settings/AIProviders.tsx | 127 ++++++++++-- src/pages/Settings/KubeconfigManager.tsx | 251 +++++++++++++++++++++++ src/pages/Settings/ShellExecution.tsx | 244 ++++++++++++++++++++++ 6 files changed, 835 insertions(+), 14 deletions(-) create mode 100644 src/components/ShellApprovalModal.tsx create mode 100644 src/pages/Settings/KubeconfigManager.tsx create mode 100644 src/pages/Settings/ShellExecution.tsx diff --git a/src/App.tsx b/src/App.tsx index 6917f5ff..1a6b7d1b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -30,6 +30,9 @@ import Ollama from "@/pages/Settings/Ollama"; import Integrations from "@/pages/Settings/Integrations"; import MCPServers from "@/pages/Settings/MCPServers"; import Security from "@/pages/Settings/Security"; +import ShellExecution from "@/pages/Settings/ShellExecution"; +import KubeconfigManager from "@/pages/Settings/KubeconfigManager"; +import { ShellApprovalModal } from "@/components/ShellApprovalModal"; const navItems = [ { to: "/", icon: Home, label: "Dashboard" }, @@ -177,9 +180,12 @@ export default function App() { } /> } /> } /> + } /> + } /> + ); } diff --git a/src/components/ShellApprovalModal.tsx b/src/components/ShellApprovalModal.tsx new file mode 100644 index 00000000..074b5d29 --- /dev/null +++ b/src/components/ShellApprovalModal.tsx @@ -0,0 +1,166 @@ +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(null); + const [isResponding, setIsResponding] = useState(false); + + useEffect(() => { + let unlisten: UnlistenFn; + + const setupListener = async () => { + unlisten = await listen( + '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 ( +
+
+ {/* Header */} +
+
+ +

Command Approval Required

+
+ +
+ + {/* Content */} +
+

+ This command requires your approval before execution +

+ + {/* Command Display */} +
+
+ + Command: +
+ {request.command} +
+ + {/* Tier Badge */} +
+ Safety Tier: + + Tier {request.tier} + +
+ + {/* Reasoning */} +
+
+ +
+
Why approval is needed:
+
{request.reasoning}
+
+
+
+ + {/* Risk Factors */} + {request.risk_factors.length > 0 && ( +
+
Risk Factors:
+
    + {request.risk_factors.map((factor, idx) => ( +
  • {factor}
  • + ))} +
+
+ )} + + {/* Safety Notice */} +
+
Safety Controls:
+
    +
  • Command execution is logged and auditable
  • +
  • 30-second timeout protection
  • +
  • PII detection before execution
  • +
  • Output is captured for review
  • +
+
+
+ + {/* Footer */} +
+ + + +
+
+
+ ); +} diff --git a/src/lib/tauriCommands.ts b/src/lib/tauriCommands.ts index cdac92d9..c9836deb 100644 --- a/src/lib/tauriCommands.ts +++ b/src/lib/tauriCommands.ts @@ -17,6 +17,7 @@ export interface ProviderConfig { session_id?: string; user_id?: string; use_datastore_upload?: boolean; + supports_tool_calling?: boolean; } export interface Message { @@ -333,6 +334,9 @@ export const applyRedactionsCmd = (logFileId: string, approvedSpanIds: string[]) export const testProviderConnectionCmd = (providerConfig: ProviderConfig) => invoke("test_provider_connection", { providerConfig }); +export const detectToolCallingSupportCmd = (providerConfig: ProviderConfig) => + invoke("detect_tool_calling_support", { providerConfig }); + export const createIssueCmd = (newIssue: NewIssue) => invoke("create_issue", { title: newIssue.title, @@ -683,3 +687,54 @@ export const listAllImageAttachmentsCmd = (search?: string, issueId?: string) => search: search ?? null, issueId: issueId ?? null, }); + +// ─── Shell Execution Commands ──────────────────────────────────────────────── + +export interface KubeconfigInfo { + id: string; + name: string; + context: string; + cluster_url?: string; + is_active: boolean; +} + +export interface CommandExecution { + id: string; + command: string; + tier: number; + approval_status: string; + exit_code: number | null; + stdout: string | null; + stderr: string | null; + execution_time_ms: number | null; + executed_at: string; +} + +export interface KubectlStatus { + installed: boolean; + path?: string; + version?: string; +} + +export const uploadKubeconfigCmd = (name: string, content: string) => + invoke("upload_kubeconfig", { name, content }); + +export const listKubeconfigsCmd = () => + invoke("list_kubeconfigs"); + +export const activateKubeconfigCmd = (id: string) => + invoke("activate_kubeconfig", { id }); + +export const deleteKubeconfigCmd = (id: string) => + invoke("delete_kubeconfig", { id }); + +export const respondToShellApprovalCmd = (approvalId: string, decision: string) => + invoke("respond_to_shell_approval", { approvalId, decision }); + +export const listCommandExecutionsCmd = (issueId?: string) => + invoke("list_command_executions", { + issueId: issueId ?? null, + }); + +export const checkKubectlInstalledCmd = () => + invoke("check_kubectl_installed"); diff --git a/src/pages/Settings/AIProviders.tsx b/src/pages/Settings/AIProviders.tsx index 0cec08f6..996fe32b 100644 --- a/src/pages/Settings/AIProviders.tsx +++ b/src/pages/Settings/AIProviders.tsx @@ -19,10 +19,13 @@ import { import { useSettingsStore } from "@/stores/settingsStore"; import { testProviderConnectionCmd, + detectToolCallingSupportCmd, saveAiProviderCmd, loadAiProvidersCmd, deleteAiProviderCmd, + listOllamaModelsCmd, type ProviderConfig, + type OllamaModel, } from "@/lib/tauriCommands"; export const CUSTOM_REST_MODELS = [ @@ -64,6 +67,7 @@ const emptyProvider: ProviderConfig = { api_format: undefined, session_id: undefined, user_id: undefined, + supports_tool_calling: false, }; export default function AIProviders() { @@ -81,9 +85,11 @@ export default function AIProviders() { const [isAdding, setIsAdding] = useState(false); const [form, setForm] = useState({ ...emptyProvider }); const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null); - const [isTesting, setIsTesting] = useState(false); + const [isTestingConnection, setIsTestingConnection] = useState(false); + const [isDetectingToolCalling, setIsDetectingToolCalling] = useState(false); const [isCustomModel, setIsCustomModel] = useState(false); const [customModelInput, setCustomModelInput] = useState(""); + const [ollamaModels, setOllamaModels] = useState([]); // Load providers from database on mount // Note: Auto-testing of active provider is handled in App.tsx on startup @@ -99,6 +105,22 @@ export default function AIProviders() { loadProviders(); }, [setProviders]); + // Load Ollama models when form provider type changes to ollama + useEffect(() => { + if (form.provider_type === "ollama") { + const loadOllamaModels = async () => { + try { + const models = await listOllamaModelsCmd(); + setOllamaModels(models); + } catch (err) { + console.error("Failed to load Ollama models:", err); + setOllamaModels([]); + } + }; + loadOllamaModels(); + } + }, [form.provider_type]); + const startAdd = () => { setForm({ ...emptyProvider }); setEditIndex(null); @@ -172,7 +194,7 @@ export default function AIProviders() { }; const handleTest = async () => { - setIsTesting(true); + setIsTestingConnection(true); setTestResult(null); try { const response = await testProviderConnectionCmd(form); @@ -180,7 +202,27 @@ export default function AIProviders() { } catch (err) { setTestResult({ success: false, message: String(err) }); } finally { - setIsTesting(false); + setIsTestingConnection(false); + } + }; + + const handleAutoDetectToolCalling = async () => { + setIsDetectingToolCalling(true); + setTestResult(null); + try { + const supportsTools = await detectToolCallingSupportCmd(form); + // Use functional update to avoid stale closure + setForm((prev) => ({ ...prev, supports_tool_calling: supportsTools })); + setTestResult({ + success: supportsTools, // Align success with actual outcome + message: supportsTools + ? "✅ Tool calling supported! Checkbox enabled automatically." + : "⚠️ Tool calling not supported. Checkbox disabled automatically.", + }); + } catch (err) { + setTestResult({ success: false, message: `Auto-detect failed: ${String(err)}` }); + } finally { + setIsDetectingToolCalling(false); } }; @@ -289,12 +331,14 @@ export default function AIProviders() { const type = v as ProviderConfig["provider_type"]; const defaults: Partial = type === "ollama" - ? { api_url: "http://localhost:11434", api_key: "", model: "llama3.2:3b" } + ? { api_url: "http://localhost:11434", api_key: "", model: "llama3.2:3b", supports_tool_calling: true } : type === "openai" - ? { api_url: "https://api.openai.com/v1" } + ? { api_url: "https://api.openai.com/v1", supports_tool_calling: true } : type === "anthropic" - ? { api_url: "https://api.anthropic.com" } - : {}; + ? { api_url: "https://api.anthropic.com", supports_tool_calling: true } + : type === "azure" + ? { supports_tool_calling: true } + : { supports_tool_calling: false }; // Custom providers default to false setForm({ ...form, provider_type: type, ...defaults }); }} > @@ -332,11 +376,35 @@ export default function AIProviders() { {!(form.provider_type === "custom" && form.api_format === CUSTOM_REST_FORMAT) && (
- setForm({ ...form, model: e.target.value })} - placeholder="gpt-4o" - /> + {form.provider_type === "ollama" ? ( + + ) : ( + setForm({ ...form, model: e.target.value })} + placeholder="gpt-4o" + /> + )}
)} @@ -506,6 +574,37 @@ export default function AIProviders() { )} )} + + {/* Tool Calling Support Toggle */} +
+
+
+ +

+ Enable if this provider supports function/tool calling for shell execution and integrations +

+
+ + setForm({ ...form, supports_tool_calling: e.target.checked }) + } + className="h-5 w-5 rounded border-gray-300 text-primary focus:ring-primary cursor-pointer" + /> +
+ +
)} @@ -532,8 +631,8 @@ export default function AIProviders() {
- + + )} + + + + {/* Configs List */} + + + + + Configured Clusters ({configs.length}) + + + + {configs.length === 0 ? ( +

+ No kubeconfig files uploaded yet +

+ ) : ( +
+ {configs.map((config) => ( +
+
+
+
+

{config.name}

+ {config.is_active && ( + + + Active + + )} +
+
+
+ Context: {config.context} +
+ {config.cluster_url && ( +
+ Cluster: {config.cluster_url} +
+ )} +
+
+ +
+ {!config.is_active && ( + + )} + +
+
+
+ ))} +
+ )} +
+
+ + {/* Info Card */} + + + About Kubeconfig Files + + +

+ Kubeconfig files contain authentication credentials and cluster connection details for + kubectl commands. +

+
    +
  • Upload your cluster's kubeconfig file (usually ~/.kube/config)
  • +
  • Multiple clusters can be configured and switched between
  • +
  • The active configuration is used for kubectl commands
  • +
  • All kubeconfig files are encrypted using AES-256-GCM
  • +
+
+
+
+ ); +} diff --git a/src/pages/Settings/ShellExecution.tsx b/src/pages/Settings/ShellExecution.tsx new file mode 100644 index 00000000..50376a1f --- /dev/null +++ b/src/pages/Settings/ShellExecution.tsx @@ -0,0 +1,244 @@ +import { useState, useEffect } from 'react'; +import { Terminal, CheckCircle, XCircle, Shield, History } from 'lucide-react'; +import { Button, Card, CardHeader, CardTitle, CardContent, Badge } from '@/components/ui'; +import { Link } from 'react-router-dom'; +import { + checkKubectlInstalledCmd, + listCommandExecutionsCmd, + type KubectlStatus, + type CommandExecution, +} from '@/lib/tauriCommands'; + +export default function ShellExecution() { + const [kubectlStatus, setKubectlStatus] = useState(null); + const [executions, setExecutions] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(''); + + const loadKubectlStatus = async () => { + try { + const status = await checkKubectlInstalledCmd(); + setKubectlStatus(status); + } catch (err) { + setError(String(err)); + } + }; + + const loadExecutions = async () => { + setIsLoading(true); + try { + const data = await listCommandExecutionsCmd(); + setExecutions(data); + } catch (err) { + setError(String(err)); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + loadKubectlStatus(); + loadExecutions(); + }, []); + + const getTierBadge = (tier: number) => { + const colors = { + 1: 'bg-green-100 text-green-700 border-green-300', + 2: 'bg-yellow-100 text-yellow-700 border-yellow-300', + 3: 'bg-red-100 text-red-700 border-red-300', + }; + return colors[tier as keyof typeof colors] || colors[1]; + }; + + const getStatusBadge = (status: string) => { + const config = { + auto: { label: 'Auto-executed', color: 'bg-blue-100 text-blue-700 border-blue-300' }, + approved: { label: 'Approved', color: 'bg-green-100 text-green-700 border-green-300' }, + denied: { label: 'Denied', color: 'bg-red-100 text-red-700 border-red-300' }, + }; + const statusConfig = config[status as keyof typeof config] || config.auto; + return statusConfig; + }; + + return ( +
+
+

Shell Execution

+

+ Configure and monitor autonomous shell command execution with intelligent safety controls +

+
+ + {error && ( +
+ {error} +
+ )} + + {/* kubectl Status */} + + + + + kubectl Status + + + + {kubectlStatus ? ( + <> +
+ {kubectlStatus.installed ? ( + <> + + kubectl is installed + + ) : ( + <> + + kubectl is not installed + + )} +
+ + {kubectlStatus.path && ( +
+ Path: {kubectlStatus.path} +
+ )} + + {kubectlStatus.version && ( +
+
{kubectlStatus.version}
+
+ )} + + ) : ( +

Checking kubectl status...

+ )} + +
+ + + +
+
+
+ + {/* Safety Architecture */} + + + + + Safety Architecture + + + +

+ Commands are automatically classified into three safety tiers: +

+ +
+
+ Tier 1 +
+
Auto-execute (Read-only)
+
+ kubectl get, describe, logs | cat, grep, ls +
+
+
+ +
+ Tier 2 +
+
Require approval (Mutating)
+
+ kubectl apply, delete, scale | ssh, chmod, systemctl restart +
+
+
+ +
+ Tier 3 +
+
Always deny (Destructive)
+
+ rm -rf, shutdown, mkfs, dd +
+
+
+
+
+
+ + {/* Command Execution History */} + + + + + Recent Command Executions ({executions.length}) + + + + {isLoading ? ( +

Loading...

+ ) : executions.length === 0 ? ( +

+ No command executions yet +

+ ) : ( +
+ {executions.slice(0, 10).map((exec) => { + const statusConfig = getStatusBadge(exec.approval_status); + return ( +
+
+
+ + {exec.command} + +
+
+ + T{exec.tier} + + + {statusConfig.label} + +
+
+ +
+ {exec.exit_code !== undefined && ( + + Exit: {exec.exit_code} + + )} + {exec.execution_time_ms !== undefined && ( + {exec.execution_time_ms}ms + )} + {new Date(exec.executed_at).toLocaleString()} +
+ + {exec.stdout && ( +
+ + Show output + +
+                          {exec.stdout}
+                        
+
+ )} +
+ ); + })} +
+ )} +
+
+
+ ); +}