import { mkdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import type { AgentConfig } from "./agent/parser.ts"; import { agentDir, agentSoulPath, getMozartDir } from "./agent/paths.ts"; import { commitContainer, createContainer, getEnvFromInspect, loadImage, podmanInspect, removeImage, saveImage, startContainer, } from "./podman.ts"; import { type ContainerAgent, createContainerAgent } from "./types.ts"; function horcruxDir(): string { return join(getMozartDir(), "horcruxes"); } export interface SealResult { path: string; agentId: string; } /** * Seal an agent: commit its container state to an image, then save as a .horcrux file. * The container can be running or stopped. */ export function seal(agentId: string, outputPath?: string): SealResult { const containerName = `mozart-${agentId}`; const tempTag = `mozart-horcrux-${agentId}-${Date.now()}`; commitContainer(containerName, tempTag); try { mkdirSync(horcruxDir(), { recursive: true }); const dest = outputPath ?? join(horcruxDir(), `${agentId}.horcrux`); saveImage(tempTag, dest); return { path: dest, agentId }; } finally { removeImage(tempTag); } } export interface LoadedHorcrux { loadedImage: string; config: AgentConfig; originalApiKey: string | null; } /** * Load a horcrux image and extract agent config without creating a container. * Safe to call for inspection/validation — no side effects on running agents. */ export async function loadHorcrux(horcruxPath: string, newName?: string): Promise { const loadedImage = loadImage(horcruxPath); try { const imageData = await podmanInspect(loadedImage, "image"); if (!imageData) throw new Error(`Failed to inspect loaded image '${loadedImage}'`); const configJson = getEnvFromInspect(imageData, "AGENT_CONFIG"); if (!configJson) throw new Error("Horcrux image missing AGENT_CONFIG environment variable"); const originalApiKey = getEnvFromInspect(imageData, "API_KEY"); const config = JSON.parse(configJson) as AgentConfig; if (newName) config.name = newName; return { loadedImage, config, originalApiKey }; } catch (err) { removeImage(loadedImage); throw err; } } export interface UnsealResult { config: AgentConfig; containerName: string; container: ContainerAgent; } /** * Create and start a container from a previously loaded horcrux image. * Call loadHorcrux() first to get the image ref and config, * then validate (e.g. name conflicts) before calling this. */ export function startFromHorcrux(loaded: LoadedHorcrux, apiKey: string): UnsealResult { const { loadedImage, config, originalApiKey } = loaded; const effectiveApiKey = apiKey || originalApiKey || ""; const containerName = createContainer(config, effectiveApiKey, loadedImage); const hostDir = agentDir(config.name); mkdirSync(hostDir, { recursive: true }); writeFileSync( agentSoulPath(config.name), `# Unsealed from horcrux\nMODEL ${config.model}\nSOUL\n${config.identity}\n`, ); const proc = startContainer(containerName); const container = createContainerAgent(config.name, config, proc); return { config, containerName, container }; } /** Clean up a loaded horcrux image (e.g. after conflict detection). */ export function cleanupHorcruxImage(loadedImage: string): void { removeImage(loadedImage); }