Skip to content

feat(httpproxy): fail-closed request handling, client auth, and distinct upstream errors#182

Merged
dgenio merged 3 commits into
mainfrom
claude/github-issues-triage-o8vv4j
Jun 14, 2026
Merged

feat(httpproxy): fail-closed request handling, client auth, and distinct upstream errors#182
dgenio merged 3 commits into
mainfrom
claude/github-issues-triage-o8vv4j

Conversation

@dgenio

@dgenio dgenio commented Jun 14, 2026

Copy link
Copy Markdown
Owner

Summary

Harden the streamable-HTTP proxy's request edge so every non-happy-path body is fail-closed instead of forwarded uninspected, add optional client authentication, and surface transport failures distinctly from policy blocks. All changes converge on internal/httpproxy (ServeHTTP/forward) plus supporting helpers in internal/mcp and flag wiring in the proxy-http CLI.

This is one coherent PR covering five tightly-coupled issues that all edit the same request path and would otherwise conflict as separate PRs.

What changed

Related issue

Fixes #128
Fixes #133
Fixes #138
Fixes #145
Fixes #152

Why

proxy-http is a security gate, so "documented hole" (ungated batches, transparently-forwarded unparseable/oversize bodies, an unauthenticated off-loopback listener) is still a hole. These were tracked as separate issues but share one code path; fixing them together keeps the request-handling contract consistent (one JSON-RPC error-envelope shape, one fail-closed boundary) and avoids three PRs racing on the same function.

How verified

  • make ci (fmt-check, vet, go test -race + coverage) — all packages pass, 81.4% total coverage.
  • New unit tests: batch reject/evaluate (all-allowed forwards, one-denied rejects all), oversize, unparsed forward vs reject, auth (missing/wrong/correct token, token not relayed upstream), upstream-failure error envelope; plus mcp helpers (IsBatch, LooksLikeJSONRPC, ParseBatch, error constructors) and CLI helpers (parseBatchPolicy, parseUnparsedPolicy, isLoopbackListen, bad-enum rejection).
  • Manual smoke test of the built binary: invalid --on-batch exits non-zero with a clear message; off-loopback bind without a token prints the guardrail warning; the warning is silent when a token is set.

Tradeoffs / risks

  • Behavior change (no back-compat constraint per request): batches are now rejected by default; clients that relied on transparent batch forwarding must send one request per body or use --on-batch evaluate. Documented in the CHANGELOG and design note (MCP is removing JSON-RPC batching, so reject-by-default is low-regret).
  • Upstream/proxy failures now return HTTP 200 + JSON-RPC error envelope instead of HTTP 502 (the MCP-correct shape).
  • --on-batch evaluate is intentionally all-or-nothing rather than per-member response reassembly, to stay fail-closed and avoid splitting streamed responses.

Scope notes

The stdio proxy also forwards batch lines ungated; an equivalent --on-batch switch there is noted as follow-up in docs/batch-handling.md (the HTTP path is where batches arrive as a single inspectable body). Related fuzzing of SSE/response parsing (#140) was deliberately left out to keep this PR focused on the request edge.

Checklist

  • Tests added or updated
  • Documentation updated if needed (threat-model, integration-guide, new batch-handling note, CHANGELOG)
  • CI passes (make ci, race + coverage)
  • Issue number included

https://claude.ai/code/session_01S6MZXehSbKmLFM9DGsee9y


Generated by Claude Code

… auth, distinct upstream errors

Make the streamable-HTTP proxy's request edge fully fail-closed and surface
transport failures distinctly, closing a documented policy-bypass and several
robustness gaps in internal/httpproxy ServeHTTP/forward.

- Gate JSON-RPC batch (array) bodies that previously slipped past the
  single-request parser and forwarded ungated. New --on-batch reject|evaluate
  (default reject); evaluate gates every member and forwards all-or-nothing.
  (#128, #145)
- Refuse oversize bodies and add --on-unparsed forward|reject for bodies that
  are not valid JSON-RPC, instead of forwarding uninspected. (#152)
- Add optional client authentication via --auth-token /
  $AGENTFENCE_PROXY_AUTH_TOKEN and a non-loopback bind guardrail warning; the
  gate token is never relayed upstream. (#138)
- Surface upstream-unreachable and internal proxy failures as distinct
  JSON-RPC error envelopes (codes -32002/-32003) addressed to the request id,
  replacing the generic HTTP 502. (#133)

Adds mcp batch/JSON-RPC detection helpers and error constructors, wires the new
flags into the proxy-http CLI, documents the cross-transport batch design note
(docs/batch-handling.md), and updates the threat model and integration guide.
Covered by new unit tests for each path; make ci (fmt-check, vet, test-race)
passes at 81.4% coverage.

https://claude.ai/code/session_01S6MZXehSbKmLFM9DGsee9y
Copilot AI review requested due to automatic review settings June 14, 2026 14:19

Copilot AI 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.

Pull request overview

This PR hardens the MCP-over-HTTP proxy request boundary (internal/httpproxy) to be fail-closed on previously ungated edge cases (notably JSON-RPC batch bodies), adds optional client authentication for the HTTP listener, and returns distinct JSON-RPC error envelopes for upstream vs internal proxy failures—backed by new unit tests and documentation updates.

Changes:

  • Add batch detection + --on-batch reject|evaluate (default reject) and --on-unparsed forward|reject, and refuse oversize bodies instead of forwarding uninspected.
  • Add optional bearer-token client authentication (--auth-token / $AGENTFENCE_PROXY_AUTH_TOKEN) plus a non-loopback bind warning.
  • Introduce distinct JSON-RPC error codes/envelopes for upstream unavailable vs proxy error, and document the new behavior.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
internal/mcp/model.go Adds new proxy-specific JSON-RPC error codes plus helpers for batch detection/parse and distinct upstream/proxy errors.
internal/mcp/model_test.go Adds unit tests covering batch helpers and new error-code helpers.
internal/httpproxy/httpproxy.go Implements fail-closed handling for batch/oversize/unparsed bodies, optional auth, and distinct upstream/proxy error envelopes.
internal/httpproxy/httpproxy_test.go Adds tests for batch reject/evaluate behavior, unparsed forward/reject, auth enforcement, and upstream failure envelopes.
docs/threat-model.md Updates HTTP-proxy surface documentation for auth, batch/unparsed/oversize behavior, and error shaping.
docs/integration-guide.md Documents new proxy-http flags and links to updated operational guidance.
docs/batch-handling.md Adds a cross-transport design note explaining the batch policy decision and migration implications.
cmd/agentfence/main.go Wires new CLI flags, env fallback for auth token, bind guardrail, and enum parsing helpers.
cmd/agentfence/main_test.go Adds tests for enum parsing, loopback detection, and failing fast on invalid flag values.
CHANGELOG.md Documents the new fail-closed behaviors, auth option, and distinct upstream/proxy error envelopes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/httpproxy/httpproxy.go
Comment thread internal/httpproxy/httpproxy.go
Comment thread internal/mcp/model.go Outdated
claude added 2 commits June 14, 2026 14:26
…k, reject empty batch

- forward() now returns a plain HTTP 502 (not a JSON-RPC envelope) when the
  request is not JSON-RPC (SSE GET, passthrough, --on-unparsed forward bodies),
  preserving the HTTP contract for non-JSON-RPC clients. JSON-RPC requests keep
  the distinct error envelope.
- Upstream/proxy failure messages no longer echo the upstream URL or dial
  address into the agent-facing envelope; full detail is logged operator-side.
- ParseBatch rejects an empty array ([]), which JSON-RPC 2.0 §6 treats as
  invalid, keeping batch handling fail-closed.

https://claude.ai/code/session_01S6MZXehSbKmLFM9DGsee9y
The --on-batch evaluate path gates each member request but forwards the
batch unchanged, so batch responses are not observed for taint tracking
and a batch with several ask-tier members prompts once per member.
Document both in the threat-model taint scope/limits list and the
batch-handling design note so operators relying on taint tracking know
to prefer the default --on-batch reject.
@dgenio dgenio merged commit 4a95342 into main Jun 14, 2026
4 checks passed
@dgenio dgenio deleted the claude/github-issues-triage-o8vv4j branch June 14, 2026 17:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment