Skip to content

fix(auth,vc): reject unusable stored tokens, allow bot auth where endpoints support it - #2190

Closed
sang-neo03 wants to merge 12 commits into
mainfrom
feat/auth-unattended-reliability
Closed

fix(auth,vc): reject unusable stored tokens, allow bot auth where endpoints support it#2190
sang-neo03 wants to merge 12 commits into
mainfrom
feat/auth-unattended-reliability

Conversation

@sang-neo03

@sang-neo03 sang-neo03 commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

Two correctness problems in how the CLI treats stored credentials and identity. auth status and auth check report a stored token as usable when it carries no access token at all or has fully expired, so an unusable credential looks healthy. Separately, three shortcuts reject --as bot in a local pre-flight check even though the endpoints they call accept an app token, which makes callers conclude the platform cannot do something it can.

The non-TTY auth login default that this branch originally carried has been reverted and will be proposed separately.

Changes

  • TokenStatus reports a new corrupted state when a stored record parses but carries no usable access token. encoding/json drops unknown fields silently, so a writer that misspells the field name leaves healthy timestamps, a full scope list, and no token — previously reported as valid.
  • Freshness values are now exported constants. The bare == "expired" / == "valid" string literals were how consumers came to treat an unknown status as usable.
  • All token consumers handle the new state: auth status reports it, GetValidAccessToken fails with a named cause instead of returning an empty bearer token, --as auto falls back to bot, and auth check no longer answers "granted" from the scope list of an unusable record. auth check also rejects a fully expired token, which it previously reported as ok: true.
  • auth status keeps status: "missing" rather than adding a new top-level enum value; the detail rides in tokenStatus: "corrupted". Note that auth list and profile list expose the raw status, so corrupted is visible there.
  • docs +search and base +title-resolve accept --as bot. Both call POST /open-apis/search/v2/doc_wiki/search, and drive +search already declared both identities for that same endpoint — the CLI gave two different answers for one API.
  • vc +detail accepts --as bot. The raw vc meeting get command has always declared both identities for GET /open-apis/vc/v1/meetings/{meeting_id}, and the documented app-identity flow in the lark-vc-agent skill (join a meeting, then read note_id / minute_token) could not complete because the local gate rejected the call before it was sent.
  • A bot querying a meeting it never joined now gets the real reason. The upstream 121005 message reads "user lacks permission" whatever identity called, which points an app-identity caller at auth login even though only joining the meeting can help. Rewritten for that one combination, following the existing minutesReadError shape in the same package; a user caller and every other error code keep the upstream wording.
  • The lark-vc-agent skill carries the calling identity through all its examples. meeting.get only returns meetings the calling identity joined, so six copy-pasteable commands that omitted --as would silently fall back to the default identity.
  • Identity declarations are pinned by tests. The existing execution tests for these shortcuts only ever pass --as user, so any declaration could previously be reverted to user-only with the package tests still green.

Scope

vc +notes, vc +recording, minutes +detail and three mail shortcuts were evaluated and deliberately left at user:

  • vc +notes and the --calendar-event-ids branch of vc +recording call endpoints that reject a tenant token outright (99991663).
  • minutes +detail depends on an endpoint that returns a business error under a tenant token. Telling "the endpoint does not serve app identities" apart from "this app has no claim to this particular resource" would require a minute owned by the app, which an app identity cannot create. Left unchanged rather than guessed at.
  • The three mail shortcuts default --mailbox to me, which has no meaning for an app identity.

vc +detail's declared scopes remain the user-side ones, so identity-aware scope hints for the bot path are not accurate yet. Splitting them needs the app-side scope requirement confirmed first, so it is left out of this change.

