Files
David Barkhausen 5784d907c0 feat(cli): PAD_TOKEN environment override for stored credentials (#879) (#1160)
* feat(cli): PAD_TOKEN environment override for stored credentials (#879)

Layer 1 of #879: if PAD_TOKEN is set, the CLI uses it as the bearer
token and skips the credential-store lookup — gh's GH_TOKEN convention.
Reads never write credentials.json, so a read-only override sidesteps
the multi-agent identity contention completely; the store is never
touched under the override.

Per the acceptance grounding notes:

- NewClientFromURL resolves PAD_TOKEN before the per-server store
  lookup (the single token-attachment chokepoint).
- whoami no longer lies under the override: it skips the store
  short-circuit and reports the effective identity via a real /me
  fetch, with an 'Auth: PAD_TOKEN environment override' line.
- auth login/logout print a gh-style stderr notice when the override
  is active. logout additionally pins its server-side session
  invalidation to the STORED token — an unpinned Logout() after the
  constructor change would have invalidated the env token's session —
  and skips the server call when there is no stored session.
- pad init's status line and server info's report disclose the
  override (env_token_override field; the auth probe uses the token
  every other command would use).

Zero behaviour change when PAD_TOKEN is unset. Token minting stays
web-only; a minimal 'pad token' CLI is offered as a follow-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(cli): review round 1 — init fails on a rejected PAD_TOKEN; login shortcut skipped under the override; logout asymmetry documented

Per the PR #1160 round-1 review:

- Bug 1: pad init's auth step no longer falls back to stored
  credentials when a set PAD_TOKEN is rejected — it fails with the
  distinct rejected-token message (mirroring whoami), which also makes
  the status line's override disclosure truthful. Test drives the real
  padInitCmd flow and asserts the stored identity is never consulted.
- Bug 2: login's 'Already logged in as <stored user>' shortcut is
  skipped when the override is active — it reads the store, and firing
  it right after envTokenNotice contradicted the notice. A second test
  pins the unchanged no-override shortcut behaviour.
- Doc ask: the deliberate logout asymmetry (the env token's own
  session is never invalidated; its lifecycle belongs to the minter,
  GH_TOKEN posture) is now stated in env_token.go's doc comment and
  the README PAD_TOKEN section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 07:54:13 -04:00

34 lines
1.4 KiB
Go

package cli
import (
"os"
"strings"
)
// EnvToken returns the value of the PAD_TOKEN environment variable,
// trimmed of surrounding whitespace. When non-empty, it overrides any
// credential stored in ~/.pad/credentials.json — the same convention as
// gh's GH_TOKEN (issue #879, layer 1).
//
// Why an env override at all: the credential store is per-server, not
// per-process, so several agent processes on one machine cannot act as
// different Pad users without contending over the file. Reads never
// write credentials.json (only login/setup/logout do), so a read-only
// override sidesteps the identity-switching contention completely —
// each process carries its own token in its environment and the store
// is never touched.
//
// Accepts either token type the server takes (padsess_ session or
// pad_ API token); API tokens are the intended fit (mint under
// Settings → API tokens in the web UI).
//
// Lifecycle asymmetry, deliberate: `pad auth logout` never invalidates
// the env token's own server-side session — it pins its Logout() call
// to the STORED token and only deletes the stored entry. The env
// token's lifecycle belongs to whoever minted it (revoke it where it
// was minted), exactly gh's GH_TOKEN posture. This is the read-only
// contract applied to logout, not an oversight.
func EnvToken() string {
return strings.TrimSpace(os.Getenv("PAD_TOKEN"))
}