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 --- packages/api/tests/agent-manager.test.ts | 23 +++++++++++++++++++++++ packages/api/tests/routes.test.ts | 23 +++++++++++++++++++++++ 2 files changed, 46 insertions(+) (limited to 'packages/api/tests') diff --git a/packages/api/tests/agent-manager.test.ts b/packages/api/tests/agent-manager.test.ts index 56cb818..2111b0e 100644 --- a/packages/api/tests/agent-manager.test.ts +++ b/packages/api/tests/agent-manager.test.ts @@ -18,6 +18,15 @@ vi.mock("@dispatch/core", () => ({ yield { type: "status", status: "idle" } as const; } }, + PermissionService: class MockPermissionService { + ask(_request: unknown, _rulesets: unknown[]) { + return Promise.resolve("once"); + } + reply(_id: string, _reply: unknown) {} + getPending() { + return []; + } + }, createReadFileTool(_wd: string): ToolDefinition { return { name: "read_file", @@ -42,6 +51,20 @@ vi.mock("@dispatch/core", () => ({ execute: async () => ["file1.ts"], }; }, + createRunShellTool(_wd: string): ToolDefinition { + return { + name: "run_shell", + description: "run shell command", + parameters: { _type: "z.ZodObject", shape: {} } as unknown as ToolDefinition["parameters"], + execute: async () => ({ stdout: "", stderr: "", exitCode: 0 }), + }; + }, + loadConfig(_dir: string) { + return { permissions: {} }; + }, + configToRuleset(_config: unknown) { + return []; + }, })); // Import after mock is defined (Vitest hoists vi.mock automatically) diff --git a/packages/api/tests/routes.test.ts b/packages/api/tests/routes.test.ts index 9f852ee..87ff436 100644 --- a/packages/api/tests/routes.test.ts +++ b/packages/api/tests/routes.test.ts @@ -19,6 +19,15 @@ vi.mock("@dispatch/core", () => ({ yield { type: "status", status: "idle" } as const; } }, + PermissionService: class MockPermissionService { + ask(_request: unknown, _rulesets: unknown[]) { + return Promise.resolve("once"); + } + reply(_id: string, _reply: unknown) {} + getPending() { + return []; + } + }, createReadFileTool(_wd: string): ToolDefinition { return { name: "read_file", @@ -43,6 +52,20 @@ vi.mock("@dispatch/core", () => ({ execute: async () => ["file1.ts"], }; }, + createRunShellTool(_wd: string): ToolDefinition { + return { + name: "run_shell", + description: "run shell command", + parameters: { _type: "z.ZodObject", shape: {} } as unknown as ToolDefinition["parameters"], + execute: async () => ({ stdout: "", stderr: "", exitCode: 0 }), + }; + }, + loadConfig(_dir: string) { + return { permissions: {} }; + }, + configToRuleset(_config: unknown) { + return []; + }, })); const { app } = await import("../src/app.js"); -- cgit v1.2.3