Skip to content

Commit f819d9d

Browse files
committed
v0.6.1: sandbox persistence on session resume
Add Sandbox bool to Session struct. When a session is created with --sandbox, the flag is saved. On resume (kode continue, kode repl --id), the sandbox is auto-re-enabled if the current config has it disabled, preventing accidental escape from the sandboxed environment. Changes: - internal/session/session.go: add Sandbox field to Session - cmd/kode/main.go: set Sandbox on session create in run(), check on resume in continueCmd() - cmd/kode/repl.go: restructure to load session before agent creation, check Sandbox flag, set on new sessions - docs/SESSIONS.md: document sandbox persistence behavior
1 parent a4c0aee commit f819d9d

4 files changed

Lines changed: 48 additions & 10 deletions

File tree

cmd/kode/main.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,8 @@ func run(args []string) error {
587587
if err != nil {
588588
return fmt.Errorf("save session: %w", err)
589589
}
590+
sess.Sandbox = resolved.Sandbox
591+
store.Save(sess)
590592
fmt.Fprintf(os.Stderr, "kode: session %s saved — continue with: kode continue \"...\"\n", sess.ID)
591593
} else {
592594
// Single-shot mode (default)
@@ -785,6 +787,12 @@ func continueCmd(args []string) error {
785787
// Resolve config (no CLI flags for continue — uses session's model)
786788
resolved := config.LoadConfig(config.CLIFlags{Model: sess.Model})
787789

790+
// Auto-apply sandbox if session was sandboxed (even if config changed)
791+
if sess.Sandbox && !resolved.Sandbox {
792+
resolved.Sandbox = true
793+
fmt.Fprintf(os.Stderr, "kode: session was sandboxed — enabling sandbox for this continuation\n")
794+
}
795+
788796
systemMessage := resolved.System
789797
if systemMessage == "" {
790798
systemMessage = defaultSystem

cmd/kode/repl.go

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,28 @@ func replCmd(args []string) error {
3131
return fmt.Errorf("session store: %w", err)
3232
}
3333

34-
// Resolve config
34+
// Load or create session first — needed to know sandbox state
35+
var sess *session.Session
36+
if sessionID != "" {
37+
sess, err = store.Load(sessionID)
38+
if err != nil {
39+
return fmt.Errorf("load session %q: %w", sessionID, err)
40+
}
41+
}
42+
43+
// Resolve config (before session creation so Session.Sandbox is set)
3544
resolved := config.LoadConfig(config.CLIFlags{})
3645
systemMessage := resolved.System
3746
if systemMessage == "" {
3847
systemMessage = defaultSystem
3948
}
4049

50+
// Auto-apply sandbox if resuming a sandboxed session
51+
if sess != nil && sess.Sandbox && !resolved.Sandbox {
52+
resolved.Sandbox = true
53+
fmt.Fprintf(os.Stderr, "kode: session was sandboxed — enabling sandbox\n")
54+
}
55+
4156
// Build tools
4257
tools := builtinTools()
4358
var sandboxCleanup func() error
@@ -85,15 +100,8 @@ func replCmd(args []string) error {
85100
}
86101
defer agent.Close()
87102

88-
// Load or create session
89-
var sess *session.Session
90-
if sessionID != "" {
91-
sess, err = store.Load(sessionID)
92-
if err != nil {
93-
return fmt.Errorf("load session %q: %w", sessionID, err)
94-
}
95-
} else {
96-
// Start fresh session with just the system message
103+
// Create session if not resuming one
104+
if sess == nil {
97105
sess, err = store.Create(
98106
[]llm.Message{{Role: "system", Content: systemMessage}},
99107
resolved.Model,
@@ -102,6 +110,8 @@ func replCmd(args []string) error {
102110
if err != nil {
103111
return fmt.Errorf("create session: %w", err)
104112
}
113+
sess.Sandbox = resolved.Sandbox
114+
store.Save(sess)
105115
}
106116

107117
fmt.Fprintf(os.Stderr, "\nkode ⚡ %s · session %s\n\n", modelLabel, sess.ID)
@@ -178,6 +188,9 @@ func handleREPLCommand(input string, sess *session.Session) bool {
178188
fmt.Fprintf(os.Stderr, "Session: %s\n", sess.ID)
179189
fmt.Fprintf(os.Stderr, "Model: %s\n", sess.Model)
180190
fmt.Fprintf(os.Stderr, "Turns: %d\n", sess.Turns)
191+
if sess.Sandbox {
192+
fmt.Fprintf(os.Stderr, "Sandbox: yes\n")
193+
}
181194
default:
182195
fmt.Fprintf(os.Stderr, "Unknown command: %s (/help for commands)\n", input)
183196
}

docs/SESSIONS.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,19 @@ Sessions are stored as JSON at `~/.kode/sessions/<id>.json`:
115115
```
116116

117117
The `Session` struct has all public fields, enabling direct manipulation. This makes advanced operations (editing, truncating, merging) trivial — load, mutate, save.
118+
119+
## Sandbox persistence
120+
121+
When a session is created with `--sandbox`, the `sandbox` flag is stored in the session file. On resume (`kode continue` or `kode repl --id <id>`), the sandbox is automatically re-enabled even if the current config has it disabled:
122+
123+
```bash
124+
kode run --session --sandbox "Install deps and build"
125+
# → session saved with sandbox=true
126+
127+
# Later, in a different terminal without sandbox config:
128+
kode continue "Run the test suite"
129+
# → kode: session was sandboxed — enabling sandbox for this continuation
130+
```
131+
132+
This prevents accidentally escaping the sandbox on resume. The sandbox image/network/memory still come from the **current** config — only the toggle bit is persisted. To force-disable sandbox on resume, pass `kode continue` in a project with `"sandbox": false` in `./kode.json` and the session flag will be overridden by the explicit config.
133+

internal/session/session.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ type Session struct {
3939
Model string `json:"model"` // model name used
4040
Turns int `json:"turns"` // number of user turns
4141
Task string `json:"task"` // first user message (label)
42+
Sandbox bool `json:"sandbox"` // was sandboxed — auto-apply on resume
4243
Messages []llm.Message `json:"messages"` // full conversation history
4344
}
4445

0 commit comments

Comments
 (0)