返回 DeepSeek-Reasonix
permission_runtime.go
根目录 / internal / control / permission_runtime.go
1 package control
2
3 import (
4 "fmt"
5 "runtime"
6 "slices"
7 "strings"
8
9 "reasonix/internal/agent"
10 "reasonix/internal/permissionpreset"
11 "reasonix/internal/sandbox"
12 )
13
14 // SessionGrantSummary is a transport-safe description of an in-memory grant.
15 // Grants remain session-local and are never converted to persistent rules.
16 type SessionGrantSummary struct {
17 Scope string `json:"scope"`
18 Target string `json:"target"`
19 }
20
21 type PermissionCapabilities struct {
22 Backend string `json:"backend"`
23 Enforcement string `json:"enforcement"`
24 SupportedPresets []string `json:"supportedPresets"`
25 UnavailableReason string `json:"unavailableReason,omitempty"`
26 WriteIsolation string `json:"writeIsolation,omitempty"`
27 ReadIsolation string `json:"readIsolation,omitempty"`
28 NetworkIsolation string `json:"networkIsolation,omitempty"`
29 }
30
31 // PermissionSnapshot is the sole user-facing permission state for a session.
32 type PermissionSnapshot struct {
33 SessionID string `json:"sessionId"`
34 Generation uint64 `json:"generation"`
35 Revision uint64 `json:"revision"`
36 Preset string `json:"preset"`
37 WorkspaceRoot string `json:"workspaceRoot"`
38 Grants []SessionGrantSummary `json:"grants"`
39 Capabilities PermissionCapabilities `json:"capabilities"`
40 }
41
42 func platformPermissionCapabilities() PermissionCapabilities {
43 return permissionCapabilitiesForPlatform(runtime.GOOS, sandbox.Available(), sandbox.UnavailableMessage())
44 }
45
46 func permissionCapabilitiesForPlatform(goos string, available bool, unavailableReason string) PermissionCapabilities {
47 backend := "none"
48 writeIsolation := ""
49 readIsolation := ""
50 networkIsolation := ""
51 switch goos {
52 case "darwin":
53 backend = "seatbelt"
54 writeIsolation = "seatbelt-filesystem"
55 readIsolation = "seatbelt-filesystem"
56 networkIsolation = "seatbelt-network"
57 case "linux":
58 backend = "bubblewrap"
59 writeIsolation = "bubblewrap-mount-namespace"
60 readIsolation = "bubblewrap-mount-namespace"
61 networkIsolation = "bubblewrap-network-namespace"
62 case "windows":
63 // No OS backend, but the presets still apply as tool-layer boundaries
64 // (file writers, approval prompts), so every preset stays selectable.
65 return PermissionCapabilities{
66 Backend: "none", Enforcement: "unavailable",
67 SupportedPresets: []string{string(permissionpreset.ReadOnly), string(permissionpreset.WorkspaceWrite), string(permissionpreset.DangerFullAccess)},
68 UnavailableReason: unavailableReason,
69 }
70 }
71 if available {
72 return PermissionCapabilities{
73 Backend: backend, Enforcement: "full",
74 SupportedPresets: []string{string(permissionpreset.ReadOnly), string(permissionpreset.WorkspaceWrite), string(permissionpreset.DangerFullAccess)},
75 WriteIsolation: writeIsolation, ReadIsolation: readIsolation, NetworkIsolation: networkIsolation,
76 }
77 }
78 return PermissionCapabilities{
79 Backend: backend, Enforcement: "unavailable",
80 SupportedPresets: []string{string(permissionpreset.DangerFullAccess)},
81 UnavailableReason: unavailableReason,
82 }
83 }
84
85 func (c *Controller) PermissionSnapshot() PermissionSnapshot {
86 c.permissionStateMu.RLock()
87 defer c.permissionStateMu.RUnlock()
88 auth := c.SessionAuthorizations()
89 grants := make([]SessionGrantSummary, 0, len(auth.Grants)+len(auth.WriteRoots)+len(auth.PlanModeReadOnlyCommands))
90 for _, target := range auth.Grants {
91 grants = append(grants, SessionGrantSummary{Scope: "tool", Target: target})
92 }
93 for _, target := range auth.WriteRoots {
94 grants = append(grants, SessionGrantSummary{Scope: "directory", Target: target})
95 }
96 for _, target := range auth.PlanModeReadOnlyCommands {
97 grants = append(grants, SessionGrantSummary{Scope: "command-prefix", Target: target})
98 }
99 return PermissionSnapshot{
100 SessionID: agent.BranchID(c.SessionPath()), Generation: c.runtimeGeneration,
101 Revision: c.permissionRevision.Load(), Preset: c.ToolApprovalMode(),
102 WorkspaceRoot: strings.TrimSpace(c.workspaceRoot), Grants: grants,
103 Capabilities: platformPermissionCapabilities(),
104 }
105 }
106
107 // RestoreSessionAuthorizations re-applies grants captured from a prior
108 // controller when the same logical session is rebuilt.
109 func (c *Controller) RestoreSessionAuthorizations(auth SessionAuthorizations) {
110 c.permissionStateMu.Lock()
111 defer c.permissionStateMu.Unlock()
112 c.approval.restoreSessionAuthorizations(auth)
113 if c.writeAccess.roots != nil && len(auth.WriteRoots) > 0 {
114 c.writeAccess.roots.GrantVerifiedSession(auth.WriteRoots)
115 }
116 }
117
118 // SetPermissionPreset applies a compare-and-set update. A stale UI or remote
119 // reply cannot mutate a newer permission generation.
120 func (c *Controller) SetPermissionPreset(preset string, expectedRevision uint64) (PermissionSnapshot, []string, error) {
121 c.permissionMu.Lock()
122 defer c.permissionMu.Unlock()
123 current := c.permissionRevision.Load()
124 if expectedRevision != current {
125 return c.PermissionSnapshot(), nil, fmt.Errorf("permission revision changed: have %d, expected %d", current, expectedRevision)
126 }
127 raw := strings.ToLower(strings.TrimSpace(preset))
128 if !permissionpreset.Valid(raw) {
129 return c.PermissionSnapshot(), nil, fmt.Errorf("permission preset must be read-only, workspace-write, or danger-full-access")
130 }
131 capabilities := platformPermissionCapabilities()
132 if !slices.Contains(capabilities.SupportedPresets, raw) {
133 return c.PermissionSnapshot(), nil, fmt.Errorf("permission preset %q is unavailable: %s", raw, capabilities.UnavailableReason)
134 }
135 drained := c.applyToolApprovalModeLocked(raw)
136 return c.PermissionSnapshot(), drained, nil
137 }
138
139 // RevokeSessionGrant removes one exact in-memory authorization. Revocation is
140 // compare-and-set protected; the new revision is published atomically with the
141 // removal before in-flight work and background processes are stopped.
142 func (c *Controller) RevokeSessionGrant(scope, target string, expectedRevision uint64) (PermissionSnapshot, error) {
143 if c == nil {
144 return PermissionSnapshot{}, fmt.Errorf("controller is nil")
145 }
146 c.permissionMu.Lock()
147 defer c.permissionMu.Unlock()
148 current := c.permissionRevision.Load()
149 if expectedRevision != current {
150 return c.PermissionSnapshot(), fmt.Errorf("permission revision changed: have %d, expected %d", current, expectedRevision)
151 }
152 c.promptResolveMu.Lock()
153 c.permissionStateMu.Lock()
154 removed := false
155 switch strings.TrimSpace(scope) {
156 case "directory":
157 if c.writeAccess.roots != nil {
158 removed = c.writeAccess.roots.RevokeSession(target)
159 }
160 case "tool", "command-prefix":
161 removed = c.approval.revokeSessionAuthorization(scope, target)
162 default:
163 c.permissionStateMu.Unlock()
164 c.promptResolveMu.Unlock()
165 return c.PermissionSnapshot(), fmt.Errorf("unknown session grant scope %q", scope)
166 }
167 if !removed {
168 c.permissionStateMu.Unlock()
169 c.promptResolveMu.Unlock()
170 return c.PermissionSnapshot(), fmt.Errorf("session grant was not found")
171 }
172 c.permissionRevision.Add(1)
173 c.permissionStateMu.Unlock()
174 turnID, cancelled := "", false
175 if c.Running() {
176 turnID, cancelled = c.cancelTurnLocked()
177 }
178 c.promptResolveMu.Unlock()
179 if cancelled {
180 c.finishCancel(turnID, true)
181 }
182 for _, job := range c.Jobs() {
183 c.CancelJob(job.ID)
184 }
185 return c.PermissionSnapshot(), nil
186 }
187
187 lines GO