Skip to content

fix(cli): tailor UNAUTHORIZED guidance by credential + scope (TNT-142)#102

Merged
jgpruitt merged 2 commits into
mainfrom
jgpruitt/session-expired
Jun 25, 2026
Merged

fix(cli): tailor UNAUTHORIZED guidance by credential + scope (TNT-142)#102
jgpruitt merged 2 commits into
mainfrom
jgpruitt/session-expired

Conversation

@jgpruitt

Copy link
Copy Markdown
Collaborator

Summary

Fixes TNT-142. The CLI's handleError rewrote any UNAUTHORIZED (HTTP 401) into "Session expired. Run 'me login' to sign in again." and cleared the stored token — gated only on a sessionServer opt, never checking which credential was actually used. Two real defects:

  1. API-key callers get a session message. With ME_API_KEY (agent key or PAT), me login is irrelevant and there's no session token (clearTokens is a no-op on the env key). Surfaced while testing feat(auth): broaden agent API-key utility in the CLI; authorize per-method on the user RPC (TNT-139) #101 / TNT-139, which broadened agent-key use across CLI commands: me group mine → "Session expired" even though the real cause was an unresolved active space.
  2. Humans can get logged out by a bad space slug. On a space-scoped command the memory endpoint returns UNAUTHORIZED ("Invalid credentials") when the space doesn't resolve — deliberately generic to avoid space enumeration, and indistinguishable from a real credential failure. For a logged-in human a stale/typo'd active space therefore wiped their session and told them to re-login.

The CLI can't tell "unknown space" from "bad credential" apart on the wire, so the guidance is tailored by credential type and command scope instead.

Changes

  • util.ts: new pure, exported describeAuthError(error, creds, scope){ message, clearSession } | null (null for non-401s, so the raw server message is kept):
    • api key / account → blame ME_API_KEY; never clear a session.
    • api key / spaceME_API_KEY invalid or active space '<slug>' missing/inaccessible → me space list.
    • session / account → genuine expiry: clear token + "Run 'me login'" (unchanged).
    • session / space → ambiguous: mention the space (me space list) and me login; do not clear the token.
    • handleError opts changed { sessionServer }{ creds?, scope? } (default account); clears the token only when clearSession is true.
  • Call sites updated by the rule builds a memory client → scope: "space": all of group/access, the memory-client handlers in agent/space, account scope for whoami/apikey/rest. The previously-unhandled memory commands (memory.*, import*, pack) now also get the key+space hint instead of the raw "Invalid credentials".
  • Tests: util.test.ts gains 6 describeAuthError cases (all 4 branches, slug present/absent, non-401 passthrough).

Server-side behavior is unchanged — the intentionally-generic 401 stays (enumeration safety); the CLI message now covers both causes.

Verification

  • ./bun run check — typecheck + lint + unit all green (the one lint warning in import-git-hook.ts is pre-existing and untouched).
  • Manual: ME_API_KEY + bad ME_SPACE → space/key hint (not "Session expired"); me whoami still works; session + bad space → token not cleared.

handleError rewrote every UNAUTHORIZED into "Session expired. Run 'me
login'" and cleared the stored token, gated only on a sessionServer
opt — it never checked which credential was actually used. So an
agent/PAT caller (ME_API_KEY) hitting a 401 was told to run `me login`
(irrelevant; clearTokens is a no-op on an env key), and a logged-in
human with a stale/typo'd active space got silently logged out: a
space-scoped 401 ("unknown space", deliberately generic to avoid
enumeration) is indistinguishable from an expired session.

Add a pure, tested `describeAuthError(error, creds, scope)` that picks
the message and whether to clear the session, by credential type and
command scope (account = user RPC, space = memory RPC):

- api key: never "me login", never clear a token; point at ME_API_KEY
  (and, for space commands, the active space + `me space list`).
- session/account: genuine expiry — clear the token, prompt re-login
  (unchanged).
- session/space: ambiguous — keep the token, mention both the space
  (`me space list`) and a possible re-login.

handleError now takes `{ creds, scope }` (default account). Call sites
updated by the rule "builds a memory client → scope: space"; the
previously-unhandled memory commands (memory.*, import*, pack) now also
get the key+space hint instead of the raw "Invalid credentials".

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes misleading CLI handling of UNAUTHORIZED (HTTP 401) by tailoring guidance based on credential type (session vs ME_API_KEY) and command scope (account/user-RPC vs space/memory-RPC), avoiding unnecessary session clearing when a 401 could be caused by an invalid/unknown active space.

Changes:

  • Adds describeAuthError(error, creds, scope) to convert 401s into actionable messages and decide whether to clear the stored session token.
  • Updates handleError to use { creds, scope } (default account) and only clear tokens on unambiguous session-expiry cases.
  • Updates CLI command call sites to pass the correct scope, and adds unit tests covering the new auth-description logic.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/cli/util.ts Introduces AuthScope + describeAuthError, and updates handleError to tailor 401 handling and token clearing.
packages/cli/util.test.ts Adds unit tests for describeAuthError branches (api key vs session; account vs space).
packages/cli/commands/whoami.ts Passes resolved credentials into handleError for account-scoped user-RPC errors.
packages/cli/commands/space.ts Passes { creds } for account-scoped space commands and { creds, scope: "space" } for invite (memory-RPC) commands.
packages/cli/commands/pack.ts Routes pack errors through handleError with space scope to improve 401 guidance.
packages/cli/commands/memory.ts Routes memory RPC errors through handleError with space scope.
packages/cli/commands/import.ts Routes import RPC errors through handleError with space scope.
packages/cli/commands/import-git.ts Routes git-import RPC errors through handleError with space scope.
packages/cli/commands/group.ts Updates group commands to pass { creds, scope: "space" } into handleError.
packages/cli/commands/apikey.ts Passes { creds } into handleError for account-scoped apiKey user-RPC calls.
packages/cli/commands/agent.ts Uses account scope for user-RPC calls and space scope for memory-RPC calls.
packages/cli/commands/access.ts Updates access commands to pass { creds, scope: "space" } into handleError.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/cli/util.ts Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: John Pruitt <jgpruitt@gmail.com>
@jgpruitt jgpruitt merged commit 9e330c0 into main Jun 25, 2026
4 checks passed
@jgpruitt jgpruitt deleted the jgpruitt/session-expired branch June 25, 2026 18:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants