Skip to content

feat(errs): add structured CLI error contract#984

Open
evandance wants to merge 1 commit into
mainfrom
feat/error-contract-framework
Open

feat(errs): add structured CLI error contract#984
evandance wants to merge 1 commit into
mainfrom
feat/error-contract-framework

Conversation

@evandance
Copy link
Copy Markdown
Collaborator

@evandance evandance commented May 20, 2026

Summary

Add a typed error framework so future PRs can replace output.ErrAPI(code, ...) call sites with &errs.PermissionError{...} etc. Go callers will branch via errors.As(&errs.XxxError{}); shell scripts, AI agents, and protocol adapters will branch on stable JSON type / subtype fields instead of regex-parsing free-form messages.

This PR is the framework slice — it adds the package and infrastructure but does not migrate any business call sites. SecurityPolicyError taxonomy migration is PR 2; per-domain shortcut migration (task / drive / calendar / im / mail / whiteboard / contact) is PRs 3+.

What's added

  • errs/ package — 9 categories (validation, authentication, authorization, config, network, api, policy, internal, confirmation), one typed struct per category, all embedding a shared RFC 7807-aligned Problem. IsXxx predicates for ergonomic matching.
  • Lark code registry (internal/errclass/codemeta.go) — single source of truth mapping Lark numeric codes to {Category, Subtype, Retryable}. Consumed by errclass.BuildAPIError for typed dispatch.
  • Typed JSON envelope writer (internal/output/errors.go WriteTypedErrorEnvelope) — emits {ok, identity, error: {type, subtype, code?, message, hint?, log_id?, retryable?, …extension fields}}. Called by cmd/root.go handleRootError ahead of the legacy writer; fires only when the error is a typed *errs.*.
  • Five CI lint guards (internal/lintcheck/, .golangci.yml): A — forbid output.ErrAPI / output.Errorf in shortcuts/* and cmd/service/*; B — every exported *Error struct embeds errs.Problem; C — no service-side mergeCodeMeta / registrar pattern (codemeta is centrally owned); D — Subtype: "ad_hoc_*" literals get a governance label (not rejected); E — Subtype value must be a declared constant or match ad_hoc_*.

What stays legacy

Most production paths emit the legacy *output.ExitError envelope unchanged:

  • output.ErrAPI / output.ErrAuth / output.ErrValidation / output.ErrNetwork / output.ErrWithHint / output.Errorf / output.ErrBare — all still return *output.ExitError and produce the legacy {ok, identity, error: {type, code, message, hint?, detail?}} envelope. errors.As(&output.ExitError{}) still matches.
  • APIClient.CheckResponse (used by cmd/api) — still routes Lark API responses through ClassifyLarkError and emits the legacy envelope.
  • Business shortcuts (~12 sites under shortcuts/) hand-construct output.ErrAPI(...) — also legacy.
  • BuildAPIError is implemented and tested but has no production caller yet — per-domain migration in PRs 3+ flips CheckResponse and call sites to use it.

User-visible wire / exit changes

Only these change in this PR:

  • *core.ConfigError exit code 2 → 3. Direct construction-site edit in internal/core/config.go (two Code: 2 sites now Code: 3). Aligns with the documented "config errors share the auth exit code" contract. errors.As(&core.ConfigError{}) still matches on the unwrap chain.
  • APIClient.DoSDKRequest / DoStream / CallAPI now wrap untyped SDK errors at the boundary. Previously, a raw fmt.Errorf escaping the SDK surfaced as plain Error: <msg> (exit 1) at the root dispatcher. It now goes through WrapDoAPIError / WrapJSONResponseParseError, emitting a JSON envelope: type=network (exit 4) for transport-class failures, type=api_error (exit 1) for JSON-decode failures. Already-classified errors (*output.ExitError, *errs.*) pass through unchanged — output.ErrAuth from a missing token still surfaces as type=auth / exit 3 instead of being downgraded to network.

No other production exit code or envelope shape changes in this PR.

Carve-outs

  • SecurityPolicyError keeps its custom envelope and exit 1. handleRootError checks errors.As(err, &spErr) first and emits via writeSecurityPolicyError, producing {type: "auth_error", code: "challenge_required"|"access_denied", message, retryable: false, challenge_url?, hint?}. OAuth/policy consumers depend on this shape; taxonomy migration is PR 2. Guardrail test: TestHandleRootError_SecurityPolicyKeepsLegacyEnvelope.
  • All business output.ErrAPI(...) call sites keep emitting the legacy envelope; per-domain migration is PRs 3+.

Test Plan

  • make unit-test (77 packages, race + gcflags, 0 FAIL)
  • validate: go build ./..., go vet ./..., gofmt -l . empty, go mod tidy, go run ./cmd/lintcheck . exit 0, golangci-lint run --new-from-rev=origin/main 0 issues, go-licenses check
  • sandbox E2E 128 pass / 15 pre-existing env failures (meta-registry --yes flag gap + test-account permission gap; not regressions)
  • acceptance-reviewer PASS (0 blockers; report in docs/superpowers/verify_results/acceptance_review_v2.md)
  • guardrail tests: TestBuildAPIError_ExitCodeMatrix, TestConsoleURL_EscapesDangerousChars, TestHandleRootError_SecurityPolicyKeepsLegacyEnvelope, TestPromoteConfigError_*, TestDoSDKRequest_AuthFailurePreservesAuthCategory, TestTryHandleMCPResponse_*, TestWrapDoAPIError_LegacyExitErrorPassesThrough

Related Issues

N/A — first PR in the feat/error-contract-* series.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 20, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds errs/ typed error taxonomy and projections; rewires client/auth/commands/output to produce and envelope typed errors; updates tests accordingly; introduces errclass code meta/classifier; adds lintcheck tool and CI step; adjusts exit code mapping and config-auth promotion; widespread deprecation notes.

Changes

Typed error system and integration

Layer / File(s) Summary
Core taxonomy and projections
errs/*
Introduces typed errors (Problem, types, subtypes), predicates, MCP/OAuth projections, wrapping helpers, and contract docs/tests.
Classification and exit codes
internal/errclass/*, internal/output/exitcode*.go, internal/output/lark_errors.go
Maps Lark codes to typed categories/subtypes, builds typed API/permission errors, and derives exit codes from categories.
Client/auth/command wiring
internal/client/*, internal/auth/*, cmd/*
Propagates typed errors through SDK, response handling, pagination, root dispatch, and scope hints; converts legacy config/auth to typed forms.
Tests migration
cmd/*_test.go, shortcuts/*_test.go, internal/*_test.go
Switches assertions to typed errors, Problem extraction, and ExitCodeOf; adds extensive new tests for classifier/projections.
Lint and CI guardrails
internal/lintcheck/*, cmd/lintcheck/main.go, .github/workflows/ci.yml, .golangci.yml
Adds repo scanner enforcing typed-error contract (rules B–E), CLI entrypoint, CI step, and forbidigo rules.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User as CLI User
  participant CLI as CLI Command
  participant Client as APIClient
  participant Class as errclass.Classify
  participant Errs as errs.Typed
  participant Out as output

  User->>CLI: run command
  CLI->>Client: DoSDKRequest/CallAPI
  Client-->>Client: WrapDoAPIError/WrapJSONResponseParseError
  Client->>Class: BuildAPIError(result, identity)
  Class-->>Client: *errs.(API|Permission|... )Error
  Client-->>CLI: return typed error
  CLI->>Errs: ProblemOf / predicates
  CLI->>Out: WriteTypedErrorEnvelope(err, identity)
  Out-->>User: typed stderr JSON + mapped exit code
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • larksuite/cli#294 — Also modifies WrapDriveMediaUploadRequestError to preserve typed errs, intersecting with this PR’s typed error propagation.
  • larksuite/cli#776 — Adjusts missing-authorization hinting; this PR migrates that flow to applyNeedAuthorizationHint on typed AuthError.
  • larksuite/cli#494 — Touches legacy ClassifyLarkError; this PR refactors it to use errclass.LookupCodeMeta.

Poem

A rabbit taps the console key,
Errors hop as types, now clean and free.
Codes to subtypes, neatly spun,
Envelopes shine—review is done.
Lint checks nibble every vine,
CI burrows: all aligned.
Thump—typed trails, by design.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/error-contract-framework

@github-actions github-actions Bot added domain/base PR touches the base domain domain/calendar PR touches the calendar domain domain/ccm PR touches the ccm domain domain/mail PR touches the mail domain domain/task PR touches the task domain size/XL Architecture-level or global-impact change labels May 20, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🧹 Nitpick comments (1)
errs/marshal_test.go (1)

63-64: ⚡ Quick win

Avoid discarding json.Marshal errors in tests.

These tests currently ignore marshal failures, which can create misleading assertions on empty/invalid payloads. Capture and assert err == nil consistently.

Proposed cleanup
- b, _ := json.Marshal(ve)
+ b, err := json.Marshal(ve)
+ if err != nil {
+   t.Fatal(err)
+ }

You can apply the same pattern to each marshal call in this file.

Also applies to: 78-79, 89-90, 108-109, 122-123, 136-137, 147-148, 164-165, 175-176, 193-194, 209-210, 224-225

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@errs/marshal_test.go` around lines 63 - 64, The tests call json.Marshal into
variables like b and then ignore the error (e.g., b, _ := json.Marshal(ve); s :=
string(b)); update each marshal invocation (all occurrences around variables b
and s) to capture the error (b, err := json.Marshal(...)) and add an assertion
that err == nil (use the test helper being used in the file, e.g.,
require.NoError(t, err) or if that framework isn't used, t.Fatalf/if err != nil
{ t.Fatalf(...) }) before converting b to string; apply this pattern to every
marshal call listed (lines near the examples: 63-64, 78-79, 89-90, 108-109,
122-123, 136-137, 147-148, 164-165, 175-176, 193-194, 209-210, 224-225).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/auth/scopes.go`:
- Around line 57-65: The current return in getAppInfo unconditionally wraps any
error as *errs.PermissionError (using errs.Problem/SubtypeAppScopeNotEnabled),
which hides the original error type and subtype; change the logic in getAppInfo
to detect and preserve non-permission errors (e.g., if err is already an
*errs.Error/*errs.Problem or not related to app-scope authorization) and only
construct and return a new *errs.PermissionError when the failure is truly an
authorization/app-scope issue; reference the existing symbols
errs.PermissionError, errs.Problem, errs.SubtypeAppScopeNotEnabled and the
getAppInfo call site to implement a type-check/inspect-and-branch so other
failure categories (network/auth/internal) are returned unchanged.

In `@cmd/lintcheck/main.go`:
- Around line 41-50: The CLI-provided root path is used without validation; call
validate.SafeInputPath on the resolved root (the local variable root) before
passing it to lintcheck.ScanRepo, handle the error returned (log or exit
appropriately) and only call lintcheck.ScanRepo with the validated/clean path;
update the code around the root variable and the lintcheck.ScanRepo call to use
the validated path and error handling to enforce path-safety.

In `@errs/subtypes_service_task.go`:
- Line 10: Update the header comment in errs/subtypes_service_task.go to point
to the correct consumer path: replace the incorrect reference to
internal/output/codemeta_task.go with internal/errclass/codemeta_task.go so the
comment accurately states where these task service subtypes are consumed; ensure
the comment text around "Task service subtypes" and any mention of
codemeta_task.go is updated accordingly.

In `@internal/client/pagination.go`:
- Around line 31-34: The fallback for identity currently sets identity :=
pagOpts.Identity and then defaults to core.AsUser if empty, which ignores
request.As; update the logic so identity is determined by checking
pagOpts.Identity first, then request.As, and only then defaulting to core.AsUser
(i.e., set identity = pagOpts.Identity if non-empty, else use request.As if
non-empty, else core.AsUser) so that checkErr and related metadata use the
correct request identity.

In `@internal/errcompat/promote.go`:
- Around line 27-29: PromoteConfigError currently dereferences cfgErr without
checking for nil; add a nil guard at the start of PromoteConfigError (the
function that accepts *core.ConfigError) and return nil (or a sensible error)
immediately if cfgErr is nil to avoid a panic, then proceed to inspect
cfgErr.Type and construct errs.AuthError / other error types as before.

In `@internal/lintcheck/rules.go`:
- Around line 125-126: The condition that tries to match RegisterServiceMap
variants misses names with infixes; update the callee check in the lint rule
(where the variable callee is compared alongside mergeCodeMeta) to use
strings.Contains(callee, "RegisterServiceMap") instead of
strings.HasPrefix/HasSuffix so any name containing "RegisterServiceMap" (e.g.,
FooRegisterServiceMapBar) is detected; keep the equality check for
"mergeCodeMeta" intact and replace only the RegisterServiceMap prefix/suffix
logic in the function in internal/lintcheck/rules.go.
- Around line 77-80: The rule currently flags any type whose name ends with
"Error" (using typeSpec.Name.Name); change it to only apply to exported Error
types by first ensuring the identifier is exported (e.g., check that the name is
non-empty and its first rune is uppercase or use a proper IsExported helper)
before applying the strings.HasSuffix(name, "Error") test, so unexported
"*Error" structs are ignored; update the condition around typeSpec.Name.Name to
return true for non-exported names and only run the suffix check for exported
identifiers.

In `@internal/lintcheck/scan.go`:
- Around line 28-29: The ScanRepo function currently consumes the CLI-controlled
root path directly (used with filepath.Join and directory walking); validate the
repository root by calling validate.SafeInputPath(root) at the start of ScanRepo
and return a clear error if validation fails before calling
LoadSubtypeAllowlists or performing any file system operations; apply the same
safe-path gate to any other functions in this file that accept a root/path (the
block referenced around the LoadSubtypeAllowlists use and the code region
covering the later scan logic) so all file reads/walks only occur after
validate.SafeInputPath has approved the input.
- Around line 41-43: Replace all direct os.* filesystem calls in scan.go with
the vfs equivalents to preserve test-mocking: change os.Stat -> vfs.Stat,
os.ReadDir -> vfs.ReadDir, os.ReadFile -> vfs.ReadFile and os.IsNotExist ->
vfs.IsNotExist, update the import to use the vfs package, and keep existing
error branches (e.g., the else-if that currently reads "!os.IsNotExist(err)")
but using vfs.IsNotExist(err) so behavior is identical while enabling vfs-based
tests; apply the same replacements for every occurrence noted (the blocks using
os.Stat, os.ReadDir, os.ReadFile and os.IsNotExist).
- Around line 311-315: CheckErrsContract is currently collecting any struct
whose name ends with "Error" (via the loop that inspects
d.Type.(*ast.StructType) and uses d.Name.Name) including unexported/internal
types; modify that logic to only register exported error types by adding an
exported-name check (e.g., use ast.IsExported(d.Name.Name) or test the first
rune is upper-case) before writing into typedErrors so only exported "*Error"
structs are added; keep the existing typedErrors map and fset.Position(d.Pos())
usage unchanged.
- Around line 29-34: The code currently treats any error from
LoadSubtypeAllowlists as "missing files" and silently disables Rule E; instead,
call LoadSubtypeAllowlists(root+"/errs") and if err is nil proceed, if
errors.Is(err, os.ErrNotExist) (or os.IsNotExist(err) / errors.Is(err,
fs.ErrNotExist) depending on imports) then set allowlist/nameset = nil to skip
Rule E, otherwise propagate or return the error (or log and fail CI) so genuine
parse/read failures are not swallowed; reference the LoadSubtypeAllowlists call
and the allowlist/nameset variables to locate where to add the is-not-exist
check and error propagation.

In `@internal/output/errors.go`:
- Around line 202-204: The code currently returns true when enc.Encode(env)
fails (encErr), which wrongly signals the envelope was written and suppresses
fallback; change the behavior so that on enc.Encode(env) error you do not report
"written" — e.g., handle or log encErr as appropriate and return false (or
propagate the error) instead of returning true; make this change where
enc.Encode(env) is called (variables enc, env, encErr).

---

Nitpick comments:
In `@errs/marshal_test.go`:
- Around line 63-64: The tests call json.Marshal into variables like b and then
ignore the error (e.g., b, _ := json.Marshal(ve); s := string(b)); update each
marshal invocation (all occurrences around variables b and s) to capture the
error (b, err := json.Marshal(...)) and add an assertion that err == nil (use
the test helper being used in the file, e.g., require.NoError(t, err) or if that
framework isn't used, t.Fatalf/if err != nil { t.Fatalf(...) }) before
converting b to string; apply this pattern to every marshal call listed (lines
near the examples: 63-64, 78-79, 89-90, 108-109, 122-123, 136-137, 147-148,
164-165, 175-176, 193-194, 209-210, 224-225).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 715761fd-2c37-462e-a0fb-fb08f53b8426

📥 Commits

Reviewing files that changed from the base of the PR and between 69c3448 and 255fc10.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (79)
  • .github/workflows/ci.yml
  • .golangci.yml
  • cmd/api/api.go
  • cmd/api/api_test.go
  • cmd/auth/scopes.go
  • cmd/config/bind_test.go
  • cmd/config/config_test.go
  • cmd/error_auth_hint.go
  • cmd/event/runtime.go
  • cmd/lintcheck/main.go
  • cmd/root.go
  • cmd/root_integration_test.go
  • cmd/root_test.go
  • cmd/service/service.go
  • cmd/service/service_test.go
  • errs/category.go
  • errs/category_test.go
  • errs/doc.go
  • errs/internal_carrier.go
  • errs/marshal_test.go
  • errs/predicates.go
  • errs/predicates_test.go
  • errs/problem.go
  • errs/problem_test.go
  • errs/projection/mcp.go
  • errs/projection/mcp_test.go
  • errs/projection/oauth.go
  • errs/projection/oauth_test.go
  • errs/subtypes.go
  • errs/subtypes_service_task.go
  • errs/types.go
  • errs/types_test.go
  • errs/wrap.go
  • errs/wrap_test.go
  • go.mod
  • internal/auth/errors.go
  • internal/auth/transport.go
  • internal/auth/uat_client.go
  • internal/client/api_errors.go
  • internal/client/api_errors_test.go
  • internal/client/client.go
  • internal/client/client_test.go
  • internal/client/pagination.go
  • internal/client/response.go
  • internal/client/response_test.go
  • internal/cmdutil/json.go
  • internal/core/config.go
  • internal/core/errors.go
  • internal/core/notconfigured.go
  • internal/errclass/classify.go
  • internal/errclass/classify_test.go
  • internal/errclass/codemeta.go
  • internal/errclass/codemeta_task.go
  • internal/errclass/codemeta_test.go
  • internal/errcompat/promote.go
  • internal/errcompat/promote_test.go
  • internal/lintcheck/rules.go
  • internal/lintcheck/rules_test.go
  • internal/lintcheck/scan.go
  • internal/lintcheck/scan_test.go
  • internal/lintcheck/typecheck.go
  • internal/lintcheck/typecheck_test.go
  • internal/output/errors.go
  • internal/output/exitcode.go
  • internal/output/exitcode_test.go
  • internal/output/lark_errors.go
  • shortcuts/base/base_execute_test.go
  • shortcuts/calendar/calendar_test.go
  • shortcuts/common/drive_media_upload.go
  • shortcuts/common/drive_media_upload_test.go
  • shortcuts/common/mcp_client_test.go
  • shortcuts/drive/drive_status_test.go
  • shortcuts/mail/mail_shortcut_validation_test.go
  • shortcuts/markdown/helpers.go
  • shortcuts/markdown/markdown_diff_test.go
  • shortcuts/task/task_body_test.go
  • shortcuts/task/task_query_helpers_test.go
  • shortcuts/task/task_upload_attachment_test.go
  • shortcuts/task/task_util_test.go
💤 Files with no reviewable changes (1)
  • cmd/error_auth_hint.go

Comment thread cmd/auth/scopes.go Outdated
Comment thread cmd/lintcheck/main.go
Comment thread errs/subtypes_service_task.go Outdated
Comment thread internal/client/pagination.go
Comment thread internal/errcompat/promote.go Outdated
Comment thread internal/lintcheck/scan.go
Comment thread internal/lintcheck/scan.go
Comment thread internal/lintcheck/scan.go
Comment thread internal/lintcheck/scan.go Outdated
Comment thread internal/output/errors.go
@evandance evandance force-pushed the feat/error-contract-framework branch from 255fc10 to 24b4a47 Compare May 20, 2026 07:38
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 20, 2026

🚀 PR Preview Install Guide

🧰 CLI update

npm i -g https://pkg.pr.new/larksuite/cli/@larksuite/cli@569e208a1e0055c3c0d601c804be5af2696177a6

🧩 Skill update

npx skills add larksuite/cli#feat/error-contract-framework -y -g

@codecov
Copy link
Copy Markdown

codecov Bot commented May 20, 2026

Codecov Report

❌ Patch coverage is 70.02562% with 351 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.76%. Comparing base (816927f) to head (569e208).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
internal/lintcheck/typed_error_completeness.go 29.11% 53 Missing and 3 partials ⚠️
internal/lintcheck/scan.go 74.27% 29 Missing and 24 partials ⚠️
internal/output/lark_errors.go 47.27% 28 Missing and 1 partial ⚠️
cmd/lintcheck/main.go 0.00% 24 Missing ⚠️
internal/output/errors.go 0.00% 23 Missing ⚠️
internal/errclass/classify.go 83.70% 19 Missing and 3 partials ⚠️
internal/lintcheck/subtype_classifier.go 81.19% 13 Missing and 9 partials ⚠️
internal/lintcheck/typecheck.go 68.11% 11 Missing and 11 partials ⚠️
cmd/service/service.go 34.48% 17 Missing and 2 partials ⚠️
internal/auth/transport.go 63.33% 10 Missing and 1 partial ⚠️
... and 15 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #984      +/-   ##
==========================================
+ Coverage   67.72%   67.76%   +0.03%     
==========================================
  Files         575      609      +34     
  Lines       54360    56133    +1773     
==========================================
+ Hits        36817    38040    +1223     
- Misses      14496    14912     +416     
- Partials     3047     3181     +134     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/auth/uat_client.go (1)

250-267: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reclassify the second refresh response before fallback token-clear.

On Line 260-Line 267, after the retry call you only check “failed or not” and then clear token. If the second response is a policy block, the function drops challenge_url/typed policy context and returns nil, which breaks the policy carve-out behavior.

Suggested patch
 			code = getInt(data, "code", -1)
 			errStr = getStr(data, "error")
 			if (code != -1 && code != 0) || errStr != "" {
+				if meta2, ok2 := errclass.LookupCodeMeta(code); ok2 && meta2.Category == errs.CategoryPolicy {
+					return nil, &errs.SecurityPolicyError{
+						Problem: errs.Problem{
+							Category: errs.CategoryPolicy,
+							Subtype:  meta2.Subtype,
+							Code:     code,
+							Message:  getStr(data, "error_description"),
+							Hint:     getStr(data, "cli_hint"),
+						},
+						ChallengeURL: getStr(data, "challenge_url"),
+					}
+				}
 				fmt.Fprintf(errOut, "[lark-cli] [WARN] uat-client: refresh failed after retry (code=%d) for %s, clearing token\n", code, opts.UserOpenId)
 				if err := RemoveStoredToken(opts.AppId, opts.UserOpenId); err != nil {
 					fmt.Fprintf(errOut, "[lark-cli] [WARN] uat-client: failed to remove token: %v\n", err)
 				}
 				return nil, nil
 			}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/auth/uat_client.go` around lines 250 - 267, After the retry call in
the refresh path (inside the metaOK && meta.Category ==
errs.CategoryAuthentication && meta.Retryable block around callEndpoint and
RemoveStoredToken), reclassify the second response before deciding to clear the
token: parse the returned data into the same error/meta representation you used
originally (i.e., recompute meta/category from data after callEndpoint), and if
the reclassified meta indicates a policy block (errs.CategoryPolicy or a
presence of challenge_url/typed policy context) return the data so the caller
can handle the policy flow instead of clearing the token; only clear the stored
token when the reclassified response is still an authentication
failure/retryable auth error. Ensure you reference the existing
variables/functions used there (callEndpoint, code/getInt, errStr/getStr, meta,
RemoveStoredToken) so you reuse the same classification logic.
♻️ Duplicate comments (2)
cmd/auth/scopes.go (1)

57-65: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve getAppInfo's original typed failures instead of always converting to PermissionError.

This branch rewrites every failure into *errs.PermissionError, so non-permission failures (e.g., network errors, authentication failures, internal errors) lose their original subtype and produce incorrect exit codes/hints. For example, a network timeout would incorrectly tell the user to "ensure the app has enabled the application:application:self_manage scope."

🐛 Proposed fix — preserve original error types
 	appInfo, err := getAppInfo(opts.Ctx, f, config.AppID)
 	if err != nil {
+		// Preserve typed errors (network, auth, etc.) — only wrap unknown
+		// errors as permission failures when we're confident the cause is
+		// scope-related.
+		var perm *errs.PermissionError
+		if errors.As(err, &perm) {
+			if perm.ConsoleURL == "" {
+				perm.ConsoleURL = errclass.ConsoleURL(string(config.Brand), config.AppID, nil)
+			}
+			return perm
+		}
+		// If it's already a typed error, return as-is
+		if _, ok := errs.UnwrapTypedError(err); ok {
+			return err
+		}
+		// Only wrap truly unknown errors as permission failures
 		return &errs.PermissionError{
 			Problem: errs.Problem{
 				Category: errs.CategoryAuthorization,

Add the "errors" import at the top of the file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/auth/scopes.go` around lines 57 - 65, The code currently converts all
failures from getAppInfo into a new *errs.PermissionError; instead, detect and
preserve typed errors from getAppInfo (use errors.As or errors.Is) and only
construct a PermissionError when the underlying error is not already an errs.*
type that should be propagated; add the "errors" import, attempt errors.As(err,
&var) to see if err is already an *errs.PermissionError (or other errs.* types)
and return it directly, otherwise build and return the PermissionError with the
same message/hint/ConsoleURL as before.
internal/output/errors.go (1)

202-204: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not report "written" when typed envelope encoding fails.

Returning true here signals success to the caller, but encoding actually failed and nothing was written. This can suppress fallback handling while emitting nothing to the user.

🐛 Proposed fix
 	if encErr := enc.Encode(env); encErr != nil {
-		return true // pretend written; we still consumed the typed error
+		return false
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/output/errors.go` around lines 202 - 204, The code currently returns
true when enc.Encode(env) fails, incorrectly signaling success; change the
branch handling enc.Encode(env) in the envelope write/emit function (the block
that checks "if encErr := enc.Encode(env); encErr != nil") to return false
(and/or propagate the actual error up if the surrounding function supports an
error return) so failure to encode does not claim the envelope was written and
callers can perform fallback/error handling.
🧹 Nitpick comments (2)
shortcuts/task/task_body_test.go (1)

71-80: ⚡ Quick win

wantSubstr expectations are currently not asserted.

Line 71 and Line 138 validate code/category, but the table’s wantSubstr is never checked, so message regressions won’t be caught.

Proposed patch
@@
 			if string(p.Category) != tt.wantType {
 				t.Errorf("error type = %q, want %q (err = %v)", p.Category, tt.wantType, err)
 			}
+			if tt.wantSubstr != "" && !strings.Contains(err.Error(), tt.wantSubstr) {
+				t.Errorf("error = %q, want substring %q", err.Error(), tt.wantSubstr)
+			}
 		})
 	}
 }
@@
 			if string(p.Category) != tt.wantType {
 				t.Errorf("error type = %q, want %q (err = %v)", p.Category, tt.wantType, err)
 			}
+			if tt.wantSubstr != "" && !strings.Contains(err.Error(), tt.wantSubstr) {
+				t.Errorf("error = %q, want substring %q", err.Error(), tt.wantSubstr)
+			}
 		})
 	}
 }

Also applies to: 138-147

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shortcuts/task/task_body_test.go` around lines 71 - 80, Tests currently check
ExitCodeOf(err) and errs.ProblemOf(err)/p.Category but never assert the table's
tt.wantSubstr, so add an assertion that the error message contains tt.wantSubstr
after the existing checks: use err.Error() (or fmt.Sprintf("%v", err)) and
t.Errorf when tt.wantSubstr is non-empty and not a substring. Apply the same
change in the other test block that uses output.ExitCodeOf and errs.ProblemOf
(the block around lines 138-147) so both places assert the message contains
tt.wantSubstr.
internal/lintcheck/rules.go (1)

259-259: 💤 Low value

Consider compiling the regex once at package level.

The ad_hoc_* regex is recompiled on every call to scanSubtype. For a linting tool that processes many files, this adds unnecessary overhead.

♻️ Proposed refactor
+var adHocRe = regexp.MustCompile(`^ad_hoc_[a-z0-9_]+$`)
+
 func scanSubtype(path, src string, allowlist, nameset map[string]struct{}, scope *TypedScope, absPath string) ([]Violation, error) {
 	fset := token.NewFileSet()
 	file, err := parser.ParseFile(fset, path, src, parser.ParseComments)
 	if err != nil {
 		return nil, err
 	}
-	adHoc := regexp.MustCompile(`^ad_hoc_[a-z0-9_]+$`)
+	adHoc := adHocRe
 	var out []Violation
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/lintcheck/rules.go` at line 259, The regex
`regexp.MustCompile(\`^ad_hoc_[a-z0-9_]+$\`)` is being compiled inside
scanSubtype on every call; move it to a package-level variable (e.g., declare
var adHoc = regexp.MustCompile(`^ad_hoc_[a-z0-9_]+$`) at top-level) and update
scanSubtype to reuse that adHoc variable so the pattern is compiled once for the
whole process.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/service/service.go`:
- Around line 288-291: The call to ac.DoAPI in serviceMethodRun (RunE) returns
raw errors from the credential/auth chain (e.g.,
DefaultTokenProvider.ResolveToken, auth.GetValidAccessToken) which must be
converted to typed output errors; modify the code around the ac.DoAPI call so
any non-nil err is wrapped using the same output.* error types used elsewhere
(for example reuse WrapDoAPIError-style wrapping or create an output-specific
wrapper) before returning from RunE, ensuring resolveAccessToken and any
DoAPI-originated errors never propagate as untyped errors.

In `@errs/predicates.go`:
- Around line 12-17: ProblemOf currently returns ok=true even when the
underlying Problem pointer is nil, causing callers (e.g., CategoryOf,
IsRetryable) to dereference nil; update ProblemOf to check the returned *Problem
from c.ProblemDetail() and only return (p, true) when p != nil, otherwise return
(nil, false). Apply the same defensive pattern to the other similar helper(s)
that call problemCarrier.ProblemDetail() (such as the functions handling
category/retry checks) so they never signal ok=true with a nil *Problem.

In `@errs/problem.go`:
- Around line 29-30: The Error method for type *Problem should guard against a
nil receiver to avoid panics when a nil *Problem is stored in an error
interface; modify (*Problem).Error() to check if p == nil and return an
appropriate string (e.g., "" or "<nil>") instead of dereferencing p.Message,
keeping the rest of the behavior unchanged and leaving ProblemDetail() as-is.

---

Outside diff comments:
In `@internal/auth/uat_client.go`:
- Around line 250-267: After the retry call in the refresh path (inside the
metaOK && meta.Category == errs.CategoryAuthentication && meta.Retryable block
around callEndpoint and RemoveStoredToken), reclassify the second response
before deciding to clear the token: parse the returned data into the same
error/meta representation you used originally (i.e., recompute meta/category
from data after callEndpoint), and if the reclassified meta indicates a policy
block (errs.CategoryPolicy or a presence of challenge_url/typed policy context)
return the data so the caller can handle the policy flow instead of clearing the
token; only clear the stored token when the reclassified response is still an
authentication failure/retryable auth error. Ensure you reference the existing
variables/functions used there (callEndpoint, code/getInt, errStr/getStr, meta,
RemoveStoredToken) so you reuse the same classification logic.

---

Duplicate comments:
In `@cmd/auth/scopes.go`:
- Around line 57-65: The code currently converts all failures from getAppInfo
into a new *errs.PermissionError; instead, detect and preserve typed errors from
getAppInfo (use errors.As or errors.Is) and only construct a PermissionError
when the underlying error is not already an errs.* type that should be
propagated; add the "errors" import, attempt errors.As(err, &var) to see if err
is already an *errs.PermissionError (or other errs.* types) and return it
directly, otherwise build and return the PermissionError with the same
message/hint/ConsoleURL as before.

In `@internal/output/errors.go`:
- Around line 202-204: The code currently returns true when enc.Encode(env)
fails, incorrectly signaling success; change the branch handling enc.Encode(env)
in the envelope write/emit function (the block that checks "if encErr :=
enc.Encode(env); encErr != nil") to return false (and/or propagate the actual
error up if the surrounding function supports an error return) so failure to
encode does not claim the envelope was written and callers can perform
fallback/error handling.

---

Nitpick comments:
In `@internal/lintcheck/rules.go`:
- Line 259: The regex `regexp.MustCompile(\`^ad_hoc_[a-z0-9_]+$\`)` is being
compiled inside scanSubtype on every call; move it to a package-level variable
(e.g., declare var adHoc = regexp.MustCompile(`^ad_hoc_[a-z0-9_]+$`) at
top-level) and update scanSubtype to reuse that adHoc variable so the pattern is
compiled once for the whole process.

In `@shortcuts/task/task_body_test.go`:
- Around line 71-80: Tests currently check ExitCodeOf(err) and
errs.ProblemOf(err)/p.Category but never assert the table's tt.wantSubstr, so
add an assertion that the error message contains tt.wantSubstr after the
existing checks: use err.Error() (or fmt.Sprintf("%v", err)) and t.Errorf when
tt.wantSubstr is non-empty and not a substring. Apply the same change in the
other test block that uses output.ExitCodeOf and errs.ProblemOf (the block
around lines 138-147) so both places assert the message contains tt.wantSubstr.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ee39d5f2-560e-4ec5-a762-b57943afc3b9

📥 Commits

Reviewing files that changed from the base of the PR and between 255fc10 and 24b4a47.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (80)
  • .github/workflows/ci.yml
  • .golangci.yml
  • cmd/api/api.go
  • cmd/api/api_test.go
  • cmd/auth/scopes.go
  • cmd/config/bind_test.go
  • cmd/config/config_test.go
  • cmd/error_auth_hint.go
  • cmd/event/runtime.go
  • cmd/lintcheck/main.go
  • cmd/root.go
  • cmd/root_integration_test.go
  • cmd/root_test.go
  • cmd/service/service.go
  • cmd/service/service_test.go
  • errs/category.go
  • errs/category_test.go
  • errs/doc.go
  • errs/internal_carrier.go
  • errs/marshal_test.go
  • errs/predicates.go
  • errs/predicates_test.go
  • errs/problem.go
  • errs/problem_test.go
  • errs/projection/mcp.go
  • errs/projection/mcp_test.go
  • errs/projection/oauth.go
  • errs/projection/oauth_test.go
  • errs/subtypes.go
  • errs/subtypes_service_task.go
  • errs/types.go
  • errs/types_test.go
  • errs/wrap.go
  • errs/wrap_test.go
  • go.mod
  • internal/auth/errors.go
  • internal/auth/transport.go
  • internal/auth/transport_test.go
  • internal/auth/uat_client.go
  • internal/client/api_errors.go
  • internal/client/api_errors_test.go
  • internal/client/client.go
  • internal/client/client_test.go
  • internal/client/pagination.go
  • internal/client/response.go
  • internal/client/response_test.go
  • internal/cmdutil/json.go
  • internal/core/config.go
  • internal/core/errors.go
  • internal/core/notconfigured.go
  • internal/errclass/classify.go
  • internal/errclass/classify_test.go
  • internal/errclass/codemeta.go
  • internal/errclass/codemeta_task.go
  • internal/errclass/codemeta_test.go
  • internal/errcompat/promote.go
  • internal/errcompat/promote_test.go
  • internal/lintcheck/rules.go
  • internal/lintcheck/rules_test.go
  • internal/lintcheck/scan.go
  • internal/lintcheck/scan_test.go
  • internal/lintcheck/typecheck.go
  • internal/lintcheck/typecheck_test.go
  • internal/output/errors.go
  • internal/output/exitcode.go
  • internal/output/exitcode_test.go
  • internal/output/lark_errors.go
  • shortcuts/base/base_execute_test.go
  • shortcuts/calendar/calendar_test.go
  • shortcuts/common/drive_media_upload.go
  • shortcuts/common/drive_media_upload_test.go
  • shortcuts/common/mcp_client_test.go
  • shortcuts/drive/drive_status_test.go
  • shortcuts/mail/mail_shortcut_validation_test.go
  • shortcuts/markdown/helpers.go
  • shortcuts/markdown/markdown_diff_test.go
  • shortcuts/task/task_body_test.go
  • shortcuts/task/task_query_helpers_test.go
  • shortcuts/task/task_upload_attachment_test.go
  • shortcuts/task/task_util_test.go
💤 Files with no reviewable changes (1)
  • cmd/error_auth_hint.go
✅ Files skipped from review due to trivial changes (6)
  • errs/internal_carrier.go
  • shortcuts/base/base_execute_test.go
  • errs/wrap.go
  • internal/core/errors.go
  • errs/subtypes_service_task.go
  • errs/doc.go

Comment thread cmd/service/service.go
Comment thread errs/predicates.go
Comment thread errs/problem.go Outdated
@evandance evandance force-pushed the feat/error-contract-framework branch from 24b4a47 to 661c2a3 Compare May 20, 2026 07:55
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (3)
cmd/auth/scopes.go (1)

57-65: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve non-authorization failures from getAppInfo instead of always remapping.

This branch rewrites every getAppInfo failure into *errs.PermissionError, which masks original network/auth/internal error types and can break downstream errors.As branching and exit-code expectations. Only map true app-scope authorization failures; return other error types unchanged.

Suggested minimal fix
+import "errors"
@@
 	appInfo, err := getAppInfo(opts.Ctx, f, config.AppID)
 	if err != nil {
-		return &errs.PermissionError{
-			Problem: errs.Problem{
-				Category: errs.CategoryAuthorization,
-				Subtype:  errs.SubtypeAppScopeNotEnabled,
-				Message:  fmt.Sprintf("failed to get app scope info: %v", err),
-				Hint:     "ensure the app has enabled the application:application:self_manage scope.",
-			},
-			ConsoleURL: errclass.ConsoleURL(string(config.Brand), config.AppID, nil),
-		}
+		var perm *errs.PermissionError
+		if errors.As(err, &perm) {
+			if perm.Subtype == errs.SubtypeAppScopeNotEnabled && perm.ConsoleURL == "" {
+				perm.ConsoleURL = errclass.ConsoleURL(string(config.Brand), config.AppID, nil)
+			}
+			return perm
+		}
+		return err
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/auth/scopes.go` around lines 57 - 65, The current code unconditionally
wraps any error from getAppInfo into an errs.PermissionError
(SubtypeAppScopeNotEnabled), which hides non-authorization failures; change the
logic in the caller of getAppInfo (the scope-checking path that currently
returns &errs.PermissionError{...}) to inspect the original error returned by
getAppInfo and only construct and return an errs.PermissionError when the error
clearly indicates the app-scope authorization issue (e.g., match or use an
exported sentinel, error type, or check for a specific error string/condition
that denotes app scope not enabled); for all other errors returned by
getAppInfo, return the original error unchanged so callers can still
errors.As/handle exit codes correctly.
errs/problem.go (1)

29-29: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard nil receivers in (*Problem).Error() to avoid panic.

A nil *Problem stored in an error interface will panic when Error() dereferences p.Message.

Suggested minimal fix
-func (p *Problem) Error() string           { return p.Message }
+func (p *Problem) Error() string {
+	if p == nil {
+		return ""
+	}
+	return p.Message
+}
#!/bin/bash
# Verify current implementation lacks a nil guard in errs/problem.go
rg -n -A4 -B1 'func \(p \*Problem\) Error\(\) string' errs/problem.go
rg -n 'if p == nil' errs/problem.go
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@errs/problem.go` at line 29, The Error method on type (*Problem) should guard
against a nil receiver to avoid panics when a nil *Problem is stored in an error
interface; update the method (func (p *Problem) Error()) to check if p == nil
and return a safe string (e.g. an empty string or "<nil>") instead of
dereferencing p.Message, otherwise return p.Message as before so callers never
panic.
cmd/lintcheck/main.go (1)

41-50: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate the CLI-supplied repo root before scanning.

root is user-controlled input and is passed to lintcheck.ScanRepo without validate.SafeInputPath, which bypasses the repo’s path-safety guardrail for untrusted arguments.

Suggested minimal fix
 import (
 	"flag"
 	"fmt"
 	"os"
 
+	"github.com/larksuite/cli/internal/validate"
 	"github.com/larksuite/cli/internal/lintcheck"
 )
@@
 	if flag.NArg() > 0 {
 		root = flag.Arg(0)
@@
 		}
 	}
+	if err := validate.SafeInputPath(root); err != nil {
+		fmt.Fprintf(os.Stderr, "lintcheck: invalid repo root: %v\n", err)
+		os.Exit(2)
+	}
 
 	violations, err := lintcheck.ScanRepo(root)

As per coding guidelines: Validate paths before reading with validate.SafeInputPath because CLI arguments are untrusted.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/lintcheck/main.go` around lines 41 - 50, The CLI-provided root variable
is untrusted and is passed directly to lintcheck.ScanRepo which bypasses
path-safety checks; validate the path using validate.SafeInputPath (call it on
root after parsing/normalizing "./...") and handle its returned error (log and
exit) before calling lintcheck.ScanRepo so only safe paths are scanned; update
the main function to replace the direct use of root with the validated path and
propagate any validation error to prevent calling lintcheck.ScanRepo with unsafe
input.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/root.go`:
- Around line 203-212: Update the dispatch-order comment to reflect the actual
handler flow: state that errs.SecurityPolicyError is routed to
writeSecurityPolicyError before the typed-envelope path, and adjust the ordering
so that (1) core.ConfigError is promoted/handled as described, (2)
errs.SecurityPolicyError is handled by writeSecurityPolicyError, (3) other typed
errors from errs/ (e.g. *errs.PermissionError, *errs.APIError) are routed to the
typed envelope writer using errs.CategoryOf/ExitCodeOf, and (4) legacy
*output.ExitError is written as-is; mention the preservation of the original
core.ConfigError on the Cause chain as before.

In `@internal/lintcheck/rules.go`:
- Around line 573-576: The service-scope detection in rules.go currently treats
all internal/* packages as service scope except internal/errclass; add an
explicit exclusion for internal/output so it does not count as service scope and
avoids Rule C false positives. Update the branching that checks package path p
(the cases referencing "internal/errclass" and "internal/") to first check for
"internal/output" (e.g., strings.HasPrefix(p, "internal/output/") ||
strings.Contains(p, "/internal/output/")) and return false for that case before
the general internal/* case so internal/output is excluded from isServiceScope
checks.

In `@shortcuts/task/task_body_test.go`:
- Around line 71-80: The test removed assertions that verify the error message
branch (wantSubstr), allowing false positives; restore a check after obtaining p
from errs.ProblemOf(err) to assert that the produced problem message (e.g.,
p.Message or string(err) depending on how the test reads the message) contains
tt.wantSubstr, using the same pattern in both places (the block around
output.ExitCodeOf/errs.ProblemOf and the similar block at the later occurrence).
Ensure you reference output.ExitCodeOf(err), errs.ProblemOf(err), and p.Category
when adding the assertion so the test fails if the wrong validation branch is
returned even when category/code remains "validation".

In `@shortcuts/task/task_util_test.go`:
- Around line 47-56: The test removed the assertion that the returned error
message contains the expected substring (wantSubstr), so restore an assertion
after the typed error check that verifies the error text includes tt.wantSubstr
(e.g., using strings.Contains(err.Error(), tt.wantSubstr) or
strings.Contains(p.Message, tt.wantSubstr) depending on the Problem type); add
the strings import if needed and fail the test with t.Errorf or t.Fatalf when
the substring is missing so branch-level validation reasons are covered.

---

Duplicate comments:
In `@cmd/auth/scopes.go`:
- Around line 57-65: The current code unconditionally wraps any error from
getAppInfo into an errs.PermissionError (SubtypeAppScopeNotEnabled), which hides
non-authorization failures; change the logic in the caller of getAppInfo (the
scope-checking path that currently returns &errs.PermissionError{...}) to
inspect the original error returned by getAppInfo and only construct and return
an errs.PermissionError when the error clearly indicates the app-scope
authorization issue (e.g., match or use an exported sentinel, error type, or
check for a specific error string/condition that denotes app scope not enabled);
for all other errors returned by getAppInfo, return the original error unchanged
so callers can still errors.As/handle exit codes correctly.

In `@cmd/lintcheck/main.go`:
- Around line 41-50: The CLI-provided root variable is untrusted and is passed
directly to lintcheck.ScanRepo which bypasses path-safety checks; validate the
path using validate.SafeInputPath (call it on root after parsing/normalizing
"./...") and handle its returned error (log and exit) before calling
lintcheck.ScanRepo so only safe paths are scanned; update the main function to
replace the direct use of root with the validated path and propagate any
validation error to prevent calling lintcheck.ScanRepo with unsafe input.

In `@errs/problem.go`:
- Line 29: The Error method on type (*Problem) should guard against a nil
receiver to avoid panics when a nil *Problem is stored in an error interface;
update the method (func (p *Problem) Error()) to check if p == nil and return a
safe string (e.g. an empty string or "<nil>") instead of dereferencing
p.Message, otherwise return p.Message as before so callers never panic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e28fc312-4b4c-4fca-9c42-735cfc58ed34

📥 Commits

Reviewing files that changed from the base of the PR and between 24b4a47 and 661c2a3.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (81)
  • .github/workflows/ci.yml
  • .golangci.yml
  • cmd/api/api.go
  • cmd/api/api_test.go
  • cmd/auth/scopes.go
  • cmd/config/bind_test.go
  • cmd/config/config_test.go
  • cmd/error_auth_hint.go
  • cmd/event/runtime.go
  • cmd/lintcheck/main.go
  • cmd/root.go
  • cmd/root_integration_test.go
  • cmd/root_test.go
  • cmd/service/service.go
  • cmd/service/service_test.go
  • errs/category.go
  • errs/category_test.go
  • errs/doc.go
  • errs/internal_carrier.go
  • errs/marshal_test.go
  • errs/predicates.go
  • errs/predicates_test.go
  • errs/problem.go
  • errs/problem_test.go
  • errs/projection/mcp.go
  • errs/projection/mcp_test.go
  • errs/projection/oauth.go
  • errs/projection/oauth_test.go
  • errs/subtypes.go
  • errs/subtypes_service_task.go
  • errs/types.go
  • errs/types_test.go
  • errs/wrap.go
  • errs/wrap_test.go
  • go.mod
  • internal/auth/errors.go
  • internal/auth/transport.go
  • internal/auth/transport_test.go
  • internal/auth/uat_client.go
  • internal/client/api_errors.go
  • internal/client/api_errors_test.go
  • internal/client/client.go
  • internal/client/client_test.go
  • internal/client/pagination.go
  • internal/client/response.go
  • internal/client/response_test.go
  • internal/cmdutil/json.go
  • internal/core/config.go
  • internal/core/errors.go
  • internal/core/notconfigured.go
  • internal/errclass/classify.go
  • internal/errclass/classify_test.go
  • internal/errclass/codemeta.go
  • internal/errclass/codemeta_task.go
  • internal/errclass/codemeta_test.go
  • internal/errcompat/promote.go
  • internal/errcompat/promote_test.go
  • internal/lintcheck/rules.go
  • internal/lintcheck/rules_test.go
  • internal/lintcheck/scan.go
  • internal/lintcheck/scan_test.go
  • internal/lintcheck/typecheck.go
  • internal/lintcheck/typecheck_test.go
  • internal/output/errors.go
  • internal/output/exitcode.go
  • internal/output/exitcode_test.go
  • internal/output/lark_errors.go
  • shortcuts/base/base_execute_test.go
  • shortcuts/calendar/calendar_test.go
  • shortcuts/common/drive_media_upload.go
  • shortcuts/common/drive_media_upload_test.go
  • shortcuts/common/mcp_client_test.go
  • shortcuts/drive/drive_status_test.go
  • shortcuts/mail/mail_shortcut_validation_test.go
  • shortcuts/markdown/helpers.go
  • shortcuts/markdown/markdown_diff_test.go
  • shortcuts/task/task_body_test.go
  • shortcuts/task/task_query_helpers_test.go
  • shortcuts/task/task_upload_attachment_test.go
  • shortcuts/task/task_util_test.go
  • tests/cli_e2e/config/bind_test.go
💤 Files with no reviewable changes (1)
  • cmd/error_auth_hint.go
✅ Files skipped from review due to trivial changes (3)
  • errs/subtypes_service_task.go
  • internal/core/errors.go
  • shortcuts/base/base_execute_test.go

Comment thread cmd/root.go Outdated
Comment thread internal/lintcheck/rules.go Outdated
Comment thread shortcuts/task/task_body_test.go Outdated
Comment thread shortcuts/task/task_util_test.go Outdated
@evandance evandance force-pushed the feat/error-contract-framework branch 2 times, most recently from 6fbf14c to 4dd7ea7 Compare May 20, 2026 08:18
@evandance evandance added domain/core CLI framework and core libraries and removed domain/base PR touches the base domain domain/calendar PR touches the calendar domain domain/mail PR touches the mail domain domain/task PR touches the task domain domain/ccm PR touches the ccm domain labels May 20, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
internal/auth/transport.go (1)

179-180: ⚡ Quick win

Add focused tests for the new policy-category branch and typed return path.

These new lines are currently uncovered; add cases that verify (1) non-policy codes are ignored, and (2) policy codes produce *errs.SecurityPolicyError with expected Problem fields and ChallengeURL.

Also applies to: 206-213

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/auth/transport.go` around lines 179 - 180, Add unit tests covering
the new branch in the error-to-transport translation that uses
errclass.LookupCodeMeta: write one test where LookupCodeMeta returns ok but
meta.Category != errs.CategoryPolicy and assert the code path ignores policy
handling (original error or non-policy behavior); and a second test where
LookupCodeMeta returns ok with meta.Category == errs.CategoryPolicy and the
function returns a *errs.SecurityPolicyError with the expected Problem fields
and ChallengeURL populated. Use the same function under test that calls
errclass.LookupCodeMeta (referenced in the diff) and assert types and field
values on the returned error to exercise the typed return path.
cmd/root.go (1)

230-233: ⚡ Quick win

Add focused tests for the newly added root-dispatch branches.

The new config-promotion path and asExitError nil-return path are currently uncovered, and both sit on the root error contract boundary. A small regression test here would materially reduce dispatch drift risk.

Also applies to: 292-297

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/root.go` around lines 230 - 233, Add focused unit tests for the new root
dispatch branches: write one test that simulates an error that satisfies
errors.As(..., *core.ConfigError) and verifies that root dispatch calls
errcompat.PromoteConfigError (and the resulting behavior/exit code) so the
config-promotion path is covered, and write another test where asExitError
returns nil to exercise the nil-return path on the root error contract boundary
and assert the dispatcher handles it as expected; target the code paths around
the root dispatch logic (the errors.As(core.ConfigError) branch, the call to
errcompat.PromoteConfigError, and the asExitError nil-return behavior) so these
two branches (mentioned in root.go) have explicit assertions.
internal/auth/uat_client.go (1)

228-244: ⚡ Quick win

Add focused tests for policy-code metadata mapping in refresh flow.

This new branch changes observable behavior (returns *errs.SecurityPolicyError with Subtype/ChallengeURL) but is currently uncovered. A dedicated unit test here would lock the contract and prevent regressions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/auth/uat_client.go` around lines 228 - 244, Add a focused unit test
that exercises the refresh flow branch where errclass.LookupCodeMeta(code)
returns metaOK and meta.Category == errs.CategoryPolicy and verifies the
returned error is a *errs.SecurityPolicyError with Problem.Subtype set to the
meta.Subtype and ChallengeURL set from the "challenge_url" field; to do this,
arrange the test to inject an error response payload containing "challenge_url",
"cli_hint", and "error_description", stub or ensure
errclass.LookupCodeMeta(code) returns a meta with CategoryPolicy and a known
Subtype, invoke the refresh method in the UAT client (the code path that
contains the meta, metaOK := errclass.LookupCodeMeta(code) block), and assert
the error type and that Problem.Subtype, Problem.Code, Problem.Message,
Problem.Hint and ChallengeURL match the expected values to lock the contract and
prevent regressions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/service/service.go`:
- Line 520: The call to checkErr is using request.As which can mismatch the
computed identity; change checkErr to use the effective identity stored in
pagOpts.Identity (set by servicePaginate) instead of request.As so
classification/hints use the correct identity. Locate the calls to checkErr in
the servicePaginate flow (the one currently passing request.As around line with
servicePaginate and the other at the later call) and replace the request.As
argument with pagOpts.Identity (or the variable holding that value) so both
error checks use the initialized pagOpts.Identity.

In `@internal/lintcheck/scan_test.go`:
- Around line 291-299: The local variable rejectCount declared and incremented
inside the loop over v is never used and causes a compilation error; remove the
declaration "rejectCount := 0" and the increment "if vv.Action ==
lintcheck.ActionReject { rejectCount++ }" (or alternatively use rejectCount in
an assertion) so only used variables remain—look for the loop iterating over v
and the rejectCount symbol in scan_test.go and delete those two lines to fix the
test build.

---

Nitpick comments:
In `@cmd/root.go`:
- Around line 230-233: Add focused unit tests for the new root dispatch
branches: write one test that simulates an error that satisfies errors.As(...,
*core.ConfigError) and verifies that root dispatch calls
errcompat.PromoteConfigError (and the resulting behavior/exit code) so the
config-promotion path is covered, and write another test where asExitError
returns nil to exercise the nil-return path on the root error contract boundary
and assert the dispatcher handles it as expected; target the code paths around
the root dispatch logic (the errors.As(core.ConfigError) branch, the call to
errcompat.PromoteConfigError, and the asExitError nil-return behavior) so these
two branches (mentioned in root.go) have explicit assertions.

In `@internal/auth/transport.go`:
- Around line 179-180: Add unit tests covering the new branch in the
error-to-transport translation that uses errclass.LookupCodeMeta: write one test
where LookupCodeMeta returns ok but meta.Category != errs.CategoryPolicy and
assert the code path ignores policy handling (original error or non-policy
behavior); and a second test where LookupCodeMeta returns ok with meta.Category
== errs.CategoryPolicy and the function returns a *errs.SecurityPolicyError with
the expected Problem fields and ChallengeURL populated. Use the same function
under test that calls errclass.LookupCodeMeta (referenced in the diff) and
assert types and field values on the returned error to exercise the typed return
path.

In `@internal/auth/uat_client.go`:
- Around line 228-244: Add a focused unit test that exercises the refresh flow
branch where errclass.LookupCodeMeta(code) returns metaOK and meta.Category ==
errs.CategoryPolicy and verifies the returned error is a
*errs.SecurityPolicyError with Problem.Subtype set to the meta.Subtype and
ChallengeURL set from the "challenge_url" field; to do this, arrange the test to
inject an error response payload containing "challenge_url", "cli_hint", and
"error_description", stub or ensure errclass.LookupCodeMeta(code) returns a meta
with CategoryPolicy and a known Subtype, invoke the refresh method in the UAT
client (the code path that contains the meta, metaOK :=
errclass.LookupCodeMeta(code) block), and assert the error type and that
Problem.Subtype, Problem.Code, Problem.Message, Problem.Hint and ChallengeURL
match the expected values to lock the contract and prevent regressions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2cc6200f-1045-49c8-a66b-0f20f8c12625

📥 Commits

Reviewing files that changed from the base of the PR and between 661c2a3 and 4dd7ea7.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (81)
  • .github/workflows/ci.yml
  • .golangci.yml
  • cmd/api/api.go
  • cmd/api/api_test.go
  • cmd/auth/scopes.go
  • cmd/config/bind_test.go
  • cmd/config/config_test.go
  • cmd/error_auth_hint.go
  • cmd/event/runtime.go
  • cmd/lintcheck/main.go
  • cmd/root.go
  • cmd/root_integration_test.go
  • cmd/root_test.go
  • cmd/service/service.go
  • cmd/service/service_test.go
  • errs/category.go
  • errs/category_test.go
  • errs/doc.go
  • errs/internal_carrier.go
  • errs/marshal_test.go
  • errs/predicates.go
  • errs/predicates_test.go
  • errs/problem.go
  • errs/problem_test.go
  • errs/projection/mcp.go
  • errs/projection/mcp_test.go
  • errs/projection/oauth.go
  • errs/projection/oauth_test.go
  • errs/subtypes.go
  • errs/subtypes_service_task.go
  • errs/types.go
  • errs/types_test.go
  • errs/wrap.go
  • errs/wrap_test.go
  • go.mod
  • internal/auth/errors.go
  • internal/auth/transport.go
  • internal/auth/transport_test.go
  • internal/auth/uat_client.go
  • internal/client/api_errors.go
  • internal/client/api_errors_test.go
  • internal/client/client.go
  • internal/client/client_test.go
  • internal/client/pagination.go
  • internal/client/response.go
  • internal/client/response_test.go
  • internal/cmdutil/json.go
  • internal/core/config.go
  • internal/core/errors.go
  • internal/core/notconfigured.go
  • internal/errclass/classify.go
  • internal/errclass/classify_test.go
  • internal/errclass/codemeta.go
  • internal/errclass/codemeta_task.go
  • internal/errclass/codemeta_test.go
  • internal/errcompat/promote.go
  • internal/errcompat/promote_test.go
  • internal/lintcheck/rules.go
  • internal/lintcheck/rules_test.go
  • internal/lintcheck/scan.go
  • internal/lintcheck/scan_test.go
  • internal/lintcheck/typecheck.go
  • internal/lintcheck/typecheck_test.go
  • internal/output/errors.go
  • internal/output/exitcode.go
  • internal/output/exitcode_test.go
  • internal/output/lark_errors.go
  • shortcuts/base/base_execute_test.go
  • shortcuts/calendar/calendar_test.go
  • shortcuts/common/drive_media_upload.go
  • shortcuts/common/drive_media_upload_test.go
  • shortcuts/common/mcp_client_test.go
  • shortcuts/drive/drive_status_test.go
  • shortcuts/mail/mail_shortcut_validation_test.go
  • shortcuts/markdown/helpers.go
  • shortcuts/markdown/markdown_diff_test.go
  • shortcuts/task/task_body_test.go
  • shortcuts/task/task_query_helpers_test.go
  • shortcuts/task/task_upload_attachment_test.go
  • shortcuts/task/task_util_test.go
  • tests/cli_e2e/config/bind_test.go
💤 Files with no reviewable changes (1)
  • cmd/error_auth_hint.go
✅ Files skipped from review due to trivial changes (3)
  • errs/doc.go
  • errs/internal_carrier.go
  • shortcuts/base/base_execute_test.go

Comment thread cmd/service/service.go Outdated
Comment thread internal/lintcheck/scan_test.go Outdated
@evandance evandance force-pushed the feat/error-contract-framework branch from 4dd7ea7 to 186e8da Compare May 20, 2026 08:38
@github-actions github-actions Bot added domain/base PR touches the base domain domain/calendar PR touches the calendar domain domain/ccm PR touches the ccm domain domain/mail PR touches the mail domain domain/task PR touches the task domain labels May 20, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
internal/lintcheck/scan_test.go (1)

278-283: 💤 Low value

Misleading comment contradicts fixture content.

The comment states "the const block is empty so no values/names are declared," but the fixture at line 282 actually declares const SubtypeKnown Subtype = "known". The test likely still passes because LoadTypedScope fails for another reason (missing internal/errclass/codemeta.go), but the comment should accurately reflect the test's setup.

♻️ Suggested comment fix
-		// subtypes.go is present so LoadSubtypeAllowlists succeeds, but the
-		// const block is empty so no values/names are declared.
+		// subtypes.go is present so LoadSubtypeAllowlists succeeds. LoadTypedScope
+		// fails because internal/errclass/codemeta.go is missing from this fixture.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/lintcheck/scan_test.go` around lines 278 - 283, Update the
misleading test comment that claims "the const block is empty so no values/names
are declared" to accurately describe the fixture in the scan_test.go snippet:
note that the errs/subtypes.go fixture declares a const SubtypeKnown Subtype =
"known", so adjust the comment near the errs/subtypes.go entry (in scan_test.go
around the LoadTypedScope/LoadSubtypeAllowlists setup) to reflect that
SubtypeKnown is declared rather than claiming the const block is empty.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/root_test.go`:
- Around line 150-166: In TestHandleRootError_SecurityPolicyKeepsLegacyEnvelope
before calling cmdutil.TestFactory, isolate the CLI config by calling
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()); this ensures TestFactory (and
its f.IOStreams) uses a per-test config directory and prevents cross-test state
bleed; update the test function
(TestHandleRootError_SecurityPolicyKeepsLegacyEnvelope) to set the env var
immediately at the start of the t.Run body before creating f via
cmdutil.TestFactory.

In `@errs/CONTRACT.md`:
- Around line 168-169: The quick-reference example for errs.InternalError
incorrectly places Subtype at the top level; update the example to show Subtype
as a field inside the Problem object (e.g., errs.InternalError{Problem: ...,
where Problem contains Subtype: SubtypeSDKFailure}) so the shape matches the
contract; locate the example string using the symbol errs.InternalError and
adjust the structure to nest Subtype under Problem accordingly.

---

Nitpick comments:
In `@internal/lintcheck/scan_test.go`:
- Around line 278-283: Update the misleading test comment that claims "the const
block is empty so no values/names are declared" to accurately describe the
fixture in the scan_test.go snippet: note that the errs/subtypes.go fixture
declares a const SubtypeKnown Subtype = "known", so adjust the comment near the
errs/subtypes.go entry (in scan_test.go around the
LoadTypedScope/LoadSubtypeAllowlists setup) to reflect that SubtypeKnown is
declared rather than claiming the const block is empty.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 195e3ed9-43ec-49b4-9d71-ee4650faf445

📥 Commits

Reviewing files that changed from the base of the PR and between bfc705f and 002d1d2.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (86)
  • .github/workflows/ci.yml
  • .golangci.yml
  • cmd/api/api.go
  • cmd/api/api_test.go
  • cmd/auth/scopes.go
  • cmd/config/bind_test.go
  • cmd/config/config_test.go
  • cmd/error_auth_hint.go
  • cmd/event/runtime.go
  • cmd/lintcheck/main.go
  • cmd/root.go
  • cmd/root_integration_test.go
  • cmd/root_test.go
  • cmd/service/service.go
  • cmd/service/service_test.go
  • errs/CONTRACT.md
  • errs/category.go
  • errs/category_test.go
  • errs/doc.go
  • errs/internal_carrier.go
  • errs/marshal_test.go
  • errs/predicates.go
  • errs/predicates_test.go
  • errs/problem.go
  • errs/problem_test.go
  • errs/projection/mcp.go
  • errs/projection/mcp_test.go
  • errs/projection/oauth.go
  • errs/projection/oauth_test.go
  • errs/subtypes.go
  • errs/subtypes_service_task.go
  • errs/types.go
  • errs/types_test.go
  • errs/wrap.go
  • errs/wrap_test.go
  • go.mod
  • internal/auth/errors.go
  • internal/auth/transport.go
  • internal/auth/transport_test.go
  • internal/auth/uat_client.go
  • internal/client/api_errors.go
  • internal/client/api_errors_test.go
  • internal/client/client.go
  • internal/client/client_test.go
  • internal/client/pagination.go
  • internal/client/response.go
  • internal/client/response_test.go
  • internal/cmdutil/json.go
  • internal/core/config.go
  • internal/core/errors.go
  • internal/core/notconfigured.go
  • internal/errclass/classify.go
  • internal/errclass/classify_test.go
  • internal/errclass/codemeta.go
  • internal/errclass/codemeta_task.go
  • internal/errclass/codemeta_test.go
  • internal/errcompat/promote.go
  • internal/errcompat/promote_test.go
  • internal/lintcheck/rules.go
  • internal/lintcheck/rules_test.go
  • internal/lintcheck/scan.go
  • internal/lintcheck/scan_test.go
  • internal/lintcheck/typecheck.go
  • internal/lintcheck/typecheck_test.go
  • internal/output/envelope.go
  • internal/output/errors.go
  • internal/output/errors_test.go
  • internal/output/exitcode.go
  • internal/output/exitcode_test.go
  • internal/output/lark_errors.go
  • shortcuts/base/base_execute_test.go
  • shortcuts/calendar/calendar_test.go
  • shortcuts/common/drive_media_upload.go
  • shortcuts/common/drive_media_upload_test.go
  • shortcuts/common/mcp_client_test.go
  • shortcuts/drive/drive_search.go
  • shortcuts/drive/drive_status_test.go
  • shortcuts/mail/mail_shortcut_validation_test.go
  • shortcuts/markdown/helpers.go
  • shortcuts/markdown/markdown_diff_test.go
  • shortcuts/sheets/lark_sheets_sheet_management.go
  • shortcuts/task/task_body_test.go
  • shortcuts/task/task_query_helpers_test.go
  • shortcuts/task/task_upload_attachment_test.go
  • shortcuts/task/task_util_test.go
  • tests/cli_e2e/config/bind_test.go
💤 Files with no reviewable changes (3)
  • shortcuts/sheets/lark_sheets_sheet_management.go
  • internal/output/errors_test.go
  • shortcuts/drive/drive_search.go
✅ Files skipped from review due to trivial changes (7)
  • go.mod
  • errs/internal_carrier.go
  • internal/output/envelope.go
  • internal/core/errors.go
  • errs/doc.go
  • internal/core/notconfigured.go
  • shortcuts/base/base_execute_test.go

Comment thread cmd/root_test.go
Comment thread errs/CONTRACT.md Outdated
@evandance evandance force-pushed the feat/error-contract-framework branch from 002d1d2 to 80cce5f Compare May 20, 2026 10:17
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/auth/uat_client.go (1)

250-267: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reclassify the retry response before deciding to clear token.

After retry, the code updates code/errStr but keeps the original meta. A retry that returns a policy code is currently treated as generic failure (token cleared, nil returned), so the caller loses the structured SecurityPolicyError challenge details.

💡 Suggested fix
 			code = getInt(data, "code", -1)
 			errStr = getStr(data, "error")
+			meta, metaOK = errclass.LookupCodeMeta(code)
+			if metaOK && meta.Category == errs.CategoryPolicy {
+				challengeUrl := getStr(data, "challenge_url")
+				cliHint := getStr(data, "cli_hint")
+				msg := getStr(data, "error_description")
+				return nil, &errs.SecurityPolicyError{
+					Problem: errs.Problem{
+						Category: errs.CategoryPolicy,
+						Subtype:  meta.Subtype,
+						Code:     code,
+						Message:  msg,
+						Hint:     cliHint,
+					},
+					ChallengeURL: challengeUrl,
+				}
+			}
 			if (code != -1 && code != 0) || errStr != "" {
 				fmt.Fprintf(errOut, "[lark-cli] [WARN] uat-client: refresh failed after retry (code=%d) for %s, clearing token\n", code, opts.UserOpenId)
 				if err := RemoveStoredToken(opts.AppId, opts.UserOpenId); err != nil {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/auth/uat_client.go` around lines 250 - 267, After performing the
retry (after callEndpoint() and after updating code and errStr via
getInt/getStr), recompute the response metadata the same way the original call
did (i.e. re-run the logic that set meta and metaOK originally) and use that
updated meta to decide whether to clear the token; specifically, if the new meta
indicates a SecurityPolicyError (meta.Category == errs.CategoryAuthentication
and the structured policy challenge case) then return the structured
SecurityPolicyError (don't clear the token), otherwise proceed to clear via
RemoveStoredToken(opts.AppId, opts.UserOpenId) and return nil,nil as before.
Ensure you reference the same meta/metaOK variables and the
callEndpoint()/getInt/getStr results when rebuilding meta so the post-retry
decision uses the fresh classification.
🧹 Nitpick comments (2)
internal/lintcheck/typecheck.go (1)

88-91: 💤 Low value

Consider logging package-level errors from packages.Load.

packages.Load may return a non-nil pkgs slice even when individual packages have errors (stored in pkg.Errors). These errors are silently ignored, which could cause subtle issues if the errs package fails to type-check (e.g., syntax error in a new commit). The scope would then be "disabled" with no diagnostic.

Consider iterating pkgs and logging any pkg.Errors at debug level, or at least checking that the errs package loaded without errors before marking the scope enabled.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/lintcheck/typecheck.go` around lines 88 - 91, packages.Load can
return pkgs with per-package errors in pkg.Errors that are currently ignored;
iterate the returned pkgs slice after packages.Load (the pkgs variable) and for
each pkg check pkg.Errors and log them (at debug level) via your logger or
return an error if the special "errs" package failed to load/type-check before
you mark the scope enabled; specifically update the code around packages.Load to
loop through pkgs, inspect pkg.Errors, emit debug logs for each error (or bail
if the errs package is among the failed packages), and only enable the scope
once the errs package is confirmed present with no pkg.Errors.
errs/marshal_test.go (1)

63-64: ⚡ Quick win

Check json.Marshal errors in tests instead of discarding them.

Several cases use b, _ := json.Marshal(...). If marshal fails, assertions can become misleading rather than fail at the root cause. Please assert err == nil in each case.

Proposed pattern
- b, _ := json.Marshal(ve)
+ b, err := json.Marshal(ve)
+ if err != nil {
+   t.Fatalf("json.Marshal failed: %v", err)
+ }
  s := string(b)

Also applies to: 78-79, 89-90, 108-109, 122-123, 147-148, 164-165, 175-176, 193-194, 209-210, 224-225

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@errs/marshal_test.go` around lines 63 - 64, Tests call json.Marshal and
discard the error (e.g., b, _ := json.Marshal(ve) followed by s := string(b));
change each occurrence to capture the error (b, err := json.Marshal(ve)) and
assert err == nil using the test helper in scope (e.g., require.NoError(t, err)
or t.Fatalf) before using s, updating every listed site (lines shown and the
other occurrences at 78-79, 89-90, 108-109, 122-123, 147-148, 164-165, 175-176,
193-194, 209-210, 224-225) so that marshal failures fail the test immediately
rather than producing misleading assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/lintcheck/scan.go`:
- Around line 196-199: LoadSubtypeAllowlists currently returns a generic
fmt.Errorf when no subtypes*.go files are found, which prevents ScanRepo from
recognizing the "missing allowlist → skip Rule E" path (it only checks
os.IsNotExist). Change LoadSubtypeAllowlists so that when found == 0 it returns
os.ErrNotExist (or an error wrapped with errors.Is(os.ErrNotExist) true) instead
of fmt.Errorf("no subtypes*.go found under %s", errsDir), so callers like
ScanRepo can detect and skip Rule E; update the return values of
LoadSubtypeAllowlists accordingly and adjust any tests if they expect the old
error string.

---

Outside diff comments:
In `@internal/auth/uat_client.go`:
- Around line 250-267: After performing the retry (after callEndpoint() and
after updating code and errStr via getInt/getStr), recompute the response
metadata the same way the original call did (i.e. re-run the logic that set meta
and metaOK originally) and use that updated meta to decide whether to clear the
token; specifically, if the new meta indicates a SecurityPolicyError
(meta.Category == errs.CategoryAuthentication and the structured policy
challenge case) then return the structured SecurityPolicyError (don't clear the
token), otherwise proceed to clear via RemoveStoredToken(opts.AppId,
opts.UserOpenId) and return nil,nil as before. Ensure you reference the same
meta/metaOK variables and the callEndpoint()/getInt/getStr results when
rebuilding meta so the post-retry decision uses the fresh classification.

---

Nitpick comments:
In `@errs/marshal_test.go`:
- Around line 63-64: Tests call json.Marshal and discard the error (e.g., b, _
:= json.Marshal(ve) followed by s := string(b)); change each occurrence to
capture the error (b, err := json.Marshal(ve)) and assert err == nil using the
test helper in scope (e.g., require.NoError(t, err) or t.Fatalf) before using s,
updating every listed site (lines shown and the other occurrences at 78-79,
89-90, 108-109, 122-123, 147-148, 164-165, 175-176, 193-194, 209-210, 224-225)
so that marshal failures fail the test immediately rather than producing
misleading assertions.

In `@internal/lintcheck/typecheck.go`:
- Around line 88-91: packages.Load can return pkgs with per-package errors in
pkg.Errors that are currently ignored; iterate the returned pkgs slice after
packages.Load (the pkgs variable) and for each pkg check pkg.Errors and log them
(at debug level) via your logger or return an error if the special "errs"
package failed to load/type-check before you mark the scope enabled;
specifically update the code around packages.Load to loop through pkgs, inspect
pkg.Errors, emit debug logs for each error (or bail if the errs package is among
the failed packages), and only enable the scope once the errs package is
confirmed present with no pkg.Errors.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 077bc9f9-fd67-46c8-9f1d-894561916833

📥 Commits

Reviewing files that changed from the base of the PR and between 002d1d2 and 80cce5f.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (86)
  • .github/workflows/ci.yml
  • .golangci.yml
  • cmd/api/api.go
  • cmd/api/api_test.go
  • cmd/auth/scopes.go
  • cmd/config/bind_test.go
  • cmd/config/config_test.go
  • cmd/error_auth_hint.go
  • cmd/event/runtime.go
  • cmd/lintcheck/main.go
  • cmd/root.go
  • cmd/root_integration_test.go
  • cmd/root_test.go
  • cmd/service/service.go
  • cmd/service/service_test.go
  • errs/CONTRACT.md
  • errs/category.go
  • errs/category_test.go
  • errs/doc.go
  • errs/internal_carrier.go
  • errs/marshal_test.go
  • errs/predicates.go
  • errs/predicates_test.go
  • errs/problem.go
  • errs/problem_test.go
  • errs/projection/mcp.go
  • errs/projection/mcp_test.go
  • errs/projection/oauth.go
  • errs/projection/oauth_test.go
  • errs/subtypes.go
  • errs/subtypes_service_task.go
  • errs/types.go
  • errs/types_test.go
  • errs/wrap.go
  • errs/wrap_test.go
  • go.mod
  • internal/auth/errors.go
  • internal/auth/transport.go
  • internal/auth/transport_test.go
  • internal/auth/uat_client.go
  • internal/client/api_errors.go
  • internal/client/api_errors_test.go
  • internal/client/client.go
  • internal/client/client_test.go
  • internal/client/pagination.go
  • internal/client/response.go
  • internal/client/response_test.go
  • internal/cmdutil/json.go
  • internal/core/config.go
  • internal/core/errors.go
  • internal/core/notconfigured.go
  • internal/errclass/classify.go
  • internal/errclass/classify_test.go
  • internal/errclass/codemeta.go
  • internal/errclass/codemeta_task.go
  • internal/errclass/codemeta_test.go
  • internal/errcompat/promote.go
  • internal/errcompat/promote_test.go
  • internal/lintcheck/rules.go
  • internal/lintcheck/rules_test.go
  • internal/lintcheck/scan.go
  • internal/lintcheck/scan_test.go
  • internal/lintcheck/typecheck.go
  • internal/lintcheck/typecheck_test.go
  • internal/output/envelope.go
  • internal/output/errors.go
  • internal/output/errors_test.go
  • internal/output/exitcode.go
  • internal/output/exitcode_test.go
  • internal/output/lark_errors.go
  • shortcuts/base/base_execute_test.go
  • shortcuts/calendar/calendar_test.go
  • shortcuts/common/drive_media_upload.go
  • shortcuts/common/drive_media_upload_test.go
  • shortcuts/common/mcp_client_test.go
  • shortcuts/drive/drive_search.go
  • shortcuts/drive/drive_status_test.go
  • shortcuts/mail/mail_shortcut_validation_test.go
  • shortcuts/markdown/helpers.go
  • shortcuts/markdown/markdown_diff_test.go
  • shortcuts/sheets/lark_sheets_sheet_management.go
  • shortcuts/task/task_body_test.go
  • shortcuts/task/task_query_helpers_test.go
  • shortcuts/task/task_upload_attachment_test.go
  • shortcuts/task/task_util_test.go
  • tests/cli_e2e/config/bind_test.go
💤 Files with no reviewable changes (3)
  • shortcuts/drive/drive_search.go
  • shortcuts/sheets/lark_sheets_sheet_management.go
  • internal/output/errors_test.go
✅ Files skipped from review due to trivial changes (5)
  • internal/output/envelope.go
  • shortcuts/task/task_body_test.go
  • errs/internal_carrier.go
  • internal/core/errors.go
  • errs/doc.go

Comment thread internal/lintcheck/scan.go
@evandance evandance force-pushed the feat/error-contract-framework branch from 80cce5f to 799a12b Compare May 20, 2026 11:04
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (1)
internal/lintcheck/scan.go (1)

196-199: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

"No subtypes found" error is not caught by os.IsNotExist check.

LoadSubtypeAllowlists returns fmt.Errorf("no subtypes*.go found...") when the directory exists but contains no matching files. However, ScanRepo (line 35) only skips Rule E for os.IsNotExist(err). This means a valid errs/ directory without subtypes files will cause ScanRepo to fail instead of gracefully skipping Rule E.

🐛 Proposed fix: wrap with os.ErrNotExist
 if found == 0 {
 	// Treat absence like a missing file — caller silently skips Rule E.
-	return nil, nil, fmt.Errorf("no subtypes*.go found under %s", errsDir)
+	return nil, nil, fmt.Errorf("%w: no subtypes*.go found under %s", os.ErrNotExist, errsDir)
 }

This aligns with the comment's intent ("Treat absence like a missing file") and allows ScanRepo to correctly skip Rule E via its os.IsNotExist(err) check.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/lintcheck/scan.go` around lines 196 - 199, LoadSubtypeAllowlists
currently returns a plain fmt.Errorf when no subtypes*.go files are found, which
ScanRepo only treats as missing when os.IsNotExist(err) is true; modify
LoadSubtypeAllowlists so that the "no subtypes*.go found under %s" error wraps
os.ErrNotExist (use fmt.Errorf("...: %w", os.ErrNotExist) or errors.Wrap) so
callers like ScanRepo and its os.IsNotExist(err) check will recognize and skip
Rule E; update the return path in LoadSubtypeAllowlists (referencing errsDir and
the existing "no subtypes*.go found" message) to wrap with os.ErrNotExist.
🧹 Nitpick comments (3)
cmd/root_test.go (1)

29-51: ⚡ Quick win

Add t.Setenv for config isolation in these tests.

These tests use cmdutil.TestFactory but don't isolate the config directory. While they may not touch config state in practice, adding isolation prevents future regressions and aligns with the coding guidelines.

♻️ Proposed fix
 func TestPersistentPreRunE_AuthCheckDisabledAnnotations(t *testing.T) {
+	t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
 	f, _, _, _ := cmdutil.TestFactory(t, nil)
 func TestPersistentPreRunE_AuthSubcommands(t *testing.T) {
+	t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
 	f, _, _, _ := cmdutil.TestFactory(t, nil)
 func TestPersistentPreRunE_ConfigSubcommands(t *testing.T) {
+	t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
 	f, _, _, _ := cmdutil.TestFactory(t, nil)

As per coding guidelines **/*_test.go: Use t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) to isolate config state in tests.

