Skip to content

Commit 733673b

Browse files
committed
feat: add global-level permissions from user config
Allow users to define permission patterns in ~/.config/cagent/config.yaml that apply across all sessions and agents as user-wide defaults. The permission cascade is now: YOLO > Session > Team > Global > ReadOnlyHint > Ask.
1 parent 9b76c88 commit 733673b

File tree

6 files changed

+448
-16
lines changed

6 files changed

+448
-16
lines changed

cmd/root/run.go

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/docker/docker-agent/pkg/cli"
2121
"github.com/docker/docker-agent/pkg/config"
2222
"github.com/docker/docker-agent/pkg/paths"
23+
"github.com/docker/docker-agent/pkg/permissions"
2324
"github.com/docker/docker-agent/pkg/profiling"
2425
"github.com/docker/docker-agent/pkg/runtime"
2526
"github.com/docker/docker-agent/pkg/session"
@@ -59,6 +60,10 @@ type runExecFlags struct {
5960

6061
// Run only
6162
hideToolResults bool
63+
64+
// globalPermsOpt is a runtime option that injects user-level global permissions.
65+
// Nil when no global permissions are configured.
66+
globalPermsOpt runtime.Opt
6267
}
6368

6469
func newRunCmd() *cobra.Command {
@@ -187,6 +192,11 @@ func (f *runExecFlags) runOrExec(ctx context.Context, out *cli.Printer, args []s
187192
}
188193
}
189194

