You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Extract the OS keyring into core/keyring_store (#215)
`core/config.py` sat at exactly the 500-line gate with zero headroom
(the next addition would break the max-file-length check). This splits
the OS keyring access — the one external secret dependency — onto its
natural seam.
## What changed
- **New `core/keyring_store.py`** — the single importer of `keyring`,
holding `KEYRING_SERVICE` plus `set_secret` / `get_secret` /
`restore_secret` / `delete_secret` / `usable`.
- **`core/config.py` (500 → 442)** — reads and writes secrets through
that wrapper and no longer imports `keyring` directly, so the documented
"secrets live in the keyring, not the dotfile" boundary is now
structural. `config.keyring_usable` stays as a thin delegator so the
auth-state facade (`doctor` / `login` and their tests) is unchanged. The
two `clear_*` paths collapse onto the shared `delete_secret`.
- Tests that reached into config's keyring internals repoint to
`keyring_store` (`config.keyring` → `keyring_store.keyring`,
`config.KEYRING_SERVICE` → `keyring_store.KEYRING_SERVICE`).
No behavior change. Full `scripts/check.sh` gate passes — 100% patch
coverage, mutation gate, CodeQL, import-linter contracts (the new module
lands in the `core` layer).
> Note: separated from #214 (eval/stream refactor, currently in the
merge queue) so it lands independently.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_018Bkx51XDcYBdnot1EF8h9U
---
_Generated by [Claude
Code](https://claude.ai/code/session_018Bkx51XDcYBdnot1EF8h9U)_
Co-authored-by: Claude <noreply@anthropic.com>
`choices`. Contract 4 also forbids `rich` here, so "no Rich below the UI
38
38
layer" is structural.
39
39
@@ -139,7 +139,7 @@ heavily-reworked commands with long bodies; small commands keep the inline
139
139
### Cross-cutting state (resolution order matters)
140
140
141
141
-**`app/context.py`** — `AppState` (profile, env) is attached to the Typer context in the root `@app.callback()`. `run_command` is the standard command wrapper.
142
-
-**`core/config.py`** — profiles persisted in `config.toml` (via `platformdirs`); the **API key lives only in the OS keyring** (`KEYRING_SERVICE = "assemblyai-cli"`), never in a dotfile. Key resolution order: `--api-key` flag (validation paths only) → `ASSEMBLYAI_API_KEY` env → keyring. **Run commands deliberately expose no `--api-key` flag** so keys can't leak into `ps`/shell history.
142
+
-**`core/config.py`** — profiles persisted in `config.toml` (via `platformdirs`); the **API key lives only in the OS keyring**, never in a dotfile. The keyring access itself is factored into **`core/keyring_store.py`** (the single importer of `keyring`, holding `KEYRING_SERVICE = "assemblyai-cli"` + `set_secret`/`get_secret`/`restore_secret`/`delete_secret`/`usable`), so the "secrets never touch the dotfile" split is structural; `config` reads/writes secrets through it and only `config.keyring_usable` re-surfaces the probe on the auth facade. Key resolution order: `--api-key` flag (validation paths only) → `ASSEMBLYAI_API_KEY` env → keyring. **Run commands deliberately expose no `--api-key` flag** so keys can't leak into `ps`/shell history.
143
143
-**`core/environments.py`** — a frozen `Environment` (api_base, streaming_host, llm_gateway_base, ams_base, stytch_*). `DEFAULT_ENV` is **`production`**; use `--sandbox` (or `--env sandbox000` / `AAI_ENV`) to target the sandbox. The active environment is a process-global set once at startup; precedence: `--env` → `AAI_ENV` → profile's stored env → default. A credential is only valid against the environment that minted it.
144
144
-**`core/client.py`** — thin wrappers over the `assemblyai` SDK (`transcribe`, `list_transcripts`, `stream_audio`, etc.). It normalizes SDK exceptions: auth failures become a single clean `auth_failure()``CLIError`; everything else becomes `APIError`. New SDK calls should follow this try/except shape.
145
145
-**`core/errors.py`** — the `CLIError` hierarchy (each with `error_type` + `exit_code`). `ui/output.py` emits errors to **stderr**; stdout stays clean for pipelines. `--json` switches to machine-readable output; it is never auto-enabled — `output.resolve_json()` deliberately keeps human text the default even when piped or agent-run.
0 commit comments