/**
* 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);
}