Skip to content

feat(cli): add yertle auth status with per-key credential provenance - #12

Merged
albertcmiller1 merged 2 commits into
mainfrom
feat/auth-status
Aug 20, 2026
Merged

feat(cli): add yertle auth status with per-key credential provenance#12
albertcmiller1 merged 2 commits into
mainfrom
feat/auth-status

Conversation

@albertcmiller1

Copy link
Copy Markdown
Contributor

Why

Credentials resolve from two sources — $YERTLE_TOKEN/$YERTLE_API_URL and ~/.yertle/config.json — with independent per-key precedence, matching gh and aws. Keeping both is necessary: the MCP server is configured purely through the env block of claude_desktop_config.json (docs/mcp/OVERVIEW.md), and the SDK's primary audience — CI, scripts, containers — has no home directory to yertle login into.

But there was no way to answer "which backend am I actually pointed at right now?" without making a request and reading the failure.

The sharp edge is that token and URL resolve independently, so a config-file token can pair with an env-var URL. That's exactly how a token issued by one backend ends up aimed at another — and it surfaces only as an opaque 401, the case _format_api_error already needed three clauses to explain.

What

$ yertle auth status
  API URL  http://localhost:8000            (from $YERTLE_API_URL)
  Token    yrt_B1ra…dbx8                    (from $YERTLE_TOKEN)

The mixed-source case is now visible before the call:

$ yertle auth status
  API URL  http://localhost:8000            (from $YERTLE_API_URL)
  Token    yrt_7_V6…Zg4A                    (from ~/.yertle/config.json)

Unauthenticated (exit 1):

$ yertle auth status
  API URL  https://api.yertle.com           (default)
  Token    —                                (not set)

Not authenticated. Run `yertle login` or set $YERTLE_TOKEN.

Changes

  • shared/auth.py — add Source and ResolvedCredentials; new resolve() reports provenance and does not raise on a missing token, so the status command can render the unauthenticated state. resolve_credentials() becomes a thin wrapper over it, so the precedence logic stays in exactly one place — it's shared by all three surfaces (CLI, MCP, SDK).
  • cli/main.py — new auth sub-app with status. Tokens are masked (prefix+suffix; short values masked entirely) because this output is what people paste into bug reports. Exits non-zero when unauthenticated, so it works as a scripted precondition check.
  • Switched cli/main.py to a module-style auth import so CONFIG_PATH is read at call time rather than bound at import; test_orgs.py patch targets follow.
  • pyproject.toml — loosen reportPrivateUsage for tests/; _mask_token's edge cases are worth testing directly.
  • Docs: note the mixed-source hazard and point at the new command.

Testing

make check is green (lint, format, pyright strict, 65 tests). New coverage: four resolve() provenance cases (both-env, mixed, default-URL, missing-token-without-raising) and five CLI tests including an explicit assertion that the raw token never appears in output.

Also exercised end-to-end against a local backend on :8000 — all four resolution paths above are real output, and yertle orgs still works.

Notes

  • yertle login stays a top-level command. Moving it to yertle auth login (the gh shape) would break the published 0.1.0 CLI — worth doing, but as its own decision.
  • One unrelated line: uv.lock picks up the 0.1.0 version bump from 83116c3, which was never re-locked. Included so it stops dirtying working trees.
  • Follow-up worth considering: save_credentials writes the token at default 0644 and clobbers rather than merges.

🤖 Generated with Claude Code

albertcmiller1 and others added 2 commits August 20, 2026 14:56
Credentials resolve from two sources (env vars and ~/.yertle/config.json)
with independent per-key precedence, matching gh/aws. That flexibility is
required — the MCP server is configured purely through the `env` block of
claude_desktop_config.json, and the SDK's primary audience (CI, containers)
has no home directory to log into — but it leaves no way to answer "which
backend am I actually pointed at right now?" short of making a request and
reading the error.

The sharp edge is that token and URL resolve independently, so a config-file
token can pair with an env-var URL. That is how a token issued by one backend
ends up aimed at another, and it surfaces only as an opaque 401 — the case
`_format_api_error` already needed three clauses to explain.

- `shared/auth.py`: add `Source` and `ResolvedCredentials`, plus `resolve()`
  which reports provenance and does not raise on a missing token (so the
  status command can render the unauthenticated state). `resolve_credentials()`
  becomes a thin wrapper over it, keeping the precedence logic in one place —
  it is shared by all three surfaces (CLI, MCP, SDK).
- `cli/main.py`: add the `auth` sub-app and `auth status`. Tokens are masked
  (prefix+suffix, whole-value for short inputs) since this output is what
  users paste into bug reports. Exits non-zero when unauthenticated so it
  works as a scripted precondition.
- Switch `cli/main.py` to a module-style `auth` import so `CONFIG_PATH` is
  read at call time rather than bound at import; `test_orgs.py` patch targets
  follow.
- Loosen `reportPrivateUsage` for tests: `_mask_token`'s edge cases are worth
  covering directly.

`yertle login` stays a top-level command. Moving it under `auth` would break
the published 0.1.0 CLI and is out of scope here.

Unrelated: uv.lock picks up the 0.1.0 version bump from 83116c3, which was
never re-locked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CI caught the status output hard-breaking the config path mid-token
(`config.js\non`) when the line exceeded console width. Not a test artifact:
any user on a narrow terminal, or with a long API URL, would see a path or
URL split across lines — neither greppable nor copy-pasteable.

- `soft_wrap=True` so the terminal wraps instead of Rich folding tokens.
- Size the value column to its contents; the fixed `:<32` padding shoved the
  source column out of alignment for long URLs and inflated the line width
  that triggered the wrap in the first place.

The regression test asserts the config path appears *contiguously* rather
than checking for a `config.json` substring — the substring version passed
against the unfixed code, since whether a break lands inside that literal
depends on where the column boundary falls.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@albertcmiller1
albertcmiller1 merged commit 2a6f333 into main Aug 20, 2026
1 check passed
@albertcmiller1
albertcmiller1 deleted the feat/auth-status branch August 20, 2026 19:17
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.

1 participant