/** * model-proxy/contract.ts — single source of truth for the "subscription model * proxy" feature (BYO-subscription inside a remote sandbox). * * ## Why this exists * * When an OverSky agent runs inside a cloud sandbox (E2B / Fargate) the model * inference is driven by the bundled Claude Code Agent SDK. By default that SDK * authenticates with a PLATFORM credential. To let a user spend their OWN Claude * subscription WITHOUT placing a high-value personal OAuth token on multi-tenant * cloud infra, we do NOT ship the credential into the sandbox. Instead: * * 1. The in-sandbox SDK is pointed at a LOOPBACK proxy via `ANTHROPIC_BASE_URL` * and given a per-session, revocable RELAY token via `ANTHROPIC_AUTH_TOKEN`. * 2. The in-sandbox daemon's loopback server tunnels each model HTTP call up * the existing daemon↔backend Socket.IO channel as a `daemon:model-proxy:*` * stream. * 3. The OverSky backend relays that stream to the USER'S OWN home daemon * (matched by userId; authorized by the relay token). * 4. The home daemon — which physically holds `~/.claude` — injects the real * subscription bearer and re-issues the request to api.anthropic.com, * streaming the response back down the same path. * * The subscription credential therefore never leaves the user's machine; a * sandbox compromise yields only a revocable relay token. See * `docs/architecture/sandbox-subscription-model-proxy-2026-06-29.md`. * * This module holds the wire contract, the env-var names, and the upstream/OAuth * configuration so the sandbox side, the backend relay, and the home daemon all * agree on one shape. It has ZERO runtime dependencies on purpose. */ /** * Env vars injected into the in-sandbox Agent SDK session (`buildSdkEnv`) when a * session runs in BYO-subscription mode. We use `ANTHROPIC_AUTH_TOKEN` (Bearer) * rather than `ANTHROPIC_API_KEY` so the SDK speaks OAuth-bearer to our loopback * — the value is a per-session RELAY token, never the real subscription token. */ export declare const MODEL_PROXY_ENV: { /** Points the SDK at the in-sandbox loopback proxy, e.g. `http://127.0.0.1:8788`. */ readonly baseUrl: "ANTHROPIC_BASE_URL"; /** Per-session relay bearer the SDK sends; validated by the loopback server. */ readonly authToken: "ANTHROPIC_AUTH_TOKEN"; }; /** * Default Anthropic upstream. The home daemon re-issues ``. Kept as * an allowlist of one so a compromised relay cannot make the home daemon hit an * arbitrary host (SSRF hardening — see `egress.ts`). */ export declare const ANTHROPIC_UPSTREAM_BASE = "https://api.anthropic.com"; /** * Claude Code OAuth client used to refresh a `claude login` subscription token. * * These are the well-known public Claude Code OAuth client values. They are * configuration, not secrets, and are overridable per deployment. * * IMPORTANT (production gate): verify `tokenUrl` + `clientId` against the * Claude Code version bundled in the home daemon before enabling refresh in * production. A wrong value fails CLOSED (refresh throws), so the worst case is * "subscription proxy unavailable", never a silent wrong-account charge. */ export declare const CLAUDE_OAUTH_DEFAULTS: { readonly tokenUrl: "https://console.anthropic.com/v1/oauth/token"; readonly clientId: "9d1c250a-e61b-44d9-88ed-5944d1962f5e"; }; /** * `anthropic-beta` value that flags an OAuth-subscription (vs API-key) request. * Must be present on every outbound request the home daemon authorizes. */ export declare const ANTHROPIC_OAUTH_BETA = "oauth-2025-04-20"; /** Default refresh skew: refresh an access token this long before it expires. */ export declare const DEFAULT_REFRESH_SKEW_MS: number; /** Location of the `claude login` credential file, relative to the user's home. */ export declare const CLAUDE_CREDENTIALS_RELATIVE_PATH: readonly [".claude", ".credentials.json"]; /** Long-lived headless token env var (`claude setup-token`, sk-ant-oat01-…). */ export declare const CLAUDE_SETUP_TOKEN_ENV = "CLAUDE_CODE_OAUTH_TOKEN"; /** * A resolved Claude subscription credential. * - `oauth`: a refreshable `claude login` bundle (has `refreshToken`/`expiresAt`). * - `setup-token`: a long-lived `claude setup-token` bearer (no refresh). */ export interface SubscriptionCredential { kind: 'oauth' | 'setup-token'; accessToken: string; refreshToken?: string; /** Epoch ms when `accessToken` expires (oauth only). */ expiresAt?: number; /** Where the credential was loaded from, for diagnostics (never the value). */ source: 'env' | 'credentials-file'; } /** * Socket.IO event names for the bidirectional model-proxy stream. Modeled on the * existing `daemon:terminal:*` streaming pattern (connection.ts) so it rides the * same transport + Redis bridge without a parallel mechanism. * * The request body is carried INLINE on REQUEST_BEGIN (the Agent SDK sends a * complete request body; only the *response* is SSE-streamed), which keeps * backend routing stateless — one event per upstream call carries everything * needed to route it. CANCEL aborts an in-flight exchange whose response is * still streaming. * * Direction is relative to the HOME daemon: * request (sandbox → backend → home): REQUEST_BEGIN, CANCEL * response (home → backend → sandbox): RESPONSE_BEGIN, RESPONSE_CHUNK, RESPONSE_END */ export declare const MODEL_PROXY_EVENTS: { readonly REQUEST_BEGIN: "daemon:model-proxy:request"; readonly CANCEL: "daemon:model-proxy:cancel"; readonly RESPONSE_BEGIN: "daemon:model-proxy:response"; readonly RESPONSE_CHUNK: "daemon:model-proxy:response:chunk"; readonly RESPONSE_END: "daemon:model-proxy:response:end"; }; /** * Stateless backend routing breadcrumb. The backend broker stamps this onto the * request going DOWN to the home daemon; the home daemon echoes it on every * response event so the backend can route the response back to the originating * sandbox daemon without holding per-exchange state. `userId` is always * re-derived from the authenticated socket — never trusted from the wire — so a * daemon can only ever route within its own user. */ export interface ModelProxyRoute { userId: string; sandboxDaemonId: string; } /** Subscription providers the model proxy can serve. */ export declare const MODEL_PROXY_PROVIDERS: { readonly CLAUDE: "claude"; readonly CODEX: "codex"; }; export type ModelProxyProviderName = (typeof MODEL_PROXY_PROVIDERS)[keyof typeof MODEL_PROXY_PROVIDERS]; /** * Per-provider env contract injected into the in-sandbox session so the inner * CLI/SDK talks to the loopback with a relay token (never the real credential): * - Claude Code: `ANTHROPIC_BASE_URL` + `ANTHROPIC_AUTH_TOKEN` (OAuth bearer). * - Codex: `OPENAI_BASE_URL` + `OPENAI_API_KEY` (the loopback validates the * relay token; the home daemon swaps in the real ChatGPT/Codex auth). */ export declare const PROVIDER_PROXY_ENV: Record; /** Opens a model-proxy exchange; carries the full request body inline. */ export interface ModelProxyRequestBegin { /** Correlation id for this exchange (one per upstream HTTP call). */ exchangeId: string; /** Session the exchange belongs to — authorization + routing key. */ sessionId: string; /** Relay token the in-sandbox SDK presented; re-validated at the backend. */ relayToken: string; /** Which subscription to authorize with (default 'claude'). */ provider?: ModelProxyProviderName; method: string; /** Upstream path, e.g. `/v1/messages?beta=true`. */ path: string; /** Inbound headers (the home daemon strips auth + injects the real bearer). */ headers: Record; /** Full request body, base64. Absent for bodiless requests. */ bodyBase64?: string; /** Stamped by the backend when relaying down to the home daemon. */ route?: ModelProxyRoute; } /** Upstream response status + headers (sent once, before chunks). */ export interface ModelProxyResponseBegin { exchangeId: string; status: number; headers: Record; /** Echoed by the home daemon so the backend can route the response back. */ route?: ModelProxyRoute; } /** A chunk of the upstream response body (base64, e.g. an SSE frame). */ export interface ModelProxyResponseChunk { exchangeId: string; dataBase64: string; route?: ModelProxyRoute; } /** Terminates an exchange; `error` set iff the exchange failed. */ export interface ModelProxyResponseEnd { exchangeId: string; error?: { code: string; message: string; }; route?: ModelProxyRoute; } /** Cancels an in-flight exchange (client abort / session teardown). */ export interface ModelProxyCancel { exchangeId: string; sessionId: string; relayToken: string; reason?: string; route?: ModelProxyRoute; } /** Max inline request-body size (base64-decoded) accepted over the tunnel. */ export declare const MODEL_PROXY_MAX_BODY_BYTES: number; /** Stable error codes surfaced across the proxy. */ export declare const MODEL_PROXY_ERROR: { readonly SUBSCRIPTION_NOT_FOUND: "subscription_not_found"; readonly SUBSCRIPTION_EXPIRED: "subscription_expired"; readonly REFRESH_FAILED: "refresh_failed"; readonly UPSTREAM_BLOCKED: "upstream_blocked"; readonly UPSTREAM_FAILED: "upstream_failed"; readonly UNAUTHORIZED: "unauthorized"; readonly CANCELLED: "cancelled"; readonly REQUEST_TOO_LARGE: "request_too_large"; /** Deterministic local request/policy denial; callers must not retry it. */ readonly REQUEST_REJECTED: "request_rejected"; }; export type ModelProxyErrorCode = (typeof MODEL_PROXY_ERROR)[keyof typeof MODEL_PROXY_ERROR]; /** Typed error carrying a stable {@link ModelProxyErrorCode}. */ export declare class ModelProxyError extends Error { code: ModelProxyErrorCode; constructor(code: ModelProxyErrorCode, message: string); }