Skip to content

fix(env): serve pgweb and db-mcp only under a path each gateway deploy draws - #455

Closed
earakely-scale wants to merge 2 commits into
mainfrom
edgararakelyan/sql-console-secret-path
Closed

earakely-scale wants to merge 2 commits into
mainfrom
edgararakelyan/sql-console-secret-path

Conversation

@earakely-scale

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

Copy link
Copy Markdown
Collaborator

What

Each gateway deploy now draws a random path for pgweb and db-mcp, and the URLs it records carry that path.

  • pgweb reads the path from PGWEB_URL_PREFIX, in compose and in container mode. It answers 404 outside the path.
  • db-mcp serves MCP at /<path>/mcp on the VM path. postgres-mcp has no option for its path, so compose starts it through a two-line shim that sets FastMCP's streamable_http_path.
    • The shim needs no image change. Prod's promoted default-db v8 has the same build digest as main, so the shim runs on the images prod uses today.
    • Container mode can't change db-mcp's entrypoint, so db-mcp keeps /mcp there.
  • Opt-in per deploy. LocalPostgresStateProvider.console_path defaults to None, and _launch draws a path for each deploy attempt. Every other render, such as the SDK's exports, is byte-identical to today.
  • Logs.
    • The logged compose redacts PGWEB_URL_PREFIX and DB_MCP_PATH.
    • The deploy logs the console URLs with the path masked.
    • The consoles' own output, which prints the path, is masked when the deploy copies it into its log.
  • run_code gives a script on the env's host input["env"]["db_mcp_url"], with the path, as it already gives mcp_url.

Why

Both consoles hold the servicedb's superuser DSN and accept any SQL.

  • The agent's website browser shares their compose network.
  • On a remote VM, they are also published on every interface and through public tunnels.

So the agent, or anyone with a tunnel URL, could run SQL around the gateway. Measured on main: the browser's same-origin POST /api/query returned rows, by name and by the VM's host IP.

Moving the browser onto its own network was tried first. A container on another network still reaches a port published on all interfaces through its gateway IP, docker0 or the VM IP, so that would have left every prod VM open. The path closes all three routes: by name, by host address and through the tunnel.

Design and full test record: https://claude.ai/artifact/L6Hm3t52JuTkRzxKYTgwNG

Compatibility

A consumer audit covered 145 sites across this repo, the Scale monorepo and synthetic-pipelines. 113 use the recorded URL as given and keep working.

Two consumers rebuild the URL from a port. They get fixes in their own PRs, and those fixes work with old and new URLs alike:

  • the viewer's "Connect live";
  • synthetic-pipelines qc_e2e.

Records from older deploys keep their old shape, and every consumer reads what is recorded.

Not covered

  • Container-mode db-mcp, until a db-mcp image that reads its path is promoted.
  • Envs deployed by older framework pins, until those pins are bumped: modal-proxy, and warm-pool envs built earlier.
  • The gateway's unauthenticated /triggers executor, which the agent can make POST to any URL on env-network. Without the path, the consoles answer it 404 too; closing that request source needs the gateway control token.

Tests

  • Unit: compose renders with and without a path, the container spec, container-mode URLs, the redaction, and run_code's DB MCP URL. All fail on main. The unit suite passes (11,567).

  • Integration: the website test now checks that the browser gets not found from both consoles, and that the operator's recorded URLs work while the bare hosts return 404.

  • EC2 box: the full gateway integration suite (27 passed) and the env, artifact and bundle suites (81 passed).

  • Chaos run on EC2, main vs this branch. It covered:

    • the browser's attack, by name and by host IP;
    • the operator's pgweb UI, its assets, /api/query, db-mcp tools and execute_sql, directly and through a path-prefix proxy;
    • console restarts and a full recreate;
    • 4 concurrent deploys.

    Every route closes and every legitimate flow keeps working. The second box run caught a regression: the local provider rewrites /app paths, which broke the shim's first form. It is fixed and covered.

  • Real Modal:

    • modal_vm: the browser is refused by name and via the VM's host IPs, and everything the operator uses works at the recorded URLs.
    • A Modal container started by create_container with only PGWEB_URL_PREFIX: 404 outside the path, 200 inside it.

🤖 Generated with Claude Code

RetriggerView in GreptileConfidence Score: 5/5

The follow-up changes appear safe to merge; both previous findings are fixed.

Summary

Gateway deploys give SQL consoles a random path and record that path in their URLs.

  • Gateway deploys put local SQL consoles behind a fresh URL path.
  • Host scripts receive the gateway's recorded DB MCP URL.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Gateway deploy draws a random path] --> B{Deployment mode}
  B -->|VM| C[pgweb and db-mcp use the path]
  B -->|Containers| D[pgweb uses the path; db-mcp keeps /mcp]
  C --> E[Record console URLs]
  D --> E
  E --> F[Give run_code the DB MCP URL]
  A --> G[Mask the path in deploy logs]
Loading

Reviews (2) · Last reviewed commit: "fix(env): log a deploy's console URLs wh..." · Reviewed by Greptile

…y draws

pgweb and db-mcp hold the servicedb's superuser DSN and take any SQL. Their ports are published, on a remote VM on
every interface and through public tunnels, and the agent's website browser shares their compose network, so the
agent, or anyone with the tunnel URL, could run SQL around the gateway.

Each gateway deploy now draws a random path for them, and the URLs it records carry it:

- pgweb reads it from PGWEB_URL_PREFIX, in the compose file and in container mode alike, and answers 404 outside it.
- db-mcp serves MCP at /<path>/mcp on the VM path. postgres-mcp has no option for it, so compose starts it through
  a two-line shim that sets FastMCP's streamable_http_path; it runs on the db-mcp images already promoted. Container
  mode, whose entrypoint can't be changed, keeps /mcp.
- Without a path, as every render outside a deploy has (exports), both serve at their roots, as before.
- The logged compose redacts both settings, the deploy logs the URLs and the consoles' own output with the path
  masked, and run_code hands a script on the env's host its DB MCP URL with the path, as it does its MCP URL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@earakely-scale
earakely-scale requested a review from a team as a code owner November 2, 2026 23:42
else LocalPostgresStateProvider()
)
if isinstance(self._state_provider, LocalPostgresStateProvider):
self._state_provider.console_path = secrets.token_urlsafe(18)

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 Token size lacks a name

The new secrets.token_urlsafe(18) leaves the path's random size as an unexplained inline number. The repository requires magic numbers to live in descriptively named class or instance variables. Move 18 into a named variable that makes its byte unit clear. This repository requirement must be satisfied before merging.

Rule Used: Store magic numbers as class or instance variables with descriptive names rather than using them inline in the code. (source)

Learned From
scaleapi/scaleapi#126388

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agent_env/providers/env_providers/env_gateway_provider.py
Line: 1377

Comment:
**Token size lacks a name**

The new `secrets.token_urlsafe(18)` leaves the path's random size as an unexplained inline number. The repository requires magic numbers to live in descriptively named class or instance variables. Move `18` into a named variable that makes its byte unit clear. This repository requirement must be satisfied before merging.

**Rule Used:** Store magic numbers as class or instance variables with descriptive names rather than using them inline in the code. ([source](https://app.greptile.com/-/switch-tenant?tenant=scale-ai&next=%2Fcustom-context%3Fmemory%3D002e0051-41ad-46c1-9098-47433c580150))

**Learned From**
[scaleapi/scaleapi#126388](https://github.com/scaleapi/scaleapi/pull/126388)

---

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

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Cursor Fix in Claude Code Fix in Codex

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 80b2e10: _CONSOLE_PATH_BYTES = 18, next to the module's other constants.

A caller's existing VM may lack the pgweb tunnel; masking the log line then read None. The path's size gets a name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@earakely-scale

Copy link
Copy Markdown
Collaborator Author

Closing unmerged. Instead of a secret path, the SQL consoles will move behind the gateway and be gated by the operator credential the gateway work adds. The branch stays for reference.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant