Skip to content

refactor(gateway): REST proxying in its own module; Gateway keeps the route handlers - #167

Merged
earakely-scale merged 1 commit into
mainfrom
refactor/gateway-rest-proxy-module
Oct 12, 2026
Merged

earakely-scale merged 1 commit into
mainfrom
refactor/gateway-rest-proxy-module

Conversation

@earakely-scale

@earakely-scale earakely-scale commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

Third of the gateway maintainability stack, rebased onto main after #165 and #166 merged. Behaviour-neutral: no wire change.

What moved. The /svc and /website reverse proxy and the backing servers' card rewrite now live in src/agent_env/env/gateway/rest_proxy.py as module functions, with the invariants stated once in the module docstring:

  • forward(request, base_url, *, role, timeout, mcp_server) is the former _proxy_rest_request body: the path forwarded as received (upstream_url / raw_remainder), one header per raw entry minus hop-by-hop and the client's role header (upstream_headers), the JSON-RPC _meta role stamp on a backing MCP server's own endpoint (is_child_mcp_path, stamp_jsonrpc_role), and the streamed UpstreamResponse that closes itself.
  • fetch_child_card, rewrite_child_card(key, card, gateway_extensions) and operations are the env-card side of the same /svc/{key} prefix.

What Gateway keeps. _proxy_rest_request resolves the service and hands off: "is this a backing MCP server" stays with the gateway, which knows its services; "is this its MCP endpoint" goes with the proxy. _rewrite_child_card and _fetch_child_card stay as thin methods because tests override them per instance. _request_role stays, since /step reads it too. The request-scope binder reads the raw role header name from the new module. gateway.py goes from 1361 to 1137 lines.

Tests. tst/unit/env/gateway: 377 passed after the rebase onto main, the same count as before the move. Together with the card-consuming suites outside it (test_env.py, test_env_card_loads.py, the task_step snapshot/sync/apply tests): 557 passed. The only test edits are the doctest runner, which now covers both modules since the examples moved with their functions, and the httpx.AsyncClient monkeypatch pointing at the module that constructs the client.

🤖 Generated with Claude Code

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge; no actionable regression was found.

Summary

Moves REST forwarding and child-card rewriting into rest_proxy.py, while Gateway keeps route handlers and service selection.

  • The rest_proxy module forwards service requests and streams replies.
  • The rest_proxy module fetches and rewrites backing-server cards.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  C[Client] --> G[Gateway route handler]
  G -->|Service URL and role| P[rest_proxy.forward]
  P --> S[Backing service]
  S --> U[UpstreamResponse]
  U --> C
  G --> F[fetch_child_card]
  F --> R[rewrite_child_card]
  R --> E[Composed environment card]
Loading

Reviews (3) · Last reviewed commit: "refactor(gateway): REST proxying in its ..." · Reviewed by Greptile

@earakely-scale
earakely-scale requested a review from a team as a code owner October 11, 2026 23:45
@earakely-scale

Copy link
Copy Markdown
Collaborator Author

Local run of tst/integration/env/gateway/gateway_test.py against 1fad9fb, since CI ignores that file in both integration jobs. The gateway image was built from this branch, and the gateway.rest_proxy logger shows the moved module serving the card inside the container; zero proxy errors in the run.

Every proxy- and card-path test passed: test_gateway_with_single_mcp_server, test_gateway_with_agentenv_environment, test_gateway_with_website_env_v1, test_renamed_registration_serves_its_registered_name, test_cardless_environment_resolves_name_from_environment_name, test_snapshot_agent_state_v1_and_fallback, the three /svc clock consumers, and the [local] trigger and clock extension cases.

16 failures, all from this machine and none in the moved code: 11 [modal] cases (Modal cannot pull localhost:5000 images), test_env_validator_task and test_gateway_triggers_nl_executor[modal_vm] (no a2a-default / claude-code-cli A2A agent in the local store), the two website tests (Playwright browser.newContext: ... browser has been closed, the x86 Chromium build the CI ignore comment names), and test_snapshot_cli (get_clean returns None after the snapshot is stored; no gateway route involved).

@earakely-scale
earakely-scale force-pushed the refactor/gateway-rest-proxy-module branch from 1fad9fb to 6a9a432 Compare October 12, 2026 02:05
@earakely-scale
earakely-scale force-pushed the refactor/gateway-caller-and-lifespans branch from 5f76657 to 7ace4a1 Compare October 12, 2026 02:05
Base automatically changed from refactor/gateway-caller-and-lifespans to main October 12, 2026 03:10
… route handlers

The /svc and /website reverse proxy (path forwarded as received, raw header
multiplicity, the gateway's role header and JSON-RPC _meta stamp, the streamed
upstream response that closes itself) and the backing servers' card rewrite now
live in rest_proxy.py as module functions. Gateway._proxy_rest_request resolves
the service and hands off; _rewrite_child_card and _fetch_child_card stay as
thin methods, since tests override them per instance. The request-scope binder
reads the raw role header name from the new module.

No wire change: same URLs, headers, _meta keys and values, same error bodies.
gateway.py goes from 1361 to 1137 lines. The doctest runner now covers both
modules, since the examples moved with their functions.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@earakely-scale
earakely-scale force-pushed the refactor/gateway-rest-proxy-module branch from 6a9a432 to 418d6fa Compare October 12, 2026 03:11
@earakely-scale

Copy link
Copy Markdown
Collaborator Author

Light chaos round on this head (418d6fa), loopback, aimed at the moved proxy: a real Gateway with an MCP echo child, a plain HTTP upstream (chunked stream, raw-path echo, env card) and a black-hole upstream, all behind the gateway's /svc, card and MCP routes. The script stays in scratch.

  • 32 concurrent 200-chunk streams: every body exact; the first chunk reaches the client 11 ms into a 2 s body (streamed, not buffered).
  • 16 streams in flight when the upstream is killed: all 16 end as a cut body (RemoteProtocolError) within 1.7 s, none hangs; a new stream succeeds once it is back.
  • 60 clients abandoning streams after 3 chunks under load: every abort returns promptly, the upstream saw all 60 cancellations (the gateway closed its upstream responses), and the process's open file descriptors are back to baseline afterwards.
  • 8 concurrent calls to an upstream that never completes the TCP connect: all 502 in 10.0 s, the connect timeout, not the data-plane read ceiling.
  • Env card: a card-bearing backing server nested with its url rewritten to /svc/blob/...; the cardless MCP child self-omits; a dead backing server is omitted with the card served in 20 ms; 20 concurrent card fetches across its restart all answer 200 in under a second, each nesting or omitting it.
  • Mixed traffic during two child restarts (8 MCP sessions with distinct roles, 4 raw JSON-RPC clients through /svc/mcp-echo/mcp sending a forged _meta, 2 /step clients): zero wrong identities on any path, the forged role and session never reach the child, failures are only the outage kinds (Connection closed, 502 while the child is down, a lost child session re-initialised), nothing past 2.5 s over 868 calls.
  • 100 concurrent requests with escaped separators (%2F, %3F, %23) in the path and query and two client role headers: the upstream saw the path and query verbatim and exactly one role header, the gateway's.

@earakely-scale
earakely-scale merged commit 6aa744f into main Oct 12, 2026
14 checks passed
@earakely-scale
earakely-scale deleted the refactor/gateway-rest-proxy-module branch October 12, 2026 03:36
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