feat(cloud): sandbox control-plane Worker (white-label Vercel) (#11)

* feat(cloud): sandbox control-plane Worker (Vercel behind an interface, entitlements, usage, egress proxy)

* ci: collect sandbox-control tests
This commit is contained in:
Neil
2026-11-09 02:19:22 -08:00
committed by Neil
parent 4f062602e4
commit c2ebfd00aa
29 changed files with 2694 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
.dev.vars
.wrangler/
node_modules/
@@ -0,0 +1,52 @@
-- One row per Orca Cloud workspace sandbox. Written before any provider call (write-ahead), so a
-- crash mid-create leaves a row the cron reconciles instead of an untracked VM.
CREATE TABLE sandboxes (
id TEXT PRIMARY KEY,
account_id TEXT NOT NULL,
user_id TEXT NOT NULL,
workspace_id TEXT NOT NULL,
provider TEXT NOT NULL,
provider_name TEXT NOT NULL UNIQUE,
status TEXT NOT NULL,
machine_size TEXT NOT NULL,
region TEXT NOT NULL,
-- Bumped on every transition; lifecycle writes compare-and-set on it.
generation INTEGER NOT NULL DEFAULT 0,
session_id TEXT,
session_started_at INTEGER,
session_deadline_at INTEGER,
lease_expires_at INTEGER,
-- Start of the not-yet-reported usage segment of the current session.
usage_settled_at INTEGER,
last_error TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
destroyed_at INTEGER
);
CREATE UNIQUE INDEX sandboxes_live_workspace
ON sandboxes (account_id, workspace_id) WHERE status <> 'destroyed';
CREATE INDEX sandboxes_status ON sandboxes (status, updated_at);
-- Every snapshot id seen for a sandbox, so destroy can prove none outlive it.
CREATE TABLE sandbox_snapshots (
snapshot_id TEXT PRIMARY KEY,
sandbox_id TEXT NOT NULL REFERENCES sandboxes (id),
recorded_at INTEGER NOT NULL,
deleted_at INTEGER
);
CREATE INDEX sandbox_snapshots_sandbox ON sandbox_snapshots (sandbox_id);
-- Usage segments owed to billing. Written before the row's settle cursor advances, so a billing
-- outage delays reports instead of dropping them; usage_id makes resends idempotent upstream.
CREATE TABLE usage_outbox (
usage_id TEXT PRIMARY KEY,
sandbox_id TEXT NOT NULL REFERENCES sandboxes (id),
account_id TEXT NOT NULL,
machine_size TEXT NOT NULL,
started_at INTEGER NOT NULL,
ended_at INTEGER NOT NULL,
attempts INTEGER NOT NULL DEFAULT 0,
sent_at INTEGER
);
CREATE INDEX usage_outbox_pending ON usage_outbox (sent_at, started_at);
+24
View File
@@ -0,0 +1,24 @@
{
"name": "@orca-cloud/sandbox-control",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "wrangler dev --port 8791 --ip 127.0.0.1",
"db:migrate:local": "wrangler d1 migrations apply SANDBOX_DB --local",
"typecheck": "tsc --noEmit",
"test": "vitest run"
},
"dependencies": {
"jose": "^6.2.13",
"zod": "^4.6.7"
},
"devDependencies": {
"@cloudflare/workers-types": "^5.20261103.1",
"@types/node": "^24.19.2",
"tsx": "^4.23.15",
"typescript": "^5.9.3",
"vitest": "^5.0.5",
"wrangler": "^4.157.0"
}
}
+73
View File
@@ -0,0 +1,73 @@
// Desktop callers present a short-lived ES256 JWT minted by Orca Cloud auth (aud=orca-sandbox,
// purpose=sandbox), verified against the auth JWKS — the billing Worker's pattern.
import {
createLocalJWKSet,
createRemoteJWKSet,
jwtVerify,
type JSONWebKeySet,
type JWTVerifyGetKey
} from 'jose'
import { z } from 'zod'
import type { Env } from './env'
const ID = z
.string()
.min(1)
.max(128)
.regex(/^[A-Za-z0-9_.:-]+$/)
const ClaimsSchema = z.object({
sub: ID,
org: ID.optional(),
purpose: z.literal('sandbox')
})
export type SandboxCaller = { userId: string; accountId: string }
const keySets = new Map<string, JWTVerifyGetKey>()
function keysFor(env: Env): JWTVerifyGetKey {
const source = env.SANDBOX_JWKS_URL ?? `inline:${env.SANDBOX_JWKS ?? ''}`
let keys = keySets.get(source)
if (!keys) {
keys = env.SANDBOX_JWKS_URL
? createRemoteJWKSet(new URL(env.SANDBOX_JWKS_URL))
: createLocalJWKSet(JSON.parse(env.SANDBOX_JWKS ?? '{"keys":[]}') as JSONWebKeySet)
keySets.set(source, keys)
}
return keys
}
export function readBearer(value: string | null): string | null {
return /^Bearer ([^\s]+)$/.exec(value ?? '')?.[1] ?? null
}
/** Same account rule as billing: an org pays for its members, otherwise the user is the account. */
export function accountIdFor(claims: { sub: string; org?: string }): string {
return claims.org ? `org:${claims.org}` : `user:${claims.sub}`
}
export async function authenticateDesktop(
env: Env,
request: Request,
now: number = Date.now()
): Promise<SandboxCaller | null> {
const token = readBearer(request.headers.get('authorization'))
if (!token) {
return null
}
try {
const { payload } = await jwtVerify(token, keysFor(env), {
audience: env.SANDBOX_JWT_AUDIENCE,
algorithms: ['ES256'],
maxTokenAge: '15m',
requiredClaims: ['exp', 'iat'],
currentDate: new Date(now),
...(env.SANDBOX_AUTH_ISSUER ? { issuer: env.SANDBOX_AUTH_ISSUER } : {})
})
const claims = ClaimsSchema.parse(payload)
return { userId: claims.sub, accountId: accountIdFor(claims) }
} catch {
return null
}
}
@@ -0,0 +1,127 @@
// Target of Vercel network-policy `forwardURL` rules. Vercel proves which sandbox sent the request
// with an OIDC token; we swap the VM's placeholder for the real credential and return a normally
// framed response (works around undici hanging on transform-rule responses). Real tokens never
// enter the VM or Vercel's policy store.
import { createRemoteJWKSet, decodeJwt, jwtVerify, type JWTVerifyGetKey } from 'jose'
import type { SandboxRow } from './store'
const VERCEL_OIDC_HOSTNAME = 'oidc.vercel.com'
/** Hosts whose clients are Node (undici); git/curl hosts use transform rules instead. */
export const PROXIED_HOSTS: ReadonlySet<string> = new Set(['api.github.com'])
const DROP_HEADERS = [
'vercel-forwarded-host',
'vercel-forwarded-scheme',
'vercel-forwarded-port',
'vercel-forwarded-path',
'vercel-sandbox-oidc-token',
'host',
'connection',
'keep-alive',
'proxy-authorization',
'transfer-encoding',
'upgrade'
]
export type CredentialBroker = {
/** Authorization header value for `host` on behalf of this sandbox's workspace, or null. */
authorizationFor(row: SandboxRow, host: string): Promise<string | null>
}
/** Until the GitHub App installation-token minter exists, proxied hosts are refused. */
export const NO_CREDENTIALS: CredentialBroker = { authorizationFor: async () => null }
export type EgressProxyDeps = {
teamId: string
projectId: string
findSandbox: (providerName: string) => Promise<SandboxRow | null>
placeholderFor: (row: SandboxRow) => string
credentials: CredentialBroker
fetch?: typeof fetch
/** Tests inject a local key set; production fetches `<issuer>/.well-known/jwks`. */
keysForIssuer?: (issuer: string) => JWTVerifyGetKey
}
const remoteKeys = new Map<string, JWTVerifyGetKey>()
function defaultKeys(issuer: string): JWTVerifyGetKey {
let keys = remoteKeys.get(issuer)
if (!keys) {
keys = createRemoteJWKSet(new URL(`${issuer.replace(/\/$/, '')}/.well-known/jwks`))
remoteKeys.set(issuer, keys)
}
return keys
}
function deny(reason: string): Response {
return new Response(reason, { status: 403, headers: { 'content-type': 'text/plain' } })
}
/** The audience Vercel signs: the forwardURL, i.e. this request's URL minus the forwarded path. */
function forwardAudience(requestUrl: URL, forwardedPath: string): string {
const strip = (path: string) => path.replace(/\/+$/, '') || '/'
const suffix = strip(new URL(forwardedPath, requestUrl.origin).pathname)
let pathname = strip(requestUrl.pathname)
if (suffix !== '/' && pathname.endsWith(suffix)) {
pathname = strip(pathname.slice(0, -suffix.length))
}
return pathname === '/' ? requestUrl.origin : requestUrl.origin + pathname
}
export async function handleEgressProxy(request: Request, deps: EgressProxyDeps): Promise<Response> {
const host = request.headers.get('vercel-forwarded-host')?.toLowerCase()
const scheme = request.headers.get('vercel-forwarded-scheme')
const port = request.headers.get('vercel-forwarded-port')
const path = request.headers.get('vercel-forwarded-path')
const token = request.headers.get('vercel-sandbox-oidc-token')
if (!host || !scheme || !port || !path || !token || !path.startsWith('/')) {
return deny('missing_proxy_headers')
}
if (!PROXIED_HOSTS.has(host) || scheme !== 'https' || port !== '443') {
return deny('host_not_proxied')
}
let claims: Record<string, unknown>
try {
const issuer = String(decodeJwt(token).iss ?? '')
const issuerUrl = new URL(issuer)
if (issuerUrl.protocol !== 'https:' || issuerUrl.hostname !== VERCEL_OIDC_HOSTNAME) {
return deny('bad_issuer')
}
const verified = await jwtVerify(token, (deps.keysForIssuer ?? defaultKeys)(issuer), {
issuer,
audience: forwardAudience(new URL(request.url), path),
algorithms: ['RS256'],
clockTolerance: 60
})
claims = verified.payload
} catch {
return deny('bad_oidc_token')
}
if (claims.team_id !== deps.teamId || claims.project_id !== deps.projectId) {
return deny('foreign_sandbox')
}
const name = typeof claims.sandbox_name === 'string' ? claims.sandbox_name : null
const row = name ? await deps.findSandbox(name) : null
if (!row || row.status !== 'running') {
return deny('sandbox_not_running')
}
if (request.headers.get('authorization') !== deps.placeholderFor(row)) {
return deny('placeholder_missing')
}
const authorization = await deps.credentials.authorizationFor(row, host)
if (!authorization) {
return deny('credential_unavailable')
}
const headers = new Headers(request.headers)
for (const name of DROP_HEADERS) {
headers.delete(name)
}
headers.set('authorization', authorization)
const hasBody = request.method !== 'GET' && request.method !== 'HEAD'
const upstream = await (deps.fetch ?? fetch)(`https://${host}${path}`, {
method: request.method,
headers,
body: hasBody ? await request.arrayBuffer() : undefined,
redirect: 'manual'
})
// Re-wrapping lets the runtime frame the body (content-length or chunked) for the VM's client.
return new Response(upstream.body, { status: upstream.status, headers: upstream.headers })
}
@@ -0,0 +1,89 @@
// Billing Worker client (cloud/apps/billing on nwparker/billing-worker): start checks and usage.
// Fails closed: without a configured billing URL no sandbox starts, except the local-dev stub.
import { z } from 'zod'
import type { Env } from './env'
export type MachineSize = 'standard' | 'large' | 'xlarge'
export type StartDecision = { allow: boolean; reason: string | null }
export type UsageSegment = {
usageId: string
accountId: string
sandboxId: string
machineSize: MachineSize
startedAt: number
endedAt: number
}
export type UsageAnswer = { canStartSandbox: boolean; reason: string | null }
export type EntitlementClient = {
checkStart(request: {
accountId: string
machineSize: MachineSize
runningSandboxes: number
}): Promise<StartDecision>
reportUsage(segment: UsageSegment): Promise<UsageAnswer>
}
export class BillingUnavailableError extends Error {
constructor(message: string) {
super(message)
this.name = 'BillingUnavailableError'
}
}
const StartAnswer = z.looseObject({ allow: z.boolean(), reason: z.string().nullable() })
const UsageResponse = z.looseObject({ canStartSandbox: z.boolean(), reason: z.string().nullable() })
/** Reasons that mean running sandboxes must stop, not just that new starts wait. */
export const STOP_RUNNING_REASONS: ReadonlySet<string> = new Set([
'included_hours_exhausted',
'overage_cap_reached',
'subscription_inactive',
'no_subscription'
])
export function createEntitlementClient(env: Env, doFetch: typeof fetch = fetch): EntitlementClient {
if (env.ENTITLEMENTS_MODE === 'stub') {
if (env.SANDBOX_ENV === 'prod') {
throw new BillingUnavailableError('stub entitlements refused in prod')
}
return {
checkStart: async () => ({ allow: true, reason: null }),
reportUsage: async () => ({ canStartSandbox: true, reason: null })
}
}
const base = env.BILLING_URL
const token = env.CONTROL_PLANE_TOKEN
async function post<T>(path: string, body: unknown, schema: z.ZodType<T>): Promise<T> {
if (!base || !token) {
throw new BillingUnavailableError('billing not configured')
}
let response: Response
try {
response = await doFetch(new URL(path, base), {
method: 'POST',
headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
body: JSON.stringify(body),
redirect: 'manual',
signal: AbortSignal.timeout(10_000)
})
} catch {
throw new BillingUnavailableError('billing unreachable')
}
if (!response.ok) {
throw new BillingUnavailableError(`billing answered ${response.status}`)
}
const parsed = schema.safeParse(await response.json().catch(() => null))
if (!parsed.success) {
throw new BillingUnavailableError('billing answered an unexpected body')
}
return parsed.data
}
return {
checkStart: (request) => post('/v1/internal/entitlements/check', request, StartAnswer),
reportUsage: (segment) => post('/v1/internal/usage', segment, UsageResponse)
}
}
+33
View File
@@ -0,0 +1,33 @@
// Sandbox control-plane bindings. Secrets are set with `wrangler secret put` (stdin), never in
// wrangler.jsonc or git. Deployed environments live in the private deploy config.
export type Env = {
SANDBOX_DB: D1Database
/** `prod` | `dev` | `local`; tagged on every provider resource so environments never mix. */
SANDBOX_ENV: string
// Vercel: Orca's own Pro team. A team-scoped token limited to Sandbox; users never see it.
VERCEL_TOKEN?: string
VERCEL_TEAM_ID?: string
VERCEL_PROJECT_ID?: string
VERCEL_REGION?: string
/** Overrides the Vercel API origin (tests only). */
VERCEL_API_BASE?: string
/** Golden image (orcad + agent CLIs) every new sandbox boots from; see image/. */
GOLDEN_SNAPSHOT_ID?: string
// Desktop tokens: short-lived ES256 JWTs from Orca Cloud auth, aud=SANDBOX_JWT_AUDIENCE.
// SANDBOX_JWKS (inline set) is for local dev and tests only.
SANDBOX_JWKS_URL?: string
SANDBOX_JWKS?: string
SANDBOX_AUTH_ISSUER?: string
SANDBOX_JWT_AUDIENCE: string
// Billing Worker (entitlement checks + usage). CONTROL_PLANE_TOKEN is shared with it.
BILLING_URL?: string
CONTROL_PLANE_TOKEN?: string
/** `stub` allows every start and records nothing: local dev only, refused when SANDBOX_ENV=prod. */
ENTITLEMENTS_MODE?: string
/** Public URL of this Worker's /v1/egress/proxy; Vercel forwardURL rules point here. */
EGRESS_PROXY_URL?: string
}
+194
View File
@@ -0,0 +1,194 @@
// Orca sandbox control plane: owns Orca's Vercel credentials and runs one persistent sandbox per
// Orca Cloud workspace for the signed-in account. Desktop routes (Orca JWT, aud=orca-sandbox):
// POST /v1/sandboxes {workspaceId, machineSize, region?, repo?} -> {sandbox, connection}
// GET /v1/sandboxes -> {sandboxes}
// GET /v1/sandboxes/:id -> {sandbox}
// POST /v1/sandboxes/:id/suspend | /resume | /rollover -> {sandbox[, connection]}
// POST /v1/sandboxes/:id/lease {extendMs} -> {sandbox}
// DELETE /v1/sandboxes/:id -> {sandbox}
// Vercel network-policy forwardURL target (Vercel sandbox OIDC): /v1/egress/proxy
import { authenticateDesktop } from './auth'
import { NO_CREDENTIALS, handleEgressProxy, type CredentialBroker } from './egress-proxy'
import { BillingUnavailableError, createEntitlementClient, type EntitlementClient } from './entitlements'
import type { Env } from './env'
import {
LifecycleError,
createSandbox,
destroySandbox,
githubPlaceholderHeader,
renewLease,
resumeSandbox,
rolloverSandbox,
suspendSandbox,
type LifecycleDeps
} from './lifecycle'
import { reconcile } from './reconcile'
import { ProviderError, type SandboxProviderApi } from './sandbox-provider'
import { VmCommandError } from './sandbox-vm-commands'
import { getSandbox, getSandboxByProviderName, listAccountSandboxes } from './store'
import { createVercelSandboxApi } from './vercel-sandbox-api'
import { CreateRequestSchema, LeaseRequestSchema, readyView, sandboxView } from './wire'
const MAX_JSON_BYTES = 16 * 1024
export type Deps = {
provider?: SandboxProviderApi
entitlements?: EntitlementClient
credentials?: CredentialBroker
now?: () => number
egressFetch?: typeof fetch
egressKeysForIssuer?: Parameters<typeof handleEgressProxy>[1]['keysForIssuer']
}
function json(body: unknown, status = 200): Response {
return Response.json(body, { status, headers: { 'cache-control': 'no-store' } })
}
function fail(error: string, status: number, reason?: string): Response {
return json(reason ? { error, reason } : { error }, status)
}
async function readJson(request: Request): Promise<unknown> {
const text = await request.text()
if (text.length > MAX_JSON_BYTES) {
throw new SyntaxError('too_large')
}
return text ? JSON.parse(text) : {}
}
/** Lifecycle routes refuse to run half-configured rather than guess at Orca's provider account. */
function lifecycleDeps(env: Env, deps: Deps): LifecycleDeps | null {
if (!env.GOLDEN_SNAPSHOT_ID || (!deps.provider && (!env.VERCEL_TOKEN || !env.VERCEL_TEAM_ID || !env.VERCEL_PROJECT_ID))) {
return null
}
return {
db: env.SANDBOX_DB,
provider:
deps.provider ??
createVercelSandboxApi({
token: env.VERCEL_TOKEN!,
teamId: env.VERCEL_TEAM_ID!,
projectId: env.VERCEL_PROJECT_ID!,
baseUrl: env.VERCEL_API_BASE
}),
entitlements: deps.entitlements ?? createEntitlementClient(env),
now: deps.now ?? Date.now,
config: {
environment: env.SANDBOX_ENV,
goldenSnapshotId: env.GOLDEN_SNAPSHOT_ID,
defaultRegion: env.VERCEL_REGION ?? 'iad1',
egressProxyUrl: env.EGRESS_PROXY_URL
}
}
}
export async function handleRequest(request: Request, env: Env, deps: Deps = {}): Promise<Response> {
const url = new URL(request.url)
try {
if (url.pathname === '/v1/egress/proxy' || url.pathname.startsWith('/v1/egress/proxy/')) {
if (!env.VERCEL_TEAM_ID || !env.VERCEL_PROJECT_ID) {
return fail('not_configured', 503)
}
return await handleEgressProxy(request, {
teamId: env.VERCEL_TEAM_ID,
projectId: env.VERCEL_PROJECT_ID,
findSandbox: (name) => getSandboxByProviderName(env.SANDBOX_DB, name),
placeholderFor: githubPlaceholderHeader,
credentials: deps.credentials ?? NO_CREDENTIALS,
fetch: deps.egressFetch,
keysForIssuer: deps.egressKeysForIssuer
})
}
const match = /^\/v1\/sandboxes(?:\/([A-Za-z0-9_]+)(?:\/(suspend|resume|lease|rollover))?)?$/.exec(url.pathname)
if (!match) {
return fail('not_found', 404)
}
const caller = await authenticateDesktop(env, request, (deps.now ?? Date.now)())
if (!caller) {
return fail('unauthorized', 401)
}
const life = lifecycleDeps(env, deps)
if (!life) {
return fail('not_configured', 503)
}
const [, id, action] = match
if (!id) {
if (request.method === 'GET') {
const rows = await listAccountSandboxes(env.SANDBOX_DB, caller.accountId)
return json({ sandboxes: rows.map(sandboxView) })
}
if (request.method !== 'POST') {
return fail('not_found', 404)
}
const body = CreateRequestSchema.safeParse(await readJson(request))
if (!body.success) {
return fail('invalid_request', 400)
}
const ready = await createSandbox(life, caller, body.data)
return json(readyView(ready.row, ready.connection))
}
const row = await getSandbox(env.SANDBOX_DB, id)
// Another account's sandbox is indistinguishable from a missing one.
if (!row || row.account_id !== caller.accountId) {
return fail('not_found', 404)
}
const route = `${request.method} ${action ?? ''}`
switch (route) {
case 'GET ':
return json({ sandbox: sandboxView(row) })
case 'DELETE ':
return json({ sandbox: sandboxView(await destroySandbox(life, row)) })
case 'POST suspend':
return json({ sandbox: sandboxView(await suspendSandbox(life, row)) })
case 'POST resume': {
const ready = await resumeSandbox(life, row)
return json(readyView(ready.row, ready.connection))
}
case 'POST rollover': {
const ready = await rolloverSandbox(life, row)
return json(readyView(ready.row, ready.connection))
}
case 'POST lease': {
const body = LeaseRequestSchema.safeParse(await readJson(request))
if (!body.success) {
return fail('invalid_request', 400)
}
return json({ sandbox: sandboxView(await renewLease(life, row, body.data.extendMs)) })
}
default:
return fail('not_found', 404)
}
} catch (error) {
return errorResponse(error)
}
}
function errorResponse(error: unknown): Response {
if (error instanceof SyntaxError) {
return fail('invalid_json', 400)
}
if (error instanceof LifecycleError) {
const status = { not_found: 404, conflict: 409, entitlement_denied: 403, provider_unavailable: 502 }[error.code]
return fail(error.code, status, error.reason)
}
if (error instanceof BillingUnavailableError) {
return fail('billing_unavailable', 503)
}
// Provider and VM failures are summarized; their bodies never reach the desktop.
if (error instanceof ProviderError || error instanceof VmCommandError) {
return fail('provider_unavailable', 502, error instanceof VmCommandError ? error.code : undefined)
}
return fail('internal_error', 500)
}
export default {
fetch(request: Request, env: Env): Promise<Response> {
return handleRequest(request, env)
},
async scheduled(_controller: ScheduledController, env: Env, ctx: ExecutionContext): Promise<void> {
const life = lifecycleDeps(env, {})
if (life) {
ctx.waitUntil(reconcile(life).then(() => undefined))
}
}
} satisfies ExportedHandler<Env>
+301
View File
@@ -0,0 +1,301 @@
// Sandbox lifecycle: one persistent provider sandbox per Orca workspace, with an Orca-owned lease,
// a forced stop before the provider's 24h cap, usage metering and write-ahead rows.
import type { EntitlementClient, MachineSize } from './entitlements'
import { compileNetworkPolicy, type CredentialRoute } from './network-policy'
import type { ProviderSandboxState, SandboxProviderApi } from './sandbox-provider'
import { ORCAD_PORT, drainOrcad, startOrcad, type OrcadReady } from './sandbox-vm-commands'
import {
NO_SESSION,
countActiveSandboxes,
getSandbox,
insertSandbox,
liveSandboxForWorkspace,
markSnapshotsDeleted,
recordSnapshots,
recordedSnapshotIds,
transition,
type SandboxRow,
type SessionFields
} from './store'
import { flushUsage, queueUsage } from './usage'
export const LEASE_MS = 30 * 60_000
const PROVIDER_SESSION_CAP_MS = 24 * 60 * 60_000
/** Orca stops 10 min early so it, not the provider, ends the session (drain first). */
export const SESSION_DEADLINE_MS = PROVIDER_SESSION_CAP_MS - 10 * 60_000
const VCPUS: Record<MachineSize, number> = { standard: 4, large: 8, xlarge: 16 }
export type LifecycleDeps = {
db: D1Database
provider: SandboxProviderApi
entitlements: EntitlementClient
now: () => number
config: {
environment: string
goldenSnapshotId: string
defaultRegion: string
egressProxyUrl?: string
}
}
export class LifecycleError extends Error {
constructor(
readonly code: 'not_found' | 'conflict' | 'entitlement_denied' | 'provider_unavailable',
readonly reason?: string
) {
super(reason ? `${code}:${reason}` : code)
this.name = 'LifecycleError'
}
}
export type Ready = { row: SandboxRow; connection: OrcadReady }
function randomId(): string {
const bytes = crypto.getRandomValues(new Uint8Array(12))
return `sbx_${Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('')}`
}
/** What the VM sends as its GitHub API credential; the egress proxy swaps it for the real token. */
export function githubPlaceholderHeader(row: SandboxRow): string {
return `Bearer orca-ph-${row.id}`
}
function egressPolicy(deps: LifecycleDeps, row: SandboxRow) {
const credentials: CredentialRoute[] = deps.config.egressProxyUrl
? [
{
host: 'api.github.com',
header: 'authorization',
placeholder: githubPlaceholderHeader(row),
via: 'forward',
forwardURL: deps.config.egressProxyUrl
}
]
: []
return compileNetworkPolicy({ mode: 'allowlist', credentials })
}
export function sessionFields(state: ProviderSandboxState, now: number): SessionFields {
const startedAt = state.session.startedAt ?? now
return {
session_id: state.session.id,
session_started_at: startedAt,
session_deadline_at: startedAt + SESSION_DEADLINE_MS,
lease_expires_at: startedAt + state.session.timeoutMs,
usage_settled_at: startedAt
}
}
async function claim(deps: LifecycleDeps, row: SandboxRow, patch: Partial<SandboxRow>): Promise<SandboxRow> {
const next = await transition(deps.db, row, patch, deps.now())
if (!next) {
throw new LifecycleError('conflict', 'concurrent_transition')
}
return next
}
async function requireStart(deps: LifecycleDeps, accountId: string, machineSize: MachineSize) {
const runningSandboxes = await countActiveSandboxes(deps.db, accountId)
const decision = await deps.entitlements.checkStart({ accountId, machineSize, runningSandboxes })
if (!decision.allow) {
throw new LifecycleError('entitlement_denied', decision.reason ?? 'denied')
}
}
async function boot(deps: LifecycleDeps, row: SandboxRow, state: ProviderSandboxState, repo?: { url: string; ref?: string }): Promise<Ready> {
if (state.sandbox.tags.orcaSandbox !== row.id) {
throw new LifecycleError('conflict', 'ownership_mismatch')
}
const connection = await startOrcad(deps.provider, state, {
sandboxId: row.id,
repo,
githubPlaceholder: deps.config.egressProxyUrl ? githubPlaceholderHeader(row) : undefined
})
const running = await claim(deps, row, { status: 'running', last_error: null, ...sessionFields(state, deps.now()) })
return { row: running, connection }
}
export async function createSandbox(
deps: LifecycleDeps,
caller: { accountId: string; userId: string },
request: { workspaceId: string; machineSize: MachineSize; region?: string; repo?: { url: string; ref?: string } }
): Promise<Ready> {
const existing = await liveSandboxForWorkspace(deps.db, caller.accountId, request.workspaceId)
if (existing) {
if (existing.status !== 'running' || !existing.session_id) {
throw new LifecycleError('conflict', `sandbox_${existing.status}`)
}
// Retried create after a lost response: hand back the same environment.
const state = await deps.provider.getSandbox(existing.provider_name, { resume: false })
if (!state || state.session.status !== 'running') {
throw new LifecycleError('conflict', 'sandbox_not_running')
}
return { row: existing, connection: await startOrcad(deps.provider, state, { sandboxId: existing.id }) }
}
await requireStart(deps, caller.accountId, request.machineSize)
const id = randomId()
const row = await insertSandbox(
deps.db,
{
id,
account_id: caller.accountId,
user_id: caller.userId,
workspace_id: request.workspaceId,
provider: 'vercel',
provider_name: `orca-${deps.config.environment}-${id}`,
machine_size: request.machineSize,
region: request.region ?? deps.config.defaultRegion
},
deps.now()
)
let state: ProviderSandboxState
try {
state = await deps.provider.createSandbox({
name: row.provider_name,
snapshotId: deps.config.goldenSnapshotId,
vcpus: VCPUS[row.machine_size],
region: row.region,
timeoutMs: LEASE_MS,
ports: [ORCAD_PORT],
networkPolicy: egressPolicy(deps, row),
tags: { orcaSandbox: row.id, orcaEnv: deps.config.environment }
})
} catch (error) {
await failAndRelease(deps, row)
throw error
}
try {
return await boot(deps, row, state, request.repo)
} catch (error) {
await failAndRelease(deps, (await getSandbox(deps.db, row.id)) ?? row)
throw error
}
}
/** A create that never became usable is destroyed now; the cron retries if this also fails. */
export async function failAndRelease(deps: LifecycleDeps, row: SandboxRow): Promise<void> {
const failed = await transition(deps.db, row, { status: 'failed', last_error: 'create_failed' }, deps.now())
if (failed) {
await destroySandbox(deps, failed).catch(() => undefined)
}
}
async function stopForSuspend(deps: LifecycleDeps, row: SandboxRow): Promise<SandboxRow> {
if (row.session_id) {
await drainOrcad(deps.provider, row.session_id)
const stopped = await deps.provider.stopSession(row.session_id)
if (stopped.snapshotId) {
await recordSnapshots(deps.db, row.id, [stopped.snapshotId], deps.now())
}
}
await queueUsage(deps.db, row, deps.now())
return claim(deps, row, { status: 'suspended', ...NO_SESSION })
}
/** The provider ended the session on its own (lease lapsed): bill up to when it stopped. */
export async function markProviderStopped(deps: LifecycleDeps, row: SandboxRow): Promise<SandboxRow> {
await queueUsage(deps.db, row, Math.min(deps.now(), row.lease_expires_at ?? deps.now()))
return claim(deps, row, { status: 'suspended', ...NO_SESSION })
}
export async function suspendSandbox(deps: LifecycleDeps, row: SandboxRow): Promise<SandboxRow> {
if (row.status === 'suspended') {
return row
}
if (row.status !== 'running') {
throw new LifecycleError('conflict', `sandbox_${row.status}`)
}
const suspending = await claim(deps, row, { status: 'suspending' })
const suspended = await stopForSuspend(deps, suspending)
await flushUsage(deps.db, deps.entitlements, deps.now(), row.id)
return suspended
}
export async function resumeSandbox(deps: LifecycleDeps, row: SandboxRow): Promise<Ready> {
if (row.status === 'running') {
const state = await deps.provider.getSandbox(row.provider_name, { resume: false })
if (state?.session.status === 'running') {
return { row, connection: await startOrcad(deps.provider, state, { sandboxId: row.id }) }
}
// The provider ended the session (lease lapsed): fall through to a real resume.
row = await markProviderStopped(deps, row)
}
if (row.status !== 'suspended') {
throw new LifecycleError('conflict', `sandbox_${row.status}`)
}
await requireStart(deps, row.account_id, row.machine_size)
const resuming = await claim(deps, row, { status: 'resuming' })
return wake(deps, resuming)
}
async function wake(deps: LifecycleDeps, row: SandboxRow): Promise<Ready> {
const state = await deps.provider.getSandbox(row.provider_name, { resume: true })
if (!state) {
await claim(deps, row, { status: 'failed', last_error: 'provider_sandbox_missing' })
throw new LifecycleError('conflict', 'environment_missing')
}
try {
return await boot(deps, row, state)
} catch (error) {
// Leave it resumable: the disk is intact, only this boot failed.
const current = await getSandbox(deps.db, row.id)
if (current?.status === 'resuming') {
await transition(deps.db, current, { status: 'suspended', ...NO_SESSION, last_error: 'resume_failed' }, deps.now())
}
throw error
}
}
/** 24h cap or opportunistic refresh: drain, stop (snapshot), boot the same disk again. */
export async function rolloverSandbox(deps: LifecycleDeps, row: SandboxRow): Promise<Ready> {
if (row.status !== 'running') {
throw new LifecycleError('conflict', `sandbox_${row.status}`)
}
const suspending = await claim(deps, row, { status: 'suspending' })
const stopped = await stopForSuspend(deps, suspending)
await flushUsage(deps.db, deps.entitlements, deps.now(), row.id)
return wake(deps, await claim(deps, stopped, { status: 'resuming' }))
}
export async function renewLease(deps: LifecycleDeps, row: SandboxRow, extendMs: number): Promise<SandboxRow> {
if (row.status !== 'running' || !row.session_id || row.lease_expires_at === null || row.session_deadline_at === null) {
throw new LifecycleError('conflict', `sandbox_${row.status}`)
}
const target = Math.min(row.session_deadline_at, deps.now() + extendMs)
if (target <= row.lease_expires_at) {
return row
}
const session = await deps.provider.extendTimeout(row.session_id, target - row.lease_expires_at)
const startedAt = row.session_started_at ?? deps.now()
return claim(deps, row, { lease_expires_at: startedAt + session.timeoutMs })
}
export async function destroySandbox(deps: LifecycleDeps, row: SandboxRow): Promise<SandboxRow> {
if (row.status === 'destroyed') {
return row
}
let destroying = row.status === 'destroying' ? row : await claim(deps, row, { status: 'destroying' })
const settled = await queueUsage(deps.db, destroying, deps.now())
if (settled !== destroying.usage_settled_at) {
destroying = await claim(deps, destroying, { usage_settled_at: settled })
}
await deps.provider.deleteSandbox(destroying.provider_name)
// Persistent sandboxes leave automatic snapshots behind delete; prove none survive.
const listed = await deps.provider.listSnapshots(destroying.provider_name)
await recordSnapshots(deps.db, row.id, listed.map((s) => s.id), deps.now())
for (const id of await recordedSnapshotIds(deps.db, row.id)) {
if (id !== deps.config.goldenSnapshotId) {
await deps.provider.deleteSnapshot(id)
}
}
const remaining = (await deps.provider.listSnapshots(destroying.provider_name)).filter(
(s) => s.status !== 'deleted' && s.id !== deps.config.goldenSnapshotId
)
if (remaining.length > 0) {
throw new LifecycleError('provider_unavailable', 'snapshot_cleanup_incomplete')
}
await markSnapshotsDeleted(deps.db, row.id, deps.now())
const destroyed = await claim(deps, destroying, { status: 'destroyed', ...NO_SESSION, destroyed_at: deps.now() })
await flushUsage(deps.db, deps.entitlements, deps.now(), row.id)
return destroyed
}
@@ -0,0 +1,110 @@
// Fail-closed egress for Orca Cloud sandboxes (Vercel v2 record-form network policy).
// - No "*" entry unless a workspace opts into open egress: unlisted domains do not resolve.
// - Credential hosts end with a 403 rule, so only requests carrying our placeholder get through.
// - Hosts Node clients use are forwarded to our egress proxy instead of transformed: with a
// transform rule Vercel's proxy answers HTTP/1.1 without content-length or transfer-encoding and
// undici fetch hangs on the body (bake-off). curl/git and Go clients are unaffected.
type Matcher = { exact?: string; startsWith?: string }
export type NetworkPolicyRule =
| {
match?: { headers?: { key: Matcher; value: Matcher }[] }
transform: { headers: Record<string, string> }[]
}
| { match?: { headers?: { key: Matcher; value: Matcher }[] }; forwardURL: string }
| { response: { statusCode: number; body: string; contentType: string } }
export type NetworkPolicy = { allow: Record<string, NetworkPolicyRule[]> }
export const EGRESS_DENIED_BODY = 'orca-egress-denied'
/** Registries, git hosting and agent model/sign-in hosts a coding VM needs. */
export const DEFAULT_EGRESS_ALLOWLIST: readonly string[] = [
'registry.npmjs.org',
'registry.yarnpkg.com',
'pypi.org',
'files.pythonhosted.org',
'crates.io',
'index.crates.io',
'static.crates.io',
'proxy.golang.org',
'sum.golang.org',
'rubygems.org',
'nodejs.org',
'archive.ubuntu.com',
'security.ubuntu.com',
'codeload.github.com',
'objects.githubusercontent.com',
'raw.githubusercontent.com',
'api.anthropic.com',
'claude.ai',
'console.anthropic.com',
'api.openai.com',
'auth.openai.com',
'chatgpt.com',
'generativelanguage.googleapis.com',
'oauth2.googleapis.com',
'accounts.google.com',
'*.onorca.dev'
]
export type CredentialRoute = {
host: string
/** Header the VM sends with the placeholder value, matched exactly. */
header: string
placeholder: string
} & (
| { via: 'transform'; value: string }
| { via: 'forward'; forwardURL: string }
)
export type EgressPolicyInput = {
mode: 'allowlist' | 'open'
allowlist?: readonly string[]
credentials?: readonly CredentialRoute[]
}
const HOST = /^(\*\.)?([a-z0-9-]+\.)+[a-z]{2,63}$/
export function compileNetworkPolicy(input: EgressPolicyInput): NetworkPolicy {
const allow: Record<string, NetworkPolicyRule[]> = {}
for (const host of input.allowlist ?? DEFAULT_EGRESS_ALLOWLIST) {
allow[checkedHost(host)] = []
}
for (const route of input.credentials ?? []) {
const host = checkedHost(route.host)
if (host.startsWith('*.')) {
throw new Error('credential routes need an exact host')
}
const match = {
headers: [{ key: { exact: route.header.toLowerCase() }, value: { exact: route.placeholder } }]
}
const claim: NetworkPolicyRule =
route.via === 'transform'
? { match, transform: [{ headers: { [route.header.toLowerCase()]: route.value } }] }
: { match, forwardURL: route.forwardURL }
const rules = allow[host]?.filter((rule) => !('response' in rule)) ?? []
allow[host] = [...rules, claim, denyRule()]
}
if (input.mode === 'open') {
allow['*'] = []
}
return { allow }
}
function denyRule(): NetworkPolicyRule {
return { response: { statusCode: 403, body: EGRESS_DENIED_BODY, contentType: 'text/plain' } }
}
function checkedHost(host: string): string {
const normalized = host.trim().toLowerCase()
if (!HOST.test(normalized)) {
throw new Error(`invalid egress host: ${host}`)
}
return normalized
}
/** Basic credential git sends when `http.<url>.extraheader` carries the placeholder token. */
export function gitBasicAuthorization(token: string): string {
return `Basic ${btoa(`x-access-token:${token}`)}`
}
@@ -0,0 +1,98 @@
// Cron pass: the control plane, not the desktop, guarantees metering, the session deadline and cleanup,
// so a closed laptop or a crash mid-call can never leave a sandbox billed-but-untracked.
import { STOP_RUNNING_REASONS } from './entitlements'
import {
destroySandbox,
failAndRelease,
markProviderStopped,
sessionFields,
suspendSandbox,
type LifecycleDeps
} from './lifecycle'
import { listByStatus, transition, type SandboxRow } from './store'
import { flushUsage, queueUsage } from './usage'
const STUCK_MS = 15 * 60_000
export type ReconcileSummary = Record<
'settled' | 'suspendedForDeadline' | 'suspendedForBilling' | 'leaseLapsed' | 'released' | 'repaired',
number
>
export async function reconcile(deps: LifecycleDeps): Promise<ReconcileSummary> {
const summary: ReconcileSummary = {
settled: 0,
suspendedForDeadline: 0,
suspendedForBilling: 0,
leaseLapsed: 0,
released: 0,
repaired: 0
}
for (const row of await listByStatus(deps.db, ['running'])) {
await guard(() => reconcileRunning(deps, row, summary))
}
const now = deps.now()
for (const row of await listByStatus(deps.db, ['creating', 'failed', 'destroying', 'suspending', 'resuming'])) {
if (now - row.updated_at < STUCK_MS) {
continue
}
await guard(async () => {
if (row.status === 'creating') {
await failAndRelease(deps, row)
summary.released++
} else if (row.status === 'failed' || row.status === 'destroying') {
await destroySandbox(deps, row)
summary.released++
} else {
await repairTransition(deps, row)
summary.repaired++
}
})
}
await flushUsage(deps.db, deps.entitlements, deps.now())
return summary
}
async function reconcileRunning(deps: LifecycleDeps, row: SandboxRow, summary: ReconcileSummary): Promise<void> {
const now = deps.now()
if (row.session_deadline_at !== null && now >= row.session_deadline_at) {
// The desktop should have rolled over; stop before the provider's hard cap kills it undrained.
await suspendSandbox(deps, row)
summary.suspendedForDeadline++
return
}
if (row.lease_expires_at !== null && now >= row.lease_expires_at) {
const state = await deps.provider.getSandbox(row.provider_name, { resume: false })
if (!state || state.session.status !== 'running') {
await markProviderStopped(deps, row)
summary.leaseLapsed++
return
}
}
const settled = await queueUsage(deps.db, row, now)
const current = settled === row.usage_settled_at ? row : await transition(deps.db, row, { usage_settled_at: settled }, now)
summary.settled++
const answer = await flushUsage(deps.db, deps.entitlements, now, row.id)
if (current && answer && !answer.canStartSandbox && STOP_RUNNING_REASONS.has(answer.reason ?? '')) {
await suspendSandbox(deps, current)
summary.suspendedForBilling++
}
}
/** A suspend/resume call died mid-way: adopt whatever the provider says is true. */
async function repairTransition(deps: LifecycleDeps, row: SandboxRow): Promise<void> {
const state = await deps.provider.getSandbox(row.provider_name, { resume: false })
if (state?.session.status === 'running') {
await transition(deps.db, row, { status: 'running', ...sessionFields(state, deps.now()) }, deps.now())
return
}
await markProviderStopped(deps, row)
}
async function guard(step: () => Promise<void>): Promise<void> {
try {
await step()
} catch {
// One bad row must not stall the pass; it is retried next tick.
}
}
@@ -0,0 +1,90 @@
// The provider calls the control plane needs, shaped after Vercel Sandbox's persistent-sandbox API.
// Lifecycle code depends only on this, so a second provider (Freestyle, E2B) is one more adapter.
import type { NetworkPolicy } from './network-policy'
export type ProviderSessionStatus =
| 'pending'
| 'running'
| 'stopping'
| 'stopped'
| 'failed'
| 'aborted'
| 'snapshotting'
export type ProviderSandbox = {
name: string
status: ProviderSessionStatus
currentSessionId: string
currentSnapshotId: string | null
tags: Record<string, string>
}
export type ProviderSession = {
id: string
status: ProviderSessionStatus
startedAt: number | null
/** Session timeout in ms, counted from start. */
timeoutMs: number
}
export type ProviderRoute = { port: number; url: string }
export type ProviderSandboxState = {
sandbox: ProviderSandbox
session: ProviderSession
routes: ProviderRoute[]
}
export type ProviderSnapshot = {
id: string
status: 'created' | 'deleted' | 'failed'
sourceSessionId: string
}
export type CreateSandboxParams = {
name: string
snapshotId: string
vcpus: number
region: string
timeoutMs: number
ports: number[]
networkPolicy: NetworkPolicy
tags: Record<string, string>
}
export type RunCommandParams = {
command: string
args?: string[]
cwd?: string
env?: Record<string, string>
sudo?: boolean
timeoutMs: number
}
export type CommandResult = { exitCode: number; stdout: string; stderr: string }
export type SandboxProviderApi = {
createSandbox(params: CreateSandboxParams): Promise<ProviderSandboxState>
/** `resume: true` boots a stopped persistent sandbox from its latest snapshot. Null when absent. */
getSandbox(name: string, options: { resume: boolean }): Promise<ProviderSandboxState | null>
stopSession(sessionId: string): Promise<{ snapshotId: string | null }>
extendTimeout(sessionId: string, durationMs: number): Promise<ProviderSession>
updateNetworkPolicy(sessionId: string, policy: NetworkPolicy): Promise<void>
runCommand(sessionId: string, params: RunCommandParams): Promise<CommandResult>
/** Resolves when already gone. */
deleteSandbox(name: string): Promise<void>
listSnapshots(name: string): Promise<ProviderSnapshot[]>
/** Resolves when already gone. */
deleteSnapshot(snapshotId: string): Promise<void>
}
/** Provider failed or answered unexpectedly; the body is never surfaced to callers. */
export class ProviderError extends Error {
constructor(
readonly status: number,
readonly code: string | null
) {
super(`provider request failed (${status}${code ? ` ${code}` : ''})`)
this.name = 'ProviderError'
}
}
@@ -0,0 +1,89 @@
// Commands the golden image installs in /opt/orca/bin (see image/provision-golden.sh). The control
// plane only ever runs these fixed entry points in a VM, never user-supplied command lines.
import { z } from 'zod'
import { ProviderError, type ProviderSandboxState, type SandboxProviderApi } from './sandbox-provider'
export const ORCAD_PORT = 6768
export const START_COMMAND = '/opt/orca/bin/orca-sandbox-start'
export const DRAIN_COMMAND = '/opt/orca/bin/orca-sandbox-drain'
const START_TIMEOUT_MS = 120_000
/** Matches the parity plan: in-flight turns get up to 5 min to become durable restart offers. */
export const DRAIN_TIMEOUT_MS = 5 * 60_000
const ReadySchema = z
.object({
pairingCode: z.string().min(1).max(8192),
projectRoot: z.string().startsWith('/').max(4096)
})
.strict()
export type OrcadReady = z.infer<typeof ReadySchema>
export class VmCommandError extends Error {
constructor(readonly code: 'orcad_start_failed' | 'orcad_ready_invalid' | 'route_missing') {
super(code)
this.name = 'VmCommandError'
}
}
export type StartInputs = {
sandboxId: string
repo?: { url: string; ref?: string }
/** Value the VM sends for credential hosts; the egress layer swaps it for the real credential. */
githubPlaceholder?: string
}
/** Starts orcad (idempotent per boot) and returns its pairing offer; pairing codes are never logged. */
export async function startOrcad(
provider: SandboxProviderApi,
state: ProviderSandboxState,
inputs: StartInputs
): Promise<OrcadReady> {
const route = state.routes.find((candidate) => candidate.port === ORCAD_PORT)
if (!route) {
throw new VmCommandError('route_missing')
}
const env: Record<string, string> = {
ORCA_SANDBOX_ID: inputs.sandboxId,
ORCA_PUBLIC_WSS: route.url.replace(/^https:/, 'wss:')
}
if (inputs.repo) {
env.ORCA_REPO_URL = inputs.repo.url
if (inputs.repo.ref) {
env.ORCA_REPO_REF = inputs.repo.ref
}
}
if (inputs.githubPlaceholder) {
env.ORCA_GITHUB_PLACEHOLDER = inputs.githubPlaceholder
}
const result = await provider.runCommand(state.session.id, {
command: START_COMMAND,
env,
timeoutMs: START_TIMEOUT_MS
})
if (result.exitCode !== 0) {
throw new VmCommandError('orcad_start_failed')
}
const lastLine = result.stdout.trim().split('\n').at(-1) ?? ''
try {
return ReadySchema.parse(JSON.parse(lastLine))
} catch {
throw new VmCommandError('orcad_ready_invalid')
}
}
/** Best effort: orcad turns working chats into durable restart offers before the VM stops. */
export async function drainOrcad(provider: SandboxProviderApi, sessionId: string): Promise<boolean> {
try {
const result = await provider.runCommand(sessionId, {
command: DRAIN_COMMAND,
timeoutMs: DRAIN_TIMEOUT_MS
})
return result.exitCode === 0
} catch (error) {
if (error instanceof ProviderError) {
return false
}
throw error
}
}
+173
View File
@@ -0,0 +1,173 @@
// D1 access for sandbox rows. Every transition is a compare-and-set on `generation`, so two
// concurrent lifecycle calls for one sandbox cannot both act on the provider.
import type { MachineSize } from './entitlements'
export type SandboxStatus =
| 'creating'
| 'running'
| 'suspending'
| 'suspended'
| 'resuming'
| 'destroying'
| 'destroyed'
| 'failed'
export type SandboxRow = {
id: string
account_id: string
user_id: string
workspace_id: string
provider: string
provider_name: string
status: SandboxStatus
machine_size: MachineSize
region: string
generation: number
session_id: string | null
session_started_at: number | null
session_deadline_at: number | null
lease_expires_at: number | null
usage_settled_at: number | null
last_error: string | null
created_at: number
updated_at: number
destroyed_at: number | null
}
export type SessionFields = Pick<
SandboxRow,
'session_id' | 'session_started_at' | 'session_deadline_at' | 'lease_expires_at' | 'usage_settled_at'
>
export const NO_SESSION: SessionFields = {
session_id: null,
session_started_at: null,
session_deadline_at: null,
lease_expires_at: null,
usage_settled_at: null
}
/** Counted against the plan's concurrency limit. */
export const ACTIVE_STATUSES: readonly SandboxStatus[] = ['creating', 'running', 'resuming', 'suspending']
export async function insertSandbox(
db: D1Database,
row: Pick<
SandboxRow,
'id' | 'account_id' | 'user_id' | 'workspace_id' | 'provider' | 'provider_name' | 'machine_size' | 'region'
>,
now: number
): Promise<SandboxRow> {
await db
.prepare(
`INSERT INTO sandboxes (id, account_id, user_id, workspace_id, provider, provider_name, status,
machine_size, region, generation, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, 'creating', ?, ?, 0, ?, ?)`
)
.bind(row.id, row.account_id, row.user_id, row.workspace_id, row.provider, row.provider_name,
row.machine_size, row.region, now, now)
.run()
const inserted = await getSandbox(db, row.id)
if (!inserted) {
throw new Error('sandbox insert lost')
}
return inserted
}
export function getSandbox(db: D1Database, id: string): Promise<SandboxRow | null> {
return db.prepare('SELECT * FROM sandboxes WHERE id = ?').bind(id).first<SandboxRow>()
}
export function getSandboxByProviderName(db: D1Database, name: string): Promise<SandboxRow | null> {
return db.prepare('SELECT * FROM sandboxes WHERE provider_name = ?').bind(name).first<SandboxRow>()
}
export function liveSandboxForWorkspace(
db: D1Database,
accountId: string,
workspaceId: string
): Promise<SandboxRow | null> {
return db
.prepare(`SELECT * FROM sandboxes WHERE account_id = ? AND workspace_id = ? AND status <> 'destroyed'`)
.bind(accountId, workspaceId)
.first<SandboxRow>()
}
export async function listAccountSandboxes(db: D1Database, accountId: string): Promise<SandboxRow[]> {
const { results } = await db
.prepare(`SELECT * FROM sandboxes WHERE account_id = ? AND status <> 'destroyed' ORDER BY created_at`)
.bind(accountId)
.all<SandboxRow>()
return results
}
export async function countActiveSandboxes(db: D1Database, accountId: string): Promise<number> {
const placeholders = ACTIVE_STATUSES.map(() => '?').join(', ')
const row = await db
.prepare(`SELECT COUNT(*) AS n FROM sandboxes WHERE account_id = ? AND status IN (${placeholders})`)
.bind(accountId, ...ACTIVE_STATUSES)
.first<{ n: number }>()
return row?.n ?? 0
}
export async function listByStatus(db: D1Database, statuses: readonly SandboxStatus[]): Promise<SandboxRow[]> {
const placeholders = statuses.map(() => '?').join(', ')
const { results } = await db
.prepare(`SELECT * FROM sandboxes WHERE status IN (${placeholders}) ORDER BY updated_at LIMIT 200`)
.bind(...statuses)
.all<SandboxRow>()
return results
}
type Patch = Partial<Omit<SandboxRow, 'id' | 'account_id' | 'generation' | 'created_at'>>
const PATCHABLE = new Set<string>([
'status', 'session_id', 'session_started_at', 'session_deadline_at', 'lease_expires_at',
'usage_settled_at', 'last_error', 'destroyed_at'
])
/** Applies `patch` only if nobody moved the row since `expected` was read; null on a lost race. */
export async function transition(
db: D1Database,
expected: SandboxRow,
patch: Patch,
now: number
): Promise<SandboxRow | null> {
const entries = Object.entries(patch).filter(([key]) => PATCHABLE.has(key))
const assignments = entries.map(([key]) => `${key} = ?`).join(', ')
const result = await db
.prepare(
`UPDATE sandboxes SET ${assignments}${assignments ? ', ' : ''}generation = generation + 1, updated_at = ?
WHERE id = ? AND generation = ?`
)
.bind(...entries.map(([, value]) => value ?? null), now, expected.id, expected.generation)
.run()
return result.meta.changes === 1 ? getSandbox(db, expected.id) : null
}
export async function recordSnapshots(db: D1Database, sandboxId: string, ids: readonly string[], now: number) {
if (ids.length === 0) {
return
}
await db.batch(
ids.map((id) =>
db
.prepare('INSERT OR IGNORE INTO sandbox_snapshots (snapshot_id, sandbox_id, recorded_at) VALUES (?, ?, ?)')
.bind(id, sandboxId, now)
)
)
}
export async function markSnapshotsDeleted(db: D1Database, sandboxId: string, now: number) {
await db
.prepare('UPDATE sandbox_snapshots SET deleted_at = ? WHERE sandbox_id = ? AND deleted_at IS NULL')
.bind(now, sandboxId)
.run()
}
export async function recordedSnapshotIds(db: D1Database, sandboxId: string): Promise<string[]> {
const { results } = await db
.prepare('SELECT snapshot_id FROM sandbox_snapshots WHERE sandbox_id = ?')
.bind(sandboxId)
.all<{ snapshot_id: string }>()
return results.map((row) => row.snapshot_id)
}
+75
View File
@@ -0,0 +1,75 @@
// Metering: running time becomes outbox segments, then billing usage reports (overage → Stripe there).
import type { EntitlementClient, MachineSize, UsageAnswer } from './entitlements'
import type { SandboxRow } from './store'
const MAX_SEGMENT_MS = 24 * 60 * 60_000
const MAX_ATTEMPTS = 50
type OutboxRow = {
usage_id: string
sandbox_id: string
account_id: string
machine_size: MachineSize
started_at: number
ended_at: number
attempts: number
}
/** Queues the row's unsettled time up to `until`; returns the new settle cursor (unchanged if none). */
export async function queueUsage(db: D1Database, row: SandboxRow, until: number): Promise<number | null> {
const from = row.usage_settled_at
if (from === null || row.session_id === null || until <= from) {
return from
}
const statements = []
for (let start = from; start < until; start += MAX_SEGMENT_MS) {
const end = Math.min(until, start + MAX_SEGMENT_MS)
statements.push(
db
.prepare(
`INSERT OR IGNORE INTO usage_outbox (usage_id, sandbox_id, account_id, machine_size, started_at, ended_at)
VALUES (?, ?, ?, ?, ?, ?)`
)
.bind(`${row.id}:${row.session_id}:${start}`, row.id, row.account_id, row.machine_size, start, end)
)
}
await db.batch(statements)
return until
}
/** Sends pending segments (one sandbox, or the oldest for the cron). Returns billing's last answer. */
export async function flushUsage(
db: D1Database,
entitlements: EntitlementClient,
now: number,
sandboxId?: string
): Promise<UsageAnswer | null> {
const { results } = await db
.prepare(
`SELECT * FROM usage_outbox WHERE sent_at IS NULL AND attempts < ? ${sandboxId ? 'AND sandbox_id = ?' : ''}
ORDER BY started_at LIMIT 100`
)
.bind(...(sandboxId ? [MAX_ATTEMPTS, sandboxId] : [MAX_ATTEMPTS]))
.all<OutboxRow>()
let answer: UsageAnswer | null = null
for (const segment of results) {
try {
answer = await entitlements.reportUsage({
usageId: segment.usage_id,
accountId: segment.account_id,
sandboxId: segment.sandbox_id,
machineSize: segment.machine_size,
startedAt: segment.started_at,
endedAt: segment.ended_at
})
await db.prepare('UPDATE usage_outbox SET sent_at = ? WHERE usage_id = ?').bind(now, segment.usage_id).run()
} catch {
await db
.prepare('UPDATE usage_outbox SET attempts = attempts + 1 WHERE usage_id = ?')
.bind(segment.usage_id)
.run()
break
}
}
return answer
}
@@ -0,0 +1,255 @@
// Vercel Sandbox REST client for Workers. `@vercel/sandbox` needs undici/fs/xdg paths, so this
// calls the same /v2 + /v3 endpoints its api-client uses (SDK 3.9.1), with Orca's team token.
import { z } from 'zod'
import type { NetworkPolicy } from './network-policy'
import {
ProviderError,
type CommandResult,
type ProviderSandboxState,
type ProviderSession,
type SandboxProviderApi
} from './sandbox-provider'
export type VercelSandboxApiConfig = {
token: string
teamId: string
projectId: string
baseUrl?: string
fetch?: typeof fetch
}
const MAX_COMMAND_OUTPUT = 256 * 1024
const Status = z.enum(['pending', 'running', 'stopping', 'stopped', 'failed', 'aborted', 'snapshotting'])
const Session = z.looseObject({
id: z.string(),
status: Status,
startedAt: z.number().optional(),
timeout: z.number()
})
const Sandbox = z.looseObject({
name: z.string(),
status: Status,
currentSessionId: z.string(),
currentSnapshotId: z.string().optional(),
tags: z.record(z.string(), z.string()).optional()
})
const Route = z.looseObject({ url: z.string(), port: z.number() })
const SandboxAndSession = z.looseObject({ sandbox: Sandbox, session: Session, routes: z.array(Route) })
const Snapshot = z.looseObject({
id: z.string(),
status: z.enum(['created', 'deleted', 'failed']),
sourceSessionId: z.string()
})
const ErrorBody = z.looseObject({ error: z.looseObject({ code: z.string().optional() }).optional() })
const CommandLine = z.looseObject({ command: z.looseObject({ exitCode: z.number().nullable() }) })
const OutputLine = z.looseObject({ stream: z.enum(['stdout', 'stderr']), data: z.string() })
const FailureLine = z.looseObject({ stream: z.literal('error'), data: z.looseObject({ code: z.string() }) })
export function createVercelSandboxApi(config: VercelSandboxApiConfig): SandboxProviderApi {
const base = config.baseUrl ?? 'https://vercel.com/api'
const doFetch = config.fetch ?? fetch
async function request(
method: string,
path: string,
options: { query?: Record<string, string | undefined>; body?: unknown } = {}
): Promise<Response> {
const url = new URL(`${base}${path}`)
url.searchParams.set('teamId', config.teamId)
for (const [key, value] of Object.entries(options.query ?? {})) {
if (value !== undefined) {
url.searchParams.set(key, value)
}
}
const response = await doFetch(url, {
method,
headers: {
authorization: `Bearer ${config.token}`,
'content-type': 'application/json'
},
body: options.body === undefined ? undefined : JSON.stringify(options.body),
redirect: 'manual'
})
if (!response.ok) {
const parsed = ErrorBody.safeParse(await response.json().catch(() => null))
throw new ProviderError(response.status, parsed.success ? (parsed.data.error?.code ?? null) : null)
}
return response
}
async function json<T>(schema: z.ZodType<T>, response: Response): Promise<T> {
const parsed = schema.safeParse(await response.json().catch(() => null))
if (!parsed.success) {
throw new ProviderError(response.status, 'invalid_response')
}
return parsed.data
}
const toSession = (session: z.infer<typeof Session>): ProviderSession => ({
id: session.id,
status: session.status,
startedAt: session.startedAt ?? null,
timeoutMs: session.timeout
})
const toState = (body: z.infer<typeof SandboxAndSession>): ProviderSandboxState => ({
sandbox: {
name: body.sandbox.name,
status: body.sandbox.status,
currentSessionId: body.sandbox.currentSessionId,
currentSnapshotId: body.sandbox.currentSnapshotId ?? null,
tags: body.sandbox.tags ?? {}
},
session: toSession(body.session),
routes: body.routes.map((route) => ({ port: route.port, url: route.url }))
})
const missingOk = async (operation: () => Promise<unknown>): Promise<void> => {
try {
await operation()
} catch (error) {
if (!(error instanceof ProviderError && error.status === 404)) {
throw error
}
}
}
return {
async createSandbox(params) {
// No `runtime` selects /v3, which boots from `source` (the golden snapshot).
const response = await request('POST', '/v3/sandboxes', {
body: {
projectId: config.projectId,
name: params.name,
source: { type: 'snapshot', snapshotId: params.snapshotId },
resources: { vcpus: params.vcpus },
region: params.region,
timeout: params.timeoutMs,
ports: params.ports,
persistent: true,
keepLastSnapshots: { count: 2 },
snapshotExpiration: 0,
networkPolicy: params.networkPolicy,
tags: params.tags
}
})
return toState(await json(SandboxAndSession, response))
},
async getSandbox(name, options) {
try {
const response = await request('GET', `/v2/sandboxes/${encodeURIComponent(name)}`, {
query: { projectId: config.projectId, resume: String(options.resume) }
})
return toState(await json(SandboxAndSession, response))
} catch (error) {
if (error instanceof ProviderError && error.status === 404) {
return null
}
throw error
}
},
async stopSession(sessionId) {
const response = await request('POST', `/v2/sandboxes/sessions/${encodeURIComponent(sessionId)}/stop`)
const body = await json(z.looseObject({ snapshot: Snapshot.optional() }), response)
return { snapshotId: body.snapshot?.id ?? null }
},
async extendTimeout(sessionId, durationMs) {
const response = await request(
'POST',
`/v2/sandboxes/sessions/${encodeURIComponent(sessionId)}/extend-timeout`,
{ body: { duration: durationMs } }
)
return toSession((await json(z.looseObject({ session: Session }), response)).session)
},
async updateNetworkPolicy(sessionId, policy: NetworkPolicy) {
await request('POST', `/v2/sandboxes/sessions/${encodeURIComponent(sessionId)}/network-policy`, {
body: policy
})
},
async runCommand(sessionId, params) {
const response = await request('POST', `/v2/sandboxes/sessions/${encodeURIComponent(sessionId)}/cmd`, {
body: {
command: params.command,
args: params.args ?? [],
cwd: params.cwd,
env: params.env ?? {},
sudo: params.sudo ?? false,
wait: true,
logs: true,
timeout: params.timeoutMs
}
})
return readCommandStream(response)
},
deleteSandbox: (name) =>
missingOk(() =>
request('DELETE', `/v2/sandboxes/${encodeURIComponent(name)}`, {
query: { projectId: config.projectId, deleteOrphanSnapshots: 'true' }
})
),
async listSnapshots(name) {
const snapshots: z.infer<typeof Snapshot>[] = []
let cursor: string | undefined
do {
const response = await request('GET', '/v2/sandboxes/snapshots', {
query: { project: config.projectId, name, limit: '50', cursor }
})
const page = await json(
z.looseObject({
snapshots: z.array(Snapshot),
pagination: z.looseObject({ next: z.string().nullable() })
}),
response
)
snapshots.push(...page.snapshots)
cursor = page.pagination.next ?? undefined
} while (cursor && snapshots.length < 1000)
return snapshots.map((s) => ({ id: s.id, status: s.status, sourceSessionId: s.sourceSessionId }))
},
deleteSnapshot: (snapshotId) =>
missingOk(() => request('DELETE', `/v2/sandboxes/snapshots/${encodeURIComponent(snapshotId)}`))
}
}
/** `wait: true` answers ndjson: the command, its log lines, then the finished command. */
async function readCommandStream(response: Response): Promise<CommandResult> {
const text = await response.text()
const output = { stdout: '', stderr: '' }
let exitCode: number | null = null
for (const line of text.split('\n')) {
if (!line.trim()) {
continue
}
let value: unknown
try {
value = JSON.parse(line)
} catch {
throw new ProviderError(response.status, 'invalid_response')
}
const command = CommandLine.safeParse(value)
const log = OutputLine.safeParse(value)
const failure = FailureLine.safeParse(value)
if (command.success) {
exitCode = command.data.command.exitCode ?? exitCode
} else if (log.success) {
if (output[log.data.stream].length < MAX_COMMAND_OUTPUT) {
output[log.data.stream] += log.data.data
}
} else if (failure.success) {
throw new ProviderError(response.status, failure.data.data.code)
} else {
throw new ProviderError(response.status, 'invalid_response')
}
}
if (exitCode === null) {
throw new ProviderError(response.status, 'command_unfinished')
}
return { exitCode, ...output }
}
+70
View File
@@ -0,0 +1,70 @@
// Request schemas and response views. Mirrors src/shared/hosted-sandbox/hosted-sandbox-wire.ts on
// the desktop; test/wire-contract.test.ts parses every response with the desktop schemas.
import { z } from 'zod'
import type { OrcadReady } from './sandbox-vm-commands'
import type { SandboxRow } from './store'
const Id = z
.string()
.min(1)
.max(128)
.regex(/^[A-Za-z0-9_.:-]+$/)
export const CreateRequestSchema = z
.object({
workspaceId: Id,
machineSize: z.enum(['standard', 'large', 'xlarge']),
region: z
.string()
.regex(/^[a-z]{3}[0-9]$/)
.optional(),
repo: z
.object({
url: z.url({ protocol: /^https$/ }).max(2048),
ref: z.string().min(1).max(255).optional()
})
.strict()
.optional()
})
.strict()
export const LeaseRequestSchema = z
.object({
extendMs: z
.number()
.int()
.min(60_000)
.max(60 * 60_000)
})
.strict()
export function sandboxView(row: SandboxRow) {
const session =
row.session_id !== null &&
row.session_started_at !== null &&
row.session_deadline_at !== null &&
row.lease_expires_at !== null
? {
id: row.session_id,
startedAt: row.session_started_at,
deadlineAt: row.session_deadline_at,
leaseExpiresAt: row.lease_expires_at
}
: null
return {
id: row.id,
workspaceId: row.workspace_id,
status: row.status,
machineSize: row.machine_size,
region: row.region,
session,
createdAt: row.created_at
}
}
export function readyView(row: SandboxRow, connection: OrcadReady) {
return {
sandbox: sandboxView(row),
connection: { type: 'orca-server' as const, ...connection }
}
}
@@ -0,0 +1,64 @@
// A D1Database over node:sqlite for tests: same SQL dialect (SQLite), same prepare/bind/first/
// all/run/batch surface, batch() in one transaction like D1.
import { readFileSync, readdirSync } from 'node:fs'
import { DatabaseSync } from 'node:sqlite'
import { fileURLToPath } from 'node:url'
type Param = string | number | null
class FakeStatement {
constructor(
private readonly db: DatabaseSync,
readonly sql: string,
readonly params: Param[] = []
) {}
bind(...params: unknown[]): FakeStatement {
for (const param of params) {
if (param === undefined || typeof param === 'boolean' || typeof param === 'object' && param !== null) {
throw new Error(`D1 cannot bind ${typeof param}`)
}
}
return new FakeStatement(this.db, this.sql, params as Param[])
}
async first<T>(): Promise<T | null> {
return (this.db.prepare(this.sql).get(...this.params) as T | undefined) ?? null
}
async all<T>(): Promise<{ results: T[]; success: true; meta: { changes: number } }> {
return { results: this.db.prepare(this.sql).all(...this.params) as T[], success: true, meta: { changes: 0 } }
}
runSync(): { success: true; meta: { changes: number } } {
const result = this.db.prepare(this.sql).run(...this.params)
return { success: true, meta: { changes: Number(result.changes) } }
}
async run() {
return this.runSync()
}
}
export function createFakeD1(): { db: D1Database; raw: DatabaseSync } {
const raw = new DatabaseSync(':memory:')
const dir = fileURLToPath(new URL('../migrations/', import.meta.url).href)
for (const file of readdirSync(dir).filter((name) => name.endsWith('.sql')).sort()) {
raw.exec(readFileSync(`${dir}${file}`, 'utf8'))
}
const db = {
prepare: (sql: string) => new FakeStatement(raw, sql),
async batch(statements: FakeStatement[]) {
raw.exec('BEGIN')
try {
const results = statements.map((statement) => statement.runSync())
raw.exec('COMMIT')
return results
} catch (error) {
raw.exec('ROLLBACK')
throw error
}
}
}
return { db: db as unknown as D1Database, raw }
}
@@ -0,0 +1,81 @@
import { SignJWT, createLocalJWKSet, exportJWK, generateKeyPair } from 'jose'
import { describe, expect, it } from 'vitest'
import { createHarness } from './harness'
const PROXY_URL = 'https://sandbox.test/v1/egress/proxy'
const ISSUER = 'https://oidc.vercel.com/team_orca'
async function setup() {
const { privateKey, publicKey } = await generateKeyPair('RS256')
const jwks = { keys: [{ ...(await exportJWK(publicKey)), kid: 'v1', alg: 'RS256' }] }
const upstream: { url: string; authorization: string | null }[] = []
const h = await createHarness(
{ EGRESS_PROXY_URL: PROXY_URL },
{
egressKeysForIssuer: () => createLocalJWKSet(jwks),
credentials: { authorizationFor: async (row, host) => (host === 'api.github.com' ? `Bearer ghs_for_${row.workspace_id}` : null) },
egressFetch: async (input, init) => {
upstream.push({ url: String(input), authorization: new Headers(init?.headers).get('authorization') })
return new Response('{"login":"orca"}', { status: 200, headers: { 'content-type': 'application/json' } })
}
}
)
const created = await h.call('POST', '/v1/sandboxes', { body: { workspaceId: 'ws-1', machineSize: 'standard' } })
const sandboxId: string = created.body.sandbox.id
async function oidc(claims: Record<string, string>, audience = PROXY_URL) {
return new SignJWT({ team_id: 'team_orca', project_id: 'prj_orca', sandbox_id: 'sbx-v', sandbox_name: `orca-test-${sandboxId}`, ...claims })
.setProtectedHeader({ alg: 'RS256', kid: 'v1' })
.setIssuer(ISSUER)
.setAudience(audience)
.setIssuedAt()
.setExpirationTime('5m')
.sign(privateKey)
}
async function proxied(init: { token: string; authorization?: string; host?: string }) {
const response = await h.send(
new Request(`${PROXY_URL}/user`, {
headers: {
'vercel-forwarded-host': init.host ?? 'api.github.com',
'vercel-forwarded-scheme': 'https',
'vercel-forwarded-port': '443',
'vercel-forwarded-path': '/user',
'vercel-sandbox-oidc-token': init.token,
...(init.authorization ? { authorization: init.authorization } : {})
}
})
)
return { status: response.status, body: await response.text() }
}
return { h, sandboxId, oidc, proxied, upstream }
}
describe('egress proxy (Vercel forwardURL target)', () => {
it('swaps the placeholder for the workspace credential on a verified sandbox request', async () => {
const { sandboxId, oidc, proxied, upstream } = await setup()
const res = await proxied({ token: await oidc({}), authorization: `Bearer orca-ph-${sandboxId}` })
expect(res).toEqual({ status: 200, body: '{"login":"orca"}' })
expect(upstream).toEqual([{ url: 'https://api.github.com/user', authorization: 'Bearer ghs_for_ws-1' }])
})
it('refuses requests without the placeholder, from other teams, or for unproxied hosts', async () => {
const { sandboxId, oidc, proxied, upstream } = await setup()
const placeholder = `Bearer orca-ph-${sandboxId}`
expect(await proxied({ token: await oidc({}), authorization: 'Bearer attacker' })).toEqual({ status: 403, body: 'placeholder_missing' })
expect(await proxied({ token: await oidc({ team_id: 'team_other' }), authorization: placeholder })).toEqual({ status: 403, body: 'foreign_sandbox' })
expect(await proxied({ token: await oidc({}, 'https://elsewhere.test'), authorization: placeholder })).toEqual({ status: 403, body: 'bad_oidc_token' })
expect(await proxied({ token: await oidc({}), authorization: placeholder, host: 'example.com' })).toEqual({ status: 403, body: 'host_not_proxied' })
expect(upstream).toEqual([])
})
it('forwards the placeholder route for api.github.com in the sandbox network policy', async () => {
const { h, sandboxId } = await setup()
const [box] = [...h.vercel.boxes.values()]
expect(box!.policy.allow['api.github.com']).toEqual([
{
match: { headers: [{ key: { exact: 'authorization' }, value: { exact: `Bearer orca-ph-${sandboxId}` } }] },
forwardURL: PROXY_URL
},
{ response: { statusCode: 403, body: 'orca-egress-denied', contentType: 'text/plain' } }
])
})
})
@@ -0,0 +1,161 @@
// In-memory Vercel Sandbox with the behaviours the bake-off measured: stop = reboot from an automatic
// snapshot, resume keeps the name and route, and delete leaves automatic snapshots behind.
import type { NetworkPolicy } from '../src/network-policy'
import {
ProviderError,
type CreateSandboxParams,
type ProviderSandboxState,
type ProviderSnapshot,
type RunCommandParams,
type SandboxProviderApi
} from '../src/sandbox-provider'
import { DRAIN_COMMAND, START_COMMAND } from '../src/sandbox-vm-commands'
type FakeBox = {
params: CreateSandboxParams
status: 'running' | 'stopped'
sessionId: string
startedAt: number
timeoutMs: number
route: string
policy: NetworkPolicy
}
export type FakeVercel = SandboxProviderApi & {
boxes: Map<string, FakeBox>
snapshots: Map<string, ProviderSnapshot & { sandboxName: string }>
commands: { sessionId: string; command: string; env: Record<string, string> }[]
failStart: boolean
failCreate: boolean
/** Simulates the provider ending a session at its timeout. */
expire(name: string): void
}
export function createFakeVercel(now: () => number): FakeVercel {
const boxes = new Map<string, FakeBox>()
const snapshots: FakeVercel['snapshots'] = new Map()
const commands: FakeVercel['commands'] = []
let counter = 0
const next = (prefix: string) => `${prefix}_${++counter}`
const state = (box: FakeBox): ProviderSandboxState => ({
sandbox: {
name: box.params.name,
status: box.status,
currentSessionId: box.sessionId,
currentSnapshotId: null,
tags: box.params.tags
},
session: { id: box.sessionId, status: box.status, startedAt: box.startedAt, timeoutMs: box.timeoutMs },
routes: [{ port: 6768, url: box.route }]
})
const bySession = (sessionId: string): FakeBox => {
const box = [...boxes.values()].find((candidate) => candidate.sessionId === sessionId)
if (!box) {
throw new ProviderError(404, 'not_found')
}
return box
}
const stop = (box: FakeBox): string => {
box.status = 'stopped'
const id = next('snap')
snapshots.set(id, { id, status: 'created', sourceSessionId: box.sessionId, sandboxName: box.params.name })
return id
}
const fake: FakeVercel = {
boxes,
snapshots,
commands,
failStart: false,
failCreate: false,
expire(name) {
const box = boxes.get(name)
if (box?.status === 'running') {
stop(box)
}
},
async createSandbox(params) {
if (fake.failCreate) {
throw new ProviderError(500, 'internal_error')
}
if (boxes.has(params.name)) {
throw new ProviderError(409, 'conflict')
}
const box: FakeBox = {
params,
status: 'running',
sessionId: next('sess'),
startedAt: now(),
timeoutMs: params.timeoutMs,
route: `https://sb-${counter}.vercel.run`,
policy: params.networkPolicy
}
boxes.set(params.name, box)
return state(box)
},
async getSandbox(name, options) {
const box = boxes.get(name)
if (!box) {
return null
}
if (box.status === 'stopped' && options.resume) {
Object.assign(box, {
status: 'running',
sessionId: next('sess'),
startedAt: now(),
timeoutMs: box.params.timeoutMs
})
}
return state(box)
},
async stopSession(sessionId) {
const box = bySession(sessionId)
return { snapshotId: box.status === 'running' ? stop(box) : null }
},
async extendTimeout(sessionId, durationMs) {
const box = bySession(sessionId)
box.timeoutMs += durationMs
return state(box).session
},
async updateNetworkPolicy(sessionId, policy) {
bySession(sessionId).policy = policy
},
async runCommand(sessionId, params: RunCommandParams) {
const box = bySession(sessionId)
if (box.status !== 'running') {
throw new ProviderError(410, 'sandbox_stopped')
}
commands.push({ sessionId, command: params.command, env: params.env ?? {} })
if (params.command === START_COMMAND) {
if (fake.failStart) {
return { exitCode: 1, stdout: '', stderr: 'orcad crashed' }
}
const ready = { pairingCode: `orca://pair?code=${box.params.name}`, projectRoot: '/vercel/sandbox/project' }
return { exitCode: 0, stdout: `booting\n${JSON.stringify(ready)}\n`, stderr: '' }
}
if (params.command === DRAIN_COMMAND) {
return { exitCode: 0, stdout: '', stderr: '' }
}
return { exitCode: 127, stdout: '', stderr: 'not found' }
},
async deleteSandbox(name) {
const box = boxes.get(name)
if (box?.status === 'running') {
stop(box)
}
boxes.delete(name)
},
async listSnapshots(name) {
return [...snapshots.values()]
.filter((snapshot) => snapshot.sandboxName === name)
.map(({ id, status, sourceSessionId }) => ({ id, status, sourceSessionId }))
},
async deleteSnapshot(snapshotId) {
const snapshot = snapshots.get(snapshotId)
if (snapshot) {
snapshot.status = 'deleted'
}
}
}
return fake
}
@@ -0,0 +1,98 @@
import { SignJWT, exportJWK, generateKeyPair } from 'jose'
import type { EntitlementClient, StartDecision, UsageAnswer, UsageSegment } from '../src/entitlements'
import type { Env } from '../src/env'
import { handleRequest, type Deps } from '../src/index'
import { reconcile } from '../src/reconcile'
import { createFakeD1 } from './d1-fake'
import { createFakeVercel } from './fake-vercel'
export const GOLDEN = 'snap_golden'
export async function createHarness(overrides: Partial<Env> = {}, extraDeps: Partial<Deps> = {}) {
const { privateKey, publicKey } = await generateKeyPair('ES256')
const jwk = { ...(await exportJWK(publicKey)), kid: 'k1', alg: 'ES256', use: 'sig' }
const { db, raw } = createFakeD1()
let now = 1_800_000_000_000
const clock = () => now
const vercel = createFakeVercel(clock)
const billing = {
decision: { allow: true, reason: null } as StartDecision,
usageAnswer: { canStartSandbox: true, reason: null } as UsageAnswer,
down: false,
checks: [] as { accountId: string; runningSandboxes: number }[],
usage: [] as UsageSegment[]
}
const entitlements: EntitlementClient = {
async checkStart(request) {
billing.checks.push({ accountId: request.accountId, runningSandboxes: request.runningSandboxes })
return billing.decision
},
async reportUsage(segment) {
if (billing.down) {
throw new Error('billing down')
}
billing.usage.push(segment)
return billing.usageAnswer
}
}
const env: Env = {
SANDBOX_DB: db,
SANDBOX_ENV: 'test',
SANDBOX_JWKS: JSON.stringify({ keys: [jwk] }),
SANDBOX_JWT_AUDIENCE: 'orca-sandbox',
SANDBOX_AUTH_ISSUER: 'https://login.onorca.dev',
GOLDEN_SNAPSHOT_ID: GOLDEN,
VERCEL_TEAM_ID: 'team_orca',
VERCEL_PROJECT_ID: 'prj_orca',
...overrides
}
const deps: Deps = { provider: vercel, entitlements, now: clock, ...extraDeps }
async function token(sub: string, org?: string, purpose = 'sandbox') {
const iat = Math.floor(now / 1000)
return new SignJWT({ sub, ...(org ? { org } : {}), purpose })
.setProtectedHeader({ alg: 'ES256', kid: 'k1' })
.setIssuer('https://login.onorca.dev')
.setAudience('orca-sandbox')
.setIssuedAt(iat)
.setExpirationTime(iat + 300)
.sign(privateKey)
}
async function send(request: Request): Promise<Response> {
return handleRequest(request, env, deps)
}
async function call(method: string, path: string, init: { body?: unknown; as?: string; bearer?: string } = {}) {
const bearer = init.bearer ?? (await token(init.as ?? 'alice'))
const request = new Request(`https://sandbox.test${path}`, {
method,
headers: { authorization: `Bearer ${bearer}`, 'content-type': 'application/json' },
body: init.body === undefined ? undefined : JSON.stringify(init.body)
})
const response = await handleRequest(request, env, deps)
return { status: response.status, body: (await response.json()) as any }
}
return {
env,
raw,
vercel,
billing,
call,
send,
token,
advance(ms: number) {
now += ms
},
now: clock,
reconcile: () =>
reconcile({
db,
provider: vercel,
entitlements,
now: clock,
config: { environment: 'test', goldenSnapshotId: GOLDEN, defaultRegion: 'iad1' }
})
}
}
@@ -0,0 +1,190 @@
import { describe, expect, it } from 'vitest'
import {
HostedSandboxListResponseSchema,
HostedSandboxReadyResponseSchema,
HostedSandboxStatusResponseSchema
} from '../../../../src/shared/hosted-sandbox/hosted-sandbox-wire'
import { DRAIN_COMMAND, START_COMMAND } from '../src/sandbox-vm-commands'
import { GOLDEN, createHarness } from './harness'
const MIN = 60_000
const create = { workspaceId: 'ws-1', machineSize: 'standard' }
describe('sandbox control plane lifecycle', () => {
it('creates a white-labelled sandbox from the golden image and returns orcad pairing', async () => {
const h = await createHarness()
const res = await h.call('POST', '/v1/sandboxes', { body: create })
expect(res.status).toBe(200)
const ready = HostedSandboxReadyResponseSchema.parse(res.body)
expect(ready.sandbox.status).toBe('running')
expect(ready.connection.projectRoot).toBe('/vercel/sandbox/project')
expect(ready.sandbox.session!.deadlineAt - ready.sandbox.session!.startedAt).toBe(23 * 60 * MIN + 50 * MIN)
expect(ready.sandbox.session!.leaseExpiresAt - ready.sandbox.session!.startedAt).toBe(30 * MIN)
const [box] = [...h.vercel.boxes.values()]
expect(box!.params).toMatchObject({
name: `orca-test-${ready.sandbox.id}`,
snapshotId: GOLDEN,
vcpus: 4,
ports: [6768],
tags: { orcaSandbox: ready.sandbox.id, orcaEnv: 'test' }
})
expect(Object.keys(box!.policy.allow)).not.toContain('*')
const start = h.vercel.commands.find((c) => c.command === START_COMMAND)
expect(start!.env.ORCA_PUBLIC_WSS).toBe(box!.route.replace('https:', 'wss:'))
expect(h.billing.checks).toEqual([{ accountId: 'user:alice', runningSandboxes: 0 }])
})
it('refuses a start billing denies without touching the provider', async () => {
const h = await createHarness()
h.billing.decision = { allow: false, reason: 'concurrency_limit' }
const res = await h.call('POST', '/v1/sandboxes', { body: create })
expect(res).toEqual({ status: 403, body: { error: 'entitlement_denied', reason: 'concurrency_limit' } })
expect(h.vercel.boxes.size).toBe(0)
expect(h.raw.prepare('SELECT COUNT(*) AS n FROM sandboxes').get()).toEqual({ n: 0 })
})
it('hides one account’s sandbox from another', async () => {
const h = await createHarness()
const { body } = await h.call('POST', '/v1/sandboxes', { body: create })
expect((await h.call('GET', `/v1/sandboxes/${body.sandbox.id}`, { as: 'bob' })).status).toBe(404)
expect((await h.call('DELETE', `/v1/sandboxes/${body.sandbox.id}`, { as: 'bob' })).status).toBe(404)
expect(HostedSandboxListResponseSchema.parse((await h.call('GET', '/v1/sandboxes', { as: 'bob' })).body).sandboxes).toEqual([])
expect(h.vercel.boxes.size).toBe(1)
})
it('returns the same environment when a create is retried for the workspace', async () => {
const h = await createHarness()
const first = await h.call('POST', '/v1/sandboxes', { body: create })
const second = await h.call('POST', '/v1/sandboxes', { body: create })
expect(second.body.sandbox.id).toBe(first.body.sandbox.id)
expect(h.vercel.boxes.size).toBe(1)
})
it('drains before stopping, meters the session, and resumes the same disk', async () => {
const h = await createHarness()
const { body } = await h.call('POST', '/v1/sandboxes', { body: create })
const id = body.sandbox.id
h.advance(20 * MIN)
const suspended = await h.call('POST', `/v1/sandboxes/${id}/suspend`)
expect(HostedSandboxStatusResponseSchema.parse(suspended.body).sandbox).toMatchObject({ status: 'suspended', session: null })
expect(h.vercel.commands.map((c) => c.command)).toEqual([START_COMMAND, DRAIN_COMMAND])
expect(h.billing.usage).toEqual([
expect.objectContaining({ sandboxId: id, accountId: 'user:alice', machineSize: 'standard', endedAt: h.now(), startedAt: h.now() - 20 * MIN })
])
const resumed = HostedSandboxReadyResponseSchema.parse((await h.call('POST', `/v1/sandboxes/${id}/resume`)).body)
expect(resumed.sandbox.status).toBe('running')
expect(resumed.sandbox.session!.id).not.toBe(body.sandbox.session.id)
expect(h.vercel.boxes.size).toBe(1)
expect(h.billing.checks).toHaveLength(2)
})
it('renews the Orca lease but never past the forced-rollover deadline', async () => {
const h = await createHarness()
const { body } = await h.call('POST', '/v1/sandboxes', { body: create })
const startedAt = body.sandbox.session.startedAt
const renewed = await h.call('POST', `/v1/sandboxes/${body.sandbox.id}/lease`, { body: { extendMs: 60 * MIN } })
expect(renewed.body.sandbox.session.leaseExpiresAt).toBe(h.now() + 60 * MIN)
h.advance(23 * 60 * MIN + 30 * MIN)
const clamped = await h.call('POST', `/v1/sandboxes/${body.sandbox.id}/lease`, { body: { extendMs: 60 * MIN } })
expect(clamped.body.sandbox.session.leaseExpiresAt).toBe(startedAt + 23 * 60 * MIN + 50 * MIN)
})
it('rolls a session over onto a fresh one from the same disk', async () => {
const h = await createHarness()
const { body } = await h.call('POST', '/v1/sandboxes', { body: create })
h.advance(12 * 60 * MIN)
const rolled = HostedSandboxReadyResponseSchema.parse((await h.call('POST', `/v1/sandboxes/${body.sandbox.id}/rollover`)).body)
expect(rolled.sandbox.id).toBe(body.sandbox.id)
expect(rolled.sandbox.session!.startedAt).toBe(h.now())
expect(h.vercel.commands.map((c) => c.command)).toEqual([START_COMMAND, DRAIN_COMMAND, START_COMMAND])
expect(h.billing.usage.map((u) => u.endedAt - u.startedAt)).toEqual([12 * 60 * MIN])
})
it('destroys the sandbox and every snapshot it left behind, idempotently', async () => {
const h = await createHarness()
const { body } = await h.call('POST', '/v1/sandboxes', { body: create })
await h.call('POST', `/v1/sandboxes/${body.sandbox.id}/suspend`)
await h.call('POST', `/v1/sandboxes/${body.sandbox.id}/resume`)
expect([...h.vercel.snapshots.values()].filter((s) => s.status === 'created')).toHaveLength(1)
const destroyed = await h.call('DELETE', `/v1/sandboxes/${body.sandbox.id}`)
expect(destroyed.body.sandbox.status).toBe('destroyed')
expect(h.vercel.boxes.size).toBe(0)
expect([...h.vercel.snapshots.values()].every((s) => s.status === 'deleted')).toBe(true)
expect((await h.call('DELETE', `/v1/sandboxes/${body.sandbox.id}`)).body.sandbox.status).toBe('destroyed')
expect((await h.call('GET', '/v1/sandboxes')).body.sandboxes).toEqual([])
})
it('cleans up a create whose orcad never came up, so the workspace can retry', async () => {
const h = await createHarness()
h.vercel.failStart = true
const res = await h.call('POST', '/v1/sandboxes', { body: create })
expect(res).toEqual({ status: 502, body: { error: 'provider_unavailable', reason: 'orcad_start_failed' } })
expect(h.vercel.boxes.size).toBe(0)
h.vercel.failStart = false
expect((await h.call('POST', '/v1/sandboxes', { body: create })).status).toBe(200)
})
it('refuses lifecycle calls until Orca’s provider account is configured', async () => {
const h = await createHarness({ GOLDEN_SNAPSHOT_ID: undefined })
expect(await h.call('POST', '/v1/sandboxes', { body: create })).toEqual({ status: 503, body: { error: 'not_configured' } })
})
it('rejects tokens minted for another purpose', async () => {
const h = await createHarness()
const res = await h.call('GET', '/v1/sandboxes', { bearer: await h.token('alice', undefined, 'billing') })
expect(res.status).toBe(401)
})
})
describe('sandbox control plane cron', () => {
it('stops a sandbox at the forced-rollover deadline when no desktop rolled it over', async () => {
const h = await createHarness()
const { body } = await h.call('POST', '/v1/sandboxes', { body: create })
await h.call('POST', `/v1/sandboxes/${body.sandbox.id}/lease`, { body: { extendMs: 60 * MIN } })
h.advance(23 * 60 * MIN + 50 * MIN)
expect((await h.reconcile()).suspendedForDeadline).toBe(1)
expect((await h.call('GET', `/v1/sandboxes/${body.sandbox.id}`)).body.sandbox.status).toBe('suspended')
})
it('bills a lapsed lease only up to when the provider stopped it', async () => {
const h = await createHarness()
const { body } = await h.call('POST', '/v1/sandboxes', { body: create })
h.advance(45 * MIN)
h.vercel.expire(`orca-test-${body.sandbox.id}`)
expect((await h.reconcile()).leaseLapsed).toBe(1)
expect(h.billing.usage.map((u) => u.endedAt - u.startedAt)).toEqual([30 * MIN])
})
it('stops running sandboxes once billing says the included hours are gone', async () => {
const h = await createHarness()
const { body } = await h.call('POST', '/v1/sandboxes', { body: create })
h.billing.usageAnswer = { canStartSandbox: false, reason: 'included_hours_exhausted' }
h.advance(10 * MIN)
expect((await h.reconcile()).suspendedForBilling).toBe(1)
expect((await h.call('GET', `/v1/sandboxes/${body.sandbox.id}`)).body.sandbox.status).toBe('suspended')
})
it('keeps usage owed while billing is down and reports it once billing is back', async () => {
const h = await createHarness()
const { body } = await h.call('POST', '/v1/sandboxes', { body: create })
h.billing.down = true
h.advance(5 * MIN)
expect((await h.call('POST', `/v1/sandboxes/${body.sandbox.id}/suspend`)).status).toBe(200)
expect(h.billing.usage).toEqual([])
h.billing.down = false
await h.reconcile()
expect(h.billing.usage.map((u) => u.endedAt - u.startedAt)).toEqual([5 * MIN])
})
it('releases a sandbox whose create died mid-way', async () => {
const h = await createHarness()
const { body } = await h.call('POST', '/v1/sandboxes', { body: create })
h.raw.prepare(`UPDATE sandboxes SET status = 'creating' WHERE id = ?`).run(body.sandbox.id)
h.advance(16 * MIN)
expect((await h.reconcile()).released).toBe(1)
expect(h.vercel.boxes.size).toBe(0)
})
})
@@ -0,0 +1,63 @@
import { describe, expect, it } from 'vitest'
import { compileNetworkPolicy, gitBasicAuthorization } from '../src/network-policy'
const deny = { response: { statusCode: 403, body: 'orca-egress-denied', contentType: 'text/plain' } }
describe('compileNetworkPolicy', () => {
it('fails closed: no wildcard, and credential hosts end with a deny rule', () => {
const policy = compileNetworkPolicy({
mode: 'allowlist',
allowlist: ['registry.npmjs.org', 'github.com'],
credentials: [
{
host: 'github.com',
header: 'Authorization',
placeholder: gitBasicAuthorization('orca-ph-1'),
via: 'transform',
value: gitBasicAuthorization('ghs_real')
},
{
host: 'api.github.com',
header: 'authorization',
placeholder: 'Bearer orca-ph-1',
via: 'forward',
forwardURL: 'https://sandbox.onorca.dev/v1/egress/proxy'
}
]
})
expect(policy).toEqual({
allow: {
'registry.npmjs.org': [],
'github.com': [
{
match: { headers: [{ key: { exact: 'authorization' }, value: { exact: 'Basic eC1hY2Nlc3MtdG9rZW46b3JjYS1waC0x' } }] },
transform: [{ headers: { authorization: 'Basic eC1hY2Nlc3MtdG9rZW46Z2hzX3JlYWw=' } }]
},
deny
],
'api.github.com': [
{
match: { headers: [{ key: { exact: 'authorization' }, value: { exact: 'Bearer orca-ph-1' } }] },
forwardURL: 'https://sandbox.onorca.dev/v1/egress/proxy'
},
deny
]
}
})
})
it('adds the wildcard only when a workspace opts into open egress', () => {
expect(compileNetworkPolicy({ mode: 'open', allowlist: [] })).toEqual({ allow: { '*': [] } })
})
it('rejects malformed hosts and wildcard credential hosts', () => {
expect(() => compileNetworkPolicy({ mode: 'allowlist', allowlist: ['https://x.com'] })).toThrow()
expect(() =>
compileNetworkPolicy({
mode: 'allowlist',
allowlist: [],
credentials: [{ host: '*.github.com', header: 'authorization', placeholder: 'p', via: 'transform', value: 'v' }]
})
).toThrow(/exact host/)
})
})
@@ -0,0 +1,95 @@
import { describe, expect, it } from 'vitest'
import { ProviderError } from '../src/sandbox-provider'
import { createVercelSandboxApi } from '../src/vercel-sandbox-api'
type Seen = { method: string; url: URL; headers: Headers; body: any }
function api(answer: (seen: Seen) => Response) {
const seen: Seen[] = []
const client = createVercelSandboxApi({
token: 'vercel_test_token',
teamId: 'team_orca',
projectId: 'prj_orca',
fetch: async (input, init) => {
const entry = {
method: init?.method ?? 'GET',
url: new URL(String(input)),
headers: new Headers(init?.headers),
body: typeof init?.body === 'string' ? JSON.parse(init.body) : undefined
}
seen.push(entry)
return answer(entry)
}
})
return { client, seen }
}
const sandboxBody = {
sandbox: { name: 'orca-prod-sbx_1', status: 'running', currentSessionId: 'sess_1', persistent: true, tags: { orcaSandbox: 'sbx_1' } },
session: { id: 'sess_1', status: 'running', startedAt: 1000, timeout: 1_800_000, memory: 8192, vcpus: 4 },
routes: [{ port: 6768, url: 'https://sb-1.vercel.run', subdomain: 'sb-1' }]
}
describe('Vercel Sandbox REST client', () => {
it('creates a persistent sandbox from the golden snapshot on the v3 endpoint', async () => {
const { client, seen } = api(() => Response.json(sandboxBody))
const state = await client.createSandbox({
name: 'orca-prod-sbx_1',
snapshotId: 'snap_golden',
vcpus: 4,
region: 'iad1',
timeoutMs: 1_800_000,
ports: [6768],
networkPolicy: { allow: { 'registry.npmjs.org': [] } },
tags: { orcaSandbox: 'sbx_1' }
})
expect(seen[0]!.method).toBe('POST')
expect(`${seen[0]!.url.origin}${seen[0]!.url.pathname}${seen[0]!.url.search}`).toBe(
'https://vercel.com/api/v3/sandboxes?teamId=team_orca'
)
expect(seen[0]!.headers.get('authorization')).toBe('Bearer vercel_test_token')
expect(seen[0]!.body).toMatchObject({
projectId: 'prj_orca',
source: { type: 'snapshot', snapshotId: 'snap_golden' },
resources: { vcpus: 4 },
persistent: true,
keepLastSnapshots: { count: 2 },
snapshotExpiration: 0
})
expect(state.routes).toEqual([{ port: 6768, url: 'https://sb-1.vercel.run' }])
expect(state.session.timeoutMs).toBe(1_800_000)
})
it('reads the command ndjson stream to the finished exit code', async () => {
const lines = [
{ command: { id: 'cmd_1', exitCode: null } },
{ stream: 'stdout', data: 'boot\n' },
{ stream: 'stderr', data: 'warn\n' },
{ stream: 'stdout', data: '{"ok":1}\n' },
{ command: { id: 'cmd_1', exitCode: 0 } }
]
const { client, seen } = api(() => new Response(lines.map((l) => JSON.stringify(l)).join('\n'), { headers: { 'content-type': 'application/x-ndjson' } }))
const result = await client.runCommand('sess_1', { command: '/opt/orca/bin/orca-sandbox-start', timeoutMs: 1000 })
expect(result).toEqual({ exitCode: 0, stdout: 'boot\n{"ok":1}\n', stderr: 'warn\n' })
expect(seen[0]!.url.pathname).toBe('/api/v2/sandboxes/sessions/sess_1/cmd')
expect(seen[0]!.body).toMatchObject({ wait: true, logs: true })
})
it('treats a missing sandbox as absent and deletes with orphan snapshots', async () => {
const { client, seen } = api((s) =>
s.method === 'GET' ? Response.json({ error: { code: 'not_found' } }, { status: 404 }) : Response.json({ error: { code: 'not_found' } }, { status: 404 })
)
expect(await client.getSandbox('orca-prod-sbx_9', { resume: true })).toBeNull()
await client.deleteSandbox('orca-prod-sbx_9')
expect(seen[0]!.url.searchParams.get('resume')).toBe('true')
expect(seen[1]!.method).toBe('DELETE')
expect(seen[1]!.url.searchParams.get('deleteOrphanSnapshots')).toBe('true')
})
it('surfaces provider failures as codes without their bodies', async () => {
const { client } = api(() => Response.json({ error: { code: 'rate_limited', message: 'secret detail' } }, { status: 429 }))
const error = await client.stopSession('sess_1').catch((caught: unknown) => caught)
expect(error).toBeInstanceOf(ProviderError)
expect((error as ProviderError).message).toBe('provider request failed (429 rate_limited)')
})
})
+14
View File
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "es2022",
"module": "es2022",
"moduleResolution": "bundler",
"lib": ["es2022"],
"types": ["@cloudflare/workers-types", "@types/node"],
"strict": true,
"noUncheckedIndexedAccess": true,
"noEmit": true,
"skipLibCheck": true
},
"include": ["src", "test", "image"]
}
@@ -0,0 +1,7 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
// A string tsconfig skips the lookup, which otherwise climbs to the desktop app's tsconfig.
oxc: { tsconfig: false },
test: { include: ['test/**/*.test.ts', 'image/**/*.test.ts'] }
})
+28
View File
@@ -0,0 +1,28 @@
{
"$schema": "node_modules/wrangler/config-schema.json",
// Local `wrangler dev` only. No routes and workers.dev is off, so a deploy from this file
// publishes nothing reachable. Deployed environments live in the private deploy config.
// Secrets (stdin only, never here): VERCEL_TOKEN (Orca's team, Sandbox scope) and
// CONTROL_PLANE_TOKEN (shared with the billing Worker). Local dev: an untracked .dev.vars.
"name": "orca-sandbox-control-local",
"main": "src/index.ts",
"compatibility_date": "2026-11-01",
"workers_dev": false,
"d1_databases": [
{
"binding": "SANDBOX_DB",
"database_name": "orca-sandbox-control-local",
"database_id": "00000000-0000-0000-0000-000000000000",
"migrations_dir": "migrations"
}
],
// Usage settlement, lease/deadline enforcement and stuck-row reconciliation.
"triggers": { "crons": ["*/5 * * * *"] },
"vars": {
"SANDBOX_ENV": "local",
"SANDBOX_JWT_AUDIENCE": "orca-sandbox",
"VERCEL_REGION": "iad1",
// Local dev never calls billing; deployed configs set BILLING_URL and drop this.
"ENTITLEMENTS_MODE": "stub"
}
}
+28
View File
@@ -348,6 +348,34 @@ importers:
specifier: ^5.0.5
version: 5.0.5(@types/node@24.19.2)(vite@8.3.4(@types/node@24.19.2)(esbuild@0.28.2)(tsx@4.23.15))
apps/sandbox-control:
dependencies:
jose:
specifier: ^6.2.13
version: 6.2.13
zod:
specifier: ^4.6.7
version: 4.6.7
devDependencies:
'@cloudflare/workers-types':
specifier: ^5.20261103.1
version: 5.20261104.1
'@types/node':
specifier: ^24.19.2
version: 24.19.2
tsx:
specifier: ^4.23.15
version: 4.23.15
typescript:
specifier: ^5.9.3
version: 5.9.3
vitest:
specifier: ^5.0.5
version: 5.0.5(@types/node@24.19.2)(vite@8.3.4(@types/node@24.19.2)(esbuild@0.28.2)(tsx@4.23.15))
wrangler:
specifier: ^4.157.0
version: 4.157.0(@cloudflare/workers-types@5.20261104.1)(@types/node@24.19.2)
packages/cloud-database:
dependencies:
'@orca-cloud/postgres-schema':
@@ -62,6 +62,15 @@ const NON_UNIT_TEST_RUNNERS = [
sources: ['cloud/apps/relay-edge/vitest.config.ts'],
mirrors: ["include: ['test/**/*.test.ts']"]
},
{
name: 'cloud sandbox-control',
include: [
'cloud/apps/sandbox-control/test/**/*.test.ts',
'cloud/apps/sandbox-control/image/**/*.test.ts'
],
sources: ['cloud/apps/sandbox-control/vitest.config.ts'],
mirrors: ["include: ['test/**/*.test.ts', 'image/**/*.test.ts']"]
},
...[
'cloud/apps/relay-fence-broker',
'cloud/packages/cloud-database',