Skip to content

feat: add strict mode identity filter, profile management and credential extension#252

Open
liangshuo-1 wants to merge 7 commits intomainfrom
refactor/plugin
Open

feat: add strict mode identity filter, profile management and credential extension#252
liangshuo-1 wants to merge 7 commits intomainfrom
refactor/plugin

Conversation

@liangshuo-1
Copy link
Copy Markdown
Collaborator

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

Summary

  • Strict Mode: Add strict mode for identity filtering and configuration, ensuring commands run with the correct identity context
  • Profile Management: Add profile subcommands (add/list/remove/rename/use) for managing multiple Lark app profiles
  • Credential Extension: Introduce credential extension framework with registry and env provider, plus VFS abstraction layer
  • Refactoring: Refactor factory default, client options, and update shortcuts to use new credential and validation patterns

Test plan

  • Unit tests for strict mode (cmd/config/strict_mode_test.go, internal/core/strict_mode_test.go)
  • Unit tests for credential extension (extension/credential/*_test.go, internal/credential/*_test.go)
  • Unit tests for bootstrap and prune (cmd/bootstrap_test.go, cmd/prune_test.go)
  • Integration tests (cmd/root_integration_test.go)
  • Existing shortcut tests still pass

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Profile management commands: add/list/use/remove/rename plus root-level --profile
    • config strict-mode command to view/set identity restrictions (bot/user/off)
    • Environment-variable credential provider activates at startup
  • Improvements

    • Credential/token resolution reworked for flexible providers and enforced strict-mode
    • Downloads converted to streaming to reduce memory and speed writes
    • Many shortcut flags now accept file/@path and stdin/- inputs
    • Commands are hidden/blocked when strict-mode disallows them
  • Tests

    • Expanded unit/integration and strict-mode test coverage

…ial extension

Port changes from feat/strict-mode-identity-filter_3 branch:
- Add strict mode for identity filtering and configuration
- Add profile management commands (add/list/remove/rename/use)
- Add credential extension framework (registry, env provider)
- Add VFS abstraction layer
- Refactor factory default and client options
- Update shortcuts to use new credential and validation patterns

Change-Id: I8c104c6b147e1901d94aefcefe35a174932c742b
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@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 3, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 3, 2026

📝 Walkthrough

Walkthrough

Adds a credential-provider system with an env provider, multi-profile config and strict-mode enforcement, a virtual filesystem abstraction (vfs), global/profile bootstrap flags, profile management commands, credential-driven token resolution for API clients/shortcuts, streaming I/O, and widespread migrations from direct os.* usage to vfs.*.

Changes

Cohort / File(s) Summary
Credential registry & providers
extension/credential/... extension/credential/env/env.go, extension/credential/*_test.go, extension/credential/registry.go
New extensible credential Provider interface, env provider registration, registry and tests.
Internal credential aggregation & defaults
internal/credential/*, internal/credential/*_test.go
New CredentialProvider aggregator, DefaultAccount/Token providers, user-info enrichment, token/account resolution, and many tests.
Multi-profile config & strict mode
internal/core/config.go, internal/core/strict_mode.go, internal/core/*_test.go
Multi-app config with named profiles, Current/Previous app helpers, profile validation, and StrictMode type + tests.
Factory / runtime wiring
internal/cmdutil/factory*.go, internal/cmdutil/testing.go
Factory now carries InvocationContext and CredentialProvider, resolves strict mode/identity, enforces CheckStrictMode, and passes Credential into API client/test factories.
CLI bootstrap, global flags & profile commands
cmd/bootstrap.go, cmd/global_flags.go, cmd/root.go, cmd/profile/*, cmd/config/strict_mode.go
Early --profile parsing, new GlobalOptions, root wiring, profile subcommands (add/list/use/remove/rename) and config strict-mode command.
Command pruning for strict mode
cmd/prune.go, cmd/prune_test.go, cmd/root_integration_test.go
Prune/replace commands incompatible with active strict mode using hidden stubs; tests and integration harness updated.
API client & streaming options
internal/client/*, internal/client/option.go
API client now accepts Credential, resolves tokens via Credential.ResolveToken, adds DoStream for streaming requests and per-request Option pattern (timeout/headers).
Virtual filesystem (vfs) & fs abstraction
internal/vfs/*, many updated files (keychain, validate, lockfile, registry, update, shortcuts, shortcuts/**)
New FS interface, OsFs implementation, DefaultFS wrapper; swapped os.* calls to vfs.* across repo.
Shortcuts I/O and token flow
shortcuts/** (many files)
Shortcut runtime now uses Credential.ResolveToken, streaming downloads/uploads, file/stdin input sources, and vfs-based file ops; many tests removed token stubs.
Profile/test harness and tests
cmd/*_test.go, internal/*_test.go, shortcuts/*_test.go
Added integration tests for credential chain, strict-mode and profile flows; removed various tenant token stubs and legacy e2e test file.
Lint/config
.golangci.yml
Added forbidigo linter rules forbidding direct os.* usage and guiding replacement patterns.
Entry point
main.go
Blank-import of env credential provider to enable environment-based credentials on startup.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant CLI as CLI (root)
    participant Bootstrap as BootstrapInvocationContext
    participant Factory as cmdutil.Factory
    participant CredProv as CredentialProvider
    participant ExtEnv as extcred/env
    participant Config as core.MultiAppConfig

    CLI->>Bootstrap: parse args (e.g. --profile)
    Bootstrap-->>CLI: InvocationContext{Profile}
    CLI->>Factory: NewDefault(InvocationContext)
    Factory->>CredProv: build chain / ResolveAccount(ctx)
    CredProv->>ExtEnv: ResolveAccount()
    alt env provides account
        ExtEnv-->>CredProv: Account + SupportedIdentities
    else
        CredProv->>Config: LoadMultiAppConfig()
        Config-->>CredProv: AppConfig for profile
    end
    CredProv-->>Factory: Resolved Account
    Factory-->>CLI: ready (Config(), ResolveStrictMode())
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • zhouyue-bytedance
  • fangshuyu-768

Poem

🐰 I found an env var, tucked in green,

Profiles sprout where defaults once had been.
VFS footprints replace each os call,
Tokens now hop from provider hall—
A tiny rabbit prunes the tree, nimble and keen.

✨ 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 refactor/plugin

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

github-actions bot commented Apr 3, 2026

🚀 PR Preview Install Guide

🧰 CLI update

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

🧩 Skill update

npx skills add larksuite/cli#refactor/plugin -y -g

liangshuo-1 and others added 2 commits April 3, 2026 20:20
- Remove unused TAT stub registrations in api and service tests
  (CredentialProvider manages tokens, SDK no longer calls TAT endpoint)
- Update strict mode integration test: +chat-create now supports user
  identity, so it should succeed under strict mode user

Change-Id: Iab51c2e12a97995e0b95dcd71df212d2d1f76570
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace direct os.Stat/Open/MkdirAll/OpenFile/Remove/ReadDir/UserHomeDir
with vfs equivalents in shortcuts/minutes, shortcuts/drive, and
internal/keychain. Add ReadDir to the vfs interface and OsFs implementation.

Change-Id: I8f97e5fb3e1731b4684d276644fcb10fae823067
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: 16

Caution

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

⚠️ Outside diff range comments (4)
shortcuts/im/im_messages_resources_download.go (1)

145-156: ⚠️ Potential issue | 🔴 Critical

Verify that DoStream converts non-2xx HTTP responses to errors, or add local status code validation.

The code relies entirely on the undocumented contract that ac.DoStream() (from larkcore SDK) returns an error for non-2xx responses. However, none of the four DoAPIStream callers (this file, drive_download, doc_media_download, sheets_sheet_export) validate the response status code before writing to file. This pattern is inconsistent with other downloads in the same codebase—minutes_download.go and drive_export_common.go explicitly check StatusCode. Without verification that DoStream converts failures to errors, an HTTP error response body could be written as a downloaded file.

Either confirm the DoStream contract with tests, or add local validation: if downloadResp.StatusCode >= 400 { return "", 0, ... } after the DoAPIStream call.

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

In `@shortcuts/im/im_messages_resources_download.go` around lines 145 - 156, After
calling runtime.DoAPIStream you must validate the HTTP status before treating
the response as a successful download: check downloadResp.StatusCode (the value
returned by runtime.DoAPIStream) and if it's >= 400 return an error (include
status and any returned body/message) instead of proceeding to write the file;
update the code around the runtime.DoAPIStream call (referencing downloadResp,
runtime.DoAPIStream and defaultIMResourceDownloadTimeout) to perform this local
check and return early on error.
internal/validate/path.go (1)

63-88: ⚠️ Potential issue | 🟠 Major

Don't mix vfs probes with filepath.EvalSymlinks.

Lines 63-75 and 102-108 now check existence through vfs, but canonicalization still goes through filepath.EvalSymlinks. That means a non-default VFS can report a path as present and then immediately fail resolving the same path against the host OS, which breaks the new abstraction and its tests.

Suggested direction
-		resolved, err = filepath.EvalSymlinks(resolved)
+		resolved, err = vfs.EvalSymlinks(resolved)
...
-	canonicalCwd, _ := filepath.EvalSymlinks(cwd)
+	canonicalCwd, _ := vfs.EvalSymlinks(cwd)
...
-			real, err := filepath.EvalSymlinks(cur)
+			real, err := vfs.EvalSymlinks(cur)

If internal/vfs does not expose this yet, I'd add a small wrapper there first so the whole validation flow stays on one filesystem boundary.

Also applies to: 102-108

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

In `@internal/validate/path.go` around lines 63 - 88, The code mixes host FS calls
(filepath.EvalSymlinks) with vfs probes (vfs.Getwd/vfs.Lstat), which breaks
non-default VFS implementations; replace the direct calls to
filepath.EvalSymlinks (both the EvalSymlinks(resolved) and EvalSymlinks(cwd)
usages) with a vfs-level symlink-resolution API (e.g., add and call
vfs.EvalSymlinks or equivalent) and ensure resolveNearestAncestor and isUnderDir
operate entirely via the vfs API so all existence checks and canonicalization
stay on the same filesystem boundary.
shortcuts/sheets/sheet_export.go (1)

115-143: ⚠️ Potential issue | 🟡 Minor

Missing early return when outputPath is empty causes unnecessary download attempt.

When outputPath is empty, the code outputs file_token and ticket (lines 116-119) but then continues to attempt the download (line 123). This download will fail at line 132-134 when SafeOutputPath("") returns a validation error.

The original intent appears to be: if no output path is specified, just return the export task result without downloading. Add an early return after outputting the tokens.

Proposed fix
 		if outputPath == "" {
 			runtime.Out(map[string]interface{}{
 				"file_token": fileToken,
 				"ticket":     ticket,
 			}, nil)
+			return nil
 		}

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

In `@shortcuts/sheets/sheet_export.go` around lines 115 - 143, When outputPath is
empty the code calls runtime.Out(...) but does not return, so the subsequent
download and calls to validate.SafeOutputPath and validate.AtomicWriteFromReader
run and fail; fix by adding an early return immediately after the
runtime.Out(...) call in the block that checks if outputPath == "" so the
function exits successfully (no download attempted) when only file_token/ticket
are being emitted; locate the check for outputPath, the runtime.Out invocation,
and add the return there to skip the DoAPIStream, SafeOutputPath, vfs.MkdirAll
and AtomicWriteFromReader logic.
shortcuts/common/runner.go (1)

465-472: ⚠️ Potential issue | 🔴 Critical

Validate strict mode before ResolveAs can coerce the shortcut identity.

If ResolveAs() already applies strict-mode/default overrides, CheckStrictMode(as) no longer sees the shortcut’s original/default identity. That lets a disallowed shortcut execute under the coerced identity instead of returning the strict-mode envelope, which matches the failing TestIntegration_StrictModeUser_ProfileOverride_DirectBotShortcutReturnsEnvelope in CI.

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

In `@shortcuts/common/runner.go` around lines 465 - 472, Call CheckStrictMode
before letting ResolveAs coerce the identity: extract the raw flag value
(asFlag) and call f.CheckStrictMode(core.Identity(asFlag)) first, returning any
error; only after strict-mode passes, call f.ResolveAs(cmd,
core.Identity(asFlag)) to apply defaults/auto-detection and continue using the
resolved identity. Update resolveShortcutIdentity (and use f.CheckStrictMode and
f.ResolveAs references) so strict-mode validation runs against the original
input before coercion.
🧹 Nitpick comments (15)
cmd/profile/list.go (1)

17-25: Consider using omitempty for optional JSON fields.

The User and TokenStatus fields are only populated when applicable, but without omitempty, they will appear as empty strings in the JSON output for profiles without logged-in users. This is minor but could make the output cleaner.

🔧 Suggested improvement
 type profileListItem struct {
 	Name        string         `json:"name"`
 	AppID       string         `json:"appId"`
 	Brand       core.LarkBrand `json:"brand"`
 	Active      bool           `json:"active"`
-	User        string         `json:"user"`
-	TokenStatus string         `json:"tokenStatus"`
+	User        string         `json:"user,omitempty"`
+	TokenStatus string         `json:"tokenStatus,omitempty"`
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/profile/list.go` around lines 17 - 25, The profileListItem struct
currently marshals User and TokenStatus as empty strings for profiles without
values; update the struct tags for the User and TokenStatus fields on
profileListItem to use the `omitempty` json option so those keys are omitted
when empty (i.e., change the `json:"user"` and `json:"tokenStatus"` tags to
include `omitempty`), leaving other fields unchanged.
cmd/config/init.go (1)

255-283: The "existing app with unchanged secret" branch has grown complex.

This section handles four distinct cases: profile mode with existing profile, profile mode without existing profile, non-profile mode with current app, and non-profile mode without config. Consider extracting this into a helper function for readability, though it's not blocking.

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

In `@cmd/config/init.go` around lines 255 - 283, The existing "result.Mode ==
'existing' && result.AppID != ''" branch mixes four paths and should be
extracted into a helper to simplify logic: create a function (e.g.,
updateExistingAppConfig(existing, result, opts) or core.UpdateExistingApp) that
encapsulates the profile vs non-profile branching, uses
existing.FindAppIndex(opts.ProfileName) to update Apps[idx].AppId/Brand/Lang
when profile mode and existing, returns a validation error when profile name not
found, and otherwise uses existing.CurrentAppConfig("") to update
AppId/Brand/Lang or return the validation error when nil; ensure the caller only
handles calling this helper and a single core.SaveMultiAppConfig(existing)
invocation and returns any error from those calls (keeping references to
result.Mode, result.AppID, opts.ProfileName, existing.FindAppIndex,
existing.CurrentAppConfig, and core.SaveMultiAppConfig).
internal/core/config.go (1)

132-152: Consider adding newline characters to the forbidden character check.

The validation rejects control characters and shell-problematic characters, but explicitly listing \n and \r in the switch statement (alongside the control character check) would make the intent clearer and match the pattern used elsewhere in the codebase for input validation.

That said, the current implementation already catches them via the control character check (r <= 0x1F), so this is just a clarity suggestion.

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

In `@internal/core/config.go` around lines 132 - 152, The ValidateProfileName
function currently rejects control characters via r <= 0x1F but does not
explicitly list newline chars; update the switch in ValidateProfileName to
include '\n' and '\r' alongside the other forbidden characters to make the
intent explicit and consistent with other validators (leave the
control-character check intact so behavior is unchanged).
internal/core/types.go (1)

16-23: Consider case-insensitive brand matching.

The ParseBrand function uses case-sensitive comparison (value == "lark"), which means inputs like "Lark" or "LARK" will default to BrandFeishu. If this is intentional for strict matching, the current implementation is fine. Otherwise, consider using strings.EqualFold for case-insensitive matching.

💡 Optional: Case-insensitive matching
 func ParseBrand(value string) LarkBrand {
-	if value == "lark" {
+	if strings.EqualFold(value, "lark") {
 		return BrandLark
 	}
 	return BrandFeishu
 }

This would require adding "strings" to the imports.

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

In `@internal/core/types.go` around lines 16 - 23, ParseBrand currently does a
case-sensitive comparison so inputs like "Lark" or "LARK" will fall back to
BrandFeishu; update ParseBrand to perform a case-insensitive match (use
strings.EqualFold(value, "lark")) and add the "strings" import so inputs with
different casing map to BrandLark while still returning BrandFeishu for
unrecognized values; keep the function signature and return values (BrandLark,
BrandFeishu) unchanged.
shortcuts/mail/mail_watch_test.go (1)

99-100: Re-add HTTP method assertion for the profile dry-run call.

At Line 99, the test only validates URL; a method regression would now pass silently. Consider asserting GET as well.

Proposed test hardening
+	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 currently
only asserts the URL for the profile dry-run call (checking apis[1].URL against
mailboxPath("me", "profile")) but misses the HTTP method; update the assertion
to also check the method (e.g., assert apis[1].Method == "GET" and call t.Fatalf
with a clear message if it differs) so a regression that changes the request
verb will fail; modify the test near the existing URL check referencing the apis
slice and mailboxPath("me","profile") to include this additional method
assertion.
internal/credential/default_provider_test.go (1)

7-13: Add a behavioral test for provider selection.

Lines 7-13 only prove the types satisfy the interfaces, so a broken dispatch/fallback chain would still compile cleanly. Given the new credential-extension path is core behavior, I'd add at least one runtime test that exercises provider lookup through DefaultTokenProvider and DefaultAccountProvider.

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

In `@internal/credential/default_provider_test.go` around lines 7 - 13, Add a
runtime behavioral test that exercises provider selection and fallback through
DefaultTokenProvider and DefaultAccountProvider instead of only asserting
interface conformance; specifically, register at least two mock providers (one
that returns a valid token/account and one that errors or is lower priority),
ensure the DefaultTokenProvider and DefaultAccountProvider lookup pipelines
prefer the higher-priority provider, and verify that when the preferred provider
fails the fallback provider is used. Use the same public constructors/types
(DefaultTokenProvider, DefaultAccountProvider) and any registry or resolver
entry points your code exposes to add/remove test providers so the test sets up
providers, calls the resolve methods on
DefaultTokenProvider/DefaultAccountProvider, asserts the returned token/account
and errors, and then cleans up the test registry state.
shortcuts/drive/drive_download.go (1)

73-80: Consider using a more accurate error code for filesystem errors.

Lines 74 and 79 use "api_error" as the error code for local filesystem operations (directory creation and file writing). A code like "filesystem_error" or "io_error" would be more accurate for debugging and monitoring purposes.

Suggested improvement
 		if err := vfs.MkdirAll(filepath.Dir(safePath), 0700); err != nil {
-			return output.Errorf(output.ExitInternal, "api_error", "cannot create parent directory: %s", err)
+			return output.Errorf(output.ExitInternal, "io_error", "cannot create parent directory: %s", err)
 		}

 		sizeBytes, err := validate.AtomicWriteFromReader(safePath, resp.Body, 0600)
 		if err != nil {
-			return output.Errorf(output.ExitInternal, "api_error", "cannot create file: %s", err)
+			return output.Errorf(output.ExitInternal, "io_error", "cannot create file: %s", err)
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/drive/drive_download.go` around lines 73 - 80, The returns for
filesystem failures use the generic "api_error" code; update the two error
returns that call vfs.MkdirAll(filepath.Dir(safePath), ...) and
validate.AtomicWriteFromReader(safePath, resp.Body, ...) to use a more accurate
code (e.g., "filesystem_error" or "io_error") instead of "api_error" so
filesystem/create-file failures are correctly categorized in output.Errorf calls
while keeping the existing error messages intact.
shortcuts/sheets/sheet_export.go (1)

136-142: Same suggestion: use accurate error codes for filesystem errors.

As noted for drive_download.go, the "api_error" code at lines 137 and 142 is misleading for local filesystem errors.

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

In `@shortcuts/sheets/sheet_export.go` around lines 136 - 142, The filesystem
error handling in the block that creates parent directories and writes the file
(calls to vfs.MkdirAll and validate.AtomicWriteFromReader using safePath and
resp.Body) incorrectly uses the "api_error" error code; change the output.Errorf
calls to use a filesystem-specific error code (e.g. "fs_error" or
"filesystem_error") and keep the same messages and exit code
(output.ExitInternal) so logs accurately reflect local filesystem failures
instead of API errors.
cmd/config/strict_mode_test.go (2)

73-89: Consider checking errors in multi-step tests.

In TestStrictMode_SetOff_Profile, line 78 ignores the error from the first cmd.Execute(). If setting "bot" fails, the test continues with incorrect state. Similarly in TestStrictMode_Reset at line 110.

Suggested improvement
 func TestStrictMode_SetOff_Profile(t *testing.T) {
 	setupStrictModeTestConfig(t)
 	f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test-app", AppSecret: "secret"})
 	cmd := NewCmdConfigStrictMode(f)
 	cmd.SetArgs([]string{"bot"})
-	cmd.Execute()
+	if err := cmd.Execute(); err != nil {
+		t.Fatalf("setup: setting bot failed: %v", err)
+	}
 	cmd = NewCmdConfigStrictMode(f)
🤖 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 73 - 89, The test
TestStrictMode_SetOff_Profile (and similarly TestStrictMode_Reset) ignores the
result of the first cmd.Execute() call (the invocation that sets "bot"), which
can leave the test in a bad state; update the test to check the error returned
by that first cmd.Execute() and fail the test if non-nil (e.g., use t.Fatal or
t.Fatalf) before continuing, locating the calls to cmd.Execute() in
TestStrictMode_SetOff_Profile and TestStrictMode_Reset to add the error check.

51-52: Consider checking errors from LoadMultiAppConfig.

Multiple tests ignore the error return from core.LoadMultiAppConfig() (lines 51, 66, 84, 99, 116). While unlikely to fail in test context, checking errors would make test failures more diagnosable.

Example fix
-	multi, _ := core.LoadMultiAppConfig()
+	multi, err := core.LoadMultiAppConfig()
+	if err != nil {
+		t.Fatalf("LoadMultiAppConfig() error: %v", err)
+	}

Also applies to: 66-67, 84-85, 99-100, 116-117

🤖 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 51 - 52, Tests currently ignore
the error return from core.LoadMultiAppConfig() (called in strict_mode_test.go
and elsewhere), which hides failures; update each test that does "multi, _ :=
core.LoadMultiAppConfig()" to capture the error (e.g., "multi, err :=
core.LoadMultiAppConfig()") and assert/handle it (t.Fatalf or require.NoError)
before calling multi.CurrentAppConfig("") so failures are reported clearly;
apply the same pattern for all occurrences at the lines noted that call
LoadMultiAppConfig.
internal/cmdutil/factory_default.go (1)

68-69: Misleading comment: AppSecret is not a placeholder.

The comment says "placeholder AppSecret" but line 129 passes acct.AppSecret which is the actual secret from the resolved account.

📝 Fix misleading comment
-	// Phase 4: LarkClient from Credential (placeholder AppSecret)
+	// Phase 4: LarkClient from Credential
 	f.LarkClient = cachedLarkClientFunc(f)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/cmdutil/factory_default.go` around lines 68 - 69, The comment above
the Lark client initialization is misleading — it says "placeholder AppSecret"
even though the real account secret (acct.AppSecret) is passed into
cachedLarkClientFunc; update the comment near f.LarkClient =
cachedLarkClientFunc(f) to remove or replace "placeholder AppSecret" with
wording that indicates the real account AppSecret is used (reference symbols:
f.LarkClient, cachedLarkClientFunc, acct.AppSecret).
cmd/service/service.go (1)

258-262: Silently swallowing token resolution errors may mask misconfigurations.

When cred.ResolveToken fails or returns nil/empty scopes, the function silently returns nil, allowing the API call to proceed without scope validation. This could mask credential configuration issues.

Consider logging a debug/warning message when token resolution fails to aid troubleshooting, while still allowing the command to proceed.

♻️ Suggested improvement
 func checkServiceScopes(ctx context.Context, cred *credential.CredentialProvider, identity core.Identity, config *core.CliConfig, method map[string]interface{}, scopes []interface{}) error {
 	result, err := cred.ResolveToken(ctx, credential.NewTokenSpec(identity, config.AppID))
-	if err != nil || result == nil || result.Scopes == "" {
+	if err != nil {
+		// Token resolution failed; skip scope check (API may still succeed)
+		return nil
+	}
+	if result == nil || result.Scopes == "" {
+		// No scopes available; skip pre-check
 		return nil
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/service/service.go` around lines 258 - 262, In checkServiceScopes, do not
silently swallow errors from cred.ResolveToken: when cred.ResolveToken(ctx,
credential.NewTokenSpec(identity, config.AppID)) returns an error, a nil result,
or an empty result.Scopes, emit a debug/warning log (e.g., via the existing
logger in config or a context-aware logger) that includes the error message and
identity/AppID so operators can troubleshoot, then continue to return nil to
preserve the current behavior; update handling around the ResolveToken call and
use the function name checkServiceScopes and symbols cred.ResolveToken,
credential.NewTokenSpec, result and result.Scopes to locate where to add the
log.
shortcuts/im/helpers.go (1)

313-323: Stream plain file URLs instead of buffering them.

This branch always builds a mediaBuffer for mediaKindFile, even when s.withDuration is false. A normal --file URL now gets fully copied into memory before upload for no functional gain.

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

In `@shortcuts/im/helpers.go` around lines 313 - 323, The code always constructs a
media buffer via newMediaBuffer even when s.withDuration is false, causing full
in-memory copies for plain file URLs; change the flow so newMediaBuffer is only
called when s.withDuration is true (to obtain mb.Duration()), and when
s.withDuration is false create or use a streaming reader for s.value and call
uploadFileFromReader with that streaming reader and the file metadata (avoiding
mb.Reader()/mb.FileName()/mb.FileType()). Keep the existing call-site
uploadFileFromReader and only switch the source/metadata to the streaming path
when not using mb so the buffer is only allocated when duration is needed.
internal/core/config_strict_mode_test.go (1)

17-19: Prefer structural assertions over exact JSON string equality.

Line 17 couples the test to exact byte ordering/format. Asserting parsed keys/values is more stable.

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

In `@internal/core/config_strict_mode_test.go` around lines 17 - 19, Replace the
exact JSON string equality with structural checks: unmarshal data into a
map[string]interface{} (or a small struct), assert that "apps" is a slice of
length 1, and that the first app object has appId == "a", appSecret == "s",
brand == "feishu"; also assert that the "users" field is either absent or empty
as required by strict mode (check absence in the map or that len(users)==0). Use
json.Unmarshal and t.Fatalf/t.Errorf for failures; reference the existing
variable data and the apps entry when making assertions.
internal/credential/credential_provider.go (1)

84-117: Consider logging non-BlockError failures for debugging.

When ResolveToken returns an error that isn't a BlockError (line 98), the error is silently swallowed as the loop continues. While this is acceptable for a non-fatal enrichment path, logging these errors would help diagnose provider issues.

🔧 Optional: Add debug logging for swallowed errors
 		if err != nil {
 			var blockErr *extcred.BlockError
 			if errors.As(err, &blockErr) {
 				return nil // provider explicitly blocks UAT; skip enrichment
 			}
+			// Log non-blocking errors for debugging (optional)
+			// log.Printf("enrichUserInfo: provider %T returned error: %v", prov, err)
 			continue
 		}
🤖 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 84 - 117, In
enrichUserInfo, when prov.ResolveToken returns an error that is not a BlockError
(the branch after errors.As in the providers loop), add a debug/info log that
records the error and identifies the provider (e.g., provider ID/name or the
prov variable) before continuing; keep the existing behavior of skipping
BlockError (return nil) and continuing on other errors, but emit the log so
non-BlockError failures are visible for debugging (use the existing logger on
CredentialProvider, e.g., p.logger, or the standard log package if no logger
field exists).
🤖 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 317-329: When multi.FindApp(config.ProfileName) returns nil the
code currently silently skips updating Users; change this so the login does not
pretend success: if app == nil return a visible error (use output.Errorf with a
clear message like "profile %s not found" and an internal exit code) instead of
proceeding, and keep the existing logic that prunes tokens via
larkauth.RemoveStoredToken, sets app.Users = []core.AppUser{{UserOpenId: openId,
UserName: userName}}, and calls core.SaveMultiAppConfig(multi) when app exists;
apply the same nil-profile handling to the other identical block that mirrors
this logic.

In `@cmd/config/show.go`:
- Around line 48-51: The command currently prints "No active profile found."
when config.CurrentAppConfig(f.Invocation.Profile) returns nil but still returns
nil; change this to return a non-zero error instead: construct and return an
error (e.g. via fmt.Errorf) that includes the requested profile name when
f.Invocation.Profile is set, and preserve the existing ErrOut print or replace
it with the error message; update the block around app :=
config.CurrentAppConfig(f.Invocation.Profile) in cmd/config/show.go (the code
handling nil app) to return that error so the command exits non-zero on missing
profile.

In `@cmd/config/strict_mode.go`:
- Around line 44-47: The current check immediately returns if app == nil, which
blocks commands using the --global flag; change the validation so the nil-app
early return only runs for non-global operations. Concretely, in the block where
app := multi.CurrentAppConfig(f.Invocation.Profile) and the subsequent if app ==
nil return statement, gate that return behind a check for the global flag (i.e.,
only enforce app != nil when the command is not running with --global). Keep the
rest of the flow unchanged so global set/show commands bypass the
profile-required validation.

In `@cmd/profile/add.go`:
- Around line 72-75: The current code in cmd/profile/add.go calls
core.LoadMultiAppConfig() and always replaces errors by creating a new
&core.MultiAppConfig{}, which risks overwriting a corrupt/unreadable config;
change the logic in the LoadMultiAppConfig() caller so that after calling
core.LoadMultiAppConfig() you only create a new MultiAppConfig when the error
indicates a missing file (e.g. os.IsNotExist or a specific "not found" error
from core), but for other errors (parse/permission) return or propagate the
error instead of silently continuing; reference the call site
(core.LoadMultiAppConfig and the local variable multi) and add an explicit
conditional that distinguishes not-found from other failures and handles each
appropriately.

In `@cmd/profile/remove.go`:
- Around line 52-71: The code currently removes secrets/tokens before persisting
the updated multi-app config which can leave the system in a partially
destructive state if core.SaveMultiAppConfig fails; change the order so you
first remove the app from multi.Apps (the slice append operation) and update
multi.CurrentApp/multi.PreviousApp as currently done, then call
core.SaveMultiAppConfig(multi) and only after that succeeds call
core.RemoveSecretStore(app.AppSecret, f.Keychain) and
larkauth.RemoveStoredToken(app.AppId, user.UserOpenId) for each user; keep the
same functions and slice manipulation (multi.Apps = append(...)), but move the
persistence step (core.SaveMultiAppConfig) before the calls to
core.RemoveSecretStore and larkauth.RemoveStoredToken so secrets are only
deleted after a successful save.

In `@cmd/profile/use.go`:
- Around line 34-37: The current handler for core.LoadMultiAppConfig() hides
non-missing errors by always returning output.ErrWithHint(... "not configured"),
so change the error handling in the use command to only map the specific “config
missing” sentinel/error to output.ErrWithHint(output.ExitValidation, "config",
"not configured", "run: lark-cli config init") and for any other error
(parse/permission/IO) return the original err unchanged; locate the call to
core.LoadMultiAppConfig() and the surrounding if err { ... } block and implement
a conditional that inspects/compares err against the missing-config sentinel (or
error type) before wrapping it with output.ErrWithHint, otherwise propagate err.

In `@cmd/prune.go`:
- Around line 69-73: Summary: the current pruning logic force-unhides group
commands by setting child.Hidden = false when child.HasAvailableSubCommands() is
true; instead preserve the original Hidden state. Fix: in the switch handling
child commands (use child.HasAvailableSubCommands() and child.Commands()),
remove the assignment child.Hidden = false so we do not unhide groups during
pruning; keep the branch that sets child.Hidden = true when
len(child.Commands()) > 0 so empty groups can be hidden, but otherwise leave
child.Hidden untouched to preserve its pre-prune value.

In `@extension/credential/env/env_test.go`:
- Around line 31-36: TestResolveAccount_NeitherSet calls
(&Provider{}).ResolveAccount and expects nil,nil but can be flaky if LARK_*
environment variables are set on the host; before invoking ResolveAccount clear
or unset all relevant env vars (all LARK_* keys) in the test setup so the
provider runs in a clean environment and then restore them after the test; apply
the same pattern to the other tests in this file that assume unset environment
(the tests mentioned around the later ranges) so each test explicitly clears
LARK_* before exercising Provider.ResolveAccount or related code paths.

In `@extension/credential/env/env.go`:
- Around line 37-54: The current switch on LARKSUITE_CLI_STRICT_MODE falls
through unknown values to token inference; change it to normalize
(strings.ToLower) and validate the env var: if LARKSUITE_CLI_STRICT_MODE is
non-empty and equals "bot" set acct.SupportedIdentities =
credential.SupportsBot, if "user" set credential.SupportsUser, if "off" set
credential.SupportsAll; if the env var is non-empty but none of these values,
return/propagate an error (fail closed) instead of inferring; only perform the
hasUAT/hasTAT inference when LARKSUITE_CLI_STRICT_MODE is unset/empty so
acct.SupportedIdentities is not widened on typos.

In `@internal/client/client.go`:
- Around line 81-90: The code calls c.Credential.ResolveToken(ctx,
credential.NewTokenSpec(as, c.Config.AppID)) and immediately uses result.Token
(in branches that set req.SupportedAccessTokenTypes and append
larkcore.WithTenantAccessToken/WithUserAccessToken), but TokenProvider can
return (nil, nil); add a nil check for result after ResolveToken in the
DoSDKRequest and DoStream flows (and the other similar sites referenced) and if
result is nil return a clear authentication error (or wrap/convert to the
existing auth error type) instead of dereferencing result.Token; ensure the
check covers both bot and user branches so the code doesn’t panic when
ResolveToken returns (nil, nil).
- Around line 125-133: The code sets httpClient.Timeout (via httpClient :=
*c.HTTP; httpClient.Timeout = cfg.timeout) which will abort response body reads
and can cut off healthy streaming responses; remove the assignment to
httpClient.Timeout and instead rely solely on the request context deadline
created by context.WithTimeout when cfg.timeout > 0 (using requestCtx and
cancel) so streaming responses are not prematurely terminated; keep the copy of
c.HTTP (httpClient := *c.HTTP) if needed for other per-request changes but do
not set httpClient.Timeout = cfg.timeout.

In `@internal/core/config_strict_mode_test.go`:
- Around line 16-26: The test currently ignores errors from
json.Marshal/json.Unmarshal which can hide failures; update all
Marshal/Unmarshal calls in this test (the marshaling of m after setting
StrictMode and the subsequent Unmarshal into parsed, as well as the other
Marshal/Unmarshal occurrences noted) to capture the error and fail the test on
error (use t.Fatalf or t.Fatal with the error), e.g., check err :=
json.Marshal(m) and err := json.Unmarshal(data, &parsed) and assert err == nil
before proceeding; this applies to the Marshal/Unmarshal around m.StrictMode,
the parsed map.Unmarshal, and the other instances in the same file.

In `@internal/credential/credential_provider_test.go`:
- Around line 95-99: The test for AccountCached is currently ignoring errors
from cp.ResolveAccount so a1 == a2 can be true when both are nil; change the
test to capture and assert both errors are nil (e.g., err1, err2 from
cp.ResolveAccount) before comparing pointers, or explicitly fail if either err
is non-nil, then assert that a1 and a2 are the same pointer; locate the calls to
cp.ResolveAccount and variables a1/a2 in the test and add the nil-error checks
and appropriate test failures before the pointer equality assertion.

In `@internal/credential/integration_test.go`:
- Around line 49-67: The test TestFullChain_Fallthrough (and the other tests
around lines 78-112) relies on the env provider but doesn't isolate environment
variables; update the tests to clear and restore all env vars the envprovider
reads (e.g., token and strict-mode vars and any provider-specific env keys)
before creating ep so the provider always falls through deterministically — in
Go, prefer using t.Setenv("VAR_NAME", "") or explicitly unset/restore around the
test; ensure the same isolation change is applied to the other tests referenced
so external env cannot alter provider-chain behavior.

In `@shortcuts/common/runner.go`:
- Around line 91-95: AccessToken currently dereferences result.Token even when
Credential.ResolveToken can return (nil, nil); update it to handle the
unresolved-token path by checking the ResolveToken return before accessing
Token: call ctx.Factory.Credential.ResolveToken(...), if err != nil return
output.ErrAuth(...), then if result == nil (or result.Token is empty) return an
auth error (e.g., output.ErrAuth("no access token resolved")) instead of
dereferencing; keep the same function names (AccessToken, ResolveToken,
credential.NewTokenSpec) so the guard is placed immediately after the
ResolveToken call.

In `@shortcuts/im/helpers.go`:
- Around line 327-339: The resolveLocalMedia function calls parseMediaDuration
on s.value before uploadFileToIM can run validate.SafeInputPath, allowing an
unsafe filesystem open for audio/video files; fix by validating the input path
first: in resolveLocalMedia call validate.SafeInputPath (or the equivalent path
validation helper used by uploadFileToIM) on s.value before calling
detectIMFileType or parseMediaDuration so any local file is checked/normalized
first, then proceed to parseMediaDuration and finally call uploadFileToIM (keep
the existing uploadImageToIM branch unchanged).

---

Outside diff comments:
In `@internal/validate/path.go`:
- Around line 63-88: The code mixes host FS calls (filepath.EvalSymlinks) with
vfs probes (vfs.Getwd/vfs.Lstat), which breaks non-default VFS implementations;
replace the direct calls to filepath.EvalSymlinks (both the
EvalSymlinks(resolved) and EvalSymlinks(cwd) usages) with a vfs-level
symlink-resolution API (e.g., add and call vfs.EvalSymlinks or equivalent) and
ensure resolveNearestAncestor and isUnderDir operate entirely via the vfs API so
all existence checks and canonicalization stay on the same filesystem boundary.

In `@shortcuts/common/runner.go`:
- Around line 465-472: Call CheckStrictMode before letting ResolveAs coerce the
identity: extract the raw flag value (asFlag) and call
f.CheckStrictMode(core.Identity(asFlag)) first, returning any error; only after
strict-mode passes, call f.ResolveAs(cmd, core.Identity(asFlag)) to apply
defaults/auto-detection and continue using the resolved identity. Update
resolveShortcutIdentity (and use f.CheckStrictMode and f.ResolveAs references)
so strict-mode validation runs against the original input before coercion.

In `@shortcuts/im/im_messages_resources_download.go`:
- Around line 145-156: After calling runtime.DoAPIStream you must validate the
HTTP status before treating the response as a successful download: check
downloadResp.StatusCode (the value returned by runtime.DoAPIStream) and if it's
>= 400 return an error (include status and any returned body/message) instead of
proceeding to write the file; update the code around the runtime.DoAPIStream
call (referencing downloadResp, runtime.DoAPIStream and
defaultIMResourceDownloadTimeout) to perform this local check and return early
on error.

In `@shortcuts/sheets/sheet_export.go`:
- Around line 115-143: When outputPath is empty the code calls runtime.Out(...)
but does not return, so the subsequent download and calls to
validate.SafeOutputPath and validate.AtomicWriteFromReader run and fail; fix by
adding an early return immediately after the runtime.Out(...) call in the block
that checks if outputPath == "" so the function exits successfully (no download
attempted) when only file_token/ticket are being emitted; locate the check for
outputPath, the runtime.Out invocation, and add the return there to skip the
DoAPIStream, SafeOutputPath, vfs.MkdirAll and AtomicWriteFromReader logic.

---

Nitpick comments:
In `@cmd/config/init.go`:
- Around line 255-283: The existing "result.Mode == 'existing' && result.AppID
!= ''" branch mixes four paths and should be extracted into a helper to simplify
logic: create a function (e.g., updateExistingAppConfig(existing, result, opts)
or core.UpdateExistingApp) that encapsulates the profile vs non-profile
branching, uses existing.FindAppIndex(opts.ProfileName) to update
Apps[idx].AppId/Brand/Lang when profile mode and existing, returns a validation
error when profile name not found, and otherwise uses
existing.CurrentAppConfig("") to update AppId/Brand/Lang or return the
validation error when nil; ensure the caller only handles calling this helper
and a single core.SaveMultiAppConfig(existing) invocation and returns any error
from those calls (keeping references to result.Mode, result.AppID,
opts.ProfileName, existing.FindAppIndex, existing.CurrentAppConfig, and
core.SaveMultiAppConfig).

In `@cmd/config/strict_mode_test.go`:
- Around line 73-89: The test TestStrictMode_SetOff_Profile (and similarly
TestStrictMode_Reset) ignores the result of the first cmd.Execute() call (the
invocation that sets "bot"), which can leave the test in a bad state; update the
test to check the error returned by that first cmd.Execute() and fail the test
if non-nil (e.g., use t.Fatal or t.Fatalf) before continuing, locating the calls
to cmd.Execute() in TestStrictMode_SetOff_Profile and TestStrictMode_Reset to
add the error check.
- Around line 51-52: Tests currently ignore the error return from
core.LoadMultiAppConfig() (called in strict_mode_test.go and elsewhere), which
hides failures; update each test that does "multi, _ :=
core.LoadMultiAppConfig()" to capture the error (e.g., "multi, err :=
core.LoadMultiAppConfig()") and assert/handle it (t.Fatalf or require.NoError)
before calling multi.CurrentAppConfig("") so failures are reported clearly;
apply the same pattern for all occurrences at the lines noted that call
LoadMultiAppConfig.

In `@cmd/profile/list.go`:
- Around line 17-25: The profileListItem struct currently marshals User and
TokenStatus as empty strings for profiles without values; update the struct tags
for the User and TokenStatus fields on profileListItem to use the `omitempty`
json option so those keys are omitted when empty (i.e., change the `json:"user"`
and `json:"tokenStatus"` tags to include `omitempty`), leaving other fields
unchanged.

In `@cmd/service/service.go`:
- Around line 258-262: In checkServiceScopes, do not silently swallow errors
from cred.ResolveToken: when cred.ResolveToken(ctx,
credential.NewTokenSpec(identity, config.AppID)) returns an error, a nil result,
or an empty result.Scopes, emit a debug/warning log (e.g., via the existing
logger in config or a context-aware logger) that includes the error message and
identity/AppID so operators can troubleshoot, then continue to return nil to
preserve the current behavior; update handling around the ResolveToken call and
use the function name checkServiceScopes and symbols cred.ResolveToken,
credential.NewTokenSpec, result and result.Scopes to locate where to add the
log.

In `@internal/cmdutil/factory_default.go`:
- Around line 68-69: The comment above the Lark client initialization is
misleading — it says "placeholder AppSecret" even though the real account secret
(acct.AppSecret) is passed into cachedLarkClientFunc; update the comment near
f.LarkClient = cachedLarkClientFunc(f) to remove or replace "placeholder
AppSecret" with wording that indicates the real account AppSecret is used
(reference symbols: f.LarkClient, cachedLarkClientFunc, acct.AppSecret).

In `@internal/core/config_strict_mode_test.go`:
- Around line 17-19: Replace the exact JSON string equality with structural
checks: unmarshal data into a map[string]interface{} (or a small struct), assert
that "apps" is a slice of length 1, and that the first app object has appId ==
"a", appSecret == "s", brand == "feishu"; also assert that the "users" field is
either absent or empty as required by strict mode (check absence in the map or
that len(users)==0). Use json.Unmarshal and t.Fatalf/t.Errorf for failures;
reference the existing variable data and the apps entry when making assertions.

In `@internal/core/config.go`:
- Around line 132-152: The ValidateProfileName function currently rejects
control characters via r <= 0x1F but does not explicitly list newline chars;
update the switch in ValidateProfileName to include '\n' and '\r' alongside the
other forbidden characters to make the intent explicit and consistent with other
validators (leave the control-character check intact so behavior is unchanged).

In `@internal/core/types.go`:
- Around line 16-23: ParseBrand currently does a case-sensitive comparison so
inputs like "Lark" or "LARK" will fall back to BrandFeishu; update ParseBrand to
perform a case-insensitive match (use strings.EqualFold(value, "lark")) and add
the "strings" import so inputs with different casing map to BrandLark while
still returning BrandFeishu for unrecognized values; keep the function signature
and return values (BrandLark, BrandFeishu) unchanged.

In `@internal/credential/credential_provider.go`:
- Around line 84-117: In enrichUserInfo, when prov.ResolveToken returns an error
that is not a BlockError (the branch after errors.As in the providers loop), add
a debug/info log that records the error and identifies the provider (e.g.,
provider ID/name or the prov variable) before continuing; keep the existing
behavior of skipping BlockError (return nil) and continuing on other errors, but
emit the log so non-BlockError failures are visible for debugging (use the
existing logger on CredentialProvider, e.g., p.logger, or the standard log
package if no logger field exists).

In `@internal/credential/default_provider_test.go`:
- Around line 7-13: Add a runtime behavioral test that exercises provider
selection and fallback through DefaultTokenProvider and DefaultAccountProvider
instead of only asserting interface conformance; specifically, register at least
two mock providers (one that returns a valid token/account and one that errors
or is lower priority), ensure the DefaultTokenProvider and
DefaultAccountProvider lookup pipelines prefer the higher-priority provider, and
verify that when the preferred provider fails the fallback provider is used. Use
the same public constructors/types (DefaultTokenProvider,
DefaultAccountProvider) and any registry or resolver entry points your code
exposes to add/remove test providers so the test sets up providers, calls the
resolve methods on DefaultTokenProvider/DefaultAccountProvider, asserts the
returned token/account and errors, and then cleans up the test registry state.

In `@shortcuts/drive/drive_download.go`:
- Around line 73-80: The returns for filesystem failures use the generic
"api_error" code; update the two error returns that call
vfs.MkdirAll(filepath.Dir(safePath), ...) and
validate.AtomicWriteFromReader(safePath, resp.Body, ...) to use a more accurate
code (e.g., "filesystem_error" or "io_error") instead of "api_error" so
filesystem/create-file failures are correctly categorized in output.Errorf calls
while keeping the existing error messages intact.

In `@shortcuts/im/helpers.go`:
- Around line 313-323: The code always constructs a media buffer via
newMediaBuffer even when s.withDuration is false, causing full in-memory copies
for plain file URLs; change the flow so newMediaBuffer is only called when
s.withDuration is true (to obtain mb.Duration()), and when s.withDuration is
false create or use a streaming reader for s.value and call uploadFileFromReader
with that streaming reader and the file metadata (avoiding
mb.Reader()/mb.FileName()/mb.FileType()). Keep the existing call-site
uploadFileFromReader and only switch the source/metadata to the streaming path
when not using mb so the buffer is only allocated when duration is needed.

In `@shortcuts/mail/mail_watch_test.go`:
- Around line 99-100: The test currently only asserts the URL for the profile
dry-run call (checking apis[1].URL against mailboxPath("me", "profile")) but
misses the HTTP method; update the assertion to also check the method (e.g.,
assert apis[1].Method == "GET" and call t.Fatalf with a clear message if it
differs) so a regression that changes the request verb will fail; modify the
test near the existing URL check referencing the apis slice and
mailboxPath("me","profile") to include this additional method assertion.

In `@shortcuts/sheets/sheet_export.go`:
- Around line 136-142: The filesystem error handling in the block that creates
parent directories and writes the file (calls to vfs.MkdirAll and
validate.AtomicWriteFromReader using safePath and resp.Body) incorrectly uses
the "api_error" error code; change the output.Errorf calls to use a
filesystem-specific error code (e.g. "fs_error" or "filesystem_error") and keep
the same messages and exit code (output.ExitInternal) so logs accurately reflect
local filesystem failures instead of API errors.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 14d486bc-8c74-4e89-a5d2-df5d325f72c1

📥 Commits

Reviewing files that changed from the base of the PR and between 135fde8 and c0fef4b.

📒 Files selected for processing (116)
  • .golangci.yml
  • 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/bootstrap.go
  • cmd/bootstrap_test.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/global_flags.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/prune.go
  • cmd/prune_test.go
  • cmd/root.go
  • cmd/root_e2e_test.go
  • cmd/root_integration_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
  • extension/credential/types_test.go
  • internal/auth/uat_client.go
  • internal/client/client.go
  • internal/client/client_test.go
  • internal/client/option.go
  • internal/client/response.go
  • internal/cmdutil/annotations.go
  • internal/cmdutil/annotations_test.go
  • internal/cmdutil/factory.go
  • internal/cmdutil/factory_default.go
  • internal/cmdutil/factory_default_test.go
  • internal/cmdutil/factory_test.go
  • internal/cmdutil/secheader.go
  • internal/cmdutil/testing.go
  • internal/core/config.go
  • internal/core/config_strict_mode_test.go
  • internal/core/config_test.go
  • internal/core/secret_resolve.go
  • internal/core/strict_mode.go
  • internal/core/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
  • internal/keychain/keychain_darwin.go
  • internal/keychain/keychain_other.go
  • internal/lockfile/lockfile.go
  • internal/registry/remote.go
  • internal/update/update.go
  • internal/validate/atomicwrite.go
  • internal/validate/path.go
  • internal/vfs/default.go
  • internal/vfs/fs.go
  • internal/vfs/osfs.go
  • internal/vfs/osfs_test.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/base_shortcut_helpers.go
  • shortcuts/base/record_upload_attachment.go
  • shortcuts/base/workflow_execute_test.go
  • shortcuts/calendar/calendar_test.go
  • shortcuts/common/helpers.go
  • shortcuts/common/runner.go
  • shortcuts/common/validate.go
  • shortcuts/doc/doc_media_download.go
  • shortcuts/doc/doc_media_insert.go
  • shortcuts/doc/doc_media_test.go
  • shortcuts/doc/doc_media_upload.go
  • shortcuts/drive/drive_download.go
  • shortcuts/drive/drive_export_common.go
  • shortcuts/drive/drive_io_test.go
  • shortcuts/drive/drive_upload.go
  • shortcuts/event/pipeline.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.go
  • shortcuts/im/helpers_network_test.go
  • shortcuts/im/helpers_test.go
  • shortcuts/im/im_messages_resources_download.go
  • shortcuts/im/im_messages_search_execute_test.go
  • shortcuts/mail/draft/patch.go
  • shortcuts/mail/emlbuilder/builder.go
  • shortcuts/mail/helpers.go
  • shortcuts/mail/mail_draft_edit.go
  • shortcuts/mail/mail_watch.go
  • shortcuts/mail/mail_watch_test.go
  • shortcuts/sheets/sheet_export.go
  • shortcuts/vc/vc_notes.go
  • shortcuts/vc/vc_notes_test.go
💤 Files with no reviewable changes (16)
  • cmd/api/api_test.go
  • cmd/service/service_test.go
  • shortcuts/base/base_advperm_test.go
  • shortcuts/base/base_dashboard_execute_test.go
  • shortcuts/base/base_execute_test.go
  • shortcuts/base/workflow_execute_test.go
  • shortcuts/calendar/calendar_test.go
  • shortcuts/im/convert_lib/helpers_test.go
  • shortcuts/im/convert_lib/thread_test.go
  • shortcuts/im/convert_lib/merge_test.go
  • shortcuts/base/base_form_execute_test.go
  • shortcuts/im/im_messages_search_execute_test.go
  • shortcuts/vc/vc_notes_test.go
  • shortcuts/doc/doc_media_test.go
  • cmd/root_e2e_test.go
  • shortcuts/base/base_role_test.go

Comment on lines +317 to 329
if multi != nil {
app := multi.FindApp(config.ProfileName)
if app != nil {
for _, oldUser := range app.Users {
if oldUser.UserOpenId != openId {
larkauth.RemoveStoredToken(config.AppID, oldUser.UserOpenId)
}
}
app.Users = []core.AppUser{{UserOpenId: openId, UserName: userName}}
if err := core.SaveMultiAppConfig(multi); err != nil {
return output.Errorf(output.ExitInternal, "internal", "failed to save config: %v", err)
}
}
app.Users = []core.AppUser{{UserOpenId: openId, UserName: userName}}
if err := core.SaveMultiAppConfig(multi); err != nil {
return output.Errorf(output.ExitInternal, "internal", "failed to save config: %v", 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

Don't silently skip the profile update when the selected profile can't be found.

The token is already persisted before this block runs. If multi.FindApp(config.ProfileName) returns nil, both paths still return success, but Users is never updated and stale user tokens are never pruned, leaving login only partially applied.

Also applies to: 399-412

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

In `@cmd/auth/login.go` around lines 317 - 329, When
multi.FindApp(config.ProfileName) returns nil the code currently silently skips
updating Users; change this so the login does not pretend success: if app == nil
return a visible error (use output.Errorf with a clear message like "profile %s
not found" and an internal exit code) instead of proceeding, and keep the
existing logic that prunes tokens via larkauth.RemoveStoredToken, sets app.Users
= []core.AppUser{{UserOpenId: openId, UserName: userName}}, and calls
core.SaveMultiAppConfig(multi) when app exists; apply the same nil-profile
handling to the other identical block that mirrors this logic.

Comment on lines +48 to +51
app := config.CurrentAppConfig(f.Invocation.Profile)
if app == nil {
fmt.Fprintln(f.IOStreams.ErrOut, "No active profile found.")
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.

⚠️ Potential issue | 🟠 Major

Return a non-zero error when profile resolution fails.

Lines 49-51 currently print an error and still return nil. If --profile points to a missing profile, the command exits 0 and automation can't distinguish that from a successful config show. Please return an error here and include the requested profile name when one was supplied.

Suggested fix
 	app := config.CurrentAppConfig(f.Invocation.Profile)
 	if app == nil {
-		fmt.Fprintln(f.IOStreams.ErrOut, "No active profile found.")
-		return nil
+		if f.Invocation.Profile != "" {
+			return fmt.Errorf("profile %q not found", f.Invocation.Profile)
+		}
+		return fmt.Errorf("no active profile found")
 	}
📝 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
app := config.CurrentAppConfig(f.Invocation.Profile)
if app == nil {
fmt.Fprintln(f.IOStreams.ErrOut, "No active profile found.")
return nil
app := config.CurrentAppConfig(f.Invocation.Profile)
if app == nil {
if f.Invocation.Profile != "" {
return fmt.Errorf("profile %q not found", f.Invocation.Profile)
}
return fmt.Errorf("no active profile found")
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/config/show.go` around lines 48 - 51, The command currently prints "No
active profile found." when config.CurrentAppConfig(f.Invocation.Profile)
returns nil but still returns nil; change this to return a non-zero error
instead: construct and return an error (e.g. via fmt.Errorf) that includes the
requested profile name when f.Invocation.Profile is set, and preserve the
existing ErrOut print or replace it with the error message; update the block
around app := config.CurrentAppConfig(f.Invocation.Profile) in
cmd/config/show.go (the code handling nil app) to return that error so the
command exits non-zero on missing profile.

Comment on lines +44 to +47
app := multi.CurrentAppConfig(f.Invocation.Profile)
if app == nil {
return output.ErrWithHint(output.ExitValidation, "config", "no active profile", "run: lark-cli config init")
}
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

Global strict-mode operations are incorrectly blocked by active-profile requirement.

The early app == nil validation prevents --global set/show flows from running, even though they don’t need a profile app.

✅ Suggested fix
 			app := multi.CurrentAppConfig(f.Invocation.Profile)
-			if app == nil {
+			if !global && app == nil {
 				return output.ErrWithHint(output.ExitValidation, "config", "no active profile", "run: lark-cli config init")
 			}
📝 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
app := multi.CurrentAppConfig(f.Invocation.Profile)
if app == nil {
return output.ErrWithHint(output.ExitValidation, "config", "no active profile", "run: lark-cli config init")
}
app := multi.CurrentAppConfig(f.Invocation.Profile)
if !global && app == nil {
return output.ErrWithHint(output.ExitValidation, "config", "no active profile", "run: lark-cli config init")
}
🤖 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 44 - 47, The current check
immediately returns if app == nil, which blocks commands using the --global
flag; change the validation so the nil-app early return only runs for non-global
operations. Concretely, in the block where app :=
multi.CurrentAppConfig(f.Invocation.Profile) and the subsequent if app == nil
return statement, gate that return behind a check for the global flag (i.e.,
only enforce app != nil when the command is not running with --global). Keep the
rest of the flow unchanged so global set/show commands bypass the
profile-required validation.

Comment on lines +72 to +75
multi, err := core.LoadMultiAppConfig()
if err != nil {
multi = &core.MultiAppConfig{}
}
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

Don't collapse every config-load failure into "start a new config".

This treats parse/permission errors the same as "config not found". If the existing multi-profile file is corrupt or unreadable, profile add will build a fresh MultiAppConfig here and can overwrite the user's current profiles on save.

🛡️ Suggested fix
+import (
+	"errors"
+	"os"
+)
+
 	// Load or create config
 	multi, err := core.LoadMultiAppConfig()
 	if err != nil {
-		multi = &core.MultiAppConfig{}
+		if !errors.Is(err, os.ErrNotExist) {
+			return output.Errorf(output.ExitInternal, "internal", "failed to load config: %v", err)
+		}
+		multi = &core.MultiAppConfig{}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/profile/add.go` around lines 72 - 75, The current code in
cmd/profile/add.go calls core.LoadMultiAppConfig() and always replaces errors by
creating a new &core.MultiAppConfig{}, which risks overwriting a
corrupt/unreadable config; change the logic in the LoadMultiAppConfig() caller
so that after calling core.LoadMultiAppConfig() you only create a new
MultiAppConfig when the error indicates a missing file (e.g. os.IsNotExist or a
specific "not found" error from core), but for other errors (parse/permission)
return or propagate the error instead of silently continuing; reference the call
site (core.LoadMultiAppConfig and the local variable multi) and add an explicit
conditional that distinguishes not-found from other failures and handles each
appropriately.

Comment on lines +52 to +71
// Cleanup keychain: app secret + user tokens
core.RemoveSecretStore(app.AppSecret, f.Keychain)
for _, user := range app.Users {
larkauth.RemoveStoredToken(app.AppId, user.UserOpenId)
}

// Remove from slice
multi.Apps = append(multi.Apps[:idx], multi.Apps[idx+1:]...)

// Fix currentApp / previousApp references
if multi.CurrentApp == removedName {
multi.CurrentApp = multi.Apps[0].ProfileName()
}
if multi.PreviousApp == removedName {
multi.PreviousApp = ""
}

if err := core.SaveMultiAppConfig(multi); err != nil {
return fmt.Errorf("failed to save config: %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

Order of operations can cause partial destructive failure.

Credentials are deleted before config persistence. If Line 69 save fails, profile config remains but its secrets/tokens are already gone.

✅ Suggested fix (save first, then cleanup)
-	// Cleanup keychain: app secret + user tokens
-	core.RemoveSecretStore(app.AppSecret, f.Keychain)
-	for _, user := range app.Users {
-		larkauth.RemoveStoredToken(app.AppId, user.UserOpenId)
-	}
-
 	// Remove from slice
 	multi.Apps = append(multi.Apps[:idx], multi.Apps[idx+1:]...)
@@
 	if err := core.SaveMultiAppConfig(multi); err != nil {
 		return fmt.Errorf("failed to save config: %w", err)
 	}
+
+	// Best-effort credential cleanup after config commit
+	core.RemoveSecretStore(app.AppSecret, f.Keychain)
+	for _, user := range app.Users {
+		larkauth.RemoveStoredToken(app.AppId, user.UserOpenId)
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/profile/remove.go` around lines 52 - 71, The code currently removes
secrets/tokens before persisting the updated multi-app config which can leave
the system in a partially destructive state if core.SaveMultiAppConfig fails;
change the order so you first remove the app from multi.Apps (the slice append
operation) and update multi.CurrentApp/multi.PreviousApp as currently done, then
call core.SaveMultiAppConfig(multi) and only after that succeeds call
core.RemoveSecretStore(app.AppSecret, f.Keychain) and
larkauth.RemoveStoredToken(app.AppId, user.UserOpenId) for each user; keep the
same functions and slice manipulation (multi.Apps = append(...)), but move the
persistence step (core.SaveMultiAppConfig) before the calls to
core.RemoveSecretStore and larkauth.RemoveStoredToken so secrets are only
deleted after a successful save.

Comment on lines +16 to +26
data, _ := json.Marshal(m)
if string(data) != `{"apps":[{"appId":"a","appSecret":"s","brand":"feishu","users":[]}]}` {
t.Errorf("StrictMode empty should be omitted, got: %s", data)
}

// StrictMode="bot" should be present
m.StrictMode = StrictModeBot
data, _ = json.Marshal(m)
var parsed map[string]interface{}
json.Unmarshal(data, &parsed)
if parsed["strictMode"] != "bot" {
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

Handle JSON (un)marshal errors explicitly in tests.

Right now these calls ignore error, which can mask serialization failures and produce noisy assertions.

✅ Suggested fix
-	data, _ := json.Marshal(m)
+	data, err := json.Marshal(m)
+	if err != nil {
+		t.Fatalf("marshal failed: %v", err)
+	}
@@
-	json.Unmarshal(data, &parsed)
+	if err := json.Unmarshal(data, &parsed); err != nil {
+		t.Fatalf("unmarshal failed: %v", err)
+	}
@@
-	data, _ := json.Marshal(app)
+	data, err := json.Marshal(app)
+	if err != nil {
+		t.Fatalf("marshal failed: %v", err)
+	}
@@
-	json.Unmarshal(data, &parsed)
+	if err := json.Unmarshal(data, &parsed); err != nil {
+		t.Fatalf("unmarshal failed: %v", err)
+	}

Also applies to: 34-36, 44-45, 53-54

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

In `@internal/core/config_strict_mode_test.go` around lines 16 - 26, The test
currently ignores errors from json.Marshal/json.Unmarshal which can hide
failures; update all Marshal/Unmarshal calls in this test (the marshaling of m
after setting StrictMode and the subsequent Unmarshal into parsed, as well as
the other Marshal/Unmarshal occurrences noted) to capture the error and fail the
test on error (use t.Fatalf or t.Fatal with the error), e.g., check err :=
json.Marshal(m) and err := json.Unmarshal(data, &parsed) and assert err == nil
before proceeding; this applies to the Marshal/Unmarshal around m.StrictMode,
the parsed map.Unmarshal, and the other instances in the same file.

Comment on lines +95 to +99
a1, _ := cp.ResolveAccount(context.Background())
a2, _ := cp.ResolveAccount(context.Background())
if a1 != a2 {
t.Error("expected same pointer (cached)")
}
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

AccountCached can pass incorrectly when resolution fails.

Lines 95-96 ignore errors, so a1 == a2 can be true for two nil values.

✅ Suggested fix
-	a1, _ := cp.ResolveAccount(context.Background())
-	a2, _ := cp.ResolveAccount(context.Background())
+	a1, err := cp.ResolveAccount(context.Background())
+	if err != nil {
+		t.Fatal(err)
+	}
+	a2, err := cp.ResolveAccount(context.Background())
+	if err != nil {
+		t.Fatal(err)
+	}
+	if a1 == nil || a2 == nil {
+		t.Fatal("expected non-nil accounts")
+	}
 	if a1 != a2 {
 		t.Error("expected same pointer (cached)")
 	}
📝 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
a1, _ := cp.ResolveAccount(context.Background())
a2, _ := cp.ResolveAccount(context.Background())
if a1 != a2 {
t.Error("expected same pointer (cached)")
}
a1, err := cp.ResolveAccount(context.Background())
if err != nil {
t.Fatal(err)
}
a2, err := cp.ResolveAccount(context.Background())
if err != nil {
t.Fatal(err)
}
if a1 == nil || a2 == nil {
t.Fatal("expected non-nil accounts")
}
if a1 != a2 {
t.Error("expected same pointer (cached)")
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/credential/credential_provider_test.go` around lines 95 - 99, The
test for AccountCached is currently ignoring errors from cp.ResolveAccount so a1
== a2 can be true when both are nil; change the test to capture and assert both
errors are nil (e.g., err1, err2 from cp.ResolveAccount) before comparing
pointers, or explicitly fail if either err is non-nil, then assert that a1 and
a2 are the same pointer; locate the calls to cp.ResolveAccount and variables
a1/a2 in the test and add the nil-error checks and appropriate test failures
before the pointer equality assertion.

Comment on lines +49 to +67
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, 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 | 🟡 Minor

Integration tests should fully isolate env state for deterministic provider-chain behavior.

These tests depend on env provider fallthrough/config path but do not clear all related env inputs (e.g., token and strict-mode vars), so external env can change outcomes.

Also applies to: 78-112

🤖 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 49 - 67, The test
TestFullChain_Fallthrough (and the other tests around lines 78-112) relies on
the env provider but doesn't isolate environment variables; update the tests to
clear and restore all env vars the envprovider reads (e.g., token and
strict-mode vars and any provider-specific env keys) before creating ep so the
provider always falls through deterministically — in Go, prefer using
t.Setenv("VAR_NAME", "") or explicitly unset/restore around the test; ensure the
same isolation change is applied to the other tests referenced so external env
cannot alter provider-chain behavior.

Comment on lines +327 to +339
func resolveLocalMedia(ctx context.Context, runtime *common.RuntimeContext, s mediaSpec) (string, error) {
fmt.Fprintf(runtime.IO().ErrOut, "uploading %s: %s\n", s.mediaType, filepath.Base(s.value))

if s.kind == mediaKindImage {
return uploadImageToIM(ctx, runtime, s.value, "message")
}

ft := detectIMFileType(s.value)
dur := ""
if s.withDuration {
dur = parseMediaDuration(s.value, ft)
}
return uploadFileToIM(ctx, runtime, s.value, ft, dur)
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

Validate the local path before parsing duration.

For --audio and --video, parseMediaDuration opens s.value before uploadFileToIM gets a chance to run validate.SafeInputPath. That reintroduces a raw filesystem read on untrusted input for every local path that maps to mp4/opus.

🔒 Suggested fix
func resolveLocalMedia(ctx context.Context, runtime *common.RuntimeContext, s mediaSpec) (string, error) {
-	fmt.Fprintf(runtime.IO().ErrOut, "uploading %s: %s\n", s.mediaType, filepath.Base(s.value))
+	safePath, err := validate.SafeInputPath(s.value)
+	if err != nil {
+		return "", err
+	}
+	fmt.Fprintf(runtime.IO().ErrOut, "uploading %s: %s\n", s.mediaType, filepath.Base(safePath))
 
 	if s.kind == mediaKindImage {
-		return uploadImageToIM(ctx, runtime, s.value, "message")
+		return uploadImageToIM(ctx, runtime, safePath, "message")
 	}
 
-	ft := detectIMFileType(s.value)
+	ft := detectIMFileType(safePath)
 	dur := ""
 	if s.withDuration {
-		dur = parseMediaDuration(s.value, ft)
+		dur = parseMediaDuration(safePath, ft)
 	}
-	return uploadFileToIM(ctx, runtime, s.value, ft, dur)
+	return uploadFileToIM(ctx, runtime, safePath, ft, dur)
}
📝 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 resolveLocalMedia(ctx context.Context, runtime *common.RuntimeContext, s mediaSpec) (string, error) {
fmt.Fprintf(runtime.IO().ErrOut, "uploading %s: %s\n", s.mediaType, filepath.Base(s.value))
if s.kind == mediaKindImage {
return uploadImageToIM(ctx, runtime, s.value, "message")
}
ft := detectIMFileType(s.value)
dur := ""
if s.withDuration {
dur = parseMediaDuration(s.value, ft)
}
return uploadFileToIM(ctx, runtime, s.value, ft, dur)
func resolveLocalMedia(ctx context.Context, runtime *common.RuntimeContext, s mediaSpec) (string, error) {
safePath, err := validate.SafeInputPath(s.value)
if err != nil {
return "", err
}
fmt.Fprintf(runtime.IO().ErrOut, "uploading %s: %s\n", s.mediaType, filepath.Base(safePath))
if s.kind == mediaKindImage {
return uploadImageToIM(ctx, runtime, safePath, "message")
}
ft := detectIMFileType(safePath)
dur := ""
if s.withDuration {
dur = parseMediaDuration(safePath, ft)
}
return uploadFileToIM(ctx, runtime, safePath, ft, dur)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/im/helpers.go` around lines 327 - 339, The resolveLocalMedia
function calls parseMediaDuration on s.value before uploadFileToIM can run
validate.SafeInputPath, allowing an unsafe filesystem open for audio/video
files; fix by validating the input path first: in resolveLocalMedia call
validate.SafeInputPath (or the equivalent path validation helper used by
uploadFileToIM) on s.value before calling detectIMFileType or parseMediaDuration
so any local file is checked/normalized first, then proceed to
parseMediaDuration and finally call uploadFileToIM (keep the existing
uploadImageToIM branch unchanged).

Change-Id: If61578631f5698f7ca2d9a946ca59753651463fb
@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 3, 2026

Greptile Summary

This large PR introduces profile management (add/list/use/remove/rename), strict-mode identity filtering, a credential extension framework with an env provider, and a VFS abstraction for testability. The architecture is well-designed: credential resolution is a clean chain (extension providers → default), strict mode pruning is composable via Cobra command replacement, and the sync.Once caching pattern is appropriate for a CLI's single-invocation lifecycle.

  • P1 — cmd/profile/rename.go: The uniqueness check FindApp(newName) matches the profile being renamed via its own AppId, causing a spurious "already exists" error when renaming a profile to a string that equals its AppId. This blocks a valid user action.
  • Previously flagged issues (env brand default, proxy bypass, AppID missing from TokenSpec, double timeout in DoStream) remain unaddressed.

Confidence Score: 4/5

Safe to merge with the rename bug fixed; all other open issues are P2 style or carry-over from prior review threads.

One P1 logic bug in cmd/profile/rename.go (false conflict blocks valid rename). Three previously-flagged P1 issues (env brand default, proxy bypass, AppID in TokenSpec) remain open. If those are intentional or deferred, score would rise; the rename bug is the only new blocker.

cmd/profile/rename.go (P1 rename conflict bug), internal/credential/credential_provider.go (P2 unnecessary UAT enrichment for bot-only accounts)

Important Files Changed

Filename Overview
cmd/profile/rename.go New rename command; FindApp(newName) incorrectly matches same profile's own AppId, blocking rename to own AppId (P1 bug)
internal/credential/credential_provider.go Unified credential provider with sync.Once caching; enrichUserInfo runs for bot-only accounts, making unnecessary UAT API calls
internal/core/config.go Major refactor: profile management (FindApp/CurrentAppConfig/ValidateProfileName), vfs migration, StrictMode on AppConfig and MultiAppConfig
internal/credential/default_provider.go Default account/token providers wrapping config+keychain; TAT resolved via direct HTTP with sync.Once caching
extension/credential/env/env.go Env-var credential provider; defaults brand to 'lark' when LARK_BRAND unset (prior review); SupportedIdentities inferred from token env vars
internal/cmdutil/factory_default.go Credential-first factory; drops LARK_CLI_NO_PROXY transport via http.DefaultTransport (prior review); token cache disabled on LarkClient
internal/client/client.go DoSDKRequest now resolves tokens via Credential; new DoStream for streaming downloads; double-timeout noted in prior review
cmd/prune.go pruneForStrictMode replaces incompatible commands with hidden stubs; pruneEmpty removes empty groups
cmd/config/strict_mode.go New strict-mode subcommand (show/set/reset) with global/profile scoping and admin-warning text
internal/cmdutil/factory.go Removes AuthConfig, adds Credential field, ResolveStrictMode, CheckStrictMode; strict mode integrated into ResolveAs
shortcuts/common/runner.go DoAPIStream delegates to client.DoStream; AccessToken uses Credential; new resolveInputFlags for @file/stdin flag support
internal/vfs/default.go VFS abstraction with global DefaultFS; all filesystem ops proxied through package-level functions for testability
cmd/root.go Bootstrap invocation context before factory construction; wires profile command and strict-mode pruning
internal/core/strict_mode.go New StrictMode type with AllowsIdentity, ForcedIdentity, IsActive — clean and correct

Sequence Diagram

sequenceDiagram
    participant CLI as lark-cli Execute()
    participant Boot as BootstrapInvocationContext
    participant Cred as CredentialProvider
    participant Ext as ExtensionProviders
    participant Def as DefaultAccountProvider
    participant Cfg as config.json
    participant Prune as pruneForStrictMode
    participant Cmd as Command.RunE

    CLI->>Boot: parse --profile flag
    Boot-->>CLI: InvocationContext{Profile}
    CLI->>Cred: NewCredentialProvider(ext, default)
    CLI->>Cred: ResolveStrictMode() → ResolveAccount()
    Cred->>Ext: ResolveAccount() [env provider]
    alt LARK_APP_ID set
        Ext-->>Cred: &Account{SupportedIdentities}
    else
        Ext-->>Cred: nil, nil
        Cred->>Def: ResolveAccount()
        Def->>Cfg: LoadMultiAppConfig()
        Cfg-->>Def: MultiAppConfig
        Def-->>Cred: &Account{SupportedIdentities from StrictMode}
    end
    Cred-->>CLI: StrictMode (bot|user|off)
    CLI->>Prune: pruneForStrictMode(rootCmd, mode)
    Note over Prune: replaces incompatible commands with hidden stubs
    CLI->>Cmd: Execute command
    Cmd->>Cred: ResolveToken(TokenSpec)
    Cred-->>Cmd: TokenResult{Token}
    Cmd->>CLI: response
Loading

Greploops — Automatically fix all review issues by running /greploops in Claude Code. It iterates: fix, push, re-review, repeat until 5/5 confidence.
Use the Greptile plugin for Claude Code to query reviews, search comments, and manage custom context directly from your terminal.

Reviews (2): Last reviewed commit: "fix: fix pre-existing test failures in t..." | Re-trigger Greptile

Comment on lines +33 to +36
}
acct := &credential.Account{AppID: appID, AppSecret: appSecret, Brand: brand}

// Explicit strict mode policy takes priority
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 Inconsistent default brand with rest of CLI

The env provider silently defaults to "lark" when LARK_BRAND is unset, but cmd/profile/add.go defaults to "feishu" and core.ParseBrand treats unrecognized values as BrandFeishu. A user who relies on env-var credentials without setting LARK_BRAND will be routed to Lark endpoints while every other config path defaults to Feishu — a silent misconfiguration that can produce confusing 401/404 errors.

Suggested change
}
acct := &credential.Account{AppID: appID, AppSecret: appSecret, Brand: brand}
// Explicit strict mode policy takes priority
brand := os.Getenv("LARK_BRAND")
if brand == "" {
brand = "feishu"
}

Comment on lines +115 to 116
lark.WithEnableTokenCache(false),
lark.WithLogLevel(larkcore.LogLevelError),
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 LARK_CLI_NO_PROXY no longer respected for Lark SDK transport

Replacing util.NewBaseTransport() with http.DefaultTransport drops the LARK_CLI_NO_PROXY proxy-bypass behaviour. The warning via WarnIfProxied still fires, but requests will continue to transit the proxy even when the user has set LARK_CLI_NO_PROXY=1. util.NewBaseTransport() clones DefaultTransport and conditionally zeroes out the Proxy field — that intent is now lost.

Suggested change
lark.WithEnableTokenCache(false),
lark.WithLogLevel(larkcore.LogLevelError),
var sdkTransport http.RoundTripper = util.NewBaseTransport()

Comment on lines +103 to +110
// Have UAT — must verify and resolve identity
hc, err := p.httpClient()
if err != nil {
return fmt.Errorf("failed to get HTTP client for user_info: %w", err)
}
info, err := fetchUserInfo(ctx, hc, acct.Brand, tok.Value)
if err != nil {
return fmt.Errorf("failed to verify user identity: %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.

P2 AppID missing from TokenSpec in enrichUserInfo

ResolveToken is called with extcred.TokenSpec{Type: extcred.TokenTypeUAT}AppID is not populated. Any extension provider that uses AppID to discriminate (e.g. a multi-tenant vault backend keyed by app ID) will receive an empty string and may skip resolution or return a token for the wrong app. The resolved acct.AppID is available here and should be passed.

Suggested change
// Have UAT — must verify and resolve identity
hc, err := p.httpClient()
if err != nil {
return fmt.Errorf("failed to get HTTP client for user_info: %w", err)
}
info, err := fetchUserInfo(ctx, hc, acct.Brand, tok.Value)
if err != nil {
return fmt.Errorf("failed to verify user identity: %w", err)
tok, err := prov.ResolveToken(ctx, extcred.TokenSpec{Type: extcred.TokenTypeUAT, AppID: acct.AppID})

Comment on lines +107 to +116
// Resolve auth
result, err := c.Credential.ResolveToken(ctx, credential.NewTokenSpec(as, c.Config.AppID))
if err != nil {
return nil, err
}

// Build URL
requestURL, err := buildStreamURL(c.Config.Brand, req)
if err != nil {
return nil, 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.

P2 Double timeout applied in DoStream when context has no deadline

When cfg.timeout > 0 and the incoming context has no deadline, both httpClient.Timeout and context.WithTimeout are set to the same cfg.timeout duration. The HTTP client timeout fires on the full round-trip (connect + read), while context.WithTimeout fires from request start; both use the same value so one is redundant. Consider setting only the context timeout and leaving httpClient.Timeout at zero, or setting only httpClient.Timeout and skipping the context derivative:

if cfg.timeout > 0 {
    if _, hasDeadline := ctx.Deadline(); !hasDeadline {
        requestCtx, cancel = context.WithTimeout(ctx, cfg.timeout)
    }
}

liangshuo-1 and others added 2 commits April 4, 2026 15:40
Add framework-level support for reading flag values from files (@path)
or stdin (-), solving the fundamental problem of passing complex text
(markdown, multi-line content) via CLI arguments where shell escaping
breaks content. Closes #239, fixes #163.

- Add File/Stdin constants and Input field to Flag struct
- Add resolveInputFlags() in runner pipeline (pre-Validate)
- Support @@ escape for literal @ prefix
- Guard against multiple stdin consumers
- Auto-append "(supports @file, - for stdin)" to help text
- Apply to: docs +create/+update --markdown, im +messages-send/+reply
  --text/--markdown/--content, task +comment --content,
  drive +add-comment --content

Change-Id: I305a326d972417542aeadd70f37b74ea456461ef
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- task/minutes: remove unused tenant_access_token httpmock stubs
  (TestFactory's testDefaultToken provides tokens directly, so the
  HTTP stub was never consumed and failed verification)
- registry: fix hasEmbeddedData() to check for actual services instead
  of just byte length (meta_data_default.json has empty services array)

Change-Id: Ic7b5fc7f9de09137a7254fe1ddf47d24ade40587
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions github-actions bot added the domain/task PR touches the task domain label Apr 4, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
internal/registry/remote_test.go (1)

32-41: Clarify helper semantics to avoid ambiguous test gating.

hasEmbeddedData() now effectively checks for non-empty services, not just embedded payload presence/parseability. That diverges from production embedded-load criteria and may skip/route tests differently when embedded JSON is valid but services is empty/null. Consider renaming this helper (e.g., hasEmbeddedServices) or splitting into two helpers (hasEmbeddedJSON vs hasEmbeddedServices) and using each explicitly per test intent.

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

In `@internal/registry/remote_test.go` around lines 32 - 41, The current helper
hasEmbeddedData() conflates presence/parseability of embeddedMetaJSON with
having non-empty reg.Services; update tests by either renaming this function to
hasEmbeddedServices() to reflect its true check (non-empty Services) or split it
into two helpers—hasEmbeddedJSON() that returns true if embeddedMetaJSON
unmarshal succeeds and hasEmbeddedServices() that checks
len(reg.Services)>0—then update test usages to call the appropriate helper
depending on whether the test is gating on parseable embedded JSON (use
hasEmbeddedJSON) or on actual embedded service data (use hasEmbeddedServices);
refer to the existing function name hasEmbeddedData, the variable
embeddedMetaJSON, and the MergedRegistry type to locate and implement the
change.
shortcuts/common/runner.go (1)

523-530: Fail fast when Input is attached to a non-string flag.

resolveInputFlags() always goes through GetString/Set, so Input on bool, int, or string_array flags will currently no-op or swallow the type mismatch until later. A small guard here would make the new metadata much harder to misuse.

♻️ Proposed change
-		raw, _ := rctx.Cmd.Flags().GetString(fl.Name)
+		raw, err := rctx.Cmd.Flags().GetString(fl.Name)
+		if err != nil {
+			return FlagErrorf("--%s: Input is only supported for string flags", fl.Name)
+		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/common/runner.go` around lines 523 - 530, resolveInputFlags
currently always calls GetString/Set on flags with Input metadata which hides
type mismatches; update resolveInputFlags to lookup each flag (use
rctx.Cmd.Flags().Lookup(fl.Name)) and fail fast if the flag's Value.Type() is
not "string" (or otherwise not compatible with GetString/Set), returning a clear
error for invalid Input usage instead of silently no-oping; this should be
applied inside resolveInputFlags before calling GetString/Set so Input on
bool/int/string_array is rejected early.
🤖 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/common/runner.go`:
- Around line 534-549: The current branch treats raw == "-" as always meaning
"read from stdin", preventing passing a literal "-" to flags (e.g., --text);
change the resolver to accept an explicit escape for a literal dash such as "\-"
(or another chosen sentinel) by first checking if raw equals the escape sequence
and, if so, set the flag value to "-" instead of reading stdin; otherwise
preserve the existing behavior where raw == "-" consumes rctx.IO().In, using the
same symbols fl.Name, Stdin, stdinUsed and rctx.IO().In, and update the flag
help/docs to document the escape.

---

Nitpick comments:
In `@internal/registry/remote_test.go`:
- Around line 32-41: The current helper hasEmbeddedData() conflates
presence/parseability of embeddedMetaJSON with having non-empty reg.Services;
update tests by either renaming this function to hasEmbeddedServices() to
reflect its true check (non-empty Services) or split it into two
helpers—hasEmbeddedJSON() that returns true if embeddedMetaJSON unmarshal
succeeds and hasEmbeddedServices() that checks len(reg.Services)>0—then update
test usages to call the appropriate helper depending on whether the test is
gating on parseable embedded JSON (use hasEmbeddedJSON) or on actual embedded
service data (use hasEmbeddedServices); refer to the existing function name
hasEmbeddedData, the variable embeddedMetaJSON, and the MergedRegistry type to
locate and implement the change.

In `@shortcuts/common/runner.go`:
- Around line 523-530: resolveInputFlags currently always calls GetString/Set on
flags with Input metadata which hides type mismatches; update resolveInputFlags
to lookup each flag (use rctx.Cmd.Flags().Lookup(fl.Name)) and fail fast if the
flag's Value.Type() is not "string" (or otherwise not compatible with
GetString/Set), returning a clear error for invalid Input usage instead of
silently no-oping; this should be applied inside resolveInputFlags before
calling GetString/Set so Input on bool/int/string_array is rejected early.
🪄 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: 01f9834f-5999-4554-96e6-e0e01827262c

📥 Commits

Reviewing files that changed from the base of the PR and between cb3fe91 and 0b26e33.

📒 Files selected for processing (12)
  • internal/registry/remote_test.go
  • shortcuts/common/runner.go
  • shortcuts/common/runner_input_test.go
  • shortcuts/common/types.go
  • shortcuts/doc/docs_create.go
  • shortcuts/doc/docs_update.go
  • shortcuts/drive/drive_add_comment.go
  • shortcuts/im/im_messages_reply.go
  • shortcuts/im/im_messages_send.go
  • shortcuts/minutes/minutes_download_test.go
  • shortcuts/task/task_comment.go
  • shortcuts/task/task_shortcut_test.go
💤 Files with no reviewable changes (2)
  • shortcuts/task/task_shortcut_test.go
  • shortcuts/minutes/minutes_download_test.go
✅ Files skipped from review due to trivial changes (1)
  • shortcuts/im/im_messages_reply.go

Comment on lines +534 to +549
// stdin: -
if raw == "-" {
if !slices.Contains(fl.Input, Stdin) {
return FlagErrorf("--%s does not support stdin (-)", fl.Name)
}
if stdinUsed {
return FlagErrorf("--%s: stdin (-) can only be used by one flag", fl.Name)
}
stdinUsed = true
data, err := io.ReadAll(rctx.IO().In)
if err != nil {
return FlagErrorf("--%s: failed to read from stdin: %v", fl.Name, err)
}
rctx.Cmd.Flags().Set(fl.Name, string(data))
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 | 🟡 Minor

Keep a way to pass a literal "-" value.

For any flag that enables Stdin, this branch makes "-" impossible to send as ordinary content because the resolver always consumes stdin instead. That is a real regression for the newly enabled plain-text flags in this PR, like --text and --markdown. Please add a documented escape or bypass for literal dash values.

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

In `@shortcuts/common/runner.go` around lines 534 - 549, The current branch treats
raw == "-" as always meaning "read from stdin", preventing passing a literal "-"
to flags (e.g., --text); change the resolver to accept an explicit escape for a
literal dash such as "\-" (or another chosen sentinel) by first checking if raw
equals the escape sequence and, if so, set the flag value to "-" instead of
reading stdin; otherwise preserve the existing behavior where raw == "-"
consumes rctx.IO().In, using the same symbols fl.Name, Stdin, stdinUsed and
rctx.IO().In, and update the flag help/docs to document the escape.

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/task PR touches the task 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