import { existsSync, mkdirSync } from "node:fs"; import { basename } from "node:path"; import type { Subprocess } from "bun"; import type { AgentConfig } from "./agent/parser.ts"; import { agentDir, agentSkillsDir, agentSoulPath } from "./agent/paths.ts"; import type { SupervisorMessage } from "./agent/protocol.ts"; import { SHUTDOWN_GRACE_MS } from "./agent/protocol.ts"; import { PROJECT_ROOT } from "./root.ts"; import { type ContainerAgent, createContainerAgent } from "./types.ts"; export const MOZART_IMAGE = "mozart-agent:latest"; export const MOZART_DEV_IMAGE = "mozart-agent-dev:latest"; export const DEV_MODE = process.env.MOZART_DEV === "1"; export function buildPodmanArgs(config: AgentConfig, apiKey: string, image?: string): string[] { const baseImage = image ?? (DEV_MODE ? MOZART_DEV_IMAGE : MOZART_IMAGE); const args = [ "--memory", config.memory ?? "256m", "--cpus", config.cpus ?? "0.5", "--tmpfs", "/tmp:rw,noexec,size=64m", "--network", "slirp4netns", ]; if (DEV_MODE) { args.push("--volume", `${PROJECT_ROOT}/src/agent:/home/src/agent:ro`); } for (const s of config.sanctums) { const mountName = basename(s.path); const mode = s.readonly ? "ro" : "rw"; args.push("--volume", `${s.path}:/mnt/${mountName}:${mode}`); } if (process.env.SEARXNG_INSTANCES) { args.push("--env", `SEARXNG_INSTANCES=${process.env.SEARXNG_INSTANCES}`); } args.push( "--env", `AGENT_CONFIG=${JSON.stringify(config)}`, "--env", `API_KEY=${apiKey}`, "--env", "AGENT_DATA_DIR=/data", baseImage, ); return args; } export function createContainer(config: AgentConfig, apiKey: string, image?: string): string { const name = `mozart-${config.name}`; killContainer(config.name); const hostDir = agentDir(config.name); mkdirSync(hostDir, { recursive: true }); const args = buildPodmanArgs(config, apiKey, image); const result = Bun.spawnSync(["podman", "create", "-i", "--name", name, ...args]); if (result.exitCode !== 0) { throw new Error(`Failed to create container '${name}': ${result.stderr.toString()}`); } return name; } export function copyIntoContainer(containerName: string, src: string, dest: string): void { const result = Bun.spawnSync(["podman", "cp", src, `${containerName}:${dest}`]); if (result.exitCode !== 0) { throw new Error(`Failed to copy '${src}' into '${containerName}': ${result.stderr.toString()}`); } } export function startContainer(containerName: string): Subprocess { return Bun.spawn(["podman", "start", "-ai", containerName], { stdin: "pipe", stdout: "pipe", stderr: "pipe", }); } /** * Full spawn flow: create container, copy initial data, start. * Used for first-time agent creation. Restart uses startContainer() directly. */ export function spawnContainer(config: AgentConfig, apiKey: string, image?: string): ContainerAgent { const name = createContainer(config, apiKey, image); const soulPath = agentSoulPath(config.name); if (existsSync(soulPath)) { copyIntoContainer(name, soulPath, "/data/agent.soul"); } const skillsPath = agentSkillsDir(config.name); if (existsSync(skillsPath)) { copyIntoContainer(name, `${skillsPath}/.`, "/data/skills"); } const proc = startContainer(name); return createContainerAgent(config.name, config, proc); } export function sendToContainer(container: ContainerAgent, msg: SupervisorMessage): void { const writer = container.process.stdin; if (!writer || typeof writer === "number") { throw new Error(`Container '${container.id}' stdin unavailable`); } const line = `${JSON.stringify(msg)}\n`; writer.write(line); writer.flush(); } export function getEnvFromInspect(data: Record, key: string): string | null { const envVars: string[] = (data as Record & { Config?: { Env?: string[] } })?.Config?.Env ?? []; const prefix = `${key}=`; for (const env of envVars) { if (env.startsWith(prefix)) return env.slice(prefix.length); } return null; } export function shutdownContainer(container: ContainerAgent): void { try { sendToContainer(container, { type: "shutdown" }); } catch { // stdin may already be closed } setTimeout(() => { try { container.process.kill(); } catch { // already dead } }, SHUTDOWN_GRACE_MS); container.state = "stopped"; container.events.close(); rejectPendingQueries(container, "Container shut down"); } export function rejectPendingQueries(container: ContainerAgent, reason: string): void { if (container.pendingQueries.size === 0) return; const err = new Error(reason); for (const [, pending] of container.pendingQueries) { pending.reject(err); } container.pendingQueries.clear(); } export function killContainer(names: string | string[]): void { const arr = Array.isArray(names) ? names : [names]; if (arr.length === 0) return; const ids = arr.map((n) => `mozart-${n}`); try { Bun.spawnSync(["podman", "kill", ...ids]); } catch {} try { Bun.spawnSync(["podman", "rm", "-f", ...ids]); } catch {} } export function stopContainers(names: string[]): void { if (names.length === 0) return; const ids = names.map((n) => `mozart-${n}`); try { Bun.spawnSync(["podman", "stop", "-t", "3", ...ids]); } catch {} } export function commitContainer(containerName: string, imageTag: string): void { const result = Bun.spawnSync(["podman", "commit", containerName, imageTag]); if (result.exitCode !== 0) { throw new Error(`Failed to commit container '${containerName}': ${result.stderr.toString()}`); } } export function saveImage(imageTag: string, outputPath: string): void { const result = Bun.spawnSync(["podman", "save", "-o", outputPath, imageTag]); if (result.exitCode !== 0) { throw new Error(`Failed to save image '${imageTag}': ${result.stderr.toString()}`); } } export function loadImage(inputPath: string): string { const result = Bun.spawnSync(["podman", "load", "-i", inputPath]); if (result.exitCode !== 0) { throw new Error(`Failed to load image from '${inputPath}': ${result.stderr.toString()}`); } const output = `${result.stdout.toString()}\n${result.stderr.toString()}`.trim(); for (const line of output.split("\n")) { const match = line.match(/Loaded image(?:\(s\))?:\s*(.+)/i); if (match) return match[1]!.trim(); } throw new Error(`Could not parse loaded image name from podman load output:\n${output}`); } export function removeImage(imageTag: string): void { try { Bun.spawnSync(["podman", "rmi", imageTag]); } catch {} } export async function podmanInspect( target: string, type: "container" | "image" = "container", ): Promise | null> { try { const cmd = type === "image" ? ["podman", "image", "inspect", "--format", "json", target] : ["podman", "inspect", "--format", "json", target]; const proc = Bun.spawn(cmd, { stdout: "pipe", stderr: "pipe" }); const code = await proc.exited; if (code !== 0) return null; const output = await new Response(proc.stdout).text(); const data = JSON.parse(output); return data[0] ?? null; } catch { return null; } } export async function isPodmanAvailable(): Promise { try { const proc = Bun.spawn(["podman", "--version"], { stdout: "pipe", stderr: "pipe" }); const code = await proc.exited; return code === 0; } catch { return false; } } export async function imageExists(name: string): Promise { try { const proc = Bun.spawn(["podman", "image", "exists", name], { stdout: "pipe", stderr: "pipe", }); const code = await proc.exited; return code === 0; } catch { return false; } }