Also applies to: 53-73

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/root_test.go` around lines 29 - 51, Add test-local config isolation by
calling t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) at the start of the
tests that call cmdutil.TestFactory; specifically, in
TestPersistentPreRunE_AuthCheckDisabledAnnotations (and the similar test
covering lines 53-73) set that env var before creating the factory and commands
(auth.NewCmdAuth, cmdconfig.NewCmdConfig, schema.NewCmdSchema, api.NewCmdApi) so
each test uses an isolated config directory.
internal/lintcheck/rules.go (2)

264-264: 💤 Low value

Consider compiling the regex once at package level.

The adHoc regex is compiled on every call to scanSubtype. For large repos with many files, this adds unnecessary overhead.

♻️ Proposed optimization
+var adHocRe = regexp.MustCompile(`^ad_hoc_[a-z0-9_]+$`)
+
 func scanSubtype(path, src string, allowlist, nameset map[string]struct{}, scope *TypedScope, absPath string) ([]Violation, error) {
 	fset := token.NewFileSet()
 	file, err := parser.ParseFile(fset, path, src, parser.ParseComments)
 	if err != nil {
 		return nil, err
 	}
-	adHoc := regexp.MustCompile(`^ad_hoc_[a-z0-9_]+$`)
 	var out []Violation
 	emit := func(pos token.Pos, c subtypeClassification) {

Then update references from adHoc to adHocRe.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/lintcheck/rules.go` at line 264, The local regexp adHoc :=
regexp.MustCompile(`^ad_hoc_[a-z0-9_]+$`) in scanSubtype is being compiled on
every call; move it to a package-level var (e.g., var adHocRe =
regexp.MustCompile(...)) so it is compiled once at init, then replace uses of
adHoc in scanSubtype with adHocRe and remove the local compilation; ensure the
new var name (adHocRe) is referenced wherever the current adHoc local variable
was used.

