From a38d5b1279db6f9de5228c173019fc2ac08daec3 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Tue, 19 May 2026 23:20:41 +0900 Subject: feat: Phase 2 — shell permissions, tree-sitter analysis, permission UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Permission engine: - Rule-based engine: wildcard matching, last-match-wins, reject cascade - PermissionService with pending/approved state, PermissionChecker interface - dispatch.yaml config loader with per-permission pattern rules Shell tool: - run_shell tool with child_process spawn, timeout, streaming output - Tree-sitter static analysis (web-tree-sitter + tree-sitter-bash WASM) - BashArity command normalization for 'always allow' patterns - FILE_COMMANDS set: rm, cp, mv, mkdir, ls, find, grep, cat, etc. Agent loop refactored: - Removed maxSteps, manual step loop with tool execution - Permission checks on shell commands (external_directory only) - Permission checks on file tools outside workspace boundary - Symlink bypass fix (realpathSync), .. false positive fix - Shell output streaming via Promise.race + setImmediate polling API layer: - PermissionManager wraps PermissionService, broadcasts via WebSocket - WebSocket handles permission-reply messages from frontend - Config loaded from dispatch.yaml, converted to ruleset Frontend: - Permission prompt modal (native dialog, focus trap, ARIA) - Always-allow confirmation flow with pattern preview - Shell output display (live streaming + final parsed result) - Permission log panel (fixed bottom-right overlay) - Exit code badge (green 0, red non-zero) 134 tests, typecheck clean on all 3 packages --- .../src/lib/components/PermissionLog.svelte | 28 +++++++ .../src/lib/components/PermissionPrompt.svelte | 97 ++++++++++++++++++++++ .../src/lib/components/ToolCallDisplay.svelte | 93 ++++++++++++++++++--- 3 files changed, 208 insertions(+), 10 deletions(-) create mode 100644 packages/frontend/src/lib/components/PermissionLog.svelte create mode 100644 packages/frontend/src/lib/components/PermissionPrompt.svelte (limited to 'packages/frontend/src/lib/components') diff --git a/packages/frontend/src/lib/components/PermissionLog.svelte b/packages/frontend/src/lib/components/PermissionLog.svelte new file mode 100644 index 0000000..f6e07f6 --- /dev/null +++ b/packages/frontend/src/lib/components/PermissionLog.svelte @@ -0,0 +1,28 @@ + + +
+ +
+ Permission Log ({entries.length}) +
+
+ {#if entries.length === 0} +

No permissions granted or denied yet.

+ {:else} + {#each entries as entry (entry.id)} +
+ + {entry.action} + + {entry.permission} + {entry.timestamp} +
+

{entry.description}

+ {/each} + {/if} +
+
diff --git a/packages/frontend/src/lib/components/PermissionPrompt.svelte b/packages/frontend/src/lib/components/PermissionPrompt.svelte new file mode 100644 index 0000000..ce7afd7 --- /dev/null +++ b/packages/frontend/src/lib/components/PermissionPrompt.svelte @@ -0,0 +1,97 @@ + + + + {#if current} + {#if !showAlwaysConfirmation} + + {:else} + + {/if} + {/if} + diff --git a/packages/frontend/src/lib/components/ToolCallDisplay.svelte b/packages/frontend/src/lib/components/ToolCallDisplay.svelte index 83d21ba..070a8e3 100644 --- a/packages/frontend/src/lib/components/ToolCallDisplay.svelte +++ b/packages/frontend/src/lib/components/ToolCallDisplay.svelte @@ -8,6 +8,39 @@ let isExpanded = $state(toolCall.isExpanded); function toggle() { isExpanded = !isExpanded; } + +interface ShellResult { + stdout: string; + stderr: string; + exitCode: number; +} + +function parseShellResult(result: string): ShellResult | null { + try { + const parsed = JSON.parse(result) as unknown; + if ( + parsed !== null && + typeof parsed === "object" && + "stdout" in parsed && + "stderr" in parsed && + "exitCode" in parsed + ) { + return { + stdout: String((parsed as Record).stdout ?? ""), + stderr: String((parsed as Record).stderr ?? ""), + exitCode: Number((parsed as Record).exitCode ?? 0), + }; + } + return null; + } catch { + return null; + } +} + +const isShell = $derived(toolCall.name === "run_shell"); +const shellResult = $derived( + isShell && toolCall.result !== undefined ? parseShellResult(toolCall.result) : null, +);
@@ -20,7 +53,11 @@ function toggle() { tool {toolCall.name} {#if toolCall.result !== undefined} - {#if toolCall.isError} + {#if isShell && shellResult !== null} + + exit {shellResult.exitCode} + + {:else if toolCall.isError} error {:else} done @@ -36,15 +73,51 @@ function toggle() {

Arguments

{JSON.stringify(toolCall.arguments, null, 2)}
- {#if toolCall.result !== undefined} -
-

Result

-
{toolCall.result}
-
- {/if} + {#if isShell && toolCall.result !== undefined} + {#if shellResult !== null} +
+

stdout:

+
{shellResult.stdout || "(empty)"}
+
+ {#if shellResult.stderr} +
+

stderr:

+
{shellResult.stderr}
+
+ {/if} +
+ exit code: + {shellResult.exitCode} +
+ {:else} +
+

Result

+
{toolCall.result}
+
+ {/if} + {:else if isShell && toolCall.shellOutput} + {#if toolCall.shellOutput.stdout} +
+

stdout

+
{toolCall.shellOutput.stdout}
+
+ {/if} + {#if toolCall.shellOutput.stderr} +
+

stderr

+
{toolCall.shellOutput.stderr}
+
+ {/if} + Running... + {:else if toolCall.result !== undefined} +
+

Result

+
{toolCall.result}
+
+ {/if} {/if} -- cgit v1.2.3