detached 8fb72f3

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
351351
352352 - **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.
353353 - **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.
354355 - **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`.
355356 - **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).
356357 - **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
369370
370371 ### `/jail`
371372
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.
373374
374375 This is a guardrail against accidents, not a hard security boundary. If you need real isolation, run zot under docker or a proper sandbox.
375376
@@ -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) {
528528 }
529529
530530 sandbox := tools.NewSandbox(args.CWD)
531+ if cfg.JailByDefault != nil && *cfg.JailByDefault {
532+ sandbox.Lock()
533+ }
531534 if args.PermissionSet != nil {
532535 sandbox.SetPermissions(args.PermissionSet)
533536 }
@@ -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) {
8787 }
8888 }
8989
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+
90119 // TestResolveExplicitFlagStaleDoesNotRepairConfig confirms the
91120 // repair-on-disk happens ONLY when the stale id came from the
92121 // 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 the119 // TestResolveExplicitFlagStaleDoesNotRepairConfig confirms the
91 // repair-on-disk happens ONLY when the stale id came from the120 // repair-on-disk happens ONLY when the stale id came from the
92 // persisted config. If the user passed --model X explicitly and X is121 // 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 {
953953 Theme: theme,
954954 InlineImagesEnabled: initialCfg.InlineImagesEnabled,
955955 AutoSwarmEnabled: initialCfg.AutoSwarmEnabled,
956+ JailByDefault: initialCfg.JailByDefault,
956957 QuickModelShortcuts: quickModelShortcuts,
957958 RecursiveFileSuggest: initialCfg.RecursiveFileSuggest,
958959 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 {
5757 // default; nil/missing means disabled. Toggle from /settings.
5858 AutoSwarmEnabled *bool `json:"auto_swarm_enabled,omitempty"`
5959
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+
6065 // RecursiveFileSuggest controls the @-mention file picker. When true
6166 // the picker fuzzy-searches the whole project tree below the working
6267 // 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 true65 // RecursiveFileSuggest controls the @-mention file picker. When true
61 // the picker fuzzy-searches the whole project tree below the working66 // 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 {
4848 // re-reading config.json on every open.
4949 AutoSwarmEnabled *bool
5050
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+
5155 // RecursiveFileSuggest mirrors the persisted recursive_file_suggest
5256 // flag at startup. When true the @-mention picker fuzzy-searches the
5357 // whole project tree instead of browsing one directory at a time.
@@ -278,6 +282,7 @@ type SettingsStore interface {
278282 SetQuickModelShortcut(slot int, providerName, model string) error
279283 SetInlineImages(enabled bool) error
280284 SetAutoSwarm(enabled bool) error
285+ SetJailByDefault(enabled bool) error
281286 SetRecursiveFileSuggest(enabled bool) error
282287 SetRespectGitignore(enabled bool) error
283288 SetCompactMode(enabled bool) error
@@ -2913,6 +2918,7 @@ func (i *Interactive) openSettingsDialog() {
29132918 autoSwarmHint = "swarm supervisor not available in this mode"
29142919 }
29152920
2921+ jailByDefault := i.cfg.JailByDefault != nil && *i.cfg.JailByDefault
29162922 recursiveFiles := i.cfg.RecursiveFileSuggest != nil && *i.cfg.RecursiveFileSuggest
29172923 respectGitignore := i.cfg.RespectGitignore == nil || *i.cfg.RespectGitignore
29182924 compactMode := i.compactModeEnabled()
@@ -3021,6 +3027,12 @@ func (i *Interactive) openSettingsDialog() {
30213027 disabled: autoSwarmDisabled,
30223028 hint: autoSwarmHint,
30233029 },
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+ },
30243036 {
30253037 key: "recursive_file_suggest",
30263038 label: "recursive @-file search",
@@ -3332,6 +3344,28 @@ func (i *Interactive) applySettingToggle(key string, value bool) {
33323344 i.statusOK = "auto-swarm " + onOff(value)
33333345 i.statusErr = ""
33343346 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()
33353369 case "recursive_file_suggest":
33363370 val := value
33373371 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 *bool49 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_suggest55 // RecursiveFileSuggest mirrors the persisted recursive_file_suggest
52 // flag at startup. When true the @-mention picker fuzzy-searches the56 // 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) error282 SetQuickModelShortcut(slot int, providerName, model string) error
279 SetInlineImages(enabled bool) error283 SetInlineImages(enabled bool) error
280 SetAutoSwarm(enabled bool) error284 SetAutoSwarm(enabled bool) error
285+ SetJailByDefault(enabled bool) error
281 SetRecursiveFileSuggest(enabled bool) error286 SetRecursiveFileSuggest(enabled bool) error
282 SetRespectGitignore(enabled bool) error287 SetRespectGitignore(enabled bool) error
283 SetCompactMode(enabled bool) error288 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.RecursiveFileSuggest2922 recursiveFiles := i.cfg.RecursiveFileSuggest != nil && *i.cfg.RecursiveFileSuggest
2917 respectGitignore := i.cfg.RespectGitignore == nil || *i.cfg.RespectGitignore2923 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 := value3370 val := value
3337 i.cfg.RecursiveFileSuggest = &val3371 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 {
5050 return SaveConfig(cfg)
5151 }
5252
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+
5362 func (configSettingsStore) SetRecursiveFileSuggest(enabled bool) error {
5463 cfg, err := LoadConfig()
5564 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+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close