on main help sign in
refactor: rename Permissions.SkipRequests to Overrides().SkipPermissionRequests
commit64f6cdf5602a75e83aeb521af8db3607b63cf6e7 authorAyman Bagabas <ayman.bagabas@gmail.com> dateThu 12 Mar 2026, 14:31 (7 months ago) parent99108f0
9 files changed+218−20
internal/app/app.go+1−1 internal/backend/backend.go+3−6 internal/cmd/root.go+1−4 internal/cmd/server.go+1−1 internal/config/config.go+1−2 internal/config/load.go+9 internal/config/scope.go+18 internal/config/store.go+32−6 internal/config/store_test.go+152
internal/app/app.go+1−1
@@ -81,7 +81,7 @@ func New(ctx context.Context, conn *sql.DB, store *config.ConfigStore) (*App, er
8181 messages := message.NewService(q)
8282 files := history.NewService(q, conn)
8383 cfg := store.Config()
84− skipPermissionsRequests := cfg.Permissions != nil && cfg.Permissions.SkipRequests
84+ skipPermissionsRequests := store.Overrides().SkipPermissionRequests
8585 var allowedTools []string
8686 if cfg.Permissions != nil && cfg.Permissions.AllowedTools != nil {
8787 allowedTools = cfg.Permissions.AllowedTools
internal/backend/backend.go+3−6
@@ -95,10 +95,7 @@ func (b *Backend) CreateWorkspace(args proto.Workspace) (*Workspace, proto.Works
9595 return nil, proto.Workspace{}, fmt.Errorf("failed to initialize config: %w", err)
9696 }
9797
98− if cfg.Config().Permissions == nil {
99− cfg.Config().Permissions = &config.Permissions{}
100− }
101− cfg.Config().Permissions.SkipRequests = args.YOLO
98+ cfg.Overrides().SkipPermissionRequests = args.YOLO
10299
103100 if err := createDotCrushDir(cfg.Config().Options.DataDirectory); err != nil {
104101 return nil, proto.Workspace{}, fmt.Errorf("failed to create data directory: %w", err)
@@ -140,7 +137,7 @@ func (b *Backend) CreateWorkspace(args proto.Workspace) (*Workspace, proto.Works
140137 Path: args.Path,
141138 DataDir: cfg.Config().Options.DataDirectory,
142139 Debug: cfg.Config().Options.Debug,
143− YOLO: cfg.Config().Permissions.SkipRequests,
140+ YOLO: cfg.Overrides().SkipPermissionRequests,
144141 Config: cfg.Config(),
145142 Env: args.Env,
146143 }
@@ -199,7 +196,7 @@ func workspaceToProto(ws *Workspace) proto.Workspace {
199196 return proto.Workspace{
200197 ID: ws.ID,
201198 Path: ws.Path,
202− YOLO: cfg.Permissions != nil && cfg.Permissions.SkipRequests,
199+ YOLO: ws.Cfg.Overrides().SkipPermissionRequests,
203200 DataDir: cfg.Options.DataDirectory,
204201 Debug: cfg.Options.Debug,
205202 Config: cfg,
internal/cmd/root.go+1−4
@@ -240,11 +240,8 @@ func setupApp(cmd *cobra.Command) (*app.App, error) {
240240 return nil, err
241241 }
242242
243+ store.Overrides().SkipPermissionRequests = yolo
243244 cfg := store.Config()
244− if cfg.Permissions == nil {
245− cfg.Permissions = &config.Permissions{}
246− }
247− cfg.Permissions.SkipRequests = yolo
248245
249246 if err := createDotCrushDir(cfg.Options.DataDirectory); err != nil {
250247 return nil, err
internal/cmd/server.go+1−1
@@ -34,7 +34,7 @@ var serverCmd = &cobra.Command{
3434 return fmt.Errorf("failed to get debug flag: %v", err)
3535 }
3636
37− cfg, err := config.Load("", dataDir, debug)
37+ cfg, err := config.Load(config.GlobalWorkspaceDir(), dataDir, debug)
3838 if err != nil {
3939 return fmt.Errorf("failed to load configuration: %v", err)
4040 }
internal/config/config.go+1−2
@@ -212,8 +212,7 @@ func (c Completions) Limits() (depth, items int) {
212212}
213213
214214type Permissions struct {
215− AllowedTools []string `json:"allowed_tools,omitempty" jsonschema:"description=List of tools that don't require permission prompts,example=bash,example=view"` // Tools that don't require permission prompts
216− SkipRequests bool `json:"-"` // Automatically accept all permissions (YOLO mode)
215+ AllowedTools []string `json:"allowed_tools,omitempty" jsonschema:"description=List of tools that don't require permission prompts,example=bash,example=view"`
217216}
218217
219218type TrailerStyle string
internal/config/load.go+9
@@ -792,6 +792,15 @@ func GlobalConfigData() string {
792792 return filepath.Join(home.Dir(), ".local", "share", appName, fmt.Sprintf("%s.json", appName))
793793}
794794
795+// GlobalWorkspaceDir returns the path to the global server workspace
796+// directory. This directory acts as a meta-workspace for the server
797+// process, giving it a real workingDir so that config loading, scoped
798+// writes, and provider resolution behave identically to project
799+// workspaces.
800+func GlobalWorkspaceDir() string {
801+ return filepath.Dir(GlobalConfigData())
802+}
803+
795804func assignIfNil[T any](ptr **T, val T) {
796805 if *ptr == nil {
797806 *ptr = &val
internal/config/scope.go+18
@@ -1,5 +1,7 @@
11package config
22
3+import "fmt"
4+
35// Scope determines which config file is targeted for read/write operations.
46type Scope int
57
@@ -9,3 +11,19 @@ const (
911 // ScopeWorkspace targets the workspace config (.crush/crush.json).
1012 ScopeWorkspace
1113)
14+
15+// String returns a human-readable label for the scope.
16+func (s Scope) String() string {
17+ switch s {
18+ case ScopeGlobal:
19+ return "global"
20+ case ScopeWorkspace:
21+ return "workspace"
22+ default:
23+ return fmt.Sprintf("Scope(%d)", int(s))
24+ }
25+}
26+
27+// ErrNoWorkspaceConfig is returned when a workspace-scoped write is
28+// attempted on a ConfigStore that has no workspace config path.
29+var ErrNoWorkspaceConfig = fmt.Errorf("no workspace config path configured")
internal/config/store.go+32−6
@@ -18,6 +18,13 @@ import (
1818 "github.com/tidwall/sjson"
1919)
2020
21+// RuntimeOverrides holds per-session settings that are never persisted to
22+// disk. They are applied on top of the loaded Config and survive only for
23+// the lifetime of the process (or workspace).
24+type RuntimeOverrides struct {
25+ SkipPermissionRequests bool
26+}
27+
2128// ConfigStore is the single entry point for all config access. It owns the
2229// pure-data Config, runtime state (working directory, resolver, known
2330// providers), and persistence to both global and workspace config files.
@@ -28,6 +35,7 @@ type ConfigStore struct {
2835 globalDataPath string // ~/.local/share/crush/crush.json
2936 workspacePath string // .crush/crush.json
3037 knownProviders []catwalk.Provider
38+ overrides RuntimeOverrides
3139}
3240
3341// Config returns the pure-data config struct (read-only after load).
@@ -63,20 +71,32 @@ func (s *ConfigStore) SetupAgents() {
6371 s.config.SetupAgents()
6472}
6573
74+// Overrides returns the runtime overrides for this store.
75+func (s *ConfigStore) Overrides() *RuntimeOverrides {
76+ return &s.overrides
77+}
78+
6679// configPath returns the file path for the given scope.
67−func (s *ConfigStore) configPath(scope Scope) string {
80+func (s *ConfigStore) configPath(scope Scope) (string, error) {
6881 switch scope {
6982 case ScopeWorkspace:
70− return s.workspacePath
83+ if s.workspacePath == "" {
84+ return "", ErrNoWorkspaceConfig
85+ }
86+ return s.workspacePath, nil
7187 default:
72− return s.globalDataPath
88+ return s.globalDataPath, nil
7389 }
7490}
7591
7692// HasConfigField checks whether a key exists in the config file for the given
7793// scope.
7894func (s *ConfigStore) HasConfigField(scope Scope, key string) bool {
79− data, err := os.ReadFile(s.configPath(scope))
95+ path, err := s.configPath(scope)
96+ if err != nil {
97+ return false
98+ }
99+ data, err := os.ReadFile(path)
80100 if err != nil {
81101 return false
82102 }
@@ -85,7 +105,10 @@ func (s *ConfigStore) HasConfigField(scope Scope, key string) bool {
85105
86106// SetConfigField sets a key/value pair in the config file for the given scope.
87107func (s *ConfigStore) SetConfigField(scope Scope, key string, value any) error {
88− path := s.configPath(scope)
108+ path, err := s.configPath(scope)
109+ if err != nil {
110+ return fmt.Errorf("%s: %w", key, err)
111+ }
89112 data, err := os.ReadFile(path)
90113 if err != nil {
91114 if os.IsNotExist(err) {
@@ -110,7 +133,10 @@ func (s *ConfigStore) SetConfigField(scope Scope, key string, value any) error {
110133
111134// RemoveConfigField removes a key from the config file for the given scope.
112135func (s *ConfigStore) RemoveConfigField(scope Scope, key string) error {
113− path := s.configPath(scope)
136+ path, err := s.configPath(scope)
137+ if err != nil {
138+ return fmt.Errorf("%s: %w", key, err)
139+ }
114140 data, err := os.ReadFile(path)
115141 if err != nil {
116142 return fmt.Errorf("failed to read config file: %w", err)
internal/config/store_test.gonew file+152
@@ -0,0 +1,152 @@
1+package config
2+
3+import (
4+ "errors"
5+ "os"
6+ "path/filepath"
7+ "testing"
8+
9+ "github.com/stretchr/testify/require"
10+)
11+
12+func TestConfigStore_ConfigPath_GlobalAlwaysWorks(t *testing.T) {
13+ t.Parallel()
14+
15+ store := &ConfigStore{
16+ globalDataPath: "/some/global/crush.json",
17+ }
18+
19+ path, err := store.configPath(ScopeGlobal)
20+ require.NoError(t, err)
21+ require.Equal(t, "/some/global/crush.json", path)
22+}
23+
24+func TestConfigStore_ConfigPath_WorkspaceReturnsPath(t *testing.T) {
25+ t.Parallel()
26+
27+ store := &ConfigStore{
28+ workspacePath: "/some/workspace/.crush/crush.json",
29+ }
30+
31+ path, err := store.configPath(ScopeWorkspace)
32+ require.NoError(t, err)
33+ require.Equal(t, "/some/workspace/.crush/crush.json", path)
34+}
35+
36+func TestConfigStore_ConfigPath_WorkspaceErrorsWhenEmpty(t *testing.T) {
37+ t.Parallel()
38+
39+ store := &ConfigStore{
40+ globalDataPath: "/some/global/crush.json",
41+ workspacePath: "",
42+ }
43+
44+ _, err := store.configPath(ScopeWorkspace)
45+ require.Error(t, err)
46+ require.True(t, errors.Is(err, ErrNoWorkspaceConfig))
47+}
48+
49+func TestConfigStore_SetConfigField_WorkspaceScopeGuard(t *testing.T) {
50+ t.Parallel()
51+
52+ store := &ConfigStore{
53+ config: &Config{},
54+ globalDataPath: filepath.Join(t.TempDir(), "global.json"),
55+ workspacePath: "",
56+ }
57+
58+ err := store.SetConfigField(ScopeWorkspace, "foo", "bar")
59+ require.Error(t, err)
60+ require.True(t, errors.Is(err, ErrNoWorkspaceConfig))
61+}
62+
63+func TestConfigStore_SetConfigField_GlobalScopeAlwaysWorks(t *testing.T) {
64+ t.Parallel()
65+
66+ dir := t.TempDir()
67+ globalPath := filepath.Join(dir, "crush.json")
68+ store := &ConfigStore{
69+ config: &Config{},
70+ globalDataPath: globalPath,
71+ }
72+
73+ err := store.SetConfigField(ScopeGlobal, "foo", "bar")
74+ require.NoError(t, err)
75+
76+ data, err := os.ReadFile(globalPath)
77+ require.NoError(t, err)
78+ require.Contains(t, string(data), `"foo"`)
79+}
80+
81+func TestConfigStore_RemoveConfigField_WorkspaceScopeGuard(t *testing.T) {
82+ t.Parallel()
83+
84+ store := &ConfigStore{
85+ config: &Config{},
86+ globalDataPath: filepath.Join(t.TempDir(), "global.json"),
87+ workspacePath: "",
88+ }
89+
90+ err := store.RemoveConfigField(ScopeWorkspace, "foo")
91+ require.Error(t, err)
92+ require.True(t, errors.Is(err, ErrNoWorkspaceConfig))
93+}
94+
95+func TestConfigStore_HasConfigField_WorkspaceScopeGuard(t *testing.T) {
96+ t.Parallel()
97+
98+ store := &ConfigStore{
99+ config: &Config{},
100+ globalDataPath: filepath.Join(t.TempDir(), "global.json"),
101+ workspacePath: "",
102+ }
103+
104+ has := store.HasConfigField(ScopeWorkspace, "foo")
105+ require.False(t, has)
106+}
107+
108+func TestConfigStore_RuntimeOverrides_Independent(t *testing.T) {
109+ t.Parallel()
110+
111+ store1 := &ConfigStore{config: &Config{}}
112+ store2 := &ConfigStore{config: &Config{}}
113+
114+ require.False(t, store1.Overrides().SkipPermissionRequests)
115+ require.False(t, store2.Overrides().SkipPermissionRequests)
116+
117+ store1.Overrides().SkipPermissionRequests = true
118+
119+ require.True(t, store1.Overrides().SkipPermissionRequests)
120+ require.False(t, store2.Overrides().SkipPermissionRequests)
121+}
122+
123+func TestConfigStore_RuntimeOverrides_MutableViaPointer(t *testing.T) {
124+ t.Parallel()
125+
126+ store := &ConfigStore{config: &Config{}}
127+ overrides := store.Overrides()
128+
129+ require.False(t, overrides.SkipPermissionRequests)
130+
131+ overrides.SkipPermissionRequests = true
132+ require.True(t, store.Overrides().SkipPermissionRequests)
133+}
134+
135+func TestGlobalWorkspaceDir(t *testing.T) {
136+ dir := t.TempDir()
137+ t.Setenv("CRUSH_GLOBAL_DATA", dir)
138+
139+ wsDir := GlobalWorkspaceDir()
140+ globalData := GlobalConfigData()
141+
142+ require.Equal(t, filepath.Dir(globalData), wsDir)
143+ require.Equal(t, dir, wsDir)
144+}
145+
146+func TestScope_String(t *testing.T) {
147+ t.Parallel()
148+
149+ require.Equal(t, "global", ScopeGlobal.String())
150+ require.Equal(t, "workspace", ScopeWorkspace.String())
151+ require.Contains(t, Scope(99).String(), "Scope(99)")
152+}