diff --git a/server/claude-sdk.js b/server/claude-sdk.js index 1abf70893..836669124 100644 --- a/server/claude-sdk.js +++ b/server/claude-sdk.js @@ -13,6 +13,9 @@ */ import { query } from '@anthropic-ai/claude-agent-sdk'; +// Used to mint unique approval request IDs when randomUUID is not available. +// This keeps parallel tool approvals from colliding; it does not add any crypto/security guarantees. +import crypto from 'crypto'; import { promises as fs } from 'fs'; import path from 'path'; import os from 'os'; @@ -20,6 +23,124 @@ import { CLAUDE_MODELS } from '../shared/modelConstants.js'; // Session tracking: Map of session IDs to active query instances const activeSessions = new Map(); +// In-memory registry of pending tool approvals keyed by requestId. +// This does not persist approvals or share across processes; it exists so the +// SDK can pause tool execution while the UI decides what to do. +const pendingToolApprovals = new Map(); + +// Default approval timeout kept under the SDK's 60s control timeout. +// This does not change SDK limits; it only defines how long we wait for the UI, +// introduced to avoid hanging the run when no decision arrives. +const TOOL_APPROVAL_TIMEOUT_MS = parseInt(process.env.CLAUDE_TOOL_APPROVAL_TIMEOUT_MS, 10) || 55000; + +// Generate a stable request ID for UI approval flows. +// This does not encode tool details or get shown to users; it exists so the UI +// can respond to the correct pending request without collisions. +function createRequestId() { + // if clause is used because randomUUID is not available in older Node.js versions + if (typeof crypto.randomUUID === 'function') { + return crypto.randomUUID(); + } + return crypto.randomBytes(16).toString('hex'); +} + +// Wait for a UI approval decision, honoring SDK cancellation. +// This does not auto-approve or auto-deny; it only resolves with UI input, +// and it cleans up the pending map to avoid leaks, introduced to prevent +// replying after the SDK cancels the control request. +function waitForToolApproval(requestId, options = {}) { + const { timeoutMs = TOOL_APPROVAL_TIMEOUT_MS, signal, onCancel } = options; + + return new Promise(resolve => { + let settled = false; + + const finalize = (decision) => { + if (settled) return; + settled = true; + cleanup(); + resolve(decision); + }; + + const cleanup = () => { + pendingToolApprovals.delete(requestId); + clearTimeout(timeout); + if (signal && abortHandler) { + signal.removeEventListener('abort', abortHandler); + } + }; + + // Timeout is local to this process; it does not override SDK timing. + // It exists to prevent the UI prompt from lingering indefinitely. + const timeout = setTimeout(() => { + onCancel?.('timeout'); + finalize(null); + }, timeoutMs); + + const abortHandler = () => { + // If the SDK cancels the control request, stop waiting to avoid + // replying after the process is no longer ready for writes. + onCancel?.('cancelled'); + finalize({ cancelled: true }); + }; + + if (signal) { + if (signal.aborted) { + onCancel?.('cancelled'); + finalize({ cancelled: true }); + return; + } + signal.addEventListener('abort', abortHandler, { once: true }); + } + + pendingToolApprovals.set(requestId, (decision) => { + finalize(decision); + }); + }); +} + +// Resolve a pending approval. This does not validate the decision payload; +// validation and tool matching remain in canUseTool, which keeps this as a +// lightweight WebSocket -> SDK relay. +function resolveToolApproval(requestId, decision) { + const resolver = pendingToolApprovals.get(requestId); + if (resolver) { + resolver(decision); + } +} + +// Match stored permission entries against a tool + input combo. +// This only supports exact tool names and the Bash(command:*) shorthand +// used by the UI; it intentionally does not implement full glob semantics, +// introduced to stay consistent with the UI's "Allow rule" format. +function matchesToolPermission(entry, toolName, input) { + if (!entry || !toolName) { + return false; + } + + if (entry === toolName) { + return true; + } + + const bashMatch = entry.match(/^Bash\((.+):\*\)$/); + if (toolName === 'Bash' && bashMatch) { + const allowedPrefix = bashMatch[1]; + let command = ''; + + if (typeof input === 'string') { + command = input.trim(); + } else if (input && typeof input === 'object' && typeof input.command === 'string') { + command = input.command.trim(); + } + + if (!command) { + return false; + } + + return command.startsWith(allowedPrefix); + } + + return false; +} /** * Maps CLI options to SDK-compatible options format @@ -52,29 +173,28 @@ function mapCliOptionsToSDK(options = {}) { if (settings.skipPermissions && permissionMode !== 'plan') { // When skipping permissions, use bypassPermissions mode sdkOptions.permissionMode = 'bypassPermissions'; - } else { - // Map allowed tools - let allowedTools = [...(settings.allowedTools || [])]; - - // Add plan mode default tools - if (permissionMode === 'plan') { - const planModeTools = ['Read', 'Task', 'exit_plan_mode', 'TodoRead', 'TodoWrite', 'WebFetch', 'WebSearch']; - for (const tool of planModeTools) { - if (!allowedTools.includes(tool)) { - allowedTools.push(tool); - } + } + + // Map allowed tools (always set to avoid implicit "allow all" defaults). + // This does not grant permissions by itself; it just configures the SDK, + // introduced because leaving it undefined made the SDK treat it as "all tools allowed." + let allowedTools = [...(settings.allowedTools || [])]; + + // Add plan mode default tools + if (permissionMode === 'plan') { + const planModeTools = ['Read', 'Task', 'exit_plan_mode', 'TodoRead', 'TodoWrite', 'WebFetch', 'WebSearch']; + for (const tool of planModeTools) { + if (!allowedTools.includes(tool)) { + allowedTools.push(tool); } } + } - if (allowedTools.length > 0) { - sdkOptions.allowedTools = allowedTools; - } + sdkOptions.allowedTools = allowedTools; - // Map disallowed tools - if (settings.disallowedTools && settings.disallowedTools.length > 0) { - sdkOptions.disallowedTools = settings.disallowedTools; - } - } + // Map disallowed tools (always set so the SDK doesn't treat "undefined" as permissive). + // This does not override allowlists; it only feeds the canUseTool gate. + sdkOptions.disallowedTools = settings.disallowedTools || []; // Map model (default to sonnet) // Valid models: sonnet, opus, haiku, opusplan, sonnet[1m] @@ -370,6 +490,76 @@ async function queryClaudeSDK(command, options = {}, ws) { tempImagePaths = imageResult.tempImagePaths; tempDir = imageResult.tempDir; + // Gate tool usage with explicit UI approval when not auto-approved. + // This does not render UI or persist permissions; it only bridges to the UI + // via WebSocket and waits for the response, introduced so tool calls pause + // instead of auto-running when the allowlist is empty. + sdkOptions.canUseTool = async (toolName, input, context) => { + if (sdkOptions.permissionMode === 'bypassPermissions') { + return { behavior: 'allow', updatedInput: input }; + } + + const isDisallowed = (sdkOptions.disallowedTools || []).some(entry => + matchesToolPermission(entry, toolName, input) + ); + if (isDisallowed) { + return { behavior: 'deny', message: 'Tool disallowed by settings' }; + } + + const isAllowed = (sdkOptions.allowedTools || []).some(entry => + matchesToolPermission(entry, toolName, input) + ); + if (isAllowed) { + return { behavior: 'allow', updatedInput: input }; + } + + const requestId = createRequestId(); + ws.send({ + type: 'claude-permission-request', + requestId, + toolName, + input, + sessionId: capturedSessionId || sessionId || null + }); + + // Wait for the UI; if the SDK cancels, notify the UI so it can dismiss the banner. + // This does not retry or resurface the prompt; it just reflects the cancellation. + const decision = await waitForToolApproval(requestId, { + signal: context?.signal, + onCancel: (reason) => { + ws.send({ + type: 'claude-permission-cancelled', + requestId, + reason, + sessionId: capturedSessionId || sessionId || null + }); + } + }); + if (!decision) { + return { behavior: 'deny', message: 'Permission request timed out' }; + } + + if (decision.cancelled) { + return { behavior: 'deny', message: 'Permission request cancelled' }; + } + + if (decision.allow) { + // rememberEntry only updates this run's in-memory allowlist to prevent + // repeated prompts in the same session; persistence is handled by the UI. + if (decision.rememberEntry && typeof decision.rememberEntry === 'string') { + if (!sdkOptions.allowedTools.includes(decision.rememberEntry)) { + sdkOptions.allowedTools.push(decision.rememberEntry); + } + if (Array.isArray(sdkOptions.disallowedTools)) { + sdkOptions.disallowedTools = sdkOptions.disallowedTools.filter(entry => entry !== decision.rememberEntry); + } + } + return { behavior: 'allow', updatedInput: decision.updatedInput ?? input }; + } + + return { behavior: 'deny', message: decision.message ?? 'User denied tool use' }; + }; + // Create SDK query instance const queryInstance = query({ prompt: finalCommand, @@ -526,5 +716,6 @@ export { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, - getActiveClaudeSDKSessions + getActiveClaudeSDKSessions, + resolveToolApproval }; diff --git a/server/index.js b/server/index.js index d6b46e51d..a638d0d61 100755 --- a/server/index.js +++ b/server/index.js @@ -58,7 +58,7 @@ import fetch from 'node-fetch'; import mime from 'mime-types'; import { getProjects, getSessions, getSessionMessages, renameProject, deleteSession, deleteProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache } from './projects.js'; -import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getActiveClaudeSDKSessions } from './claude-sdk.js'; +import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getActiveClaudeSDKSessions, resolveToolApproval } from './claude-sdk.js'; import { spawnCursor, abortCursorSession, isCursorSessionActive, getActiveCursorSessions } from './cursor-cli.js'; import { queryCodex, abortCodexSession, isCodexSessionActive, getActiveCodexSessions } from './openai-codex.js'; import gitRoutes from './routes/git.js'; @@ -804,6 +804,18 @@ function handleChatConnection(ws) { provider, success }); + } else if (data.type === 'claude-permission-response') { + // Relay UI approval decisions back into the SDK control flow. + // This does not persist permissions; it only resolves the in-flight request, + // introduced so the SDK can resume once the user clicks Allow/Deny. + if (data.requestId) { + resolveToolApproval(data.requestId, { + allow: Boolean(data.allow), + updatedInput: data.updatedInput, + message: data.message, + rememberEntry: data.rememberEntry + }); + } } else if (data.type === 'cursor-abort') { console.log('[DEBUG] Abort Cursor session:', data.sessionId); const success = abortCursorSession(data.sessionId); diff --git a/src/components/ChatInterface.jsx b/src/components/ChatInterface.jsx index d5abc0a66..a9d5110dc 100644 --- a/src/components/ChatInterface.jsx +++ b/src/components/ChatInterface.jsx @@ -37,6 +37,7 @@ import Fuse from 'fuse.js'; import CommandMenu from './CommandMenu'; import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS } from '../../shared/modelConstants'; +import { safeJsonParse } from '../lib/utils.js'; // Helper function to decode HTML entities in text function decodeHtmlEntities(text) { @@ -236,6 +237,102 @@ const safeLocalStorage = { } }; +const CLAUDE_SETTINGS_KEY = 'claude-settings'; + + +function getClaudeSettings() { + const raw = safeLocalStorage.getItem(CLAUDE_SETTINGS_KEY); + if (!raw) { + return { + allowedTools: [], + disallowedTools: [], + skipPermissions: false, + projectSortOrder: 'name' + }; + } + + try { + const parsed = JSON.parse(raw); + return { + ...parsed, + allowedTools: Array.isArray(parsed.allowedTools) ? parsed.allowedTools : [], + disallowedTools: Array.isArray(parsed.disallowedTools) ? parsed.disallowedTools : [], + skipPermissions: Boolean(parsed.skipPermissions), + projectSortOrder: parsed.projectSortOrder || 'name' + }; + } catch { + return { + allowedTools: [], + disallowedTools: [], + skipPermissions: false, + projectSortOrder: 'name' + }; + } +} + +function buildClaudeToolPermissionEntry(toolName, toolInput) { + if (!toolName) return null; + if (toolName !== 'Bash') return toolName; + + const parsed = safeJsonParse(toolInput); + const command = typeof parsed?.command === 'string' ? parsed.command.trim() : ''; + if (!command) return toolName; + + const tokens = command.split(/\s+/); + if (tokens.length === 0) return toolName; + + // For Bash, allow the command family instead of every Bash invocation. + if (tokens[0] === 'git' && tokens[1]) { + return `Bash(${tokens[0]} ${tokens[1]}:*)`; + } + return `Bash(${tokens[0]}:*)`; +} + +// Normalize tool inputs for display in the permission banner. +// This does not sanitize/redact secrets; it is strictly formatting so users +// can see the raw input that triggered the permission prompt. +function formatToolInputForDisplay(input) { + if (input === undefined || input === null) return ''; + if (typeof input === 'string') return input; + try { + return JSON.stringify(input, null, 2); + } catch { + return String(input); + } +} + +function getClaudePermissionSuggestion(message, provider) { + if (provider !== 'claude') return null; + if (!message?.toolResult?.isError) return null; + + const toolName = message?.toolName; + const entry = buildClaudeToolPermissionEntry(toolName, message.toolInput); + + if (!entry) return null; + + const settings = getClaudeSettings(); + const isAllowed = settings.allowedTools.includes(entry); + return { toolName, entry, isAllowed }; +} + +function grantClaudeToolPermission(entry) { + if (!entry) return { success: false }; + + const settings = getClaudeSettings(); + const alreadyAllowed = settings.allowedTools.includes(entry); + const nextAllowed = alreadyAllowed ? settings.allowedTools : [...settings.allowedTools, entry]; + const nextDisallowed = settings.disallowedTools.filter(tool => tool !== entry); + const updatedSettings = { + ...settings, + allowedTools: nextAllowed, + disallowedTools: nextDisallowed, + lastUpdated: new Date().toISOString() + }; + + safeLocalStorage.setItem(CLAUDE_SETTINGS_KEY, JSON.stringify(updatedSettings)); + return { success: true, alreadyAllowed, updatedSettings }; +} + // Common markdown components to ensure consistent rendering (tables, inline code, links, etc.) const markdownComponents = { code: ({ node, inline, className, children, ...props }) => { @@ -356,7 +453,7 @@ const markdownComponents = { }; // Memoized message component to prevent unnecessary re-renders -const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFileOpen, onShowSettings, autoExpandTools, showRawParameters, showThinking, selectedProject }) => { +const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFileOpen, onShowSettings, onGrantToolPermission, autoExpandTools, showRawParameters, showThinking, selectedProject, provider }) => { const isGrouped = prevMessage && prevMessage.type === message.type && ((prevMessage.type === 'assistant') || (prevMessage.type === 'user') || @@ -364,6 +461,13 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile (prevMessage.type === 'error')); const messageRef = React.useRef(null); const [isExpanded, setIsExpanded] = React.useState(false); + const permissionSuggestion = getClaudePermissionSuggestion(message, provider); + const [permissionGrantState, setPermissionGrantState] = React.useState('idle'); + + React.useEffect(() => { + setPermissionGrantState('idle'); + }, [permissionSuggestion?.entry, message.toolId]); + React.useEffect(() => { if (!autoExpandTools || !messageRef.current || !message.isToolUse) return; @@ -1358,6 +1462,59 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile ); })()} + {permissionSuggestion && ( +
+
+ + {onShowSettings && ( + + )} +
+
+ Adds {permissionSuggestion.entry} to Allowed Tools. +
+ {permissionGrantState === 'error' && ( +
+ Unable to update permissions. Please try again. +
+ )} + {(permissionSuggestion.isAllowed || permissionGrantState === 'granted') && ( +
+ Permission saved. Retry the request to use the tool. +
+ )} +
+ )} ); @@ -1688,6 +1845,10 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess const MESSAGES_PER_PAGE = 20; const [isSystemSessionChange, setIsSystemSessionChange] = useState(false); const [permissionMode, setPermissionMode] = useState('default'); + // In-memory queue of tool permission prompts for the current UI view. + // These are not persisted and do not survive a page refresh; introduced so + // the UI can present pending approvals while the SDK waits. + const [pendingPermissionRequests, setPendingPermissionRequests] = useState([]); const [attachedImages, setAttachedImages] = useState([]); const [uploadingImages, setUploadingImages] = useState(new Map()); const [imageErrors, setImageErrors] = useState(new Map()); @@ -1735,6 +1896,9 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess const [codexModel, setCodexModel] = useState(() => { return localStorage.getItem('codex-model') || CODEX_MODELS.DEFAULT; }); + // Track provider transitions so we only clear approvals when provider truly changes. + // This does not sync with the backend; it just prevents UI prompts from disappearing. + const lastProviderRef = useRef(provider); // Load permission mode for the current session useEffect(() => { if (selectedSession?.id) { @@ -1754,6 +1918,23 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess localStorage.setItem('selected-provider', selectedSession.__provider); } }, [selectedSession]); + + // Clear pending permission prompts when switching providers; filter when switching sessions. + // This does not preserve prompts across provider changes; it exists to keep the + // Claude approval flow intact while preventing prompts from a different provider. + useEffect(() => { + if (lastProviderRef.current !== provider) { + setPendingPermissionRequests([]); + lastProviderRef.current = provider; + } + }, [provider]); + + // When the selected session changes, drop prompts that belong to other sessions. + // This does not attempt to migrate prompts across sessions; it only filters, + // introduced so the UI does not show approvals for a session the user is no longer viewing. + useEffect(() => { + setPendingPermissionRequests(prev => prev.filter(req => !req.sessionId || req.sessionId === selectedSession?.id)); + }, [selectedSession?.id]); // Load Cursor default model from config useEffect(() => { @@ -3014,6 +3195,13 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess if (onReplaceTemporarySession) { onReplaceTemporarySession(latestMessage.sessionId); } + + // Attach the real session ID to any pending permission requests so they + // do not disappear during the "new-session -> real-session" transition. + // This does not create or auto-approve requests; it only keeps UI state aligned. + setPendingPermissionRequests(prev => prev.map(req => ( + req.sessionId ? req : { ...req, sessionId: latestMessage.sessionId } + ))); } break; @@ -3244,6 +3432,55 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess }]); break; + case 'claude-permission-request': { + // Receive a tool approval request from the backend and surface it in the UI. + // This does not approve anything automatically; it only queues a prompt, + // introduced so the user can decide before the SDK continues. + if (provider !== 'claude' || !latestMessage.requestId) { + break; + } + + setPendingPermissionRequests(prev => { + if (prev.some(req => req.requestId === latestMessage.requestId)) { + return prev; + } + return [ + ...prev, + { + requestId: latestMessage.requestId, + toolName: latestMessage.toolName || 'UnknownTool', + input: latestMessage.input, + context: latestMessage.context, + sessionId: latestMessage.sessionId || null, + receivedAt: new Date() + } + ]; + }); + + // Keep the session in a "waiting" state while approval is pending. + // This does not resume the run; it only updates the UI status so the + // user knows Claude is blocked on a decision. + setIsLoading(true); + setCanAbortSession(true); + setClaudeStatus({ + text: 'Waiting for permission', + tokens: 0, + can_interrupt: true + }); + break; + } + + case 'claude-permission-cancelled': { + // Backend cancelled the approval (timeout or SDK cancel); remove the banner. + // We currently do not show a user-facing warning here; this is intentional + // to avoid noisy alerts when the SDK cancels in the background. + if (!latestMessage.requestId) { + break; + } + setPendingPermissionRequests(prev => prev.filter(req => req.requestId !== latestMessage.requestId)); + break; + } + case 'claude-error': setChatMessages(prev => [...prev, { type: 'error', @@ -3440,6 +3677,9 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess if (selectedProject && latestMessage.exitCode === 0) { safeLocalStorage.removeItem(`chat_messages_${selectedProject.name}`); } + // Conversation finished; clear any stale permission prompts. + // This does not remove saved permissions; it only resets transient UI state. + setPendingPermissionRequests([]); break; case 'codex-response': @@ -3615,6 +3855,10 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess } } + // Abort ends the run; clear permission prompts to avoid dangling UI state. + // This does not change allowlists; it only clears the current banner. + setPendingPermissionRequests([]); + setChatMessages(prev => [...prev, { type: 'assistant', content: 'Session interrupted by user.', @@ -4133,6 +4377,43 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess } }, [input, isLoading, selectedProject, attachedImages, currentSessionId, selectedSession, provider, permissionMode, onSessionActive, cursorModel, claudeModel, codexModel, sendMessage, setInput, setAttachedImages, setUploadingImages, setImageErrors, setIsTextareaExpanded, textareaRef, setChatMessages, setIsLoading, setCanAbortSession, setClaudeStatus, setIsUserScrolledUp, scrollToBottom]); + const handleGrantToolPermission = useCallback((suggestion) => { + if (!suggestion || provider !== 'claude') { + return { success: false }; + } + return grantClaudeToolPermission(suggestion.entry); + }, [provider]); + + // Send a UI decision back to the server (single or batched request IDs). + // This does not validate tool inputs or permissions; the backend enforces rules. + // It exists so "Allow & remember" can resolve multiple queued prompts at once. + const handlePermissionDecision = useCallback((requestIds, decision) => { + const ids = Array.isArray(requestIds) ? requestIds : [requestIds]; + const validIds = ids.filter(Boolean); + if (validIds.length === 0) { + return; + } + + validIds.forEach((requestId) => { + sendMessage({ + type: 'claude-permission-response', + requestId, + allow: Boolean(decision?.allow), + updatedInput: decision?.updatedInput, + message: decision?.message, + rememberEntry: decision?.rememberEntry + }); + }); + + setPendingPermissionRequests(prev => { + const next = prev.filter(req => !validIds.includes(req.requestId)); + if (next.length === 0) { + setClaudeStatus(null); + } + return next; + }); + }, [sendMessage]); + // Store handleSubmit in ref so handleCustomCommand can access it useEffect(() => { handleSubmitRef.current = handleSubmit; @@ -4711,10 +4992,12 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess createDiff={createDiff} onFileOpen={onFileOpen} onShowSettings={onShowSettings} + onGrantToolPermission={handleGrantToolPermission} autoExpandTools={autoExpandTools} showRawParameters={showRawParameters} showThinking={showThinking} selectedProject={selectedProject} + provider={provider} /> ); })} @@ -4769,6 +5052,101 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess {/* Permission Mode Selector with scroll to bottom button - Above input, clickable for mobile */}
+ {pendingPermissionRequests.length > 0 && ( + // Permission banner for tool approvals. This renders the input, allows + // "allow once" or "allow & remember", and supports batching similar requests. + // It does not persist permissions by itself; persistence is handled by + // the existing localStorage-based settings helpers, introduced to surface + // approvals before tool execution resumes. +
+ {pendingPermissionRequests.map((request) => { + const rawInput = formatToolInputForDisplay(request.input); + const permissionEntry = buildClaudeToolPermissionEntry(request.toolName, rawInput); + const settings = getClaudeSettings(); + const alreadyAllowed = permissionEntry + ? settings.allowedTools.includes(permissionEntry) + : false; + const rememberLabel = alreadyAllowed ? 'Allow (saved)' : 'Allow & remember'; + // Group pending prompts that resolve to the same allow rule so + // a single "Allow & remember" can clear them in one click. + // This does not attempt fuzzy matching; it only batches identical rules. + const matchingRequestIds = permissionEntry + ? pendingPermissionRequests + .filter(item => buildClaudeToolPermissionEntry(item.toolName, formatToolInputForDisplay(item.input)) === permissionEntry) + .map(item => item.requestId) + : [request.requestId]; + + return ( +
+
+
+
+ Permission required +
+
+ Tool: {request.toolName} +
+
+ {permissionEntry && ( +
+ Allow rule: {permissionEntry} +
+ )} +
+ + {rawInput && ( +
+ + View tool input + +
+                          {rawInput}
+                        
+
+ )} + +
+ + + +
+
+ ); + })} +
+ )} +