11package main
22
33import (
4+ "bufio"
45 "bytes"
56 "encoding/json"
67 "fmt"
8+ "os"
79 "os/exec"
810 "strings"
11+
12+ "github.com/BackendStack21/kode/internal/danger"
913)
1014
1115// shellTool is kode's built-in tool that lets the agent run shell commands.
@@ -33,11 +37,24 @@ import (
3337// command string as JSON; the shell tool executes it as-is.
3438// - Error output is merged into stdout (stderr follows stdout in output).
3539// - Empty output returns "(no output)" so the LLM always gets a response.
40+ // - Commands are classified by risk (see internal/danger). High-risk
41+ // commands in non-sandboxed mode prompt the user for approval via /dev/tty.
3642type shellTool struct {
3743 // containerName, when set, routes commands through "docker exec"
3844 // into this container. Set by setupSandbox() when --sandbox is active.
3945 // When empty, commands run directly on the host.
4046 containerName string
47+
48+ // dangerousConfig controls per-class actions and allow/denylists.
49+ dangerousConfig danger.DangerousConfig
50+
51+ // trustedClasses caches user-approved risk classes for this process.
52+ // Set when user presses T (trust this session) at the prompt.
53+ trustedClasses map [danger.RiskClass ]bool
54+
55+ // ttyPath is the path to the terminal device for approval prompts.
56+ // Overridden in tests to mock user input.
57+ ttyPath string
4158}
4259
4360func (t * shellTool ) Name () string { return "shell" }
@@ -46,7 +63,10 @@ func (t *shellTool) Description() string {
4663 return `Run a shell command and return its output.
4764Use for: reading files, listing directories, running tests, building code, and git operations.
4865In sandbox mode (--sandbox), commands run inside the Docker container with restricted permissions.
49- In host mode (default), commands run with the same permissions as the kode process.`
66+ In host mode (default), commands run with the same permissions as the kode process.
67+
68+ Risk classes: safe, local_write, system_write, destructive, network_egress, code_execution, install, blocked
69+ High-risk operations may prompt for approval (configurable via dangerous section in kode.json).`
5070}
5171
5272func (t * shellTool ) Schema () any {
@@ -57,6 +77,10 @@ func (t *shellTool) Schema() any {
5777 "type" : "string" ,
5878 "description" : "The shell command to execute. Supports pipes, redirects, and multi-line scripts." ,
5979 },
80+ "description" : map [string ]any {
81+ "type" : "string" ,
82+ "description" : "Optional: explain what this command does and why. Shown in the approval prompt for high-risk operations." ,
83+ },
6084 },
6185 "required" : []string {"command" },
6286 }
@@ -67,7 +91,8 @@ func (t *shellTool) Schema() any {
6791// Both stdout and stderr are captured and merged into the return string.
6892func (t * shellTool ) Call (args string ) (string , error ) {
6993 var input struct {
70- Command string `json:"command"`
94+ Command string `json:"command"`
95+ Description string `json:"description,omitempty"`
7196 }
7297 if err := json .Unmarshal ([]byte (args ), & input ); err != nil {
7398 return "" , fmt .Errorf ("shell: parse args: %w" , err )
@@ -76,6 +101,11 @@ func (t *shellTool) Call(args string) (string, error) {
76101 return "" , fmt .Errorf ("shell: empty command" )
77102 }
78103
104+ // Check approval before executing
105+ if err := t .checkApproval (input .Command , input .Description ); err != nil {
106+ return "" , err
107+ }
108+
79109 cmd := t .buildCmd (input .Command )
80110
81111 var outBuf , errBuf bytes.Buffer
@@ -99,6 +129,88 @@ func (t *shellTool) Call(args string) (string, error) {
99129 return output , nil
100130}
101131
132+ // checkApproval classifies the command and prompts the user if needed.
133+ func (t * shellTool ) checkApproval (cmd , description string ) error {
134+ // Check allowlist/denylist + risk class via dangerous config
135+ action := t .dangerousConfig .ActionForCommand (cmd )
136+
137+ switch action {
138+ case danger .Allow :
139+ return nil
140+ case danger .Deny :
141+ return fmt .Errorf ("operation denied by configuration: %s" , cmd )
142+ case danger .Prompt :
143+ return t .promptUser (cmd , description )
144+ default :
145+ return nil
146+ }
147+ }
148+
149+ // promptUser opens /dev/tty and asks the user to approve the command.
150+ func (t * shellTool ) promptUser (cmd , description string ) error {
151+ cls := danger .Classify (cmd )
152+
153+ // Check session trust cache
154+ if t .trustedClasses != nil && t .trustedClasses [cls ] {
155+ return nil
156+ }
157+
158+ // Open /dev/tty for interactive approval
159+ ttyPath := t .ttyPath
160+ if ttyPath == "" {
161+ ttyPath = "/dev/tty"
162+ }
163+ tty , err := os .OpenFile (ttyPath , os .O_RDWR , 0 )
164+ if err != nil {
165+ // Non-interactive: use configured fallback
166+ action := t .dangerousConfig .NonInteractiveAction ()
167+ if action == danger .Deny {
168+ return fmt .Errorf ("operation denied (non-interactive mode): %s" , cmd )
169+ }
170+ return nil
171+ }
172+ defer tty .Close ()
173+
174+ // Build the prompt
175+ fmt .Fprintf (os .Stderr , "\n ⚠️ \033 [1mRisk:\033 [0m %s\n " , cls )
176+ fmt .Fprintf (os .Stderr , " \033 [1mRun:\033 [0m %s\n " , cmd )
177+ if description != "" {
178+ fmt .Fprintf (os .Stderr , " \033 [1mWhy:\033 [0m %s\n " , description )
179+ }
180+ fmt .Fprint (os .Stderr , "\n [A]pprove [D]eny [?] Context [T]rust session: " )
181+
182+ // Read a single line of input from the TTY
183+ reader := bufio .NewReader (tty )
184+ line , err := reader .ReadString ('\n' )
185+ if err != nil {
186+ return fmt .Errorf ("approval prompt error: %w" , err )
187+ }
188+ line = strings .TrimSpace (strings .ToLower (line ))
189+
190+ switch line {
191+ case "a" , "approve" :
192+ return nil
193+ case "t" , "trust" :
194+ // Cache this risk class for the session
195+ if t .trustedClasses == nil {
196+ t .trustedClasses = make (map [danger.RiskClass ]bool )
197+ }
198+ t .trustedClasses [cls ] = true
199+ return nil
200+ case "?" , "context" :
201+ fmt .Fprintf (tty , "\n Command: %s\n " , cmd )
202+ fmt .Fprintf (tty , " Risk class: %s\n " , cls )
203+ if description != "" {
204+ fmt .Fprintf (tty , " Description: %s\n " , description )
205+ }
206+ fmt .Fprintf (tty , " Trust this class: %v\n " , t .trustedClasses [cls ])
207+ // Re-prompt
208+ return t .promptUser (cmd , description )
209+ default :
210+ return fmt .Errorf ("operation denied by user: %s" , cmd )
211+ }
212+ }
213+
102214// buildCmd constructs the exec.Cmd for the given shell command.
103215//
104216// When sandbox mode is active (containerName is non-empty), the command
0 commit comments