feat(httpproxy): fail-closed request handling, client auth, and distinct upstream errors#182
Merged
Merged
Conversation
… 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
There was a problem hiding this comment.
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.
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 ininternal/mcpand flag wiring in theproxy-httpCLI.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
--on-batch reject|evaluate(defaultreject, fail-closed).evaluategates everytools/callmember and forwards the whole batch only if all are allowed (all-or-nothing); each member's decision is audited. Cross-transport design note added atdocs/batch-handling.md.--on-unparsed forward|rejectfor bodies that don't parse as JSON-RPC. Valid non-tools/callmethods (initialize/ping/notifications) are still forwarded so sessions keep working.--auth-token(or$AGENTFENCE_PROXY_AUTH_TOKEN) requiresAuthorization: Bearer <token>; unauthenticated requests get HTTP 401 and reach neither the policy edge nor the upstream. The gate token is compared in constant time and never relayed upstream. A startup warning fires when--listenbinds a non-loopback address without a token.-32002upstream unavailable,-32003proxy error) addressed to the request id, replacing the generic HTTP 502, so an operator can tell a transport failure apart from a policy block.Related issue
Fixes #128
Fixes #133
Fixes #138
Fixes #145
Fixes #152
Why
proxy-httpis 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.mcphelpers (IsBatch,LooksLikeJSONRPC,ParseBatch, error constructors) and CLI helpers (parseBatchPolicy,parseUnparsedPolicy,isLoopbackListen, bad-enum rejection).--on-batchexits 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
--on-batch evaluate. Documented in the CHANGELOG and design note (MCP is removing JSON-RPC batching, so reject-by-default is low-regret).--on-batch evaluateis 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-batchswitch there is noted as follow-up indocs/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
make ci, race + coverage)https://claude.ai/code/session_01S6MZXehSbKmLFM9DGsee9y
Generated by Claude Code