Test Plan

  • Unit tests pass — go test -race and go test -gcflags="all=-N -l" each run clean across all packages. make unit-test combines both flags, which segfaults inside runtime.main on go1.26.0 darwin/arm64 before any test executes; the same crash reproduces on a clean main, so it is a local toolchain issue (CI runs linux/amd64).
  • go vet ./..., gofmt -l . and go mod tidy clean. origin/main merged in; full suite green afterwards.
  • Contract tests verified to fail when the implementation is reverted — checked individually: neutering the empty-token check reddens 6 tests across internal/auth and internal/identitydiag; dropping the expired branch in auth check reddens its subtest; reverting the three AuthTypes declarations plus the 121005 rewrite reddens 4 assertions, each printing actual-vs-want; removing --as from one skill reference file reddens the skill check with the exact file and line.
  • Bot identity verified against the live API, not just the local gate: POST /open-apis/search/v2/doc_wiki/search --as bot returns results with the search:docs:read app scope granted, and docs +search / base +title-resolve now reach it. Before the grant the same call returned 99991672 app_scope_not_applied — a missing grant, which is not evidence either way about the token type.
  • vc +detail verified end to end with a build of this branch: after joining a meeting via vc +meeting-join --as bot, both api GET /open-apis/vc/v1/meetings/{id} --as bot and vc +detail --as bot return the meeting, and access persists after leaving. Against a meeting the app never joined, the rewritten message appears instead of the upstream "user lacks permission" wording.
  • The shortcuts left at user were each probed directly rather than inferred from metadata; the observed codes are quoted in the Scope section above.
  • auth status --json on a valid credential still reports ready / available: true / valid — no false positives. The corruption case is covered by a test that writes a token file with a misspelled field name and asserts status: missing plus tokenStatus: corrupted.

Related Issues

  • None

Summary by CodeRabbit

  • New Features
    • Added flexible authentication login behavior with --wait and --no-wait.
    • Non-interactive sessions return device codes immediately by default, with clear blocking authorization guidance.
    • Documentation, search, title resolution, and meeting features now support user or bot authentication.
    • Meeting detail errors for bots now provide clearer guidance when participation is required.
  • Bug Fixes
    • Corrupted, empty, and expired tokens are detected and reported accurately.
    • Authentication checks and diagnostics now provide structured errors and recovery guidance.
    • Invalid tokens no longer appear valid or trigger unnecessary refresh attempts.

TokenStatus judged freshness from the expiry timestamps alone, so a stored
record whose accessToken was empty came back "valid". encoding/json drops
unknown fields silently, which means a writer that misspells the field name
(the reported case used "userAccessToken" instead of "accessToken") produces
exactly that record: healthy-looking timestamps, complete scope list, no
usable token.

Every consumer then drew the wrong conclusion from it:

- auth status reported ready / available / valid, contradicting the business
  commands that kept failing
- GetValidAccessToken returned the empty string as a bearer token, so calls
  failed at the server instead of locally with a cause
- --as auto resolved to the user identity, because only "expired" fell back
  to bot
- auth check answered "granted" from the scope list of an unusable record

TokenStatus now checks the access token before the timestamps and reports
the new "corrupted" status, and all four consumers handle it. auth status
keeps reporting status "missing" rather than a new enum value: consumers
already branch on missing and its recovery action (re-authorize) is the
right one here, so the distinguishing detail rides in tokenStatus and the
message instead of breaking the output contract.

A corrupted record is not deleted. The refresh token next to an empty access
token is equally suspect, and keeping the file lets whoever wrote it see what
landed on disk — that diagnosis took a long time precisely because nothing
named the field.
… not a terminal

auth login always polled until the device code expired. The command already
detected a non-terminal environment, but only used that to print a longer
hint — the hint itself said "this command blocks for up to 10 minutes, set
your runner timeout to 600s". In a sandbox nobody opens the verification URL,
so those ten minutes always elapse and the caller's whole turn is spent
waiting for a result that cannot arrive.

The default now follows stdout: unattended runs (piped output, agent sandbox,
CI) return the device code and verification URL immediately, and callers
decide when to poll with --device-code. An interactive terminal still blocks,
since someone is there to scan.

Both preferences stay expressible. --wait keeps the poll on a pipe, --no-wait
forces the non-blocking path on a terminal, and an explicit --no-wait=false is
read as a request to block rather than as an unset flag the terminal check may
override. Passing --wait and --no-wait together is a caller bug, so it fails
with a typed validation error before any device code is issued rather than
resolving to one of them silently.

The switch is announced on stderr when neither flag was passed, because a
caller that asked for neither mode must be able to tell why it received a
device code instead of a completed login, and how to get blocking back.
@sang-neo03
sang-neo03 requested a review from liangshuo-1 as a code owner August 5, 2026 04:27
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

auth login now resolves blocking behavior from explicit flags and terminal output. Stored corrupted tokens receive typed errors and consistent diagnostics. Several shortcuts now support bot authentication, including meeting-detail error guidance.

