sink_permissions.go

 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}