195+
// Build global permissions checker from user config settings.
196+
if userSettings.Permissions != nil {
197+
f.globalPermsOpt = runtime.WithGlobalPermissions(permissions.NewChecker(userSettings.Permissions))
198+
}
199+
190200
// Start fake proxy if --fake is specified
191201
fakeCleanup, err := setupFakeProxy(f.fakeResponses, f.fakeStreamDelay, &f.runConfig)
192202
if err != nil {
@@ -333,12 +343,17 @@ func (f *runExecFlags) createLocalRuntimeAndSession(ctx context.Context, loadRes
333343
AgentDefaultModels: loadResult.AgentDefaultModels,
334344
}
335345

336-
localRt, err := runtime.New(t,
346+
rtOpts := []runtime.Opt{
337347
runtime.WithSessionStore(sessStore),
338348
runtime.WithCurrentAgent(f.agentName),
339349
runtime.WithTracer(otel.Tracer(AppName)),
340350
runtime.WithModelSwitcherConfig(modelSwitcherCfg),
341-
)
351+
}
352+
if f.globalPermsOpt != nil {
353+
rtOpts = append(rtOpts, f.globalPermsOpt)
354+
}
355+
356+
localRt, err := runtime.New(t, rtOpts...)
342357
if err != nil {
343358
return nil, nil, fmt.Errorf("creating runtime: %w", err)
344359
}
@@ -506,12 +521,17 @@ func (f *runExecFlags) createSessionSpawner(agentSource config.Source, sessStore
506521
}
507522

508523
// Create the local runtime
509-
localRt, err := runtime.New(team,
524+
rtOpts := []runtime.Opt{
510525
runtime.WithSessionStore(sessStore),
511526
runtime.WithCurrentAgent(f.agentName),
512527
runtime.WithTracer(otel.Tracer(AppName)),
513528
runtime.WithModelSwitcherConfig(modelSwitcherCfg),
514-
)
529+
}
530+
if f.globalPermsOpt != nil {
531+
rtOpts = append(rtOpts, f.globalPermsOpt)
532+
}
533+
534+
localRt, err := runtime.New(team, rtOpts...)
515535
if err != nil {
516536
return nil, nil, nil, err
517537
}

pkg/runtime/runtime.go

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/docker/docker-agent/pkg/config/types"
1818
"github.com/docker/docker-agent/pkg/hooks"
1919
"github.com/docker/docker-agent/pkg/modelsdev"
20+
"github.com/docker/docker-agent/pkg/permissions"
2021
"github.com/docker/docker-agent/pkg/session"
2122
"github.com/docker/docker-agent/pkg/sessiontitle"
2223
"github.com/docker/docker-agent/pkg/team"
@@ -117,7 +118,7 @@ type Runtime interface {
117118
// Summarize generates a summary for the session
118119
Summarize(ctx context.Context, sess *session.Session, additionalPrompt string, events chan Event)
119120

120-
// PermissionsInfo returns the team-level permission patterns (allow/ask/deny).
121+
// PermissionsInfo returns the team-level and global permission patterns (allow/ask/deny).
121122
// Returns nil if no permissions are configured.
122123
PermissionsInfo() *PermissionsInfo
123124

@@ -188,6 +189,11 @@ type LocalRuntime struct {
188189
env []string // Environment variables for hooks execution
189190
modelSwitcherCfg *ModelSwitcherConfig
190191

192+
// globalPermissions holds user-level permission patterns loaded from the
193+
// user config (~/.config/cagent/config.yaml). These are evaluated after
194+
// session and team permissions in the approval cascade.
195+
globalPermissions *permissions.Checker
196+
191197
// retryOnRateLimit enables retry-with-backoff for HTTP 429 (rate limit) errors
192198
// when no fallback models are configured. When false (default), 429 errors are
193199
// treated as non-retryable and immediately fail or skip to the next model.
@@ -259,6 +265,15 @@ func WithEnv(env []string) Opt {
259265
}
260266
}
261267

268+
// WithGlobalPermissions sets user-level permission patterns loaded from the
269+
// user config (~/.config/cagent/config.yaml). These are evaluated after session
270+
// and team permissions in the approval cascade, acting as user-wide defaults.
271+
func WithGlobalPermissions(checker *permissions.Checker) Opt {
272+
return func(r *LocalRuntime) {
273+
r.globalPermissions = checker
274+
}
275+
}
276+
262277
// WithRetryOnRateLimit enables automatic retry with backoff for HTTP 429 (rate limit)
263278
// errors when no fallback models are available. When enabled, the runtime will honor
264279
// the Retry-After header from the provider's response to determine wait time before
@@ -759,18 +774,29 @@ func (r *LocalRuntime) UpdateSessionTitle(ctx context.Context, sess *session.Ses
759774
return nil
760775
}
761776

762-
// PermissionsInfo returns the team-level permission patterns.
763-
// Returns nil if no permissions are configured.
777+
// PermissionsInfo returns the merged team-level and global permission patterns.
778+
// Returns nil if no permissions are configured at either level.
764779
func (r *LocalRuntime) PermissionsInfo() *PermissionsInfo {
765-
permChecker := r.team.Permissions()
766-
if permChecker == nil || permChecker.IsEmpty() {
780+
teamChecker := r.team.Permissions()
781+
teamEmpty := teamChecker == nil || teamChecker.IsEmpty()
782+
globalEmpty := r.globalPermissions == nil || r.globalPermissions.IsEmpty()
783+
784+
if teamEmpty && globalEmpty {
767785
return nil
768786
}
769-
return &PermissionsInfo{
770-
Allow: permChecker.AllowPatterns(),
771-
Ask: permChecker.AskPatterns(),
772-
Deny: permChecker.DenyPatterns(),
787+
788+
result := &PermissionsInfo{}
789+
if !teamEmpty {
790+
result.Allow = append(result.Allow, teamChecker.AllowPatterns()...)
791+
result.Ask = append(result.Ask, teamChecker.AskPatterns()...)
792+
result.Deny = append(result.Deny, teamChecker.DenyPatterns()...)
793+
}
794+
if !globalEmpty {
795+
result.Allow = append(result.Allow, r.globalPermissions.AllowPatterns()...)
796+
result.Ask = append(result.Ask, r.globalPermissions.AskPatterns()...)
797+
result.Deny = append(result.Deny, r.globalPermissions.DenyPatterns()...)
773798
}
799+
return result
774800
}
775801

776802
// ResetStartupInfo resets the startup info emission flag.

0 commit comments

Comments
 (0)