Changes

Terminal-aware login waiting

Layer / File(s) Summary
Login wait resolution and device-code behavior
cmd/auth/login.go, README.md, README.zh.md, cmd/config/bind_messages.go
auth login adds --wait, derives behavior from flags and terminal output, rejects conflicts, and documents the behavior.
Login wait-mode validation
cmd/auth/login_test.go, cmd/auth/auth_test.go, cmd/config/bind_warning_test.go
Tests cover defaults, wait modes, conflicts, device-code output, polling, command wiring, help text, and bind guidance.

Corrupted stored-token handling

Layer / File(s) Summary
Token status and retrieval contract
internal/auth/token_store.go, internal/auth/errors.go, internal/auth/uat_client.go, internal/auth/token_corrupted_test.go
Blank access-token records receive a corrupted status. Retrieval returns a typed error and preserves the stored record.
Corrupted-token command and identity handling
cmd/auth/check.go, cmd/auth/check_test.go, internal/credential/*, internal/identitydiag/*
Scope checks reject unusable tokens. Identity selection falls back to bot. Diagnostics report missing status and re-login guidance.

Shortcut authentication types

Layer / File(s) Summary
Shortcut authentication configuration and meeting-detail handling
shortcuts/base/base_resolve.go, shortcuts/doc/docs_search.go, shortcuts/vc/vc_detail.go, shortcuts/*/*_test.go, skills/lark-vc-agent/SKILL.md
Base title resolution, document search, and meeting-detail retrieval accept user and bot authentication. Meeting-detail errors rewrite bot permission code 121005, and the VC workflow preserves the selected identity.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant AuthLogin
  participant DeviceAuthorization
  participant TokenPolling
  User->>AuthLogin: Select --wait, --no-wait, or default
  AuthLogin->>AuthLogin: Resolve wait mode from flags and terminal output
  AuthLogin->>DeviceAuthorization: Request device authorization
  AuthLogin->>TokenPolling: Poll when blocking
  AuthLogin-->>User: Return device-code data or authorization result
Loading

Possibly related PRs

Suggested labels: bug

Suggested reviewers: liangshuo-1

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.37% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main token-handling and bot-authentication changes in the pull request.
Description check ✅ Passed The description includes all required template sections and provides detailed scope, changes, testing, and issue information.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/auth-unattended-reliability

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions github-actions Bot added the size/L Large or sensitive change across domains or core paths label Aug 5, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

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

Inline comments:
In `@cmd/auth/check.go`:
- Around line 83-89: Update the corrupted-token branch in the authentication
check command to return the appropriate typed errs authentication error with
token-invalid metadata instead of output.ErrBare(1). Preserve the JSON predicate
result on stdout, and update the command contract and tests to verify the typed
error envelope is emitted on stderr.

In `@cmd/auth/login_test.go`:
- Around line 1252-1261: Update the error assertions in the relevant login test
to obtain the shared problem metadata with errs.ProblemOf(err), asserting its
Category and Subtype values. Retain errors.As only for extracting the concrete
errs.ValidationError and validating its Param value, removing the direct Subtype
assertion on verr.

In `@cmd/auth/login.go`:
- Around line 67-71: Update the option-resolution logic around the no-wait
handling in the login command to detect when the “wait” flag was explicitly
provided, including an explicit false value, and force the resolved path to wait
unless --no-wait is explicitly enabled. Add a command test covering --wait=false
with terminal stdout and verify it resolves to the waiting behavior.

In `@internal/auth/token_corrupted_test.go`:
- Around line 151-163: Extend the assertions in the token-corruption test after
errs.ProblemOf(err) succeeds to verify problem.Category is
errs.CategoryAuthentication, problem.Subtype is errs.SubtypeTokenInvalid, and
IsNeedUserAuthorizationError(err) returns true. Keep the existing
message-fragment and JSON-marshalling checks unchanged, and do not add a Param
assertion because errs.Problem does not expose that field.
🪄 Autofix

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 Plus

Run ID: 021771ef-c675-417b-b8d8-41fa883e6f6d

📥 Commits

Reviewing files that changed from the base of the PR and between ebdeda8 and 5777019.

