diff --git a/internal/ir/catalog.go b/internal/ir/catalog.go index 5b9369b4..6379a234 100644 --- a/internal/ir/catalog.go +++ b/internal/ir/catalog.go @@ -341,13 +341,31 @@ func extractToolAuthMetadata(schema map[string]any, productID string) *ToolAuthM if err := json.Unmarshal(data, &metadata); err != nil { return nil } - if len(metadata.RequiredScopes) == 0 && len(metadata.RequiredPermissions) == 0 && - len(metadata.ClientObservedScopes) == 0 && metadata.AuthMetaHash == "" { + if !hasMeaningfulToolAuthMetadata(metadata) { return nil } return &metadata } +func hasMeaningfulToolAuthMetadata(metadata ToolAuthMetadata) bool { + return strings.TrimSpace(metadata.Version) != "" || + strings.TrimSpace(metadata.ProductCode) != "" || + strings.TrimSpace(metadata.Domain) != "" || + len(metadata.ClientObservedScopes) > 0 || + len(metadata.RequiredScopes) > 0 || + len(metadata.RequiredPermissions) > 0 || + len(metadata.RecommendedScopes) > 0 || + len(metadata.ExcludedScopes) > 0 || + len(metadata.GrantProductCodes) > 0 || + strings.TrimSpace(metadata.RiskHint) != "" || + strings.TrimSpace(metadata.RiskAction) != "" || + metadata.ConfirmationRequired || + len(metadata.Identities) > 0 || + strings.TrimSpace(metadata.Source) != "" || + strings.TrimSpace(metadata.AuthMetaVersion) != "" || + strings.TrimSpace(metadata.AuthMetaHash) != "" +} + func productGrantAuthMetadata(productID string) *ToolAuthMetadata { productID = strings.TrimSpace(productID) if productID == "" { diff --git a/internal/ir/catalog_test.go b/internal/ir/catalog_test.go index 79d42d26..eccaaa2d 100644 --- a/internal/ir/catalog_test.go +++ b/internal/ir/catalog_test.go @@ -265,6 +265,51 @@ func TestBuildCatalogCarriesDingTalkAuthMetadata(t *testing.T) { } } +func TestBuildCatalogCarriesGrantOnlyAuthMetadata(t *testing.T) { + t.Parallel() + + catalog := BuildCatalog([]discovery.RuntimeServer{ + { + Server: market.ServerDescriptor{ + Key: "calendar-key", + DisplayName: "日历", + Endpoint: "https://example.com/server/calendar", + }, + Tools: []transport.ToolDescriptor{ + { + Name: "createEvent", + InputSchema: map[string]any{ + "type": "object", + "x-dingtalk-auth": map[string]any{ + "grantProductCodes": []any{"calendar"}, + "recommendedScopes": []any{"calendar.event:create"}, + "riskAction": "high-risk-write", + "confirmationRequired": true, + }, + }, + }, + }, + }, + }) + + tool, ok := catalog.Products[0].FindTool("createEvent") + if !ok { + t.Fatalf("FindTool(createEvent) = not found") + } + if tool.Auth == nil { + t.Fatal("tool.Auth = nil") + } + if got := tool.Auth.GrantProductCodes; len(got) != 1 || got[0] != "calendar" { + t.Fatalf("GrantProductCodes = %#v, want calendar", got) + } + if got := tool.Auth.RecommendedScopes; len(got) != 1 || got[0] != "calendar.event:create" { + t.Fatalf("RecommendedScopes = %#v, want calendar.event:create", got) + } + if !tool.Auth.ConfirmationRequired { + t.Fatal("ConfirmationRequired = false, want true") + } +} + func TestBuildCatalogFallsBackToProductGrantAuthMetadata(t *testing.T) { t.Parallel() diff --git a/internal/pat/chmod.go b/internal/pat/chmod.go index 7819aac6..6cce6c0f 100644 --- a/internal/pat/chmod.go +++ b/internal/pat/chmod.go @@ -213,7 +213,7 @@ grantType 规则: if c != nil && c.DryRun() { if usesPlan { - planArgs := buildBatchPlanArgs(scopes, productCodes, recommend, grantType, true) + planArgs := buildBatchPlanArgs(scopes, productCodes, recommend, grantType, sessionID, true) result, err := callPATBatchPlan(cmd.Context(), c, agentCode, sessionID, planArgs) if err != nil { return fmt.Errorf("pat chmod plan failed: %w", err) @@ -244,7 +244,7 @@ grantType 规则: sessionID = resolveSessionIDFromEnv() } if usesPlan { - planArgs := buildBatchPlanArgs(scopes, productCodes, recommend, grantType, true) + planArgs := buildBatchPlanArgs(scopes, productCodes, recommend, grantType, sessionID, true) planResult, err := callPATBatchPlan(cmd.Context(), c, agentCode, sessionID, planArgs) if err != nil { return fmt.Errorf("pat chmod plan failed: %w", err) @@ -269,6 +269,7 @@ grantType 规则: toolArgs["agentCode"] = agentCode } if sessionID != "" { + batchArgs["sessionId"] = sessionID toolArgs["sessionId"] = sessionID } // Legacy server schema accepted singular "scope"; clone the @@ -405,14 +406,18 @@ func callPATBatchPlan(ctx context.Context, c edition.ToolCaller, agentCode, sess }) } -func buildBatchPlanArgs(scopes []string, productCodes []string, recommend bool, grantType string, dryRun bool) map[string]any { - return map[string]any{ +func buildBatchPlanArgs(scopes []string, productCodes []string, recommend bool, grantType string, sessionID string, dryRun bool) map[string]any { + args := map[string]any{ "scopes": scopes, "productCodes": productCodes, "recommend": recommend, "grantType": grantType, "dryRun": dryRun, } + if sessionID != "" { + args["sessionId"] = sessionID + } + return args } func extractSelectedScopes(result *edition.ToolResult) ([]string, error) { diff --git a/internal/pat/chmod_test.go b/internal/pat/chmod_test.go index 8699857c..326e48cb 100644 --- a/internal/pat/chmod_test.go +++ b/internal/pat/chmod_test.go @@ -314,6 +314,37 @@ func TestChmod_productsFlagPlansThenGrantsSelectedScopes(t *testing.T) { } } +func TestChmod_productsSessionModePassesSessionIDToPlanAndGrant(t *testing.T) { + t.Setenv(agentCodeEnv, "qoderwork") + fake := &sequenceToolCaller{responses: []string{ + `{"success":true,"data":{"selectedScopes":["calendar.event:read"]}}`, + `{"success":true,"data":{"grantedScopes":["calendar.event:read"]}}`, + }} + cmd := newChmodCommand(fake) + _ = cmd.Flags().Set("products", "calendar") + _ = cmd.Flags().Set("session-id", "session-123") + + if err := cmd.RunE(cmd, nil); err != nil { + t.Fatalf("chmod RunE error = %v", err) + } + + if len(fake.calls) != 2 { + t.Fatalf("CallTool count = %d, want 2", len(fake.calls)) + } + if got := fake.calls[0].args["grantType"]; got != "session" { + t.Fatalf("plan grantType = %#v, want session", got) + } + if got := fake.calls[0].args["sessionId"]; got != "session-123" { + t.Fatalf("plan sessionId = %#v, want session-123", got) + } + if got := fake.calls[1].args["grantType"]; got != "session" { + t.Fatalf("grant grantType = %#v, want session", got) + } + if got := fake.calls[1].args["sessionId"]; got != "session-123" { + t.Fatalf("grant sessionId = %#v, want session-123", got) + } +} + func TestChmod_recommendFlagPlansThenGrantsWithoutPositionalScopes(t *testing.T) { t.Setenv(agentCodeEnv, "qoderwork") fake := &sequenceToolCaller{responses: []string{