525-530: 💤 Low value

unquoteSimple doesn't handle escape sequences.

This strips quotes but doesn't process escapes like \" or \\. If a subtype literal contains escapes, the value won't match the allowlist. Given that subtypes are simple snake_case identifiers, this is likely fine in practice, but worth documenting the limitation.

📝 Optional: add a clarifying comment
+// unquoteSimple strips the outer quotes from a string literal without
+// processing escape sequences. This is intentionally simple since Subtype
+// values are snake_case identifiers that never contain escapes.
 func unquoteSimple(quoted string) string {
 	if len(quoted) >= 2 && (quoted[0] == '"' || quoted[0] == '`') {
 		return quoted[1 : len(quoted)-1]
 	}
 	return quoted
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/lintcheck/rules.go` around lines 525 - 530, The unquoteSimple
function currently strips surrounding quotes but ignores escape sequences (e.g.,
\" or \\), which can cause mismatches; update unquoteSimple to handle common Go
string escapes (at least backslash and escaped quote) or, if you prefer to keep
it simple, add a clear comment above unquoteSimple documenting this limitation
and stating that it does not unescape escape sequences and is only intended for
simple snake_case identifiers; reference the function name unquoteSimple in the
comment so future readers know where the limitation applies.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@errs/CONTRACT.md`:
- Line 232: Update the grammar in the line that currently reads "Add a MCP/OAuth
projection adapter if the type maps to one of those" by changing "a MCP" to "an
MCP" so it reads "Add an MCP/OAuth projection adapter if the type maps to one of
those"; locate this exact phrase in the CONTRACT.md content and make the
single-word article replacement.

In `@errs/types.go`:
- Line 16: Each Unwrap() method currently dereferences its receiver and can
panic if the typed pointer is nil; update every Unwrap method (for types
ValidationError, AuthenticationError, NotFoundError, ConflictError,
PermissionError, InternalError) to guard the receiver at the top (if e == nil)
and return nil in that case, otherwise return e.Cause—this prevents panics when
errors.Unwrap calls these methods on a nil typed pointer.

In `@internal/client/api_errors.go`:
- Around line 75-82: The transport detection is too broad: change the url.Error
and syscall.Errno checks to only classify true network transport failures—when
encountering a *url.Error (variable urlErr) unwrap its underlying error
(urlErr.Err or errors.Unwrap(urlErr)) and return true only if that cause is
context.DeadlineExceeded or context.Canceled or implements net.Error (and
indicates a network error); for syscall.Errno (variable syscallErr) replace the
blanket match with an explicit whitelist comparing syscallErr to only
syscall.ECONNREFUSED, syscall.ECONNRESET, syscall.ETIMEDOUT,
syscall.EHOSTUNREACH, and syscall.ENETUNREACH so only those errno values result
in CategoryNetwork.

In `@internal/output/lark_errors.go`:
- Around line 73-97: The legacyHints map is missing entries for refresh-token
error codes used by ClassifyLarkError, causing those cases to return empty
hints; add map entries for the refresh-token codes (e.g., 20026, 20037, 20064,
20073 and retryable 20050) in legacyHints with the same re-auth guidance string
("run: lark-cli auth login to re-authorize" or the canonical re-auth message
used elsewhere) so ClassifyLarkError will return the expected legacy hint for
those error paths.

In `@shortcuts/mail/mail_shortcut_validation_test.go`:
- Around line 14-27: The helper assertValidationError currently rejects legacy
*output.ExitError because it requires errs.IsValidation(err); change the
validation check to accept either errs.IsValidation(err) OR
output.ExitCodeOf(err) == output.ExitValidation (i.e., treat legacy
exit-code-based validation errors as valid). Update the conditional and its
failure message in assertValidationError so it only fails when neither condition
holds, preserving the subsequent exit-code assertion and message checks.

---

Duplicate comments:
In `@internal/lintcheck/scan.go`:
- Around line 196-199: LoadSubtypeAllowlists currently returns a plain
fmt.Errorf when no subtypes*.go files are found, which ScanRepo only treats as
missing when os.IsNotExist(err) is true; modify LoadSubtypeAllowlists so that
the "no subtypes*.go found under %s" error wraps os.ErrNotExist (use
fmt.Errorf("...: %w", os.ErrNotExist) or errors.Wrap) so callers like ScanRepo
and its os.IsNotExist(err) check will recognize and skip Rule E; update the
return path in LoadSubtypeAllowlists (referencing errsDir and the existing "no
subtypes*.go found" message) to wrap with os.ErrNotExist.

---

Nitpick comments:
In `@cmd/root_test.go`:
- Around line 29-51: Add test-local config isolation by calling
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) at the start of the tests that
call cmdutil.TestFactory; specifically, in
TestPersistentPreRunE_AuthCheckDisabledAnnotations (and the similar test
covering lines 53-73) set that env var before creating the factory and commands
(auth.NewCmdAuth, cmdconfig.NewCmdConfig, schema.NewCmdSchema, api.NewCmdApi) so
each test uses an isolated config directory.

In `@internal/lintcheck/rules.go`:
- Line 264: The local regexp adHoc := regexp.MustCompile(`^ad_hoc_[a-z0-9_]+$`)
in scanSubtype is being compiled on every call; move it to a package-level var
(e.g., var adHocRe = regexp.MustCompile(...)) so it is compiled once at init,
then replace uses of adHoc in scanSubtype with adHocRe and remove the local
compilation; ensure the new var name (adHocRe) is referenced wherever the
current adHoc local variable was used.
- Around line 525-530: The unquoteSimple function currently strips surrounding
quotes but ignores escape sequences (e.g., \" or \\), which can cause
mismatches; update unquoteSimple to handle common Go string escapes (at least
backslash and escaped quote) or, if you prefer to keep it simple, add a clear
comment above unquoteSimple documenting this limitation and stating that it does
not unescape escape sequences and is only intended for simple snake_case
identifiers; reference the function name unquoteSimple in the comment so future
readers know where the limitation applies.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b8b64581-d109-43a6-8f54-aba00cea974f

📥 Commits

Reviewing files that changed from the base of the PR and between 80cce5f and 799a12b.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (94)
  • .github/workflows/ci.yml
  • .golangci.yml
  • cmd/api/api.go
  • cmd/api/api_test.go
  • cmd/auth/login_result.go
  • cmd/auth/scopes.go
  • cmd/config/bind_test.go
  • cmd/config/config_test.go
  • cmd/error_auth_hint.go
  • cmd/event/runtime.go
  • cmd/lintcheck/main.go
  • cmd/platform_guards.go
  • cmd/prune.go
  • cmd/root.go
  • cmd/root_integration_test.go
  • cmd/root_test.go
  • cmd/service/service.go
  • cmd/service/service_test.go
  • errs/CONTRACT.md
  • errs/category.go
  • errs/category_test.go
  • errs/doc.go
  • errs/internal_carrier.go
  • errs/marshal_test.go
  • errs/predicates.go
  • errs/predicates_test.go
  • errs/problem.go
  • errs/problem_test.go
  • errs/projection/mcp.go
  • errs/projection/mcp_test.go
  • errs/projection/oauth.go
  • errs/projection/oauth_test.go
  • errs/subtypes.go
  • errs/subtypes_service_task.go
  • errs/types.go
  • errs/types_test.go
  • errs/wrap.go
  • errs/wrap_test.go
  • go.mod
  • internal/auth/errors.go
  • internal/auth/transport.go
  • internal/auth/transport_test.go
  • internal/auth/uat_client.go
  • internal/client/api_errors.go
  • internal/client/api_errors_test.go
  • internal/client/client.go
  • internal/client/client_test.go
  • internal/client/pagination.go
  • internal/client/response.go
  • internal/client/response_test.go
  • internal/cmdpolicy/apply.go
  • internal/cmdutil/confirm.go
  • internal/cmdutil/json.go
  • internal/core/config.go
  • internal/core/errors.go
  • internal/core/notconfigured.go
  • internal/errclass/classify.go
  • internal/errclass/classify_test.go
  • internal/errclass/codemeta.go
  • internal/errclass/codemeta_task.go
  • internal/errclass/codemeta_test.go
  • internal/errcompat/promote.go
  • internal/errcompat/promote_test.go
  • internal/hook/install.go
  • internal/lintcheck/rules.go
  • internal/lintcheck/rules_test.go
  • internal/lintcheck/scan.go
  • internal/lintcheck/scan_test.go
  • internal/lintcheck/typecheck.go
  • internal/lintcheck/typecheck_test.go
  • internal/output/envelope.go
  • internal/output/errors.go
  • internal/output/errors_test.go
  • internal/output/exitcode.go
  • internal/output/exitcode_test.go
  • internal/output/lark_errors.go
  • shortcuts/base/base_execute_test.go
  • shortcuts/calendar/calendar_test.go
  • shortcuts/calendar/errors.go
  • shortcuts/common/drive_media_upload.go
  • shortcuts/common/drive_media_upload_test.go
  • shortcuts/common/mcp_client_test.go
  • shortcuts/drive/drive_search.go
  • shortcuts/drive/drive_status_test.go
  • shortcuts/drive/list_remote.go
  • shortcuts/mail/mail_shortcut_validation_test.go
  • shortcuts/markdown/helpers.go
  • shortcuts/markdown/markdown_diff_test.go
  • shortcuts/sheets/lark_sheets_sheet_management.go
  • shortcuts/task/task_body_test.go
  • shortcuts/task/task_query_helpers_test.go
  • shortcuts/task/task_upload_attachment_test.go
  • shortcuts/task/task_util_test.go
  • tests/cli_e2e/config/bind_test.go
💤 Files with no reviewable changes (3)
  • shortcuts/drive/drive_search.go
  • shortcuts/sheets/lark_sheets_sheet_management.go
  • internal/output/errors_test.go
✅ Files skipped from review due to trivial changes (14)
  • internal/cmdutil/confirm.go
  • internal/hook/install.go
  • shortcuts/calendar/errors.go
  • cmd/prune.go
  • internal/core/errors.go
  • errs/internal_carrier.go
  • shortcuts/drive/list_remote.go
  • cmd/auth/login_result.go
  • cmd/platform_guards.go
  • internal/core/notconfigured.go
  • errs/doc.go
  • .golangci.yml
  • internal/cmdpolicy/apply.go
  • shortcuts/base/base_execute_test.go

Comment thread errs/CONTRACT.md Outdated
Comment thread errs/types.go Outdated
Comment thread internal/client/api_errors.go Outdated
Comment thread internal/output/lark_errors.go
Comment thread shortcuts/mail/mail_shortcut_validation_test.go Outdated
@evandance evandance force-pushed the feat/error-contract-framework branch 2 times, most recently from e1c4e7c to a789959 Compare May 20, 2026 11:25
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
internal/client/api_errors.go (1)

71-87: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

*url.Error can still be misclassified as transport due to the earlier net.Error match.

At Line 71, errors.As(err, &netErr) runs before the Line 79 *url.Error guard. Since *url.Error satisfies net.Error, non-transport cases (for example invalid/unsupported URL scheme) can still be tagged as CategoryNetwork.

Proposed fix
 func isTransportError(err error) bool {
 	if err == nil {
 		return false
 	}
 	if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
 		return true
 	}
-	var netErr net.Error
-	if errors.As(err, &netErr) {
-		return true
-	}
 	// *url.Error is too broad on its own — it also wraps things like
 	// "unsupported protocol scheme" (a programmer/config bug, not a
 	// network failure). Treat it as transport only when its wrapped cause
 	// is a context cancel/deadline or itself a net.Error.
 	var urlErr *url.Error
 	if errors.As(err, &urlErr) && urlErr != nil {
 		if errors.Is(urlErr.Err, context.DeadlineExceeded) || errors.Is(urlErr.Err, context.Canceled) {
 			return true
 		}
 		var innerNetErr net.Error
 		if errors.As(urlErr.Err, &innerNetErr) {
 			return true
 		}
+		return false
+	}
+	var netErr net.Error
+	if errors.As(err, &netErr) {
+		return true
 	}
In Go standard library, does *net/url.Error implement net.Error, and can it wrap non-transport errors like "unsupported protocol scheme"?
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/client/api_errors.go` around lines 71 - 87, The current ordering
misclassifies some *url.Error instances as net.Error because errors.As(err,
&netErr) runs first; reorder the checks so you first try errors.As(err, &urlErr)
(the urlErr handling block that inspects urlErr.Err for
context.DeadlineExceeded, context.Canceled, or an inner net.Error) and only if
that fails fall back to errors.As(err, &netErr); alternatively, explicitly skip
treating url.Error as a plain net.Error by checking for *url.Error (urlErr)
before the net.Error branch so invalid/unsupported URL-scheme errors are not
labeled CategoryNetwork.
🧹 Nitpick comments (1)
errs/projection/oauth_test.go (1)

80-95: ⚡ Quick win

Also assert empty mapped value when ok == false

This case should also verify got == "" to pin the full fallback contract (consistent with the other negative tests in this file).

Proposed patch
 func TestOAuthErrorFor_OtherCategoriesNotMapped(t *testing.T) {
 	for _, cat := range []errs.Category{
 		errs.CategoryConfig,
 		errs.CategoryNetwork,
 		errs.CategoryAPI,
 		errs.CategoryPolicy,
 		errs.CategoryInternal,
 		errs.CategoryConfirmation,
 	} {
 		t.Run(string(cat), func(t *testing.T) {
 			got, ok := OAuthErrorFor(cat, errs.Subtype("anything"))
 			if ok {
 				t.Errorf("ok = true, want false for %q (got %q)", cat, got)
 			}
+			if got != "" {
+				t.Errorf("oauthError = %q, want empty for %q", got, cat)
+			}
 		})
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@errs/projection/oauth_test.go` around lines 80 - 95, Update the test
TestOAuthErrorFor_OtherCategoriesNotMapped to also assert that the returned
mapped string is empty when ok is false: after calling OAuthErrorFor(cat,
errs.Subtype("anything")) and checking ok is false, add an assertion that got ==
"" (or use t.Errorf/t.Fatalf to report if got is non-empty) so the test fully
verifies the fallback contract of OAuthErrorFor returning an empty string when
no mapping exists.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/lintcheck/typecheck.go`:
- Around line 151-157: The doc for ResolveSubtypeIdent is incorrect about the
meaning of the two return booleans; update the comment to match the
implementation of ResolveSubtypeIdent(resolved bool, ok bool): state that
ok=true means the typed scope is available and the function made a definitive
resolution (even if the identifier resolves to a non-errs package or a
non-Subtype constant, in which case resolved=false indicates a definitive
rejection), and ok=false only when the typed scope is disabled (caller should
defer to AST-only resolution). Mention the exact return tuple (resolved, ok) and
the three cases the function distinguishes so callers won't mis-handle the
definitive-rejection branch.

---

Duplicate comments:
In `@internal/client/api_errors.go`:
- Around line 71-87: The current ordering misclassifies some *url.Error
instances as net.Error because errors.As(err, &netErr) runs first; reorder the
checks so you first try errors.As(err, &urlErr) (the urlErr handling block that
inspects urlErr.Err for context.DeadlineExceeded, context.Canceled, or an inner
net.Error) and only if that fails fall back to errors.As(err, &netErr);
alternatively, explicitly skip treating url.Error as a plain net.Error by
checking for *url.Error (urlErr) before the net.Error branch so
invalid/unsupported URL-scheme errors are not labeled CategoryNetwork.

---

Nitpick comments:
In `@errs/projection/oauth_test.go`:
- Around line 80-95: Update the test TestOAuthErrorFor_OtherCategoriesNotMapped
to also assert that the returned mapped string is empty when ok is false: after
calling OAuthErrorFor(cat, errs.Subtype("anything")) and checking ok is false,
add an assertion that got == "" (or use t.Errorf/t.Fatalf to report if got is
non-empty) so the test fully verifies the fallback contract of OAuthErrorFor
returning an empty string when no mapping exists.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7c5c37f9-2848-4521-a49e-2941e0eee498

📥 Commits

Reviewing files that changed from the base of the PR and between 799a12b and e1c4e7c.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (94)
  • .github/workflows/ci.yml
  • .golangci.yml
  • cmd/api/api.go
  • cmd/api/api_test.go
  • cmd/auth/login_result.go
  • cmd/auth/scopes.go
  • cmd/config/bind_test.go
  • cmd/config/config_test.go
  • cmd/error_auth_hint.go
  • cmd/event/runtime.go
  • cmd/lintcheck/main.go
  • cmd/platform_guards.go
  • cmd/prune.go
  • cmd/root.go
  • cmd/root_integration_test.go
  • cmd/root_test.go
  • cmd/service/service.go
  • cmd/service/service_test.go
  • errs/CONTRACT.md
  • errs/category.go
  • errs/category_test.go
  • errs/doc.go
  • errs/internal_carrier.go
  • errs/marshal_test.go
  • errs/predicates.go
  • errs/predicates_test.go
  • errs/problem.go
  • errs/problem_test.go
  • errs/projection/mcp.go
  • errs/projection/mcp_test.go
  • errs/projection/oauth.go
  • errs/projection/oauth_test.go
  • errs/subtypes.go
  • errs/subtypes_service_task.go
  • errs/types.go
  • errs/types_test.go
  • errs/wrap.go
  • errs/wrap_test.go
  • go.mod
  • internal/auth/errors.go
  • internal/auth/transport.go
  • internal/auth/transport_test.go
  • internal/auth/uat_client.go
  • internal/client/api_errors.go
  • internal/client/api_errors_test.go
  • internal/client/client.go
  • internal/client/client_test.go
  • internal/client/pagination.go
  • internal/client/response.go
  • internal/client/response_test.go
  • internal/cmdpolicy/apply.go
  • internal/cmdutil/confirm.go
  • internal/cmdutil/json.go
  • internal/core/config.go
  • internal/core/errors.go
  • internal/core/notconfigured.go
  • internal/errclass/classify.go
  • internal/errclass/classify_test.go
  • internal/errclass/codemeta.go
  • internal/errclass/codemeta_task.go
  • internal/errclass/codemeta_test.go
  • internal/errcompat/promote.go
  • internal/errcompat/promote_test.go
  • internal/hook/install.go
  • internal/lintcheck/rules.go
  • internal/lintcheck/rules_test.go
  • internal/lintcheck/scan.go
  • internal/lintcheck/scan_test.go
  • internal/lintcheck/typecheck.go
  • internal/lintcheck/typecheck_test.go
  • internal/output/envelope.go
  • internal/output/errors.go
  • internal/output/errors_test.go
  • internal/output/exitcode.go
  • internal/output/exitcode_test.go
  • internal/output/lark_errors.go
  • shortcuts/base/base_execute_test.go
  • shortcuts/calendar/calendar_test.go
  • shortcuts/calendar/errors.go
  • shortcuts/common/drive_media_upload.go
  • shortcuts/common/drive_media_upload_test.go
  • shortcuts/common/mcp_client_test.go
  • shortcuts/drive/drive_search.go
  • shortcuts/drive/drive_status_test.go
  • shortcuts/drive/list_remote.go
  • shortcuts/mail/mail_shortcut_validation_test.go
  • shortcuts/markdown/helpers.go
  • shortcuts/markdown/markdown_diff_test.go
  • shortcuts/sheets/lark_sheets_sheet_management.go
  • shortcuts/task/task_body_test.go
  • shortcuts/task/task_query_helpers_test.go
  • shortcuts/task/task_upload_attachment_test.go
  • shortcuts/task/task_util_test.go
  • tests/cli_e2e/config/bind_test.go
💤 Files with no reviewable changes (3)
  • shortcuts/drive/drive_search.go
  • internal/output/errors_test.go
  • shortcuts/sheets/lark_sheets_sheet_management.go
✅ Files skipped from review due to trivial changes (13)
  • errs/category_test.go
  • internal/cmdutil/confirm.go
  • cmd/auth/login_result.go
  • shortcuts/base/base_execute_test.go
  • shortcuts/drive/list_remote.go
  • internal/core/errors.go
  • cmd/prune.go
  • errs/internal_carrier.go
  • cmd/platform_guards.go
  • shortcuts/calendar/errors.go
  • internal/hook/install.go
  • errs/doc.go
  • internal/output/envelope.go

Comment thread internal/lintcheck/typecheck.go Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/output/errors.go`:
- Around line 241-242: WriteTypedErrorEnvelope is returning true regardless of
errors from buf.WriteTo(w); change it to check the error returned by
buf.WriteTo(w) (using the existing buf and w variables) and return false when
buf.WriteTo returns a non-nil error so callers don’t assume the envelope was
written successfully; ensure the function still returns true only when
buf.WriteTo succeeds and any write error is propagated to the caller via the
boolean result.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ec481368-d2f8-45b1-91bb-9729e633c240

📥 Commits

Reviewing files that changed from the base of the PR and between e1c4e7c and a789959.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (94)
  • .github/workflows/ci.yml
  • .golangci.yml
  • cmd/api/api.go
  • cmd/api/api_test.go
  • cmd/auth/login_result.go
  • cmd/auth/scopes.go
  • cmd/config/bind_test.go
  • cmd/config/config_test.go
  • cmd/error_auth_hint.go
  • cmd/event/runtime.go
  • cmd/lintcheck/main.go
  • cmd/platform_guards.go
  • cmd/prune.go
  • cmd/root.go
  • cmd/root_integration_test.go
  • cmd/root_test.go
  • cmd/service/service.go
  • cmd/service/service_test.go
  • errs/ERROR_CONTRACT.md
  • errs/category.go
  • errs/category_test.go
  • errs/doc.go
  • errs/internal_carrier.go
  • errs/marshal_test.go
  • errs/predicates.go
  • errs/predicates_test.go
  • errs/problem.go
  • errs/problem_test.go
  • errs/projection/mcp.go
  • errs/projection/mcp_test.go
  • errs/projection/oauth.go
  • errs/projection/oauth_test.go
  • errs/subtypes.go
  • errs/subtypes_service_task.go
  • errs/types.go
  • errs/types_test.go
  • errs/wrap.go
  • errs/wrap_test.go
  • go.mod
  • internal/auth/errors.go
  • internal/auth/transport.go
  • internal/auth/transport_test.go
  • internal/auth/uat_client.go
  • internal/client/api_errors.go
  • internal/client/api_errors_test.go
  • internal/client/client.go
  • internal/client/client_test.go
  • internal/client/pagination.go
  • internal/client/response.go
  • internal/client/response_test.go
  • internal/cmdpolicy/apply.go
  • internal/cmdutil/confirm.go
  • internal/cmdutil/json.go
  • internal/core/config.go
  • internal/core/errors.go
  • internal/core/notconfigured.go
  • internal/errclass/classify.go
  • internal/errclass/classify_test.go
  • internal/errclass/codemeta.go
  • internal/errclass/codemeta_task.go
  • internal/errclass/codemeta_test.go
  • internal/errcompat/promote.go
  • internal/errcompat/promote_test.go
  • internal/hook/install.go
  • internal/lintcheck/rules.go
  • internal/lintcheck/rules_test.go
  • internal/lintcheck/scan.go
  • internal/lintcheck/scan_test.go
  • internal/lintcheck/typecheck.go
  • internal/lintcheck/typecheck_test.go
  • internal/output/envelope.go
  • internal/output/errors.go
  • internal/output/errors_test.go
  • internal/output/exitcode.go
  • internal/output/exitcode_test.go
  • internal/output/lark_errors.go
  • shortcuts/base/base_execute_test.go
  • shortcuts/calendar/calendar_test.go
  • shortcuts/calendar/errors.go
  • shortcuts/common/drive_media_upload.go
  • shortcuts/common/drive_media_upload_test.go
  • shortcuts/common/mcp_client_test.go
  • shortcuts/drive/drive_search.go
  • shortcuts/drive/drive_status_test.go
  • shortcuts/drive/list_remote.go
  • shortcuts/mail/mail_shortcut_validation_test.go
  • shortcuts/markdown/helpers.go
  • shortcuts/markdown/markdown_diff_test.go
  • shortcuts/sheets/lark_sheets_sheet_management.go
  • shortcuts/task/task_body_test.go
  • shortcuts/task/task_query_helpers_test.go
  • shortcuts/task/task_upload_attachment_test.go
  • shortcuts/task/task_util_test.go
  • tests/cli_e2e/config/bind_test.go
💤 Files with no reviewable changes (3)
  • shortcuts/sheets/lark_sheets_sheet_management.go
  • shortcuts/drive/drive_search.go
  • internal/output/errors_test.go
✅ Files skipped from review due to trivial changes (14)
  • shortcuts/calendar/errors.go
  • cmd/auth/login_result.go
  • errs/category.go
  • cmd/prune.go
  • internal/cmdpolicy/apply.go
  • internal/core/errors.go
  • go.mod
  • shortcuts/base/base_execute_test.go
  • internal/hook/install.go
  • shortcuts/drive/list_remote.go
  • cmd/platform_guards.go
  • internal/output/envelope.go
  • shortcuts/task/task_body_test.go
  • errs/doc.go

Comment thread internal/output/errors.go Outdated
@evandance evandance force-pushed the feat/error-contract-framework branch 8 times, most recently from 42c94f9 to 750d291 Compare May 21, 2026 03:31
@github-actions github-actions Bot removed the domain/task PR touches the task domain label May 21, 2026
@evandance evandance force-pushed the feat/error-contract-framework branch 10 times, most recently from 8476062 to 136aad7 Compare May 21, 2026 12:12
Introduce a typed error contract framework for lark-cli so in-process
Go callers can branch via errors.As(&errs.XxxError{}) and shell scripts,
AI agents, and protocol adapters can branch on stable JSON type/subtype
fields instead of regex-parsing free-form messages.

Adds:
- Canonical taxonomy under errs/ (9 categories + typed Error structs
  embedding a shared Problem, RFC 7807-aligned)
- Centralized Lark code metadata + identity-aware BuildAPIError dispatch
- Typed JSON envelope writer alongside the legacy envelope writer
- MCP / OAuth (RFC 6750 Bearer) projection adapters
- Five CI lint guards preventing ad-hoc taxonomy drift

Backward compatibility: legacy *output.ExitError producers (ErrAPI,
ErrWithHint, Errorf, ErrBare) and business shortcuts that use them
continue to render the legacy envelope unchanged. SecurityPolicyError
wire format and exit code are preserved via a carve-out; taxonomy
migration is deferred to PR 2. Domain-specific business migration is
staged across PR 3+.

Framework-direct paths now return typed *errs.*Error: ErrAuth /
ErrValidation / ErrNetwork emit category literals on the wire
(authentication / validation / network), *core.ConfigError is promoted
at the cmd/root boundary with exit code aligned from 2 to 3, and Lark
API permission denials classified by BuildAPIError exit 3.

At the SDK boundary, WrapDoAPIError preserves any already-classified
error (legacy *output.ExitError or typed *errs.*) so output.ErrAuth
from missing credentials surfaces with the auth category and exit 3
intact instead of being downgraded to a network error. Policy responses
classified by BuildAPIError (codes 21000 / 21001) extract challenge_url
and the canonical hint from the response body, matching what the
auth transport already surfaces at the HTTP layer; non-https
challenge URLs are dropped.

First PR in the feat/error-contract-* series.
@evandance evandance force-pushed the feat/error-contract-framework branch from 136aad7 to 569e208 Compare May 21, 2026 12:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

domain/base PR touches the base domain domain/calendar PR touches the calendar domain domain/ccm PR touches the ccm domain domain/mail PR touches the mail domain feature size/XL Architecture-level or global-impact change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant