Skip to content

Add --health subcommand + return non-zero exit code on auth failure #6

Description

@sapihav

Add --health subcommand + return non-zero exit code on auth failure

Two related improvements to make the CLI a better citizen in orchestrated environments (the Tessera dossier agent, but also any other downstream consumer that wants to validate setup before invoking the CLI for real).

Part 1 — --health subcommand

Problem

Today, the only way to verify the CLI is correctly configured (binary present + valid *_API_KEY exported + provider reachable) is to issue a real query. That:

  • Burns budget on the operator's account.
  • Returns a result-shaped envelope, so callers have to special-case "this was a probe, not a real search."
  • Doesn't give a binary go/no-go signal — the caller must inspect content to infer whether auth worked.

Proposal

Add a perplexity --health (or perplexity health) subcommand whose contract is:

  • Make the smallest possible authenticated call against the provider's API. For most providers, this is a GET /me / GET /v1/models / equivalent ping that returns a tiny, billing-free response. If the provider has no free probe, use the cheapest possible search with n=1 against a fixed canary query.
  • Exit 0 if and only if the call returned a successful response (i.e. the API accepted the key and returned valid output).
  • Exit non-zero on any failure: missing env var, HTTP 401/403, network unreachable, malformed response. Stderr should give a one-line diagnosis (missing $XXX_API_KEY / HTTP 401: invalid_api_key / connection refused / etc.).
  • Stdout: optionally a tiny JSON envelope {"schema_version":"1","provider":"...","command":"health","status":"ok","elapsed_ms":N} so machine-readable consumers don't have to parse stderr. Empty stdout is also acceptable; the exit code is the contract.

Why it matters

The Tessera dossier agent's /tools pre-flight panel (sapihav/tessera#38) currently shows three states per CLI: ok (binary present + env var set), missing_key (binary present, env var unset), not_installed. There's a fourth state — key_invalid (binary present, env var set, but value is wrong/expired/rate-limited) — that today silently degrades to ok and the operator only finds out by running a dossier and seeing a 401. With --health, the panel can show key_invalid truthfully.

It's also useful for the dossier skill itself (sapihav/osint-dossier Phase 0) to validate before fanning out — see sapihav/osint-dossier#3.

Done when

  • perplexity --health exists and follows the contract above.
  • README documents the subcommand and its exit codes.
  • The probe call's billing impact is documented (e.g. "uses GET /v1/models, no usage charge" or "uses 1 cheapest-tier search").

Part 2 — Exit non-zero on auth failure

Problem (concrete)

A direct shell probe in the Tessera production container, against a host where perplexity_API_KEY is set to an empty string (operator never bought provider credits, so the env var was set to ""):

$ perplexity search "test query"
error: perplexity API returned HTTP 401: {"error":{"message":"Invalid API key provided. ...",
                                              "type":"invalid_api_key","code":401}}
$ echo $?
0

The CLI prints the HTTP 401 to stdout and exits 0. Downstream code reading $? cannot distinguish a 401-failed call from a successful call. This is the immediate cause of two real downstream bugs in our stack:

  1. Cost mis-attribution. Tessera's cost_tracking.py:tool_complete_handler falls through to the price-table fallback when no _cost envelope is found, and bills the per-call USD rate. So a 401 that never actually charged the provider's account gets attributed as a real cost in our portal. The operator sees fictional spend.
  2. Skill seed degradation. The dossier skill's first-volley.sh may treat exit-0 as "this CLI contributed seed data," then drop the unparseable error text downstream — see Phase 1 fan-out silently drops valid exa + tavily results — only jina reaches merged_from osint-dossier#3 for the full story.

Both downstream bugs would still need their own fixes (Tessera should also gate on output shape, the skill should also validate envelope schema), but the cleanest place to detect auth failure is in the CLI itself — it's the layer that holds the HTTP response and knows what 401 means.

Proposal

  • HTTP 4xx and 5xx responses → non-zero exit code. Suggested mapping:
    • 1 for any HTTP error (4xx, 5xx)
    • 2 for missing/empty *_API_KEY
    • 3 for network errors (connection refused, DNS, timeout)
    • 4 for malformed response (provider returned non-JSON when JSON expected)
  • Error message goes to stderr, not stdout. Stdout stays reserved for the success envelope.
  • README documents the exit-code mapping next to the command list.

Why it matters

Same as Part 1 — orchestrators (the dossier skill, Tessera's cost tracker, anyone wrapping these CLIs) need a deterministic signal of success/failure that doesn't require parsing prose stderr. Exit code is the standard answer.

Done when

  • Auth failures (401, 403) exit non-zero.
  • Network failures exit non-zero.
  • Missing env var exits non-zero (don't even hit the API).
  • Error text goes to stderr, not stdout.
  • Success output (JSON envelope) goes to stdout, exit 0.
  • README has a "Exit codes" section.

Severity / scope

Medium — both parts. Currently masked because operators notice eventually (when a dossier turns up empty seed data or a fictional cost row), but the fix is small and gives every downstream consumer a clean, deterministic interface.

Environment of repro

  • CLI version: latest from install.sh as of 2026-05-04
  • Host: Tessera production container (Debian trixie-slim, Python 3.13, Node 22)
  • Trigger: perplexity_API_KEY="" (empty string) — common when an operator removes credits or never had any.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions