rename: /lock -> /jail, /unlock -> /unjail

User-facing slash commands renamed to /jail and /unjail. The
internal Sandbox type (Lock/Unlock/Locked methods, atomic.Bool
field) keeps its mutex-style names because those describe the
implementation, not the feature. Everything the user sees swaps:

- slashCatalog: /jail + /unjail entries and descriptions.
- runSlash handlers: case "/jail" / case "/unjail"; status line
  reports "jailed to <cwd>" / "unjailed".
- Status bar tag: "· jailed · ~/cwd" (was "· locked ·").
- Sandbox error messages: "jailed: path X is outside sandbox
  root Y (use /unjail to disable)" etc.
- README: table rows, section heading, body text, busy-mode
  section all updated.
- Website (/Users/pat/Sites/zot): Tools section prose updated.
- SDK doc comment in pkg/zotcore refers to /jail.

Internal identifiers (Sandbox, Lock(), Unlock(), Locked(),
CheckPath, CheckCommand, slashCancelsTurn switch) unchanged.

Verified: go vet clean, go test -race ./... clean, bun
typecheck + lint + build clean on the site.
This commit is contained in:
patriceckhart 2026-04-20 08:57:40 +02:00
parent c610a3a645
commit b6fc3fd886
8 changed files with 23 additions and 22 deletions

View file

@ -161,7 +161,7 @@ zot --help
- `edit`: one or more exact-match replacements in an existing file.
- `bash`: run a shell command in the session cwd, with merged stdout/stderr and a timeout.
When the sandbox is on (see `/lock`), all four tools refuse paths outside the session cwd.
When the sandbox is on (see `/jail`), all four tools refuse paths outside the session cwd.
## Modes
@ -194,8 +194,8 @@ Type `/` in the TUI to open the autocomplete popup. Available commands:
| `/btw` | Side chat with full context that doesn't add to the main thread. |
| `/skills` | List discovered skills (SKILL.md files) and preview their bodies. |
| `/compact` | Summarize the transcript into one message to free up context. |
| `/lock` | Confine tools to the current directory. |
| `/unlock` | Allow tools to touch paths outside again. |
| `/jail` | Confine tools to the current directory. |
| `/unjail` | Allow tools to touch paths outside again. |
| `/reload-ext` | Hot-reload all extensions (re-read manifests, respawn subprocesses, rebuild tool registry). |
| `/yolo` | Turn off `--no-yolo` confirmation for the rest of this session. |
| `/clear` | Clear the chat transcript. |
@ -236,9 +236,9 @@ Sends the current transcript through the model with a structured summarization p
zot also auto-compacts in the background: after any turn that leaves context usage at or above **85%** of the model's window, the agent kicks off a condense pass on its own. You'll see `condensing history, esc to cancel` above the status bar and an `(auto)` tag next to the context percentage; `esc` aborts it without touching the transcript.
### `/lock`
### `/jail`
Enforces a sandbox rooted at the cwd shown in the status bar. `read`, `write`, and `edit` resolve their target path (including through symlinks) and refuse anything outside the sandbox. `bash` refuses obvious escape patterns: `sudo`, `rm -rf /`, leading `cd /`, `cd ..`, `cd ~`, `chmod -R`, `dd of=/`, and similar. The status bar shows `locked, ~/your/cwd` while active.
Enforces a sandbox rooted at the cwd shown in the status bar. `read`, `write`, and `edit` resolve their target path (including through symlinks) and refuse anything outside the sandbox. `bash` refuses obvious escape patterns: `sudo`, `rm -rf /`, leading `cd /`, `cd ..`, `cd ~`, `chmod -R`, `dd of=/`, and similar. The status bar shows `jailed, ~/your/cwd` while active.
This is a guardrail against accidents, not a hard security boundary. If you need real isolation, run zot under docker or a proper sandbox.
@ -273,7 +273,8 @@ Frames containing images are full-repainted (no differential diff) to prevent st
You can keep typing while the agent is working. Pressing `enter` during a turn queues the message instead of interrupting: it shows up above the status bar as `sliding in: <text>` and is delivered as the next user turn the moment the current one finishes. Queue as many as you want; they run in order. `esc` or `ctrl+c` cancels the active turn and drops the queue so a runaway turn doesn't flood you with stale follow-ups.
Slash commands also work while the agent is busy. Read-only ones (`/help`, `/jump`, `/btw`, `/sessions`, `/skills`, `/lock`, `/unlock`, `/exit`) take effect immediately. Destructive ones (`/clear`, `/compact`, `/login`, `/logout`, `/model`, `/reload-ext`) cancel the active turn first and then run.
Slash commands also work while the agent is busy. Read-only ones (`/help`, `/jump`, `/btw`, `/sessions`, `/skills`, `/jail`, `/unjail`, `/exit`) take effect immediately. Destructive ones (`/clear`, `/compact`, `/login`, `/logout`, `/model`, `/reload-ext`) cancel the active turn first and then run.
## Keys (interactive mode)

View file

