Add jail-by-default setting
- commit
- 8fb72f3673a522d9aa7479d5ea508c086e9c582d
- author
- patriceckhart · · 2026-07-14 16:41 UTC
- parent
- 6b4f716
- signature
- unsigned
- refs
- tag: v0.2.86
9 files · +137 -1
M README.md +2 -1
| @@ -351,6 +351,7 @@ Opens a dialog with every persistent setting. `up`/`down` to navigate, `enter` o | |||
| 351 | 351 | ||
| 352 | 352 | - **render images when supported** — draw screenshots / `read`-returned images inline using the terminal's image protocol, or fall back to a text placeholder. Auto-detected from `TERM_PROGRAM`; the toggle overrides the detection. The row is greyed out and forced off on terminals that don't speak any image protocol. | |
| 353 | 353 | - **auto-swarm** — let the main agent spawn background sub-agents in parallel via a built-in `swarm_spawn` tool. Off by default. When on, the tool is registered with the running agent, the system prompt gains a short addendum telling the model to delegate independent sub-tasks proactively, and zot watches every sub-agent the main agent spawns. As soon as the last sub-agent in a batch finishes its initial task, an `[auto-swarm update]` message is injected back into the chat with each agent's status / task / transcript tail, so the main agent can summarise the collective outcome. Flipping off mid-session removes the tool from the live agent and strips the addendum on the next turn — the model stops trying to delegate. See `/swarm` for the dashboard that lets you monitor, message, kill, or remove the spawned agents. | |
| 354 | + | - **jail new sessions by default**: start every new agent with tools confined to its working directory. Off by default. The setting applies to interactive, print, JSON, RPC, and background-agent runs, persists as `jail_by_default`, and immediately updates the current interactive session. `/jail` and `/unjail` remain session-scoped overrides and do not change this default. | |
| 354 | 355 | - **compact transcript rendering**: reduce visual chrome in the chat transcript. Tool calls render as a quiet header plus indented output instead of a bordered box, and sent messages render without padded background bubbles. Off by default. Changes apply immediately and persist to `config.json` as `compact_mode`. | |
| 355 | 356 | - **TUI settings**: opens a sub-view for input layout and status placement. **Input style** can be `plain` (default prompt line), `lines` (separator lines above and below the input), or `block` (a user-bubble-style input block). **Status position** places model, usage, and working-directory information above or below the input. **Working spinner position** places the busy spinner above or below the input. Changes apply immediately and persist to `config.json` as `tui_input_style`, `tui_status_position`, and `tui_working_position` (`above_input` or `below_input` for the position fields). | |
| 356 | 357 | - **thinking level**: choose reasoning for supported models: off, minimum, low, medium, high, xhigh, or max. The `max` tier is opt-in and sent natively to GPT-5.6 and adaptive-thinking Claude models; unsupported backends clamp it to their highest accepted effort. The change is persisted to `config.json` and applied to the next model call. | |
| @@ -369,7 +370,7 @@ zot also auto-compacts in the background: after any turn that leaves context usa | |||
| 369 | 370 | ||
| 370 | 371 | ### `/jail` | |
| 371 | 372 | ||
| 372 | - | 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. | |
| 373 | + | 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. Enable **jail new sessions by default** in `/settings` to persist this behavior across launches; `/unjail` then unlocks only the current session. | |
| 373 | 374 | ||
| 374 | 375 | This is a guardrail against accidents, not a hard security boundary. If you need real isolation, run zot under docker or a proper sandbox. | |
| 375 | 376 | ||
| @@ -351,6 +351,7 @@ Opens a dialog with every persistent setting. `up`/`down` to navigate, `enter` o | |||||
| 351 | 351 | ||||
| 352 | - **render images when supported** — draw screenshots / `read`-returned images inline using the terminal's image protocol, or fall back to a text placeholder. Auto-detected from `TERM_PROGRAM`; the toggle overrides the detection. The row is greyed out and forced off on terminals that don't speak any image protocol. | 352 | - **render images when supported** — draw screenshots / `read`-returned images inline using the terminal's image protocol, or fall back to a text placeholder. Auto-detected from `TERM_PROGRAM`; the toggle overrides the detection. The row is greyed out and forced off on terminals that don't speak any image protocol. | ||
| 353 | - **auto-swarm** — let the main agent spawn background sub-agents in parallel via a built-in `swarm_spawn` tool. Off by default. When on, the tool is registered with the running agent, the system prompt gains a short addendum telling the model to delegate independent sub-tasks proactively, and zot watches every sub-agent the main agent spawns. As soon as the last sub-agent in a batch finishes its initial task, an `[auto-swarm update]` message is injected back into the chat with each agent's status / task / transcript tail, so the main agent can summarise the collective outcome. Flipping off mid-session removes the tool from the live agent and strips the addendum on the next turn — the model stops trying to delegate. See `/swarm` for the dashboard that lets you monitor, message, kill, or remove the spawned agents. | 353 | - **auto-swarm** — let the main agent spawn background sub-agents in parallel via a built-in `swarm_spawn` tool. Off by default. When on, the tool is registered with the running agent, the system prompt gains a short addendum telling the model to delegate independent sub-tasks proactively, and zot watches every sub-agent the main agent spawns. As soon as the last sub-agent in a batch finishes its initial task, an `[auto-swarm update]` message is injected back into the chat with each agent's status / task / transcript tail, so the main agent can summarise the collective outcome. Flipping off mid-session removes the tool from the live agent and strips the addendum on the next turn — the model stops trying to delegate. See `/swarm` for the dashboard that lets you monitor, message, kill, or remove the spawned agents. | ||
| 354 | + | - **jail new sessions by default**: start every new agent with tools confined to its working directory. Off by default. The setting applies to interactive, print, JSON, RPC, and background-agent runs, persists as `jail_by_default`, and immediately updates the current interactive session. `/jail` and `/unjail` remain session-scoped overrides and do not change this default. | |||
| 354 | - **compact transcript rendering**: reduce visual chrome in the chat transcript. Tool calls render as a quiet header plus indented output instead of a bordered box, and sent messages render without padded background bubbles. Off by default. Changes apply immediately and persist to `config.json` as `compact_mode`. | 355 | - **compact transcript rendering**: reduce visual chrome in the chat transcript. Tool calls render as a quiet header plus indented output instead of a bordered box, and sent messages render without padded background bubbles. Off by default. Changes apply immediately and persist to `config.json` as `compact_mode`. | ||
| 355 | - **TUI settings**: opens a sub-view for input layout and status placement. **Input style** can be `plain` (default prompt line), `lines` (separator lines above and below the input), or `block` (a user-bubble-style input block). **Status position** places model, usage, and working-directory information above or below the input. **Working spinner position** places the busy spinner above or below the input. Changes apply immediately and persist to `config.json` as `tui_input_style`, `tui_status_position`, and `tui_working_position` (`above_input` or `below_input` for the position fields). | 356 | - **TUI settings**: opens a sub-view for input layout and status placement. **Input style** can be `plain` (default prompt line), `lines` (separator lines above and below the input), or `block` (a user-bubble-style input block). **Status position** places model, usage, and working-directory information above or below the input. **Working spinner position** places the busy spinner above or below the input. Changes apply immediately and persist to `config.json` as `tui_input_style`, `tui_status_position`, and `tui_working_position` (`above_input` or `below_input` for the position fields). | ||
| 356 | - **thinking level**: choose reasoning for supported models: off, minimum, low, medium, high, xhigh, or max. The `max` tier is opt-in and sent natively to GPT-5.6 and adaptive-thinking Claude models; unsupported backends clamp it to their highest accepted effort. The change is persisted to `config.json` and applied to the next model call. | 357 | - **thinking level**: choose reasoning for supported models: off, minimum, low, medium, high, xhigh, or max. The `max` tier is opt-in and sent natively to GPT-5.6 and adaptive-thinking Claude models; unsupported backends clamp it to their highest accepted effort. The change is persisted to `config.json` and applied to the next model call. | ||
| @@ -369,7 +370,7 @@ zot also auto-compacts in the background: after any turn that leaves context usa | |||||
| 369 | 370 | ||||
| 370 | ### `/jail` | 371 | ### `/jail` | ||
| 371 | 372 | ||||
| 372 | - | 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. | 373 | + | 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. Enable **jail new sessions by default** in `/settings` to persist this behavior across launches; `/unjail` then unlocks only the current session. |
| 373 | 374 | ||||
| 374 | This is a guardrail against accidents, not a hard security boundary. If you need real isolation, run zot under docker or a proper sandbox. | 375 | This is a guardrail against accidents, not a hard security boundary. If you need real isolation, run zot under docker or a proper sandbox. | ||
| 375 | 376 | ||||
M packages/agent/build.go +3 -0
| @@ -528,6 +528,9 @@ func Resolve(args Args, requireCred bool) (Resolved, error) { | |||
| 528 | 528 | } | |
| 529 | 529 | ||
| 530 | 530 | sandbox := tools.NewSandbox(args.CWD) | |
| 531 | + | if cfg.JailByDefault != nil && *cfg.JailByDefault { | |
| 532 | + | sandbox.Lock() | |
| 533 | + | } | |
| 531 | 534 | if args.PermissionSet != nil { | |
| 532 | 535 | sandbox.SetPermissions(args.PermissionSet) | |
| 533 | 536 | } | |
| @@ -528,6 +528,9 @@ func Resolve(args Args, requireCred bool) (Resolved, error) { | |||||
| 528 | } | 528 | } | ||
| 529 | 529 | ||||
| 530 | sandbox := tools.NewSandbox(args.CWD) | 530 | sandbox := tools.NewSandbox(args.CWD) | ||
| 531 | + | if cfg.JailByDefault != nil && *cfg.JailByDefault { | |||
| 532 | + | sandbox.Lock() | |||
| 533 | + | } | |||
| 531 | if args.PermissionSet != nil { | 534 | if args.PermissionSet != nil { | ||
| 532 | sandbox.SetPermissions(args.PermissionSet) | 535 | sandbox.SetPermissions(args.PermissionSet) | ||
| 533 | } | 536 | } | ||
M packages/agent/build_test.go +29 -0
| @@ -87,6 +87,35 @@ func TestResolveFallsBackWhenConfiguredModelIsGone(t *testing.T) { | |||
| 87 | 87 | } | |
| 88 | 88 | } | |
| 89 | 89 | ||
| 90 | + | func TestResolveAppliesJailByDefault(t *testing.T) { | |
| 91 | + | for _, tc := range []struct { | |
| 92 | + | name string | |
| 93 | + | config Config | |
| 94 | + | want bool | |
| 95 | + | }{ | |
| 96 | + | {name: "missing defaults to unlocked", config: Config{Provider: "openai", Model: "gpt-5"}}, | |
| 97 | + | {name: "enabled starts locked", config: Config{Provider: "openai", Model: "gpt-5", JailByDefault: boolPtr(true)}, want: true}, | |
| 98 | + | } { | |
| 99 | + | t.Run(tc.name, func(t *testing.T) { | |
| 100 | + | t.Setenv("ZOT_HOME", t.TempDir()) | |
| 101 | + | t.Setenv("OPENAI_API_KEY", "test-key") | |
| 102 | + | if err := SaveConfig(tc.config); err != nil { | |
| 103 | + | t.Fatal(err) | |
| 104 | + | } | |
| 105 | + | ||
| 106 | + | r, err := Resolve(Args{}, false) | |
| 107 | + | if err != nil { | |
| 108 | + | t.Fatalf("Resolve failed: %v", err) | |
| 109 | + | } | |
| 110 | + | if got := r.Sandbox.Locked(); got != tc.want { | |
| 111 | + | t.Fatalf("sandbox locked = %v, want %v", got, tc.want) | |
| 112 | + | } | |
| 113 | + | }) | |
| 114 | + | } | |
| 115 | + | } | |
| 116 | + | ||
| 117 | + | func boolPtr(v bool) *bool { return &v } | |
| 118 | + | ||
| 90 | 119 | // TestResolveExplicitFlagStaleDoesNotRepairConfig confirms the | |
| 91 | 120 | // repair-on-disk happens ONLY when the stale id came from the | |
| 92 | 121 | // persisted config. If the user passed --model X explicitly and X is | |
| @@ -87,6 +87,35 @@ func TestResolveFallsBackWhenConfiguredModelIsGone(t *testing.T) { | |||||
| 87 | } | 87 | } | ||
| 88 | } | 88 | } | ||
| 89 | 89 | ||||
| 90 | + | func TestResolveAppliesJailByDefault(t *testing.T) { | |||
| 91 | + | for _, tc := range []struct { | |||
| 92 | + | name string | |||
| 93 | + | config Config | |||
| 94 | + | want bool | |||
| 95 | + | }{ | |||
| 96 | + | {name: "missing defaults to unlocked", config: Config{Provider: "openai", Model: "gpt-5"}}, | |||
| 97 | + | {name: "enabled starts locked", config: Config{Provider: "openai", Model: "gpt-5", JailByDefault: boolPtr(true)}, want: true}, | |||
| 98 | + | } { | |||
| 99 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 100 | + | t.Setenv("ZOT_HOME", t.TempDir()) | |||
| 101 | + | t.Setenv("OPENAI_API_KEY", "test-key") | |||
| 102 | + | if err := SaveConfig(tc.config); err != nil { | |||
| 103 | + | t.Fatal(err) | |||
| 104 | + | } | |||
| 105 | + | ||||
| 106 | + | r, err := Resolve(Args{}, false) | |||
| 107 | + | if err != nil { | |||
| 108 | + | t.Fatalf("Resolve failed: %v", err) | |||
| 109 | + | } | |||
| 110 | + | if got := r.Sandbox.Locked(); got != tc.want { | |||
| 111 | + | t.Fatalf("sandbox locked = %v, want %v", got, tc.want) | |||
| 112 | + | } | |||
| 113 | + | }) | |||
| 114 | + | } | |||
| 115 | + | } | |||
| 116 | + | ||||
| 117 | + | func boolPtr(v bool) *bool { return &v } | |||
| 118 | + | ||||
| 90 | // TestResolveExplicitFlagStaleDoesNotRepairConfig confirms the | 119 | // TestResolveExplicitFlagStaleDoesNotRepairConfig confirms the | ||
| 91 | // repair-on-disk happens ONLY when the stale id came from the | 120 | // repair-on-disk happens ONLY when the stale id came from the | ||
| 92 | // persisted config. If the user passed --model X explicitly and X is | 121 | // persisted config. If the user passed --model X explicitly and X is | ||
M packages/agent/cli.go +1 -0
| @@ -953,6 +953,7 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||
| 953 | 953 | Theme: theme, | |
| 954 | 954 | InlineImagesEnabled: initialCfg.InlineImagesEnabled, | |
| 955 | 955 | AutoSwarmEnabled: initialCfg.AutoSwarmEnabled, | |
| 956 | + | JailByDefault: initialCfg.JailByDefault, | |
| 956 | 957 | QuickModelShortcuts: quickModelShortcuts, | |
| 957 | 958 | RecursiveFileSuggest: initialCfg.RecursiveFileSuggest, | |
| 958 | 959 | RespectGitignore: initialCfg.RespectGitignore, | |
| @@ -953,6 +953,7 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||||
| 953 | Theme: theme, | 953 | Theme: theme, | ||
| 954 | InlineImagesEnabled: initialCfg.InlineImagesEnabled, | 954 | InlineImagesEnabled: initialCfg.InlineImagesEnabled, | ||
| 955 | AutoSwarmEnabled: initialCfg.AutoSwarmEnabled, | 955 | AutoSwarmEnabled: initialCfg.AutoSwarmEnabled, | ||
| 956 | + | JailByDefault: initialCfg.JailByDefault, | |||
| 956 | QuickModelShortcuts: quickModelShortcuts, | 957 | QuickModelShortcuts: quickModelShortcuts, | ||
| 957 | RecursiveFileSuggest: initialCfg.RecursiveFileSuggest, | 958 | RecursiveFileSuggest: initialCfg.RecursiveFileSuggest, | ||
| 958 | RespectGitignore: initialCfg.RespectGitignore, | 959 | RespectGitignore: initialCfg.RespectGitignore, | ||
M packages/agent/config.go +5 -0
| @@ -57,6 +57,11 @@ type Config struct { | |||
| 57 | 57 | // default; nil/missing means disabled. Toggle from /settings. | |
| 58 | 58 | AutoSwarmEnabled *bool `json:"auto_swarm_enabled,omitempty"` | |
| 59 | 59 | ||
| 60 | + | // JailByDefault confines tools to the session working directory when | |
| 61 | + | // a new agent starts. Off by default; nil/missing means disabled. | |
| 62 | + | // The live session can still override this with /jail or /unjail. | |
| 63 | + | JailByDefault *bool `json:"jail_by_default,omitempty"` | |
| 64 | + | ||
| 60 | 65 | // RecursiveFileSuggest controls the @-mention file picker. When true | |
| 61 | 66 | // the picker fuzzy-searches the whole project tree below the working | |
| 62 | 67 | // directory; nil/missing/false keeps the default directory-by- | |
| @@ -57,6 +57,11 @@ type Config struct { | |||||
| 57 | // default; nil/missing means disabled. Toggle from /settings. | 57 | // default; nil/missing means disabled. Toggle from /settings. | ||
| 58 | AutoSwarmEnabled *bool `json:"auto_swarm_enabled,omitempty"` | 58 | AutoSwarmEnabled *bool `json:"auto_swarm_enabled,omitempty"` | ||
| 59 | 59 | ||||
| 60 | + | // JailByDefault confines tools to the session working directory when | |||
| 61 | + | // a new agent starts. Off by default; nil/missing means disabled. | |||
| 62 | + | // The live session can still override this with /jail or /unjail. | |||
| 63 | + | JailByDefault *bool `json:"jail_by_default,omitempty"` | |||
| 64 | + | ||||
| 60 | // RecursiveFileSuggest controls the @-mention file picker. When true | 65 | // RecursiveFileSuggest controls the @-mention file picker. When true | ||
| 61 | // the picker fuzzy-searches the whole project tree below the working | 66 | // the picker fuzzy-searches the whole project tree below the working | ||
| 62 | // directory; nil/missing/false keeps the default directory-by- | 67 | // directory; nil/missing/false keeps the default directory-by- | ||
M packages/agent/modes/interactive.go +34 -0
| @@ -48,6 +48,10 @@ type InteractiveConfig struct { | |||
| 48 | 48 | // re-reading config.json on every open. | |
| 49 | 49 | AutoSwarmEnabled *bool | |
| 50 | 50 | ||
| 51 | + | // JailByDefault mirrors the persisted jail_by_default preference. | |
| 52 | + | // The current sandbox may differ after a session-scoped /jail or /unjail. | |
| 53 | + | JailByDefault *bool | |
| 54 | + | ||
| 51 | 55 | // RecursiveFileSuggest mirrors the persisted recursive_file_suggest | |
| 52 | 56 | // flag at startup. When true the @-mention picker fuzzy-searches the | |
| 53 | 57 | // whole project tree instead of browsing one directory at a time. | |
| @@ -278,6 +282,7 @@ type SettingsStore interface { | |||
| 278 | 282 | SetQuickModelShortcut(slot int, providerName, model string) error | |
| 279 | 283 | SetInlineImages(enabled bool) error | |
| 280 | 284 | SetAutoSwarm(enabled bool) error | |
| 285 | + | SetJailByDefault(enabled bool) error | |
| 281 | 286 | SetRecursiveFileSuggest(enabled bool) error | |
| 282 | 287 | SetRespectGitignore(enabled bool) error | |
| 283 | 288 | SetCompactMode(enabled bool) error | |
| @@ -2913,6 +2918,7 @@ func (i *Interactive) openSettingsDialog() { | |||
| 2913 | 2918 | autoSwarmHint = "swarm supervisor not available in this mode" | |
| 2914 | 2919 | } | |
| 2915 | 2920 | ||
| 2921 | + | jailByDefault := i.cfg.JailByDefault != nil && *i.cfg.JailByDefault | |
| 2916 | 2922 | recursiveFiles := i.cfg.RecursiveFileSuggest != nil && *i.cfg.RecursiveFileSuggest | |
| 2917 | 2923 | respectGitignore := i.cfg.RespectGitignore == nil || *i.cfg.RespectGitignore | |
| 2918 | 2924 | compactMode := i.compactModeEnabled() | |
| @@ -3021,6 +3027,12 @@ func (i *Interactive) openSettingsDialog() { | |||
| 3021 | 3027 | disabled: autoSwarmDisabled, | |
| 3022 | 3028 | hint: autoSwarmHint, | |
| 3023 | 3029 | }, | |
| 3030 | + | { | |
| 3031 | + | key: "jail_by_default", | |
| 3032 | + | label: "jail new sessions by default", | |
| 3033 | + | desc: "confine tools to the session working directory unless /unjail is used", | |
| 3034 | + | value: jailByDefault, | |
| 3035 | + | }, | |
| 3024 | 3036 | { | |
| 3025 | 3037 | key: "recursive_file_suggest", | |
| 3026 | 3038 | label: "recursive @-file search", | |
| @@ -3332,6 +3344,28 @@ func (i *Interactive) applySettingToggle(key string, value bool) { | |||
| 3332 | 3344 | i.statusOK = "auto-swarm " + onOff(value) | |
| 3333 | 3345 | i.statusErr = "" | |
| 3334 | 3346 | i.mu.Unlock() | |
| 3347 | + | case "jail_by_default": | |
| 3348 | + | val := value | |
| 3349 | + | i.cfg.JailByDefault = &val | |
| 3350 | + | if i.cfg.SettingsStore != nil { | |
| 3351 | + | if err := i.cfg.SettingsStore.SetJailByDefault(value); err != nil { | |
| 3352 | + | i.mu.Lock() | |
| 3353 | + | i.statusErr = "settings: " + err.Error() | |
| 3354 | + | i.mu.Unlock() | |
| 3355 | + | return | |
| 3356 | + | } | |
| 3357 | + | } | |
| 3358 | + | if i.cfg.Sandbox != nil { | |
| 3359 | + | if value { | |
| 3360 | + | i.cfg.Sandbox.Lock() | |
| 3361 | + | } else { | |
| 3362 | + | i.cfg.Sandbox.Unlock() | |
| 3363 | + | } | |
| 3364 | + | } | |
| 3365 | + | i.mu.Lock() | |
| 3366 | + | i.statusOK = "jail by default " + onOff(value) | |
| 3367 | + | i.statusErr = "" | |
| 3368 | + | i.mu.Unlock() | |
| 3335 | 3369 | case "recursive_file_suggest": | |
| 3336 | 3370 | val := value | |
| 3337 | 3371 | i.cfg.RecursiveFileSuggest = &val | |
| @@ -48,6 +48,10 @@ type InteractiveConfig struct { | |||||
| 48 | // re-reading config.json on every open. | 48 | // re-reading config.json on every open. | ||
| 49 | AutoSwarmEnabled *bool | 49 | AutoSwarmEnabled *bool | ||
| 50 | 50 | ||||
| 51 | + | // JailByDefault mirrors the persisted jail_by_default preference. | |||
| 52 | + | // The current sandbox may differ after a session-scoped /jail or /unjail. | |||
| 53 | + | JailByDefault *bool | |||
| 54 | + | ||||
| 51 | // RecursiveFileSuggest mirrors the persisted recursive_file_suggest | 55 | // RecursiveFileSuggest mirrors the persisted recursive_file_suggest | ||
| 52 | // flag at startup. When true the @-mention picker fuzzy-searches the | 56 | // flag at startup. When true the @-mention picker fuzzy-searches the | ||
| 53 | // whole project tree instead of browsing one directory at a time. | 57 | // whole project tree instead of browsing one directory at a time. | ||
| @@ -278,6 +282,7 @@ type SettingsStore interface { | |||||
| 278 | SetQuickModelShortcut(slot int, providerName, model string) error | 282 | SetQuickModelShortcut(slot int, providerName, model string) error | ||
| 279 | SetInlineImages(enabled bool) error | 283 | SetInlineImages(enabled bool) error | ||
| 280 | SetAutoSwarm(enabled bool) error | 284 | SetAutoSwarm(enabled bool) error | ||
| 285 | + | SetJailByDefault(enabled bool) error | |||
| 281 | SetRecursiveFileSuggest(enabled bool) error | 286 | SetRecursiveFileSuggest(enabled bool) error | ||
| 282 | SetRespectGitignore(enabled bool) error | 287 | SetRespectGitignore(enabled bool) error | ||
| 283 | SetCompactMode(enabled bool) error | 288 | SetCompactMode(enabled bool) error | ||
| @@ -2913,6 +2918,7 @@ func (i *Interactive) openSettingsDialog() { | |||||
| 2913 | autoSwarmHint = "swarm supervisor not available in this mode" | 2918 | autoSwarmHint = "swarm supervisor not available in this mode" | ||
| 2914 | } | 2919 | } | ||
| 2915 | 2920 | ||||
| 2921 | + | jailByDefault := i.cfg.JailByDefault != nil && *i.cfg.JailByDefault | |||
| 2916 | recursiveFiles := i.cfg.RecursiveFileSuggest != nil && *i.cfg.RecursiveFileSuggest | 2922 | recursiveFiles := i.cfg.RecursiveFileSuggest != nil && *i.cfg.RecursiveFileSuggest | ||
| 2917 | respectGitignore := i.cfg.RespectGitignore == nil || *i.cfg.RespectGitignore | 2923 | respectGitignore := i.cfg.RespectGitignore == nil || *i.cfg.RespectGitignore | ||
| 2918 | compactMode := i.compactModeEnabled() | 2924 | compactMode := i.compactModeEnabled() | ||
| @@ -3021,6 +3027,12 @@ func (i *Interactive) openSettingsDialog() { | |||||
| 3021 | disabled: autoSwarmDisabled, | 3027 | disabled: autoSwarmDisabled, | ||
| 3022 | hint: autoSwarmHint, | 3028 | hint: autoSwarmHint, | ||
| 3023 | }, | 3029 | }, | ||
| 3030 | + | { | |||
| 3031 | + | key: "jail_by_default", | |||
| 3032 | + | label: "jail new sessions by default", | |||
| 3033 | + | desc: "confine tools to the session working directory unless /unjail is used", | |||
| 3034 | + | value: jailByDefault, | |||
| 3035 | + | }, | |||
| 3024 | { | 3036 | { | ||
| 3025 | key: "recursive_file_suggest", | 3037 | key: "recursive_file_suggest", | ||
| 3026 | label: "recursive @-file search", | 3038 | label: "recursive @-file search", | ||
| @@ -3332,6 +3344,28 @@ func (i *Interactive) applySettingToggle(key string, value bool) { | |||||
| 3332 | i.statusOK = "auto-swarm " + onOff(value) | 3344 | i.statusOK = "auto-swarm " + onOff(value) | ||
| 3333 | i.statusErr = "" | 3345 | i.statusErr = "" | ||
| 3334 | i.mu.Unlock() | 3346 | i.mu.Unlock() | ||
| 3347 | + | case "jail_by_default": | |||
| 3348 | + | val := value | |||
| 3349 | + | i.cfg.JailByDefault = &val | |||
| 3350 | + | if i.cfg.SettingsStore != nil { | |||
| 3351 | + | if err := i.cfg.SettingsStore.SetJailByDefault(value); err != nil { | |||
| 3352 | + | i.mu.Lock() | |||
| 3353 | + | i.statusErr = "settings: " + err.Error() | |||
| 3354 | + | i.mu.Unlock() | |||
| 3355 | + | return | |||
| 3356 | + | } | |||
| 3357 | + | } | |||
| 3358 | + | if i.cfg.Sandbox != nil { | |||
| 3359 | + | if value { | |||
| 3360 | + | i.cfg.Sandbox.Lock() | |||
| 3361 | + | } else { | |||
| 3362 | + | i.cfg.Sandbox.Unlock() | |||
| 3363 | + | } | |||
| 3364 | + | } | |||
| 3365 | + | i.mu.Lock() | |||
| 3366 | + | i.statusOK = "jail by default " + onOff(value) | |||
| 3367 | + | i.statusErr = "" | |||
| 3368 | + | i.mu.Unlock() | |||
| 3335 | case "recursive_file_suggest": | 3369 | case "recursive_file_suggest": | ||
| 3336 | val := value | 3370 | val := value | ||
| 3337 | i.cfg.RecursiveFileSuggest = &val | 3371 | i.cfg.RecursiveFileSuggest = &val | ||
A packages/agent/modes/jail_setting_test.go +30 -0
| @@ -0,0 +1,30 @@ | |||
| 1 | + | package modes | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | "github.com/patriceckhart/zot/packages/agent/tools" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | func TestJailByDefaultSettingUpdatesLiveSandbox(t *testing.T) { | |
| 10 | + | sandbox := tools.NewSandbox(t.TempDir()) | |
| 11 | + | i := &Interactive{ | |
| 12 | + | cfg: InteractiveConfig{Sandbox: sandbox}, | |
| 13 | + | } | |
| 14 | + | ||
| 15 | + | i.applySettingToggle("jail_by_default", true) | |
| 16 | + | if !sandbox.Locked() { | |
| 17 | + | t.Fatal("enabling jail by default did not lock the live sandbox") | |
| 18 | + | } | |
| 19 | + | if i.cfg.JailByDefault == nil || !*i.cfg.JailByDefault { | |
| 20 | + | t.Fatal("interactive config did not record enabled jail default") | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | i.applySettingToggle("jail_by_default", false) | |
| 24 | + | if sandbox.Locked() { | |
| 25 | + | t.Fatal("disabling jail by default did not unlock the live sandbox") | |
| 26 | + | } | |
| 27 | + | if i.cfg.JailByDefault == nil || *i.cfg.JailByDefault { | |
| 28 | + | t.Fatal("interactive config did not record disabled jail default") | |
| 29 | + | } | |
| 30 | + | } | |
| @@ -0,0 +1,30 @@ | |||||
| 1 | + | package modes | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | "github.com/patriceckhart/zot/packages/agent/tools" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | func TestJailByDefaultSettingUpdatesLiveSandbox(t *testing.T) { | |||
| 10 | + | sandbox := tools.NewSandbox(t.TempDir()) | |||
| 11 | + | i := &Interactive{ | |||
| 12 | + | cfg: InteractiveConfig{Sandbox: sandbox}, | |||
| 13 | + | } | |||
| 14 | + | ||||
| 15 | + | i.applySettingToggle("jail_by_default", true) | |||
| 16 | + | if !sandbox.Locked() { | |||
| 17 | + | t.Fatal("enabling jail by default did not lock the live sandbox") | |||
| 18 | + | } | |||
| 19 | + | if i.cfg.JailByDefault == nil || !*i.cfg.JailByDefault { | |||
| 20 | + | t.Fatal("interactive config did not record enabled jail default") | |||
| 21 | + | } | |||
| 22 | + | ||||
| 23 | + | i.applySettingToggle("jail_by_default", false) | |||
| 24 | + | if sandbox.Locked() { | |||
| 25 | + | t.Fatal("disabling jail by default did not unlock the live sandbox") | |||
| 26 | + | } | |||
| 27 | + | if i.cfg.JailByDefault == nil || *i.cfg.JailByDefault { | |||
| 28 | + | t.Fatal("interactive config did not record disabled jail default") | |||
| 29 | + | } | |||
| 30 | + | } | |||
M packages/agent/settings_store.go +9 -0
| @@ -50,6 +50,15 @@ func (configSettingsStore) SetAutoSwarm(enabled bool) error { | |||
| 50 | 50 | return SaveConfig(cfg) | |
| 51 | 51 | } | |
| 52 | 52 | ||
| 53 | + | func (configSettingsStore) SetJailByDefault(enabled bool) error { | |
| 54 | + | cfg, err := LoadConfig() | |
| 55 | + | if err != nil { | |
| 56 | + | return err | |
| 57 | + | } | |
| 58 | + | cfg.JailByDefault = &enabled | |
| 59 | + | return SaveConfig(cfg) | |
| 60 | + | } | |
| 61 | + | ||
| 53 | 62 | func (configSettingsStore) SetRecursiveFileSuggest(enabled bool) error { | |
| 54 | 63 | cfg, err := LoadConfig() | |
| 55 | 64 | if err != nil { | |
| @@ -50,6 +50,15 @@ func (configSettingsStore) SetAutoSwarm(enabled bool) error { | |||||
| 50 | return SaveConfig(cfg) | 50 | return SaveConfig(cfg) | ||
| 51 | } | 51 | } | ||
| 52 | 52 | ||||
| 53 | + | func (configSettingsStore) SetJailByDefault(enabled bool) error { | |||
| 54 | + | cfg, err := LoadConfig() | |||
| 55 | + | if err != nil { | |||
| 56 | + | return err | |||
| 57 | + | } | |||
| 58 | + | cfg.JailByDefault = &enabled | |||
| 59 | + | return SaveConfig(cfg) | |||
| 60 | + | } | |||
| 61 | + | ||||
| 53 | func (configSettingsStore) SetRecursiveFileSuggest(enabled bool) error { | 62 | func (configSettingsStore) SetRecursiveFileSuggest(enabled bool) error { | ||
| 54 | cfg, err := LoadConfig() | 63 | cfg, err := LoadConfig() | ||
| 55 | if err != nil { | 64 | if err != nil { | ||
A packages/agent/settings_store_test.go +24 -0
| @@ -0,0 +1,24 @@ | |||
| 1 | + | package agent | |
| 2 | + | ||
| 3 | + | import "testing" | |
| 4 | + | ||
| 5 | + | func TestConfigSettingsStorePersistsJailByDefault(t *testing.T) { | |
| 6 | + | t.Setenv("ZOT_HOME", t.TempDir()) | |
| 7 | + | if err := SaveConfig(Config{Theme: "dark"}); err != nil { | |
| 8 | + | t.Fatal(err) | |
| 9 | + | } | |
| 10 | + | ||
| 11 | + | if err := (configSettingsStore{}).SetJailByDefault(true); err != nil { | |
| 12 | + | t.Fatal(err) | |
| 13 | + | } | |
| 14 | + | cfg, err := LoadConfig() | |
| 15 | + | if err != nil { | |
| 16 | + | t.Fatal(err) | |
| 17 | + | } | |
| 18 | + | if cfg.JailByDefault == nil || !*cfg.JailByDefault { | |
| 19 | + | t.Fatal("jail_by_default was not persisted as enabled") | |
| 20 | + | } | |
| 21 | + | if cfg.Theme != "dark" { | |
| 22 | + | t.Fatalf("unrelated config changed: theme = %q, want dark", cfg.Theme) | |
| 23 | + | } | |
| 24 | + | } | |
| @@ -0,0 +1,24 @@ | |||||
| 1 | + | package agent | |||
| 2 | + | ||||
| 3 | + | import "testing" | |||
| 4 | + | ||||
| 5 | + | func TestConfigSettingsStorePersistsJailByDefault(t *testing.T) { | |||
| 6 | + | t.Setenv("ZOT_HOME", t.TempDir()) | |||
| 7 | + | if err := SaveConfig(Config{Theme: "dark"}); err != nil { | |||
| 8 | + | t.Fatal(err) | |||
| 9 | + | } | |||
| 10 | + | ||||
| 11 | + | if err := (configSettingsStore{}).SetJailByDefault(true); err != nil { | |||
| 12 | + | t.Fatal(err) | |||
| 13 | + | } | |||
| 14 | + | cfg, err := LoadConfig() | |||
| 15 | + | if err != nil { | |||
| 16 | + | t.Fatal(err) | |||
| 17 | + | } | |||
| 18 | + | if cfg.JailByDefault == nil || !*cfg.JailByDefault { | |||
| 19 | + | t.Fatal("jail_by_default was not persisted as enabled") | |||
| 20 | + | } | |||
| 21 | + | if cfg.Theme != "dark" { | |||
| 22 | + | t.Fatalf("unrelated config changed: theme = %q, want dark", cfg.Theme) | |||
| 23 | + | } | |||
| 24 | + | } | |||