📒 Files selected for processing (13)
  • cmd/auth/auth_test.go
  • cmd/auth/check.go
  • cmd/auth/check_test.go
  • cmd/auth/login.go
  • cmd/auth/login_test.go
  • internal/auth/errors.go
  • internal/auth/token_corrupted_test.go
  • internal/auth/token_store.go
  • internal/auth/uat_client.go
  • internal/credential/credential_provider.go
  • internal/credential/credential_provider_test.go
  • internal/identitydiag/diagnostics.go
  • internal/identitydiag/diagnostics_test.go

Comment thread cmd/auth/check.go Outdated
Comment on lines +83 to +89
// The scope list of a corrupted record still looks complete, so reporting
// scopes here would answer "granted" for a credential that cannot make a
// single call. Fail with the cause instead.
if larkauth.TokenStatus(stored) == larkauth.TokenStatusCorrupted {
output.PrintJson(f.IOStreams.Out, map[string]interface{}{"ok": false, "error": "corrupted_token", "missing": required})
return output.ErrBare(1)
}

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

Return a typed error for the corrupted-token failure.

Line 88 returns output.ErrBare(1). This bypasses the required typed command-error contract. Return an errs authentication error with token-invalid metadata. Update the command contract and tests so stdout carries the predicate result and stderr carries the typed error envelope.

As per coding guidelines, command-facing failures must use typed errs.* errors and must not use legacy output.Err* helpers.

