Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func TestAuthLoginCmd_HelpGuidesNonStreamingAgentsToSplitFlow(t *testing.T) {
"only delivers final turn messages",
"--no-wait --json",
"send the verification URL (or QR code) to the user as your final message",
"run --device-code in a later step",
"run --resume in a later step",
} {
if !strings.Contains(got, want) {
t.Fatalf("help missing %q, got:\n%s", want, got)
Expand Down
35 changes: 27 additions & 8 deletions cmd/auth/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ package auth
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"sort"
"strings"
"time"
Expand Down Expand Up @@ -36,6 +38,7 @@ type LoginOptions struct {
Exclude []string
NoWait bool
DeviceCode string
Resume bool
}

var pollDeviceToken = larkauth.PollDeviceToken
Expand All @@ -52,7 +55,7 @@ func NewCmdAuthLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.
For AI agents: this command blocks until the user completes authorization in the
browser. If your harness or agent tool only delivers final turn messages, use --no-wait --json,
send the verification URL (or QR code) to the user as your final message, end the turn, then
run --device-code in a later step after the user confirms authorization. Use 'lark-cli auth qrcode'
run --resume in a later step after the user confirms authorization. Use 'lark-cli auth qrcode'
to generate QR codes (supports ASCII and PNG formats).`,
RunE: func(cmd *cobra.Command, args []string) error {
if mode := f.ResolveStrictMode(cmd.Context()); mode == core.StrictModeBot {
Expand Down Expand Up @@ -84,8 +87,9 @@ to generate QR codes (supports ASCII and PNG formats).`,
cmd.Flags().StringSliceVar(&opts.Exclude, "exclude", nil,
"scopes to exclude from the request (repeatable or comma-separated, e.g. --exclude drive:file:download)")
cmd.Flags().BoolVar(&opts.JSON, "json", false, "structured JSON output")
cmd.Flags().BoolVar(&opts.NoWait, "no-wait", false, "initiate device authorization and return immediately; use --device-code to complete")
cmd.Flags().BoolVar(&opts.NoWait, "no-wait", false, "initiate device authorization and return immediately; use --resume to complete later")
cmd.Flags().StringVar(&opts.DeviceCode, "device-code", "", "poll and complete authorization with a device code from a previous --no-wait call")
cmd.Flags().BoolVar(&opts.Resume, "resume", false, "resume the latest pending authorization created by --no-wait")
Comment on lines +90 to +92

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add direct parsing coverage for --resume.

The new Cobra flag is not exercised by the supplied flag-parsing test. Add a command-level assertion that --resume sets LoginOptions.Resume.

As per coding guidelines, every behavior change needs a test alongside the change.

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

In `@cmd/auth/login.go` around lines 90 - 92, Add command-level flag-parsing
coverage for the `--resume` option in the login command tests, asserting that
parsing `--resume` sets `LoginOptions.Resume` to true. Keep the existing parsing
coverage unchanged and place the assertion alongside the other login flag tests.

Source: Coding guidelines


cmdutil.RegisterFlagCompletion(cmd, "domain", func(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return completeDomain(toComplete), cobra.ShellCompDirectiveNoFileComp
Expand Down Expand Up @@ -132,6 +136,21 @@ func authLoginRun(opts *LoginOptions) error {
}
msg := getLoginMsg(lang)

if opts.Resume {
if opts.DeviceCode != "" || opts.NoWait || opts.Scope != "" || opts.Recommend || len(opts.Domains) > 0 || len(opts.Exclude) > 0 {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--resume cannot be combined with authorization request options or --device-code").WithParam("--resume")
}
pending, err := loadPendingLogin(config.AppID)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return errs.NewAuthenticationError(errs.SubtypeUnknown, "no unexpired pending authorization to resume").
WithHint("start a fresh split flow with `lark-cli auth login --scope <scope> --no-wait --json`, send its verification URL to the user, then run `lark-cli auth login --resume` only after the user confirms authorization")
}
return errs.NewInternalError(errs.SubtypeStorage, "failed to load pending authorization: %v", err).WithCause(err)
Comment on lines +139 to +149

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Classify missing pending state as a failed precondition.

--resume is valid input, but no resumable state exists. Returning AuthenticationError/Unknown breaks the typed command contract; use ValidationError/FailedPrecondition and retain the recovery hint. Add metadata assertions using errs.ProblemOf.

As per coding guidelines, valid requests made in the wrong system state must use errs.NewValidationError(errs.SubtypeFailedPrecondition, ...).WithHint(...).

Proposed fix
-				return errs.NewAuthenticationError(errs.SubtypeUnknown, "no unexpired pending authorization to resume").
+				return errs.NewValidationError(errs.SubtypeFailedPrecondition, "no unexpired pending authorization to resume").
 					WithHint("start a fresh split flow with `lark-cli auth login --scope <scope> --no-wait --json`, send its verification URL to the user, then run `lark-cli auth login --resume` only after the user confirms authorization")
📝 Committable suggestion

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

Suggested change
if opts.Resume {
if opts.DeviceCode != "" || opts.NoWait || opts.Scope != "" || opts.Recommend || len(opts.Domains) > 0 || len(opts.Exclude) > 0 {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--resume cannot be combined with authorization request options or --device-code").WithParam("--resume")
}
pending, err := loadPendingLogin(config.AppID)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return errs.NewAuthenticationError(errs.SubtypeUnknown, "no unexpired pending authorization to resume").
WithHint("start a fresh split flow with `lark-cli auth login --scope <scope> --no-wait --json`, send its verification URL to the user, then run `lark-cli auth login --resume` only after the user confirms authorization")
}
return errs.NewInternalError(errs.SubtypeStorage, "failed to load pending authorization: %v", err).WithCause(err)
if opts.Resume {
if opts.DeviceCode != "" || opts.NoWait || opts.Scope != "" || opts.Recommend || len(opts.Domains) > 0 || len(opts.Exclude) > 0 {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--resume cannot be combined with authorization request options or --device-code").WithParam("--resume")
}
pending, err := loadPendingLogin(config.AppID)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return errs.NewValidationError(errs.SubtypeFailedPrecondition, "no unexpired pending authorization to resume").
WithHint("start a fresh split flow with `lark-cli auth login --scope <scope> --no-wait --json`, send its verification URL to the user, then run `lark-cli auth login --resume` only after the user confirms authorization")
}
return errs.NewInternalError(errs.SubtypeStorage, "failed to load pending authorization: %v", err).WithCause(err)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/auth/login.go` around lines 139 - 149, Update the missing-pending-state
branch in the opts.Resume flow to return
errs.NewValidationError(errs.SubtypeFailedPrecondition, ...) instead of an
authentication/unknown error, preserving the existing recovery hint. Add
metadata assertions using errs.ProblemOf to verify the returned problem has the
expected validation category and failed-precondition subtype.

Source: Coding guidelines

}
opts.DeviceCode = pending.DeviceCode
}

log := func(format string, a ...interface{}) {
if !opts.JSON {
fmt.Fprintf(f.IOStreams.ErrOut, format+"\n", a...)
Expand Down Expand Up @@ -200,7 +219,7 @@ func authLoginRun(opts *LoginOptions) error {
log("View all options:")
log(msg.HintFooter)
log("")
log("Note: this command blocks until authorization is complete. For non-streaming agent harnesses, use --no-wait --json, send the verification URL as the final message of the turn, then run --device-code in a later step after the user confirms authorization.")
log("Note: this command blocks until authorization is complete. For non-streaming agent harnesses, use --no-wait --json, send the verification URL as the final message of the turn, then run --resume in a later step after the user confirms authorization.")
return errs.NewValidationError(errs.SubtypeInvalidArgument, "please specify the scopes to authorize").WithParam("--scope")
}
}
Expand Down Expand Up @@ -272,7 +291,7 @@ func authLoginRun(opts *LoginOptions) error {

// --no-wait: return immediately with device code and URL
if opts.NoWait {
if err := saveLoginRequestedScope(authResp.DeviceCode, finalScope); err != nil {
if err := savePendingLogin(authResp.DeviceCode, config.AppID, finalScope, authResp.ExpiresIn); err != nil {
fmt.Fprintf(f.IOStreams.ErrOut, "[lark-cli] [WARN] auth login: failed to cache requested scopes: %v\n", err)
}
Comment on lines +294 to 296

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Fail --no-wait when resumable state cannot be persisted.

The command currently returns success and instructs the agent to use --resume, even though that follow-up is guaranteed to fail. Return a typed storage error with the underlying cause, and add an error-path metadata/cause test.

Proposed fix
 	if opts.NoWait {
 		if err := savePendingLogin(authResp.DeviceCode, config.AppID, finalScope, authResp.ExpiresIn); err != nil {
-			fmt.Fprintf(f.IOStreams.ErrOut, "[lark-cli] [WARN] auth login: failed to cache requested scopes: %v\n", err)
+			return errs.NewInternalError(errs.SubtypeStorage,
+				"failed to save pending authorization: %v", err).WithCause(err)
 		}
📝 Committable suggestion

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

Suggested change
if err := savePendingLogin(authResp.DeviceCode, config.AppID, finalScope, authResp.ExpiresIn); err != nil {
fmt.Fprintf(f.IOStreams.ErrOut, "[lark-cli] [WARN] auth login: failed to cache requested scopes: %v\n", err)
}
if err := savePendingLogin(authResp.DeviceCode, config.AppID, finalScope, authResp.ExpiresIn); err != nil {
return errs.NewInternalError(errs.SubtypeStorage,
"failed to save pending authorization: %v", err).WithCause(err)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/auth/login.go` around lines 294 - 296, Update the savePendingLogin error
path in the login command so --no-wait returns a typed storage error that
preserves the underlying cause instead of logging a warning and continuing; keep
existing behavior for other modes. Add an error-path test verifying the returned
error type and wrapped cause metadata.

Source: Coding guidelines

data := map[string]interface{}{
Expand All @@ -283,9 +302,9 @@ func authLoginRun(opts *LoginOptions) error {
"**CRITICAL: You MUST include the QR image in your response.** Generating the file alone is NOT enough—use image tags, inline images, or file attachments to display it." +
"**Display order:** Output the URL first, then place the QR code image below the URL." +
"**URL Output Rules:** Treat verification_url as an opaque string that cannot be modified. Do NOT URL-encode/decode or add spaces/punctuation." +
"For agent harnesses that only deliver final turn messages, make the QR code image (or URL) the final message of the turn and return control to the user; do not block on --device-code in the same turn. **Before ending the turn, tell the user to come back and notify you after completing authorization.**" +
"**After the user confirms authorization:** YOU must execute `lark-cli auth login --device-code <device_code>` yourself." +
"**Do NOT cache verification_url or device_code for future use.** Always run `lark-cli auth login --no-wait --json` fresh when authorization is needed.",
"For agent harnesses that only deliver final turn messages, make the QR code image (or URL) the final message of the turn and return control to the user; do not run --resume in the same turn. **Before ending the turn, tell the user to come back and notify you after completing authorization.**" +
"**After the user confirms authorization:** YOU must execute `lark-cli auth login --resume` yourself. The CLI keeps the pending device code locally; do not copy it into a task comment or ask the user to run a command." +
"If --resume reports that the pending authorization expired, start a fresh flow with `lark-cli auth login --no-wait --json` and show the new URL.",
}
encoder := json.NewEncoder(f.IOStreams.Out)
encoder.SetEscapeHTML(false)
Expand Down Expand Up @@ -403,7 +422,7 @@ func authLoginPollDeviceCode(opts *LoginOptions, config *core.CliConfig, msg *lo
fmt.Fprintf(f.IOStreams.ErrOut, "[lark-cli] [WARN] auth login: failed to load cached requested scopes: %v\n", err)
}
cleanupRequestedScope := func() {
if err := removeLoginRequestedScope(opts.DeviceCode); err != nil {
if err := removePendingLogin(opts.DeviceCode, config.AppID); err != nil {
fmt.Fprintf(f.IOStreams.ErrOut, "[lark-cli] [WARN] auth login: failed to remove cached requested scopes: %v\n", err)
}
}
Expand Down
4 changes: 2 additions & 2 deletions cmd/auth/login_messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ var loginMsgZh = &loginMsg{

OpenURL: "在浏览器中打开以下链接进行认证:\n\n",
WaitingAuth: "等待用户授权...",
AgentTimeoutHint: "[AI agent] 此命令最长阻塞约 10 分钟,等待用户在浏览器内完成授权。请确保 runner 的 timeout >= 600s。若你的 harness 或 agent 工具只会把最终回复发给用户,请改用 \"lark-cli auth login --no-wait --json\" 拿到 device_code 和 verification_url,把 verification_url 作为本轮最终消息发给用户并结束本轮;等用户回复已完成授权后,再在后续步骤运行 \"lark-cli auth login --device-code <code>\" 续上轮询。**不要在同一轮里展示 URL 后立刻阻塞执行 --device-code**,也不要短 timeout 反复重试;每次重启会作废上一轮的 device code,导致用户授权链接失效。**必须生成二维码并展示**: 你必须调用 lark-cli auth qrcode 将 verification_url 转为二维码,这是必须步骤,不要跳过。优先生成 PNG 二维码(--output);仅当用户明确要求时才使用 ASCII(--ascii)。**关键:生成后必须在回复中展示图片**,仅生成文件不算完成,须用 image 标签或内联图片等方式将图片包含在回复中。**展示顺序**: 先输出 URL,再将二维码图片置于 URL 下方完整展示。**URL 输出规则**: 将 verification_url 视为不可修改的 opaque string,不要做任何修改(包括 URL 编码/解码、添加空格或标点)。",
AgentTimeoutHint: "[AI agent] 此命令最长阻塞约 10 分钟,等待用户在浏览器内完成授权。请确保 runner 的 timeout >= 600s。若你的 harness 或 agent 工具只会把最终回复发给用户,请改用 \"lark-cli auth login --no-wait --json\" 拿到 verification_url,把 verification_url 作为本轮最终消息发给用户并结束本轮;等用户回复已完成授权后,再在后续步骤运行 \"lark-cli auth login --resume\" 续上轮询。CLI 会在本机保存 pending device code,不要把它写入回复或任务评论。**不要在同一轮里展示 URL 后立刻阻塞执行 --resume**,也不要短 timeout 反复重试;如果 pending 授权过期,再发起一条新的链接。**必须生成二维码并展示**: 你必须调用 lark-cli auth qrcode 将 verification_url 转为二维码,这是必须步骤,不要跳过。优先生成 PNG 二维码(--output);仅当用户明确要求时才使用 ASCII(--ascii)。**关键:生成后必须在回复中展示图片**,仅生成文件不算完成,须用 image 标签或内联图片等方式将图片包含在回复中。**展示顺序**: 先输出 URL,再将二维码图片置于 URL 下方完整展示。**URL 输出规则**: 将 verification_url 视为不可修改的 opaque string,不要做任何修改(包括 URL 编码/解码、添加空格或标点)。",
AuthSuccess: "已收到授权确认,正在获取用户信息并校验授权结果...",
LoginSuccess: "授权成功! 用户: %s (%s)",
AuthorizedUser: "当前授权账号: %s (%s)",
Expand Down Expand Up @@ -97,7 +97,7 @@ var loginMsgEn = &loginMsg{

OpenURL: "Open this URL in your browser to authenticate:\n\n",
WaitingAuth: "Waiting for user authorization...",
AgentTimeoutHint: "[AI agent] This command blocks for up to ~10 minutes while waiting for the user to authorize in their browser. Make sure your runner's timeout is >= 600s. If your harness or agent tool only delivers final turn messages, use \"lark-cli auth login --no-wait --json\" to get device_code and verification_url, present verification_url to the user exactly as the final message of this turn, then end the turn; after the user replies that they authorized, run \"lark-cli auth login --device-code <code>\" in a later step to resume polling. **Do NOT show the URL and then immediately block on --device-code in the same turn**, and do not retry with a short timeout; each restart invalidates the previous device code and makes the earlier authorization URL useless.**MUST generate QR code AND display it:** You MUST call lark-cli auth qrcode to convert verification_url into a QR code. This is a required step, do NOT skip it. Prefer PNG QR code (--output); use ASCII (--ascii) only when the user explicitly requests it.**CRITICAL: You MUST include the QR image in your response.** Generating the file alone is NOT enough—use image tags, inline images, or file attachments to display it.**Display order:** Output the URL first, then place the QR code image below the URL.**URL Output Rules:** Treat verification_url as an opaque string that cannot be modified. Do NOT URL-encode/decode or add spaces/punctuation.",
AgentTimeoutHint: "[AI agent] This command blocks for up to ~10 minutes while waiting for the user to authorize in their browser. Make sure your runner's timeout is >= 600s. If your harness or agent tool only delivers final turn messages, use \"lark-cli auth login --no-wait --json\" to get verification_url, present it to the user exactly as the final message of this turn, then end the turn; after the user replies that they authorized, run \"lark-cli auth login --resume\" in a later step to resume polling. The CLI stores the pending device code locally; do not put it in a response or task comment. **Do NOT show the URL and then immediately block on --resume in the same turn**, and do not retry with a short timeout; start a fresh flow only if the pending authorization expires.**MUST generate QR code AND display it:** You MUST call lark-cli auth qrcode to convert verification_url into a QR code. This is a required step, do NOT skip it. Prefer PNG QR code (--output); use ASCII (--ascii) only when the user explicitly requests it.**CRITICAL: You MUST include the QR image in your response.** Generating the file alone is NOT enough—use image tags, inline images, or file attachments to display it.**Display order:** Output the URL first, then place the QR code image below the URL.**URL Output Rules:** Treat verification_url as an opaque string that cannot be modified. Do NOT URL-encode/decode or add spaces/punctuation.",
AuthSuccess: "Authorization confirmed, fetching user info and validating granted scopes...",
LoginSuccess: "Authorization successful! User: %s (%s)",
AuthorizedUser: "Authorized account: %s (%s)",
Expand Down
4 changes: 2 additions & 2 deletions cmd/auth/login_messages_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,12 @@ func TestLoginMsg_FormatStrings(t *testing.T) {
// TestAgentTimeoutHint_CarriesKeyInfo guards the contract that the synchronous
// auth-login output tells AI agents three things: (a) this command blocks for
// minutes — set a long runner timeout, (b) the alternative is the --no-wait +
// --device-code split-flow, and (c) non-streaming harnesses must end the turn
// --resume split-flow, and (c) non-streaming harnesses must end the turn
// after presenting the URL instead of blocking in the same turn.
func TestAgentTimeoutHint_CarriesKeyInfo(t *testing.T) {
for _, lang := range []i18n.Lang{i18n.LangZhCN, i18n.LangEnUS} {
hint := getLoginMsg(lang).AgentTimeoutHint
for _, want := range []string{"--no-wait", "--device-code", "turn"} {
for _, want := range []string{"--no-wait", "--resume", "turn"} {
if lang == i18n.LangZhCN && want == "turn" {
want = "本轮"
}
Expand Down
65 changes: 65 additions & 0 deletions cmd/auth/login_scope_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"os"
"path/filepath"
"regexp"
"time"

larkauth "github.com/larksuite/cli/internal/auth"
"github.com/larksuite/cli/internal/core"
Expand All @@ -22,6 +23,13 @@ type loginScopeCacheRecord struct {
RequestedScope string `json:"requested_scope"`
}

type pendingLoginRecord struct {
DeviceCode string `json:"device_code"`
AppID string `json:"app_id"`
RequestedScope string `json:"requested_scope"`
ExpiresAt int64 `json:"expires_at"`
}

// loginScopeCacheDir returns the directory used to persist auth login --no-wait
// requested scopes keyed by device_code.
func loginScopeCacheDir() string {
Expand All @@ -33,6 +41,10 @@ func loginScopeCachePath(deviceCode string) string {
return filepath.Join(loginScopeCacheDir(), sanitizeLoginScopeCacheKey(deviceCode)+".json")
}

func pendingLoginPath(appID string) string {
return filepath.Join(loginScopeCacheDir(), "latest-"+sanitizeLoginScopeCacheKey(appID)+".json")
}

// sanitizeLoginScopeCacheKey converts a device_code into a safe filename token.
func sanitizeLoginScopeCacheKey(deviceCode string) string {
sanitized := loginScopeCacheSafeChars.ReplaceAllString(deviceCode, "_")
Expand All @@ -54,6 +66,45 @@ func saveLoginRequestedScope(deviceCode, requestedScope string) error {
return validate.AtomicWrite(loginScopeCachePath(deviceCode), data, 0600)
}

// savePendingLogin persists the latest split-flow authorization so a later
// agent turn can resume it without copying a device code through the model's
// conversation context. The per-device scope record remains for backwards
// compatibility with the explicit --device-code flow.
func savePendingLogin(deviceCode, appID, requestedScope string, expiresIn int) error {
if err := saveLoginRequestedScope(deviceCode, requestedScope); err != nil {
return err
}
record := pendingLoginRecord{
DeviceCode: deviceCode,
AppID: appID,
RequestedScope: requestedScope,
ExpiresAt: time.Now().Add(time.Duration(expiresIn) * time.Second).Unix(),
}
data, err := json.Marshal(record)
if err != nil {
return err
}
return validate.AtomicWrite(pendingLoginPath(appID), data, 0600)
}

func loadPendingLogin(appID string) (*pendingLoginRecord, error) {
path := pendingLoginPath(appID)
data, err := vfs.ReadFile(path)
if err != nil {
return nil, err
}
var record pendingLoginRecord
if err := json.Unmarshal(data, &record); err != nil {
_ = vfs.Remove(path)
return nil, err
}
if record.DeviceCode == "" || record.AppID != appID || record.ExpiresAt <= time.Now().Unix() {
_ = vfs.Remove(path)
return nil, os.ErrNotExist
Comment on lines +101 to +103

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Remove the expired flow’s per-device scope record too.

Expiration deletes only latest-<app>.json, leaving the device-scoped cache indefinitely. Clean both records and extend the expiration test to assert both files disappear.

Proposed fix
-	if record.DeviceCode == "" || record.AppID != appID || record.ExpiresAt <= time.Now().Unix() {
+	if record.DeviceCode == "" || record.AppID != appID {
 		_ = vfs.Remove(path)
 		return nil, os.ErrNotExist
 	}
+	if record.ExpiresAt <= time.Now().Unix() {
+		_ = removeLoginRequestedScope(record.DeviceCode)
+		_ = vfs.Remove(path)
+		return nil, os.ErrNotExist
+	}
📝 Committable suggestion

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

Suggested change
if record.DeviceCode == "" || record.AppID != appID || record.ExpiresAt <= time.Now().Unix() {
_ = vfs.Remove(path)
return nil, os.ErrNotExist
if record.DeviceCode == "" || record.AppID != appID {
_ = vfs.Remove(path)
return nil, os.ErrNotExist
}
if record.ExpiresAt <= time.Now().Unix() {
_ = removeLoginRequestedScope(record.DeviceCode)
_ = vfs.Remove(path)
return nil, os.ErrNotExist
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/auth/login_scope_cache.go` around lines 101 - 103, Update the expiration
branch in the scope-cache lookup to remove both the latest app record and the
corresponding device-scoped record before returning os.ErrNotExist. Extend the
expiration test to verify that both cache files are deleted.

}
return &record, nil
}

// loadLoginRequestedScope loads the cached requested scope string for a device_code.
// It returns an empty string if no cache entry exists.
func loadLoginRequestedScope(deviceCode string) (string, error) {
Expand Down Expand Up @@ -81,6 +132,20 @@ func removeLoginRequestedScope(deviceCode string) error {
return err
}

func removePendingLogin(deviceCode, appID string) error {
firstErr := removeLoginRequestedScope(deviceCode)
path := pendingLoginPath(appID)
if data, err := vfs.ReadFile(path); err == nil {
var record pendingLoginRecord
if json.Unmarshal(data, &record) == nil && record.DeviceCode == deviceCode {
if err := vfs.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) && firstErr == nil {
firstErr = err
}
}
}
return firstErr
Comment on lines +135 to +146

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Do not silently ignore pending-file read failures.

If vfs.ReadFile fails for reasons other than absence, cleanup returns success and leaves stale resumable state behind. Preserve that error so the command can warn the caller.

Proposed fix
 	path := pendingLoginPath(appID)
-	if data, err := vfs.ReadFile(path); err == nil {
+	data, err := vfs.ReadFile(path)
+	if err != nil {
+		if !errors.Is(err, os.ErrNotExist) && firstErr == nil {
+			firstErr = err
+		}
+		return firstErr
+	}
+	{
 		var record pendingLoginRecord
 		if json.Unmarshal(data, &record) == nil && record.DeviceCode == deviceCode {
📝 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 removePendingLogin(deviceCode, appID string) error {
firstErr := removeLoginRequestedScope(deviceCode)
path := pendingLoginPath(appID)
if data, err := vfs.ReadFile(path); err == nil {
var record pendingLoginRecord
if json.Unmarshal(data, &record) == nil && record.DeviceCode == deviceCode {
if err := vfs.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) && firstErr == nil {
firstErr = err
}
}
}
return firstErr
func removePendingLogin(deviceCode, appID string) error {
firstErr := removeLoginRequestedScope(deviceCode)
path := pendingLoginPath(appID)
data, err := vfs.ReadFile(path)
if err != nil {
if !errors.Is(err, os.ErrNotExist) && firstErr == nil {
firstErr = err
}
return firstErr
}
{
var record pendingLoginRecord
if json.Unmarshal(data, &record) == nil && record.DeviceCode == deviceCode {
if err := vfs.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) && firstErr == nil {
firstErr = err
}
}
}
return firstErr
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/auth/login_scope_cache.go` around lines 135 - 146, Update
removePendingLogin to preserve vfs.ReadFile errors when the pending-login file
exists but cannot be read, while treating os.ErrNotExist as benign. Propagate
the read error through firstErr when no earlier cleanup error has been recorded,
and keep the existing record-matching removal behavior unchanged.

}

// shouldRemoveLoginRequestedScope indicates whether the requested-scope cache
// should be removed after polling finishes.
func shouldRemoveLoginRequestedScope(result *larkauth.DeviceFlowResult) bool {
Expand Down
Loading
Loading