Skip to content

fix: address code review issues in credential provider system#223

Closed
liangshuo-1 wants to merge 6 commits intomainfrom
fix/credential-provider-review-fixes
Closed

fix: address code review issues in credential provider system#223
liangshuo-1 wants to merge 6 commits intomainfrom
fix/credential-provider-review-fixes

Conversation

@liangshuo-1
Copy link
Copy Markdown
Collaborator

@liangshuo-1 liangshuo-1 commented Apr 2, 2026

Summary

  • Critical: Fix drive test compilation failure, mail_watch DryRun test assertions (3 APIs not 2), BlockError handling in enrichUserInfo, strict-mode --reset arg conflict, --global on override warning
  • Important: Change internal TokenType from int to string (eliminate mapping), t.Setenv migration, utf8.RuneCountInString for profile name validation, json.Marshal error handling, keychain cleanup on profile AppId change, ctx param ordering, profile add --use PreviousApp fix
  • Documentation: sync.Once + context behavior, UAT non-caching rationale

Context

Code review of 2e342fd (unified credential provider system, written by Codex) identified 6 Critical, 18 Important, and 17 Minor issues across 4 review domains (core packages, integration layer, command layer, shortcuts/tests). This PR addresses all Critical and key Important issues.

Test plan

  • go build ./... passes
  • All 6 mail_watch DryRun tests pass (were failing before)
  • shortcuts/drive tests pass (were failing to compile before)
  • internal/credential, internal/cmdutil, internal/core, cmd/config, cmd/auth, cmd/service all pass

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Profile management CLI: add, list (JSON), use, remove, rename profiles; new persistent --profile flag.
    • New config strict-mode command to view/set/reset strict-mode and show its source.
    • Environment-based credential provider (use env vars for app creds/tokens).
  • Improvements

    • Config init supports named profiles and safer secret handling.
    • Commands respect the active profile; config show now includes profile metadata.
    • Strict-mode enforced across commands (blocks disallowed identities).

liangshuo-1 and others added 2 commits April 2, 2026 14:27
Converge scattered UAT/TAT/AppID/AppSecret acquisition into a unified
CredentialProvider with enterprise plugin support.

Architecture:
- Credential-first Factory init: HttpClient → Credential → Config/LarkClient
- Public extension API (extension/credential/) following database/sql driver pattern
- Enterprise plugins via wrapper main.go + blank import (Caddy/CoreDNS pattern)
- SDK token cache disabled — Credential manages all tokens
- Direct HTTP for TAT/user_info — no circular dependency with LarkClient

Key design decisions:
- Provider interface: ResolveAccount(ctx) + ResolveToken(ctx, spec)
- Flow control: nil,nil=skip / &result,nil=handle / nil,&BlockError=block
- Account = core.CliConfig type alias (zero field copying)
- TokenSpec struct for token parameters (not functional options)
- enrichUserInfo mandatory when UAT available (security)
- ProfileOverride late-binding via closure

New packages:
- extension/credential/ — public Provider interface, registry, TokenSpec, BlockError
- extension/credential/env/ — built-in EnvProvider
- internal/credential/ — CredentialProvider, DefaultAccountProvider, DefaultTokenProvider

Change-Id: I62c7628ffe229b0b665f40bfb81b550e542ef45d
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Critical fixes:
- Fix drive_io_test.go compilation (missing registerDriveBotTokenStub)
- Fix mail_watch_test.go DryRun assertions (3 APIs, not 2)
- Handle BlockError correctly in enrichUserInfo (respect chain-stop)
- Reject --reset with value arg in strict-mode command
- Warn when --global on has profile-level overrides

Important fixes:
- Change internal TokenType from int (iota) to string (match extension)
- Replace os.Setenv/Unsetenv with t.Setenv in tests
- Use utf8.RuneCountInString for profile name length validation
- Handle json.Marshal error in TAT request body
- Clean up old keychain secret when profile AppId changes
- Move ctx to first param in checkServiceScopes (Go convention)
- Fix profile add --use setting PreviousApp to self
- Add sync.Once context behavior documentation
- Remove duplicate comment in NewAPIClientWithConfig
- Add UAT non-caching rationale comment

Change-Id: I0dae93d15b6abf97a3dde5157d7bd6d0c6568700
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 2, 2026

📝 Walkthrough

Walkthrough

Adds a pluggable credential system (extension providers + registry + defaults), profile-aware configuration and commands, strict-mode enforcement, credential-driven token resolution across CLI and SDK wiring, refactors media uploads to stream-based handling, and updates many tests to inject or rely on credential providers.

Changes

