From e5ef7ef331f64ab5ff45e44803ad8461172060e4 Mon Sep 17 00:00:00 2001 From: yballul-bytedance <273011618+yballul-bytedance@users.noreply.github.com> Date: Fri, 31 Jul 2026 00:55:31 +0800 Subject: [PATCH] feat(base): classify Base resource permission denials as typed errors Map the Base API 91403 response code to a typed PermissionError instead of a generic API error. The error carries the resolved identity, a permission-denied subtype, code, and log ID, plus an actionable hint that tells AI agents to ask the Base owner for access rather than repeatedly running auth login or switching identities for a resource-level denial. Co-authored-by: TRAE CLI --- shortcuts/base/base_errors.go | 30 ++++++++++++++++++++++++++++ shortcuts/base/base_errors_test.go | 32 ++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/shortcuts/base/base_errors.go b/shortcuts/base/base_errors.go index f9ca405963..2cf4e1c95f 100644 --- a/shortcuts/base/base_errors.go +++ b/shortcuts/base/base_errors.go @@ -160,6 +160,9 @@ func baseAPIErrorFromResult(resultMap map[string]interface{}, cc errclass.Classi if logID := extractBaseErrorLogID(resultMap); logID != "" { resultMap["log_id"] = logID } + if err := baseResourcePermissionError(resultMap, cc, hint); err != nil { + return err + } err := errclass.BuildAPIError(resultMap, cc) if err == nil { return nil @@ -170,6 +173,33 @@ func baseAPIErrorFromResult(resultMap map[string]interface{}, cc errclass.Classi return err } +func baseResourcePermissionError(resultMap map[string]interface{}, cc errclass.ClassifyContext, upstreamHint string) error { + code, ok := util.ToFloat64(resultMap["code"]) + if !ok || int(code) != 91403 { + return nil + } + msg, _ := resultMap["msg"].(string) + if strings.TrimSpace(msg) == "" { + msg = "you don't have permission" + } + identity := strings.TrimSpace(cc.Identity) + if identity == "" { + identity = "current identity" + } + hint := fmt.Sprintf("resource access denied for %s; ask the Base owner to grant access, or retry once with --as user only if a user identity is already logged in. Do not run auth login or switch identities repeatedly for this resource-level permission error", identity) + if upstreamHint != "" { + hint = upstreamHint + "; " + hint + } + err := errs.NewPermissionError(errs.SubtypePermissionDenied, "%s", msg). + WithCode(91403). + WithHint("%s", hint) + err.Identity = identity + if logID, _ := resultMap["log_id"].(string); strings.TrimSpace(logID) != "" { + err.WithLogID(strings.TrimSpace(logID)) + } + return err +} + func enrichBaseAPIErrorFromBody(err error, body []byte, cc errclass.ClassifyContext) error { if _, ok := errs.ProblemOf(err); !ok { return err diff --git a/shortcuts/base/base_errors_test.go b/shortcuts/base/base_errors_test.go index 61d4083713..877fe96270 100644 --- a/shortcuts/base/base_errors_test.go +++ b/shortcuts/base/base_errors_test.go @@ -118,6 +118,38 @@ func TestHandleBaseAPIResultClassifiesKnownPermissionCode(t *testing.T) { } } +func TestHandleBaseAPIResultClassifiesBaseResourcePermission(t *testing.T) { + result := map[string]interface{}{ + "code": 91403, + "msg": "you don't have permission", + "data": map[string]interface{}{}, + } + + _, err := handleBaseAPIResultAny(result, nil, "list dashboards") + p, ok := errs.ProblemOf(err) + if !ok { + t.Fatalf("expected typed error, got %T %v", err, err) + } + if p.Code != 91403 { + t.Fatalf("code=%d", p.Code) + } + if p.Category != errs.CategoryAuthorization || p.Subtype != errs.SubtypePermissionDenied { + t.Fatalf("category/subtype=%s/%s", p.Category, p.Subtype) + } + perm, ok := err.(*errs.PermissionError) + if !ok { + t.Fatalf("err=%T, want *errs.PermissionError", err) + } + if perm.Identity != "current identity" { + t.Fatalf("identity=%q", perm.Identity) + } + for _, want := range []string{"resource access denied", "Base owner", "Do not run auth login"} { + if !strings.Contains(p.Hint, want) { + t.Fatalf("hint=%q missing %q", p.Hint, want) + } + } +} + func TestAttachBaseResponseLogIDFromHeader(t *testing.T) { result := map[string]interface{}{ "code": 91402,