mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-25 16:21:23 +00:00
Squash merge PR #115 after resolving changelog and SQLite migration-map conflicts with current dev. Renumbered channel-context PostgreSQL migration to 000075 and bumped PG required schema to 75 plus SQLite schema to 44 so it follows the run timeline migration. Local checks passed: go test ./..., go build ./..., go build -tags sqliteonly ./..., go vet ./..., and pnpm -C ui/web build. PR CI run 26705617311 passed release-versioning, go, and web.
211 lines
7.6 KiB
Go
211 lines
7.6 KiB
Go
package http
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/crypto"
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
)
|
|
|
|
type fakeChannelCapabilityMCPStore struct {
|
|
accessible []store.MCPAccessInfo
|
|
userCreds map[uuid.UUID]*store.MCPUserCredentials
|
|
}
|
|
|
|
func (s *fakeChannelCapabilityMCPStore) CreateServer(context.Context, *store.MCPServerData) error {
|
|
return nil
|
|
}
|
|
func (s *fakeChannelCapabilityMCPStore) GetServer(context.Context, uuid.UUID) (*store.MCPServerData, error) {
|
|
return nil, sql.ErrNoRows
|
|
}
|
|
func (s *fakeChannelCapabilityMCPStore) GetServerByName(context.Context, string) (*store.MCPServerData, error) {
|
|
return nil, sql.ErrNoRows
|
|
}
|
|
func (s *fakeChannelCapabilityMCPStore) ListServers(context.Context) ([]store.MCPServerData, error) {
|
|
return nil, nil
|
|
}
|
|
func (s *fakeChannelCapabilityMCPStore) UpdateServer(context.Context, uuid.UUID, map[string]any) error {
|
|
return nil
|
|
}
|
|
func (s *fakeChannelCapabilityMCPStore) DeleteServer(context.Context, uuid.UUID) error {
|
|
return nil
|
|
}
|
|
func (s *fakeChannelCapabilityMCPStore) GrantToAgent(context.Context, *store.MCPAgentGrant) error {
|
|
return nil
|
|
}
|
|
func (s *fakeChannelCapabilityMCPStore) RevokeFromAgent(context.Context, uuid.UUID, uuid.UUID) error {
|
|
return nil
|
|
}
|
|
func (s *fakeChannelCapabilityMCPStore) ListAgentGrants(context.Context, uuid.UUID) ([]store.MCPAgentGrant, error) {
|
|
return nil, nil
|
|
}
|
|
func (s *fakeChannelCapabilityMCPStore) ListServerGrants(context.Context, uuid.UUID) ([]store.MCPAgentGrant, error) {
|
|
return nil, nil
|
|
}
|
|
func (s *fakeChannelCapabilityMCPStore) GrantToUser(context.Context, *store.MCPUserGrant) error {
|
|
return nil
|
|
}
|
|
func (s *fakeChannelCapabilityMCPStore) RevokeFromUser(context.Context, uuid.UUID, string) error {
|
|
return nil
|
|
}
|
|
func (s *fakeChannelCapabilityMCPStore) CountAgentGrantsByServer(context.Context) (map[uuid.UUID]int, error) {
|
|
return map[uuid.UUID]int{}, nil
|
|
}
|
|
func (s *fakeChannelCapabilityMCPStore) ListAccessible(context.Context, uuid.UUID, string) ([]store.MCPAccessInfo, error) {
|
|
return s.accessible, nil
|
|
}
|
|
func (s *fakeChannelCapabilityMCPStore) CreateRequest(context.Context, *store.MCPAccessRequest) error {
|
|
return nil
|
|
}
|
|
func (s *fakeChannelCapabilityMCPStore) ListPendingRequests(context.Context) ([]store.MCPAccessRequest, error) {
|
|
return nil, nil
|
|
}
|
|
func (s *fakeChannelCapabilityMCPStore) ReviewRequest(context.Context, uuid.UUID, bool, string, string) error {
|
|
return nil
|
|
}
|
|
func (s *fakeChannelCapabilityMCPStore) GetUserCredentials(_ context.Context, serverID uuid.UUID, _ string) (*store.MCPUserCredentials, error) {
|
|
if creds := s.userCreds[serverID]; creds != nil {
|
|
return creds, nil
|
|
}
|
|
return nil, sql.ErrNoRows
|
|
}
|
|
func (s *fakeChannelCapabilityMCPStore) SetUserCredentials(context.Context, uuid.UUID, string, store.MCPUserCredentials) error {
|
|
return nil
|
|
}
|
|
func (s *fakeChannelCapabilityMCPStore) DeleteUserCredentials(context.Context, uuid.UUID, string) error {
|
|
return nil
|
|
}
|
|
|
|
type fakeChannelCapabilityCLIStore struct {
|
|
binaries []store.SecureCLIBinary
|
|
userCreds map[uuid.UUID]*store.SecureCLIUserCredential
|
|
}
|
|
|
|
func (s *fakeChannelCapabilityCLIStore) Create(context.Context, *store.SecureCLIBinary) error {
|
|
return nil
|
|
}
|
|
func (s *fakeChannelCapabilityCLIStore) Get(context.Context, uuid.UUID) (*store.SecureCLIBinary, error) {
|
|
return nil, sql.ErrNoRows
|
|
}
|
|
func (s *fakeChannelCapabilityCLIStore) Update(context.Context, uuid.UUID, map[string]any) error {
|
|
return nil
|
|
}
|
|
func (s *fakeChannelCapabilityCLIStore) Delete(context.Context, uuid.UUID) error {
|
|
return nil
|
|
}
|
|
func (s *fakeChannelCapabilityCLIStore) List(context.Context) ([]store.SecureCLIBinary, error) {
|
|
return nil, nil
|
|
}
|
|
func (s *fakeChannelCapabilityCLIStore) LookupByBinary(context.Context, string, *uuid.UUID, string) (*store.SecureCLIBinary, error) {
|
|
return nil, sql.ErrNoRows
|
|
}
|
|
func (s *fakeChannelCapabilityCLIStore) ListEnabled(context.Context) ([]store.SecureCLIBinary, error) {
|
|
return nil, nil
|
|
}
|
|
func (s *fakeChannelCapabilityCLIStore) ListForAgent(context.Context, uuid.UUID) ([]store.SecureCLIBinary, error) {
|
|
return s.binaries, nil
|
|
}
|
|
func (s *fakeChannelCapabilityCLIStore) IsRegisteredBinary(context.Context, string) (bool, error) {
|
|
return false, nil
|
|
}
|
|
func (s *fakeChannelCapabilityCLIStore) GetUserCredentials(_ context.Context, binaryID uuid.UUID, _ string) (*store.SecureCLIUserCredential, error) {
|
|
if creds := s.userCreds[binaryID]; creds != nil {
|
|
return creds, nil
|
|
}
|
|
return nil, sql.ErrNoRows
|
|
}
|
|
func (s *fakeChannelCapabilityCLIStore) SetUserCredentials(context.Context, uuid.UUID, string, []byte) error {
|
|
return nil
|
|
}
|
|
func (s *fakeChannelCapabilityCLIStore) SetUserCredentialsTyped(context.Context, uuid.UUID, string, []byte, *string, *string) error {
|
|
return nil
|
|
}
|
|
func (s *fakeChannelCapabilityCLIStore) DeleteUserCredentials(context.Context, uuid.UUID, string) error {
|
|
return nil
|
|
}
|
|
func (s *fakeChannelCapabilityCLIStore) ListUserCredentials(context.Context, uuid.UUID) ([]store.SecureCLIUserCredential, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func TestChannelContextCapabilitiesExposeOnlyCredentialMetadata(t *testing.T) {
|
|
token := "channel-capabilities-key"
|
|
setupTestCache(t, map[string]*store.APIKeyData{
|
|
crypto.HashAPIKey(token): {Scopes: []string{"operator.write"}, OwnerID: "tenant-user-1"},
|
|
})
|
|
|
|
instID := uuid.New()
|
|
agentID := uuid.New()
|
|
channelName := "discord-admin"
|
|
mcpID := uuid.New()
|
|
cliID := uuid.New()
|
|
mcpStore := &fakeChannelCapabilityMCPStore{
|
|
accessible: []store.MCPAccessInfo{{
|
|
Server: store.MCPServerData{
|
|
BaseModel: store.BaseModel{ID: mcpID},
|
|
Name: "github",
|
|
DisplayName: "GitHub",
|
|
APIKey: "global-value",
|
|
Enabled: true,
|
|
},
|
|
ToolAllow: []string{"issues.create"},
|
|
}},
|
|
userCreds: map[uuid.UUID]*store.MCPUserCredentials{
|
|
mcpID: {APIKey: "user-value"},
|
|
},
|
|
}
|
|
cliStore := &fakeChannelCapabilityCLIStore{
|
|
binaries: []store.SecureCLIBinary{{
|
|
BaseModel: store.BaseModel{ID: cliID},
|
|
BinaryName: "gh",
|
|
Description: "GitHub CLI",
|
|
EncryptedEnv: []byte(`{"VALUE":"global-value"}`),
|
|
IsGlobal: true,
|
|
Enabled: true,
|
|
}},
|
|
userCreds: map[uuid.UUID]*store.SecureCLIUserCredential{
|
|
cliID: {BinaryID: cliID, UserID: "tenant-user-1", EncryptedEnv: []byte(`{"VALUE":"user-value"}`)},
|
|
},
|
|
}
|
|
handler := NewChannelInstancesHandler(
|
|
&stubChannelInstanceStore{inst: &store.ChannelInstanceData{BaseModel: store.BaseModel{ID: instID}, Name: channelName, ChannelType: "discord", AgentID: agentID}},
|
|
nil, nil, nil, nil, nil,
|
|
)
|
|
handler.SetCapabilityStores(mcpStore, cliStore)
|
|
mux := http.NewServeMux()
|
|
handler.RegisterRoutes(mux)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v1/channels/instances/"+instID.String()+"/contexts/channel/"+channelName+"/capabilities", nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
rec := httptest.NewRecorder()
|
|
mux.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
bodyText := rec.Body.String()
|
|
if strings.Contains(bodyText, "global-value") || strings.Contains(bodyText, "user-value") {
|
|
t.Fatalf("capability response leaked credential material: %s", bodyText)
|
|
}
|
|
var body struct {
|
|
MCP []channelCapabilityDTO `json:"mcp"`
|
|
SecureCLI []channelCapabilityDTO `json:"secure_cli"`
|
|
}
|
|
if err := json.Unmarshal([]byte(bodyText), &body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(body.MCP) != 1 || body.MCP[0].CredentialSource != "user" || !body.MCP[0].HasCredential {
|
|
t.Fatalf("mcp capability = %+v", body.MCP)
|
|
}
|
|
if len(body.SecureCLI) != 1 || body.SecureCLI[0].Source != "global" || body.SecureCLI[0].CredentialSource != "user" || !body.SecureCLI[0].HasCredential {
|
|
t.Fatalf("secure cli capability = %+v", body.SecureCLI)
|
|
}
|
|
}
|