Authentication
How the Solwyn CLI stores credentials, resolves them, what a key's scope allows, and which credential each command requires
Credentials live in your operating system's keychain — never in config files, never in argv, never echoed from credential input. The CLI reads and writes them through the keyring library and hands the secret to an Authorization header only at the request boundary. Nothing else touches it.
Where credentials are stored
Every stored secret sits under the keychain service name solwyn-cli, as three separate entries:
| Entry | Holds |
|---|---|
api_key | A project API key |
access_token | A JWT access token |
refresh_token | A JWT refresh token |
Your platform's native secret store backs the keychain:
| Platform | Backend |
|---|---|
| macOS | Keychain Services |
| Linux | SecretService (GNOME Keyring, KDE Wallet) |
| Windows | Windows Credential Manager |
When no keychain backend is available, the CLI treats the keychain as empty rather than failing — so a container with SOLWYN_API_KEY set works, and a container without one reports that authentication is required.
The CLI never accepts a supplied key or password as a command-line argument, and never echoes credential input. Newly created project keys are deliberately revealed once or stored securely. Project keys are read from a hidden prompt or from stdin; passwords are read from a hidden prompt only. A credential in your shell history is a credential you did not get from Solwyn.
Two kinds of credential
Solwyn recognizes two credential types, and they authorize different things.
A JWT pair — an access token plus a refresh token — represents you, the logged-in user, with your account role: owner, admin, or member. You obtain it with solwyn login, which prompts for your email and password on an interactive terminal. When an eligible request receives a definite 401 rejection, the CLI refreshes the pair once and retries once; you do not re-login when an access token expires. Mutations that must not be replayed — keys rotate, keys revoke, projects create, and a batch budget check — skip that retry and fail immediately on a 401 instead.
A project key authorizes one project. It has the shape sk_proj_ followed by 64 lowercase hexadecimal characters — validated against the pattern ^sk_proj_[a-f0-9]{64}$ before it is ever stored. You provide it with solwyn login --with-key (a hidden prompt on a terminal, or piped over stdin), or through the SOLWYN_API_KEY environment variable.
Key scopes
Every project key carries a scope: full or read_only. A full key can do everything a project key may do. A read_only key satisfies project-key reads — status, costs, budget status, runs list, notifications show — but is refused by anything that writes: budget check, budget confirm, record, notifications set. The refusal is a 403 with the code read_only_key, exit 3, and a hint that names the fix:
Rotate to a full-scope key with solwyn keys rotate --scope full.You choose a scope when a key is minted — projects create --scope for a project's first key, keys rotate --scope for a replacement — and keys show reports the current one. Give a read-only key to a dashboard or a monitoring job; give a full key to the SDK and to any agent that will run the budget guardrail loop.
Resolution precedence
When a command needs a credential, the CLI resolves one in a fixed order. The first source that yields a value wins:
SOLWYN_API_KEY environment variable — if set, its value is used as a project key. A malformed value fails immediately with exit 3; it does not fall through to the keychain.
Stored project key — the keychain's api_key entry.
Stored JWT pair — the keychain's access_token and refresh_token entries. A pair with one entry missing is reported as incomplete and does not select another identity.
If none resolve, the command reports authentication_required with the hint Set SOLWYN_API_KEY or run solwyn login. and exits 3. An environment key always shadows a stored key, and a stored key always shadows a stored JWT pair — set SOLWYN_API_KEY and you act as that project regardless of what the keychain holds.
What each command requires
Some commands act on your whole account and need your user login; some act on a single project and need that project's key; some accept either. Every command's --help states its requirement on the Auth: line.
| Credential | Commands |
|---|---|
| Project key only | budget check, budget confirm, record; writes need full scope |
| JWT reads | overview, audit, projects list, keys show, channels list/show, account alert-email show, account phone show, alerts account show/preview, alerts activity/pending, alerts … --account, costs --account, attest --account, team members list, plan change --dry-run, and opt-in --with-last-fired reads |
| Owner/admin JWT | projects create/rename/delete (project.write), keys rotate/revoke (key.write), budget set/restore-defaults, budget rule and threshold writes, runs stop (budget.write), webhook writes (project.write), tags retire, team invites list/send/resend/revoke |
| Owner JWT | Billing writes including checkout/portal creation, team members set-role, account rename, account alert-email set/clear, channel mutations/test/delete, account-alert mutations |
| JWT with target-specific role | team members remove and team leave: owner removes admin/member, admin removes member, any role removes itself; last-owner protection applies |
| Either credential | status, whoami, forecast, budget status/uncounted, ordinary budget rules list, rule preview, project costs/analytics, providers, runs list/show, tags list/values, project alert history/export, project attest, notifications show, projects show, plan, plan list |
| Conditional | notifications set accepts full-scope key or owner/admin JWT; Slack/PagerDuty test accepts JWT or full-scope key; email test requires JWT; SMS test checks eligibility without sending |
| Optional | doctor; init accepts existing credentials or interactive bootstrap |
| No credential | Local login, logout, config, agent-context, mcp-config; public invite show performs a credential-free preview read |
account recover requires JWT but no active membership. invite accept uses selected JWT, while explicit --new-user requires a terminal and no selected credential; it never silently falls back to signup. See invitation workflows. No CLI command transfers ownership or implements an email-link verification flow.
solwyn mcp resolves a credential per tool call, not at startup. notifications set accepts either credential but needs a full-scope key to write; notifications test email requires a login because it sends to your account address.
Use the wrong type and the command fails with exit 3 before any request is dispatched. A key-only command run under a JWT reports project_key_required with the hint to set SOLWYN_API_KEY or store a key with solwyn login --with-key. A login-only command run under a project key reports authentication_required (or jwt_required) with a message naming the operation and a hint that depends on the key's source:
| Key came from | Hint |
|---|---|
SOLWYN_API_KEY | Unset SOLWYN_API_KEY, run solwyn login, then retry keys show. |
| the keychain | Run solwyn logout, then solwyn login, and retry keys show. |
Authorization failures exit 3
Since 0.2.0, an HTTP 403 maps to exit 3, the same as a 401. Four cases carry a specific hint:
| Situation | Code | Hint |
|---|---|---|
| A read-only key attempted a write | read_only_key | Rotate to a full-scope key with solwyn keys rotate --scope full. |
| A project key was used against a different project | (varies) | Select the project associated with this key, or configure a key for the selected project. |
| A project key was used where a login is required | (varies) | Run solwyn login to authenticate with your user account. |
| Your role cannot perform the action | owner_required, admin_required, role_required, permission_required | Your role cannot perform this action; use solwyn whoami to inspect it. |
A 402 — a plan-tier or quota limit — is not an authorization failure. It exits 1, carries the API's message, and links to the dashboard's billing page.
Revoked membership and capacity
A JWT response with account_membership_revoked means login alone cannot restore membership. Use account recover to idempotently create a personal account, or return the active one. If a key masks JWT, unset SOLWYN_API_KEY and deliberately clear stored credentials with logout before logging in. Recovery stays CLI-only.
Transient auth_capacity_exceeded is retried once with the shared Retry-After wait capped at 10 seconds; continued overload exits 1, not bad-credential exit 3. Other unknown write outcomes do not imply no effect. See reliability rules.
Logging in
$ solwyn login
Email: alice@example.com
Password: ********Stores a JWT pair in the keychain. Email and password login requires an interactive terminal — run it in a non-interactive context and it exits 3 with a hint to use SOLWYN_API_KEY or a project key instead.
$ solwyn login --with-key
Project key: ****************On a terminal, the key is read from a hidden prompt. With no terminal, it is read from stdin — pipe it in:
$ echo "$SOLWYN_PROJECT_KEY" | solwyn login --with-keyEither way the key is validated against ^sk_proj_[a-f0-9]{64}$ and stored in the keychain's api_key entry. A malformed key is rejected before storage.
Logging out
$ solwyn logoutClears all three keychain entries — api_key, access_token, and refresh_token. It is idempotent: an entry already absent counts as cleared. Environment variables are not touched; if SOLWYN_API_KEY is set, it still resolves after a logout.
CI, containers, and agents
In pipelines and containers there is usually no keychain backend. Provide a project key through SOLWYN_API_KEY instead — it sits at the top of the resolution order, so it works with no keychain present:
jobs:
check-budget:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install solwyn-cli
- run: solwyn --project ${{ vars.SOLWYN_PROJECT_ID }} budget status
env:
SOLWYN_API_KEY: ${{ secrets.SOLWYN_API_KEY }}Because budget check, budget confirm, and record require a project key, SOLWYN_API_KEY is exactly the credential automated budget guardrails run on — and it must be a full-scope key. A read-only key passes every read in a pipeline and then fails the first write with exit 3. See Scripting for the machine contract and Agents for wiring guardrails into agent loops.
Checking your state
$ solwyn whoamiReports which credential resolved, its source (keyring or environment), and — under a JWT pair — your identity, email_verified, account id, role, seat usage, and projects. It is the fastest way to confirm which project a given shell will act as, and which role a mutation will run under. doctor goes further and probes the API with the resolved credential.
Next steps
- Configuration — set a default project and API URL in
config.toml - init — one-command onboarding that stores a credential and verifies it end-to-end
keys— inspect a key's scope, rotate it, or revoke it- Privacy — the wire contract the SDK and CLI share