Repository navigation
Release 0.4.1: prevent potential logging of API keys, and other fixes - #159
Conversation
GregHolmes
left a comment
There was a problem hiding this comment.
Request changes
[B1] OpenCode sessions are not recognized as AI-agent sessions. With OPENCODE=1, an environment-defined DEEPGRAM_BASE_URL=https://evil.example passes the new host gate and can receive the user's API key.
[B2] A rejected symlinked default config still supplies its API key in an AI session. The trust-root check rejects the link, but the fallback rereads it through the symlink and treats a current-user 0600 target as credential-trusted.
I also found a whitespace-check follow-up in the new CRLF config.py lines.
Verified: the PR CI is green and 2,750 focused local security tests pass, but neither blocker is covered by those tests.
3f76185 to
e5aed1f
Compare
Bring cli#159 into 0.5.0. 0.5.0 keeps its agents table: output.py takes AI_AGENT_ENV_MARKERS from agents.HARD_ENV_VARS, which already covers AGENT and CURSOR_EXTENSION_HOST_ROLE. OPENCODE_CLIENT becomes a hard signal there (any value), as #159 requires for the base-URL key gate.
|
Both blockers are in e5aed1f. B1: OPENCODE and OPENCODE_CLIENT are agent markers, with a test covering evil.example. B2: a refused trust root is never reread, with tests for symlinked file, folder and hard link. Whitespace: cr-at-eol in .gitattributes, git diff --check is clean. Re-requesting review. |
- Turn telemetry off for every test from the root conftest. - Point the reporting endpoint at a closed local port, so a test that runs main() can never reach a real endpoint. - Telemetry's own tests clear the switch to cover the default.
…to a command dg turns `dg debug-audio` into `dg debug audio`. It did that to every argument that began with a command group's name and a hyphen, wherever it appeared, so `dg listen debug-call.wav` read the file as `debug call.wav`. A root flag such as --quiet also took the next word as its value, so `dg --quiet debug-audio` wasn't expanded. - Expand only the command name, after the root options, and keep every argument after it as typed. - Read root options the way click does: flags take no value, --opt=value and -Xvalue carry theirs, and a short cluster such as -qv or -qpprod is read letter by letter. - Stop at --. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Send no telemetry. 0.4.1 has no built-in destination: the Sentry DSN that 0.2.21 to 0.4.0 used is removed, so dg sends no error reports, sessions, traces, profiles or logs, whether a command succeeds or fails. Without a destination, no error hooks are installed and the reporting SDK is never imported. - Accept DEEPCTL_TELEMETRY_DSN only for a loopback address, for tests. Any other value is ignored, and the README now says so. - Keep the error-report code for a later opt-in, reachable only through that override: one report per process, and only for an error a command didn't raise on purpose. Each report is built from an allowlist (command path, flag names, versions, OS and frame locations), with no values and no local variables. The SDK loads only when there is something to send, with its optional features off, no proxy and a short time limit. The empty proxy settings keep a report to a loopback address from leaving the machine through a proxy set in the environment or the system settings. - A command's own ClickException now passes through BaseCommand as a message to the user. A ClickException or UsageError raised inside a command is no longer printed after a "Command failed:" line. Exit codes are unchanged. - DEEPCTL_TELEMETRY_DISABLED and telemetry.enabled are still accepted, and the new --no-telemetry flag is accepted too, but none of them has an effect in this version. - DEEPCTL_TELEMETRY_DISABLED turns telemetry off for any non-empty value except 0, false, no and off. Before, only 1, true and yes did. - Read the telemetry setting from the user config only (the default file or a --config file), and treat a config that can't be read as opted out. A project deepgram.yaml no longer turns telemetry on or off, as it could in 0.4.0. - Read root options the way click does, so root --help honors --no-telemetry and --config in any order. - dg mcp hands a proxy failure to the same code, without writing to stdout. - End --help with a line that says this version of dg sends no telemetry. The README's telemetry section says the same, says that dg still contacts the Deepgram API and pypi.org, says the three settings above are accepted but have no effect, and says DEEPCTL_TELEMETRY_DSN is ignored unless it points at the local machine. Golden files check both. - Describe deepctl-telemetry as error reporting that sends no telemetry in this version. - Raise deepctl-telemetry's floors to sentry-sdk>=2.35.0,<3 and deepctl-core>=0.3.0. sentry-sdk stays a dependency only for a later opt-in; 0.4.1 never imports it unless a loopback test DSN is set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Install the rich traceback handler with show_locals=False. It was on, so a crash could print local values, such as an API key. - Keep the rest of the traceback as it was, on stderr. - Test that a local's value doesn't appear in the output.
- Save only the user's own settings, never values that came from the project file or the environment. - Write the file owner-only and atomically, and restrict a readable file that holds a key. - Never overwrite a config that couldn't be read, and lock so that concurrent saves don't lose each other's changes. - Keep the sections a save doesn't manage, such as telemetry.enabled: false. In 0.4.0 every save dropped the telemetry section, so dg login, dg projects, dg update and the update version-check cache erased that opt-out. - Don't read a --config path that isn't a regular file, such as a FIFO, <(...) or /dev/stdin. dg warns and continues without it. - Refuse oversized or deeply nested YAML at load, and never quote the file's content in an error. - dg --record-install-method exits 1 when it can't write the config file, so the install script doesn't take a failed write for success. - Print config warnings on stderr even under --quiet, through the new print_security_warning. - Generate the 64 KiB config test files at test time; a test pins their sha256.
…nfig file can set A deepgram.yaml in the current directory, or a --config file from a cloned repository, could set the API key, the API host, the ffprobe path and other settings that decide where your key goes or what dg runs. - Read only output settings and a regional Deepgram base_url from a project deepgram.yaml. Its api_key, project_id and profile selection, its api, tools, plugins, update and telemetry sections, and any other base_url are ignored, with one warning per key that names the key, never its value. Telemetry settings now come only from the user config, the environment and --no-telemetry. - A --config file other than the default config.yaml, even one in the same folder, can still hold credentials and profiles, but its api, tools, plugins and update sections, and any non-regional base_url, are ignored with a warning. - In an AI agent session, the user config can set those sections and a non-regional base_url only when it is the file at its standard location under the real home directory, reached without a symlink, with one hard link, owned by the user and not writable by the group or others. Otherwise, including a file found through HOME or XDG_CONFIG_HOME, dg handles those settings as it does for a --config file. A file another user owns or the group or others can write also loses its API keys and dg_token_expires_at, and dg doesn't save over it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An AI coding agent that runs pytest exports markers such as CLAUDECODE or AI_AGENT, which put dg into its agent trust mode and failed tests that expect a person at the keyboard. The root conftest now removes those markers for every test. Tests that cover agent mode set the markers, or patch is_ai_agent, themselves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
API keys and request data go only to the host the user chose, and never follow a redirect somewhere else. - Send credentials only to a regional Deepgram API host, a host in api.allowed_hosts, or DEEPGRAM_BASE_URL. Refuse other hosts before any request is made. - Don't trust DEEPGRAM_BASE_URL in an AI agent session: there it must name a regional host or one in api.allowed_hosts. - Ignore DEEPGRAM_CLI_INSECURE in an AI agent session, with a warning, so TLS verification stays on. - Apply the same check in dg login, dg mcp and dg listen. - dg login checks a DEEPGRAM_CLI_BASE_URL login host the same way, against the Deepgram identity host. - dg login no longer saves --base-url or DEEPGRAM_BASE_URL to the profile. It prints that the URL was used for this login only. The new dg login --save-base-url saves it after a person confirms at a terminal, and for a non-Deepgram host also adds the host to api.allowed_hosts. - In dg api, attach the key to an absolute URL only for a trusted host, and don't follow redirects. A request to another host is sent without the key, with a warning. - Add --allow-foreign-host=<host> to dg api. It sends the key to that host only when the value matches the request URL's host and the URL is https. It is refused in an AI agent session. - Resolve a relative dg api endpoint, such as /v1/projects, against the profile's base URL instead of always https://api.deepgram.com. The base URL passes the same host check first. - Build the SDK client with follow_redirects=False, so REST calls (dg listen and dg transcribe on files, dg speak with Aura, dg read and the rest) stop at a 3xx instead of sending the audio or text on to another host. The error names both origins. - Open every WebSocket (live dg listen from the microphone or stdin, and dg speak with Flux) through one connect in deepctl-core that doesn't follow a redirect on the upgrade, to the same origin or another. The command fails with exit 1 before any audio or text is sent, with wording that matches the dg api redirect note. Without it, the key, audio or text could follow a redirect to another host. - dg speak with Flux now opens /v2/speak itself, with the SDK's headers, instead of through the SDK's connect, so it gets the same refusal. - dg debug probe stops its upstream connect at a redirect too. aiohttp has no switch for this on ws_connect, so a trace hook ends it after the 3xx and before any request to the new location. Its aiohttp floor moves to 3.11, the first release with the ClientWSTimeout the probe already uses; below that it couldn't connect at all. - Raise the websockets floor for deepctl-cmd-listen from 11.0 to 13.1. Live listen now uses the asyncio client, which 13.0 added (on 11 and 12 it couldn't connect at all), and 13.1 is the first release whose client handles a 3xx the same way as every later one. - Show a regional host in the --base-url help and say other hosts must be listed in api.allowed_hosts. The insecure-URL errors name every loopback host plain http may use (localhost, 127.0.0.0/8 and [::1]). - Document which hosts get your API key, api.allowed_hosts and what a project deepgram.yaml can't set, in the README and llms-full.txt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The agent-session trust-root tests and the broken-config CLI tests ran only on Linux because they built ~/.config paths. They now put the user config where platformdirs does on each platform (Library/Application Support on macOS), build expected messages with display_path, and count open descriptors in /dev/fd on macOS. They still skip on Windows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…a relative PATH entry dg looked ffprobe up with shutil.which, which follows relative PATH entries such as "." (and on Windows searches the current directory first), so it could run an ffprobe from the current directory, such as one shipped in a cloned repository. - Find ffprobe on PATH from absolute entries only, skipping empty and relative entries and the current directory, on every OS. - Use a configured tools.ffprobe_path only when it is absolute. dg ffprobe --path stores the absolute form of the path it's given. - dg debug audio now honors tools.ffprobe_path and runs ffprobe by its absolute path. - dg ffprobe status and reset find ffprobe on PATH the same way, so neither reports an ffprobe from the current directory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Refuse dg ffprobe --path and dg plugin install, update and remove in an AI agent session, since they change what later commands run. - Scripts and CI without an agent marker still work as before. - llms-full.txt and llms.txt say what an AI agent session is and what dg refuses or ignores in one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- dg update runs brew upgrade deepgram, and both upgrade hints name the deepgram formula, after brew update. - The formula is published as deepgram, so `brew upgrade deepctl` failed for Homebrew users. - Update the installation and update command tests to match. - Name it in the llms-full.txt install list too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Reserve built-in command names for their own modules. A plugin that
claims one isn't registered, and dg names its package in a warning.
- Require plugin command names to match ^[a-z][a-z0-9-]{0,63}$, so a
plugin can't use a name that looks like a built-in.
- Let plugins.allow_override in the user config name a plugin that may
replace a built-in. It prints a notice each time it runs.
- List only plugins Deepgram publishes in dg plugin search.
- In an AI agent session, use the plugin environment that dg plugin
install manages only from the real home directory, never one found
through HOME, and skip it when it is reached through a symlink or
lies inside a working directory other than the home directory.
Plugins installed in dg's own Python environment still load.
- Refuse a plugin entry point named like a built-in or reserved
command by that name, before its module is imported, unless
plugins.allow_override lists it. A subplugin listed as "debug audio"
passes. A plugin refused for the name its class sets, or for an
invalid name, is still imported and then dropped.
- Show plugin names and metadata as plain text, with control
characters replaced and Rich markup escaped (safe_text.py).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Check Host and Origin before the WebSocket handshake and before any upstream connection is opened with the API key. - Require a Host header of 127.0.0.1:PORT or localhost:PORT, where PORT is the probe's own port. - Accept no Origin (native clients) or a page on localhost, 127.0.0.1 or [::1]. Refuse everything else with 403. - Native clients that send one of those Host headers, and local pages, work as before, with no new options.
…tions dg debug audio and the shared ffprobe helper pass ffprobe its input as the absolute path with a file: prefix, so a file name that starts with - is no longer read as an ffprobe option. ffprobe reports the name it was given, so dg debug audio -o json or -o yaml now shows audio_info.format.filename with the file: prefix. --ffprobe-args is now split with shlex, like a POSIX shell on every OS, so a quoted value stays one argument. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Other values keep TLS verification on, and so do hosts other than localhost, 127.0.0.1 and [::1]. When the variable is set and a request goes to another host, dg warns once for that host that its certificate is still verified. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dg mcp --transport sse now exits with an error before reading credentials or opening a port. streamable-http was never served: 0.4.0 already rejected it, and it now gets the same error. Remove --transport from your editor's MCP config to use stdio. python -m deepctl_cmd_mcp refuses both the same way, matched case-insensitively, instead of failing on streamable-http with a usage error. The READMEs, llms-full.txt and the landing page drop the SSE examples. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion dg init ran git clone with the template URL as its first argument, so a URL that starts with - was read as a git option. dg now passes the URL after --, and git takes it as the repository. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dg init writes your API key to .env in the new project. - Create .env owner-only (0600) at any umask, and narrow an existing .env to 0600 when dg writes the key or finds it already set. - Add .env to .gitignore unless git already ignores it, asking git check-ignore when it can. - Refuse to write through a .env or .gitignore symlink, since the cloned template controls its links. - Warn when the template already tracks .env, since ignoring a tracked file doesn't keep the key out of a commit, and suggest git rm --cached .env. - Report a .env that can't be written as a warning, as for an auth failure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When install.sh or install.ps1 has to install uv (no uv, pipx or pip found), it now installs uv 0.12.24 instead of the latest release, and checks its installer against the sha256 pinned for that release before running it. The new DEEPCTL_UV_INSTALLER_URL variable points either script at a mirror of the installer, which must serve the same pinned file. A forced reinstall through pip now runs pip with -U --force-reinstall. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Create ~/.deepctl/plugins and the plugin venv as 0700 at any umask, and narrow folders an earlier version left open on the next install. - In an AI agent session, load no plugins from folders that another user owns or others can write, and warn once. - Outside an agent session, warn and still load the plugins.
…ol is called dg mcp ran deepgram-mcp's proxy, which connected to Deepgram's documentation search before answering initialize and passed the remote's tool list through. The stdio server now lives in deepctl_cmd_mcp.proxy, vendored from deepgram-mcp 0.1.1 (MIT): - initialize and tools/list come from fixed local text; nothing connects until a tools/call, and each call opens its own session; - a refused key, no key or a remote that is down is an isError result for that call, and the server keeps running; - only search_deepgram_knowledge_sources is forwarded; - the HTTP client follows no redirects (same-origin ones included), keeps no cookies and verifies TLS; - stdout carries only JSON-RPC: fd 1 points at stderr while the server runs. dg mcp no longer checks the key at startup; it is read on each call. The deepgram-mcp dependency is gone, so a fresh install has no deepgram-mcp console script. mcp's floor rises to 1.22, the oldest release that runs this server with current pydantic and takes an httpx client factory. uv.lock drops deepgram-mcp. dg mcp --help, its messages, the package description and the docs now describe an MCP server that gives your AI editor Deepgram documentation search over stdio. The editor setup examples pass DEEPGRAM_API_KEY through the env block instead of writing the key into .mcp.json. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The web page's plugin card ran dg plugin install deepctl-cmd-studio, a package that isn't on PyPI, and called it a community plugin. It now installs deepctl-plugin-example, the example plugin Deepgram publishes, under the title "Install a plugin". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README said CLI flags beat environment variables, which beat the profile config, which beat the project config, for every setting. - A project deepgram.yaml is read after the user config, so the settings it may still set (output.* and a regional base_url) win over the user config. The order is now CLI flags, environment variables, the project deepgram.yaml, then the user config. - The API key has its own order: --api-key, the key dg login saved in the system keyring, DEEPGRAM_API_KEY, then an api_key in the config file. The README and llms-full.txt now say so. - llms-full.txt no longer names a version. It said 0.2.4, and nothing updates it on release. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…command The plugin example README says a plugin whose entry point is named like a built-in command, such as login, is skipped before it's imported, unless plugins.allow_override in the user config lists it. It links to the main README's Plugins section by its GitHub URL, so the link also works on the package's PyPI page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Windows, platformdirs ignores HOME and XDG_CONFIG_HOME, so the opt-out tests read and wrote the real %LOCALAPPDATA% config. Point it at tmp_path with WIN_PD_OVERRIDE_LOCAL_APPDATA, in-process and in the probe subprocesses, and give the probes a tmp USERPROFILE. Stub the update and skills startup checks in telemetry_on, as test_plugin_override_notice.py does, so the tests stop writing the real ~/.cache/deepctl. Use os.devnull instead of "/dev/null". On Windows, Click rejected "/dev/null" as a missing path, so the [dev-null] case passed without running. Skip the /dev/stdin and bash process-substitution cases on Windows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Windows the agent trust root comes from SHGetFolderPathW, not _real_home, so the TestAllowOverride tests made folders in, and could write allow_override into, the real %LOCALAPPDATA% config. A new _set_real_home helper patches both, so no test reaches the real home. Also on Windows: - build the sidecar venv with the Lib/site-packages layout the loader expects - build POSIX-looking test paths with os.path so the expected hint text matches - when the account can't create symlinks (WinError 1314), fall back to a directory junction instead of skipping. A non-admin can make a junction, and the agent venv checks refuse it too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Point HOME and USERPROFILE at tmp_path in the base URL tests. On Windows tmp_path sits under the real home, so messages showed "~\..." instead of the full path the tests expect. - Write the user config text test's file as bytes, so Windows doesn't turn \n into \r\n. - Run the concurrent-save child with spawn where fork doesn't exist. POSIX still uses fork. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Build absolute paths with os.path.abspath. Python 3.13+ on Windows doesn't count "/opt/..." as absolute. - Expect "file:" plus the native path. - On Windows, give the fake ffprobe an .exe name and set PATHEXT, so the PATH lookup tests run there. Before, the current-directory and relative-entry checks passed whether or not the code skipped those entries. - Read the file_info timestamp in UTC so the test passes in any time zone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CodeQL's YAML extractor recurses once per nesting level and ran out of stack on block_dash_deep.yaml, which failed the Analyze (python) job. Build that file and the other four nested thousands deep (deep10k_flow_map, deep10k_flow_seq, quote_bypass_dq, quote_bypass_sq) at test time, like the three 64 KiB bombs, and check each against the sha256 of the committed original. The tests still run every bomb in every placement. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Windows a redirected or piped stdout uses the ANSI code page (cp1252, say). The --help banner's block art isn't in it, so `dg --help > file` and `dg --help | more` stopped with "'charmap' codec can't encode". A stdout or stderr that isn't UTF-8, and whose error handler raises on such characters (strict, or surrogateescape under a C locale), now writes them as backslash escapes. The encoding itself is kept, so a real console keeps the one Python chose, and a UTF-8 stream is untouched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…crashing On macOS, stat() on a symlink that is being replaced can fail with EINVAL, and on Python 3.13 and earlier Path.exists() passes that error on, so dg stopped with an OSError traceback before it read the config. A folder that can't be searched did the same. When the lookup fails, dg now tries to read the file anyway: the trust checks run on the file it opens, and a read that fails is reported like any other unreadable config, which is never saved over. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…trust root OpenCode sessions (OPENCODE, OPENCODE_CLIENT) weren't recognised as AI agent sessions, so an environment DEEPGRAM_BASE_URL such as https://evil.example passed the host gate and could receive the user's API key. Add them, along with the other agent-set markers: CLAUDE_CODE, CLAUDE_CODE_IS_COWORK, CODEX_CI, CODEX_THREAD_ID, COPILOT_CLI, GOOSE_TERMINAL, AUGMENT_AGENT, ANTIGRAVITY_AGENT, AGENT=amp|goose and CURSOR_EXTENSION_HOST_ROLE=agent-exec. Variables a person's own shell sets too (CURSOR_TRACE_ID, REPL_ID) stay out. When the trust-root check refused the default config, the fallback read it again through its path and kept a current-user 0600 target's API key. Now a refused trust root is never read again: a file refused before it was opened (a symlink on the way) is not used at all and saves through it are refused, and one refused after the checked open (hard links, owner, writable) is used from that open with its credentials stripped. Mark the CRLF files this release touches whitespace=cr-at-eol so git diff --check passes without rewriting their line endings.
e5aed1f to
c7d9e35
Compare
Bring the 0.4.1 release into 0.5.0: the @deepgram/cli npm launcher (#146), the Homebrew bump that merges itself (#135), and the 0.4.1 fixes (#159). How the two sides fit: - src/deepctl/main.py is 0.5.0's. Its main() reads and removes DEEPCTL_LAUNCHER before deepctl_core is imported and keeps the value in LAUNCHER / LAUNCHER_CAPTURED, which deepctl_core._launcher (from 0.4.1) and deepctl_cmd_update.channel both read. Crash tracebacks still never show frame locals (install(show_locals=False), now installed lazily). - deepctl-cmd-update is 0.5.0's, which already detects npm and npx installs; 0.4.1's duplicate detection there is dropped. The plugin command keeps 0.4.1's isolated venv for npm installs, and the skills prompt keeps 0.4.1's rule that an npm launch is not one-shot. - Versions are lockstep at 0.4.1 (what main last released); the plugin example stays at its released 0.1.13. release-please now also moves npm/package.json with the root version. - scripts/check_release_bumps.py and its release-bump-gate are removed: with lockstep versions every release bumps every package, so no package's code can reach PyPI under an already-published version. The build job checks lockstep versions once more before publishing. - release.yml keeps 0.5.0's re-pin jobs, trusted publishing and tag recovery, and adds 0.4.1's pinned build tools, fresh-venv PyPI check, Homebrew auto-merge and publish-npm. test.yml adds the three npm launcher jobs to All checks. - conftest.py forgets the launcher main() captured between tests, so a test that ran main() no longer decides what later launcher tests see.
#165) The 0.5.0 release, based on 0.4.1 (#159). Review after 0.4.1, and merge only after 0.4.1 is released. 88 commits, on 0.4.1's head `55d3e980`. ### Startup, lockstep versions and dependency floors (1–10) Startup work: `dg --version` and `dg mcp` start without importing click, commands load from a generated manifest, plugin discovery and help are cached, and config loads without pydantic on the help path. Also adds the competitive benchmark harness, versions every first-party package in lockstep, and raises the dependency floors (click 8.2, caps on runtime majors). <details><summary>Commits</summary> - a6a212a feat(bench): add competitive benchmark harness with gates - 27f5859 perf(cli): start dg without importing click for --version and mcp - 1ccf4a0 perf(core): import deepctl_core names lazily - a3c698a build(release): version every first-party package in lockstep - 92d0130 build(deps): require click 8.2 and cap majors of runtime deps - b7fd286 feat(cli): load commands from a generated manifest - 745e258 perf(cli): cache plugin discovery and help output - edab526 refactor(commands): keep network modules out of command import time - 394f6c2 perf(core): load config without pydantic on the help path - 680e5fb perf(cli): render subcommand help from specs </details> ### Login for agents, and listen, speak and debug streaming (11–28) `dg login` accepts real API keys and asks for `--yes` instead of cancelling, and agents can finish device login with `--complete`. Live `dg listen` finishes cleanly on Ctrl-C and end of input with bounded memory; `dg speak` streams audio as it arrives and gains `--out-file` (`-o/--output` for the audio file is deprecated). Agent mode drops status chrome from stderr. `dg debug network` sends no stored credentials, debug browser and probe run on websockets, and numpy, ffmpeg-python and requests are dropped. <details><summary>Commits</summary> - a5052b6 fix(login): accept real API keys and ask for --yes instead of cancelling - 582c3f0 feat(login): let agents finish device login with --complete - e80a8a3 perf(core): drop the pre-flight key check and reuse one HTTP client - b0e0780 fix(output): drop status chrome from stderr in agent mode - c58d7b5 fix(listen): finish live streams cleanly on Ctrl-C and end of input - 9ddea92 perf(listen): bound live-stream memory and frame latency - b80e66f fix(listen): keep machine output to the final result - ee0b1cc perf(speak): stream audio to pipes and players as it arrives - 361bf3e feat(speak): add --out-file and deprecate -o/--output for the audio file - 6244ab9 fix(speak): don't read stdin or write audio to a pipe in agent mode - c915891 fix(listen): report a failed stdout write as an output error - 1550284 fix(cli): treat Ctrl-C while the CLI is loading as cancelled - 8cc787e perf(listen): stream prerecorded uploads from disk - ecba215 feat(listen): add --timeout for prerecorded transcription - 4491eb1 perf(listen): check URL sources without fetching them - 61eaa62 fix(debug): send no stored credentials from debug network - 960b0c3 build(deps): drop numpy, ffmpeg-python and requests from commands - 9466058 refactor(debug): run debug browser and probe on websockets </details> ### Credentials resolver, transport, release workflow and update check (29–36) 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.yml` with trusted publishing. The update check never waits, detects pipx, uv and npm installs correctly, and reports an unreachable index as an error. <details><summary>Commits</summary> - b9d015a feat(core): resolve credentials in one place - 1a409f2 fix(core): send API requests through one transport - 85dedaf fix(debug): take the probe's and network's host from the transport - 327547e ci: keep the CLI smoke runs from sending telemetry - 4d194c0 ci: publish every release from release.yml with trusted publishing - 9dc82c1 fix(update): never wait for the update check - 9c18f1b fix(update): detect pipx, uv and npm installs correctly - fc32cc3 fix(update): report an unreachable index as an error </details> ### Telemetry switches, dg init, JSON output and listen output formats (37–49) Telemetry switches (`DO_NOT_TRACK`, `CI`, `DEEPCTL_TELEMETRY=log|off`) are accepted; this version still sends no telemetry. `dg init` clones only Deepgram templates, keeps repository settings from running programs, can resume, and needs `--yes` for setup steps outside a terminal. JSON output off a terminal skips rich. `dg listen` prints the server body for `-o json`, adds `-o text`, and saves large results to a file in agent sessions. <details><summary>Commits</summary> - a3c4a4a feat(telemetry): honor DO_NOT_TRACK and CI - 6096c73 perf(telemetry): skip the flush when no report was queued - f76f032 feat(telemetry): add DEEPCTL_TELEMETRY=log and =off - 707672e docs(telemetry): publish the report's field schema - 8823fb0 fix(init): clone only Deepgram templates, with plain names - 4a762a2 fix(init): keep repository settings from running programs - bbaa6ba feat(init): resume a rerun in a clone dg init recorded - a4eefcb feat(init): setup steps need --yes outside a terminal - 5e85c87 perf(output): write JSON off a terminal without rich - dd53c99 test(transport): close the shared client after each test - 1034dba feat(listen): print the server's body for prerecorded -o json - 3aa3436 feat(listen): add -o text for the transcript alone - 39b2847 feat(listen): save large results to a file in agent sessions </details> ### dg mcp, agent mode, CLI and config fixes (50–73) `dg mcp` shares one proxy and tool table and keeps stdout for JSON-RPC from the start. AI agents are detected from one documented table, and `dg help agent-mode` explains agent mode. Warnings stay off stdout, config fixes (XDG on macOS, locked config files), Flux model naming, per-OS stdin and microphone help, and coded messages for cancel and unexpected errors. Ends with review fix-ups (a plugin named like a built-in alias is refused before import, `dg login --complete` checks the base URL, test and README fixes). <details><summary>Commits</summary> - 0a33a42 refactor(mcp): share one proxy and tool table for every dg mcp path - 9b32d50 fix(mcp): keep stdout for JSON-RPC from the start of dg mcp - 4eee896 docs(mcp): describe dg mcp as the documentation search it is - eee53c1 feat(core): detect AI agents from one documented table - 6a74a66 feat(help): explain agent mode with dg help agent-mode - aefb1ba fix(cli): keep warnings and notices off stdout - b7d589d style: say what each check does in config and skills comments - c1dc7e9 fix(cli): escape characters the output encoding can't write - 4318989 fix(login): show a profile's config-file key in profiles --list - 24c31b1 fix(config): count ~/.config on macOS when XDG_CONFIG_HOME names it - ac2d323 fix(config): wait for a locked config file once per run - 337ba3c fix(listen,speak): name Flux models, not API versions - 37e2414 fix(listen): give each OS its own stdin and microphone help - b9048ef test: give timing checks room on a busy host - 26a7890 fix(cli): use coded messages for cancel and unexpected errors - 7c415cb test(debug-probe): run the ignored-SIGINT check only on Linux - bf22611 fix(plugin): refuse a plugin named like a built-in alias before importing it - 020cec3 fix(login): check the base URL before dg login --complete saves a key - 085b54c test: merge the two fixtures that clear AI agent markers - d19dad9 docs: compare with 0.4.1 in comments, the release before 0.5.0 - 62fe497 build: regenerate the stale package READMEs and run the generator with uv - b2e1889 docs(telemetry): say this version sends no telemetry - 659e71b test(cli): patch both built-in tables in the missing-package test - c5511b2 test(mcp): let the docs lint pass the entry point's transport refusal </details> ### Review and CI fixes, and the release marker (74–88) Fixes from the review and from this PR's first CI run. Windows: the pending device login and `dg init`'s records are saved by path where a directory can't be opened; `--out-file` and `-o` work without `os.fchmod`, write in binary mode, and don't take a pipe or console for another file; the tests now collect and run there. macOS: the agent trust root follows `~/.config` only where platformdirs does, and the terminal, config path, socket and signal tests pass. Tests that signal their own process run in a child pytest process. The help cache key records the telemetry sink, an agent's state directory is under the account's real home, an update check that didn't finish is retried after an hour, `dg mcp` needs mcp 1.23.1, and the tests run against installed packages at their floors. Ends with `chore: release 0.5.0`, which carries `Release-As: 0.5.0`. <details><summary>Commits</summary> - f8467e8 fix(login): save the pending login and init's records on Windows - df1505f fix(speak): write --out-file and -o files correctly on Windows - 9935ef2 fix(speak): don't take a Windows pipe or console for another file - 6930c53 test: let the 0.5.0 tests run on Windows - 63762d0 fix(config): on macOS, follow ~/.config only where platformdirs does - df1e7da test: make the terminal, config path and socket tests pass on macOS - e880751 test(speak): send each test signal to one thread, in a child process - fc69d48 build(deps): require mcp 1.23.1 or later for dg mcp - 5217565 test: let the unit tests run against installed packages at their floors - ea7c121 fix(cli): key cached root help on the telemetry sink setting - 4ebe797 fix(init): keep an agent's resume records and logins under the real home - dfc9e9b fix(update): retry an update check that didn't finish after an hour - d8f8096 test(debug-probe): check the Origin refusal on a running proxy - cedf89c test(cli): run 0.4.1's output encoding checks on 0.5.0 - 2e97002 chore: release 0.5.0 </details> ### Upgrade notes - **Telemetry:** none is sent, as in 0.4.1. `DO_NOT_TRACK`, `CI` and `DEEPCTL_TELEMETRY=log|off` are accepted, and only affect a local loopback sink set with `DEEPCTL_TELEMETRY_DSN`, which is for tests. `--no-telemetry` is still accepted and documented as doing nothing. - **Versions:** every package moves to 0.5.0 together; `deepctl-telemetry`, for example, goes from 0.0.7 to 0.5.0. Each package pins `deepctl==0.5.0`. Upgrading one `deepctl-cmd-*` now upgrades the whole set, or fails to resolve, and an older `deepctl-cmd-*` can't be mixed with a newer `deepctl`. The packages no longer get their own GitHub releases or tags. - **`--version`:** prints the name it was run as: `dg, version 0.5.0` or `deepgram, version 0.5.0` (0.4.x printed `deepctl, version …` for every console script). `deepctl` and `python -m deepctl` still print `deepctl, version …`. - **listen:** - `-o json` and `-o yaml` print the server's response body, for everyone, not only in agent sessions. A 2xx response whose body isn't a JSON object now exits 1. `DEEPCTL_LISTEN_JSON=sdk` brings back 0.4.x's SDK-shaped JSON for prerecorded audio. - Live `-o json` and `-o yaml` print only the final result, not every interim message. - In an agent session, `dg listen` with no source (no file, URL or `--stdin`) is refused instead of opening the microphone. - URL sources are no longer fetched or checked before the request; `--check-url` asks for the check. - Without the pre-flight key check, a bad or missing key is reported from the API's answer, so those error texts changed. - **speak:** `-o/--output FILE` is deprecated in favor of `--out-file`. `-o FILE` now replaces the file through a temp file, so it never writes through a symlink. - **Agent sessions:** - no status chrome on stderr, and warnings stay off stdout; - large listen results are saved to a file; - speak doesn't read stdin or write audio to a pipe; - `dg init`'s resume records and a pending device login live under the account's own home directory, not `$HOME`. - **init:** setup steps need `--yes` when not run from a terminal, and outside a terminal the app starts only with `--start`. Only Deepgram templates are cloned. - **login:** - `-y/--yes` answers the confirmations, and `-f/--force-write` still works as the same flag; - without a terminal, the confirmations return `confirmation_required` instead of "cancelled"; - `--complete` finishes an agent's device login. - **api:** `dg api` sends a `User-Agent`, `X-Fern-Platform` and a session ID header, only to hosts the key may be sent to. - **debug network:** sends no stored credentials. - **Update check:** never waits. A check that failed, or didn't finish before dg exited, is tried again after an hour instead of the next day. - **Dependencies:** - click >= 8.2, with caps below the next major of click, httpx, pydantic, rich, platformdirs, keyring and websockets; - `deepctl-cmd-mcp` needs mcp >= 1.23.1 (was 1.22.0); - numpy, ffmpeg-python, aiohttp and typer are no longer installed, and the commands no longer depend on requests. The `deepctl` package itself still lists `requests`. - **Plugin API:** `DeepgramClient.ws_base_url()` is removed, and `deepctl_core.websocket.RedirectRefused` is now the transport's error, no longer a `click.ClickException`. - **Releases:** published from `release.yml` with trusted publishing. This needs the `release` environment and the release GitHub App (`RELEASE_APP_CLIENT_ID` and `RELEASE_APP_PRIVATE_KEY`) set up before the 0.5.0 release PR. ### Notes - Tests on this head: Docker (Linux) Python 3.10 5,648 passed and Python 3.14 5,649 passed, 0 failed; lint, types, lock, floors, READMEs and build pass. The lowest-direct-dependencies job passes on Python 3.10 and 3.12 in Docker. macOS (local) Python 3.10 5,406 passed and Python 3.14 5,407 passed, 0 failed, and the signal and config race tests passed 30 runs in a row on each. Windows can't be run locally: the Windows paths are tested on every OS by taking away what Windows lacks, and CI on this PR runs the real matrix. - After 0.4.1's release PR merges, this branch is rebased onto main and the lockstep versions are re-baselined to the versions that release set. - Merge with a merge commit, not a squash: release-please reads the individual commits. The last commit carries `Release-As: 0.5.0`; without it, release-please would propose a patch release instead of 0.5.0. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
deepctl 0.4.1
This release prevents potential logging of API keys, and fixes how dg handles credentials, configuration, plugins and local servers.
DEEPGRAM_BASE_URL.deepgram.yamlcan no longer set credentials, trusted hosts or the programs dg runs.If you used 0.2.21 to 0.4.0, we recommend upgrading to 0.4.1 and rotating any API key you used with it. If you logged in through your browser, log in again after upgrading.
What's fixed
Credentials and network (deepctl-core, api, login, listen, speak, mcp, debug-probe)
api.deepgram.com,api.eu.deepgram.com,api.au.deepgram.comorapi.in.deepgram.com, and only over https on the default port), a host listed inapi.allowed_hostsin the user's own config, orDEEPGRAM_BASE_URL. ADEEPGRAM_BASE_URLset in an AI agent session doesn't count.dg mcpsends the key toapi.dx.deepgram.com, or to a--base-urlhost listed inapi.allowed_hosts. Device login (dg loginwithout--api-key) usesid.dx.deepgram.comunder the same rule. Any other host is refused before a request is made.dg listen,dg speakwith Flux,dg debug probe) anddg apino longer follow redirects. Without this change, a 3xx response could pass the key, audio or text on to another host.DEEPGRAM_CLI_INSECUREturns off TLS verification only when it is set to1/true, only for localhost, 127.0.0.1 and ::1, and never in an AI agent session.Configuration (deepctl-core)
deepgram.yamlcan no longer set credentials, trust settings or the programs dg runs. Any--configfile other than the defaultconfig.yamlcan no longer set trust settings or the programs dg runs.Plugins (deepctl-core, plugin)
plugins.allow_override. Plugin command names have to match a strict pattern.dg plugin searchlists only plugins that Deepgram publishes.dg plugin installmanages only from the real home directory, reached without a symlink, and skips it if another user owns its folders or the group or others can write to them. Plugins installed in dg's own Python environment still load.dg plugin install/update/removeanddg ffprobe --path.Local servers and tools (mcp, debug-probe, debug-audio, shared-utils, ffprobe, init, install scripts)
dg mcpruns over stdio only. The SSE mode, which had no authentication, is removed. Streamable HTTP was never supported. The docs proxy now ships insidedeepctl-cmd-mcp(vendored from deepgram-mcp 0.1.1, MIT). It forwards onlysearch_deepgram_knowledge_sourcesand makes no network connection until a tool is called.dg debug probeaccepts connections only from native clients and pages on localhost, 127.0.0.1 or [::1]. Requests from any other origin get a 403 before the upstream connection opens.file:<absolute path>, so a file name that starts with-is never read as an option. ffprobe itself is looked up from an absolute configured path or fromPATH, skipping relative entries.dg initwrites.envowner-only, adds it to.gitignore, and clones with--.install.shorinstall.ps1has to install uv (no uv, pipx or pip found), it installs a pinned uv release and checks its installer against its sha256 before running it.Telemetry and crash output (telemetry, root)
DEEPCTL_TELEMETRY_DSN, which exists for tests.dg --helpends with "This version of dg sends no telemetry." The README says the same: no error reports, usage data or other telemetry. dg still contacts the Deepgram API for the commands you run, and pypi.org to check for updates.dg login,dg projectsanddg updatecould erase atelemetry.enabled: falseopt-out, because every config save dropped thetelemetrysection. 0.4.1 keeps it, so it is still in place if reporting comes back.Other
dg updatename thedeepgramHomebrew formula. The old hint,brew upgrade deepctl, failed.dg listen debug-call.wavused to read the file asdebug call.wav. Only the command name itself is expanded (dg debug-audiostill works).dg --helpand other output no longer crash when stdout or stderr can't encode a character, as with a redirected or piped Windows console (cp1252). Characters the stream can't write are escaped instead.Upgrade notes
dg mcp --transport ssenow exits with an error.--transport streamable-httpwas never supported and still exits with an error. Only stdio is supported.--portand--hostare still accepted, and ignored.--transport,--portand--hostfrom your editor's MCP config, or set--transporttostdio.dg mcpexposes onlysearch_deepgram_knowledge_sourcesand reads the API key on each call rather than at startup.deepgram-mcp; the docs proxy ships inside deepctl-cmd-mcp. A pip or pipx upgrade leaves the olddeepgram-mcppackage and itsdeepgram-mcpcommand installed (with pipx, inside deepctl's environment, not on your PATH).uv tool upgrade deepctlremoves it.pip uninstall deepgram-mcp. pipx:pipx runpip deepctl uninstall deepgram-mcp. Point editor configs atdg mcp.api.deepgram.com,api.eu.deepgram.com,api.au.deepgram.com,api.in.deepgram.com(over https on the default port only), hosts inapi.allowed_hosts, orDEEPGRAM_BASE_URL. ADEEPGRAM_BASE_URLset in an AI agent session doesn't count.dg mcpsends it toapi.dx.deepgram.comor a host inapi.allowed_hosts, anddg loginwithout--api-keyusesid.dx.deepgram.comunder the same rule. dg refuses a--base-urlor profile that points anywhere else.host:port) toapi.allowed_hostsin your userconfig.yaml.http://base URL is refused unless the host is loopback (localhost, 127.0.0.0/8 or [::1]). A loopback base URL from--base-urlor a profile must also be listed inapi.allowed_hosts. OnlyDEEPGRAM_BASE_URLis exempt, and not in an AI agent session.api.allowed_hosts, or setDEEPGRAM_BASE_URLoutside an AI agent session.deepgram.yamlis read only foroutput.*settings and a regional Deepgrambase_url. Everything else is ignored with a warning:api_key,project_id, the active/default profile, otherbase_urls, and theapi,tools,telemetry,updateandpluginssections.config.yaml, or set them with env vars or flags.--configfile other than the defaultconfig.yamlloses itsapi,tools,pluginsandupdatesections, and any non-regionalbase_url. It can still hold credentials and profiles.--configpath that isn't a regular file (a FIFO,<(...)or/dev/stdin) is no longer read. dg warns and runs without it.dg_token_expires_at, and handles itsapi,tools,pluginsandupdatesections and any non-regionalbase_urlas it does for a--configfile.chmod go-wthe file and its folder, orchownit to yourself.HOMEorXDG_CONFIG_HOMEinstead of the real home. dg keeps using its API key, but dropsapi.allowed_hosts(the wholeapisection),tools,plugins,updateand any non-regionalbase_url.DEEPGRAM_CLI_INSECUREworks only for1/true, only for localhost/127.0.0.1/[::1], and never in an AI agent session.DEEPCTL_TELEMETRY_DISABLEDandtelemetry.enabledare still accepted, and the new--no-telemetryflag is accepted too, but none of them has an effect in this version.DEEPCTL_TELEMETRY_DISABLEDnow reads any non-empty value except0,false,noandoff(in any case) as off; before, only1,trueandyesdid.DEEPCTL_TELEMETRY_DSNis honored only for a loopback host, for tests. Any other value is ignored. There is no built-in destination. The README section on overriding the destination was removed, and the README now says the variable is ignored unless it points at the local machine.^[a-z][a-z0-9-]{0,63}$. A plugin can't replace a built-in command: dg runs the built-in and prints a warning. A plugin whose entry point is named like a built-in command is skipped before it's imported.plugins.allow_overridein your userconfig.yaml(see README, "Plugins").dg plugin searchlists only plugins that Deepgram publishes.dg plugin install/update/removeanddg ffprobe --path.PATHare skipped.dg ffprobe --path /absolute/path/to/ffprobeagain if needed.dg loginno longer saves--base-urlorDEEPGRAM_BASE_URLto the profile; it prints that the URL was used for this login only.dg login --save-base-urlwith the URL and confirm at a terminal. For a non-Deepgram host, this also adds the host toapi.allowed_hosts.dg apiresolves relative paths against the profile's base URL, not alwaysapi.deepgram.com. An absolute URL to a host that isn't trusted is sent without your key.--allow-foreign-host=<host>to include the key. It must match the request host and is refused in an AI agent session.dg apiinstead prints a warning that names both origins, then prints the 3xx response.dg debug proberefuses browser pages that aren't on localhost/127.0.0.1/[::1] (403).mcp(1.22),websocketsin deepctl-cmd-listen (13.1),aiohttpin deepctl-cmd-debug-probe (3.11) andsentry-sdkin deepctl-telemetry (>=2.35.0,<3). sentry-sdk stays a dependency only for a later opt-in; 0.4.1 never imports it unless a loopback test DSN is set.ClickExceptionorUsageErrorraised inside a command is no longer printed after a "Command failed:" line; only theError:line remains. Exit codes are unchanged.install.shorinstall.ps1has to install uv (no uv, pipx or pip found), it installs uv 0.12.24, checked against a pinned sha256, instead of the latest uv. The newDEEPCTL_UV_INSTALLER_URLvariable points either script at a mirror of the uv installer, which must serve the same file.DEEPCTL_UV_INSTALLER_URLto a mirror of uv 0.12.24's installer.Test results
CI on this PR runs the full matrix, Python 3.10 to 3.14 on each OS, with 0 failures:
Sub-package floors on deepctl-core are raised to
>=0.3.1by hand on the release PR, because the release sync job (check_dependency_floors.py --fix) only moves the root floors. The packages that import a deepctl-core API new in 0.3.1 are deepctl-cmd-api, deepctl-cmd-debug-audio, deepctl-cmd-debug-probe, deepctl-cmd-ffprobe, deepctl-cmd-init, deepctl-cmd-listen, deepctl-cmd-login, deepctl-cmd-mcp, deepctl-cmd-plugin, deepctl-cmd-projects, deepctl-cmd-update and deepctl-telemetry.Known follow-ups
deepgram-skills-v1.7.0, also v1.8.0 in fix(core): install deepgram-skills-v1.8.0 by default #154) still tells agents to rundg mcp --transport sse --port 8000, which now fails.🤖 Generated with Claude Code