Skip to content

feat(extensions): let custom extensions reuse pup's client and formatter#564

Merged
platinummonkey merged 2 commits into
mainfrom
feat/extension-client-formatter-reuse
Jun 5, 2026
Merged

feat(extensions): let custom extensions reuse pup's client and formatter#564
platinummonkey merged 2 commits into
mainfrom
feat/extension-client-formatter-reuse

Conversation

@platinummonkey
Copy link
Copy Markdown
Collaborator

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.rs and formatter.rs. This PR exposes those engines through the runtime contract so an extension in any language can shell out to the parent pup binary and reuse them, with no Rust linking.

Changes

  • pup api reuses the client auth handler (src/commands/api.rs): routes through client::apply_auth (made pub in src/client.rs) instead of hand-rolled headers. This gains the per-endpoint OAuth/API-key fallback — pup api now correctly uses API keys on OAuth-excluded endpoints like /api/v2/api_keys (previously it sent a bearer token and got 403). All of pup api's curl-like behavior (-i, --verbose, error-body passthrough, absolute URLs) is preserved.
  • pup api honors --output: responses render through formatter::format_and_print, so pup api v2/monitors -o table and agent-mode envelopes now work instead of always pretty-JSON.
  • New pup format / fmt command (src/commands/format.rs): reads JSON from stdin (or --input FILE) and renders it through the shared formatter, with optional --count / --command / --next-action agent-envelope metadata. The reader is injectable so the stdin path is unit-tested.
  • Extension env contract closes the loop (src/config.rs, src/useragent.rs): from_env now reads PUP_OUTPUT / PUP_READ_ONLY / PUP_AUTO_APPROVE and 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 the global --output flag from a defaulted String to Option<String> (validated via the new resolve_output_format helper in src/main.rs), because the "json" default was unconditionally clobbering any env-derived format.
  • Docs (docs/EXTENSIONS.md): documents the pup api | pup format reuse 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 format

Testing

  • 1255 unit/integration tests pass (cargo test --bin pup -- --test-threads=1); cargo fmt --check and cargo clippy -- -D warnings clean.
  • New tests: pup format (file + stdin + -/None reader, table, agent envelope, invalid JSON, empty, missing file); pup api table-output and OAuth-excluded API-key fallback (relative + absolute URL, asserting DD-API-KEY present and authorization absent even when a bearer token is set); config PUP_* inheritance + DD_OUTPUT-wins precedence; useragent PUP_AGENT_MODE; resolve_output_format (explicit override / absent-keeps-resolved / invalid-errors).
  • Verified live: pup api v1/org -o table (OAuth + table render), PUP_OUTPUT=csv pup format inheritance, PUP_AGENT_MODE=true pup fmt envelope.

Notes

  • Behavior change: default pup api JSON output now goes through the shared formatter (alphabetically key-sorted, Go-style HTML escaping), matching every other pup command.
  • Behavior change: a malformed explicit --output value is now a hard error instead of being silently ignored. Ambient DD_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: a targets_configured_host guard only attaches Datadog credentials when the absolute URL's scheme + host + effective port match cfg.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).

platinummonkey and others added 2 commits June 5, 2026 09:05
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>
@platinummonkey platinummonkey requested a review from a team as a code owner June 5, 2026 14:24
@dd-octo-sts-019303
Copy link
Copy Markdown

🐑 PR Shepherd is maintaining this PR

I 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 flow-skip label.

@platinummonkey platinummonkey merged commit 3c33379 into main Jun 5, 2026
6 checks passed
@platinummonkey platinummonkey deleted the feat/extension-client-formatter-reuse branch June 5, 2026 15:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants