diff options
| author | Adam Malczewski <[email protected]> | 2026-05-19 23:20:41 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-05-19 23:20:41 +0900 |
| commit | a38d5b1279db6f9de5228c173019fc2ac08daec3 (patch) | |
| tree | 32c3a535d0b74872ef952b4a44d4d5ba2ec9d638 /packages/api/tests | |
| parent | 0ae805b28b5160b8d9fb43635fa172961f6550cc (diff) | |
| download | dispatch-a38d5b1279db6f9de5228c173019fc2ac08daec3.tar.gz dispatch-a38d5b1279db6f9de5228c173019fc2ac08daec3.zip | |
feat: Phase 2 — shell permissions, tree-sitter analysis, permission UI
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
Diffstat (limited to 'packages/api/tests')
| -rw-r--r-- | packages/api/tests/agent-manager.test.ts | 23 | ||||
| -rw-r--r-- | packages/api/tests/routes.test.ts | 23 |
2 files changed, 46 insertions, 0 deletions
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"); |
