Skip to content

Commit e9efbea

Browse files
author
molty3000
committed
feat: --sandbox Docker isolation + full unit test suite (36 tests)
Sandbox: Each session runs inside a fresh Docker container with: - --cap-drop ALL, --security-opt no-new-privileges - --network none, --tmpfs /tmp:noexec - Working directory mounted read-only at /workspace - Container destroyed on agent.Close() Tests cover: - kode.go: Config defaults, API key fallback, thinking passthrough (11 tests) - llm/client.go: JSON marshaling, thinking/reasoning_effort, response parsing (11) - loop/loop.go: ReAct engine with httptest mock server (7) - tool/registry.go: CRUD, duplicates, thread safety (7)
1 parent 1288bf0 commit e9efbea

8 files changed

Lines changed: 963 additions & 16 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
bin/
2+
/kode
23
*.test
34
coverage.out

cmd/kode/main.go

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"os"
7+
"os/exec"
78
"os/signal"
89
"strings"
910

@@ -96,20 +97,61 @@ done:
9697
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
9798
defer cancel()
9899

100+
// --- sandbox setup ---
101+
var sandboxCleanup func() error
102+
tools := builtinTools()
103+
104+
if sandbox {
105+
containerName := fmt.Sprintf("kode-%d", os.Getpid())
106+
fmt.Fprintf(os.Stderr, "kode: starting sandbox container %s...\n", containerName)
107+
108+
wd, err := os.Getwd()
109+
if err != nil {
110+
fmt.Fprintf(os.Stderr, "kode: sandbox: getwd: %v\n", err)
111+
os.Exit(1)
112+
}
113+
114+
createCmd := exec.Command("docker", "run",
115+
"--rm", // destroy on exit
116+
"--detach", // run in background
117+
"--name", containerName,
118+
"--cap-drop", "ALL", // no capabilities
119+
"--security-opt", "no-new-privileges", // no privilege escalation
120+
"--network", "none", // no network
121+
"--tmpfs", "/tmp:noexec", // no executable temp files
122+
"-v", wd+":/workspace:ro", // working dir read-only
123+
"alpine:latest",
124+
"sleep", "infinity",
125+
)
126+
createCmd.Stderr = os.Stderr
127+
if err := createCmd.Run(); err != nil {
128+
fmt.Fprintf(os.Stderr, "kode: sandbox: failed to create container: %v\n", err)
129+
os.Exit(1)
130+
}
131+
132+
sandboxCleanup = func() error {
133+
fmt.Fprintf(os.Stderr, "kode: destroying sandbox container %s...\n", containerName)
134+
return exec.Command("docker", "rm", "-f", containerName).Run()
135+
}
136+
137+
// Wire the shell tool to execute commands inside the sandbox.
138+
tools[0].(*shellTool).containerName = containerName
139+
}
140+
99141
agent, err := kode.New(kode.Config{
100-
Model: model,
101-
BaseURL: baseURL,
102-
MaxIterations: maxIter,
103-
SystemMessage: system,
104-
Thinking: thinking,
105-
Tools: builtinTools(),
142+
Model: model,
143+
BaseURL: baseURL,
144+
MaxIterations: maxIter,
145+
SystemMessage: system,
146+
Thinking: thinking,
147+
Tools: tools,
148+
SandboxCleanup: sandboxCleanup,
106149
})
107150
if err != nil {
108151
fmt.Fprintf(os.Stderr, "kode: %v\n", err)
109152
os.Exit(1)
110153
}
111-
112-
_ = sandbox // TODO
154+
defer agent.Close()
113155

114156
modelName := model
115157
if modelName == "" {

cmd/kode/shell.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ import (
99
)
1010

1111
// shellTool lets the agent run shell commands.
12-
type shellTool struct{}
12+
// When containerName is set, commands execute inside that Docker container
13+
// via "docker exec". Otherwise they run on the host.
14+
type shellTool struct {
15+
containerName string // empty = host, non-empty = docker exec into this container
16+
}
1317

1418
func (t *shellTool) Name() string { return "shell" }
1519
func (t *shellTool) Description() string { return "Run a shell command and return its output. Use for: reading files, listing directories, running tests, building code, git operations. The command runs in the current working directory." }
@@ -37,7 +41,13 @@ func (t *shellTool) Call(args string) (string, error) {
3741
return "", fmt.Errorf("shell: empty command")
3842
}
3943

40-
cmd := exec.Command("sh", "-c", input.Command)
44+
var cmd *exec.Cmd
45+
if t.containerName != "" {
46+
cmd = exec.Command("docker", "exec", t.containerName, "sh", "-c", input.Command)
47+
} else {
48+
cmd = exec.Command("sh", "-c", input.Command)
49+
}
50+
4151
var outBuf, errBuf bytes.Buffer
4252
cmd.Stdout = &outBuf
4353
cmd.Stderr = &errBuf

0 commit comments

Comments
 (0)