import { api } from "@code/backend/convex/_generated/api"; import type { Id } from "@code/backend/convex/_generated/dataModel"; import { parseAgentEnv } from "@code/env/agent"; import { decodeIssueWorkspaceResult, makeIssueWorkspacePlan, } from "@code/primitives/project-workspace"; import { createSandboxSessionEnv } from "@flue/runtime"; import type { FileStat, SandboxApi, SandboxFactory } from "@flue/runtime"; import { ConvexHttpClient } from "convex/browser"; import { Effect } from "effect"; import * as v from "valibot"; const execResultSchema = v.object({ exitCode: v.number(), stderr: v.string(), stdout: v.string(), }); const statResultSchema = v.object({ isDirectory: v.boolean(), isSymbolicLink: v.boolean(), mtimeMs: v.number(), size: v.number(), }); interface ExecuteOptions { readonly signal?: AbortSignal; readonly timeoutMs?: number; } class AgentOsSandboxApi implements SandboxApi { readonly #actorKey: string[]; readonly #client: ConvexHttpClient; readonly #daemonId: string; constructor( client: ConvexHttpClient, daemonId: string, actorKey: readonly string[] ) { this.#client = client; this.#daemonId = daemonId; this.#actorKey = [...actorKey]; } async #execute( method: string, args: unknown[], options: ExecuteOptions = {} ): Promise { const commandId = await this.#client.mutation(api.daemonCommands.enqueue, { actorKey: this.#actorKey, args, daemonId: this.#daemonId, method, }); const timeoutMs = options.timeoutMs ?? 120_000; const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { if (options.signal?.aborted) { // oxlint-disable-next-line no-await-in-loop -- cancellation must settle before the poll exits. await this.#client.mutation(api.daemonCommands.cancel, { commandId }); throw options.signal.reason ?? new Error("AgentOS command aborted"); } // oxlint-disable-next-line no-await-in-loop -- each query observes the next durable command state. const command = await this.#client.query(api.daemonCommands.get, { commandId, }); if (!command) { throw new Error(`AgentOS command ${commandId} disappeared`); } if (command.status === "succeeded") { return command.result; } if (command.status === "failed" || command.status === "cancelled") { throw new Error( command.error ?? `AgentOS command ${method} ${command.status}` ); } const { promise, resolve } = Promise.withResolvers(); setTimeout(() => resolve(null), 200); // oxlint-disable-next-line no-await-in-loop -- polling intentionally waits between sequential reads. await promise; } await this.#client.mutation(api.daemonCommands.cancel, { commandId }); throw new Error(`AgentOS command ${method} timed out after ${timeoutMs}ms`); } async readFile(path: string): Promise { return new TextDecoder().decode(await this.readFileBuffer(path)); } async readFileBuffer(path: string): Promise { const result = await this.#execute("readFile", [path]); if (result instanceof ArrayBuffer) { return new Uint8Array(result); } if (ArrayBuffer.isView(result)) { return new Uint8Array( result.buffer.slice( result.byteOffset, result.byteOffset + result.byteLength ) ); } throw new TypeError( `AgentOS readFile returned non-binary data for ${path}` ); } async writeFile(path: string, content: string | Uint8Array): Promise { const serialized = typeof content === "string" ? content : Uint8Array.from(content).buffer; await this.#execute("writeFile", [path, serialized]); } async stat(path: string): Promise { const result = v.parse( statResultSchema, await this.#execute("stat", [path]) ); return { isDirectory: result.isDirectory, isFile: !result.isDirectory && !result.isSymbolicLink, isSymbolicLink: result.isSymbolicLink, mtime: new Date(result.mtimeMs), size: result.size, }; } async readdir(path: string): Promise { return v.parse(v.array(v.string()), await this.#execute("readdir", [path])); } async exists(path: string): Promise { return v.parse(v.boolean(), await this.#execute("exists", [path])); } async mkdir( path: string, options?: { readonly recursive?: boolean } ): Promise { if (!options?.recursive) { await this.#execute("mkdir", [path]); return; } const quotedPath = `'${path.replaceAll("'", `'"'"'`)}'`; const result = v.parse( execResultSchema, await this.#execute("exec", [`mkdir -p ${quotedPath}`]) ); if (result.exitCode !== 0) { throw new Error(result.stderr || `Could not create ${path}`); } } async rm( path: string, options?: { readonly force?: boolean; readonly recursive?: boolean } ): Promise { if (options?.force && !(await this.exists(path))) { return; } await this.#execute("deleteFile", [ path, { recursive: options?.recursive }, ]); } async exec( command: string, options?: { readonly cwd?: string; readonly env?: Record; readonly signal?: AbortSignal; readonly timeoutMs?: number; } ): Promise<{ exitCode: number; stderr: string; stdout: string }> { return v.parse( execResultSchema, await this.#execute( "exec", [ command, { ...(options?.cwd === undefined ? {} : { cwd: options.cwd }), ...(options?.env === undefined ? {} : { env: options.env }), }, ], { signal: options?.signal, timeoutMs: options?.timeoutMs } ) ); } } export const agentOs = ( runtimeEnv: Record ): SandboxFactory => { const env = parseAgentEnv(runtimeEnv); const client = new ConvexHttpClient(env.CONVEX_URL); return { async createSessionEnv({ id }) { // Flue controls this id from the issue document selected by the web app. const issueId = id as Id<"projectIssues">; const run = await client.mutation(api.agentWorkspace.ensureRun, { daemonId: env.DAEMON_ID, issueId, token: env.FLUE_DB_TOKEN, }); if (!run) { throw new Error(`Could not initialize AgentOS work run for ${id}`); } const sandbox = new AgentOsSandboxApi( client, env.DAEMON_ID, run.actorKey ); const context = await client.query(api.agentWorkspace.get, { issueId, token: env.FLUE_DB_TOKEN, }); const plan = await Effect.runPromise( makeIssueWorkspacePlan({ artifacts: context.artifacts.map((artifact) => ({ content: artifact.content, path: artifact.path, })), branchName: context.run?.branchName, checkoutPath: context.run?.checkoutPath, contextFiles: context.contextDocuments.map((document) => ({ content: document.content, kind: document.kind, path: document.path, })), defaultBranch: context.run?.baseBranch ?? context.source?.defaultBranch, issueBody: context.issue.body, issueId: String(context.issue._id), issueNumber: context.issue.number, issueTitle: context.issue.title, sourceUrl: context.run?.sourceUrl ?? context.source?.url, }) ); const [controlDirectory, checkoutOperation, ...stagingOperations] = plan.operations; if (!controlDirectory || controlDirectory._tag !== "Mkdir") { throw new Error( "Issue workspace plan must start with a control directory" ); } await sandbox.mkdir(controlDirectory.path, { recursive: true }); if (!checkoutOperation || checkoutOperation._tag !== "Exec") { throw new Error("Issue workspace plan must include a checkout command"); } const checkoutCommandResult = await sandbox.exec( checkoutOperation.command, { cwd: checkoutOperation.cwd, timeoutMs: checkoutOperation.timeoutMs, } ); await Promise.all( stagingOperations .filter((operation) => operation._tag === "Mkdir") .map((operation) => sandbox.mkdir(operation.path, { recursive: true }) ) ); await Promise.all( stagingOperations .filter((operation) => operation._tag === "WriteFile") .map((operation) => sandbox.writeFile(operation.path, operation.content) ) ); await Effect.runPromise( decodeIssueWorkspaceResult({ command: checkoutCommandResult, plan, }) ); return createSandboxSessionEnv(sandbox, "/workspace"); }, }; };