feat(cli): add yertle auth status with per-key credential provenance - #12
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Credentials resolve from two sources —
$YERTLE_TOKEN/$YERTLE_API_URLand~/.yertle/config.json— with independent per-key precedence, matchingghandaws. Keeping both is necessary: the MCP server is configured purely through theenvblock ofclaude_desktop_config.json(docs/mcp/OVERVIEW.md), and the SDK's primary audience — CI, scripts, containers — has no home directory toyertle logininto.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_erroralready needed three clauses to explain.What
The mixed-source case is now visible before the call:
Unauthenticated (exit 1):
Changes
shared/auth.py— addSourceandResolvedCredentials; newresolve()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— newauthsub-app withstatus. 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.cli/main.pyto a module-styleauthimport soCONFIG_PATHis read at call time rather than bound at import;test_orgs.pypatch targets follow.pyproject.toml— loosenreportPrivateUsagefortests/;_mask_token's edge cases are worth testing directly.Testing
make checkis green (lint, format, pyright strict, 65 tests). New coverage: fourresolve()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, andyertle orgsstill works.Notes
yertle loginstays a top-level command. Moving it toyertle auth login(theghshape) would break the published 0.1.0 CLI — worth doing, but as its own decision.uv.lockpicks up the0.1.0version bump from 83116c3, which was never re-locked. Included so it stops dirtying working trees.save_credentialswrites the token at default0644and clobbers rather than merges.🤖 Generated with Claude Code