From 76d8bba33223899c6b517cbecd942d5e047d96ce Mon Sep 17 00:00:00 2001 From: lucaronin Date: Mon, 27 Apr 2026 16:46:32 +0200 Subject: [PATCH] fix: remove hidden Claude permission bypass --- docs/ARCHITECTURE.md | 2 +- docs/GETTING-STARTED.md | 2 +- src-tauri/src/claude_cli.rs | 17 ++++++-- src/utils/ai-agent.test.ts | 4 +- src/utils/ai-agent.ts | 87 ++++++++++++++++++++++++------------- 5 files changed, 74 insertions(+), 38 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 0516859f..2b3778fd 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -218,7 +218,7 @@ Full agent mode — spawns the selected local CLI agent as a subprocess with too 1. **Frontend** (`AiPanel` + `useCliAiAgent` + `aiAgents.ts`) — streaming UI with reasoning blocks, tool action cards, response display, onboarding, and default-agent selection 2. **Backend** (`ai_agents.rs`) — normalizes agent availability and streaming, dispatching to per-agent adapters -3. **Agent adapters** — Claude Code still uses `claude_cli.rs`; Codex runs through `codex --sandbox workspace-write --ask-for-approval never exec --json` so app-launched sessions can edit the active vault while the dangerous bypass mode remains disabled +3. **Agent adapters** — Claude Code still uses `claude_cli.rs` with `acceptEdits`, strict Tolaria MCP config, and a file/search-only built-in tool list; Codex runs through `codex --sandbox workspace-write --ask-for-approval never exec --json`. Both app-launched paths can edit the active vault without enabling dangerous permission bypass modes. 4. **MCP Integration** — Claude receives the generated MCP config file path, while Codex receives the same Tolaria MCP server via transient `-c mcp_servers.tolaria.*` config overrides CLI-agent availability intentionally does not depend only on the desktop app's inherited `PATH`. The detectors check the current process path, the user's login shell, and supported local/toolchain install locations such as native `~/.local/bin`, local `~/.claude/local`, Mise/asdf shims, npm-global, Homebrew, Windows `%APPDATA%\npm`/pnpm/Scoop shims, Windows `.exe` launchers, and the macOS Codex app resource path so first-run onboarding works on fresh macOS and Windows installs. diff --git a/docs/GETTING-STARTED.md b/docs/GETTING-STARTED.md index 36e7080a..3c2db36e 100644 --- a/docs/GETTING-STARTED.md +++ b/docs/GETTING-STARTED.md @@ -417,5 +417,5 @@ BASE_URL="http://localhost:5173" npx playwright test tests/smoke/.spec.ts 1. **Agent system prompt**: Edit `src/utils/ai-agent.ts` (inline system prompt string) 2. **Context building**: Edit `src/utils/ai-context.ts` for what data is sent to the agent 3. **Tool action display**: Edit `src/components/AiActionCard.tsx` -4. **Claude CLI arguments**: Edit `src-tauri/src/claude_cli.rs` (`run_agent_stream()`) +4. **Claude CLI arguments**: Edit `src-tauri/src/claude_cli.rs` (`run_agent_stream()`; keep app-managed launches on strict Tolaria MCP config, `acceptEdits`, and the scoped file/search tool list) 5. **Shared agent adapters / Codex args**: Edit `src-tauri/src/ai_agents.rs` (keep Codex sandboxed with active-vault `workspace-write`; do not use the dangerous bypass unless an ADR explicitly designs a new mode) diff --git a/src-tauri/src/claude_cli.rs b/src-tauri/src/claude_cli.rs index a987c0c8..a76d9241 100644 --- a/src-tauri/src/claude_cli.rs +++ b/src-tauri/src/claude_cli.rs @@ -269,7 +269,11 @@ fn build_agent_args(req: &AgentStreamRequest) -> Result, String> { "--include-partial-messages".into(), "--mcp-config".into(), mcp_config, - "--dangerously-skip-permissions".into(), + "--strict-mcp-config".into(), + "--permission-mode".into(), + "acceptEdits".into(), + "--tools".into(), + "Read,Edit,MultiEdit,Write,Glob,Grep,LS".into(), "--no-session-persistence".into(), ]; @@ -1066,11 +1070,16 @@ mod tests { assert!(args.contains(&"-p".to_string())); assert!(args.contains(&"create note".to_string())); assert!(args.contains(&"--mcp-config".to_string())); - assert!(args.contains(&"--dangerously-skip-permissions".to_string())); + assert!(args.contains(&"--strict-mcp-config".to_string())); + assert!(args.contains(&"--permission-mode".to_string())); + assert!(args.contains(&"acceptEdits".to_string())); + assert!(args.contains(&"--tools".to_string())); + assert!(args.contains(&"Read,Edit,MultiEdit,Write,Glob,Grep,LS".to_string())); + assert!(!args.contains(&"--dangerously-skip-permissions".to_string())); + assert!(!args.contains(&"bypassPermissions".to_string())); + assert!(!args.contains(&"Bash".to_string())); assert!(args.contains(&"--no-session-persistence".to_string())); assert!(!args.contains(&"--append-system-prompt".to_string())); - // Native tools must NOT be disabled - assert!(!args.contains(&"--tools".to_string())); } } diff --git a/src/utils/ai-agent.test.ts b/src/utils/ai-agent.test.ts index 1844b9c4..ce87a060 100644 --- a/src/utils/ai-agent.test.ts +++ b/src/utils/ai-agent.test.ts @@ -13,7 +13,9 @@ describe('buildAgentSystemPrompt', () => { it('returns preamble when no vault context', () => { const prompt = buildAgentSystemPrompt() expect(prompt).toContain('working inside Tolaria') - expect(prompt).toContain('full shell access') + expect(prompt).toContain('active vault') + expect(prompt).toContain('Avoid shell commands') + expect(prompt).not.toContain('full shell access') expect(prompt).not.toContain('Vault context') }) diff --git a/src/utils/ai-agent.ts b/src/utils/ai-agent.ts index e223d0c7..b96b4e30 100644 --- a/src/utils/ai-agent.ts +++ b/src/utils/ai-agent.ts @@ -1,8 +1,8 @@ /** - * AI Agent utilities — Claude CLI agent mode with full shell access + MCP vault tools. + * AI Agent utilities — Claude CLI agent mode with scoped file tools + MCP vault tools. * - * The agent has full native tool access (bash, read, write, edit) plus - * Tolaria-specific MCP tools (search_notes, get_vault_context, get_note, open_note). + * App-managed sessions can edit files in the active vault and use Tolaria-specific + * MCP tools (search_notes, get_vault_context, get_note, open_note). * The frontend receives streaming events for text, tool calls, and completion. */ @@ -13,8 +13,9 @@ import { isTauri } from '../mock-tauri' const AGENT_SYSTEM_PREAMBLE = `You are working inside Tolaria, a personal knowledge management app. Notes are markdown files with YAML frontmatter. Standard fields: title, type (aliased is_a), date, tags. -You have full shell access. Use bash for file operations, search, bulk edits. +You can edit markdown files in the active vault. Prefer file edit tools for note changes. Use the provided MCP tools for: full-text search (search_notes), vault orientation (get_vault_context), parsed note reading (get_note), and opening notes in the UI (open_note). +Avoid shell commands; app-managed sessions are intentionally scoped to vault file edits and Tolaria MCP tools. When you create or edit a note, call open_note(path) so the user sees it in Tolaria. When you mention or reference a note by name, always use [[Note Title]] wikilink syntax so the user can click to open it. @@ -62,8 +63,53 @@ function mockAgentResponse(message: string): string { return `[mock-no-history] You said: "${message}" — This note is related to [[Build Laputa App]] and [[Matteo Cellini]].` } +function emitMockAgentResponse(message: string, callbacks: AgentStreamCallbacks): void { + setTimeout(() => { + callbacks.onText(mockAgentResponse(message)) + callbacks.onDone() + }, 300) +} + +function createStreamCloser(callbacks: AgentStreamCallbacks): () => void { + let closed = false + return () => { + if (closed) return + closed = true + callbacks.onDone() + } +} + +function handleClaudeStreamEvent( + data: ClaudeAgentStreamEvent, + callbacks: AgentStreamCallbacks, + closeStream: () => void, +): void { + switch (data.kind) { + case 'TextDelta': + callbacks.onText(data.text) + return + case 'ThinkingDelta': + callbacks.onThinking(data.text) + return + case 'ToolStart': + callbacks.onToolStart(data.tool_name, data.tool_id, data.input) + return + case 'ToolDone': + callbacks.onToolDone(data.tool_id, data.output) + return + case 'Error': + callbacks.onError(data.message) + return + case 'Done': + closeStream() + return + default: + return + } +} + /** - * Stream an agent task through the Claude CLI subprocess with full tool access. + * Stream an agent task through the Claude CLI subprocess with scoped tool access. * The CLI handles the tool-use loop; we receive events for UI updates. */ export async function streamClaudeAgent( @@ -73,38 +119,16 @@ export async function streamClaudeAgent( callbacks: AgentStreamCallbacks, ): Promise { if (!isTauri()) { - setTimeout(() => { - callbacks.onText(mockAgentResponse(message)) - callbacks.onDone() - }, 300) + emitMockAgentResponse(message, callbacks) return } const { invoke } = await import('@tauri-apps/api/core') const { listen } = await import('@tauri-apps/api/event') + const closeStream = createStreamCloser(callbacks) const unlisten = await listen('claude-agent-stream', (event) => { - const data = event.payload - switch (data.kind) { - case 'TextDelta': - callbacks.onText(data.text) - break - case 'ThinkingDelta': - callbacks.onThinking(data.text) - break - case 'ToolStart': - callbacks.onToolStart(data.tool_name, data.tool_id, data.input) - break - case 'ToolDone': - callbacks.onToolDone(data.tool_id, data.output) - break - case 'Error': - callbacks.onError(data.message) - break - case 'Done': - callbacks.onDone() - break - } + handleClaudeStreamEvent(event.payload, callbacks, closeStream) }) try { @@ -115,9 +139,10 @@ export async function streamClaudeAgent( vault_path: vaultPath, }, }) + closeStream() } catch (err) { callbacks.onError(err instanceof Error ? err.message : String(err)) - callbacks.onDone() + closeStream() } finally { unlisten() }