Skip to content

feat(gateway): a keyed deploy's role key, forwarded headers, and triggers by route (FD-4683) - #443

Open
williamloo-scale wants to merge 1 commit into
wloo/website-notes-examplefrom
wloo/website-gateway-auth
Open

williamloo-scale wants to merge 1 commit into
wloo/website-notes-examplefrom
wloo/website-gateway-auth

Conversation

@williamloo-scale

@williamloo-scale williamloo-scale commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #429.

The gateway side of website/v1: roles that can't be spoofed, the address a site is reached at, and triggers matched by route.

  • Role key, opt-in per deploy. A deploy with keyed_roles (deploy_env's keyed_roles: true, or env deploy --keyed-roles) draws a secret role key, gives it to the gateway (AGENTENV_ROLE_KEY) and to each website server (WEBSITE_ROLE_KEY), and records it on the deployed env (DeployedEnv.role_key, sent by role_headers(role)). Its gateway believes a role other than default, on REST and MCP, only beside AgentEnv-Role-Key equal to the key. It strips a client's key and sends its own upstream only beside a verified role, so SDK sites believe it. deploy_agent, the episode proxy, post-turn hook /step calls and the generated MCP CLI send the key. Any other deploy draws none, and its gateway believes the role header alone, as before. A step asking for keyed_roles of an env whose provider doesn't take them is refused at save.
  • Forwarded headers. /svc and /website set X-Forwarded-Host, -Proto and -Prefix and drop the client's copies. The SDK takes the prefix as the root path, so base_url, request.url and redirects name the gateway's address.
  • Triggers by route. A website trigger's when may name op, a route of the site's card, and then matches only requests that reach it. Registration refuses an op the card doesn't name. triggers/v1 declares website_op, so an older gateway is refused.

Before merging

  • On a keyed_roles deploy, a caller that sends a non-default role without role_headers(role) is default; agentenvhub-sdk and the hub don't send the key yet, so their tasks shouldn't turn it on until they do. Unkeyed deploys, the default, are unaffected.
  • An agent deployed with a role receives the key, so the key protects against pages and browsers, not a hostile agent.
  • The key is stored in plaintext on the instance record, beside the control token.
  • The CLI's default role cli is default on a keyed deploy.

Test

Unit suite: 12,712 passed (one GitHub-clone test from main, which hangs on the local machine on main too, deselected).

🤖 Generated with Claude Code

RetriggerView in GreptileConfidence Score: 5/5

No new blocking issue was found; the earlier reserved-path finding remains non-blocking.

Fix All in CursorFindings

  1. P2 Reserved paths reach site handlers ▶
Fix with agent prompt
### Issue 1
packages/agentenv-protocol/src/agentenv_protocol/website/environment.py:undefined-348
Adding the forwarded prefix to `scope["path"]` breaks `_unreserved`, which still checks that full path with `reserved_path`. For a site with `@api.post("/{rest:path}")`, a request to `/agentenv/unknown` with `X-Forwarded-Prefix: /website/shop` now runs the site's handler instead of returning 404. A write handler could change site data on a request that should be refused.

Check the site-relative path with `get_route_path(request.scope)` in `_unreserved`, as `_Fallback` now does.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Adds optional role keys, forwards the site's public address, and lets website triggers match named routes.

  • Keyed deploys require a secret beside each non-default role.
  • Proxied websites now see the public address and path callers used.
  • Website triggers can now target a named route from the site’s card.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Caller sends role and optional key] --> B[Gateway checks key]
  B --> C[Verified role or default]
  C --> D[Forward request with public address]
  D --> E[Website answers]
  E --> F[Match named route]
  F --> G[Run matching triggers]
Loading

Reviews (11) · Last reviewed commit: "feat(gateway): a keyed deploy's role key..." · Reviewed by Greptile

@williamloo-scale williamloo-scale changed the title website: gateway role key, forwarded headers, triggers by route (FD-4683) feat(gateway): a keyed deploy's role key, forwarded headers, and triggers by route (FD-4683) Nov 3, 2026
@williamloo-scale
williamloo-scale marked this pull request as ready for review November 3, 2026 00:24
@williamloo-scale
williamloo-scale requested a review from a team as a code owner November 3, 2026 00:24
Comment thread src/agent_env/env/gateway/rest_proxy.py Resolved
Comment thread src/agent_env/env/gateway/gateway.py Resolved
Comment thread src/agent_env/env/gateway/gateway.py Resolved
Comment thread src/agent_env/env/gateway/rest_proxy.py Resolved
prefix = forwarded_prefix(_first(headers.get(b"x-forwarded-prefix")))
if prefix:
scope["root_path"] = unquote(prefix) + scope.get("root_path", "")
scope["path"] = unquote(prefix) + scope["path"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Reserved paths reach site handlers

Adding the forwarded prefix to scope["path"] breaks _unreserved, which still checks that full path with reserved_path. For a site with @api.post("/{rest:path}"), a request to /agentenv/unknown with X-Forwarded-Prefix: /website/shop now runs the site's handler instead of returning 404. A write handler could change site data on a request that should be refused.

Check the site-relative path with get_route_path(request.scope) in _unreserved, as _Fallback now does.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/agentenv-protocol/src/agentenv_protocol/website/environment.py
Line: 348

Comment:
**Reserved paths reach site handlers**

Adding the forwarded prefix to `scope["path"]` breaks `_unreserved`, which still checks that full path with `reserved_path`. For a site with `@api.post("/{rest:path}")`, a request to `/agentenv/unknown` with `X-Forwarded-Prefix: /website/shop` now runs the site's handler instead of returning 404. A write handler could change site data on a request that should be refused.

Check the site-relative path with `get_route_path(request.scope)` in `_unreserved`, as `_Fallback` now does.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Cursor Fix in Claude Code Fix in Codex

@williamloo-scale
williamloo-scale force-pushed the wloo/website-notes-example branch from d76be33 to a68ff2a Compare November 3, 2026 21:50
@williamloo-scale
williamloo-scale force-pushed the wloo/website-gateway-auth branch from 8dcebd1 to 4c1fcc1 Compare November 3, 2026 21:50
@williamloo-scale
williamloo-scale force-pushed the wloo/website-notes-example branch from a68ff2a to f1a665a Compare November 3, 2026 22:52
@williamloo-scale
williamloo-scale force-pushed the wloo/website-gateway-auth branch from 4c1fcc1 to 22947c3 Compare November 3, 2026 22:52
…gers by route (FD-4683)

This PR's change, rebased as one commit onto #429.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

This branch has not been deployed

No deployments
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