Skip to content

feat(cli): read-only 'api get' passthrough with anonymous-by-default reads#17

Open
mhusbynflow wants to merge 8 commits into
masterfrom
mhusbynflow/cli-run-request-command
Open

feat(cli): read-only 'api get' passthrough with anonymous-by-default reads#17
mhusbynflow wants to merge 8 commits into
masterfrom
mhusbynflow/cli-run-request-command

Conversation

@mhusbynflow

Copy link
Copy Markdown
Collaborator

What

Adds a read-only flowbio api get <path> [--param KEY=VALUE ...] CLI command: an authenticated-or-anonymous GET passthrough that writes the raw API response body to stdout.

Motivation. The flow-ai skill reads Flow via curl with the token attached through a command substitution ($(< ~/.config/flow/api-token)). Claude Code cannot auto-approve a Bash command containing command substitution, so every read prompts for permission. A flowbio api get … command has no substitution and is allowlistable once as Bash(flowbio api get:*). It also consolidates token handling in the CLI and keeps the token out of argv and the transcript.

How

  • HttpTransport.get_text + Client.get_raw: raw-body GET primitive (returns the response body as text, unparsed).
  • Output.emit_raw: writes the body to stdout verbatim (no newline, no JSON reshaping) in both human and --json modes.
  • New api resource with a single get verb: positional PATH (relative to the base URL, leading slash optional), repeatable --param KEY=VALUE (URL-encoded), and a ?-in-path rejection that points at --param.
  • GET-only by construction and host-constrained (path is always joined onto the configured base URL), so it cannot mutate remote state or reach another host.
  • AnonymousCredentials + an allow_anonymous fallback in resolve_credentials: with no token available and no --login, api get reads anonymously (public resources only). A token still wins when present; --login still forces auth. The fallback is opt-in per command, so the upload commands still require credentials.
  • Errors flow through the existing dispatch handler: standard status→exit-code mapping (4 not found, 3 auth, 5 bad request) and, under --json, a {"message", "status_code"} stderr envelope.
  • Docs: an api get section in source/cli.rst (concept + one worked example, no --help duplication).

Verification

  • Test-first throughout. uv run pytest tests/unit → 303 passed.
  • uv run ruff check clean on all changed files.
  • Adversarial review pass (independent reviewer): approved; verified precedence, upload-scoping, and host-constraint safety.

Out of scope

  • Wiring the flow-ai skill (separate repo) to call this command instead of curl.
  • Non-GET / mutations; bulk multi-file downloads.

Note

The branch also carries the design spec and implementation plan under docs/superpowers/ (the artifacts these commits were built from). Happy to drop them from the PR if you'd rather they not land on master.

🤖 Generated with Claude Code

mhusbynflow and others added 8 commits July 14, 2026 12:46
The flow-ai skill reads Flow via curl with the token attached through a
command substitution ($(< token-file)), which Claude Code cannot
auto-approve, so every read prompts for permission. Specify a read-only
`flowbio api get <path>` CLI command that resolves credentials itself,
giving the skill a single allowlistable command in place of curl.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The CLI's api-get passthrough needs to return an API response body
verbatim. Add HttpTransport.get_text and Client.get_raw alongside the
existing typed getters, returning the undecoded body text.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The api-get passthrough must surface the response body byte-for-byte so
the caller can pipe it through jq. Add emit_raw, which writes to stdout
unchanged in both human and --json modes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The flow-ai skill reads Flow via curl with the token attached through a
command substitution, which Claude Code cannot auto-approve, so every
read prompts for permission. Add 'flowbio api get <path> [--param k=v]',
a GET-only command that resolves credentials itself and writes the raw
response body to stdout, giving the skill a single allowlistable command.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cover the read-only concept, the --param encoding rule, credential
precedence, and the exit-code contract with one worked example, per the
CLI-docs convention of not duplicating --help output.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Clarify the get_text docstring (it returns text without parsing JSON) and
align the api-get RST example with the bash lexer used by the sibling
command examples.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The api-get command previously resolved credentials for every invocation,
so with no token available and a non-interactive stdin it exited 2 instead
of serving public data. Flow serves public resources unauthenticated, so a
read should not require a token.

