WIP: agent playground with a model × harness picker - #70
Conversation
Adds /chat: one playground where both axes are chosen per turn — the model BitRouter routes to, and the harness driving the agent loop. Harnesses are a flat registry (lib/harnesses.ts) rendered as a single picker. Unavailable entries stay visible with the reason they cannot run, because the picker is the clearest statement of what the product is. - ai-sdk streamText loop + docs tools, no server session - pi in-process runtime on @ai-sdk/sandbox-just-bash - bitrouter-* BitRouter's own catalog over ACP, in a sandbox VM - claude-code / codex / opencode listed, vendor-API entries The BitRouter entries are the point: @ai-sdk/harness-acp is the AI SDK's generic adapter, and `bitrouter acp serve` speaks vanilla ACP over stdio, so one createACP() per catalog id turns BitRouter's agents into AI SDK harnesses with their traffic routed rather than going direct to a vendor. Session-backed harnesses share one pool (lib/harness-sessions.ts): process-local and evictable by design, which one long-lived `next start` on Railway supports. Changing either axis rebuilds the runtime, so the client starts a fresh chat rather than showing a transcript the agent has no memory of. Verified end-to-end against real Vercel Sandboxes (stages 1-3; see docs/bitrouter-acp-harness-plan.md §8). Two findings are baked into the code: `xz` is missing from the sandbox image and must be installed in onBootstrap, and @vercel/sandbox does not read the VERCEL_* env vars — they are threaded through explicitly. WIP: no full harness turn has run yet (needs router credentials and spends model tokens), and the gemini-cli entry is listed but permanently unavailable — it sends a non-Bearer key the router rejects. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A turn completes end to end on codex-acp (stop_reason=EndTurn, 4.7s), which settles the routed path, --model, and the bridge's WebSocket dial. Three blockers found, none in the AI SDK integration: - anthropic/claude-haiku-4.5 and qwen/qwen3.8-max have no tools-capable provider, so they cannot back any ACP harness - --model does not take effect for claude-acp (ANTHROPIC_MODEL is ignored by claude-code-acp 0.16.2); codex-acp is unaffected - host tools reach the sandbox but are not surfaced into the agent's toolset Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Stage 4 findings are recorded in the plan ( That settles the routed path, Three blockers remain, all upstream of this PR rather than in the AI SDK wiring:
Given (2) and (3), I'd narrow the shipped set to |
The playground spent one shared BITROUTER_API_KEY on behalf of anonymous
visitors, read from process.env at each point of use. That is the reason
Pi and the ACP harnesses ship dark: a live agent session per visitor,
with a shell and a sandbox VM behind it, and nothing capping the spend.
Introduce lib/playground-credential.ts as the single place the playground
learns who is paying, and make everything downstream take a resolved
credential as an argument. Two implementations:
byo-key (default) BITROUTER_API_KEY from the environment. A clone with
nothing configured behaves exactly as before.
session forwards the visitor's console session cookie to the
console's token endpoint, server-side, and receives a
short-lived credential scoped to them. Keeping the forward on
the server matters: a minted bra_ is a signed JWT the router
verifies with no round trip, so nothing can revoke it before it
expires, and it must not reach page JavaScript.
resolveCredential throws CredentialError with the status the caller should
send, so the route can tell "not signed in" (401) from "grant spent" (402)
from "console down" (503) from "misconfigured deployment" (500) — three
cases the playground has to report differently.
Session-backed harnesses hold their token in a process environment with no
refresh path, so the credential is bound when the agent is built. The pool
holds it, revokes it on teardown, and rebuilds rather than hand back a
session whose next turn would 401.
The ACP change is the subtle one. forwardEnv names variables on *this*
process, so it can only ever forward the house key. The adapter's `env`
option carries a value, and is documented as persisting it "in bootstrap
and lifecycle compatibility identity" — but that value reaches only
implementationIdentity, a hash gating resume-compatibility within one
session, where the credential is fixed. It does not reach the sandbox's
implementation.json (env keys only) or the bootstrap file set, so the
snapshot BOOTSTRAP_HASH gates stays shared across visitors instead of
paying a cold `dnf install xz` per session.
Availability in harnesses.server.ts is now mode-aware — session mode has no
BITROUTER_API_KEY, and every harness would otherwise have greyed out.
Phase 1 ships gate-don't-degrade, the opposite of the plan's §5.2. The
anonymous house-key fallback is only safe behind an IP rate limit and a
restricted model set, both later phases; shipping it now would mean an
uncapped house key on an unauthenticated page. Recorded in the plan with
the two places to reopen it.
Nothing mints yet — the console endpoint is phase 2. Verified locally that
byo-key reaches the router unchanged and that session mode 401s a
signed-out request.
Testing the seam needed a server-only stub alias in vitest.config.ts; that
module is a Next build-time marker with no npm package behind it, which is
why this repo's server surface had no tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Phase 1: a per-visitor credential seam (e14bfbd)The playground spent one shared
The forward stays on the server deliberately. A minted
The ACP change is the subtle one
So the snapshot Also in here
Verified locally
149 tests pass; no new type errors (the 7 remaining are pre-existing, in One deliberate divergencePhase 1 ships gate, don't degrade — the opposite of the plan's §5.2, which wanted signed-out visitors to fall back to the house key on That fallback is only safe behind an IP rate limit and a restricted model set, both later phases. Shipping it now would mean an uncapped house key on an unauthenticated page — the exact thing this plan exists to end. Gating is the conservative state to hold meanwhile, and reopening it is a change to two places, recorded in §3.4. Not in hereNothing mints yet. The console's |
Draft — not ready to merge. See Before this ships below.
Adds
/chat: one playground where both axes are chosen per turn — the model BitRouter routes to, and the harness driving the agent loop. Replaces the earlier split between/chatand/chat/harness.The harness axis
A flat registry (
lib/harnesses.ts) rendered as one picker. Entries that cannot run stay visible with the reason rather than being hidden — the picker is the clearest statement of what the product is, so an unconfigured option should still show.ai-sdkstreamTextloop + docs tools, no server sessionpi@ai-sdk/sandbox-just-bashbitrouter-claude-acp/-codex-acp/-gemini-cli/-pi-acpclaude-code/codex/opencodeThe BitRouter entries are the point.
@ai-sdk/harness-acpis the AI SDK's generic adapter —@ai-sdk/harness-grok-buildis nothing but acreateACP()call — andbitrouter acp servespeaks vanilla ACP over stdio. So onecreateACP()per catalog id turns BitRouter's agents into AI SDK harnesses whose traffic is routed rather than going direct to a vendor.BitRouter · Claude CodeandClaude Codesit side by side deliberately: same runtime, different endpoint.Notable design points
lib/harness-sessions.ts) — process-local and evictable by design, which one long-livednext starton Railway supports. Documented as such rather than pretending to durability.resetsHistory).lib/harness-sandbox.server.ts) so the intended Railway swap is a single edit.Verified
Typecheck clean, 129 tests, build compiling, picker checked in-browser. End-to-end against real Vercel Sandboxes, stages 1–3 (plan §8):
Two findings from that run are baked into the code:
xzis missing from the sandbox image. glibc 2.34 < 2.35 means bitrouter falls back to its musl.tar.xz, which tar cannot unpack. Fixable inonBootstrap— which runs after the adapter's bootstrap — only because pnpm 10 ignores bitrouter's build script, so the binary is fetched lazily on first spawn. Verified in production order.@vercel/sandboxnever readsVERCEL_TOKEN/PROJECT_ID/TEAM_ID. They appear only in its README as values a caller passes explicitly. They are now threaded through, and the local OIDC path is recognised.Before this ships
--direct, so the routed path (--base-url+ key), whether--modelaccepts a rawCHAT_MODELSid, and the bridge's WebSocket dial are all unexercised./api/chat/playgroundis unauthenticated and spends the shared key; the BitRouter entries are a VM per chat session. Keep the sandbox credentials unset until this is addressed.session/resume. The playground does not need it; recommendation is no, plus a two-line masking fix inbitrouter.Known issues (deliberate)
bitrouter-gemini-cliis listed but permanently unavailable: it sends a non-Bearer key the router rejects, and is deprecated upstream. Encoded asknownIssuerather than shipped broken.components/ai/search.tsx(buttonVariants({ color })) andcomponents/markdown.tsxare untouched.Plan and full findings:
docs/bitrouter-acp-harness-plan.md🤖 Generated with Claude Code