Cohort / File(s) Summary
Credential framework & providers
extension/credential/types.go, extension/credential/registry.go, extension/credential/registry_test.go, extension/credential/env/env.go, extension/credential/env/env_test.go
Introduce extension provider types, a thread-safe provider registry, and an env-based credential provider with tests.
Internal credential engine
internal/credential/types.go, internal/credential/credential_provider.go, internal/credential/credential_provider_test.go, internal/credential/default_provider.go, internal/credential/default_provider_test.go, internal/credential/user_info.go, internal/credential/integration_test.go
Add CredentialProvider that aggregates extension providers, caches account resolution, enriches via user_info, and resolves UAT/TAT tokens with default resolvers; includes unit/integration tests.
Factory & initialization
internal/cmdutil/factory.go, internal/cmdutil/factory_default.go, internal/cmdutil/testing.go, internal/cmdutil/factory_test.go
Refactor Factory to be credential-first: add Credential & ProfileOverride, remove ResolveConfig/AuthConfig, implement strict-mode helpers and enforce strict-mode in ResolveAs; derive config and SDK client from credential resolution.
Core config & profiles
internal/core/config.go, internal/core/config_strict_mode_test.go, internal/core/types.go
Add profile metadata (Name, StrictMode, CurrentApp, PreviousApp), profile selection helpers, profile-aware RequireConfig/RequireAuth, ValidateProfileName, and ParseBrand helper with tests.
Profile CLI
cmd/profile/profile.go, cmd/profile/add.go, cmd/profile/list.go, cmd/profile/use.go, cmd/profile/remove.go, cmd/profile/rename.go
Add profile command with subcommands to add/list/use/remove/rename profiles, including keychain secret handling, validation, and persistence.
Config commands & strict-mode
cmd/config/config.go, cmd/config/init.go, cmd/config/init_interactive.go, cmd/config/show.go, cmd/config/default_as.go, cmd/config/strict_mode.go, cmd/config/strict_mode_test.go
Profile-aware init (--name), show/default-as use current profile, delegate brand parsing, and add config strict-mode (global/profile/reset/env precedence) with tests.
Auth commands
cmd/auth/login.go, cmd/auth/login_strict_test.go, cmd/auth/logout.go, cmd/auth/list.go
Make auth commands profile-aware, enforce strict-mode blocking for user login, and ensure token persistence targets the selected profile safely; add strict-mode login test.
API/Service/Shortcuts integration
cmd/api/api.go, cmd/api/api_test.go, cmd/service/service.go, cmd/service/service_test.go, shortcuts/common/runner.go, shortcuts/vc/vc_notes.go
Add strict-mode checks after identity resolution, switch config acquisition to f.Config(), and replace stored-token scope checks with credential.ResolveToken-derived scopes; adjust signatures and remove many token stubs in tests.
API client wiring
internal/client/client.go, internal/client/client_test.go
APIClient gains a Credential field; DoSDKRequest now resolves tokens via credential provider and applies SDK auth options; tests inject static token resolver and disable SDK token cache.
Shortcuts media refactor
shortcuts/im/helpers.go
Replace temp-file URL downloads with stream/buffer-based uploads, add limitedReadCloser and mediaBuffer (duration/type detection), and reader-based upload helpers; large internal refactor.
Tests: token stub removals & credential injection
many *_test.go (e.g., cmd/root_e2e_test.go, numerous shortcuts/* tests, internal/*_test.go)
Remove many explicit tenant-access-token HTTP stubs; tests now inject credential providers or rely on default resolvers and adjust setup/assertions accordingly.
CLI root & main
cmd/root.go, main.go
Add --profile persistent flag and wiring to set ProfileOverride; register profile subcommand; blank-import env provider to auto-register it at startup.
Linters
.golangci.yml
Enable forbidigo with path exceptions and forbid rules for certain calls in shortcuts/.

Sequence Diagram(s)

sequenceDiagram
    participant CLI
    participant Factory
    participant CredentialProvider
    participant ExtProviders
    participant DefaultProviders
    participant HTTP
    CLI->>Factory: start command (--profile, --as)
    Factory->>Factory: ResolveAs (enforce strict-mode)
    Factory->>CredentialProvider: ResolveAccount(ctx)
    CredentialProvider->>ExtProviders: iterate ResolveAccount
    alt ext provider returns account
        ExtProviders-->>CredentialProvider: Account
    else none
        CredentialProvider->>DefaultProviders: ResolveAccount fallback
        DefaultProviders-->>CredentialProvider: Account or error
    end
    CredentialProvider->>HTTP: maybe fetch user_info (needs UAT)
    CredentialProvider->>ExtProviders: ResolveToken(spec for UAT)
    alt ext provider returns UAT
        ExtProviders-->>CredentialProvider: UAT + scopes
    else
        CredentialProvider->>DefaultProviders: ResolveToken(UAT)
        DefaultProviders-->>CredentialProvider: UAT + scopes
    end
    CredentialProvider-->>Factory: CliConfig (account)
    CLI->>CredentialProvider: ResolveToken(TokenSpec for API call)
    CredentialProvider->>ExtProviders: iterate ResolveToken
    alt ext provider returns token
        ExtProviders-->>CredentialProvider: TokenResult
    else
        CredentialProvider->>DefaultProviders: ResolveToken fallback
        DefaultProviders-->>CredentialProvider: TokenResult
    end
    CredentialProvider-->>CLI: TokenResult (value + scopes)
    CLI->>HTTP: call API with token (Authorization)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • fangshuyu-768

Poem

🐇 In burrows of code where tokens hide,
Profiles planted, neatly side by side.
Strict-mode watches, makes the callers hop right,
Providers fetch carrots from day and night,
Hooray — the CLI hops forward, cozy and bright!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main objective: fixing code review issues in the credential provider system. It directly maps to the core changes across multiple files.
Description check ✅ Passed The description provides comprehensive coverage of critical and important fixes, context from code review, test plan verification, and structure follows the template with Summary, Changes, and Test Plan sections.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/credential-provider-review-fixes

Comment @coderabbitai help to get the list of available commands and usage tips.

@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/im PR touches the im domain domain/mail PR touches the mail domain domain/vc PR touches the vc domain size/XL Architecture-level or global-impact change labels Apr 2, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 2, 2026

🚀 PR Preview Install Guide

🧰 CLI update

npm i -g https://pkg.pr.new/larksuite/cli/@larksuite/cli@0d69221233ef4152342309c7c64ace87bfbf7b28

🧩 Skill update

npx skills add larksuite/cli#fix/credential-provider-review-fixes -y -g

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 2, 2026

Greptile Summary

This PR addresses a comprehensive code review of the unified credential provider system introduced in 2e342fd. It resolves 6 Critical and a selection of Important issues across the credential layer, command layer, and test infrastructure.

Key improvements include:

  • Credential system unification: CredentialProvider / DefaultTokenProvider / DefaultAccountProvider establish a clean provider chain with documented sync.Once semantics, BlockError handling, and zero-cost string-based TokenType constants.
  • Multi-profile support: core.MultiAppConfig gains CurrentApp / PreviousApp / per-AppConfig.Name fields; profile add/use/remove/rename commands are fully implemented with correct keychain cleanup and PreviousApp tracking.
  • Strict-mode enforcement: New config strict-mode command, Factory.IsStrictMode() / CheckStrictMode(), auth login strict-mode block, and strict-mode propagation through shortcuts, service, and api command paths.
  • Test quality: t.Setenv migration eliminates leaked env vars; drive test compilation fixed; mail_watch DryRun assertions corrected to expect 3 API calls.
  • Scope pre-checks: Now routed through CredentialProvider.ResolveToken consistently across shortcuts, service, and vc-notes.

One UX gap remains: ResolveAs silently coerces --as user to core.AsBot in strict mode with no warning or error, whereas auth login explicitly surfaces a "strict mode is enabled" message. This inconsistency can produce confusing downstream errors ("bot is not supported, this command only supports: user") with no mention of strict mode as the cause.

Confidence Score: 5/5

Safe to merge; all critical and important code-review issues from the original review have been addressed.

All 6 Critical issues (drive test compilation, mail_watch assertions, BlockError in enrichUserInfo, strict-mode --reset conflict, --global on warning, json.Marshal error handling) are resolved. The remaining new finding — silent --as user override in strict mode — is a UX inconsistency (P2) that does not cause data loss or incorrect API calls. All other open items are P2 suggestions.

internal/cmdutil/factory.go — the strict-mode override path in ResolveAs should emit a warning or error when an explicit --as flag is silently coerced to bot.

Important Files Changed

Filename Overview
internal/cmdutil/factory.go Adds strict mode enforcement and Credential field; ResolveAs silently overrides explicit --as user to bot in strict mode without user feedback, inconsistent with auth login which returns an explicit error.
internal/credential/credential_provider.go New unified entry point for credential resolution; properly documents sync.Once context semantics and BlockError asymmetry between ResolveToken and enrichUserInfo.
internal/credential/default_provider.go New default account/token provider using keychain + direct HTTP; TAT cached via sync.Once, UAT uncached by design with clear rationale in comments.
cmd/config/strict_mode.go New strict-mode command; --reset/--global conflict check correct; --global on warns about profile-level off overrides but --global off has no symmetric warning for profile-level on (flagged in previous review thread).
shortcuts/common/runner.go checkScopePrereqs now calls ResolveToken (potential network call) instead of pure in-memory lookup; ctx param ordering issue and bot-mode TAT fetch flagged in previous threads.
internal/core/config.go Adds full multi-profile support (CurrentApp, PreviousApp, Name field on AppConfig); ValidateProfileName uses utf8.RuneCountInString correctly; CurrentAppConfig resolution priority is well-documented.
internal/cmdutil/testing.go Adds CredentialProvider stub to test factory; testDefaultToken.ResolveToken always returns empty Scopes, meaning scope-guard tests pass silently (flagged in previous review thread).
cmd/auth/login.go Correctly errors out in strict mode with explicit message; login and device-code paths now look up profile by name (multi.FindApp(config.ProfileName)) instead of hardcoded Apps[0].

Sequence Diagram

sequenceDiagram
    participant CLI as CLI Command
    participant Factory as cmdutil.Factory
    participant CP as CredentialProvider
    participant EnvP as env.Provider (extension)
    participant DefAcct as DefaultAccountProvider
    participant DefTok as DefaultTokenProvider
    participant KC as Keychain/Config

    CLI->>Factory: ResolveAs(cmd, flagAs)
    Factory->>Factory: IsStrictMode()? → force AsBot if true
    Factory-->>CLI: resolved identity

    CLI->>Factory: Config()
    Factory->>CP: ResolveAccount(ctx) [sync.Once]
    CP->>EnvP: ResolveAccount(ctx)
    alt LARK_APP_ID+SECRET set
        EnvP-->>CP: &Account{...}
        CP->>CP: enrichUserInfo() via LARK_USER_ACCESS_TOKEN
    else env vars absent
        EnvP-->>CP: nil, nil (skip)
        CP->>DefAcct: ResolveAccount(ctx)
        DefAcct->>KC: RequireConfigForProfile
        KC-->>DefAcct: AppConfig
        DefAcct-->>CP: *Account
    end
    CP-->>Factory: *Account (cached)
    Factory-->>CLI: *core.CliConfig

    CLI->>Factory: Credential.ResolveToken(ctx, TokenSpec{TAT|UAT})
    Factory->>CP: ResolveToken(ctx, req)
    CP->>EnvP: ResolveToken(ctx, spec)
    alt env token set
        EnvP-->>CP: &Token{Value}
        CP-->>CLI: &TokenResult{Token, Scopes}
    else
        EnvP-->>CP: nil, nil (skip)
        CP->>DefTok: ResolveToken(ctx, req)
        alt TAT
            DefTok->>DefTok: doResolveTAT [sync.Once] → HTTP POST
        else UAT
            DefTok->>DefTok: auth.GetValidAccessToken → keychain
        end
        DefTok-->>CP: *TokenResult
        CP-->>CLI: *TokenResult
    end
Loading

Reviews (4): Last reviewed commit: "fix: address bot review feedback (3 item..." | Re-trigger Greptile

Comment on lines +448 to 455
// checkScopePrereqs performs a fast local check: does the token
// contain all scopes declared by the shortcut? Returns the missing ones.
// If scope data is unavailable, returns nil (let the API call handle it).
func checkScopePrereqs(f *cmdutil.Factory, ctx context.Context, appID string, identity core.Identity, required []string) []string {
result, err := f.Credential.ResolveToken(ctx, credential.NewTokenSpec(identity, appID))
if err != nil || result == nil || result.Scopes == "" {
return nil
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Scope pre-check now triggers live token resolution (network calls)

checkScopePrereqs previously did a pure in-memory lookup (auth.GetStoredToken) with no side-effects. It is now replaced by a call to f.Credential.ResolveToken, which fully resolves the token — including making an HTTP request to the TAT endpoint for bot-mode or refreshing a UAT for user-mode on the very first invocation.

For bot mode, NewTokenSpec returns a TAT spec, which calls DefaultTokenProvider.resolveTATdoResolveTAT, an HTTP POST to /open-apis/auth/v3/tenant_access_token/internal. The old code had an explicit as != core.AsUser guard in checkShortcutScopes that prevented any scope pre-check for bots; that guard was removed, so the TAT fetch now always fires for every shortcut invocation.

The TAT is cached via sync.Once so only the first invocation pays the cost, but the change still:

  1. Converts a zero-cost pre-flight check into a blocking network call on the first command run.
  2. Makes the scope check silently succeed (returns nil) on any network error — so the check no longer does anything useful when the token endpoint is unreachable; previously it reliably checked the locally stored scopes.

Consider restoring the as != core.AsUser guard in checkShortcutScopes (bot tokens don't carry scope metadata, so the check is a no-op for bots) and keeping a fast-path for user mode that reads from auth.GetStoredToken before falling back to Credential.ResolveToken.

Comment on lines +448 to 455
// checkScopePrereqs performs a fast local check: does the token
// contain all scopes declared by the shortcut? Returns the missing ones.
// If scope data is unavailable, returns nil (let the API call handle it).
func checkScopePrereqs(f *cmdutil.Factory, ctx context.Context, appID string, identity core.Identity, required []string) []string {
result, err := f.Credential.ResolveToken(ctx, credential.NewTokenSpec(identity, appID))
if err != nil || result == nil || result.Scopes == "" {
return nil
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 ctx should be the first parameter per Go convention

Both checkScopePrereqs and checkShortcutScopes place ctx context.Context as the second parameter (after f *cmdutil.Factory). Go's official convention and the standard library consistently require context.Context to be the first parameter.

checkServiceScopes in cmd/service/service.go already follows the correct ordering (ctx context.Context first), creating an inconsistency between the two files.

Suggested change
// checkScopePrereqs performs a fast local check: does the token
// contain all scopes declared by the shortcut? Returns the missing ones.
// If scope data is unavailable, returns nil (let the API call handle it).
func checkScopePrereqs(f *cmdutil.Factory, ctx context.Context, appID string, identity core.Identity, required []string) []string {
result, err := f.Credential.ResolveToken(ctx, credential.NewTokenSpec(identity, appID))
if err != nil || result == nil || result.Scopes == "" {
return nil
}
func checkScopePrereqs(ctx context.Context, f *cmdutil.Factory, appID string, identity core.Identity, required []string) []string {

The same fix applies to checkShortcutScopes:

// before
func checkShortcutScopes(f *cmdutil.Factory, ctx context.Context, as core.Identity, ...)
// after
func checkShortcutScopes(ctx context.Context, f *cmdutil.Factory, as core.Identity, ...)

Comment on lines 71 to +75
}
return f, stdoutBuf, stderrBuf, reg
}

type testDefaultAcct struct {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 testDefaultToken returns empty Scopes for all types — may mask scope-related test failures

testDefaultToken.ResolveToken returns a TokenResult with an empty Scopes field unconditionally for both UAT and TAT. Because Scopes is always empty, every call to checkScopePrereqs / checkShortcutScopes / checkServiceScopes will short-circuit at the result.Scopes == "" guard and skip the scope check entirely.

Tests that exercise scope pre-checking logic will silently pass even when the scope-check code is broken, because the stub never populates scope data.

Consider extending the stub to accept configurable scope data so individual tests can inject realistic values when exercising scope guards:

type testDefaultToken struct {
    scopes string // empty by default; set in scope-related tests
}

This would make scope-guard tests self-contained and explicit about what they are testing.

Comment on lines +116 to +133
func (p *CredentialProvider) ResolveToken(ctx context.Context, req TokenSpec) (*TokenResult, error) {
for _, prov := range p.providers {
tok, err := prov.ResolveToken(ctx, extcred.TokenSpec{
Type: extcred.TokenType(req.Type),
Identity: string(req.Identity),
AppID: req.AppID,
})
if err != nil {
return nil, err
}
if tok != nil {
return &TokenResult{Token: tok.Value, Scopes: tok.Scopes}, nil
}
}
if p.defaultToken != nil {
return p.defaultToken.ResolveToken(ctx, req)
}
return nil, fmt.Errorf("no credential provider returned a token for %s", req.Type)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 BlockError handled differently in ResolveToken vs enrichUserInfo

In enrichUserInfo, a BlockError from a provider is explicitly treated as "skip enrichment and return success":

if errors.As(err, &blockErr) {
    return nil // provider explicitly blocks UAT; skip enrichment
}

In ResolveToken, any error (including BlockError) immediately propagates as a hard failure. This asymmetry is intentional per the design, but is undocumented at the ResolveToken call-site. A brief inline comment clarifying the intended semantics would reduce ambiguity for future provider authors:

if err != nil {
    // BlockError propagates as a hard failure — it stops the chain
    // and signals to the caller that the provider actively denied access.
    return nil, err
}

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment thread cmd/config/strict_mode.go
Comment on lines +74 to +82
}
boolVal := value == "on"

if global {
multi.StrictMode = boolVal
if boolVal {
for _, a := range multi.Apps {
if a.StrictMode != nil && !*a.StrictMode {
fmt.Fprintf(f.IOStreams.ErrOut, "Warning: profile %q has strict-mode explicitly set to off, which overrides the global setting. Use `lark-cli config strict-mode --reset` in that profile to inherit global.\n", a.ProfileName())
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 --global off does not warn about profile-level on overrides

When setting --global on, the code correctly warns about profiles that have StrictMode explicitly set to off. However, the symmetric case is unhandled: setting --global off when some profiles have StrictMode explicitly set to on produces no warning, even though those profiles will remain in strict mode.

} else if !boolVal {
    for _, a := range multi.Apps {
        if a.StrictMode != nil && *a.StrictMode {
            fmt.Fprintf(f.IOStreams.ErrOut,
                "Warning: profile %q has strict-mode explicitly set to on, which overrides the global setting.\n",
                a.ProfileName())
        }
    }
}

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: 19

🧹 Nitpick comments (4)
shortcuts/mail/mail_watch_test.go (1)

99-100: Reinstate method assertion for the profile dry-run call.

Line 99 now checks only URL, so a method regression on the profile endpoint would pass unnoticed. Please keep the explicit GET assertion for this API step as well.

Suggested patch
+	if apis[1].Method != "GET" {
+		t.Fatalf("unexpected profile method: %s", apis[1].Method)
+	}
 	if apis[1].URL != mailboxPath("me", "profile") {
 		t.Fatalf("unexpected profile url: %s", apis[1].URL)
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/mail/mail_watch_test.go` around lines 99 - 100, The test removed
the HTTP method check for the profile dry-run step; restore an explicit
assertion that apis[1].Method == "GET" alongside the existing URL check so a
method regression on the profile endpoint is caught. Locate the assertion that
currently checks apis[1].URL against mailboxPath("me", "profile") and add a
sibling assertion verifying the Method field equals "GET" (consistent with other
API step checks in this test) so the profile dry-run call is validated for both
URL and method.
shortcuts/im/helpers_network_test.go (1)

29-33: Assert the requested credential spec in the stub.

This resolver returns a token for any credential.TokenSpec, so these tests stop detecting regressions where the IM runtime asks for the wrong identity or token type. Capture the incoming spec and fail on unexpected values to keep the new credential wiring covered.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/im/helpers_network_test.go` around lines 29 - 33, The
staticShortcutTokenResolver stub currently returns the same token regardless of
the incoming credential.TokenSpec, which hides regressions; update the stub
(staticShortcutTokenResolver and its ResolveToken method) to capture the
incoming credential.TokenSpec and assert it matches an expected spec (e.g.,
store an expected spec or validator function on the resolver struct), and return
an error when it does not match; use a deterministic comparison
(reflect.DeepEqual or explicit field checks) so tests fail when the IM runtime
requests the wrong identity/token type before returning the TokenResult.
internal/client/client_test.go (1)

37-42: Make the token stub verify the requested spec.

Returning "test-token" for every credential.TokenSpec means these tests won't catch regressions where the client asks for the wrong identity or token flavor. Recording the incoming spec and asserting it in the helper would keep the credential-path coverage meaningful.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/client/client_test.go` around lines 37 - 42, The
staticTokenResolver::ResolveToken helper currently returns the same token for
every credential.TokenSpec, so update staticTokenResolver to store an expected
credential.TokenSpec (add a field like expectedSpec) and in ResolveToken compare
the incoming spec against that expectedSpec (or record the received spec into a
field like receivedSpec and return an error if it differs) before returning the
token; update tests to construct staticTokenResolver with the expected spec and
assert no mismatch occurred.
internal/client/client.go (1)

34-40: Add a nil guard before dereferencing Credential in DoSDKRequest().

Credential is not initialized in the empty APIClient{} composite literal pattern. While the production factory properly populates it, adding an early nil check prevents panics on misuse and provides a clearer error message.

Proposed fix
 func (c *APIClient) DoSDKRequest(ctx context.Context, req *larkcore.ApiReq, as core.Identity, extraOpts ...larkcore.RequestOptionFunc) (*larkcore.ApiResp, error) {
 	var opts []larkcore.RequestOptionFunc
 
+	if c.Credential == nil {
+		return nil, output.Errorf(output.ExitInternal, "internal", "credential provider is not configured")
+	}
 	result, err := c.Credential.ResolveToken(ctx, credential.NewTokenSpec(as, c.Config.AppID))
 	if err != nil {
 		return nil, err
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/client/client.go` around lines 34 - 40, DoSDKRequest currently
dereferences APIClient.Credential without checking for nil; add a nil guard at
the start of the APIClient.DoSDKRequest method to detect when c.Credential is
nil and return a descriptive error instead of panicking. Specifically, inside
the DoSDKRequest receiver (APIClient), check if c == nil or c.Credential == nil
and return an error like "missing credential provider on APIClient" (or wrap
with fmt.Errorf) before using c.Credential so callers receive a clear failure
instead of a nil pointer panic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/auth/login.go`:
- Around line 108-111: The code currently assumes config.ProfileName resolves
via core.LoadMultiAppConfig() and multi.FindApp(config.ProfileName) and silently
proceeds when app == nil, skipping Users overwrite and token cleanup and
defaulting lang to "zh"; change this so you either (1) resolve the target app
with a robust fallback—try finding by AppID, fall back to the "current" app in
multi config, or use a sensible default profile—then proceed with Users
overwrite and old-token cleanup, or (2) if no fallback is appropriate,
return/propagate an explicit error instead of continuing; update the blocks
using core.LoadMultiAppConfig(), multi.FindApp(config.ProfileName), and any
logic that sets lang or updates Users so they only skip when a clear fallback is
applied or an error is returned (also apply same fix to the other occurrences
around the indicated ranges).

In `@cmd/config/init.go`:
- Around line 120-129: When updating an existing profile where the AppId changes
(in the multi.Apps[idx] update block), also reset stored user state to avoid
carrying users/tokens from the previous app: set multi.Apps[idx].Users to an
empty slice or nil after changing AppId/AppSecret (and ensure any secret cleanup
via core.RemoveSecretStore remains). Mirror this same Users reset in the other
profile-update branch that handles the unchanged-secret path so both update
paths clear orphaned user state when AppId changes.

In `@cmd/config/strict_mode_test.go`:
- Around line 90-107: The setup step in TestStrictMode_Reset is not validated:
capture and check the error from the initial
NewCmdConfigStrictMode(...).SetArgs([]string{"on"}).Execute() call and assert
that the profile override was applied (load the config via
core.LoadMultiAppConfig() and assert app.StrictMode != nil && *app.StrictMode ==
true) before running the reset; then run the reset command and keep the existing
post-reset assertion. Reference NewCmdConfigStrictMode, cmd.Execute,
core.LoadMultiAppConfig, and app.StrictMode to locate and change the test.
- Around line 14-29: The shared test setup function setupStrictModeTestConfig
should clear the LARKSUITE_CLI_STRICT_MODE environment so tests don't inherit
the runner's setting; update setupStrictModeTestConfig to call
t.Setenv("LARKSUITE_CLI_STRICT_MODE", "") (or otherwise unset it) at the start
of the helper, leaving explicit tests (e.g., the override test) to set the
variable themselves.

In `@cmd/config/strict_mode.go`:
- Around line 47-57: The branch handling reset currently allows a simultaneous
--global flag and silently ignores it; add an explicit check at the start of the
reset branch to fail fast if both flags are present (e.g. if reset && global)
and return a validation error (similar to existing output.ErrValidation) so no
profile-scoped write occurs. Update the reset handling in the same function that
references app.StrictMode, core.SaveMultiAppConfig and f.IOStreams.ErrOut to
perform this check before clearing app.StrictMode or saving multi, returning an
error message like "--reset cannot be used with --global".

In `@cmd/profile/add.go`:
- Around line 72-75: The current handling around core.LoadMultiAppConfig()
silently converts any load error into an empty core.MultiAppConfig, risking
overwriting real configs; change it so you only initialize multi =
&core.MultiAppConfig{} when the error explicitly indicates "config not found"
(e.g. errors.Is(err, os.ErrNotExist) or a package sentinel like
core.ErrNotFound), otherwise return or surface the error (propagate it up from
the command) so parse/read failures are not swallowed; update the logic around
the multi variable and error handling in the Add profile path (the call to
core.LoadMultiAppConfig) to use errors.Is checks and fail fast for non-missing
errors.

In `@cmd/profile/list.go`:
- Around line 40-43: The code in cmd/profile/list.go currently treats any error
from core.LoadMultiAppConfig() as "not configured"; change this to only
special-case the explicit missing/uninitialized sentinel (e.g.,
core.ErrNotConfigured or whatever sentinel the core package exposes) by using
errors.Is(err, core.ErrNotConfigured) or the core-provided check, print the init
hint in that case, and for any other error propagate or return the original err
(or print the real error to ErrOut) so corrupted/unreadable config errors are
not swallowed.

In `@cmd/profile/remove.go`:
- Around line 49-70: Move the config mutation and call to
core.SaveMultiAppConfig(multi) to occur before any credential/keychain deletion
so we never remove secrets unless the profile removal is persisted;
specifically, after removing the app from multi.Apps and updating
multi.CurrentApp/multi.PreviousApp, call core.SaveMultiAppConfig(multi) and only
if it returns nil proceed to call core.RemoveSecretStore(app.AppSecret,
f.Keychain) and larkauth.RemoveStoredToken(app.AppId, user.UserOpenId) for each
user. Ensure you still return the save error immediately if SaveMultiAppConfig
fails and do not delete credentials in that error path.

In `@extension/credential/env/env_test.go`:
- Around line 31-63: Add a small helper (e.g., clearLarkEnv(t *testing.T)) that
clears the relevant env vars before each test by calling t.Setenv("LARK_APP_ID",
"") , t.Setenv("LARK_APP_SECRET", "") and t.Setenv("LARK_BRAND", "") (or
os.Unsetenv if you prefer) and invoke it at the start of
TestResolveAccount_NeitherSet, TestResolveAccount_OnlyIDSet,
TestResolveAccount_OnlySecretSet, TestResolveAccount_DefaultBrand (and the other
test around lines 87-92) so ResolveAccount (Provider.ResolveAccount) runs with a
clean environment.

In `@internal/cmdutil/factory_default.go`:
- Around line 72-83: The http.Client created for the Lark SDK lacks a Timeout
and can hang indefinitely; update the client passed to lark.WithHttpClient to
include a timeout (e.g. Timeout: 30 * time.Second) so SDK-backed requests mirror
the CLI's 30s timeout, ensuring you import time and keep the existing Transport
(UserAgentTransport and auth.SecurityPolicyTransport) and CheckRedirect
(safeRedirectPolicy) configuration intact.
- Around line 131-135: The fallback token resolution calls
DefaultTokenProvider.resolveUAT/resolveTAT which use
p.defaultAcct.ResolveAccount() (only keychain/config), so change construction to
ensure token fallback uses the aggregated account chain: create the aggregated
CredentialProvider first (using extcred.Providers()), then pass that
CredentialProvider into the DefaultTokenProvider (or modify DefaultTokenProvider
to accept and call credentialProvider.ResolveAccount() rather than
p.defaultAcct.ResolveAccount()). Update buildCredentialProvider to construct
credential.NewCredentialProvider(...) before creating defaultToken (or adjust
parameter order) so ResolveToken falls back via
CredentialProvider.ResolveAccount() consistent with
CredentialProvider.ResolveAccount().

In `@internal/cmdutil/factory.go`:
- Around line 121-124: The IsStrictMode method currently treats any non-empty
env value that isn't exactly "true" or "1" as false, silently disabling strict
mode; update Factory.IsStrictMode to normalize the env value (trim whitespace
and toLower) and accept a small explicit set of truthy values (e.g., "true",
"1", "yes", "on") and falsy values (e.g., "false", "0", "no", "off"); if the env
is set but not in either set, DO NOT return false—fall back to the factory's
configured strict setting (the existing config field on Factory, e.g.,
f.config.StrictMode or equivalent) so invalid env values do not silently
downgrade the policy.

In `@internal/core/config.go`:
- Around line 66-84: CurrentAppConfig currently ignores the
LARKSUITE_CLI_PROFILE env while RequireConfigForProfile applies it separately;
centralize the effective-profile resolution so both policy checks and config
loading use the same value by updating CurrentAppConfig (or adding a shared
resolver like ResolveEffectiveProfile) to consult LARKSUITE_CLI_PROFILE first,
then profileOverride, then m.CurrentApp, then fallback to m.Apps[0]; adjust
callers (e.g., RequireConfigForProfile) to use this single resolver instead of
applying the env fallback themselves so strict-mode paths and config lookups
target the same profile.

In `@internal/credential/credential_provider_test.go`:
- Around line 11-24: TestCredentialProvider_AccountCached currently doesn't
validate caching because mockExtProvider.ResolveAccount always returns the same
pointer; update the test to either (a) add a call counter field on
mockExtProvider and increment it inside ResolveAccount then assert the counter
== 1 after two ResolveAccount calls, or (b) change ResolveAccount to return a
newly allocated *extcred.Account each invocation and assert the provider's
invocation count is 1; reference mockExtProvider and its ResolveAccount method
and the TestCredentialProvider_AccountCached test to implement the
counter/assertion change (also apply the same fix to the similar assertions
around lines 90-99).

In `@internal/credential/credential_provider.go`:
- Around line 87-95: The loop over p.providers incorrectly calls
prov.ResolveToken(ctx, extcred.TokenSpec{Type: extcred.TokenTypeUAT}) without
identifying which account/app/user to resolve and it swallows all non-BlockError
errors; change the ResolveToken call to populate TokenSpec with the
account/app/user context used for enrichment (so multi-account providers can
disambiguate), and change the error handling so that: if errors.As(err,
&extcred.BlockError{}) then continue (skip), if err != nil return err (surface
real provider failures), and if err==nil && tok==nil continue (provider
skipped). Update references in this block to p.providers, ResolveToken,
extcred.TokenSpec, TokenTypeUAT, and extcred.BlockError accordingly.

In `@internal/credential/integration_test.go`:
- Around line 43-61: TestFullChain_Fallthrough assumes envprovider.Provider
returns no creds but doesn't clear process env; ensure the test unsets or clears
LARK_APP_ID, LARK_APP_SECRET, and LARK_USER_ACCESS_TOKEN before creating ep :=
&envprovider.Provider{} so the env provider cannot win. Modify
TestFullChain_Fallthrough to call os.Unsetenv (or equivalent) for those three
env names at the start and optionally restore previous values after the test,
then proceed to construct credential.NewCredentialProvider and assert the mock
default token path.

In `@internal/credential/user_info.go`:
- Around line 52-55: The user_info handler currently treats API responses with
Code==0 as success even if result.Data.OpenID is empty; update the logic in the
function that constructs and returns &userInfo{OpenID: result.Data.OpenID, Name:
result.Data.Name} to validate result.Data.OpenID is non-empty and return an
error (e.g., fmt.Errorf("user_info missing open_id")) when it is empty so
malformed payloads are rejected before updating accounts; reference the result,
result.Data.OpenID and userInfo construction in your change.

In `@shortcuts/common/runner.go`:
- Around line 92-96: The code calls ctx.Factory.Credential.ResolveToken(...) and
immediately dereferences result.Token, which can panic if ResolveToken returns
(nil, nil); add a nil-check after the call (e.g., if result == nil) and return a
proper auth error via output.ErrAuth with a clear message (including context
like AppID) instead of dereferencing; update the return path in the function
that contains this call so it safely handles a nil result from ResolveToken
before accessing result.Token.

---

Nitpick comments:
In `@internal/client/client_test.go`:
- Around line 37-42: The staticTokenResolver::ResolveToken helper currently
returns the same token for every credential.TokenSpec, so update
staticTokenResolver to store an expected credential.TokenSpec (add a field like
expectedSpec) and in ResolveToken compare the incoming spec against that
expectedSpec (or record the received spec into a field like receivedSpec and
return an error if it differs) before returning the token; update tests to
construct staticTokenResolver with the expected spec and assert no mismatch
occurred.

In `@internal/client/client.go`:
- Around line 34-40: DoSDKRequest currently dereferences APIClient.Credential
without checking for nil; add a nil guard at the start of the
APIClient.DoSDKRequest method to detect when c.Credential is nil and return a
descriptive error instead of panicking. Specifically, inside the DoSDKRequest
receiver (APIClient), check if c == nil or c.Credential == nil and return an
error like "missing credential provider on APIClient" (or wrap with fmt.Errorf)
before using c.Credential so callers receive a clear failure instead of a nil
pointer panic.

In `@shortcuts/im/helpers_network_test.go`:
- Around line 29-33: The staticShortcutTokenResolver stub currently returns the
same token regardless of the incoming credential.TokenSpec, which hides
regressions; update the stub (staticShortcutTokenResolver and its ResolveToken
method) to capture the incoming credential.TokenSpec and assert it matches an
expected spec (e.g., store an expected spec or validator function on the
resolver struct), and return an error when it does not match; use a
deterministic comparison (reflect.DeepEqual or explicit field checks) so tests
fail when the IM runtime requests the wrong identity/token type before returning
the TokenResult.

In `@shortcuts/mail/mail_watch_test.go`:
- Around line 99-100: The test removed the HTTP method check for the profile
dry-run step; restore an explicit assertion that apis[1].Method == "GET"
alongside the existing URL check so a method regression on the profile endpoint
is caught. Locate the assertion that currently checks apis[1].URL against
mailboxPath("me", "profile") and add a sibling assertion verifying the Method
field equals "GET" (consistent with other API step checks in this test) so the
profile dry-run call is validated for both URL and method.
🪄 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: 30f71a27-ce1c-4479-bfca-26897298bdfc

📥 Commits

Reviewing files that changed from the base of the PR and between 6692300 and d56fb95.

📒 Files selected for processing (66)
  • cmd/api/api.go
  • cmd/api/api_test.go
  • cmd/auth/list.go
  • cmd/auth/login.go
  • cmd/auth/login_strict_test.go
  • cmd/auth/logout.go
  • cmd/config/config.go
  • cmd/config/default_as.go
  • cmd/config/init.go
  • cmd/config/init_interactive.go
  • cmd/config/show.go
  • cmd/config/strict_mode.go
  • cmd/config/strict_mode_test.go
  • cmd/profile/add.go
  • cmd/profile/list.go
  • cmd/profile/profile.go
  • cmd/profile/remove.go
  • cmd/profile/rename.go
  • cmd/profile/use.go
  • cmd/root.go
  • cmd/root_e2e_test.go
  • cmd/service/service.go
  • cmd/service/service_test.go
  • extension/credential/env/env.go
  • extension/credential/env/env_test.go
  • extension/credential/registry.go
  • extension/credential/registry_test.go
  • extension/credential/types.go
  • internal/client/client.go
  • internal/client/client_test.go
  • internal/cmdutil/factory.go
  • internal/cmdutil/factory_default.go
  • internal/cmdutil/factory_test.go
  • internal/cmdutil/testing.go
  • internal/core/config.go
  • internal/core/config_strict_mode_test.go
  • internal/core/types.go
  • internal/credential/credential_provider.go
  • internal/credential/credential_provider_test.go
  • internal/credential/default_provider.go
  • internal/credential/default_provider_test.go
  • internal/credential/integration_test.go
  • internal/credential/types.go
  • internal/credential/types_test.go
  • internal/credential/user_info.go
  • main.go
  • shortcuts/base/base_advperm_test.go
  • shortcuts/base/base_dashboard_execute_test.go
  • shortcuts/base/base_execute_test.go
  • shortcuts/base/base_form_execute_test.go
  • shortcuts/base/base_role_test.go
  • shortcuts/base/workflow_execute_test.go
  • shortcuts/calendar/calendar_test.go
  • shortcuts/common/runner.go
  • shortcuts/doc/doc_media_test.go
  • shortcuts/drive/drive_io_test.go
  • shortcuts/im/convert_lib/helpers_test.go
  • shortcuts/im/convert_lib/merge_test.go
  • shortcuts/im/convert_lib/runtime_test.go
  • shortcuts/im/convert_lib/thread_test.go
  • shortcuts/im/coverage_additional_test.go
  • shortcuts/im/helpers_network_test.go
  • shortcuts/im/im_messages_search_execute_test.go
  • shortcuts/mail/mail_watch_test.go
  • shortcuts/vc/vc_notes.go
  • shortcuts/vc/vc_notes_test.go
💤 Files with no reviewable changes (17)
  • shortcuts/vc/vc_notes_test.go
  • shortcuts/doc/doc_media_test.go
  • shortcuts/im/convert_lib/helpers_test.go
  • shortcuts/base/base_dashboard_execute_test.go
  • shortcuts/im/convert_lib/thread_test.go
  • shortcuts/im/convert_lib/merge_test.go
  • shortcuts/base/base_advperm_test.go
  • shortcuts/im/coverage_additional_test.go
  • cmd/api/api_test.go
  • shortcuts/base/base_role_test.go
  • shortcuts/base/base_form_execute_test.go
  • shortcuts/calendar/calendar_test.go
  • shortcuts/im/im_messages_search_execute_test.go
  • shortcuts/base/base_execute_test.go
  • cmd/root_e2e_test.go
  • cmd/service/service_test.go
  • shortcuts/base/workflow_execute_test.go

Comment thread cmd/auth/login.go
Comment on lines +108 to +111
if multi, _ := core.LoadMultiAppConfig(); multi != nil {
if app := multi.FindApp(config.ProfileName); app != nil {
lang = app.Lang
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't silently skip profile bookkeeping when config.ProfileName can't be resolved.

These new lookups assume the profile name is always populated and still present in the config. If it is blank or stale, login still stores the token but skips Users overwrite and old-token cleanup while reporting success; the same lookup also silently falls back to "zh" for UI copy. Please resolve the target app with a fallback (for example, current app or AppID match) or return an error instead of continuing on app == nil.

Also applies to: 314-325, 396-407

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/auth/login.go` around lines 108 - 111, The code currently assumes
config.ProfileName resolves via core.LoadMultiAppConfig() and
multi.FindApp(config.ProfileName) and silently proceeds when app == nil,
skipping Users overwrite and token cleanup and defaulting lang to "zh"; change
this so you either (1) resolve the target app with a robust fallback—try finding
by AppID, fall back to the "current" app in multi config, or use a sensible
default profile—then proceed with Users overwrite and old-token cleanup, or (2)
if no fallback is appropriate, return/propagate an explicit error instead of
continuing; update the blocks using core.LoadMultiAppConfig(),
multi.FindApp(config.ProfileName), and any logic that sets lang or updates Users
so they only skip when a clear fallback is applied or an error is returned (also
apply same fix to the other occurrences around the indicated ranges).

Comment thread cmd/config/init.go
Comment thread cmd/config/strict_mode_test.go
Comment on lines +90 to +107
// First set it on
cmd := NewCmdConfigStrictMode(f)
cmd.SetArgs([]string{"on"})
cmd.Execute()

// Then reset
cmd = NewCmdConfigStrictMode(f)
cmd.SetArgs([]string{"--reset"})
err := cmd.Execute()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

multi, _ := core.LoadMultiAppConfig()
app := multi.CurrentAppConfig("")
if app.StrictMode != nil {
t.Errorf("expected nil StrictMode after reset, got %v", *app.StrictMode)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

TestStrictMode_Reset can pass without proving reset works.

The initial "on" command ignores its error and there is no intermediate assertion, so a broken setup step or a no-op reset still leaves StrictMode == nil and this test passes. Assert the profile override became true before running --reset.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/config/strict_mode_test.go` around lines 90 - 107, The setup step in
TestStrictMode_Reset is not validated: capture and check the error from the
initial NewCmdConfigStrictMode(...).SetArgs([]string{"on"}).Execute() call and
assert that the profile override was applied (load the config via
core.LoadMultiAppConfig() and assert app.StrictMode != nil && *app.StrictMode ==
true) before running the reset; then run the reset command and keep the existing
post-reset assertion. Reference NewCmdConfigStrictMode, cmd.Execute,
core.LoadMultiAppConfig, and app.StrictMode to locate and change the test.

Comment thread cmd/config/strict_mode.go
Comment on lines +87 to +95
for _, prov := range p.providers {
tok, err := prov.ResolveToken(ctx, extcred.TokenSpec{Type: extcred.TokenTypeUAT})
if err != nil {
var blockErr *extcred.BlockError
if errors.As(err, &blockErr) {
return nil // provider explicitly blocks UAT; skip enrichment
}
continue
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't suppress real token-provider failures during account enrichment.

Line 88 asks each provider for a UAT using only Type, so multi-account providers cannot tell which app/user to resolve. Then Lines 89-95 swallow every non-BlockError, even though nil, nil is already the provider contract for “skip”. That can bind the wrong user to the resolved account or mask a broken provider.

Suggested fix
-		tok, err := prov.ResolveToken(ctx, extcred.TokenSpec{Type: extcred.TokenTypeUAT})
+		tok, err := prov.ResolveToken(ctx, extcred.TokenSpec{
+			Type:     extcred.TokenTypeUAT,
+			Identity: string(core.AsUser),
+			AppID:    acct.AppID,
+		})
 		if err != nil {
 			var blockErr *extcred.BlockError
 			if errors.As(err, &blockErr) {
 				return nil // provider explicitly blocks UAT; skip enrichment
 			}
-			continue
+			return fmt.Errorf("%s failed to resolve UAT for user enrichment: %w", prov.Name(), err)
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/credential/credential_provider.go` around lines 87 - 95, The loop
over p.providers incorrectly calls prov.ResolveToken(ctx,
extcred.TokenSpec{Type: extcred.TokenTypeUAT}) without identifying which
account/app/user to resolve and it swallows all non-BlockError errors; change
the ResolveToken call to populate TokenSpec with the account/app/user context
used for enrichment (so multi-account providers can disambiguate), and change
the error handling so that: if errors.As(err, &extcred.BlockError{}) then
continue (skip), if err != nil return err (surface real provider failures), and
if err==nil && tok==nil continue (provider skipped). Update references in this
block to p.providers, ResolveToken, extcred.TokenSpec, TokenTypeUAT, and
extcred.BlockError accordingly.

Comment on lines +43 to +61
func TestFullChain_Fallthrough(t *testing.T) {
// env provider returns nil (no env vars set), falls through to default token
ep := &envprovider.Provider{}
mock := &mockDefaultTokenProvider{token: "mock_tok", scopes: "drive:read"}

cp := credential.NewCredentialProvider(
[]extcred.Provider{ep},
nil, mock, nil,
)
result, err := cp.ResolveToken(context.Background(), credential.TokenSpec{
Type: credential.TokenTypeUAT, Identity: core.AsUser, AppID: "app1",
})
if err != nil {
t.Fatal(err)
}
if result.Token != "mock_tok" || result.Scopes != "drive:read" {
t.Errorf("unexpected: %+v", result)
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Clear credential env vars before the fallthrough case.

TestFullChain_Fallthrough assumes the env provider returns nothing, but it never unsets LARK_APP_ID, LARK_APP_SECRET, or LARK_USER_ACCESS_TOKEN. If those are present in the runner environment, the env provider wins and this test stops exercising the default resolver.

🧪 Proposed fix
 func TestFullChain_Fallthrough(t *testing.T) {
+	t.Setenv("LARK_APP_ID", "")
+	t.Setenv("LARK_APP_SECRET", "")
+	t.Setenv("LARK_USER_ACCESS_TOKEN", "")
+
 	// env provider returns nil (no env vars set), falls through to default token
 	ep := &envprovider.Provider{}
 	mock := &mockDefaultTokenProvider{token: "mock_tok", scopes: "drive:read"}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func TestFullChain_Fallthrough(t *testing.T) {
// env provider returns nil (no env vars set), falls through to default token
ep := &envprovider.Provider{}
mock := &mockDefaultTokenProvider{token: "mock_tok", scopes: "drive:read"}
cp := credential.NewCredentialProvider(
[]extcred.Provider{ep},
nil, mock, nil,
)
result, err := cp.ResolveToken(context.Background(), credential.TokenSpec{
Type: credential.TokenTypeUAT, Identity: core.AsUser, AppID: "app1",
})
if err != nil {
t.Fatal(err)
}
if result.Token != "mock_tok" || result.Scopes != "drive:read" {
t.Errorf("unexpected: %+v", result)
}
}
func TestFullChain_Fallthrough(t *testing.T) {
t.Setenv("LARK_APP_ID", "")
t.Setenv("LARK_APP_SECRET", "")
t.Setenv("LARK_USER_ACCESS_TOKEN", "")
// env provider returns nil (no env vars set), falls through to default token
ep := &envprovider.Provider{}
mock := &mockDefaultTokenProvider{token: "mock_tok", scopes: "drive:read"}
cp := credential.NewCredentialProvider(
[]extcred.Provider{ep},
nil, mock, nil,
)
result, err := cp.ResolveToken(context.Background(), credential.TokenSpec{
Type: credential.TokenTypeUAT, Identity: core.AsUser, AppID: "app1",
})
if err != nil {
t.Fatal(err)
}
if result.Token != "mock_tok" || result.Scopes != "drive:read" {
t.Errorf("unexpected: %+v", result)
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/credential/integration_test.go` around lines 43 - 61,
TestFullChain_Fallthrough assumes envprovider.Provider returns no creds but
doesn't clear process env; ensure the test unsets or clears LARK_APP_ID,
LARK_APP_SECRET, and LARK_USER_ACCESS_TOKEN before creating ep :=
&envprovider.Provider{} so the env provider cannot win. Modify
TestFullChain_Fallthrough to call os.Unsetenv (or equivalent) for those three
env names at the start and optionally restore previous values after the test,
then proceed to construct credential.NewCredentialProvider and assert the mock
default token path.

Comment on lines +52 to +55
if result.Code != 0 {
return nil, fmt.Errorf("user_info API error: [%d] %s", result.Code, result.Msg)
}
return &userInfo{OpenID: result.Data.OpenID, Name: result.Data.Name}, nil
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reject user_info successes that omit open_id.

open_id is the stable identifier this enrichment path needs. Returning success with OpenID == "" lets a malformed payload look valid and can leave the upstream account update in a bad state.

Suggested guard
 	if result.Code != 0 {
 		return nil, fmt.Errorf("user_info API error: [%d] %s", result.Code, result.Msg)
 	}
+	if result.Data.OpenID == "" {
+		return nil, fmt.Errorf("user_info API returned empty open_id")
+	}
 	return &userInfo{OpenID: result.Data.OpenID, Name: result.Data.Name}, nil
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if result.Code != 0 {
return nil, fmt.Errorf("user_info API error: [%d] %s", result.Code, result.Msg)
}
return &userInfo{OpenID: result.Data.OpenID, Name: result.Data.Name}, nil
if result.Code != 0 {
return nil, fmt.Errorf("user_info API error: [%d] %s", result.Code, result.Msg)
}
if result.Data.OpenID == "" {
return nil, fmt.Errorf("user_info API returned empty open_id")
}
return &userInfo{OpenID: result.Data.OpenID, Name: result.Data.Name}, nil
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/credential/user_info.go` around lines 52 - 55, The user_info handler
currently treats API responses with Code==0 as success even if
result.Data.OpenID is empty; update the logic in the function that constructs
and returns &userInfo{OpenID: result.Data.OpenID, Name: result.Data.Name} to
validate result.Data.OpenID is non-empty and return an error (e.g.,
fmt.Errorf("user_info missing open_id")) when it is empty so malformed payloads
are rejected before updating accounts; reference the result, result.Data.OpenID
and userInfo construction in your change.

Comment on lines +92 to +96
result, err := ctx.Factory.Credential.ResolveToken(ctx.ctx, credential.NewTokenSpec(ctx.As(), ctx.Config.AppID))
if err != nil {
return "", output.ErrAuth("failed to get access token: %s", err)
}
return token, nil
return result.Token, nil
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Handle a missing token result before dereferencing it.

ResolveToken is already treated as nullable elsewhere in this PR. If it returns (nil, nil) here, result.Token panics instead of returning an auth error.

Suggested fix
 	result, err := ctx.Factory.Credential.ResolveToken(ctx.ctx, credential.NewTokenSpec(ctx.As(), ctx.Config.AppID))
 	if err != nil {
 		return "", output.ErrAuth("failed to get access token: %s", err)
 	}
+	if result == nil || result.Token == "" {
+		return "", output.ErrAuth("failed to get access token: no token resolved for %s", ctx.As())
+	}
 	return result.Token, nil
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
result, err := ctx.Factory.Credential.ResolveToken(ctx.ctx, credential.NewTokenSpec(ctx.As(), ctx.Config.AppID))
if err != nil {
return "", output.ErrAuth("failed to get access token: %s", err)
}
return token, nil
return result.Token, nil
result, err := ctx.Factory.Credential.ResolveToken(ctx.ctx, credential.NewTokenSpec(ctx.As(), ctx.Config.AppID))
if err != nil {
return "", output.ErrAuth("failed to get access token: %s", err)
}
if result == nil || result.Token == "" {
return "", output.ErrAuth("failed to get access token: no token resolved for %s", ctx.As())
}
return result.Token, nil
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/common/runner.go` around lines 92 - 96, The code calls
ctx.Factory.Credential.ResolveToken(...) and immediately dereferences
result.Token, which can panic if ResolveToken returns (nil, nil); add a
nil-check after the call (e.g., if result == nil) and return a proper auth error
via output.ErrAuth with a clear message (including context like AppID) instead
of dereferencing; update the return path in the function that contains this call
so it safely handles a nil result from ResolveToken before accessing
result.Token.

Comment thread shortcuts/vc/vc_notes.go
Comment on lines +447 to 453
result, err := runtime.Factory.Credential.ResolveToken(ctx, credential.NewTokenSpec(runtime.As(), runtime.Config.AppID))
if err == nil && result != nil && result.Scopes != "" {
if missing := auth.MissingScopes(result.Scopes, required); len(missing) > 0 {
return output.ErrWithHint(output.ExitAuth, "missing_scope",
fmt.Sprintf("missing required scope(s): %s", strings.Join(missing, ", ")),
fmt.Sprintf("run `lark-cli auth login --scope \"%s\"` in the background. It blocks and outputs a verification URL — retrieve the URL and open it in a browser to complete login.", strings.Join(missing, " ")))
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Keep scope validation side-effect free.

ResolveToken is an active provider operation, not just a metadata lookup. Calling it from Validate makes argument validation depend on live credential resolution, and the err == nil gate silently disables the scope check whenever the provider errors. This should read cached scope metadata only, or skip the extra check when that metadata is unavailable.

tuxedomm and others added 2 commits April 2, 2026 20:51
Replace downloadURLToTempFile with two new paths:
- Image uploads: stream HTTP response body directly as io.Reader
- File/video/audio uploads: buffer in memory via mediaBuffer struct,
  which provides Duration() for seek-like access and Reader() for upload

SDK Formdata.content() already reads everything into memory, so temp
files were redundant disk I/O. Local file upload paths unchanged.

Change-Id: I264bf114155e56bee32a9729e6673c6bae43d355
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract mediaSpec struct and resolve helpers to eliminate repetitive
if/else branches. Image/file/audio follow a uniform pattern via
resolveOneMedia; video stays separate due to its dual-key output.

Change-Id: Id27b92ef44c23f3a143994a3d1aee3baba8be112
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@shortcuts/im/helpers.go`:
- Around line 348-359: The code uploads the video (resolveOneMedia(videoSpec))
before verifying the cover, wasting work if the cover is invalid; swap the order
so you construct coverSpec and call resolveOneMedia(ctx, runtime, coverSpec)
first (handling its error with the same fmt.Errorf("cover image upload failed:
%w", err) or mediaFallbackOrError as appropriate), then call resolveOneMedia for
videoSpec (fKey) only after coverKey succeeds; update any variables (videoSpec,
videoVal, videoCoverVal, mediaKindImage, maxImageUploadSize) usage to match the
new flow and keep existing error-return semantics.
- Around line 879-881: The log prints the raw download error which can leak
sensitive URL data; add a helper (e.g., redactDownloadError) that strips URL
details by detecting *url.Error via errors.As and returning only the operation
and inner error (or err.Error() otherwise), then change the download failure
branch in downloadURLToReader's caller (the code around
sanitizeURLForDisplay(imgURL) in the block where rc, _, err :=
downloadURLToReader(...) is handled) to log the sanitized URL and
redactDownloadError(err) instead of the raw err (replace the %v usage with the
redacted string).
- Around line 190-191: The code currently infers extension from the original
rawURL (filepath.Ext(fileNameFromURL(rawURL))) which can be empty if redirects
changed the final resource; change it to use the final response URL from
resp.Request (e.g., resp.Request.URL) to compute the extension instead. Update
the code around the ext computation to check resp and resp.Request for nil,
derive finalURL := resp.Request.URL.String() when available, call
fileNameFromURL(finalURL) and then filepath.Ext(...) so mediaBuffer.FileType()
gets the correct extension (fall back to rawURL only if resp.Request is nil).
🪄 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: dce63b7e-34d9-47f6-b2af-c2885eef615a

📥 Commits

Reviewing files that changed from the base of the PR and between d56fb95 and 6412cca.

📒 Files selected for processing (2)
  • shortcuts/im/coverage_additional_test.go
  • shortcuts/im/helpers.go

Comment thread shortcuts/im/helpers.go
Comment on lines 190 to +191
ext := filepath.Ext(fileNameFromURL(rawURL))
tmpFile, err := os.CreateTemp("", "lark-media-*"+ext)
if err != nil {
return "", fmt.Errorf("create temp file: %w", err)
}
return resp, ext, nil
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Infer the media extension from the final response URL.

http.Client may already have followed redirects by Line 190. If a short/signed URL without an extension lands on .mp4 or .ogg, filepath.Ext(fileNameFromURL(rawURL)) stays empty, so mediaBuffer.FileType() falls back to stream and duration parsing is skipped for --audio / --video.

💡 Suggested fix
-	ext := filepath.Ext(fileNameFromURL(rawURL))
+	finalURL := rawURL
+	if resp.Request != nil && resp.Request.URL != nil {
+		finalURL = resp.Request.URL.String()
+	}
+	ext := filepath.Ext(fileNameFromURL(finalURL))
 	return resp, ext, nil
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ext := filepath.Ext(fileNameFromURL(rawURL))
tmpFile, err := os.CreateTemp("", "lark-media-*"+ext)
if err != nil {
return "", fmt.Errorf("create temp file: %w", err)
}
return resp, ext, nil
finalURL := rawURL
if resp.Request != nil && resp.Request.URL != nil {
finalURL = resp.Request.URL.String()
}
ext := filepath.Ext(fileNameFromURL(finalURL))
return resp, ext, nil
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/im/helpers.go` around lines 190 - 191, The code currently infers
extension from the original rawURL (filepath.Ext(fileNameFromURL(rawURL))) which
can be empty if redirects changed the final resource; change it to use the final
response URL from resp.Request (e.g., resp.Request.URL) to compute the extension
instead. Update the code around the ext computation to check resp and
resp.Request for nil, derive finalURL := resp.Request.URL.String() when
available, call fileNameFromURL(finalURL) and then filepath.Ext(...) so
mediaBuffer.FileType() gets the correct extension (fall back to rawURL only if
resp.Request is nil).

Comment thread shortcuts/im/helpers.go
Comment on lines +348 to +359
fKey, err := resolveOneMedia(ctx, runtime, videoSpec)
if err != nil {
return mediaFallbackOrError(videoVal, "video", err)
}

coverSpec := mediaSpec{
value: videoCoverVal, flagName: "--video-cover", mediaType: "cover image",
kind: mediaKindImage, maxSize: maxImageUploadSize, resultKey: "image_key",
}
coverKey, err := resolveOneMedia(ctx, runtime, coverSpec)
if err != nil {
return "", "", fmt.Errorf("cover image upload failed: %w", err)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Resolve --video-cover before uploading the video.

Line 348 performs the expensive remote file upload before Line 357 proves the required cover can be resolved. If the cover path/URL is bad, the command fails only after a wasted write that the user will usually retry.

💡 Suggested fix
-	fKey, err := resolveOneMedia(ctx, runtime, videoSpec)
-	if err != nil {
-		return mediaFallbackOrError(videoVal, "video", err)
-	}
-
 	coverSpec := mediaSpec{
 		value: videoCoverVal, flagName: "--video-cover", mediaType: "cover image",
 		kind: mediaKindImage, maxSize: maxImageUploadSize, resultKey: "image_key",
 	}
 	coverKey, err := resolveOneMedia(ctx, runtime, coverSpec)
 	if err != nil {
 		return "", "", fmt.Errorf("cover image upload failed: %w", err)
 	}
+
+	fKey, err := resolveOneMedia(ctx, runtime, videoSpec)
+	if err != nil {
+		return mediaFallbackOrError(videoVal, "video", err)
+	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fKey, err := resolveOneMedia(ctx, runtime, videoSpec)
if err != nil {
return mediaFallbackOrError(videoVal, "video", err)
}
coverSpec := mediaSpec{
value: videoCoverVal, flagName: "--video-cover", mediaType: "cover image",
kind: mediaKindImage, maxSize: maxImageUploadSize, resultKey: "image_key",
}
coverKey, err := resolveOneMedia(ctx, runtime, coverSpec)
if err != nil {
return "", "", fmt.Errorf("cover image upload failed: %w", err)
coverSpec := mediaSpec{
value: videoCoverVal, flagName: "--video-cover", mediaType: "cover image",
kind: mediaKindImage, maxSize: maxImageUploadSize, resultKey: "image_key",
}
coverKey, err := resolveOneMedia(ctx, runtime, coverSpec)
if err != nil {
return "", "", fmt.Errorf("cover image upload failed: %w", err)
}
fKey, err := resolveOneMedia(ctx, runtime, videoSpec)
if err != nil {
return mediaFallbackOrError(videoVal, "video", err)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/im/helpers.go` around lines 348 - 359, The code uploads the video
(resolveOneMedia(videoSpec)) before verifying the cover, wasting work if the
cover is invalid; swap the order so you construct coverSpec and call
resolveOneMedia(ctx, runtime, coverSpec) first (handling its error with the same
fmt.Errorf("cover image upload failed: %w", err) or mediaFallbackOrError as
appropriate), then call resolveOneMedia for videoSpec (fKey) only after coverKey
succeeds; update any variables (videoSpec, videoVal, videoCoverVal,
mediaKindImage, maxImageUploadSize) usage to match the new flow and keep
existing error-return semantics.

Comment thread shortcuts/im/helpers.go
Comment on lines +879 to 881
rc, _, err := downloadURLToReader(ctx, runtime, imgURL, maxImageUploadSize)
if err != nil {
fmt.Fprintf(runtime.IO().ErrOut, "warning: failed to download image %s: %v\n", sanitizeURLForDisplay(imgURL), err)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Redact the underlying download error before logging it.

This branch already sanitizes imgURL, but %v on err can still include the full source URL from url.Error or URL-parse errors. Failed markdown-image downloads would then leak signed query parameters into stderr / CI logs.

💡 Suggested fix
-			fmt.Fprintf(runtime.IO().ErrOut, "warning: failed to download image %s: %v\n", sanitizeURLForDisplay(imgURL), err)
+			fmt.Fprintf(runtime.IO().ErrOut, "warning: failed to download image %s: %s\n", sanitizeURLForDisplay(imgURL), redactDownloadError(err))
func redactDownloadError(err error) string {
	var urlErr *url.Error
	if errors.As(err, &urlErr) {
		return fmt.Sprintf("%s: %v", urlErr.Op, urlErr.Err)
	}
	return err.Error()
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/im/helpers.go` around lines 879 - 881, The log prints the raw
download error which can leak sensitive URL data; add a helper (e.g.,
redactDownloadError) that strips URL details by detecting *url.Error via
errors.As and returning only the operation and inner error (or err.Error()
otherwise), then change the download failure branch in downloadURLToReader's
caller (the code around sanitizeURLForDisplay(imgURL) in the block where rc, _,
err := downloadURLToReader(...) is handled) to log the sanitized URL and
redactDownloadError(err) instead of the raw err (replace the %v usage with the
redacted string).

Add forbidigo lint rules scoped to shortcuts/ (excluding tests) to
prevent temp file intermediary patterns and direct process exits.
Only checks incremental code (--new-from-rev), existing code unaffected.

Change-Id: Ie7f2e79193bb1298e28bf1613961de470042dd16
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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)
.golangci.yml (1)

62-67: os\.Remove\b excludes os.RemoveAll — consider clarifying intent.

The word boundary \b in the pattern means os.RemoveAll will not be flagged. While os.RemoveAll is not currently used in shortcuts/, it's worth being explicit about whether both functions should be forbidden or just one. If both should be forbidden, remove the \b or add a separate pattern for os.RemoveAll.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.golangci.yml around lines 62 - 67, The golangci-lint rule currently targets
the regex pattern `os\.Remove\b` which excludes `os.RemoveAll`; update the rule
for clarity and coverage: decide if both `os.Remove` and `os.RemoveAll` should
be forbidden in `shortcuts/` and then either remove the `\b` from the pattern or
add an explicit second pattern for `os.RemoveAll`, and update the rule message
to mention both `os.Remove` and `os.RemoveAll` and reference
`shortcuts/im/helpers.go` as the recommended alternative.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.golangci.yml:
- Around line 54-73: Move the per-pattern exclude-godoc-examples settings into
the forbidigo top-level: under the forbidigo block add exclude-godoc-examples:
true once, then keep the forbid list (patterns os\.CreateTemp, os\.Remove\b,
os\.Exit and their msg entries) unchanged beneath it; retain analyze-types: true
at the same level so the option applies globally instead of being ignored
per-pattern.
- Line 30: Pre-existing use of os.Exit in the shortcuts mail watcher will break
the new forbidigo linter; fix it by either adding a localized suppression
comment "//nolint:forbidigo" on the specific os.Exit(...) invocation (include a
short TODO referencing a tracking issue/PR for refactor) or update CI lint
invocation to pass "--new-from-rev=origin/main" so pre-existing violations are
ignored; target the os.Exit call in the mail watcher code path and apply one of
these two mitigations.

---

Nitpick comments:
In @.golangci.yml:
- Around line 62-67: The golangci-lint rule currently targets the regex pattern
`os\.Remove\b` which excludes `os.RemoveAll`; update the rule for clarity and
coverage: decide if both `os.Remove` and `os.RemoveAll` should be forbidden in
`shortcuts/` and then either remove the `\b` from the pattern or add an explicit
second pattern for `os.RemoveAll`, and update the rule message to mention both
`os.Remove` and `os.RemoveAll` and reference `shortcuts/im/helpers.go` as the
recommended alternative.
🪄 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: ea3932f0-9cf7-4c65-855d-6087e211b642

📥 Commits

Reviewing files that changed from the base of the PR and between 6412cca and cf5dc61.

📒 Files selected for processing (1)
  • .golangci.yml

Comment thread .golangci.yml
- reassign # checks that package variables are not reassigned
- unconvert # removes unnecessary type conversions
- unused # checks for unused constants, variables, functions and types
- forbidigo # forbids specific function calls
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all os.Exit usages in shortcuts/ non-test files that would violate the new rule
rg -n --type=go 'os\.Exit' shortcuts/ -g '!*_test.go'

Repository: larksuite/cli

Length of output: 103


🏁 Script executed:

cat -n .golangci.yml | sed -n '20,80p'

Repository: larksuite/cli

Length of output: 2894


🏁 Script executed:

sed -n '440,450p' shortcuts/mail/mail_watch.go

Repository: larksuite/cli

Length of output: 382


Address pre-existing os.Exit(0) violation at shortcuts/mail/mail_watch.go:444.

The forbidigo rule being added forbids os.Exit in the shortcuts/ directory. Existing code at line 444 violates this rule and will fail linting unless mitigated. Either add //nolint:forbidigo to that line with a tracking issue for refactoring, or ensure CI consistently runs with --new-from-rev=origin/main to skip linting pre-existing violations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.golangci.yml at line 30, Pre-existing use of os.Exit in the shortcuts mail
watcher will break the new forbidigo linter; fix it by either adding a localized
suppression comment "//nolint:forbidigo" on the specific os.Exit(...) invocation
(include a short TODO referencing a tracking issue/PR for refactor) or update CI
lint invocation to pass "--new-from-rev=origin/main" so pre-existing violations
are ignored; target the os.Exit call in the mail watcher code path and apply one
of these two mitigations.

Comment thread .golangci.yml
Comment on lines +54 to +73
forbidigo:
forbid:
- pattern: os\.CreateTemp
msg: >-
Do not use os.CreateTemp in shortcuts/ for temp file intermediary logic.
Use downloadURLToReader (streaming) or mediaBuffer (in-memory) instead.
See shortcuts/im/helpers.go for reference.
exclude-godoc-examples: true
- pattern: os\.Remove\b
msg: >-
Do not use os.Remove in shortcuts/ for temp file cleanup.
Avoid temp files entirely: use io.Reader streaming or in-memory buffers.
See shortcuts/im/helpers.go for reference.
exclude-godoc-examples: true
- pattern: os\.Exit
msg: >-
Do not use os.Exit in shortcuts/. Return an error instead and let
the caller (cmd layer) decide how to terminate.
exclude-godoc-examples: true
analyze-types: true
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Does golangci-lint forbidigo support exclude-godoc-examples per pattern or only at the top level?

💡 Result:

No, golangci-lint forbidigo does not support exclude-godoc-examples per pattern. It is a top-level global setting that applies to all forbid rules. The official golangci-lint documentation shows exclude-godoc-examples as a top-level option under forbidigo settings, separate from individual pattern definitions: linters: settings: forbidigo: exclude-godoc-examples: false Complex patterns can specify fields like pattern, pkg, and msg, but exclude-godoc-examples is not included per pattern. It is passed once to forbidigo.OptionExcludeGodocExamples in the linter implementation, applying globally. The .golangci.reference.yml confirms this structure, listing exclude-godoc-examples at the forbidigo level, not nested under individual forbid entries. The forbidigo library itself provides OptionExcludeGodocExamples as a single global option, not per-rule.

Citations:


Move exclude-godoc-examples: true to the top level of forbidigo settings.

The exclude-godoc-examples option is not supported per-pattern in forbidigo; it must be placed at the forbidigo top level to apply globally to all patterns. As currently configured, the per-pattern settings will be ignored and godoc examples may still be flagged.

Corrected configuration
forbidigo:
  exclude-godoc-examples: true
  forbid:
    - pattern: os\.CreateTemp
      msg: >-
        Do not use os.CreateTemp in shortcuts/ for temp file intermediary logic.
        Use downloadURLToReader (streaming) or mediaBuffer (in-memory) instead.
        See shortcuts/im/helpers.go for reference.
    - pattern: os\.Remove\b
      msg: >-
        Do not use os.Remove in shortcuts/ for temp file cleanup.
        Avoid temp files entirely: use io.Reader streaming or in-memory buffers.
        See shortcuts/im/helpers.go for reference.
    - pattern: os\.Exit
      msg: >-
        Do not use os.Exit in shortcuts/. Return an error instead and let
        the caller (cmd layer) decide how to terminate.
  analyze-types: true
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.golangci.yml around lines 54 - 73, Move the per-pattern
exclude-godoc-examples settings into the forbidigo top-level: under the
forbidigo block add exclude-godoc-examples: true once, then keep the forbid list
(patterns os\.CreateTemp, os\.Remove\b, os\.Exit and their msg entries)
unchanged beneath it; retain analyze-types: true at the same level so the option
applies globally instead of being ignored per-pattern.

- Reject --reset --global together in strict-mode command
- Clear Users and UAT tokens when saveAsProfile detects AppId change
- Clear LARKSUITE_CLI_STRICT_MODE in test setup to avoid CI pollution

Change-Id: I1b6f68984a837a5534b30942c8f0f492c5d6c9fa
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.

♻️ Duplicate comments (1)
cmd/config/init.go (1)

257-277: ⚠️ Potential issue | 🟠 Major

Mirror AppId-change cleanup in unchanged-secret interactive updates.

The unchanged-secret flow still rewrites AppId without clearing Users and stored tokens, so stale user state can be carried into a different app context. The same cleanup logic already exists in saveAsProfile, but is missing here.

Suggested fix
 		} else if result.Mode == "existing" && result.AppID != "" {
 			// Existing app with unchanged secret — update app ID and brand only
 			if opts.ProfileName != "" && existing != nil {
 				// Profile mode: update named profile in-place
 				if idx := existing.FindAppIndex(opts.ProfileName); idx >= 0 {
+					if existing.Apps[idx].AppId != result.AppID {
+						for _, user := range existing.Apps[idx].Users {
+							auth.RemoveStoredToken(existing.Apps[idx].AppId, user.UserOpenId)
+						}
+						existing.Apps[idx].Users = []core.AppUser{}
+					}
 					existing.Apps[idx].AppId = result.AppID
 					existing.Apps[idx].Brand = result.Brand
 					existing.Apps[idx].Lang = opts.Lang
 				} else {
 					return output.ErrValidation("App Secret cannot be empty for new profile")
 				}
 				if err := core.SaveMultiAppConfig(existing); err != nil {
 					return output.Errorf(output.ExitInternal, "internal", "failed to save config: %v", err)
 				}
 			} else if existing != nil {
 				app := existing.CurrentAppConfig("")
 				if app != nil {
+					if app.AppId != result.AppID {
+						for _, user := range app.Users {
+							auth.RemoveStoredToken(app.AppId, user.UserOpenId)
+						}
+						app.Users = []core.AppUser{}
+					}
 					app.AppId = result.AppID
 					app.Brand = result.Brand
 					app.Lang = opts.Lang
 					if err := core.SaveMultiAppConfig(existing); err != nil {
 						return output.Errorf(output.ExitInternal, "internal", "failed to save config: %v", err)
 					}
 				} else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/config/init.go` around lines 257 - 277, The interactive update path that
updates existing.CurrentAppConfig("") (inside the else-if existing != nil block)
only overwrites AppId/Brand/Lang and fails to clear per-app user state and
tokens, causing stale Users/tokens to persist; mirror the cleanup used in
saveAsProfile by clearing app.Users and any stored tokens/session state for that
app before calling core.SaveMultiAppConfig(existing), ensuring you perform the
same resets you already apply in saveAsProfile when result.AppID changes.
🧹 Nitpick comments (1)
cmd/config/strict_mode.go (1)

111-120: Consider extracting shared logic to avoid drift with Factory.IsStrictMode().

This function duplicates the priority resolution logic from Factory.IsStrictMode() in internal/cmdutil/factory.go:119-134. If either implementation is updated independently, the display command may report a different effective value than what runtime actually enforces.

Extract to a shared helper (e.g., in internal/core) that both can call:

// In internal/core/strict_mode.go
func ResolveStrictMode(multi *MultiAppConfig, app *AppConfig) (effective bool, source string) {
    if v := os.Getenv("LARKSUITE_CLI_STRICT_MODE"); v != "" {
        return v == "true" || v == "1", "env LARKSUITE_CLI_STRICT_MODE"
    }
    if app != nil && app.StrictMode != nil {
        return *app.StrictMode, fmt.Sprintf("profile %q", app.ProfileName())
    }
    return multi.StrictMode, "global"
}

Then Factory.IsStrictMode() can call this and discard the source string.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/config/strict_mode.go` around lines 111 - 120, The priority/resolution
logic in resolveStrictModeStatus duplicates Factory.IsStrictMode; extract it
into a single helper in internal/core (e.g., func ResolveStrictMode(multi
*core.MultiAppConfig, app *core.AppConfig) (bool, string)) implementing the env
-> app profile -> multi fallback, then have resolveStrictModeStatus and
Factory.IsStrictMode call that helper (Factory.IsStrictMode can ignore the
returned source string). Update imports/usages accordingly so both use the
shared ResolveStrictMode implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@cmd/config/init.go`:
- Around line 257-277: The interactive update path that updates
existing.CurrentAppConfig("") (inside the else-if existing != nil block) only
overwrites AppId/Brand/Lang and fails to clear per-app user state and tokens,
causing stale Users/tokens to persist; mirror the cleanup used in saveAsProfile
by clearing app.Users and any stored tokens/session state for that app before
calling core.SaveMultiAppConfig(existing), ensuring you perform the same resets
you already apply in saveAsProfile when result.AppID changes.

---

Nitpick comments:
In `@cmd/config/strict_mode.go`:
- Around line 111-120: The priority/resolution logic in resolveStrictModeStatus
duplicates Factory.IsStrictMode; extract it into a single helper in
internal/core (e.g., func ResolveStrictMode(multi *core.MultiAppConfig, app
*core.AppConfig) (bool, string)) implementing the env -> app profile -> multi
fallback, then have resolveStrictModeStatus and Factory.IsStrictMode call that
helper (Factory.IsStrictMode can ignore the returned source string). Update
imports/usages accordingly so both use the shared ResolveStrictMode
implementation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4142c7f7-a172-4b03-aeb6-3bad5fce29a8

📥 Commits

Reviewing files that changed from the base of the PR and between cf5dc61 and 0d69221.

📒 Files selected for processing (3)
  • cmd/config/init.go
  • cmd/config/strict_mode.go
  • cmd/config/strict_mode_test.go
✅ Files skipped from review due to trivial changes (1)
  • cmd/config/strict_mode_test.go

@liangshuo-1 liangshuo-1 closed this Apr 3, 2026
@liangshuo-1 liangshuo-1 deleted the fix/credential-provider-review-fixes branch April 3, 2026 11:00
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/im PR touches the im domain domain/mail PR touches the mail domain domain/vc PR touches the vc domain size/XL Architecture-level or global-impact change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants