openskynet / src /agents /sandbox /registry.ts
Darochin's picture
Mirror OpenSkyNet workspace snapshot from Git HEAD
fc93158 verified
import fs from "node:fs/promises";
import { writeJsonAtomic } from "../../infra/json-files.js";
import { acquireSessionWriteLock } from "../session-write-lock.js";
import { SANDBOX_BROWSER_REGISTRY_PATH, SANDBOX_REGISTRY_PATH } from "./constants.js";
export type SandboxRegistryEntry = {
containerName: string;
sessionKey: string;
createdAtMs: number;
lastUsedAtMs: number;
image: string;
configHash?: string;
};
type SandboxRegistry = {
entries: SandboxRegistryEntry[];
};
export type SandboxBrowserRegistryEntry = {
containerName: string;
sessionKey: string;
createdAtMs: number;
lastUsedAtMs: number;
image: string;
configHash?: string;
cdpPort: number;
noVncPort?: number;
};
type SandboxBrowserRegistry = {
entries: SandboxBrowserRegistryEntry[];
};
type RegistryReadMode = "strict" | "fallback";
type RegistryEntry = {
containerName: string;
};
type RegistryFile<T extends RegistryEntry> = {
entries: T[];
};
type UpsertEntry = RegistryEntry & {
createdAtMs: number;
image: string;
configHash?: string;
};
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object";
}
function isRegistryEntry(value: unknown): value is RegistryEntry {
return isRecord(value) && typeof value.containerName === "string";
}
function isRegistryFile<T extends RegistryEntry>(value: unknown): value is RegistryFile<T> {
if (!isRecord(value)) {
return false;
}
const maybeEntries = value.entries;
return Array.isArray(maybeEntries) && maybeEntries.every(isRegistryEntry);
}
async function withRegistryLock<T>(registryPath: string, fn: () => Promise<T>): Promise<T> {
const lock = await acquireSessionWriteLock({ sessionFile: registryPath, allowReentrant: false });
try {
return await fn();
} finally {
await lock.release();
}
}
async function readRegistryFromFile<T extends RegistryEntry>(
registryPath: string,
mode: RegistryReadMode,
): Promise<RegistryFile<T>> {
try {
const raw = await fs.readFile(registryPath, "utf-8");
const parsed = JSON.parse(raw) as unknown;
if (isRegistryFile<T>(parsed)) {
return parsed;
}
if (mode === "fallback") {
return { entries: [] };
}
throw new Error(`Invalid sandbox registry format: ${registryPath}`);
} catch (error) {
const code = (error as { code?: string } | null)?.code;
if (code === "ENOENT") {
return { entries: [] };
}
if (mode === "fallback") {
return { entries: [] };
}
if (error instanceof Error) {
throw error;
}
throw new Error(`Failed to read sandbox registry file: ${registryPath}`, { cause: error });
}
}
async function writeRegistryFile<T extends RegistryEntry>(
registryPath: string,
registry: RegistryFile<T>,
): Promise<void> {
await writeJsonAtomic(registryPath, registry, { trailingNewline: true });
}
export async function readRegistry(): Promise<SandboxRegistry> {
return await readRegistryFromFile<SandboxRegistryEntry>(SANDBOX_REGISTRY_PATH, "fallback");
}
function upsertEntry<T extends UpsertEntry>(entries: T[], entry: T): T[] {
const existing = entries.find((item) => item.containerName === entry.containerName);
const next = entries.filter((item) => item.containerName !== entry.containerName);
next.push({
...entry,
createdAtMs: existing?.createdAtMs ?? entry.createdAtMs,
image: existing?.image ?? entry.image,
configHash: entry.configHash ?? existing?.configHash,
});
return next;
}
function removeEntry<T extends RegistryEntry>(entries: T[], containerName: string): T[] {
return entries.filter((entry) => entry.containerName !== containerName);
}
async function withRegistryMutation<T extends RegistryEntry>(
registryPath: string,
mutate: (entries: T[]) => T[] | null,
): Promise<void> {
await withRegistryLock(registryPath, async () => {
const registry = await readRegistryFromFile<T>(registryPath, "strict");
const next = mutate(registry.entries);
if (next === null) {
return;
}
await writeRegistryFile(registryPath, { entries: next });
});
}
export async function updateRegistry(entry: SandboxRegistryEntry) {
await withRegistryMutation<SandboxRegistryEntry>(SANDBOX_REGISTRY_PATH, (entries) =>
upsertEntry(entries, entry),
);
}
export async function removeRegistryEntry(containerName: string) {
await withRegistryMutation<SandboxRegistryEntry>(SANDBOX_REGISTRY_PATH, (entries) => {
const next = removeEntry(entries, containerName);
if (next.length === entries.length) {
return null;
}
return next;
});
}
export async function readBrowserRegistry(): Promise<SandboxBrowserRegistry> {
return await readRegistryFromFile<SandboxBrowserRegistryEntry>(
SANDBOX_BROWSER_REGISTRY_PATH,
"fallback",
);
}
export async function updateBrowserRegistry(entry: SandboxBrowserRegistryEntry) {
await withRegistryMutation<SandboxBrowserRegistryEntry>(
SANDBOX_BROWSER_REGISTRY_PATH,
(entries) => upsertEntry(entries, entry),
);
}
export async function removeBrowserRegistryEntry(containerName: string) {
await withRegistryMutation<SandboxBrowserRegistryEntry>(
SANDBOX_BROWSER_REGISTRY_PATH,
(entries) => {
const next = removeEntry(entries, containerName);
if (next.length === entries.length) {
return null;
}
return next;
},
);
}