#902

Fix shared-page chat prop leak and AI Gateway credential brokering

Openblurrahwants to merge intofrom

Fixes the two security findings raised on #890. Targets blurrah/agent-harness-sdk rather than main, since both pieces of affected code land with that PR.

1. Public shared pages no longer receive the chat row

/shared/[shareId] passed the whole chats row into SharedChatContent. That component is a client component, so every field became part of the page's RSC payload — a page that needs no session to load. The row now carries harnessSessionState: the opaque external-harness resume blob (chats.harness_session_state) that only the chat workflow reads, so it was readable by anyone holding a share link.

The page now projects the row before it becomes a prop:

  • apps/web/app/shared/[shareId]/public-shared-chat.ts defines PublicSharedChat (id, title — the only fields the component renders) and toPublicSharedChat().
  • SharedChatContent's chats[].chat is typed as PublicSharedChat instead of Chat, so the row cannot be passed by accident.

It is an allowlist, not an omit-list on purpose: Omit<Chat, "harnessSessionState"> would have fixed today's leak and shipped the next server-only column added to chats. This matches how the chat APIs already treat the same field (ChatSummary in lib/db/sessions.ts, SessionChat in hooks/use-session-chats.ts).

The other two public share surfaces were checked and already project explicitly: /api/shared/[shareId]/status returns only isStreaming, and the markdown route reads named fields.

2. The deployment's AI Gateway credential is no longer brokered into every sandbox

buildDefaultCredentialBrokeringPolicy fell back to process.env.AI_GATEWAY_API_KEY, so any sandbox created or reconnected by this deployment got a network policy that makes the platform attach Authorization: Bearer <deployment gateway credential> to its requests to ai-gateway.vercel.sh.

Nothing in the app ever passed aiGatewayApiKey explicitly, so the environment fallback was the only thing enabling brokering — and it applied to all ~30 connect paths: session provisioning, lifecycle kicks and hibernation, dev-server and code-editor connects, git status/diff/files reads, commit and PR flows, snapshot refreshes, base-snapshot builds, and agent tool calls. A sandbox runs untrusted code — the agent's bash tool, repository build scripts and dev servers on public preview URLs, the in-sandbox editor — so any of that could spend the deployment's gateway budget (or use its OIDC token) with the platform signing the requests, unmetered and unattributable to a user. Classic confused deputy.

Brokering is now opt-in per create/connect call, and reads nothing from the environment:

  • packages/sandbox/vercel/sandbox.ts: the env fallback (and the "no key available" warning that went with it) is gone. Passing aiGatewayApiKey is the opt-in; the rationale is documented where the policy is built so a fallback does not get reintroduced as a convenience.
  • aiGatewayApiKey is threaded through ConnectOptions (packages/sandbox/factory.ts, packages/sandbox/vercel/connect.ts) so a caller can actually opt in through connectSandbox.
  • apps/web/app/api/internal/harness-runner/route.ts is the one caller that does. It already refreshed the credential with ensureGatewayApiKeyEnv() before connecting; it now uses the returned key instead of leaving it to the environment, i.e. brokers it for the turn it is about to run.
  • scripts/codex-harness-smoke.ts does the same, so the smoke path keeps exercising what production does. Its auth precedence is unchanged (gateway env, then CODEX_API_KEY/OPENAI_API_KEY, then mint).

Because a policy update replaces the whole policy, a connect that does not opt in also revokes brokering a previous harness turn established — a session sandbox does not keep standing gateway access after its harness turn ends.

What this does not claim to fix

The AI SDK harness adapters resolve the gateway credential from the host environment and pass it as the env of the bridge process they spawn inside the sandbox (resolveClaudeCodeEnv/resolveCodexEnv → AI_GATEWAY_API_KEY, ANTHROPIC_API_KEY, CODEX_API_KEY). So during an external-harness turn the credential is present inside that VM regardless of the network policy. That is upstream behavior in @ai-sdk/harness-* and out of scope here; what changes is that a sandbox no longer gets standing, platform-signed gateway access just for existing — only a harness turn puts the credential in reach, and only in the sandbox running that turn.

Tests

  • packages/sandbox/vercel/sandbox.test.ts: the four tests that set AI_GATEWAY_API_KEY and asserted brokering now assert the opposite — with the deployment key in the environment, create, connect, and a later setGitHubAuthToken update all produce a policy with no ai-gateway.vercel.sh entry. Explicit-key coverage (create, connect, preserved across GitHub setup-auth clearing) is kept and no longer depends on the environment.
  • packages/sandbox/vercel/connect.test.ts (new): pins that connectVercel forwards the caller's key on the named-connect path, the create path, and the create-if-missing fallback — and passes nothing when the caller does not opt in. VercelSandbox is mocked, so nothing here touches the Vercel SDK.
  • apps/web/app/api/internal/harness-runner/route.test.ts: the accepted-request test now asserts connectSandbox receives aiGatewayApiKey, and a new test covers a run where no credential is available (nothing is brokered rather than an undefined key being passed).
  • apps/web/app/shared/[shareId]/page.test.ts: a chat row carrying harnessSessionState renders with chat equal to exactly { id, title }, and the serialized props do not contain the resume payload.

Docs: AGENTS.md gains a "Sandbox credential brokering" section next to the internal-endpoints rules, docs/agents/lessons-learned.md records both lessons, and apps/web/.env.example documents AI_GATEWAY_API_KEY and its blast radius.

pnpm check, pnpm typecheck, pnpm test:isolated (all 60 test files), and pnpm --dir apps/web db:check pass.

🤖 Generated with Claude Code

Activity

2
vercel[bot]2mo ago

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
open-agents ReadyPreview, v0Aug 29, 2026 8:07am