🤖 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/check.go` around lines 83 - 89, Update the corrupted-token branch in
the authentication check command to return the appropriate typed errs
authentication error with token-invalid metadata instead of output.ErrBare(1).
Preserve the JSON predicate result on stdout, and update the command contract
and tests to verify the typed error envelope is emitted on stderr.

Source: Coding guidelines

Comment thread cmd/auth/login_test.go Outdated
Comment thread cmd/auth/login.go Outdated
Comment thread internal/auth/token_corrupted_test.go
@codecov

codecov Bot commented Aug 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.13043% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.02%. Comparing base (875d20a) to head (c338894).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
internal/auth/uat_client.go 40.00% 2 Missing and 1 partial ⚠️
internal/auth/token_store.go 60.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2190      +/-   ##
==========================================
+ Coverage   76.00%   76.02%   +0.02%     
==========================================
  Files         966      966              
  Lines      102541   102577      +36     
==========================================
+ Hits        77933    77989      +56     
+ Misses      18704    18680      -24     
- Partials     5904     5908       +4     

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

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

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

🚀 PR Preview Install Guide

🧰 CLI update

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

🧩 Skill update

npx skills add larksuite/cli#feat/auth-unattended-reliability -y -g

…ed wait flags

Review follow-ups on the unattended-auth changes.

auth check rejected a corrupted token but still answered "granted" for an
expired one. Both statuses leave the scope list intact and both already make
`--as auto` fall back to bot, so reporting scopes from either contradicts the
rest of the tree. The rejection now covers expired as well, reported as
"expired_token" to keep the cause diagnosable. needs_refresh stays accepted:
that token serves calls after the automatic refresh on next use, so its scopes
are the ones the caller will actually have.

auth login only inspected --no-wait's explicit state, so --wait=false was
indistinguishable from an absent --wait and the terminal default kept blocking
against a stated preference. Both flags now fold into one waitIntent resolved
from cobra's Changed() state, where an explicitly negated flag means the
opposite of its name. Setting both is a conflict regardless of their values.
LoginOptions.effectiveWaitIntent keeps the booleans working for callers that
construct the struct directly instead of going through flag parsing, so a set
field never silently does nothing.

The no-scope hint claimed the command blocks and told callers to pass
--no-wait --json. It now reports what will actually happen, branching on stdout
because that path is also reached on a terminal with --json.

Error-path assertions now check category and reach the preserved cause via
errors.As rather than only the subtype and the helper predicate.
config bind --identity user-default hands the Agent a one-call login flow and
promises the command "returns automatically once authorization completes". That
promise no longer holds by itself: Agent runtimes capture stdout, and auth login
now returns the device code and exits 0 there instead of polling. An Agent
following the old wording would read that exit as a completed login and continue
with the user's request while the user has authorized nothing.

The instruction now spells out --wait and why it cannot be dropped, in both
languages, and the README agent steps match. The bind message test pins the flag
so removing it fails rather than silently restoring the mismatch.

Keeping the blocking single call (rather than teaching the two-phase device-code
flow here) suits the runtimes this message targets: OpenClaw and Hermes relay
stderr to the user in real time, so the URL reaches them while the command waits.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

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

Inline comments:
In `@cmd/auth/check_test.go`:
- Around line 249-250: Extend the TokenStatus test table in
cmd/auth/check_test.go with a whitespace-only AccessToken fixture such as "
\t\n", and assert it returns "corrupted_token", alongside the existing
empty-token corrupted case.
- Around line 281-298: Extend the error assertions in the authCheckRun test
using errs.ProblemOf to verify the typed problem category and subtype, while
retaining the existing exit-code and JSON contract checks. Where the status
error wraps a lower-layer cause, assert cause preservation; use errors.As to
inspect Param only when the error is a *errs.ValidationError, since
errs.ProblemOf exposes only problem-level fields.
- Around line 289-301: Update the response parsing in the affected auth check
tests, especially TestAuthCheckRun_NeedsRefreshTokenStillReportsScopes, to
unmarshal into a typed auth-check response projection instead of
map[string]interface{}. Assert that granted contains exactly the im:message
scope, with no missing scope, while retaining the existing ok and error
assertions. Apply the same typed projection to the additional affected assertion
block.

In `@cmd/auth/login.go`:
- Around line 316-323: Update the note-selection branch in the login flow to use
the resolved noWait value instead of stdoutIsTerminal. Revise both messages to
describe the actual blocking or immediate-return behavior determined by noWait,
without assuming anything about the output stream.
🪄 Autofix

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 Plus

Run ID: befc402c-0aa6-4c6c-9a69-d8ccacd6eaa2

📥 Commits

Reviewing files that changed from the base of the PR and between 5777019 and e7e720b.

📒 Files selected for processing (9)
  • README.md
  • README.zh.md
  • cmd/auth/check.go
  • cmd/auth/check_test.go
  • cmd/auth/login.go
  • cmd/auth/login_test.go
  • cmd/config/bind_messages.go
  • cmd/config/bind_warning_test.go
  • internal/auth/token_corrupted_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • cmd/auth/check.go
  • internal/auth/token_corrupted_test.go

Comment thread cmd/auth/check_test.go
Comment on lines +249 to +250
{name: "corrupted", accessToken: "", expiresIn: time.Hour, refreshIn: 24 * time.Hour, wantError: "corrupted_token"},
{name: "expired", accessToken: "user-access-token", expiresIn: -2 * time.Hour, refreshIn: -time.Hour, wantError: "expired_token"},

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 | 🟡 Minor | ⚡ Quick win

Cover the whitespace-only corrupted-token path.

TokenStatus classifies a whitespace-only AccessToken as corrupted after trimming it. This table tests only "". Add a " \t\n" fixture that expects "corrupted_token" so auth check cannot accept an unusable whitespace token.

As per coding guidelines, “Every behavior change must have an accompanying test.”

🤖 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/check_test.go` around lines 249 - 250, Extend the TokenStatus test
table in cmd/auth/check_test.go with a whitespace-only AccessToken fixture such
as " \t\n", and assert it returns "corrupted_token", alongside the existing
empty-token corrupted case.

Source: Coding guidelines

Comment thread cmd/auth/check_test.go
Comment on lines +281 to +298
err := authCheckRun(&CheckOptions{Factory: f, Scope: "im:message"})
if err == nil {
t.Fatalf("expected a non-zero exit for a %s token", tc.name)
}
if got := output.ExitCodeOf(err); got != 1 {
t.Errorf("exit code = %d, want 1", got)
}

var got map[string]interface{}
if jsonErr := json.Unmarshal(stdout.Bytes(), &got); jsonErr != nil {
t.Fatalf("json.Unmarshal(stdout) error = %v, stdout:\n%s", jsonErr, stdout.String())
}
if ok, _ := got["ok"].(bool); ok {
t.Fatalf("ok = true, want false; stdout:\n%s", stdout.String())
}
if got["error"] != tc.wantError {
t.Fatalf("error = %v, want %s; stdout:\n%s", got["error"], tc.wantError, stdout.String())
}

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 | 🟡 Minor | ⚡ Quick win

Assert typed error metadata at the status boundary.

