/* eslint-disable prefer-destructuring -- field captures before mutation are intentional */ /* eslint-disable max-classes-per-file -- runtime and handle form one service. */ import opencodePkg from "@agentos-software/opencode"; import { AgentOs } from "@rivet-dev/agentos-core"; import type { SessionStreamEntry } from "@rivet-dev/agentos-core"; import { Effect } from "effect"; import type { DockerSandboxOptions } from "./docker-sandbox"; import { DockerSandboxProvider } from "./docker-sandbox"; import { OrbConfigurationError, OrbSandboxError, OrbSessionError, orbActorKey, transitionOrbState, transitionRunState, } from "./domain"; import type { OrbIdentity, OrbId, OrbModelGatewayConfig, OrbProjectContext, OrbRunId, OrbSessionId, OrbState, OrbStateError, RunState, } from "./domain"; import type { OrbEvent } from "./events"; import { makeOrbEvent, normalizeSessionEvent, redactSecrets } from "./events"; import { prepareOpenCodeConfig } from "./opencode-config"; import { evaluatePermission } from "./permission-policy"; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export interface OrbEnv { readonly dockerImage?: string; readonly dockerWorkspace?: string; readonly rivetEndpoint?: string; } export interface OrbCreateInput { readonly context: OrbProjectContext; readonly docker: Omit; readonly gateway: OrbModelGatewayConfig; readonly identity: OrbIdentity; } // --------------------------------------------------------------------------- // OrbHandle // --------------------------------------------------------------------------- export class OrbHandle { readonly id: OrbId; readonly runId: OrbRunId; readonly identity: OrbIdentity; readonly actorKey: string; private vm: AgentOs | null = null; private dockerProvider: DockerSandboxProvider | null = null; private sessionId: string | undefined; private orbState: OrbState = "creating"; private runState: RunState = "queued"; private eventSequence = 0; private readonly eventListeners = new Set<(event: OrbEvent) => void>(); private unsubscribeSession: (() => void) | null = null; private context: OrbProjectContext | undefined; private gateway: OrbModelGatewayConfig | undefined; constructor(id: OrbId, runId: OrbRunId, identity: OrbIdentity) { this.id = id; this.runId = runId; this.identity = identity; this.actorKey = orbActorKey(identity); } get state(): OrbState { return this.orbState; } get currentSessionId(): string | undefined { return this.sessionId; } get docker(): DockerSandboxProvider | null { return this.dockerProvider; } get vmInstance(): AgentOs | null { return this.vm; } // ----------------------------------------------------------------------- // Event API // ----------------------------------------------------------------------- onEvent(listener: (event: OrbEvent) => void): () => void { this.eventListeners.add(listener); return () => { this.eventListeners.delete(listener); }; } emitEvent(event: OrbEvent): void { for (const listener of this.eventListeners) { listener(event); } } private nextSequence(): number { this.eventSequence += 1; return this.eventSequence; } // ----------------------------------------------------------------------- // State transitions // ----------------------------------------------------------------------- readonly setOrbState = (to: OrbState): Effect.Effect => transitionOrbState({ from: this.orbState, to }).pipe( Effect.tap((next) => Effect.sync(() => { this.orbState = next; }) ), Effect.asVoid ); readonly setRunState = (to: RunState): Effect.Effect => transitionRunState({ from: this.runState, to }).pipe( Effect.tap((next) => Effect.sync(() => { this.runState = next; }) ), Effect.asVoid ); // ----------------------------------------------------------------------- // Internal attachment // ----------------------------------------------------------------------- _attachVm(vm: AgentOs): void { this.vm = vm; this.unsubscribeSession = vm.onSessionEvent((entry: SessionStreamEntry) => { const normalized = normalizeSessionEvent(entry); if (normalized) { this.emitEvent({ ...normalized, sequence: this.nextSequence() }); } if ( typeof entry === "object" && entry !== null && "type" in entry && entry.type === "permission_request" ) { void this.handlePermissionRequest( entry as unknown as { requestId: string; options: { description?: string; id: string; title?: string; }[]; toolCall?: { kind?: string; name?: string; title?: string }; } ); } }); this.emitEvent( makeOrbEvent(this.nextSequence(), "vm_booted", { text: "VM booted" }) ); } _attachDocker(provider: DockerSandboxProvider): void { this.dockerProvider = provider; } _configure(input: { readonly context: OrbProjectContext; readonly gateway: OrbModelGatewayConfig; }): void { this.context = input.context; this.gateway = input.gateway; } // ----------------------------------------------------------------------- // Permission handler // ----------------------------------------------------------------------- private async handlePermissionRequest(request: { readonly requestId: string; readonly options: { description?: string; id: string; title?: string; }[]; readonly toolCall?: { kind?: string; name?: string; title?: string }; }): Promise { const vm = this.vm; const sessionId = this.sessionId; if (!vm || !sessionId) { return; } const decision = evaluatePermission(request); if (!decision.allow) { this.emitEvent( makeOrbEvent(this.nextSequence(), "permission_denied", { text: `Permission denied for: ${request.toolCall?.title ?? "unknown"}`, }) ); } if (decision.optionId) { await vm .respondPermission({ optionId: decision.optionId, requestId: request.requestId, sessionId, }) .catch( // eslint-disable-next-line no-empty-function -- best-effort permission response () => {} ); } } // ----------------------------------------------------------------------- // Repository preparation // ----------------------------------------------------------------------- readonly prepareRepository = Effect.fn("Orb.prepareRepository")( function* prepareRepository( this: OrbHandle, input: { readonly baseBranch?: string; readonly branchName?: string } ) { if (!this.dockerProvider) { return yield* Effect.fail( new OrbSandboxError({ message: "Docker sandbox is not attached", reason: "ContainerStart", }) ); } yield* this.setOrbState("prepared"); yield* this.setRunState("provisioning"); const client = yield* Effect.tryPromise({ catch: (cause) => new OrbSandboxError({ message: `Failed to start Docker sandbox: ${cause instanceof Error ? cause.message : String(cause)}`, reason: "ContainerStart", }), try: () => { const dp = this.dockerProvider; if (!dp) { throw new Error("Docker provider detached"); } return dp.start(); }, }); const repoUrl = this.context?.repositoryUrl; if (repoUrl) { const branch = input.branchName ?? "main"; const base = input.baseBranch ?? "main"; // eslint-disable-next-line no-use-before-define -- module-level helper const baseRef = `origin/${base}`; // eslint-disable-next-line no-use-before-define -- module-level helper const cloneCmd = `git clone --branch ${shellQuote(base)} --single-branch ${shellQuote(repoUrl)} /workspace/repository || git clone ${shellQuote(repoUrl)} /workspace/repository`; // eslint-disable-next-line no-use-before-define -- module-level helper const checkoutCmd = `cd /workspace/repository && git checkout -b ${shellQuote(branch)} ${shellQuote(baseRef)} 2>/dev/null || git checkout ${shellQuote(branch)} 2>/dev/null || true`; const result = yield* Effect.tryPromise({ catch: (cause) => new OrbSandboxError({ message: `Repository checkout failed: ${cause instanceof Error ? cause.message : String(cause)}`, reason: "CommandFailed", }), try: () => client.runProcess({ command: `${cloneCmd} && ${checkoutCmd}`, cwd: "/workspace", timeoutMs: 300_000, }), }); if (result.exitCode !== 0) { return yield* Effect.fail( new OrbSandboxError({ message: `Repository clone failed: ${result.stderr}`, reason: "CommandFailed", }) ); } } else { yield* Effect.tryPromise({ catch: (cause) => new OrbSandboxError({ message: `Failed to create workspace: ${cause instanceof Error ? cause.message : String(cause)}`, reason: "CommandFailed", }), try: () => client.runProcess({ command: "mkdir -p /workspace/repository", cwd: "/workspace", }), }); } const ctx = this.context; const vm = this.vm; if (ctx && vm) { yield* Effect.tryPromise({ catch: () => null, // eslint-disable-next-line no-empty-function -- best-effort context staging try: () => vm .mkdir("/mnt/sandbox/control", { recursive: true }) .then(() => vm.writeFile( "/mnt/sandbox/control/issue.md", `# ${ctx.issueTitle}\n\n${ctx.issueBody}\n` ) ), }); for (const file of ctx.contextFiles) { yield* Effect.tryPromise({ catch: () => null, try: () => vm.writeFile(`/mnt/sandbox/control/${file.path}`, file.content), }); } } yield* this.setRunState("preparing"); } ); // ----------------------------------------------------------------------- // OpenCode session // ----------------------------------------------------------------------- readonly openSession = Effect.fn("Orb.openSession")( function* openSession(this: OrbHandle) { if (!this.vm) { return yield* Effect.fail( new OrbSessionError({ message: "AgentOS VM is not attached", reason: "OpenSession", }) ); } if (!this.gateway || !this.context) { return yield* Effect.fail( new OrbConfigurationError({ message: "Orb is not configured with gateway and context", reason: "MissingGateway", }) ); } const config = yield* prepareOpenCodeConfig({ context: this.context, gateway: this.gateway, }); const vm = this.vm; yield* Effect.tryPromise({ catch: (cause) => new OrbSessionError({ message: `Failed to create config directory: ${cause instanceof Error ? cause.message : String(cause)}`, reason: "OpenSession", }), try: () => vm.mkdir("/root/.config/opencode", { recursive: true }), }); yield* Effect.tryPromise({ catch: (cause) => new OrbSessionError({ message: `Failed to write OpenCode config: ${cause instanceof Error ? cause.message : String(cause)}`, reason: "OpenSession", }), try: () => vm.writeFile(config.configPath, config.configJson), }); const agents = yield* Effect.tryPromise({ catch: (cause) => new OrbSessionError({ message: `Failed to list agents: ${cause instanceof Error ? cause.message : String(cause)}`, reason: "AgentNotInstalled", }), try: () => vm.listAgents(), }); const hasOpencode = agents.some( (a) => a.id === "opencode" && a.installed ); if (!hasOpencode) { return yield* Effect.fail( new OrbSessionError({ message: "OpenCode agent is not installed in the VM", reason: "AgentNotInstalled", }) ); } yield* Effect.tryPromise({ catch: (cause) => new OrbSessionError({ message: `Failed to open OpenCode session: ${cause instanceof Error ? cause.message : String(cause)}`, reason: "OpenSession", }), try: () => vm.openSession({ agent: "opencode", cwd: "/mnt/sandbox/repository", permissionPolicy: "ask", skipOsInstructions: false, }), }); const sessionInfo = yield* Effect.tryPromise({ catch: (cause) => new OrbSessionError({ message: `Failed to read session info: ${cause instanceof Error ? cause.message : String(cause)}`, reason: "OpenSession", }), try: () => vm.getSession(), }); this.sessionId = sessionInfo.sessionId; this.emitEvent( makeOrbEvent(this.nextSequence(), "session_opened", { text: `Session ${sessionInfo.sessionId} opened`, }) ); yield* this.setOrbState("running"); yield* this.setRunState("running"); return sessionInfo.sessionId as OrbSessionId; } ); // ----------------------------------------------------------------------- // Send task — raw prompt to model, redacted copy in events only // ----------------------------------------------------------------------- readonly sendTask = Effect.fn("Orb.sendTask")(function* sendTask( this: OrbHandle, prompt: string ) { if (!this.vm || !this.sessionId) { return yield* Effect.fail( new OrbSessionError({ message: "No active OpenCode session", reason: "SessionNotFound", }) ); } // eslint-disable-next-line prefer-destructuring -- captured before mutation const vm = this.vm; // eslint-disable-next-line prefer-destructuring -- captured before mutation const sessionId = this.sessionId; // Send raw prompt — no redaction of outgoing content. const result = yield* Effect.tryPromise({ catch: (cause) => new OrbSessionError({ message: `Prompt failed: ${cause instanceof Error ? cause.message : String(cause)}`, reason: "PromptFailed", }), try: () => vm.prompt({ content: [{ text: prompt, type: "text" }], sessionId, }), }); return result; }); // ----------------------------------------------------------------------- // Execute command via Docker // ----------------------------------------------------------------------- readonly executeCommand = Effect.fn("Orb.executeCommand")( function* executeCommand( this: OrbHandle, input: { readonly command: string; readonly cwd?: string; readonly env?: Readonly>; readonly timeoutMs?: number; } ) { if (!this.dockerProvider) { return yield* Effect.fail( new OrbSandboxError({ message: "Docker sandbox is not attached", reason: "ContainerStart", }) ); } const client = yield* Effect.tryPromise({ catch: (cause) => new OrbSandboxError({ message: `Failed to get sandbox client: ${cause instanceof Error ? cause.message : String(cause)}`, reason: "ContainerStart", }), try: () => { const dp = this.dockerProvider; if (!dp) { throw new Error("Docker provider detached"); } return dp.start(); }, }); const result = yield* Effect.tryPromise({ catch: (cause) => new OrbSandboxError({ message: `Command failed: ${cause instanceof Error ? cause.message : String(cause)}`, reason: "CommandFailed", }), try: () => client.runProcess({ command: input.command, ...(input.cwd === undefined ? {} : { cwd: input.cwd }), ...(input.env === undefined ? {} : { env: input.env }), ...(input.timeoutMs === undefined ? {} : { timeoutMs: input.timeoutMs }), }), }); // Emit redacted copy for logs/UI. this.emitEvent( makeOrbEvent(this.nextSequence(), "command_executed", { command: redactSecrets(input.command), exitCode: result.exitCode ?? undefined, }) ); return result; } ); // ----------------------------------------------------------------------- // Cancel / dispose // ----------------------------------------------------------------------- readonly cancel = Effect.fn("Orb.cancel")(function* cancel(this: OrbHandle) { // eslint-disable-next-line prefer-destructuring -- captured before mutation const vm = this.vm; // eslint-disable-next-line prefer-destructuring -- captured before mutation const sessionId = this.sessionId; if (vm && sessionId) { yield* Effect.tryPromise({ catch: () => null, try: () => vm.cancelPrompt({ sessionId }), }).pipe(Effect.ignore); } yield* this.setOrbState("cancelled"); yield* this.setRunState("cancelled"); this.emitEvent( makeOrbEvent(this.nextSequence(), "session_closed", { text: "Orb cancelled", }) ); }); readonly dispose = Effect.fn("Orb.dispose")( function* dispose(this: OrbHandle) { // eslint-disable-next-line prefer-destructuring -- captured before mutation const vm = this.vm; // eslint-disable-next-line prefer-destructuring -- captured before mutation const sessionId = this.sessionId; // eslint-disable-next-line prefer-destructuring -- captured before mutation const docker = this.dockerProvider; if (vm && sessionId) { yield* Effect.tryPromise({ catch: () => null, try: () => vm.cancelPrompt({ sessionId }), }).pipe(Effect.ignore); } // Dispose the VM before its sandbox so the mount unbinds cleanly. if (vm) { if (this.unsubscribeSession) { this.unsubscribeSession(); this.unsubscribeSession = null; } yield* Effect.tryPromise({ catch: () => null, try: () => vm.dispose(), }).pipe(Effect.ignore); } if (docker) { yield* Effect.tryPromise({ catch: () => null, try: () => docker.dispose(), }).pipe(Effect.ignore); } yield* this.setOrbState("disposed"); this.eventListeners.clear(); this.vm = null; this.dockerProvider = null; this.sessionId = undefined; } ); } // --------------------------------------------------------------------------- // OrbRuntime // --------------------------------------------------------------------------- export class OrbRuntime { private readonly orbs = new Map(); private readonly env: OrbEnv; constructor(env: OrbEnv = {}) { this.env = env; } readonly createOrb = Effect.fn("OrbRuntime.createOrb")(function* createOrb( this: OrbRuntime, input: OrbCreateInput ) { if (!input.gateway.apiKey.trim()) { return yield* Effect.fail( new OrbConfigurationError({ message: "Model gateway API key is required", reason: "MissingGateway", }) ); } const orbId = `orb-${input.identity.projectId}-${input.identity.runId}` as OrbId; const runId = `run-${input.identity.runId}` as OrbRunId; const handle = new OrbHandle(orbId, runId, input.identity); handle._configure({ context: input.context, gateway: input.gateway, }); // 1. Create and start Docker sandbox (the provider owns its container). const dockerOptions: DockerSandboxOptions = { ...input.docker, ...(this.env.dockerImage === undefined ? {} : { image: this.env.dockerImage }), }; const provider = yield* DockerSandboxProvider.create(dockerOptions); handle._attachDocker(provider); // 2-3. Start the sandbox, create the AgentOS VM, and link OpenCode. Any // failure here disposes the VM before its sandbox so neither leaks. let createdVm: AgentOs | null = null; const vm = yield* Effect.gen(function* vm() { const sandboxClient = yield* Effect.tryPromise({ catch: (cause) => new OrbSandboxError({ message: `Failed to start Docker sandbox: ${cause instanceof Error ? cause.message : String(cause)}`, reason: "ContainerStart", }), try: () => provider.start(), }); const created = yield* Effect.tryPromise({ catch: (cause) => new OrbSessionError({ message: `Failed to create AgentOS VM: ${cause instanceof Error ? cause.message : String(cause)}`, reason: "OpenSession", }), try: () => AgentOs.create({ sandbox: { client: sandboxClient, dispose: false, mountPath: "/mnt/sandbox", readOnly: false, sandboxRoot: "/workspace", }, software: [opencodePkg], }), }); createdVm = created; yield* Effect.tryPromise({ catch: (cause) => new OrbSessionError({ message: `Failed to link OpenCode: ${cause instanceof Error ? cause.message : String(cause)}`, reason: "AgentNotInstalled", }), try: () => created.linkSoftware({ path: opencodePkg.packagePath }), }); return created; }).pipe( Effect.tapError(() => Effect.tryPromise({ catch: () => null, // eslint-disable-next-line no-use-before-define -- module-level cleanup helper try: () => disposeVmBeforeSandbox(createdVm, provider), }).pipe(Effect.ignore) ) ); // 4. Attach VM to handle. handle._attachVm(vm); this.orbs.set(orbId, handle); return handle; }); getOrb(id: string): OrbHandle | undefined { return this.orbs.get(id); } listOrbs(): readonly OrbHandle[] { return [...this.orbs.values()]; } } // --------------------------------------------------------------------------- // Partial-failure cleanup — dispose the VM before its sandbox // --------------------------------------------------------------------------- const disposeVmBeforeSandbox = async ( vm: AgentOs | null, docker: DockerSandboxProvider ): Promise => { if (vm) { try { await vm.dispose(); } catch { // best-effort; container removal below is the hard guarantee } } await docker.dispose(); }; // --------------------------------------------------------------------------- // Shell quoting // --------------------------------------------------------------------------- // eslint-disable-next-line no-use-before-define -- module-level helper const shellQuote = (value: string): string => `'${value.replaceAll("'", `'"'"'`)}'`;