diff --git a/AGENTS.md b/AGENTS.md index 559b4baa3..4d1389904 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -65,6 +65,14 @@ Both notices recommend the same fix command: `lark-cli update`. The skills notic | `internal/vfs/` | Filesystem abstraction (use `vfs.*` instead of `os.*`) | | `internal/validate/path.go` | Path safety validation | +## Auth / TAT operator notes + +- The current TAT path is documented inline in `internal/credential/`, `cmd/config/init_probe.go`, `cmd/auth/status.go`, and `internal/identitydiag/diagnostics.go`. Historical materials that talk about `SEC_AUTH` or `cmd/sec/*` should be treated as drift, not as current-tree entry points. +- `config init` only performs a best-effort post-save probe. A typed auth error means the credentials were deterministically rejected; transport / timeout / parse failures are intentionally treated as ambiguous noise and do not prove the config is bad. +- `auth status --verify` and runtime bot commands share the same credential chain. A successful bot verify confirms the current token source can call `/open-apis/bot/v3/info`, but it does not prove every downstream bot scope or API path will work. +- Historical reports still matter: sandboxed runners may fail to read OS keychain state, and users can complete browser authorization while local state remains missing or stale. Treat those as environment / local-state risks to inspect, not proof that the remote authorization page never succeeded. +- The notes above are derived from current mainline code plus historical reports. They are intentionally not a live verification guarantee; real token usability still depends on valid app config, local state, and network reachability. + ## Who Uses This CLI This CLI's primary consumers include AI agents (Claude Code, Cursor, Gemini CLI). Your code is read by machines — error messages, output format, and flag design all directly affect agent success rates. diff --git a/cmd/auth/status.go b/cmd/auth/status.go index f0cf85e4d..e5d25085b 100644 --- a/cmd/auth/status.go +++ b/cmd/auth/status.go @@ -40,6 +40,10 @@ func NewCmdAuthStatus(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobr return cmd } +// authStatusRun reports the configured identities. When --verify is enabled, +// the bot branch does not mint a token independently — it reuses the shared +// credential resolution path, so `auth status --verify` exercises the same TAT +// source selection logic as normal bot API calls. func authStatusRun(opts *StatusOptions) error { f := opts.Factory diff --git a/cmd/config/init_probe.go b/cmd/config/init_probe.go index e56aa1445..37d1cb0a7 100644 --- a/cmd/config/init_probe.go +++ b/cmd/config/init_probe.go @@ -28,6 +28,10 @@ const probeTimeout = 3 * time.Second // so that valid configurations and transient/upstream noise never block the // command. // +// This is one of the repository's three main TAT entry points: config init +// validates freshly-saved app credentials here, while runtime bot calls and +// `auth status --verify` go through credential.ResolveToken. +// // The function performs up to two HTTP calls in series, bounded by // probeTimeout: // diff --git a/cmd/event/consume.go b/cmd/event/consume.go index 0f8134b68..61dee8e0a 100644 --- a/cmd/event/consume.go +++ b/cmd/event/consume.go @@ -322,7 +322,10 @@ func sanitizeOutputDir(dir string) (string, error) { return safe, nil } -// resolveTenantToken fetches the app's tenant access token. +// resolveTenantToken fetches the app's tenant access token for event consume. +// This is a concrete runtime consumer of the shared bot-token path: event +// consume does not own a separate exchange flow, it asks the credential layer +// for the same TAT that other bot-mode commands use. func resolveTenantToken(ctx context.Context, f *cmdutil.Factory, appID string) (string, error) { if ctx == nil { ctx = context.Background() diff --git a/extension/credential/env/env.go b/extension/credential/env/env.go index 054d27d85..f3893f7e5 100644 --- a/extension/credential/env/env.go +++ b/extension/credential/env/env.go @@ -94,6 +94,9 @@ func (p *Provider) ResolveAccount(ctx context.Context) (*credential.Account, err return acct, nil } +// ResolveToken returns a token directly from environment variables. For bot +// identity this is a straight TAT override that bypasses the built-in +// app_id/app_secret mint path entirely. func (p *Provider) ResolveToken(ctx context.Context, req credential.TokenSpec) (*credential.Token, error) { var envKey string switch req.Type { diff --git a/extension/credential/sidecar/provider.go b/extension/credential/sidecar/provider.go index 99948939a..6fdb0ab37 100644 --- a/extension/credential/sidecar/provider.go +++ b/extension/credential/sidecar/provider.go @@ -103,6 +103,8 @@ func (p *Provider) ResolveAccount(ctx context.Context) (*credential.Account, err // ResolveToken returns a sentinel token whose value encodes the token type. // The transport interceptor reads this sentinel to determine the identity // (user vs bot), strips it, and the sidecar injects the real token. +// In other words, sidecar mode proxies the normal UAT/TAT contract; it does +// not introduce a new open-platform token-exchange protocol inside this repo. // Returns nil, nil when sidecar mode is not active. func (p *Provider) ResolveToken(ctx context.Context, req credential.TokenSpec) (*credential.Token, error) { if os.Getenv(envvars.CliAuthProxy) == "" { diff --git a/extension/transport/sidecar/interceptor.go b/extension/transport/sidecar/interceptor.go index 6d928bd8a..8f87d443b 100644 --- a/extension/transport/sidecar/interceptor.go +++ b/extension/transport/sidecar/interceptor.go @@ -64,6 +64,9 @@ type Interceptor struct { // Supports two auth patterns: // - Standard OpenAPI: Authorization: Bearer // - MCP protocol: X-Lark-MCP-UAT/TAT: +// +// For bot traffic the sentinel stands in for TAT only long enough for the +// proxy hop; the real tenant access token is injected by the sidecar. func (i *Interceptor) PreRoundTrip(req *http.Request) func(resp *http.Response, err error) { identity, authHeader := detectSentinel(req) if identity == "" { diff --git a/internal/credential/credential_provider.go b/internal/credential/credential_provider.go index 8f20be11d..dbccf4280 100644 --- a/internal/credential/credential_provider.go +++ b/internal/credential/credential_provider.go @@ -301,6 +301,16 @@ func (p *CredentialProvider) doResolveIdentityHint(ctx context.Context) (*Identi } // ResolveToken resolves an access token. +// +// For bot identity this is the runtime TAT entry point used across the CLI: +// 1. an active credential source (for example env or sidecar) gets first +// chance to return a token; +// 2. extension providers are tried next; +// 3. the built-in default provider falls back to FetchTAT(app_id, app_secret) +// and caches that tenant access token with sync.Once for the process. +// +// The env provider may supply a real TAT directly; the sidecar provider returns +// a sentinel token that the transport layer rewrites into a proxied request. func (p *CredentialProvider) ResolveToken(ctx context.Context, req TokenSpec) (*TokenResult, error) { source, err := p.selectedCredentialSource(ctx) if err != nil { diff --git a/internal/credential/default_provider.go b/internal/credential/default_provider.go index 9482b2845..426510c9b 100644 --- a/internal/credential/default_provider.go +++ b/internal/credential/default_provider.go @@ -147,6 +147,8 @@ func (p *DefaultTokenProvider) resolveUAT(ctx context.Context) (*TokenResult, er } // resolveTAT resolves a tenant access token. Result is cached after first call. +// This is the built-in runtime path behind bot token resolution when no +// external credential source overrides it. // NOTE: Uses sync.Once — only the context from the first call is used. func (p *DefaultTokenProvider) resolveTAT(ctx context.Context) (*TokenResult, error) { p.tatOnce.Do(func() { diff --git a/internal/credential/tat_fetch.go b/internal/credential/tat_fetch.go index eecaed20e..051d00937 100644 --- a/internal/credential/tat_fetch.go +++ b/internal/credential/tat_fetch.go @@ -18,6 +18,11 @@ import ( // that already hold plaintext credentials (e.g. the post-`config init` probe) // can validate them without a second keychain round-trip. // +// This is the built-in TAT mint path for the current repository: +// app_id + app_secret -> /open-apis/auth/v3/tenant_access_token/internal. +// It is reused by both the post-config probe and the default runtime bot-token +// resolver. The tree does not contain a separate app_ticket-based TAT exchange. +// // A non-zero TAT response code means the server inspected the payload and // rejected the credentials; FetchTAT returns the canonical typed error from // classifyTATResponseCode — the SAME classification doResolveTAT (and thus diff --git a/internal/credential/types.go b/internal/credential/types.go index 98af4d475..25dc818b1 100644 --- a/internal/credential/types.go +++ b/internal/credential/types.go @@ -97,6 +97,10 @@ type AccountProvider interface { } // TokenType distinguishes UAT from TAT. +// In this repository, TAT always means "tenant access token" for bot identity: +// either minted from app_id + app_secret by the built-in provider, supplied +// directly by an extension provider, or proxied through authsidecar. There is +// no app_ticket -> tenant_access_token exchange implementation in this tree. // Uses string constants matching extension/credential.TokenType for zero-cost conversion. type TokenType string diff --git a/internal/identitydiag/diagnostics.go b/internal/identitydiag/diagnostics.go index f8eb648d6..024362bce 100644 --- a/internal/identitydiag/diagnostics.go +++ b/internal/identitydiag/diagnostics.go @@ -217,6 +217,10 @@ func diagnoseUser(ctx context.Context, f *cmdutil.Factory, cfg *core.CliConfig, return id } +// resolveBotToken is the bot-verification consumer of the shared credential +// chain. It intentionally reuses ResolveToken(core.AsBot, appID) so auth +// diagnostics observe the same env-provider, sidecar, strict-mode, and +// built-in FetchTAT behavior as normal runtime bot commands. func resolveBotToken(ctx context.Context, f *cmdutil.Factory, cfg *core.CliConfig) (string, error) { if f == nil || f.Credential == nil { return "", &credential.TokenUnavailableError{Type: credential.TokenTypeTAT} diff --git a/main_noauthsidecar.go b/main_noauthsidecar.go index 514afda63..3c4d7c1f8 100644 --- a/main_noauthsidecar.go +++ b/main_noauthsidecar.go @@ -8,6 +8,8 @@ // compiled in under that tag; deploying the plain build into an environment // that expects sidecar isolation would silently fall back to direct env // credential use — exactly the failure mode the feature is meant to prevent. +// Without the authsidecar build tag, the binary cannot safely proxy either UAT +// or TAT through the sidecar contract at all. // // When LARKSUITE_CLI_AUTH_PROXY is set, we refuse to run rather than ignore // the variable. The operator either rebuilt without realizing (wrong