Repository navigation
Expand file tree
/
Copy pathchatv3_approval.go
More file actions
261 lines (246 loc) · 11.4 KB
/
Copy pathchatv3_approval.go
File metadata and controls
261 lines (246 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
package main
import (
"github.com/Agent-Field/codeaf/internal/approval"
"github.com/Agent-Field/codeaf/internal/config"
"github.com/Agent-Field/codeaf/internal/session"
)
// v3Gate is the one method of a running session these seams reach for: the door
// onto the gate a conversation is behind right now
// ([session.Agent.SetApprovalPolicy]).
//
// IT IS AN INTERFACE SO THE BINDING CAN BE READ. Which agent a banked rule
// reaches is the whole of what went wrong here — the trio was minted once around
// the boot agent and went on pushing into it after /new had closed it — and
// internal/session keeps the standing gate private, as it should. Narrowing the
// parameter to the method that is actually used lets a test hold the receiving
// end and say which conversation was written to, without any of internal/session
// growing a reader for the sake of a test.
type v3Gate interface {
SetApprovalPolicy(policy *approval.Policy)
}
// v3Rebuilder is the newer door on the same agent: a session that knows its
// own posture rebuilds the gate FOR that posture (internal/session's
// approvalposture.go), so a rule banked inside a conversation walked to allow
// lands on an allow gate. [refreshV3Policy] and [applyV3Approvals] prefer it
// and keep the plain push for a gate that does not have it.
type v3Rebuilder interface {
ApprovalDial() bool
RebuildApprovalGate() error
}
// v3Rebuilt asks the agent to rebuild its own gate where it can, and reports
// whether it could. AN AGENT WITH THE METHODS AND NO DOOR IS NOT REBUILT HERE:
// a session assembled without [session.Config.ApprovalGate] — a test's, a
// worker's — refuses the rebuild, and a refusal that silenced the plain push
// left a banked rule reaching nobody's gate (the person was asked twice for
// one command). So the door is asked first, and only a session that has one
// takes this road.
func v3Rebuilt(agent v3Gate) (bool, error) {
rebuilder, ok := agent.(v3Rebuilder)
if !ok || !rebuilder.ApprovalDial() {
return false, nil
}
return true, rebuilder.RebuildApprovalGate()
}
// v3ApprovalGate is the door a conversation moves its own gate through
// ([session.ApprovalGate]): the same three rows, the same project layer, the
// same floors as the launch, asked for by posture instead of by flag.
//
// THE POSTURE IS A WORD FOR A PAIR. The ladder folds the guardian row into the
// mode (internal/session's approvalposture.go says why), so `ask` is prompt
// with the guardian stood down, `guardian` is prompt with it up, and `allow`
// and `deny` are the row's own words with the guardian irrelevant. An empty
// posture is the rows exactly as they stand — which is what an untouched
// conversation runs at and what `auto` hands back to.
type v3ApprovalGate struct {
workspace, profileDir string
headless bool
}
func (g v3ApprovalGate) Build(posture string) (*approval.Policy, bool, error) {
mode := ""
guardian := config.GuardianEnabledAt(g.profileDir)
switch posture {
case session.PostureAsk:
mode, guardian = string(approval.ActionPrompt), false
case session.PostureGuardian:
mode, guardian = string(approval.ActionPrompt), true
case session.PostureAllow:
mode, guardian = string(approval.ActionAllow), false
case session.PostureDeny:
mode, guardian = string(approval.ActionDeny), false
}
if mode == "" && g.headless {
var err error
mode, err = config.HeadlessToolApprovalModeAt(g.workspace, g.profileDir)
if err != nil {
return nil, false, err
}
}
policy, err := v3PolicyMode(g.workspace, g.profileDir, mode)
if err != nil {
return nil, false, err
}
return policy, guardian, nil
}
// Standing is the rows as they stand, said in the ladder's words. An unreadable
// mode row reads as the strictest answer it could be, which is internal/config's
// own law for that row and the one direction a garbled setting may be wrong in.
func (g v3ApprovalGate) Standing() string {
mode, err := config.ProjectStringAt(g.workspace, g.profileDir, config.KeyToolApprovalMode)
if g.headless {
mode, err = config.HeadlessToolApprovalModeAt(g.workspace, g.profileDir)
}
if err != nil {
mode = string(approval.ActionPrompt)
}
switch mode {
case string(approval.ActionAllow):
return session.PostureAllow
case string(approval.ActionDeny):
return session.PostureDeny
}
if config.GuardianEnabledAt(g.profileDir) {
return session.PostureGuardian
}
return session.PostureAsk
}
// The consent card's door back to disk: where "always" is written down.
//
// It is a PAIR — one seam for a tool, one for a shell command — handed to the
// surface together, because they answer the same question against the two rows
// that can answer it: tools.approval names a tool, tools.bashPatterns names a
// command line, and the card knows which of the two it is looking at.
//
// It lives in its own file beside chatv3.go for chatv2_rail.go's stated reason:
// the seam that lets the v3 surface reach a registry row is a different thing
// from the window's assembly, and keeping them apart means a wave editing one
// does not collide with a wave editing the other.
//
// THE PROFILE IS THE PLACE. A repository's .codeaf/config.json is never written
// from here — it is a file a team commits, and a keystroke on a consent card
// must not commit to somebody's repository. internal/config's approvalmemory.go
// states the honest consequence: while a repository answers one of these rows,
// its answer replaces the person's WHOLE at launch, so what is written here
// takes effect everywhere except inside that repository.
// saveToolApproval remembers one tool's allow in the person's own profile.
//
// The error is returned rather than dropped here, which is the ONE difference
// from saveRail's shape (chatv2_rail.go). The rail's caller has nothing left to
// say once the sidebar has moved; this one's caller is about to print a receipt
// claiming the answer was saved, so it has to be told whether that is true. The
// failure is still dropped — the surface drops it, silently, and keeps the
// session-scoped always it always had.
func saveToolApproval(profileDir, tool string) error {
return config.RememberToolApproval(profileDir, tool, "allow")
}
// saveBashApproval remembers one whole command line as an allow rule.
//
// It is never a deny, and neither is the seam above: the card only ever persists
// a yes (internal/tui3's consent.go says why), and a standing never is a line a
// person writes in the settings sheet on purpose.
func saveBashApproval(profileDir, command string) error {
return config.RememberBashApproval(profileDir, command)
}
// bankToolApproval and bankBashApproval are the two seams the surface actually
// holds: write the rule down, then hand the running session the gate that rule
// is part of.
//
// THE SECOND HALF IS WHAT MAKES THE FIRST HALF MEAN ANYTHING TODAY. Without it,
// pressing always saved a preference that the running gate could not see, and
// the only thing stopping the next question was internal/session's
// session-scoped memo — which covers this agent's remaining life and nothing
// beyond it. The rebuild is the same v3Policy the launch ran, over the same
// three rows through the same project layer, so a banked rule reaches the gate
// by exactly the road it would have taken on the next launch, minus the launch.
//
// THE WRITE'S ERROR IS THE ONE THAT TRAVELS. The surface is about to say
// "saved" and it must be told whether that is true (internal/tui3's Options
// says why), and what is true is whether the row reached the disk. A rebuild
// that then fails has not made the write a lie: the rule IS saved and the next
// session will read it, so the error is dropped and the gate already standing
// stays standing. That case needs a settings file to have stopped parsing
// between the write and the read a moment later, which the launch's own read of
// the same rows would already have refused to start on.
func bankToolApproval(agent v3Gate, workspace, profileDir string, yolo bool) func(string) error {
return func(tool string) error {
if err := saveToolApproval(profileDir, tool); err != nil {
return err
}
refreshV3Policy(agent, workspace, profileDir, yolo)
return nil
}
}
func bankBashApproval(agent v3Gate, workspace, profileDir string, yolo bool) func(string) error {
return func(command string) error {
if err := saveBashApproval(profileDir, command); err != nil {
return err
}
refreshV3Policy(agent, workspace, profileDir, yolo)
return nil
}
}
// applyV3Approvals is the permissions panel's live seam: a line taken back
// there is already off the disk by the time this runs, and this is what makes
// the gate this conversation is running on agree.
//
// THE REBUILD'S ERROR IS THE ONE THAT TRAVELS HERE, which is the opposite of
// the banking pair above and for the same reason stated the other way round.
// There the write was the claim and the rebuild was a bonus; here the drop has
// already been written and the only open question is whether the running gate
// heard about it. A rebuild that fails means the rule is gone from the file and
// still standing in this session, and the panel says exactly that — the receipt
// names the next session rather than claiming the line is already gone.
func applyV3Approvals(agent v3Gate, workspace, profileDir string, yolo bool) func() error {
return func() error {
if rebuilt, err := v3Rebuilt(agent); rebuilt {
return err
}
policy, err := v3Policy(workspace, profileDir, yolo)
if err != nil {
return err
}
agent.SetApprovalPolicy(policy)
return nil
}
}
// v3CurrentGate copies a launch config and swaps in the gate as it stands right
// now. It is what /new and /resume open a second conversation on.
//
// THE SAME COMPLAINT, ONE DOOR OVER. cfg is the config this window launched on
// and it carries the policy that launch built, so an agent opened after somebody
// banked a rule would start behind the gate that rule was written to change —
// /new would quietly resurrect exactly the asking the keystroke had just
// stopped, and the person would have no way to connect the two.
//
// THE ROWS ARE RE-READ HERE RATHER THAN PUSHED, which is the whole reason this
// needs no lock: the read happens inside one closure on one keystroke and writes
// nothing that anything else can see. A pushed policy belongs to a session that
// already exists; this one is a config being assembled for a session that does
// not exist yet, and assembling it is the launch's own job done again.
//
// A REBUILD THAT FAILS KEEPS THE LAUNCH'S GATE, for the reason
// [session.Agent.SetApprovalPolicy] refuses a nil: the answer to "I could not
// read the rules" is never a session with no rules. The new conversation then
// opens exactly as it did before this function existed.
func v3CurrentGate(cfg session.Config, workspace, profileDir string, yolo bool) session.Config {
if policy, err := v3Policy(workspace, profileDir, yolo); err == nil {
cfg.ApprovalPolicy = policy
}
return cfg
}
// refreshV3Policy rebuilds the gate from the rows as they stand and pushes it
// into the running session. A rebuild that fails pushes nothing, and
// [session.Agent.SetApprovalPolicy] refuses a nil for the same reason: the safe
// answer to "I could not read the rules" is the gate that is already there.
func refreshV3Policy(agent v3Gate, workspace, profileDir string, yolo bool) {
if agent == nil {
return
}
if rebuilt, _ := v3Rebuilt(agent); rebuilt {
return
}
policy, err := v3Policy(workspace, profileDir, yolo)
if err != nil {
return
}
agent.SetApprovalPolicy(policy)
}