@ -301,7 +301,7 @@ func (r Resolved) NewClient() provider.Client {
}
// UseSandbox replaces the sandbox pointer that every tool in r's
// registry references. Used to keep the /lock state stable across
// registry references. Used to keep the /jail state stable across
// agent rebuilds (e.g. /login, /model switching providers).
func (r *Resolved) UseSandbox(s *tools.Sandbox) {
if s == nil || r == nil {

View file

@ -66,7 +66,7 @@ type InteractiveConfig struct {
// chat. Nil channel = no banner, no startup cost.
UpdateInfoChan <-chan UpdateInfo
// Sandbox is the shared sandbox pointer. Toggled by /lock and /unlock.
// Sandbox is the shared sandbox pointer. Toggled by /jail and /unjail.
Sandbox *tools.Sandbox
// LoadSession swaps the current session for the one at path. The
@ -1052,7 +1052,7 @@ func (i *Interactive) handleKey(ctx context.Context, k tui.Key) (done bool) {
// /compact, /logout, /login, /model) cancel the active turn
// first and wait for the goroutine to wind down so they don't
// race with a streaming response. Safe commands (/help,
// /jump, /sessions, /lock, /unlock, /exit) run immediately
// /jump, /sessions, /jail, /unjail, /exit) run immediately
// without disturbing the active turn.
if slashCancelsTurn(head) {
i.cancelAndWaitForIdle()
@ -1238,7 +1238,7 @@ func (i *Interactive) runSlash(ctx context.Context, cmd string) (done bool) {
i.openSkillsDialog()
case "/compact":
i.runCompact(ctx, false)
case "/lock":
case "/jail":
if i.cfg.Sandbox == nil {
i.mu.Lock()
i.statusErr = "sandbox not available in this build"
@ -1247,10 +1247,10 @@ func (i *Interactive) runSlash(ctx context.Context, cmd string) (done bool) {
}
i.cfg.Sandbox.Lock()
i.mu.Lock()
i.statusOK = "locked to " + i.cfg.CWD + " (tools cannot touch paths outside this directory)"
i.statusOK = "jailed to " + i.cfg.CWD + " (tools cannot touch paths outside this directory)"
i.statusErr = ""
i.mu.Unlock()
case "/unlock":
case "/unjail":
if i.cfg.Sandbox == nil {
i.mu.Lock()
i.statusErr = "sandbox not available in this build"
@ -1259,7 +1259,7 @@ func (i *Interactive) runSlash(ctx context.Context, cmd string) (done bool) {
}
i.cfg.Sandbox.Unlock()
i.mu.Lock()
i.statusOK = "unlocked"
i.statusOK = "unjailed"
i.statusErr = ""
i.mu.Unlock()
case "/reload-ext":

View file

@ -41,8 +41,8 @@ var slashCatalog = []slashCommand{
{Name: "/btw", Desc: "side-chat that doesn't add to the main thread (saves tokens)"},
{Name: "/skills", Desc: "list discovered skills (SKILL.md files)"},
{Name: "/compact", Desc: "summarize and replace the transcript to free up context"},
{Name: "/lock", Desc: "confine tools to the current directory"},
{Name: "/unlock", Desc: "allow tools to touch paths outside this directory"},
{Name: "/jail", Desc: "confine tools to the current directory"},
{Name: "/unjail", Desc: "allow tools to touch paths outside this directory"},
{Name: "/reload-ext", Desc: "hot-reload all extensions (re-read manifests and respawn)"},
{Name: "/yolo", Desc: "turn off --no-yolo confirmation for the rest of this session"},
{Name: "/clear", Desc: "clear the chat transcript"},

View file

@ -22,7 +22,7 @@ const (
// ReadTool reads file contents from disk.
type ReadTool struct {
CWD string
Sandbox *Sandbox // when locked, confines reads to the sandbox root
Sandbox *Sandbox // when jailed, confines reads to the sandbox root
}
type readArgs struct {

View file

@ -51,13 +51,13 @@ func (s *Sandbox) CheckPath(path string) error {
return fmt.Errorf("sandbox path: %w", err)
}
if !isUnder(rootAbs, target) {
return fmt.Errorf("locked: path %q is outside sandbox root %q (use /unlock to disable)", path, s.Root)
return fmt.Errorf("jailed: path %q is outside sandbox root %q (use /unjail to disable)", path, s.Root)
}
return nil
}
// CheckCommand applies a lightweight sanity check to a bash command
// when locked. We cannot fully sandbox a shell, but we can reject the
// when jailed. We cannot fully sandbox a shell, but we can reject the
// most obvious escapes so the model does not accidentally touch files
// outside root via absolute paths.
func (s *Sandbox) CheckCommand(cmd string) error {
@ -78,7 +78,7 @@ func (s *Sandbox) CheckCommand(cmd string) error {
lower := strings.ToLower(cmd)
for _, b := range banned {
if strings.Contains(lower, strings.ToLower(b)) {
return fmt.Errorf("locked: command contains banned pattern %q (use /unlock to disable)", b)
return fmt.Errorf("jailed: command contains banned pattern %q (use /unjail to disable)", b)
}
}
// Heuristic: reject a leading `cd /` or `cd ~` that tries to move
@ -89,7 +89,7 @@ func (s *Sandbox) CheckCommand(cmd string) error {
first = strings.TrimSpace(strings.SplitN(first, "&&", 2)[0])
if strings.HasPrefix(first, "cd /") || strings.HasPrefix(first, "cd ~") ||
strings.HasPrefix(first, "cd $HOME") || strings.HasPrefix(first, "cd ..") {
return fmt.Errorf("locked: cd outside sandbox root is not allowed (use /unlock to disable)")
return fmt.Errorf("jailed: cd outside sandbox root is not allowed (use /unjail to disable)")
}
return nil
}

View file

@ -1085,7 +1085,7 @@ func StatusBar(p StatusBarParams) []string {
cwd := shortenHome(p.CWD)
if p.Locked && cwd != "" {
cwd = locked · " + cwd
cwd = jailed · " + cwd
}
primary := leftBuilder.String()

View file

@ -79,7 +79,7 @@ type Config struct {
// NoTools disables every tool. Useful for chat-only embeddings.
NoTools bool
// Lock confines tools to CWD. Same effect as the /lock command.
// Lock confines tools to CWD. Same effect as the /jail command.
Lock bool
}