docs: document sandbox lifecycle registry

This commit is contained in:
Peter Steinberger
2026-06-04 00:57:14 -04:00
parent 9b4e2fa8a8
commit 5b36bbf83e
3 changed files with 31 additions and 0 deletions
+7
View File
@@ -13,6 +13,9 @@ import {
} from "./registry.js";
import { resolveSandboxAgentId } from "./shared.js";
/**
* CLI-facing sandbox management helpers for listing and removing runtime/browser containers.
*/
export type SandboxContainerInfo = SandboxRegistryEntry & {
running: boolean;
imageMatch: boolean;
@@ -32,6 +35,7 @@ function toBrowserDockerRuntimeEntry(entry: SandboxBrowserRegistryEntry): Sandbo
};
}
/** Lists registered sandbox containers with live backend status and config-label match state. */
export async function listSandboxContainers(): Promise<SandboxContainerInfo[]> {
const config = getRuntimeConfig();
const registry = await readRegistry();
@@ -65,6 +69,7 @@ export async function listSandboxContainers(): Promise<SandboxContainerInfo[]> {
return results;
}
/** Lists registered browser sandbox containers with live Docker status. */
export async function listSandboxBrowsers(): Promise<SandboxBrowserInfo[]> {
const config = getRuntimeConfig();
const registry = await readBrowserRegistry();
@@ -88,6 +93,7 @@ export async function listSandboxBrowsers(): Promise<SandboxBrowserInfo[]> {
return results;
}
/** Removes one sandbox container from its backend and registry. */
export async function removeSandboxContainer(containerName: string): Promise<void> {
const config = getRuntimeConfig();
const registry = await readRegistry();
@@ -103,6 +109,7 @@ export async function removeSandboxContainer(containerName: string): Promise<voi
await removeRegistryEntry(containerName);
}
/** Removes one browser sandbox container, registry entry, and any in-process bridge server. */
export async function removeSandboxBrowserContainer(containerName: string): Promise<void> {
const config = getRuntimeConfig();
const registry = await readBrowserRegistry();
+17
View File
@@ -12,6 +12,12 @@ import {
} from "./constants.js";
import { hashTextSha256 } from "./hash.js";
/**
* Persistent sandbox registry storage for runtime and browser containers.
*
* Entries are sharded by container-name hash to avoid a single hot JSON file while preserving
* migration support for older monolithic registry files.
*/
export type SandboxRegistryEntry = {
containerName: string;
backendId?: string;
@@ -122,6 +128,7 @@ async function readLegacyRegistryFile(registryPath: string): Promise<RegistryFil
}
}
/** Reads all registered sandbox runtime containers from the sharded registry. */
export async function readRegistry(): Promise<SandboxRegistry> {
const entries = await readShardedEntries<SandboxRegistryEntry>(SANDBOX_CONTAINERS_DIR);
return {
@@ -139,6 +146,8 @@ async function withEntryLock<T>(
fn: () => Promise<T>,
): Promise<T> {
const entryPath = shardedEntryFilePath(dir, containerName);
// Entry-level locks keep independent container updates concurrent while serializing writes for
// the same container hash path.
const lock = await acquireSessionWriteLock({
sessionFile: entryPath,
allowReentrant: false,
@@ -280,6 +289,7 @@ function legacyRegistryTargets(): LegacyRegistryTarget[] {
];
}
/** Inspects old monolithic registry files without mutating them. */
export async function inspectLegacySandboxRegistryFiles(): Promise<
LegacySandboxRegistryInspection[]
> {
@@ -307,6 +317,7 @@ export async function inspectLegacySandboxRegistryFiles(): Promise<
return inspections;
}
/** Migrates old monolithic registry files into sharded entry files when present. */
export async function migrateLegacySandboxRegistryFiles(): Promise<
LegacySandboxRegistryMigrationResult[]
> {
@@ -317,6 +328,7 @@ export async function migrateLegacySandboxRegistryFiles(): Promise<
return results;
}
/** Reads one registered sandbox runtime container by container name. */
export async function readRegistryEntry(
containerName: string,
): Promise<SandboxRegistryEntry | null> {
@@ -324,6 +336,7 @@ export async function readRegistryEntry(
return entry ? normalizeSandboxRegistryEntry(entry) : null;
}
/** Creates or updates one sandbox runtime registry entry, preserving immutable creation fields. */
export async function updateRegistry(entry: SandboxRegistryEntry) {
await withEntryLock(SANDBOX_CONTAINERS_DIR, entry.containerName, async () => {
const existing = await readShardedEntry<SandboxRegistryEntry>(
@@ -342,16 +355,19 @@ export async function updateRegistry(entry: SandboxRegistryEntry) {
});
}
/** Removes one sandbox runtime registry entry by container name. */
export async function removeRegistryEntry(containerName: string) {
await withEntryLock(SANDBOX_CONTAINERS_DIR, containerName, async () => {
await removeShardedEntry(SANDBOX_CONTAINERS_DIR, containerName);
});
}
/** Reads all registered browser sandbox containers from the sharded registry. */
export async function readBrowserRegistry(): Promise<SandboxBrowserRegistry> {
return { entries: await readShardedEntries<SandboxBrowserRegistryEntry>(SANDBOX_BROWSERS_DIR) };
}
/** Creates or updates one browser sandbox registry entry, preserving immutable creation fields. */
export async function updateBrowserRegistry(entry: SandboxBrowserRegistryEntry) {
await withEntryLock(SANDBOX_BROWSERS_DIR, entry.containerName, async () => {
const existing = await readShardedEntry<SandboxBrowserRegistryEntry>(
@@ -367,6 +383,7 @@ export async function updateBrowserRegistry(entry: SandboxBrowserRegistryEntry)
});
}
/** Removes one browser sandbox registry entry by container name. */
export async function removeBrowserRegistryEntry(containerName: string) {
await withEntryLock(SANDBOX_BROWSERS_DIR, containerName, async () => {
await removeShardedEntry(SANDBOX_BROWSERS_DIR, containerName);
+7
View File
@@ -5,6 +5,10 @@ import { resolveUserPath } from "../../utils.js";
import { resolveAgentIdFromSessionKey } from "../agent-scope.js";
import { hashTextSha256 } from "./hash.js";
/**
* Shared sandbox naming and scope helpers used by runtime, registry, and CLI management code.
*/
/** Converts an arbitrary session key into a bounded filesystem/container-safe slug. */
export function slugifySessionKey(value: string) {
const trimmed = value.trim() || "session";
const hash = hashTextSha256(trimmed).slice(0, 8);
@@ -15,12 +19,14 @@ export function slugifySessionKey(value: string) {
return `${base}-${hash}`;
}
/** Resolves the per-session sandbox workspace directory under the configured sandbox root. */
export function resolveSandboxWorkspaceDir(root: string, sessionKey: string) {
const resolvedRoot = resolveUserPath(root);
const slug = slugifySessionKey(sessionKey);
return path.join(resolvedRoot, slug);
}
/** Resolves the registry scope key for session-, agent-, or shared-scope sandbox lifetimes. */
export function resolveSandboxScopeKey(scope: "session" | "agent" | "shared", sessionKey: string) {
const trimmed = sessionKey.trim() || "main";
if (scope === "shared") {
@@ -33,6 +39,7 @@ export function resolveSandboxScopeKey(scope: "session" | "agent" | "shared", se
return `agent:${agentId}`;
}
/** Extracts the agent id represented by a sandbox scope key, when one exists. */
export function resolveSandboxAgentId(scopeKey: string): string | undefined {
const trimmed = scopeKey.trim();
if (!trimmed || trimmed === "shared") {