This test checks the rendered JSON error code and exit status only. Add assertions for the typed error category and subtype with errs.ProblemOf, and verify cause preservation where the status error wraps a lower-layer error. Keep the JSON assertions because they test the command contract.

Based on learnings, errs.ProblemOf exposes problem-level fields only; use errors.As for Param only when the error is a *errs.ValidationError.

🤖 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/check_test.go` around lines 281 - 298, Extend the error assertions
in the authCheckRun test using errs.ProblemOf to verify the typed problem
category and subtype, while retaining the existing exit-code and JSON contract
checks. Where the status error wraps a lower-layer cause, assert cause
preservation; use errors.As to inspect Param only when the error is a
*errs.ValidationError, since errs.ProblemOf exposes only problem-level fields.

Sources: Coding guidelines, Learnings

Comment thread cmd/auth/check_test.go
Comment on lines +289 to +301
var got map[string]interface{}
if jsonErr := json.Unmarshal(stdout.Bytes(), &got); jsonErr != nil {
t.Fatalf("json.Unmarshal(stdout) error = %v, stdout:\n%s", jsonErr, stdout.String())
}
if ok, _ := got["ok"].(bool); ok {
t.Fatalf("ok = true, want false; stdout:\n%s", stdout.String())
}
if got["error"] != tc.wantError {
t.Fatalf("error = %v, want %s; stdout:\n%s", got["error"], tc.wantError, stdout.String())
}
if _, present := got["granted"]; present {
t.Fatalf("granted must be absent for a %s token, stdout:\n%s", tc.name, stdout.String())
}

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 | 🟡 Minor | ⚡ Quick win

Use a typed response projection and assert the granted scope.

Replace map[string]interface{} with one typed auth check response projection. In TestAuthCheckRun_NeedsRefreshTokenStillReportsScopes, assert that granted contains exactly "im:message" and that no scope is missing. ok: true alone passes if scope reporting regresses.

As per coding guidelines, “Parse map[string]interface{} into typed structs at the boundary” and “contract tests must assert the changed field or behavior directly.”

Also applies to: 342-347

🤖 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/check_test.go` around lines 289 - 301, Update the response parsing
in the affected auth check tests, especially
TestAuthCheckRun_NeedsRefreshTokenStillReportsScopes, to unmarshal into a typed
auth-check response projection instead of map[string]interface{}. Assert that
granted contains exactly the im:message scope, with no missing scope, while
retaining the existing ok and error assertions. Apply the same typed projection
to the additional affected assertion block.

Source: Coding guidelines

Comment thread cmd/auth/login.go Outdated
docs +search and base +title-resolve both call
POST /open-apis/search/v2/doc_wiki/search, which accepts a
tenant_access_token. Declaring user-only made the CLI reject --as bot
locally, before any request was sent, so callers concluded the platform
could not search docs under an app identity. drive +search already
declared both identities for the same endpoint.

Verified by a real bot-identity call after granting the search:docs:read
bot scope, not from metadata alone: registry has no Search v2 record and
the pinned SDK v3.7.2 reports user-only for this endpoint while v3.9.4
reports user and tenant.
@github-actions github-actions Bot added domain/base PR touches the base domain domain/ccm PR touches the ccm domain size/XL Architecture-level or global-impact change and removed size/L Large or sensitive change across domains or core paths labels Aug 5, 2026
@sang-neo03 sang-neo03 changed the title feat(auth): fix corrupted-token reporting and non-TTY login blocking feat(auth): fix corrupted-token reporting, non-TTY login blocking, and bot-rejected doc search Aug 5, 2026
vc +detail declared user-only, which broke the bot flow the
lark-vc-agent skill documents: after a bot joins a meeting it is told to
call +detail for note_id / minute_token, but the pre-flight identity
gate rejected the call before it reached the API.

Verified against the live API. GET /open-apis/vc/v1/meetings/{id} with a
tenant token succeeds once the bot is a participant, and keeps working
after the bot leaves. The earlier 121005 permission_denied that had this
endpoint written off came from probing a meeting the bot had never
joined — resource-level permission, not a token-type restriction, as
codemeta_vc.go already documents ("caller is not a participant").

The companion .../recording call returns 122001 (meeting status
unexpected) for an in-progress meeting with no recording, which is a
business state rather than an authorization rejection.
@github-actions github-actions Bot added the domain/vc PR touches the vc domain label Aug 5, 2026
@sang-neo03 sang-neo03 changed the title feat(auth): fix corrupted-token reporting, non-TTY login blocking, and bot-rejected doc search feat(auth): fix corrupted-token reporting, non-TTY login blocking, and bot-rejected shortcuts Aug 5, 2026
…ined

meeting.get answers 121005 with "user lacks permission for the requested
resource" whatever identity made the call. Under a bot that wording sends
the caller to auth login, but no user authorization can fix it — the
endpoint only serves participants and the app never joined the meeting.
Rewrite that one combination to name the cause and point at
vc +meeting-join, following the existing minutesReadError shape. A user
caller and every other error code keep the upstream wording.

This surfaced only once 43394b4 let a bot reach the endpoint at all, so
it lands with that change rather than separately.

Also pin the identity contracts from 06c738c and 43394b4 with
AuthTypes assertions for docs +search, base +title-resolve and
vc +detail. Their existing execution tests only ever pass --as user, so
reverting any of the three declarations to user-only left the suite
green. Each new assertion was verified to fail on revert.

The lark-vc-agent skill told the agent to keep the bot identity for a
meeting_id obtained by joining, then showed `vc +detail` with no --as,
which falls back to the user identity and hits the same 121005 from the
other side. Both call sites now carry the identity.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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

Inline comments:
In `@shortcuts/vc/vc_detail_test.go`:
- Around line 45-77: Extend the “bot 121005 is rewritten” test around
meetingDetailPermissionError to assert that ProblemOf preserves the original
error’s Category and Subtype, and verify the returned problem retains the
original typed error as its cause. Keep the existing rewritten bot message and
hint assertions, and apply the same cause-preservation check where appropriate
without changing the user or other-code wording expectations.

In `@skills/lark-vc-agent/SKILL.md`:
- Line 79: Update the meeting artifact lookup command in the listed instruction
to pass the inherited identity via `--as`, preserving the bot identity when the
preceding join used `--as bot`; keep the user identity for user-joined meetings.
🪄 Autofix

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 Plus

Run ID: d9ee2f72-23a6-4320-914a-3f4e3e14d8c0

📥 Commits

Reviewing files that changed from the base of the PR and between 43394b4 and 9773b25.

📒 Files selected for processing (5)
  • shortcuts/base/base_resolve_test.go
  • shortcuts/doc/docs_search_test.go
  • shortcuts/vc/vc_detail.go
  • shortcuts/vc/vc_detail_test.go
  • skills/lark-vc-agent/SKILL.md

Comment on lines +45 to +77
t.Run("bot 121005 is rewritten", func(t *testing.T) {
err := errs.NewPermissionError(errs.SubtypePermissionDenied, upstream).
WithCode(recordingNoPermissionCode)
p, ok := errs.ProblemOf(meetingDetailPermissionError(bareMeetingQueryRuntime(core.AsBot), err))
if !ok {
t.Fatalf("ProblemOf failed for %v", err)
}
if strings.Contains(p.Message, "user lacks permission") {
t.Errorf("Message = %q, want the bot-participant wording instead", p.Message)
}
if !strings.Contains(p.Hint, "vc +meeting-join") {
t.Errorf("Hint = %q, want it to point at vc +meeting-join", p.Hint)
}
})

t.Run("user identity keeps the upstream wording", func(t *testing.T) {
err := errs.NewPermissionError(errs.SubtypePermissionDenied, upstream).
WithCode(recordingNoPermissionCode)
p, _ := errs.ProblemOf(meetingDetailPermissionError(bareMeetingQueryRuntime(core.AsUser), err))
if p.Message != upstream {
t.Errorf("Message = %q, want it left unchanged", p.Message)
}
})

t.Run("other codes keep the upstream wording", func(t *testing.T) {
const scopeMsg = "app scope not applied"
err := errs.NewPermissionError(errs.SubtypeAppScopeNotApplied, scopeMsg).
WithCode(output.LarkErrAppScopeNotEnabled)
p, _ := errs.ProblemOf(meetingDetailPermissionError(bareMeetingQueryRuntime(core.AsBot), err))
if p.Message != scopeMsg {
t.Errorf("Message = %q, want it left unchanged", p.Message)
}
})

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 | 🟡 Minor | ⚡ Quick win

Assert typed metadata and error preservation.

