Skip to content

feat(groom): add key-broker.mjs — localhost header-injecting API-key proxy + unit tests (BE-4419)#72

Open
mattmillerai wants to merge 3 commits into
mainfrom
matt/be-4419-key-broker
Open

feat(groom): add key-broker.mjs — localhost header-injecting API-key proxy + unit tests (BE-4419)#72
mattmillerai wants to merge 3 commits into
mainfrom
matt/be-4419-key-broker

Conversation

@mattmillerai

Copy link
Copy Markdown
Contributor

ELI-5

The groom agent that cleans up code runs on a shared runner where it can read any file and any process's environment. If we handed it the real Anthropic API key, it could just read the key straight out of /proc. So instead we run a tiny local doorman (key-broker.mjs): it holds the real key, the agent gets a fake one, and the agent points the Claude CLI at the doorman (ANTHROPIC_BASE_URL=http://127.0.0.1:<port>). Every real request passes through the doorman, which swaps the fake key for the real one before sending it upstream. The key only ever arrives on the doorman's stdin — never in its environment or command line — so it can't be recovered from /proc. This PR adds the doorman script and its tests only; wiring it into groom.yml is a sibling ticket (BE-4311).

What this adds

  • .github/groom/key-broker.mjs — a dependency-free (node:http/node:https, node ≥18, ESM) localhost proxy that reads the real key from stdin's first line, listens on 127.0.0.1:<port>, answers the CLI's HEAD/GET / connectivity probe locally with 200, forwards /v1/* to the upstream (streaming both directions so SSE works) after stripping inbound x-api-key/authorization/host and injecting the real x-api-key + upstream host, returns 404 for anything else, and 502 (static body) on an upstream connection error. Config via GROOM_BROKER_PORT (default 8199) and GROOM_BROKER_UPSTREAM (default https://api.anthropic.com, overridable for tests). It never logs headers/bodies — at most method path -> status.
  • .github/groom/tests/test_key_broker.py — a unittest integration suite that boots the real script under node against a local fake upstream (skipped when node is absent). Discovery picks it up automatically via the existing test-groom-scripts.yml.
  • A key-broker.mjs section in .github/groom/README.md (what it's for, the stdin-not-env rule and why, the env knobs, and the note that groom.yml wiring lands in BE-4311).

Tests

Covers every case the ticket enumerates, plus a startup guard:

  • injects the real key + strips inbound x-api-key/authorization, and round-trips a non-200 upstream status and body;
  • request body reaches the upstream byte-for-byte;
  • HEAD /200, not forwarded;
  • GET /anything-else404, not forwarded;
  • the broker process's /proc/<pid>/{environ,cmdline} do not contain the real key (Linux-only, skipped elsewhere — so it exercises in CI on ubuntu-latest, skips on the macOS dev box);
  • upstream down → 502 and the broker stays alive for a subsequent request;
  • (extra) empty/absent stdin key → non-zero exit.

python3 -m unittest discover -s .github/groom/tests -p 'test_*.py' -vRan 66 tests ... OK (skipped=1) locally (the 1 skip is the Linux /proc assertion on macOS).

Acceptance criteria

  • ✅ The unittest discovery passes locally (and is expected to pass in CI — ubuntu-latest ships node, so the node-gated class runs there and the /proc assertion is live).
  • grep -c ANTHROPIC .github/groom/key-broker.mjs0 (the key is never read from env/argv; the only env reads are GROOM_BROKER_PORT/GROOM_BROKER_UPSTREAM). The comments were deliberately worded to avoid the provider token so this stays literally 0.
  • ✅ No changes to .github/workflows/groom.yml (nor any workflow file — test-groom-scripts.yml already triggers on .github/groom/** and ubuntu-latest's preinstalled node is sufficient, so no setup-node step was needed).

Judgment calls / notes

  • Resumed prior crash residue. An untracked, near-complete key-broker.mjs from an earlier interrupted attempt was on the branch; I reviewed it adversarially, rebased the work onto the current origin/main, and kept the sound implementation. The one substantive fix: the residue's header comment falsely claimed grep -c ANTHROPIC "is 0" while its own comments contained the token (actual count was 3) — reworded so the count is genuinely 0.
  • Added client-side stream-error guards (req/res/upstreamRes error handlers) beyond the ticket's enumerated branches: a CLI that aborts mid-stream would otherwise throw an unhandled stream error and crash this long-lived proxy. These no-op/teardown handlers don't change the specified happy path.
  • Test hardening: the fake upstream overrides server_bind to skip HTTPServer's socket.getfqdn() reverse-DNS lookup, which hung ~35s per test on the dev box (and is a general flake risk); the suite now runs in ~3s.
  • Negative-claim falsification (self-review clause e): N/A. The 404/502/startup-die paths are a proxy's correct protocol behavior, not a user-facing capability denial (no "X not supported" dead-end, no product surface to probe), so the falsification discipline doesn't apply here.

@mattmillerai mattmillerai added the agent-coded Authored by the agent-work loop label Jul 24, 2026
@mattmillerai
mattmillerai marked this pull request as ready for review July 24, 2026 21:26
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 25 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a1c4f386-04d9-447d-ac3a-07e8fc0909ed

📥 Commits

Reviewing files that changed from the base of the PR and between 1241e1f and 55081e2.

📒 Files selected for processing (1)
  • .github/groom/tests/test_key_broker.py
📝 Walkthrough

Walkthrough

Adds a localhost Node.js API-key broker that reads credentials from stdin, forwards authenticated /v1/* requests, handles failures and streaming, and includes Python integration tests plus updated documentation.

Changes

Key Broker

Layer / File(s) Summary
Startup configuration and key intake
.github/groom/key-broker.mjs
Validates port and upstream settings, restricts plaintext upstreams to loopback hosts, reads the key from stdin, and listens only on 127.0.0.1.
Request routing and forwarding
.github/groom/key-broker.mjs
Handles local probe and 404 routes, strips inbound credentials, injects x-api-key, forwards /v1/* traffic, streams responses, and returns 502 on upstream failures.
Integration coverage and documentation
.github/groom/tests/test_key_broker.py, .github/groom/README.md
Adds end-to-end broker tests covering authentication, bodies, routing, failures, disconnects, path prefixes, startup validation, and process visibility; documents operation and test execution.

Sequence Diagram(s)

sequenceDiagram
  participant GroomCLI
  participant key-broker.mjs
  participant FakeUpstream
  GroomCLI->>key-broker.mjs: Send /v1/* request
  key-broker.mjs->>key-broker.mjs: Remove inbound auth headers
  key-broker.mjs->>FakeUpstream: Forward request with stdin API key
  FakeUpstream-->>key-broker.mjs: Return response or stream
  key-broker.mjs-->>GroomCLI: Forward status, headers, and body
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch matt/be-4419-key-broker
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch matt/be-4419-key-broker

Comment @coderabbitai help to get the list of available commands.

@mattmillerai mattmillerai added the cursor-review Multi-model cursor review label Jul 24, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Cursor Review — Consolidated panel

Triggered by @mattmillerai.

Found 10 finding(s).

Severity Count
🟠 High 2
🟡 Medium 3
🟢 Low 5

Panel: 8/8 reviewers contributed findings.

Comment thread .github/groom/key-broker.mjs Outdated
Comment thread .github/groom/key-broker.mjs Outdated
Comment thread .github/groom/key-broker.mjs Outdated
Comment thread .github/groom/key-broker.mjs Outdated
Comment thread .github/groom/key-broker.mjs Outdated
Comment thread .github/groom/key-broker.mjs
Comment thread .github/groom/key-broker.mjs Outdated
Comment thread .github/groom/key-broker.mjs Outdated
Comment thread .github/groom/key-broker.mjs Outdated
Comment thread .github/groom/README.md
…uards (BE-4419)

Address the cursor-review panel findings on the localhost key-broker proxy:

- Stream teardown: forward both directions via stream.pipeline so an upstream
  reset mid-response or a client abort mid-upload tears down BOTH sides instead
  of crashing the broker on an unhandled 'error'. A client disconnect now aborts
  the outbound real-key request (res 'close' -> upstreamReq.destroy()) so it
  can't linger and leak its socket.
- Post-headers upstream failure now res.destroy()s (caller sees a broken
  connection) instead of res.end() presenting a truncated body as success.
- Add a generous socket-inactivity timeout so a stalled upstream is reclaimed
  rather than pinning the forwarded request + socket forever.
- Wrap client.request in try/catch (it can throw synchronously on an illegal
  request target / injected key) and trim() the key (strips CR + stray
  whitespace) so a copy-pasted secret can't throw an illegal-header error.
- Restrict cleartext http:// upstreams to loopback hosts so a misconfigured
  remote http upstream can't transmit the injected key in the clear.
- Prepend the upstream URL's path prefix so a non-root upstream isn't dropped.
- Reject GROOM_BROKER_PORT strings with trailing garbage (8199x, 1e3).
- README: recommend keying the readiness wait-loop off the readiness LINE (proof
  the broker bound the port), not a bare port-connect check.

Tests: add coverage for client-abort survival, path-prefix forwarding, strict
port rejection, and plaintext-non-loopback upstream rejection.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/groom/tests/test_key_broker.py:
- Around line 163-191: Add a regression test alongside
test_injects_real_key_and_strips_inbound_credentials that starts the broker with
a stdin key containing leading/trailing spaces and CRLF, then sends a request
and verifies the upstream x-api-key equals REAL_KEY rather than the untrimmed
input. Reuse the existing broker startup, cleanup, port-waiting, connection, and
record-capture helpers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 98d5dc6a-f283-473b-b263-47bef8d265cf

📥 Commits

Reviewing files that changed from the base of the PR and between 29a81ca and 1241e1f.

📒 Files selected for processing (3)
  • .github/groom/README.md
  • .github/groom/key-broker.mjs
  • .github/groom/tests/test_key_broker.py

Comment thread .github/groom/tests/test_key_broker.py
Feed the broker a padded/CRLF key on stdin and assert the trimmed value —
not the raw input — reaches x-api-key upstream, locking down the
key-broker.mjs .trim() hardening. Parameterizes _start_broker with an
optional raw stdin payload. Addresses CodeRabbit review on PR #72.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent-coded Authored by the agent-work loop cursor-review Multi-model cursor review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant