1package acp
2
3import (
4 "encoding/json"
5 "log/slog"
6
7 "github.com/charmbracelet/crush/internal/permission"
8 "github.com/coder/acp-go-sdk"
9)
10
11// HandlePermission translates a permission request to an ACP permission request.
12func (s *Sink) HandlePermission(req permission.PermissionRequest, permissions permission.Service) {
13 // Only handle permissions for our session.
14 if req.SessionID != s.sessionID {
15 return
16 }
17
18 slog.Debug("ACP permission request", "tool", req.ToolName, "action", req.Action)
19
20 // Build the tool call for the permission request.
21 toolCall := acp.RequestPermissionToolCall{
22 ToolCallId: acp.ToolCallId(req.ToolCallID),
23 Title: acp.Ptr(req.Description),
24 Kind: acp.Ptr(acp.ToolKindEdit),
25 Status: acp.Ptr(acp.ToolCallStatusPending),
26 Locations: []acp.ToolCallLocation{{Path: req.Path}},
27 RawInput: req.Params,
28 }
29
30 // For edit tools, include diff content so the client can show the proposed
31 // changes.
32 if meta := extractEditParams(req.Params); meta != nil && meta.FilePath != "" {
33 toolCall.Content = []acp.ToolCallContent{
34 acp.ToolDiffContent(meta.FilePath, meta.NewContent, meta.OldContent),
35 }
36 }
37
38 resp, err := s.conn.RequestPermission(s.ctx, acp.RequestPermissionRequest{
39 SessionId: acp.SessionId(s.sessionID),
40 ToolCall: toolCall,
41 Options: []acp.PermissionOption{
42 {Kind: acp.PermissionOptionKindAllowOnce, Name: "Allow", OptionId: "allow"},
43 {Kind: acp.PermissionOptionKindAllowAlways, Name: "Allow always", OptionId: "allow_always"},
44 {Kind: acp.PermissionOptionKindRejectOnce, Name: "Deny", OptionId: "deny"},
45 },
46 })
47 if err != nil {
48 slog.Error("Failed to request permission", "error", err)
49 permissions.Deny(req)
50 return
51 }
52
53 if resp.Outcome.Cancelled != nil {
54 permissions.Deny(req)
55 return
56 }
57
58 if resp.Outcome.Selected != nil {
59 switch string(resp.Outcome.Selected.OptionId) {
60 case "allow":
61 permissions.Grant(req)
62 case "allow_always":
63 permissions.GrantPersistent(req)
64 default:
65 permissions.Deny(req)
66 }
67 }
68}
69
70// editParams holds fields needed for diff content in permission requests.
71type editParams struct {
72 FilePath string `json:"file_path"`
73 OldContent string `json:"old_content"`
74 NewContent string `json:"new_content"`
75}
76
77// extractEditParams attempts to extract edit parameters from permission params.
78func extractEditParams(params any) *editParams {
79 if params == nil {
80 return nil
81 }
82
83 // Try JSON round-trip to extract fields.
84 data, err := json.Marshal(params)
85 if err != nil {
86 return nil
87 }
88
89 var ep editParams
90 if err := json.Unmarshal(data, &ep); err != nil {
91 return nil
92 }
93
94 return &ep
95}