Line 45 checks only the rewritten message and hint. Assert that Category and Subtype remain unchanged. Also assert that meetingDetailPermissionError preserves the original typed error. This prevents a message-only rewrite from changing the error contract.

As per coding guidelines, “Error-path tests must assert typed metadata through errs.ProblemOf … and verify cause preservation rather than relying only on message substrings.”

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

In `@shortcuts/vc/vc_detail_test.go` around lines 45 - 77, Extend the “bot 121005
is rewritten” test around meetingDetailPermissionError to assert that ProblemOf
preserves the original error’s Category and Subtype, and verify the returned
problem retains the original typed error as its cause. Keep the existing
rewritten bot message and hint assertions, and apply the same cause-preservation
check where appropriate without changing the user or other-code wording
expectations.

Source: Coding guidelines

Comment thread skills/lark-vc-agent/SKILL.md Outdated
meeting.get only returns meetings the calling identity joined, so a
meeting_id obtained by a bot join is unreadable as the user. The skill
already stated that rule, but four copy-pasteable commands omitted --as
and would silently fall back to the default identity:

  references/lark-vc-agent-meeting-join.md
  references/lark-vc-agent-meeting-events.md  (two sites)
  references/lark-vc-agent-meeting-leave.md

The two join/leave sites are bot-only flows and now pass --as bot. The
two meeting-events sites are identity-neutral — that same error is
reachable as the user, and the surrounding table already tells the reader
to switch back to --as user when the meeting_id came from user discovery
— so they say "carry the upstream identity" instead of hardcoding bot.
Examples in lark-vc, lark-note, lark-calendar and lark-minutes are left
alone: those start from a calendar entry or a minute, where the user
identity is correct.

Guarded by a test that walks the whole skill directory rather than the
entry file, since fixing SKILL.md while leaving the references stale is
exactly what happened here.
A completed login reports `"event": "authorization_complete"`, but the
--no-wait payload carried no event at all. Both paths exit 0, so a caller
had to infer "no token was stored" from the absence of a field that was
never documented as a signal. The payload now reports
`"event": "authorization_requested"`, using the same vocabulary.

Additive only — no existing field changes meaning, so callers that read
device_code or verification_url are unaffected.

Both names move into constants so the pair cannot drift, while the tests
keep asserting the literal strings: following the constant there would
let a rename pass CI while breaking every caller.
Reverts the unattended-login change: auth login no longer decides whether
to block from whether stdout is a terminal, and the --wait flag it added
is gone. The owner asked for this to land as its own change rather than
riding along with the token fixes.

Reverted to the merge base, not to origin/main. login.go and
login_test.go moved on main after this branch forked (#2189), and taking
those files from origin/main would have pulled that work in under a
revert commit. Merging main will bring it in on its own.

This also undoes a mistake of mine: main already emits
`"event": "device_authorization"` in the --no-wait payload. The reverted
change had dropped that field while rewriting the block, and I "restored"
it under a different name (authorization_requested), which would have
broken any caller matching on the original value. Reverting the file
restores the original name.

Still in this PR: the corrupted / expired stored-token fixes, and bot
auth for docs +search, base +title-resolve and vc +detail.
@sang-neo03 sang-neo03 changed the title feat(auth): fix corrupted-token reporting, non-TTY login blocking, and bot-rejected shortcuts fix(auth,vc): reject unusable stored tokens, allow bot auth where endpoints support it Aug 5, 2026
SKILL.md's post-meeting step still showed a bare
`lark-cli vc +detail --meeting-ids <meeting.id>`; the identity requirement
sat in the prose beside it. An agent copies the command, not the
sentence, so it still fell back to the default identity. The command now
carries `--as <upstream identity>`, matching the two meeting-events
sites; the join and leave sites keep `--as bot` because those flows are
bot-only.

The check that was supposed to catch this had a false negative: it
matched whole lines, and that line did contain "--as" — inside the
sentence explaining that --as bot is required. It now inspects only the
spans a reader would copy (fenced code-block lines, and the contents of
inline-code spans), so prose can no longer mask a bare command.

Sequenced deliberately: the new predicate was run before touching
SKILL.md and failed on exactly that line, which is what shows it catches
the case the old one missed.
@sang-neo03 sang-neo03 closed this Aug 6, 2026
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/ccm PR touches the ccm 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.

1 participant