Add an AnonymousCredentials strategy (no-op authenticate, no Authorization
header) and an allow_anonymous fallback in resolve_credentials: when no
token resolves and --login was not passed, api get reads anonymously. A
token still wins when present and --login still forces authentication. The
fallback is opt-in per command (api get only) via set_defaults, so the
upload commands still require credentials.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mhusbynflow

Copy link
Copy Markdown
Collaborator Author

@claude please review this PR

@claude

claude Bot commented Jul 14, 2026

Copy link
Copy Markdown

Claude finished @mhusbynflow's task in 2m 2s —— View job


Review — feat(cli): read-only 'api get' passthrough

  • Read changed source files
  • Review transport / client / auth layers
  • Review CLI (_api, _parser, _output, _auth)
  • Review tests & docs
  • Post review feedback

Overall this is a clean, well-scoped, thoroughly tested change. Test-first is evident, the layering is right (transport primitive → client escape hatch → CLI verb), docstrings explain why, and the docs stick to concepts as CLAUDE.md asks. I verified the two safety claims (GET-only, host-constrained) and they hold. A few notes below, none blocking.


Safety claims — verified ✅

  • Host constraint holds. HttpTransport._url (flowbio/v2/_transport.py:109) always prepends the configured base URL and only lstrip("/")s the path. A crafted path can't escape the host: //evil.com → slashes stripped → .../api/evil.com; http://evil.com.../api/http://evil.com where app.flow.bio remains the authority and the rest is an (encoded) path segment. .. traversal stays same-host. Good.
  • Anonymous + 401 refresh interact correctly. _can_refresh() gates on the Authorization header, so an anonymous read that hits a 401 won't attempt a token refresh — correct.
  • Upload scoping. allow_anonymous defaults to False and is only set on the api get parser, so upload/mutating commands still require credentials.

Minor notes (non-blocking)

  1. AnonymousCredentials.authenticate doesn't clear an existing tokenflowbio/v2/auth.py. It's a no-op, so the docstring's "leave the transport without an Authorization header" is only true on a fresh transport. If a caller reuses a client (log_in(TokenCredentials(...)) then log_in(AnonymousCredentials())), the old header persists. Harmless for the CLI (fresh Client per invocation), but the docstring slightly overpromises for library reuse. Consider either softening the wording or calling transport.clear_token() if such a method exists. Fix this →

  2. "Verbatim" is text-verbatim, not byte-verbatim. get_text returns response.text (flowbio/v2/_transport.py:185), which httpx charset-decodes, and emit_raw re-encodes to stdout. For the Flow JSON/UTF-8 API this is exactly right; just worth being aware it isn't a byte passthrough (that would be get_bytes). Fine as-is given the intended use.

  3. No trailing newline in interactive useemit_raw (flowbio/cli/_output.py:93) writes with no newline. This is the correct choice for piping into jq and matches curl, but a human running flowbio api get /pipelines in a terminal will see the shell prompt glued to the output. Deliberate and documented — flagging only so it's a conscious call.

  4. params type diverges from sibling methods. get/get_bytes take params: dict | None; get_text/get_raw take list[tuple[str, str]] | None. The divergence is justified (repeatable keys like sample_types) and documented, and httpx accepts both — just noting the inconsistency in case you'd prefer to align the family later.

Docs artifacts

The docs/superpowers/** spec + plan (721 lines) — as you flagged in the PR body, this is a maintainer call. My leaning: keep design artifacts out of master unless the repo already has a convention of committing them, since they'll drift from the code. Easy to drop from the branch if so.

Tests

Coverage is strong across all four layers (transport, client, auth resolution, CLI end-to-end incl. anonymous, bearer, URL-encoding, repeated params, ?-rejection, missing-=, and the 404→exit-4 JSON envelope). Two small gaps you could optionally add: an anonymous-read-hits-401 test asserting no refresh is attempted, and a --param key= (empty value) case. Neither is required.


Nice work — I'd approve modulo the docs-artifact decision, which is yours to make.
· mhusbynflow/cli-run-request-command

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.

1 participant