Repository navigation
0.5.0 (3/5): one credentials resolver and transport, release workflow, update check - #162
Closed
dg-coreylweathers wants to merge 8 commits into
Closed
dg-coreylweathers wants to merge 8 commits into
dg-coreylweathers wants to merge 8 commits into
Conversation
deepctl_core.credentials.resolve() decides the API key and project ID
for a run and says where each came from (key_source, profile_source,
untrusted_source). The AuthManager methods (get_api_key, get_project_id,
get_credential_source, is_authenticated, has_profile_credentials) answer
from it, once per run, and dg whoami takes its key source from it.
Precedence is 0.4.1's, unchanged. Because Config merges DEEPGRAM_API_KEY
into the active default profile, that is --api-key > keyring > env >
file for the default profile, and --api-key > keyring > file > env for
another profile.
User-visible changes:
- With no API key, commands return the coded auth_required error: a
coded result on stdout under a machine format and two stderr lines
(ERROR and HINT). The error text "DEEPGRAM_API_KEY is not set" is
unchanged; the old two-line "not set in the configuration file ... Run
deepctl login" message is replaced by the catalogue hint. When the
only key was in a user config file an AI agent session doesn't trust,
the error is E-KEY-UNTRUSTED with next_step null.
- A DEEPGRAM_API_KEY, DEEPGRAM_PROFILE, DEEPGRAM_PROJECT_ID or
DEEPGRAM_BASE_URL that still holds an unexpanded ${...} placeholder is
ignored, with one W-ENV-PLACEHOLDER warning naming the variable.
- Off a terminal (including MCP), a keyring read gets 2 s; when the
keyring doesn't answer, dg uses the config file or the environment
instead and prints one W-KEYRING-TIMEOUT warning. This covers dg
whoami and dg profiles too. With --api-key the keyring isn't read or
imported.
- "Using credentials from environment variables" now also appears when
DEEPGRAM_API_KEY is the key in use; "No project ID specified" appears
only for commands that need a project.
- Commands that use the key remove an expired credential's keyring
entry, as in 0.4.1, but only when the expiry comes from a trusted user
file. whoami, login's checks, is_authenticated() and
has_profile_credentials() stay read-only, as in 0.4.1.
- auth_invalid's details.key_source comes from the same resolution, so
the error path reads the keyring no second time.
- AuthManager builds its HTTP client on first use, and group commands
get a lazy client, so `dg debug <sub> --help` loads no httpx or
keyring.
The env-vs-stored mismatch check is implemented behind a constant that
stays off until login writes fingerprints.
deepctl_core.transport.Transport owns where API requests go and what
they carry. The SDK facade builds its client through it. REST and
WebSocket base URLs come only from check_credential_host (http -> ws,
https -> wss), and Authorization is attached only after that check.
User-visible changes:
- One redirect refusal for REST and WebSocket: a 3xx is
transport.RedirectRefused, the coded network_unavailable error
(reason "unexpected redirect") with 0.4.1's text, which names both
origins and says nothing was sent there (or no audio/text was sent,
for a socket). for_status() maps every 3xx to it, so the client's own
redirect check (_failed) and 0.4.1's separate ClickException go;
deepctl_core.websocket re-exports the error and stays the only
WebSocket open. Its message and exit_code read as before. Redirects
were already refused; neither the key nor an upload body reaches
another origin.
- A connection reset or closed after the request started (a read or
write error, or the server disconnecting without a response) is
network_unavailable with E-NETWORK-DROPPED ("dropped before the
response was complete"). A reset while connecting, DNS and TLS
failures keep E-NETWORK.
- DEEPGRAM_CLI_INSECURE now applies to the SDK's REST requests too, with
0.4.1's rule: only 1 or true count, only localhost, 127.0.0.1 and
[::1] skip certificate verification, every other host is verified
(with one W-INSECURE-REMOTE warning), and AI agent sessions ignore it.
The login client uses the same rule. WebSocket streaming, dg api and
debug probe always verify.
- A WebSocket upgrade refused with an HTTP status maps through the same
table as REST; DNS, connect and TLS failures map to
network_unavailable, and an open that times out maps to timeout.
- The shared client keeps no cookies.
A backend seam (RestBackend by default) is where #142's SageMaker
backend plugs in. shared_client, map_exception and raise_for stay
module-level with unchanged signatures. dg whoami takes its default host
from hosts.DEFAULT_BASE_URL.
dg api takes its base URL from the Transport's host check and sends its
request on the process's client, so a dg api call opens one connection
like every other command. It still verifies TLS for every host, as in
0.4.1: when a person has DEEPGRAM_CLI_INSECURE set, dg api uses a
separate always-verified client, and a TLS failure on a loopback host
then says "DEEPGRAM_CLI_INSECURE doesn't apply to dg api, which always
verifies TLS certificates." It asks for no redirects, and keeps 0.4.1's
host scoping for the key.
debug probe's upstream is the Transport's checked WebSocket base instead of a hard-coded api.deepgram.com, so it follows the profile's base URL and a refused host never gets the key. The Authorization header comes from the Transport after its host check. User-visible changes: - An upstream that rejects the key at the upgrade (401) stops the proxy with the coded error, exit 1, because every connection would fail the same way. Other open streams are closed first, so it exits within seconds. - Any other refused upgrade (including 403, which is about one request's scope), an unreachable upstream, or an open that takes over 10 s fails only that client's connection, with the coded error on stderr; the proxy keeps serving. - The proxy's status lines go to stderr, so stdout holds only the result. debug network's default endpoint, its latency row and its proxy advice name the host from hosts.DEFAULT_BASE_URL, through the Transport. It still sends no credentials. debug toolkit's examples build their streaming URL from the same constant. The one api.deepgram.com left in a .py source outside hosts.py is the foreign-host hint in messages.py, which a person copies; it's marked 'host-literal: user text'.
release.yml installs the built wheels and the published release and runs deepctl in both. test.yml runs deepctl --version outside pytest, where conftest.py's opt-out doesn't reach. Turn telemetry off for all of them, and point test.yml's run at a dead loopback endpoint.
publish-on-release.yml rebuilt and published a tag when someone published a GitHub release by hand. Replace it with a tag input on release.yml's workflow_dispatch, so every PyPI upload comes from one workflow file. A dispatch with a tag skips release-please, checks that the tag is a vX.Y.Z published (not draft) GitHub release in the history of main, release/* or next/*, builds that tag, and refuses to go on unless the build holds the deepctl wheel for the tag's version. A release that release-please cuts builds the tag it just created, not the branch the run started on, and goes through the same version check. The build and install-test jobs now read the repository only and keep no credentials in the checkout. The publish job no longer reads a long-lived API token. It runs in the pypi environment with id-token: write, and each PyPI project accepts it as a trusted publisher bound to release.yml and that environment. Each upload sends every sub-package first and the root package last, in a second step that runs only if the first succeeded. The root pins every sub-package ==, so it is never on the index without them. A tag dispatch can also pick index: testpypi. That uploads the same build to TestPyPI from a separate testpypi environment, so the setup can be tried end to end before the first real OIDC release, and it advertises nothing.
The update, plugin-update and skills checks no longer join their threads at exit. A notice prints only if its check already finished; otherwise it is dropped and the daemon thread ends with the process. The checks now skip CI, agent and other non-interactive runs, dg mcp, a stderr that isn't a terminal, and DEEPCTL_NO_UPDATE_CHECK. httpx loads only inside the check thread. Each check records when it ran in the cache directory (update-check.json and plugin-update-check.json; 0600 files, replaced atomically). The record is written just before the request goes out, so a failed or unfinished request waits a full interval before the next attempt, and a run that ends earlier doesn't use the interval up. When no check is due and the saved answer names a newer version, the notice prints from it, at most once per interval (notified_at). The cache directory must be private: one this user owns with group or other access (a plain mkdir under umask 002) is tightened to 0700. If it is owned by someone else or can't be tightened, the checks don't run, with no network and no state. dg update keeps its last answer there too, and no longer writes the installation method or path. The old config keys are still read while no cache file exists, and are never written again. It shows the installation method by its plain name.
Install detection now looks at sys.prefix, not the interpreter it links to. A pipx or uv tool environment on Homebrew's Python resolves its interpreter into the Cellar, which used to read as a Homebrew install and send dg update to brew upgrade. - Homebrew: sys.prefix under Cellar/deepgram/ only. - uvx: sys.prefix in uv's cache under archive-v*, honoring UV_CACHE_DIR. The uv internal interpreter variable alone no longer counts, since uv run sets it for project environments too. - pipx and pipx run: sys.prefix under pipx's venvs or .cache; a PIPX_HOME variable alone no longer counts. - uv tool: sys.prefix directly under the uv tool directory; dg update runs uv tool install deepctl@latest, since uv tool upgrade leaves a pinned install where it is. - npm: DEEPCTL_LAUNCHER=npm or npx; dg update prints npm i -g @deepgram/cli@latest. The startup check skips npx runs as it does uvx and pipx run, and the uvx advice now covers a uv tool install shadowing uvx. dg update --dry-run prints the detected method and the command that updates it, without checking or installing.
When neither PyPI nor, for Homebrew, the GitHub release answers, dg update used to report the running version as the latest. It now exits 1 with update_check_failed (E-UPDATE-CHECK, "Couldn't check for updates: couldn't reach pypi.org.") through the coded error path. On stderr, agent mode shows ERROR: and HINT: lines; human mode shows the error after ✗, followed by the hint. Under -o json, stdout carries the coded payload with details.host. The failed attempt is recorded in the update cache file like any other.
dg-coreylweathers
requested review from
GregHolmes and
deepgram-kiley
as code owners
November 7, 2026 02:35
Contributor
Author
|
Consolidated into #165, which has all 74 commits of 0.5.0 in one PR. |
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.
Part 3 of 5 of the 0.5.0 release, stacked on part 2. Review in order. Merge only after the PR below it has merged, and only after 0.4.1 is released.
Credentials are resolved in one place and every API request goes through one transport, so the host checks from 0.4.1 apply everywhere. Releases publish from
release.ymlwith trusted publishing. The update check never waits, detects pipx, uv and npm installs correctly, and reports an unreachable index as an error.Commits
Notes
🤖 Generated with Claude Code