import { createClient } from "rivetkit/client"; import type { AgentType, AddRepoInput, AppConfig, FoundryAppSnapshot, FoundryBillingPlanId, CreateTaskInput, AppEvent, SessionEvent, SandboxProcessesEvent, TaskRecord, TaskSummary, TaskWorkbenchChangeModelInput, TaskWorkbenchCreateTaskInput, TaskWorkbenchCreateTaskResponse, TaskWorkbenchDiffInput, TaskWorkbenchRenameInput, TaskWorkbenchRenameSessionInput, TaskWorkbenchSelectInput, TaskWorkbenchSetSessionUnreadInput, TaskWorkbenchSendMessageInput, TaskWorkbenchSnapshot, TaskWorkbenchTabInput, TaskWorkbenchUpdateDraftInput, TaskEvent, WorkbenchTaskDetail, WorkbenchTaskSummary, WorkbenchSessionDetail, WorkspaceEvent, WorkspaceSummarySnapshot, HistoryEvent, HistoryQueryInput, ProviderId, RepoOverview, RepoStackActionInput, RepoStackActionResult, RepoRecord, StarSandboxAgentRepoInput, StarSandboxAgentRepoResult, SwitchResult, UpdateFoundryOrganizationProfileInput, } from "@sandbox-agent/foundry-shared"; import type { ProcessCreateRequest, ProcessInfo, ProcessLogFollowQuery, ProcessLogsResponse, ProcessSignalQuery } from "sandbox-agent"; import { createMockBackendClient } from "./mock/backend-client.js"; import { taskKey, taskSandboxKey, workspaceKey } from "./keys.js"; export type TaskAction = "push" | "sync" | "merge" | "archive" | "kill"; export interface SandboxSessionRecord { id: string; agent: string; agentSessionId: string; lastConnectionId: string; createdAt: number; destroyedAt?: number; status?: "pending_provision" | "pending_session_create" | "ready" | "running" | "idle" | "error"; } export interface SandboxSessionEventRecord { id: string; eventIndex: number; sessionId: string; createdAt: number; connectionId: string; sender: "client" | "agent"; payload: unknown; } export type SandboxProcessRecord = ProcessInfo; export interface ActorConn { on(event: string, listener: (payload: any) => void): () => void; onError(listener: (error: unknown) => void): () => void; dispose(): Promise; } interface WorkspaceHandle { connect(): ActorConn; addRepo(input: AddRepoInput): Promise; listRepos(input: { workspaceId: string }): Promise; createTask(input: CreateTaskInput): Promise; listTasks(input: { workspaceId: string; repoId?: string }): Promise; getRepoOverview(input: { workspaceId: string; repoId: string }): Promise; runRepoStackAction(input: RepoStackActionInput): Promise; history(input: HistoryQueryInput): Promise; switchTask(taskId: string): Promise; getTask(input: { workspaceId: string; taskId: string }): Promise; attachTask(input: { workspaceId: string; taskId: string; reason?: string }): Promise<{ target: string; sessionId: string | null }>; pushTask(input: { workspaceId: string; taskId: string; reason?: string }): Promise; syncTask(input: { workspaceId: string; taskId: string; reason?: string }): Promise; mergeTask(input: { workspaceId: string; taskId: string; reason?: string }): Promise; archiveTask(input: { workspaceId: string; taskId: string; reason?: string }): Promise; killTask(input: { workspaceId: string; taskId: string; reason?: string }): Promise; useWorkspace(input: { workspaceId: string }): Promise<{ workspaceId: string }>; starSandboxAgentRepo(input: StarSandboxAgentRepoInput): Promise; getWorkspaceSummary(input: { workspaceId: string }): Promise; applyTaskSummaryUpdate(input: { taskSummary: WorkbenchTaskSummary }): Promise; removeTaskSummary(input: { taskId: string }): Promise; reconcileWorkbenchState(input: { workspaceId: string }): Promise; createWorkbenchTask(input: TaskWorkbenchCreateTaskInput): Promise; markWorkbenchUnread(input: TaskWorkbenchSelectInput): Promise; renameWorkbenchTask(input: TaskWorkbenchRenameInput): Promise; renameWorkbenchBranch(input: TaskWorkbenchRenameInput): Promise; createWorkbenchSession(input: TaskWorkbenchSelectInput & { model?: string }): Promise<{ tabId: string }>; renameWorkbenchSession(input: TaskWorkbenchRenameSessionInput): Promise; setWorkbenchSessionUnread(input: TaskWorkbenchSetSessionUnreadInput): Promise; updateWorkbenchDraft(input: TaskWorkbenchUpdateDraftInput): Promise; changeWorkbenchModel(input: TaskWorkbenchChangeModelInput): Promise; sendWorkbenchMessage(input: TaskWorkbenchSendMessageInput): Promise; stopWorkbenchSession(input: TaskWorkbenchTabInput): Promise; closeWorkbenchSession(input: TaskWorkbenchTabInput): Promise; publishWorkbenchPr(input: TaskWorkbenchSelectInput): Promise; revertWorkbenchFile(input: TaskWorkbenchDiffInput): Promise; reloadGithubOrganization(): Promise; reloadGithubPullRequests(): Promise; reloadGithubRepository(input: { repoId: string }): Promise; reloadGithubPullRequest(input: { repoId: string; prNumber: number }): Promise; } interface AppWorkspaceHandle { connect(): ActorConn; getAppSnapshot(input: { sessionId: string }): Promise; skipAppStarterRepo(input: { sessionId: string }): Promise; starAppStarterRepo(input: { sessionId: string; organizationId: string }): Promise; selectAppOrganization(input: { sessionId: string; organizationId: string }): Promise; updateAppOrganizationProfile(input: UpdateFoundryOrganizationProfileInput & { sessionId: string }): Promise; triggerAppRepoImport(input: { sessionId: string; organizationId: string }): Promise; beginAppGithubInstall(input: { sessionId: string; organizationId: string }): Promise<{ url: string }>; createAppCheckoutSession(input: { sessionId: string; organizationId: string; planId: FoundryBillingPlanId }): Promise<{ url: string }>; createAppBillingPortalSession(input: { sessionId: string; organizationId: string }): Promise<{ url: string }>; cancelAppScheduledRenewal(input: { sessionId: string; organizationId: string }): Promise; resumeAppSubscription(input: { sessionId: string; organizationId: string }): Promise; recordAppSeatUsage(input: { sessionId: string; workspaceId: string }): Promise; } interface TaskHandle { getTaskSummary(): Promise; getTaskDetail(): Promise; getSessionDetail(input: { sessionId: string }): Promise; connect(): ActorConn; } interface TaskSandboxHandle { connect(): ActorConn; createSession(input: { id?: string; agent: string; model?: string; sessionInit?: { cwd?: string; }; }): Promise<{ id: string }>; listSessions(input?: { cursor?: string; limit?: number }): Promise<{ items: SandboxSessionRecord[]; nextCursor?: string }>; getEvents(input: { sessionId: string; cursor?: string; limit?: number }): Promise<{ items: SandboxSessionEventRecord[]; nextCursor?: string }>; createProcess(input: ProcessCreateRequest): Promise; listProcesses(): Promise<{ processes: SandboxProcessRecord[] }>; getProcessLogs(processId: string, query?: ProcessLogFollowQuery): Promise; stopProcess(processId: string, query?: ProcessSignalQuery): Promise; killProcess(processId: string, query?: ProcessSignalQuery): Promise; deleteProcess(processId: string): Promise; rawSendSessionMethod(sessionId: string, method: string, params: Record): Promise; destroySession(sessionId: string): Promise; sandboxAgentConnection(): Promise<{ endpoint: string; token?: string }>; providerState(): Promise<{ providerId: ProviderId; sandboxId: string; state: string; at: number }>; } interface RivetClient { workspace: { getOrCreate(key?: string | string[], opts?: { createWithInput?: unknown }): WorkspaceHandle; }; task: { get(key?: string | string[]): TaskHandle; getOrCreate(key?: string | string[], opts?: { createWithInput?: unknown }): TaskHandle; }; taskSandbox: { get(key?: string | string[]): TaskSandboxHandle; getOrCreate(key?: string | string[], opts?: { createWithInput?: unknown }): TaskSandboxHandle; getForId(actorId: string): TaskSandboxHandle; }; } export interface BackendClientOptions { endpoint: string; defaultWorkspaceId?: string; mode?: "remote" | "mock"; } export interface BackendClient { getAppSnapshot(): Promise; connectWorkspace(workspaceId: string): Promise; connectTask(workspaceId: string, repoId: string, taskId: string): Promise; connectSandbox(workspaceId: string, providerId: ProviderId, sandboxId: string): Promise; subscribeApp(listener: () => void): () => void; signInWithGithub(): Promise; signOutApp(): Promise; skipAppStarterRepo(): Promise; starAppStarterRepo(organizationId: string): Promise; selectAppOrganization(organizationId: string): Promise; updateAppOrganizationProfile(input: UpdateFoundryOrganizationProfileInput): Promise; triggerAppRepoImport(organizationId: string): Promise; reconnectAppGithub(organizationId: string): Promise; completeAppHostedCheckout(organizationId: string, planId: FoundryBillingPlanId): Promise; openAppBillingPortal(organizationId: string): Promise; cancelAppScheduledRenewal(organizationId: string): Promise; resumeAppSubscription(organizationId: string): Promise; recordAppSeatUsage(workspaceId: string): Promise; addRepo(workspaceId: string, remoteUrl: string): Promise; listRepos(workspaceId: string): Promise; createTask(input: CreateTaskInput): Promise; listTasks(workspaceId: string, repoId?: string): Promise; getRepoOverview(workspaceId: string, repoId: string): Promise; runRepoStackAction(input: RepoStackActionInput): Promise; getTask(workspaceId: string, taskId: string): Promise; listHistory(input: HistoryQueryInput): Promise; switchTask(workspaceId: string, taskId: string): Promise; attachTask(workspaceId: string, taskId: string): Promise<{ target: string; sessionId: string | null }>; runAction(workspaceId: string, taskId: string, action: TaskAction): Promise; createSandboxSession(input: { workspaceId: string; providerId: ProviderId; sandboxId: string; prompt: string; cwd?: string; agent?: AgentType | "opencode"; }): Promise<{ id: string; status: "running" | "idle" | "error" }>; listSandboxSessions( workspaceId: string, providerId: ProviderId, sandboxId: string, input?: { cursor?: string; limit?: number }, ): Promise<{ items: SandboxSessionRecord[]; nextCursor?: string }>; listSandboxSessionEvents( workspaceId: string, providerId: ProviderId, sandboxId: string, input: { sessionId: string; cursor?: string; limit?: number }, ): Promise<{ items: SandboxSessionEventRecord[]; nextCursor?: string }>; createSandboxProcess(input: { workspaceId: string; providerId: ProviderId; sandboxId: string; request: ProcessCreateRequest }): Promise; listSandboxProcesses(workspaceId: string, providerId: ProviderId, sandboxId: string): Promise<{ processes: SandboxProcessRecord[] }>; getSandboxProcessLogs( workspaceId: string, providerId: ProviderId, sandboxId: string, processId: string, query?: ProcessLogFollowQuery, ): Promise; stopSandboxProcess( workspaceId: string, providerId: ProviderId, sandboxId: string, processId: string, query?: ProcessSignalQuery, ): Promise; killSandboxProcess( workspaceId: string, providerId: ProviderId, sandboxId: string, processId: string, query?: ProcessSignalQuery, ): Promise; deleteSandboxProcess(workspaceId: string, providerId: ProviderId, sandboxId: string, processId: string): Promise; subscribeSandboxProcesses(workspaceId: string, providerId: ProviderId, sandboxId: string, listener: () => void): () => void; sendSandboxPrompt(input: { workspaceId: string; providerId: ProviderId; sandboxId: string; sessionId: string; prompt: string; notification?: boolean; }): Promise; sandboxSessionStatus( workspaceId: string, providerId: ProviderId, sandboxId: string, sessionId: string, ): Promise<{ id: string; status: "running" | "idle" | "error" }>; sandboxProviderState( workspaceId: string, providerId: ProviderId, sandboxId: string, ): Promise<{ providerId: ProviderId; sandboxId: string; state: string; at: number }>; getSandboxAgentConnection(workspaceId: string, providerId: ProviderId, sandboxId: string): Promise<{ endpoint: string; token?: string }>; getWorkspaceSummary(workspaceId: string): Promise; getTaskDetail(workspaceId: string, repoId: string, taskId: string): Promise; getSessionDetail(workspaceId: string, repoId: string, taskId: string, sessionId: string): Promise; getWorkbench(workspaceId: string): Promise; subscribeWorkbench(workspaceId: string, listener: () => void): () => void; createWorkbenchTask(workspaceId: string, input: TaskWorkbenchCreateTaskInput): Promise; markWorkbenchUnread(workspaceId: string, input: TaskWorkbenchSelectInput): Promise; renameWorkbenchTask(workspaceId: string, input: TaskWorkbenchRenameInput): Promise; renameWorkbenchBranch(workspaceId: string, input: TaskWorkbenchRenameInput): Promise; createWorkbenchSession(workspaceId: string, input: TaskWorkbenchSelectInput & { model?: string }): Promise<{ tabId: string }>; renameWorkbenchSession(workspaceId: string, input: TaskWorkbenchRenameSessionInput): Promise; setWorkbenchSessionUnread(workspaceId: string, input: TaskWorkbenchSetSessionUnreadInput): Promise; updateWorkbenchDraft(workspaceId: string, input: TaskWorkbenchUpdateDraftInput): Promise; changeWorkbenchModel(workspaceId: string, input: TaskWorkbenchChangeModelInput): Promise; sendWorkbenchMessage(workspaceId: string, input: TaskWorkbenchSendMessageInput): Promise; stopWorkbenchSession(workspaceId: string, input: TaskWorkbenchTabInput): Promise; closeWorkbenchSession(workspaceId: string, input: TaskWorkbenchTabInput): Promise; publishWorkbenchPr(workspaceId: string, input: TaskWorkbenchSelectInput): Promise; revertWorkbenchFile(workspaceId: string, input: TaskWorkbenchDiffInput): Promise; reloadGithubOrganization(workspaceId: string): Promise; reloadGithubPullRequests(workspaceId: string): Promise; reloadGithubRepository(workspaceId: string, repoId: string): Promise; reloadGithubPullRequest(workspaceId: string, repoId: string, prNumber: number): Promise; health(): Promise<{ ok: true }>; useWorkspace(workspaceId: string): Promise<{ workspaceId: string }>; starSandboxAgentRepo(workspaceId: string): Promise; } export function rivetEndpoint(config: AppConfig): string { return `http://${config.backend.host}:${config.backend.port}/v1/rivet`; } export function createBackendClientFromConfig(config: AppConfig): BackendClient { return createBackendClient({ endpoint: rivetEndpoint(config), defaultWorkspaceId: config.workspace.default, }); } function stripTrailingSlash(value: string): string { return value.replace(/\/$/, ""); } function normalizeLegacyBackendEndpoint(endpoint: string): string { const normalized = stripTrailingSlash(endpoint); if (normalized.endsWith("/api/rivet")) { return `${normalized.slice(0, -"/api/rivet".length)}/v1/rivet`; } return normalized; } function deriveBackendEndpoints(endpoint: string): { appEndpoint: string; rivetEndpoint: string } { const normalized = normalizeLegacyBackendEndpoint(endpoint); if (normalized.endsWith("/rivet")) { return { appEndpoint: normalized.slice(0, -"/rivet".length), rivetEndpoint: normalized, }; } return { appEndpoint: normalized, rivetEndpoint: `${normalized}/rivet`, }; } function signedOutAppSnapshot(): FoundryAppSnapshot { return { auth: { status: "signed_out", currentUserId: null }, activeOrganizationId: null, onboarding: { starterRepo: { repoFullName: "rivet-dev/sandbox-agent", repoUrl: "https://github.com/rivet-dev/sandbox-agent", status: "pending", starredAt: null, skippedAt: null, }, }, users: [], organizations: [], }; } export function createBackendClient(options: BackendClientOptions): BackendClient { if (options.mode === "mock") { return createMockBackendClient(options.defaultWorkspaceId); } const endpoints = deriveBackendEndpoints(options.endpoint); const rivetApiEndpoint = endpoints.rivetEndpoint; const appApiEndpoint = endpoints.appEndpoint; const client = createClient({ endpoint: rivetApiEndpoint }) as unknown as RivetClient; const workbenchSubscriptions = new Map< string, { listeners: Set<() => void>; disposeConnPromise: Promise<(() => Promise) | null> | null; } >(); const sandboxProcessSubscriptions = new Map< string, { listeners: Set<() => void>; disposeConnPromise: Promise<(() => Promise) | null> | null; } >(); const appSubscriptions = { listeners: new Set<() => void>(), disposeConnPromise: null as Promise<(() => Promise) | null> | null, }; const appRequest = async (path: string, init?: RequestInit): Promise => { const headers = new Headers(init?.headers); if (init?.body && !headers.has("Content-Type")) { headers.set("Content-Type", "application/json"); } const res = await fetch(`${appApiEndpoint}${path}`, { ...init, headers, credentials: "include", }); if (!res.ok) { throw new Error(`app request failed: ${res.status} ${res.statusText}`); } return (await res.json()) as T; }; const getSessionId = async (): Promise => { const res = await fetch(`${appApiEndpoint}/auth/get-session`, { credentials: "include", }); if (res.status === 401) { return null; } if (!res.ok) { throw new Error(`auth session request failed: ${res.status} ${res.statusText}`); } const data = (await res.json().catch(() => null)) as { session?: { id?: string | null } | null } | null; const sessionId = data?.session?.id; return typeof sessionId === "string" && sessionId.length > 0 ? sessionId : null; }; const workspace = async (workspaceId: string): Promise => client.workspace.getOrCreate(workspaceKey(workspaceId), { createWithInput: workspaceId, }); const appWorkspace = async (): Promise => client.workspace.getOrCreate(workspaceKey("app"), { createWithInput: "app", }) as unknown as AppWorkspaceHandle; const task = async (workspaceId: string, repoId: string, taskId: string): Promise => client.task.get(taskKey(workspaceId, repoId, taskId)); const sandboxByKey = async (workspaceId: string, _providerId: ProviderId, sandboxId: string): Promise => { return (client as any).taskSandbox.get(taskSandboxKey(workspaceId, sandboxId)); }; function isActorNotFoundError(error: unknown): boolean { const message = error instanceof Error ? error.message : String(error); return message.includes("Actor not found"); } const sandboxByActorIdFromTask = async (workspaceId: string, providerId: ProviderId, sandboxId: string): Promise => { const ws = await workspace(workspaceId); const rows = await ws.listTasks({ workspaceId }); const candidates = [...rows].sort((a, b) => b.updatedAt - a.updatedAt); for (const row of candidates) { try { const detail = await ws.getTask({ workspaceId, taskId: row.taskId }); if (detail.providerId !== providerId) { continue; } const sandbox = detail.sandboxes.find( (sb) => sb.sandboxId === sandboxId && sb.providerId === providerId && typeof (sb as any).sandboxActorId === "string" && (sb as any).sandboxActorId.length > 0, ) as { sandboxActorId?: string } | undefined; if (sandbox?.sandboxActorId) { return (client as any).taskSandbox.getForId(sandbox.sandboxActorId); } } catch (error) { const message = error instanceof Error ? error.message : String(error); if (!isActorNotFoundError(error) && !message.includes("Unknown task")) { throw error; } // Best effort fallback path; ignore missing task actors here. } } return null; }; const withSandboxHandle = async ( workspaceId: string, providerId: ProviderId, sandboxId: string, run: (handle: TaskSandboxHandle) => Promise, ): Promise => { const handle = await sandboxByKey(workspaceId, providerId, sandboxId); try { return await run(handle); } catch (error) { if (!isActorNotFoundError(error)) { throw error; } const fallback = await sandboxByActorIdFromTask(workspaceId, providerId, sandboxId); if (!fallback) { throw error; } return await run(fallback); } }; const connectWorkspace = async (workspaceId: string): Promise => { return (await workspace(workspaceId)).connect() as ActorConn; }; const connectTask = async (workspaceId: string, repoId: string, taskIdValue: string): Promise => { return (await task(workspaceId, repoId, taskIdValue)).connect() as ActorConn; }; const connectSandbox = async (workspaceId: string, providerId: ProviderId, sandboxId: string): Promise => { try { return (await sandboxByKey(workspaceId, providerId, sandboxId)).connect() as ActorConn; } catch (error) { if (!isActorNotFoundError(error)) { throw error; } const fallback = await sandboxByActorIdFromTask(workspaceId, providerId, sandboxId); if (!fallback) { throw error; } return fallback.connect() as ActorConn; } }; const getWorkbenchCompat = async (workspaceId: string): Promise => { const summary = await (await workspace(workspaceId)).getWorkspaceSummary({ workspaceId }); const tasks = ( await Promise.all( summary.taskSummaries.map(async (taskSummary) => { let detail; try { detail = await (await task(workspaceId, taskSummary.repoId, taskSummary.id)).getTaskDetail(); } catch (error) { if (isActorNotFoundError(error)) { return null; } throw error; } const sessionDetails = await Promise.all( detail.sessionsSummary.map(async (session) => { try { const full = await (await task(workspaceId, detail.repoId, detail.id)).getSessionDetail({ sessionId: session.id }); return [session.id, full] as const; } catch (error) { if (isActorNotFoundError(error)) { return null; } throw error; } }), ); const sessionDetailsById = new Map(sessionDetails.filter((entry): entry is readonly [string, WorkbenchSessionDetail] => entry !== null)); return { id: detail.id, repoId: detail.repoId, title: detail.title, status: detail.status, repoName: detail.repoName, updatedAtMs: detail.updatedAtMs, branch: detail.branch, pullRequest: detail.pullRequest, tabs: detail.sessionsSummary.map((session) => { const full = sessionDetailsById.get(session.id); return { id: session.id, sessionId: session.sessionId, sessionName: session.sessionName, agent: session.agent, model: session.model, status: session.status, thinkingSinceMs: session.thinkingSinceMs, unread: session.unread, created: session.created, draft: full?.draft ?? { text: "", attachments: [], updatedAtMs: null }, transcript: full?.transcript ?? [], }; }), fileChanges: detail.fileChanges, diffs: detail.diffs, fileTree: detail.fileTree, minutesUsed: detail.minutesUsed, }; }), ) ).filter((task): task is TaskWorkbenchSnapshot["tasks"][number] => task !== null); const projects = summary.repos .map((repo) => ({ id: repo.id, label: repo.label, updatedAtMs: tasks.filter((task) => task.repoId === repo.id).reduce((latest, task) => Math.max(latest, task.updatedAtMs), repo.latestActivityMs), tasks: tasks.filter((task) => task.repoId === repo.id).sort((left, right) => right.updatedAtMs - left.updatedAtMs), })) .filter((repo) => repo.tasks.length > 0); return { workspaceId, repos: summary.repos.map((repo) => ({ id: repo.id, label: repo.label })), projects, tasks: tasks.sort((left, right) => right.updatedAtMs - left.updatedAtMs), }; }; const subscribeWorkbench = (workspaceId: string, listener: () => void): (() => void) => { let entry = workbenchSubscriptions.get(workspaceId); if (!entry) { entry = { listeners: new Set(), disposeConnPromise: null, }; workbenchSubscriptions.set(workspaceId, entry); } entry.listeners.add(listener); if (!entry.disposeConnPromise) { entry.disposeConnPromise = (async () => { const handle = await workspace(workspaceId); const conn = (handle as any).connect(); const unsubscribeEvent = conn.on("workbenchUpdated", () => { const current = workbenchSubscriptions.get(workspaceId); if (!current) { return; } for (const currentListener of [...current.listeners]) { currentListener(); } }); const unsubscribeError = conn.onError(() => {}); return async () => { unsubscribeEvent(); unsubscribeError(); await conn.dispose(); }; })().catch(() => null); } return () => { const current = workbenchSubscriptions.get(workspaceId); if (!current) { return; } current.listeners.delete(listener); if (current.listeners.size > 0) { return; } workbenchSubscriptions.delete(workspaceId); void current.disposeConnPromise?.then(async (disposeConn) => { await disposeConn?.(); }); }; }; const sandboxProcessSubscriptionKey = (workspaceId: string, providerId: ProviderId, sandboxId: string): string => `${workspaceId}:${providerId}:${sandboxId}`; const subscribeSandboxProcesses = (workspaceId: string, providerId: ProviderId, sandboxId: string, listener: () => void): (() => void) => { const key = sandboxProcessSubscriptionKey(workspaceId, providerId, sandboxId); let entry = sandboxProcessSubscriptions.get(key); if (!entry) { entry = { listeners: new Set(), disposeConnPromise: null, }; sandboxProcessSubscriptions.set(key, entry); } entry.listeners.add(listener); if (!entry.disposeConnPromise) { entry.disposeConnPromise = (async () => { const conn = await connectSandbox(workspaceId, providerId, sandboxId); const unsubscribeEvent = conn.on("processesUpdated", () => { const current = sandboxProcessSubscriptions.get(key); if (!current) { return; } for (const currentListener of [...current.listeners]) { currentListener(); } }); const unsubscribeError = conn.onError(() => {}); return async () => { unsubscribeEvent(); unsubscribeError(); await conn.dispose(); }; })().catch(() => null); } return () => { const current = sandboxProcessSubscriptions.get(key); if (!current) { return; } current.listeners.delete(listener); if (current.listeners.size > 0) { return; } sandboxProcessSubscriptions.delete(key); void current.disposeConnPromise?.then(async (disposeConn) => { await disposeConn?.(); }); }; }; const subscribeApp = (listener: () => void): (() => void) => { appSubscriptions.listeners.add(listener); if (!appSubscriptions.disposeConnPromise) { appSubscriptions.disposeConnPromise = (async () => { const handle = await appWorkspace(); const conn = (handle as any).connect(); const unsubscribeEvent = conn.on("appUpdated", () => { for (const currentListener of [...appSubscriptions.listeners]) { currentListener(); } }); const unsubscribeError = conn.onError(() => {}); return async () => { unsubscribeEvent(); unsubscribeError(); await conn.dispose(); }; })().catch(() => null); } return () => { appSubscriptions.listeners.delete(listener); if (appSubscriptions.listeners.size > 0) { return; } void appSubscriptions.disposeConnPromise?.then(async (disposeConn) => { await disposeConn?.(); }); appSubscriptions.disposeConnPromise = null; }; }; return { async getAppSnapshot(): Promise { const sessionId = await getSessionId(); if (!sessionId) { return signedOutAppSnapshot(); } return await (await appWorkspace()).getAppSnapshot({ sessionId }); }, async connectWorkspace(workspaceId: string): Promise { return await connectWorkspace(workspaceId); }, async connectTask(workspaceId: string, repoId: string, taskIdValue: string): Promise { return await connectTask(workspaceId, repoId, taskIdValue); }, async connectSandbox(workspaceId: string, providerId: ProviderId, sandboxId: string): Promise { return await connectSandbox(workspaceId, providerId, sandboxId); }, subscribeApp(listener: () => void): () => void { return subscribeApp(listener); }, async signInWithGithub(): Promise { const callbackURL = typeof window !== "undefined" ? `${window.location.origin}/organizations` : `${appApiEndpoint.replace(/\/$/, "")}/organizations`; const response = await appRequest<{ url: string; redirect?: boolean }>("/auth/sign-in/social", { method: "POST", body: JSON.stringify({ provider: "github", callbackURL, disableRedirect: true, }), }); if (typeof window !== "undefined") { window.location.assign(response.url); } }, async signOutApp(): Promise { return await appRequest("/app/sign-out", { method: "POST" }); }, async skipAppStarterRepo(): Promise { const sessionId = await getSessionId(); if (!sessionId) { throw new Error("No active auth session"); } return await (await appWorkspace()).skipAppStarterRepo({ sessionId }); }, async starAppStarterRepo(organizationId: string): Promise { const sessionId = await getSessionId(); if (!sessionId) { throw new Error("No active auth session"); } return await (await appWorkspace()).starAppStarterRepo({ sessionId, organizationId }); }, async selectAppOrganization(organizationId: string): Promise { const sessionId = await getSessionId(); if (!sessionId) { throw new Error("No active auth session"); } return await (await appWorkspace()).selectAppOrganization({ sessionId, organizationId }); }, async updateAppOrganizationProfile(input: UpdateFoundryOrganizationProfileInput): Promise { const sessionId = await getSessionId(); if (!sessionId) { throw new Error("No active auth session"); } return await (await appWorkspace()).updateAppOrganizationProfile({ sessionId, organizationId: input.organizationId, displayName: input.displayName, slug: input.slug, primaryDomain: input.primaryDomain, }); }, async triggerAppRepoImport(organizationId: string): Promise { const sessionId = await getSessionId(); if (!sessionId) { throw new Error("No active auth session"); } return await (await appWorkspace()).triggerAppRepoImport({ sessionId, organizationId }); }, async reconnectAppGithub(organizationId: string): Promise { const sessionId = await getSessionId(); if (!sessionId) { throw new Error("No active auth session"); } const response = await (await appWorkspace()).beginAppGithubInstall({ sessionId, organizationId }); if (typeof window !== "undefined") { window.location.assign(response.url); } }, async completeAppHostedCheckout(organizationId: string, planId: FoundryBillingPlanId): Promise { const sessionId = await getSessionId(); if (!sessionId) { throw new Error("No active auth session"); } const response = await (await appWorkspace()).createAppCheckoutSession({ sessionId, organizationId, planId }); if (typeof window !== "undefined") { window.location.assign(response.url); } }, async openAppBillingPortal(organizationId: string): Promise { const sessionId = await getSessionId(); if (!sessionId) { throw new Error("No active auth session"); } const response = await (await appWorkspace()).createAppBillingPortalSession({ sessionId, organizationId }); if (typeof window !== "undefined") { window.location.assign(response.url); } }, async cancelAppScheduledRenewal(organizationId: string): Promise { const sessionId = await getSessionId(); if (!sessionId) { throw new Error("No active auth session"); } return await (await appWorkspace()).cancelAppScheduledRenewal({ sessionId, organizationId }); }, async resumeAppSubscription(organizationId: string): Promise { const sessionId = await getSessionId(); if (!sessionId) { throw new Error("No active auth session"); } return await (await appWorkspace()).resumeAppSubscription({ sessionId, organizationId }); }, async recordAppSeatUsage(workspaceId: string): Promise { const sessionId = await getSessionId(); if (!sessionId) { throw new Error("No active auth session"); } return await (await appWorkspace()).recordAppSeatUsage({ sessionId, workspaceId }); }, async addRepo(workspaceId: string, remoteUrl: string): Promise { return (await workspace(workspaceId)).addRepo({ workspaceId, remoteUrl }); }, async listRepos(workspaceId: string): Promise { return (await workspace(workspaceId)).listRepos({ workspaceId }); }, async createTask(input: CreateTaskInput): Promise { return (await workspace(input.workspaceId)).createTask(input); }, async starSandboxAgentRepo(workspaceId: string): Promise { return (await workspace(workspaceId)).starSandboxAgentRepo({ workspaceId }); }, async listTasks(workspaceId: string, repoId?: string): Promise { return (await workspace(workspaceId)).listTasks({ workspaceId, repoId }); }, async getRepoOverview(workspaceId: string, repoId: string): Promise { return (await workspace(workspaceId)).getRepoOverview({ workspaceId, repoId }); }, async runRepoStackAction(input: RepoStackActionInput): Promise { return (await workspace(input.workspaceId)).runRepoStackAction(input); }, async getTask(workspaceId: string, taskId: string): Promise { return (await workspace(workspaceId)).getTask({ workspaceId, taskId, }); }, async listHistory(input: HistoryQueryInput): Promise { return (await workspace(input.workspaceId)).history(input); }, async switchTask(workspaceId: string, taskId: string): Promise { return (await workspace(workspaceId)).switchTask(taskId); }, async attachTask(workspaceId: string, taskId: string): Promise<{ target: string; sessionId: string | null }> { return (await workspace(workspaceId)).attachTask({ workspaceId, taskId, reason: "cli.attach", }); }, async runAction(workspaceId: string, taskId: string, action: TaskAction): Promise { if (action === "push") { await (await workspace(workspaceId)).pushTask({ workspaceId, taskId, reason: "cli.push", }); return; } if (action === "sync") { await (await workspace(workspaceId)).syncTask({ workspaceId, taskId, reason: "cli.sync", }); return; } if (action === "merge") { await (await workspace(workspaceId)).mergeTask({ workspaceId, taskId, reason: "cli.merge", }); return; } if (action === "archive") { await (await workspace(workspaceId)).archiveTask({ workspaceId, taskId, reason: "cli.archive", }); return; } await (await workspace(workspaceId)).killTask({ workspaceId, taskId, reason: "cli.kill", }); }, async createSandboxSession(input: { workspaceId: string; providerId: ProviderId; sandboxId: string; prompt: string; cwd?: string; agent?: AgentType | "opencode"; }): Promise<{ id: string; status: "running" | "idle" | "error" }> { const created = await withSandboxHandle(input.workspaceId, input.providerId, input.sandboxId, async (handle) => handle.createSession({ agent: input.agent ?? "claude", sessionInit: { cwd: input.cwd, }, }), ); if (input.prompt.trim().length > 0) { await withSandboxHandle(input.workspaceId, input.providerId, input.sandboxId, async (handle) => handle.rawSendSessionMethod(created.id, "session/prompt", { prompt: [{ type: "text", text: input.prompt }], }), ); } return { id: created.id, status: "idle", }; }, async listSandboxSessions( workspaceId: string, providerId: ProviderId, sandboxId: string, input?: { cursor?: string; limit?: number }, ): Promise<{ items: SandboxSessionRecord[]; nextCursor?: string }> { return await withSandboxHandle(workspaceId, providerId, sandboxId, async (handle) => handle.listSessions(input ?? {})); }, async listSandboxSessionEvents( workspaceId: string, providerId: ProviderId, sandboxId: string, input: { sessionId: string; cursor?: string; limit?: number }, ): Promise<{ items: SandboxSessionEventRecord[]; nextCursor?: string }> { return await withSandboxHandle(workspaceId, providerId, sandboxId, async (handle) => handle.getEvents(input)); }, async createSandboxProcess(input: { workspaceId: string; providerId: ProviderId; sandboxId: string; request: ProcessCreateRequest; }): Promise { return await withSandboxHandle(input.workspaceId, input.providerId, input.sandboxId, async (handle) => handle.createProcess(input.request)); }, async listSandboxProcesses(workspaceId: string, providerId: ProviderId, sandboxId: string): Promise<{ processes: SandboxProcessRecord[] }> { return await withSandboxHandle(workspaceId, providerId, sandboxId, async (handle) => handle.listProcesses()); }, async getSandboxProcessLogs( workspaceId: string, providerId: ProviderId, sandboxId: string, processId: string, query?: ProcessLogFollowQuery, ): Promise { return await withSandboxHandle(workspaceId, providerId, sandboxId, async (handle) => handle.getProcessLogs(processId, query)); }, async stopSandboxProcess( workspaceId: string, providerId: ProviderId, sandboxId: string, processId: string, query?: ProcessSignalQuery, ): Promise { return await withSandboxHandle(workspaceId, providerId, sandboxId, async (handle) => handle.stopProcess(processId, query)); }, async killSandboxProcess( workspaceId: string, providerId: ProviderId, sandboxId: string, processId: string, query?: ProcessSignalQuery, ): Promise { return await withSandboxHandle(workspaceId, providerId, sandboxId, async (handle) => handle.killProcess(processId, query)); }, async deleteSandboxProcess(workspaceId: string, providerId: ProviderId, sandboxId: string, processId: string): Promise { await withSandboxHandle(workspaceId, providerId, sandboxId, async (handle) => handle.deleteProcess(processId)); }, subscribeSandboxProcesses(workspaceId: string, providerId: ProviderId, sandboxId: string, listener: () => void): () => void { return subscribeSandboxProcesses(workspaceId, providerId, sandboxId, listener); }, async sendSandboxPrompt(input: { workspaceId: string; providerId: ProviderId; sandboxId: string; sessionId: string; prompt: string; notification?: boolean; }): Promise { await withSandboxHandle(input.workspaceId, input.providerId, input.sandboxId, async (handle) => handle.rawSendSessionMethod(input.sessionId, "session/prompt", { prompt: [{ type: "text", text: input.prompt }], }), ); }, async sandboxSessionStatus( workspaceId: string, providerId: ProviderId, sandboxId: string, sessionId: string, ): Promise<{ id: string; status: "running" | "idle" | "error" }> { return { id: sessionId, status: "idle", }; }, async sandboxProviderState( workspaceId: string, providerId: ProviderId, sandboxId: string, ): Promise<{ providerId: ProviderId; sandboxId: string; state: string; at: number }> { return await withSandboxHandle(workspaceId, providerId, sandboxId, async (handle) => handle.providerState()); }, async getSandboxAgentConnection(workspaceId: string, providerId: ProviderId, sandboxId: string): Promise<{ endpoint: string; token?: string }> { return await withSandboxHandle(workspaceId, providerId, sandboxId, async (handle) => handle.sandboxAgentConnection()); }, async getWorkspaceSummary(workspaceId: string): Promise { return (await workspace(workspaceId)).getWorkspaceSummary({ workspaceId }); }, async getTaskDetail(workspaceId: string, repoId: string, taskIdValue: string): Promise { return (await task(workspaceId, repoId, taskIdValue)).getTaskDetail(); }, async getSessionDetail(workspaceId: string, repoId: string, taskIdValue: string, sessionId: string): Promise { return (await task(workspaceId, repoId, taskIdValue)).getSessionDetail({ sessionId }); }, async getWorkbench(workspaceId: string): Promise { return await getWorkbenchCompat(workspaceId); }, subscribeWorkbench(workspaceId: string, listener: () => void): () => void { return subscribeWorkbench(workspaceId, listener); }, async createWorkbenchTask(workspaceId: string, input: TaskWorkbenchCreateTaskInput): Promise { return (await workspace(workspaceId)).createWorkbenchTask(input); }, async markWorkbenchUnread(workspaceId: string, input: TaskWorkbenchSelectInput): Promise { await (await workspace(workspaceId)).markWorkbenchUnread(input); }, async renameWorkbenchTask(workspaceId: string, input: TaskWorkbenchRenameInput): Promise { await (await workspace(workspaceId)).renameWorkbenchTask(input); }, async renameWorkbenchBranch(workspaceId: string, input: TaskWorkbenchRenameInput): Promise { await (await workspace(workspaceId)).renameWorkbenchBranch(input); }, async createWorkbenchSession(workspaceId: string, input: TaskWorkbenchSelectInput & { model?: string }): Promise<{ tabId: string }> { return await (await workspace(workspaceId)).createWorkbenchSession(input); }, async renameWorkbenchSession(workspaceId: string, input: TaskWorkbenchRenameSessionInput): Promise { await (await workspace(workspaceId)).renameWorkbenchSession(input); }, async setWorkbenchSessionUnread(workspaceId: string, input: TaskWorkbenchSetSessionUnreadInput): Promise { await (await workspace(workspaceId)).setWorkbenchSessionUnread(input); }, async updateWorkbenchDraft(workspaceId: string, input: TaskWorkbenchUpdateDraftInput): Promise { await (await workspace(workspaceId)).updateWorkbenchDraft(input); }, async changeWorkbenchModel(workspaceId: string, input: TaskWorkbenchChangeModelInput): Promise { await (await workspace(workspaceId)).changeWorkbenchModel(input); }, async sendWorkbenchMessage(workspaceId: string, input: TaskWorkbenchSendMessageInput): Promise { await (await workspace(workspaceId)).sendWorkbenchMessage(input); }, async stopWorkbenchSession(workspaceId: string, input: TaskWorkbenchTabInput): Promise { await (await workspace(workspaceId)).stopWorkbenchSession(input); }, async closeWorkbenchSession(workspaceId: string, input: TaskWorkbenchTabInput): Promise { await (await workspace(workspaceId)).closeWorkbenchSession(input); }, async publishWorkbenchPr(workspaceId: string, input: TaskWorkbenchSelectInput): Promise { await (await workspace(workspaceId)).publishWorkbenchPr(input); }, async revertWorkbenchFile(workspaceId: string, input: TaskWorkbenchDiffInput): Promise { await (await workspace(workspaceId)).revertWorkbenchFile(input); }, async reloadGithubOrganization(workspaceId: string): Promise { await (await workspace(workspaceId)).reloadGithubOrganization(); }, async reloadGithubPullRequests(workspaceId: string): Promise { await (await workspace(workspaceId)).reloadGithubPullRequests(); }, async reloadGithubRepository(workspaceId: string, repoId: string): Promise { await (await workspace(workspaceId)).reloadGithubRepository({ repoId }); }, async reloadGithubPullRequest(workspaceId: string, repoId: string, prNumber: number): Promise { await (await workspace(workspaceId)).reloadGithubPullRequest({ repoId, prNumber }); }, async health(): Promise<{ ok: true }> { const workspaceId = options.defaultWorkspaceId; if (!workspaceId) { throw new Error("Backend client default workspace is required for health checks"); } await (await workspace(workspaceId)).useWorkspace({ workspaceId, }); return { ok: true }; }, async useWorkspace(workspaceId: string): Promise<{ workspaceId: string }> { return (await workspace(workspaceId)).useWorkspace({ workspaceId }); }, }; }