diff --git a/internal/proto/permission.go b/internal/proto/permission.go index 5834de628e41a290d0bc391fbe3ead2505eb742a..981f1bc3bcda1162ccdeaf44e43c143f528e61e5 100644 --- a/internal/proto/permission.go +++ b/internal/proto/permission.go @@ -118,6 +118,12 @@ func unmarshalToolParams(toolName string, raw json.RawMessage) (any, error) { return nil, err } return params, nil + case AgenticFetchToolName: + var params AgenticFetchPermissionsParams + if err := json.Unmarshal(raw, ¶ms); err != nil { + return nil, err + } + return params, nil case ViewToolName: var params ViewPermissionsParams if err := json.Unmarshal(raw, ¶ms); err != nil { diff --git a/internal/proto/permission_test.go b/internal/proto/permission_test.go new file mode 100644 index 0000000000000000000000000000000000000000..5b0e9d7f43fa287dc6d589663d177a290296585f --- /dev/null +++ b/internal/proto/permission_test.go @@ -0,0 +1,186 @@ +package proto_test + +import ( + "encoding/json" + "testing" + + "github.com/charmbracelet/crush/internal/agent/tools" + "github.com/charmbracelet/crush/internal/proto" + "github.com/stretchr/testify/require" +) + +// TestPermissionRequestParamsTypeAssertable guards the permission +// dialog's type assertions across the client/server boundary. The TUI +// asserts PermissionRequest.Params to tools.*PermissionsParams; when +// the request round-trips over the SSE wire (server → client), the +// decoded value must be the same Go type, otherwise the dialog +// renders empty content. +func TestPermissionRequestParamsTypeAssertable(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + toolName string + params any + assert func(t *testing.T, got any) + }{ + { + name: "bash", + toolName: tools.BashToolName, + params: tools.BashPermissionsParams{ + Description: "list files", + Command: "ls -la", + WorkingDir: "/tmp", + RunInBackground: false, + }, + assert: func(t *testing.T, got any) { + v, ok := got.(tools.BashPermissionsParams) + require.True(t, ok, "params must decode as tools.BashPermissionsParams, got %T", got) + require.Equal(t, "list files", v.Description) + require.Equal(t, "ls -la", v.Command) + require.Equal(t, "/tmp", v.WorkingDir) + }, + }, + { + name: "edit", + toolName: tools.EditToolName, + params: tools.EditPermissionsParams{ + FilePath: "/tmp/x.go", + OldContent: "old", + NewContent: "new", + }, + assert: func(t *testing.T, got any) { + v, ok := got.(tools.EditPermissionsParams) + require.True(t, ok, "params must decode as tools.EditPermissionsParams, got %T", got) + require.Equal(t, "/tmp/x.go", v.FilePath) + require.Equal(t, "old", v.OldContent) + require.Equal(t, "new", v.NewContent) + }, + }, + { + name: "write", + toolName: tools.WriteToolName, + params: tools.WritePermissionsParams{ + FilePath: "/tmp/x.go", + NewContent: "new", + }, + assert: func(t *testing.T, got any) { + v, ok := got.(tools.WritePermissionsParams) + require.True(t, ok, "params must decode as tools.WritePermissionsParams, got %T", got) + require.Equal(t, "/tmp/x.go", v.FilePath) + require.Equal(t, "new", v.NewContent) + }, + }, + { + name: "multiedit", + toolName: tools.MultiEditToolName, + params: tools.MultiEditPermissionsParams{ + FilePath: "/tmp/x.go", + OldContent: "old", + NewContent: "new", + }, + assert: func(t *testing.T, got any) { + v, ok := got.(tools.MultiEditPermissionsParams) + require.True(t, ok, "params must decode as tools.MultiEditPermissionsParams, got %T", got) + require.Equal(t, "/tmp/x.go", v.FilePath) + }, + }, + { + name: "ls", + toolName: tools.LSToolName, + params: tools.LSPermissionsParams{ + Path: "/tmp", + Ignore: []string{".git"}, + Depth: 2, + }, + assert: func(t *testing.T, got any) { + v, ok := got.(tools.LSPermissionsParams) + require.True(t, ok, "params must decode as tools.LSPermissionsParams, got %T", got) + require.Equal(t, "/tmp", v.Path) + require.Equal(t, []string{".git"}, v.Ignore) + require.Equal(t, 2, v.Depth) + }, + }, + { + name: "view", + toolName: tools.ViewToolName, + params: tools.ViewPermissionsParams{ + FilePath: "/tmp/x.go", + Offset: 10, + Limit: 100, + }, + assert: func(t *testing.T, got any) { + v, ok := got.(tools.ViewPermissionsParams) + require.True(t, ok, "params must decode as tools.ViewPermissionsParams, got %T", got) + require.Equal(t, "/tmp/x.go", v.FilePath) + }, + }, + { + name: "fetch", + toolName: tools.FetchToolName, + params: tools.FetchPermissionsParams{ + URL: "https://example.com", + Format: "text", + }, + assert: func(t *testing.T, got any) { + v, ok := got.(tools.FetchPermissionsParams) + require.True(t, ok, "params must decode as tools.FetchPermissionsParams, got %T", got) + require.Equal(t, "https://example.com", v.URL) + }, + }, + { + name: "download", + toolName: tools.DownloadToolName, + params: tools.DownloadPermissionsParams{ + URL: "https://example.com/x.zip", + FilePath: "/tmp/x.zip", + Timeout: 30, + }, + assert: func(t *testing.T, got any) { + v, ok := got.(tools.DownloadPermissionsParams) + require.True(t, ok, "params must decode as tools.DownloadPermissionsParams, got %T", got) + require.Equal(t, "https://example.com/x.zip", v.URL) + require.Equal(t, "/tmp/x.zip", v.FilePath) + }, + }, + { + name: "agentic_fetch", + toolName: tools.AgenticFetchToolName, + params: tools.AgenticFetchPermissionsParams{ + URL: "https://example.com", + Prompt: "summarize this page", + }, + assert: func(t *testing.T, got any) { + v, ok := got.(tools.AgenticFetchPermissionsParams) + require.True(t, ok, "params must decode as tools.AgenticFetchPermissionsParams, got %T", got) + require.Equal(t, "https://example.com", v.URL) + require.Equal(t, "summarize this page", v.Prompt) + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // Build a server-side request with the tool's concrete + // params type, marshal to JSON (the wire path), then + // decode back through proto.PermissionRequest. + outbound := proto.PermissionRequest{ + ID: "perm-1", + SessionID: "sess-1", + ToolCallID: "call-1", + ToolName: tc.toolName, + Path: "/tmp", + Params: tc.params, + } + data, err := json.Marshal(outbound) + require.NoError(t, err) + + var inbound proto.PermissionRequest + require.NoError(t, json.Unmarshal(data, &inbound)) + + tc.assert(t, inbound.Params) + }) + } +} diff --git a/internal/proto/tools.go b/internal/proto/tools.go index 09774ac0a22b672ff7df81d968db21ef35517c02..e151c9deca8b2f13ca59ecec34d8614f628dcd94 100644 --- a/internal/proto/tools.go +++ b/internal/proto/tools.go @@ -1,5 +1,12 @@ package proto +// The wire schema for per-tool permission parameters is owned by the +// tool itself, not duplicated here. We alias the canonical types so +// there is exactly one source of truth and so values survive a +// round-trip across the client/server boundary as the same Go type +// the UI asserts on. +import "github.com/charmbracelet/crush/internal/agent/tools" + // ToolResponseType represents the type of tool response. type ToolResponseType string @@ -25,10 +32,7 @@ type BashParams struct { } // BashPermissionsParams represents the permission parameters for the bash tool. -type BashPermissionsParams struct { - Command string `json:"command"` - Timeout int `json:"timeout"` -} +type BashPermissionsParams = tools.BashPermissionsParams // BashResponseMetadata represents the metadata for a bash tool response. type BashResponseMetadata struct { @@ -53,11 +57,7 @@ type DownloadParams struct { } // DownloadPermissionsParams represents the permission parameters for the download tool. -type DownloadPermissionsParams struct { - URL string `json:"url"` - FilePath string `json:"file_path"` - Timeout int `json:"timeout,omitempty"` -} +type DownloadPermissionsParams = tools.DownloadPermissionsParams const EditToolName = "edit" @@ -70,11 +70,7 @@ type EditParams struct { } // EditPermissionsParams represents the permission parameters for the edit tool. -type EditPermissionsParams struct { - FilePath string `json:"file_path"` - OldContent string `json:"old_content,omitempty"` - NewContent string `json:"new_content,omitempty"` -} +type EditPermissionsParams = tools.EditPermissionsParams // EditResponseMetadata represents the metadata for an edit tool response. type EditResponseMetadata struct { @@ -94,11 +90,14 @@ type FetchParams struct { } // FetchPermissionsParams represents the permission parameters for the fetch tool. -type FetchPermissionsParams struct { - URL string `json:"url"` - Format string `json:"format"` - Timeout int `json:"timeout,omitempty"` -} +type FetchPermissionsParams = tools.FetchPermissionsParams + +// AgenticFetchToolName is the name of the agentic_fetch tool. +const AgenticFetchToolName = tools.AgenticFetchToolName + +// AgenticFetchPermissionsParams represents the permission parameters for the +// agentic_fetch tool. +type AgenticFetchPermissionsParams = tools.AgenticFetchPermissionsParams const GlobToolName = "glob" @@ -139,10 +138,7 @@ type LSParams struct { } // LSPermissionsParams represents the permission parameters for the ls tool. -type LSPermissionsParams struct { - Path string `json:"path"` - Ignore []string `json:"ignore"` -} +type LSPermissionsParams = tools.LSPermissionsParams // TreeNode represents a node in a directory tree. type TreeNode struct { @@ -174,11 +170,7 @@ type MultiEditParams struct { } // MultiEditPermissionsParams represents the permission parameters for the multi-edit tool. -type MultiEditPermissionsParams struct { - FilePath string `json:"file_path"` - OldContent string `json:"old_content,omitempty"` - NewContent string `json:"new_content,omitempty"` -} +type MultiEditPermissionsParams = tools.MultiEditPermissionsParams // MultiEditResponseMetadata represents the metadata for a multi-edit tool response. type MultiEditResponseMetadata struct { @@ -215,11 +207,7 @@ type ViewParams struct { } // ViewPermissionsParams represents the permission parameters for the view tool. -type ViewPermissionsParams struct { - FilePath string `json:"file_path"` - Offset int `json:"offset"` - Limit int `json:"limit"` -} +type ViewPermissionsParams = tools.ViewPermissionsParams // ViewResponseMetadata represents the metadata for a view tool response. type ViewResponseMetadata struct { @@ -236,11 +224,7 @@ type WriteParams struct { } // WritePermissionsParams represents the permission parameters for the write tool. -type WritePermissionsParams struct { - FilePath string `json:"file_path"` - OldContent string `json:"old_content,omitempty"` - NewContent string `json:"new_content,omitempty"` -} +type WritePermissionsParams = tools.WritePermissionsParams // WriteResponseMetadata represents the metadata for a write tool response. type WriteResponseMetadata struct {