Skip to content

Add a flag-gated inference gateway so provider keys stay out of task sandboxes#447

Merged
mrubens merged 8 commits into
developfrom
inference-gateway
Jul 17, 2026
Merged

Add a flag-gated inference gateway so provider keys stay out of task sandboxes#447
mrubens merged 8 commits into
developfrom
inference-gateway

Conversation

@mrubens

@mrubens mrubens commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds an inference gateway at /api/inference/:provider/* and, behind the InferenceGateway feature flag, routes task-sandbox inference through it so provider API keys stay on the control plane. Every key-authenticated provider in the setup catalog is covered: OpenRouter, Anthropic, OpenAI, Google Gemini, Vercel AI Gateway, Requesty, Baseten, Together AI, Moonshot AI, MiniMax, OpenCode Zen, xAI, and Amazon Bedrock (Mantle, with region-templated upstreams resolved from AWS_REGION per request). Google Vertex (request signing) and ChatGPT subscriptions (client-managed OAuth refresh) still flow directly. Sandboxes authenticate with their run-scoped token (ROOMOTE_CLOUD_TOKEN); the gateway resolves the deployment's provider key server-side, injects it upstream, and streams the response back.

This extends the existing broker pattern (integration MCP proxy, git credential proxy, comms brokering, OIDC federation) to LLM traffic. The flag is off by default; nothing changes for deployments that don't enable it.

The gateway

  • Auth: registered as task-token in the central route-policy manifest; the handler additionally verifies the run token's task run still exists (same pattern as the MCP proxy). The Anthropic and Gemini SDKs send their "API key" through x-api-key / x-goog-api-key rather than Authorization, so the token middleware accepts the run token from those headers on the /api/inference surface only.
  • Path allowlists: each provider exposes exact inference endpoints only (messages/chat/responses/embeddings/models, plus count_tokens); Google model routes match nested paths because the model ID and action live below /models. Everything else 403s, so a run token can never reach a provider's account, billing, or admin surface (e.g. /v1/messages/batches and OpenRouter /v1/key are rejected).
  • Key resolution: server-side with the same precedence the task runtime uses (runtime env first, then persisted encrypted deployment env vars), including the GOOGLE_GENERATIVE_AI_API_KEY alias for Gemini. MiniMax and Bedrock Mantle proxy through their Anthropic-compatible endpoints; the aggregators are OpenAI-compatible bearer-auth; Vercel uses the @ai-sdk/gateway protocol under /v1/ai. Upstream bases follow models.dev, the registry OpenCode itself resolves providers from.
  • Streaming: requests and responses stream through in both directions using the no-body-timeout dispatcher and logged proxy stream wrapper, so long SSE inference streams work and client disconnects clean up.

The flag-gated wiring

Flag: InferenceGateway (deployment metadata key inference_gateway), togglable from the admin metadata controls. Evaluation fails closed to direct provider keys if unavailable, so a Redis outage cannot break task inference.

When enabled:

  • The dequeue/resume/env-reload path withholds gateway-covered provider keys from the sandbox env (including raw deployment env vars carrying those key names) and emits R_INFERENCE_GATEWAY_URL derived from the sandbox-reachable platform API URL. Control-plane inference (routing, titles, summaries) keeps direct keys since it holds no run token.
  • Each controller spawn path evaluates the flag and withholds covered operator keys from the worker daemon env.
  • The worker's OpenCode config rebases covered providers onto the gateway (baseURL = gateway URL + provider + SDK base-path suffix, apiKey = {env:ROOMOTE_CLOUD_TOKEN}), following the Bedrock Mantle pattern. OpenRouter attribution headers are preserved; the openai provider is untouched when a ChatGPT subscription record is present.
  • Keys the gateway cannot serve (Vertex service-account JSON) and ChatGPT-subscription OAuth still flow directly; AWS_REGION also still flows since it is configuration, not a secret.

Provider definitions (ids, key env vars, upstream URLs, allowlists, SDK base-path suffixes) live in @roomote/types so the gateway and worker share one source of truth.

Testing

  • Gateway handler tests: token-type rejection, missing task run, unknown provider, exact-path allowlist (including account-surface paths nested under former prefixes), missing key, header injection/stripping for all three auth styles, query string and SSE passthrough, upstream error handling, method restrictions
  • Resolver, OpenCode config, middleware token-extraction, and worker-env tests for the flag-gated behavior
  • Full suites pass across types, feature-flags, db, sdk, compute-providers, controller, api, and worker; typecheck, lint, knip clean
  • Verified end-to-end locally (Modal sandbox, OpenRouter): opencode config rebased onto the gateway, no provider key values anywhere in the sandbox filesystem or process envs, task inference streamed through /api/inference/openrouter/v1 and completed normally

Rollout notes

  • Suggested order: enable the flag on internal deployments (OpenRouter) first.
  • Environment-snapshot setup env still carries provider keys when stored as deployment env vars (setup commands may reference them); the pre-snapshot scrub ([Fix] Snapshot credentials persist after task sleep #445) keeps them out of snapshot images.

Adds /api/inference/:provider/* to the API server. Task sandboxes
authenticate with their run-scoped token; the gateway resolves the
deployment's provider API key server-side (runtime env first, then
persisted deployment env vars), injects it upstream, and streams the
response back. Upstream paths are allow-listed per provider so run
tokens can only reach inference endpoints.

Covers OpenRouter, Anthropic, OpenAI, and Google Gemini. Providers that
need request signing (Vertex) or client-managed OAuth refresh (ChatGPT
subscriptions) are not proxied yet.
@roomote-roomote

roomote-roomote Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

No code issues found. See task

  • High apps/api/src/handlers/inference/registry.ts - Narrowed provider allowlists to explicit inference endpoints, rejecting Anthropic batch and OpenRouter key-management paths.
  • Medium packages/db/src/lib/model-runtime-config.ts - Provider-key lookup now queries and decrypts only requested persisted keys.
  • Medium apps/api/src/handlers/inference/registry.ts - Google now accepts both GEMINI_API_KEY and GOOGLE_GENERATIVE_AI_API_KEY.
  • High apps/worker/src/run-task/run-task.ts - Gateway-served keys are stripped from the merged harness env, including keys inherited by warm workers, before the gateway URL is added.
  • High apps/web/src/lib/mock-utils.ts - Added InferenceGateway to the exhaustive mock feature-flag map.

Reviewed 401bb08

@mrubens

mrubens commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

@roomote-roomote address the feedback

@roomote-roomote

Copy link
Copy Markdown
Contributor

I'm on it. I started a dedicated PR follow-up task for this request, and I'll keep updates here. See task

@roomote-roomote

roomote-roomote Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Addressed all three inference-gateway findings in 593d95d:

  • Restricted provider routes to explicit inference endpoints.
  • Targeted persisted provider-key lookups instead of decrypting every deployment variable.
  • Added the GOOGLE_GENERATIVE_AI_API_KEY fallback for Gemini.

Focused API and DB tests passed, along with API/DB type checks and the pre-push lint, typecheck, and knip gates. See task Revert this commit

@roomote-roomote roomote-roomote Bot changed the title Add inference gateway for task sandbox model provider traffic [Fix] Prevent task sandboxes from reaching provider account endpoints Jul 16, 2026
* Route sandbox inference through the gateway behind a deployment flag

When R_INFERENCE_GATEWAY is enabled (control-plane env or persisted
deployment env var), task sandboxes no longer receive raw provider API
keys for gateway-covered providers (OpenRouter, Anthropic, OpenAI,
Google Gemini). Instead:

- The dequeue/resume env resolver emits R_INFERENCE_GATEWAY_URL and
  withholds covered keys from the sandbox env (control-plane inference
  keeps direct keys since it holds no run token).
- The worker daemon launch env skips covered operator keys.
- The worker's OpenCode config rebases covered providers onto the
  gateway with the run token as the API key, preserving OpenRouter
  attribution headers and leaving ChatGPT-subscription OAuth and
  Bedrock/Vertex untouched.
- The API token middleware accepts the run token from x-api-key /
  x-goog-api-key on the inference gateway surface only, since the
  Anthropic and Gemini SDKs send their key through those headers.

Provider definitions are shared between the gateway and the worker via
@roomote/types so path suffixes and env keys cannot drift.

* Gate the inference gateway on a feature flag instead of an env var

Replaces the R_INFERENCE_GATEWAY env var with an InferenceGateway
feature flag stored in deployment metadata (metadata key
inference_gateway), so the gateway can be toggled from the admin
controls like other deployment-level features.

- resolveEffectiveModelRuntimeEnv takes an explicit inferenceGateway
  option instead of sniffing env vars; the dequeue path evaluates the
  flag through the shared evaluator and fails closed (direct keys) if
  flag evaluation is unavailable.
- Worker daemon env builders take inferenceGatewayEnabled via
  BuildWorkerEnvOptions; each controller spawn path evaluates the flag
  before building the env.
- Control-plane inference is unaffected: callers that do not pass the
  option keep direct provider keys.

---------

Co-authored-by: Matt Rubens <2600+mrubens@users.noreply.github.com>
@mrubens mrubens changed the title [Fix] Prevent task sandboxes from reaching provider account endpoints Add a flag-gated inference gateway so provider keys stay out of task sandboxes Jul 16, 2026
Comment thread packages/types/src/inference-gateway.ts Fixed
Comment thread packages/types/src/inference-gateway.ts Fixed
mrubens added 3 commits July 16, 2026 18:06
Extends the gateway registry from four providers to every provider the
setup catalog supports with API-key auth: Vercel AI Gateway, Requesty,
Baseten, Together AI, Moonshot AI, MiniMax, OpenCode Zen, xAI, and
Amazon Bedrock (Mantle). Upstream bases follow models.dev, the registry
OpenCode itself resolves providers from.

- MiniMax and Bedrock Mantle proxy through their Anthropic-compatible
  endpoints (x-api-key); the rest are OpenAI-compatible bearer-auth.
- Bedrock upstreams are region-templated: the gateway resolves
  AWS_REGION (deployment env, default us-east-1) per request and
  validates it before substitution. The region var still flows to
  sandboxes; only the bearer token is withheld.
- Nested-path allowance is now a per-provider registry field instead of
  a hardcoded Google check, reused for the Vercel AI Gateway protocol.
- The worker's gateway rebase needs no changes: it is model-prefix
  driven, and the Bedrock Mantle merge runs before the gateway merge so
  its baseURL/apiKey are overridden while its model config is kept.

Still direct: Google Vertex (request signing) and ChatGPT subscriptions
(client-managed OAuth refresh).
Merges develop and addresses the /code-review findings, consolidating
the gateway decision to a single per-run signal.

- Gate the /api/inference endpoint on the InferenceGateway flag: it was
  live on every deployment, so a run token could pull a provider key its
  sandbox never held. Fails closed on evaluation error.
- Reject encoded-slash / dot-segment traversal on nested-path providers
  (google, vercel) so a run token cannot ride the allowlist to a
  non-inference upstream endpoint.
- Build the sandbox gateway URL worker-side from the container-reachable
  platform URL instead of dequeue's raw TRPC_URL (fixes a Docker/
  self-host outage where the emitted localhost URL was unreachable).
- Thread one authoritative served-keys list (R_INFERENCE_GATEWAY_KEYS):
  the resolver withholds and advertises exactly the configured,
  gateway-covered keys; dequeue redaction strips exactly that set (so a
  provider key the deployment set for its own code survives); the worker
  strips exactly that set from the harness env (defeating daemon-env
  re-injection on pre-flag sandboxes) and rebases exactly those
  providers (so a repo-pinned provider is not withheld-without-rebase).
- Apply the same gateway withholding to the snapshot env path.
- Prefer GOOGLE_GENERATIVE_AI_API_KEY over GEMINI_API_KEY to match the
  SDK's pre-gateway precedence when both are set.
- Share one fail-closed flag evaluator across dequeue, spawn, and the
  gateway; share the Bedrock region/id constants via @roomote/types.
- Fix stale comments that claimed Bedrock keys still flow directly.
@mrubens

mrubens commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the /code-review findings in fcf06bef9 (also merged develop, which fixes the duplicate-import CI failure). Summary:

Fixed in code

  • Flag gate on the endpoint/api/inference now checks the InferenceGateway flag and fails closed; it is no longer live on deployments that haven't opted in.
  • Encoded-traversal guard..%2F / dot-segments are rejected before the nested-path allowlist (google, vercel).
  • Docker/self-host URL — the sandbox gateway URL is now built worker-side from the container-reachable platform URL, not dequeue's raw TRPC_URL.
  • Single served-keys signal (R_INFERENCE_GATEWAY_KEYS) — the resolver withholds/advertises exactly the configured gateway-covered keys; dequeue redaction, the worker harness-env strip, and the provider rebase all key off that one set. This fixes: warm-sandbox daemon-env re-injection, the redaction-stripped-user-keys regression, and the withheld-without-rebase asymmetry for repo-pinned providers.
  • Snapshot env now goes through the same gateway-aware withholding.
  • Gemini precedence prefers GOOGLE_GENERATIVE_AI_API_KEY; stale Bedrock comments corrected; flag evaluator and Bedrock constants de-duplicated.

Rollout requirements (documented, not code-enforced)

  • Enable the flag only once all worker sandboxes/snapshots are on a build containing this change — an old worker resuming a snapshot won't understand R_INFERENCE_GATEWAY_KEYS and would get neither a key nor a gateway route (same class as the R_* rename rollout).
  • Drain warm/standby sandboxes when flipping the flag: a sandbox spawned before the flip keeps provider keys in its daemon /proc until recycled (the harness env and env.sh are scrubbed, but the daemon's own initial environ can't be rewritten in place).
  • Gateway mode requires provider keys reachable by the API service (deployment env or DB), not only the controller's process env; the gateway returns a clear 404 otherwise.

mrubens added 2 commits July 16, 2026 19:26
The helper now calls getRedis(); without Redis (CI) the real connection
floods ioredis unhandled-error events and fails the modal/daytona/blaxel
spawn suites. Mock it to a fixed value like the other spawn dependencies.
CodeQL flagged the anchored `/\/+$/` in the gateway URL builders as a
polynomial regular expression on uncontrolled input. Strip trailing
slashes with a bounded char-code loop instead; add unit coverage for the
URL builders and key lookups.
@mrubens

mrubens commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

@roomote-roomote review this again

@roomote-roomote

Copy link
Copy Markdown
Contributor

I started a PR review task for this request. See task

@mrubens
mrubens merged commit eaf3639 into develop Jul 17, 2026
16 of 17 checks passed
@mrubens
mrubens deleted the inference-gateway branch July 17, 2026 00:11
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.

2 participants