import { execFileSync } from "node:child_process"; import * as fsSync from "node:fs"; import * as fs from "node:fs/promises"; import { tmpdir } from "node:os"; import * as path from "node:path"; import * as posixPath from "node:path/posix"; import { fileURLToPath } from "node:url"; import "./native-client.js"; import { resolvePublishedSidecarBinary } from "./binary.js"; import { findCargoBinary, resolveCargoBinary } from "./cargo.js"; import type { JsRuntimeConfig } from "./generated/JsRuntimeConfig.js"; import type { PermissionsPolicy } from "./generated/PermissionsPolicy.js"; import type { VmLimitsConfig } from "./generated/VmLimitsConfig.js"; import type { VmUserConfig } from "./generated/VmUserConfig.js"; import { type AuthenticatedSession, type CreatedVm, type LocalCompatMount, NativeSidecarKernelProxy, type RootFilesystemEntry, SidecarProcess, type SidecarRegisteredHostCallbackDefinition, type SidecarRequestFrame, type SidecarResponsePayload, serializeMountConfigForSidecar, } from "./kernel-proxy.js"; export const AF_INET = 2; export const AF_UNIX = 1; export const SOCK_STREAM = 1; export const SOCK_DGRAM = 2; export const SIGTERM = 15; const S_IFREG = 0o100000; const S_IFDIR = 0o040000; const S_IFLNK = 0o120000; const MAX_SYMLINK_DEPTH = 40; const KERNEL_COMMAND_STUB = "#!/bin/sh\n# kernel command stub\n"; const NODE_RUNTIME_BOOTSTRAP_COMMANDS = ["node", "npm", "npx"] as const; const KERNEL_POSIX_BOOTSTRAP_DIRS = [ "/dev", "/proc", "/tmp", "/bin", "/lib", "/sbin", "/boot", "/etc", "/root", "/run", "/srv", "/sys", "/opt", "/opt/agentos", "/opt/agentos/bin", "/mnt", "/media", "/home", "/home/agentos", "/workspace", "/usr", "/usr/bin", "/usr/games", "/usr/include", "/usr/lib", "/usr/libexec", "/usr/man", "/usr/local", "/usr/local/bin", "/usr/sbin", "/usr/share", "/usr/share/man", "/var", "/var/cache", "/var/empty", "/var/lib", "/var/lock", "/var/log", "/var/run", "/var/spool", "/var/tmp", ] as const; const REPO_ROOT = fileURLToPath(new URL("../../..", import.meta.url)); const SIDECAR_BINARY = path.join( REPO_ROOT, "target/debug/agentos-native-sidecar", ); const SIDECAR_BUILD_INPUTS = [ path.join(REPO_ROOT, "Cargo.toml"), path.join(REPO_ROOT, "Cargo.lock"), path.join(REPO_ROOT, "crates/bridge"), path.join(REPO_ROOT, "crates/build-support"), path.join(REPO_ROOT, "crates/execution"), path.join(REPO_ROOT, "crates/kernel"), path.join(REPO_ROOT, "crates/native-sidecar"), path.join(REPO_ROOT, "crates/native-sidecar-core"), path.join(REPO_ROOT, "crates/sidecar-protocol"), path.join(REPO_ROOT, "crates/v8-runtime"), path.join(REPO_ROOT, "crates/vfs"), path.join(REPO_ROOT, "crates/vm-config"), path.join(REPO_ROOT, "packages/build-tools/bridge-src"), path.join(REPO_ROOT, "packages/build-tools/package.json"), path.join(REPO_ROOT, "packages/build-tools/scripts/build-v8-bridge.mjs"), path.join(REPO_ROOT, "packages/core/fixtures/base-filesystem.json"), path.join(REPO_ROOT, "packages/runtime-core/fixtures/base-filesystem.json"), path.join(REPO_ROOT, "pnpm-lock.yaml"), ] as const; export type KernelBootTimingPhase = | "filesystem_snapshot" | "sidecar_spawn" | "session_open" | "vm_create" | "vm_ready" | "vm_configure"; export interface KernelBootTiming { phase: KernelBootTimingPhase; durationMs: number; } let ensuredSidecarBinary: string | null = null; export type StdioChannel = "stdout" | "stderr"; export type TimingMitigation = "off" | "freeze"; export type PermissionMode = "allow" | "deny"; export type PermissionDecision = PermissionMode; export interface VirtualDirEntry { name: string; isDirectory: boolean; isSymbolicLink?: boolean; } export interface VirtualStat { mode: number; size: number; sizeExact?: bigint; blocks: number; dev: number; rdev: number; isDirectory: boolean; isSymbolicLink: boolean; atimeMs: number; mtimeMs: number; ctimeMs: number; birthtimeMs: number; ino: number; inoExact?: bigint; nlink: number; nlinkExact?: bigint; uid: number; gid: number; } export interface VirtualFileSystem { readFile(path: string): Promise; readTextFile(path: string): Promise; readDir(path: string): Promise; readDirWithTypes(path: string): Promise; writeFile(path: string, content: string | Uint8Array): Promise; createDir(path: string): Promise; mkdir(path: string, options?: { recursive?: boolean }): Promise; exists(path: string): Promise; stat(path: string): Promise; removeFile(path: string): Promise; removeDir(path: string): Promise; rename(oldPath: string, newPath: string): Promise; realpath(path: string): Promise; symlink(target: string, linkPath: string): Promise; readlink(path: string): Promise; lstat(path: string): Promise; link(oldPath: string, newPath: string): Promise; chmod(path: string, mode: number): Promise; chown( path: string, uid: number, gid: number, options?: { followSymlinks?: boolean }, ): Promise; utimes(path: string, atime: number, mtime: number): Promise; truncate(path: string, length: number): Promise; pread(path: string, offset: number, length: number): Promise; pwrite(path: string, offset: number, data: Uint8Array): Promise; } export interface NetworkAccessRequest { url?: string; host?: string; port?: number; protocol?: string; } export interface FsPermissionRule { mode: PermissionMode; operations?: string[]; paths?: string[]; } export interface PatternPermissionRule { mode: PermissionMode; operations?: string[]; patterns?: string[]; } export interface RulePermissions { default?: PermissionMode; rules: TRule[]; } export type FsPermissions = PermissionMode | RulePermissions; export type NetworkPermissions = | PermissionMode | RulePermissions; export type ChildProcessPermissions = | PermissionMode | RulePermissions; export type ProcessPermissions = | PermissionMode | RulePermissions; export type EnvPermissions = | PermissionMode | RulePermissions; export type BindingPermissions = | PermissionMode | RulePermissions; export interface ProcessInfo { pid: number; ppid: number; pgid: number; sid: number; driver: string; command: string; args: string[]; cwd: string; status: "running" | "exited"; exitCode: number | null; startTime: number; exitTime: number | null; } export interface ManagedProcess { pid: number; writeStdin(data: Uint8Array | string): void; closeStdin(): void; kill(signal?: number): void; wait(): Promise; readonly exitCode: number | null; } export interface ShellHandle { pid: number; write(data: Uint8Array | string): void; /** Ordered PTY output containing stdout and stderr exactly once. */ onData: ((data: Uint8Array) => void) | null; resize(cols: number, rows: number): void; kill(signal?: number): void; wait(): Promise; } export interface OpenShellOptions { command?: string; args?: string[]; env?: Record; cwd?: string; cols?: number; rows?: number; /** Optional stderr-only diagnostic tap; do not render it alongside `onData`. */ onStderr?: (data: Uint8Array) => void; } export interface ConnectTerminalOptions extends OpenShellOptions { onData?: (data: Uint8Array) => void; } export interface ExecOptions { env?: Record; cwd?: string; stdin?: string | Uint8Array; timeout?: number; onStdout?: (data: Uint8Array) => void; onStderr?: (data: Uint8Array) => void; captureStdio?: boolean; filePath?: string; cpuTimeLimitMs?: number; timingMitigation?: TimingMitigation; } export interface ExecResult { exitCode: number; stdout: string; stderr: string; } export interface RunResult { value?: T; code: number; errorMessage?: string; } export interface KernelSpawnOptions extends ExecOptions { stdio?: "pipe" | "inherit"; stdinFd?: number; stdoutFd?: number; stderrFd?: number; streamStdin?: boolean; } export type KernelExecOptions = ExecOptions; export type KernelExecResult = ExecResult; export type StatInfo = VirtualStat; export type DirEntry = VirtualDirEntry; export type StdioEvent = { channel: StdioChannel; message: string }; export type StdioHook = (event: StdioEvent) => void; export interface Permissions { fs?: FsPermissions; network?: NetworkPermissions; childProcess?: ChildProcessPermissions; process?: ProcessPermissions; env?: EnvPermissions; binding?: BindingPermissions; } /** A worked example shown alongside a registered binding. */ export interface BindingExample { /** What this example demonstrates. */ description: string; /** Example input matching the binding's input schema. */ input: unknown; } /** * A host-side binding that guest code can invoke as a shell command. The guest * runs the binding by name and the invocation round-trips back to the host JS * `handler`, whose return value is passed back to the guest. Bindings never run * inside the guest: they execute on the host, so they are the bridge for giving * sandboxed guest code controlled, named capabilities (the kind AI agents call * as tools). */ export interface BindingDefinition { /** Human-readable description of what the binding does. */ description: string; /** JSON Schema describing the binding's input. */ inputSchema: object; /** Abort the invocation after this many milliseconds. */ timeoutMs?: number; /** Worked examples shown alongside the binding. */ examples?: BindingExample[]; /** * Extra command names the guest can use to invoke this binding, in addition * to the key it is registered under. */ commandAliases?: string[]; /** * Host handler invoked when guest code runs the binding. Receives the parsed * input and returns a JSON-serializable result delivered back to the guest. */ handler: (input: unknown) => unknown | Promise; } export interface ResourceBudgets { maxOutputBytes?: number; maxBridgeCalls?: number; maxTimers?: number; maxChildProcesses?: number; maxHandles?: number; } export interface ProcessConfig { cwd?: string; env?: Record; argv?: string[]; stdinIsTTY?: boolean; stdoutIsTTY?: boolean; stderrIsTTY?: boolean; } export interface OSConfig { homedir?: string; tmpdir?: string; } export interface CommandExecutor { spawn( command: string, args: string[], options?: KernelSpawnOptions, ): ManagedProcess; } export interface NetworkAdapter { fetch( url: string, options?: { method?: string; headers?: Record; body?: unknown; }, ): Promise<{ ok: boolean; status: number; statusText: string; headers: Record; body: string; url: string; redirected: boolean; }>; dnsLookup(hostname: string): Promise<{ address?: string; family?: number; error?: string; code?: string; }>; httpRequest( url: string, options?: { method?: string; headers?: Record; body?: unknown; }, ): Promise<{ status: number; statusText: string; headers: Record; body: string; url: string; }>; } export interface SystemDriver { filesystem?: VirtualFileSystem; network?: NetworkAdapter; commandExecutor?: CommandExecutor; permissions?: Permissions; runtime: { process: ProcessConfig; os: OSConfig; }; } export interface RuntimeDriverOptions { system: SystemDriver; runtime: { process: ProcessConfig; os: OSConfig; }; memoryLimit?: number; cpuTimeLimitMs?: number; timingMitigation?: TimingMitigation; onStdio?: StdioHook; payloadLimits?: { base64TransferBytes?: number; jsonPayloadBytes?: number; }; resourceBudgets?: ResourceBudgets; } export interface NodeRuntimeDriver { exec(code: string, options?: ExecOptions): Promise; run(code: string, filePath?: string): Promise>; dispose(): void; terminate?(): Promise; readonly network?: Pick< NetworkAdapter, "fetch" | "dnsLookup" | "httpRequest" >; } export interface NodeRuntimeDriverFactory { createRuntimeDriver(options: RuntimeDriverOptions): NodeRuntimeDriver; } export interface KernelInterface { vfs: VirtualFileSystem; } export interface Kernel extends KernelInterface { mount(driver: KernelRuntimeDriver): Promise; dispose(): Promise; exec(command: string, options?: KernelExecOptions): Promise; spawn( command: string, args: string[], options?: KernelSpawnOptions, ): ManagedProcess; openShell(options?: OpenShellOptions): ShellHandle; connectTerminal(options?: ConnectTerminalOptions): Promise; mountFs( path: string, fs: VirtualFileSystem, options?: { readOnly?: boolean }, ): void; unmountFs(path: string): void; readFile(path: string): Promise; writeFile(path: string, content: string | Uint8Array): Promise; mkdir(path: string): Promise; readdir(path: string): Promise; stat(path: string): Promise; exists(path: string): Promise; removeFile(path: string): Promise; removeDir(path: string): Promise; rename(oldPath: string, newPath: string): Promise; pwrite(path: string, offset: number, data: Uint8Array): Promise; vmFetch(request: { port: number; method: string; path: string; headersJson: string; body?: string; }): Promise; registerBindings(bindings: Record): Promise; getResourceSnapshot(): Promise<{ runningProcesses: number; exitedProcesses: number; fdTables: number; openFds: number; pipes: number; pipeBufferedBytes: number; ptys: number; ptyBufferedInputBytes: number; ptyBufferedOutputBytes: number; sockets: number; socketListeners: number; socketConnections: number; socketBufferedBytes: number; socketDatagramQueueLen: number; queueSnapshots: Array<{ name: string; category: string; depth: number; highWater: number; capacity: number; fillPercent: number; }>; }>; readonly commands: ReadonlyMap; readonly processes: ReadonlyMap; readonly env: Record; readonly cwd: string; readonly socketTable: { hasHostNetworkAdapter(): boolean; findListener(_request: unknown): unknown | null; findListenerAsync(_request: unknown): Promise; findBoundUdp(_request: unknown): unknown | null; }; readonly processTable: { getSignalState(_pid: number): { handlers: Map }; }; readonly timerTable: Record; readonly zombieTimerCount: number; } export interface BindingTree { [key: string]: BindingFunction | BindingTree; } export type BindingFunction = (...args: unknown[]) => unknown; export interface ModuleAccessOptions { cwd?: string; } export interface NodeDriverOptions { filesystem?: VirtualFileSystem; networkAdapter?: NetworkAdapter; commandExecutor?: CommandExecutor; permissions?: Permissions; processConfig?: ProcessConfig; osConfig?: OSConfig; moduleAccess?: ModuleAccessOptions; } export interface DefaultNetworkAdapterOptions { loopbackExemptPorts?: number[]; } export interface NodeRuntimeOptions { systemDriver?: SystemDriver; runtimeDriverFactory?: NodeRuntimeDriverFactory; permissions?: Partial; memoryLimit?: number; moduleAccessPaths?: string[]; bindings?: BindingTree; loopbackExemptPorts?: number[]; moduleAccessCwd?: string; packageRoots?: Array<{ hostPath: string; vmPath: string }>; } export type NodeRuntimeDriverFactoryOptions = Record; export type NodeExecutionDriverOptions = RuntimeDriverOptions; export interface KernelRuntimeDriver { readonly kind: "node" | "wasmvm"; readonly name: string; readonly commands: string[]; readonly commandDirs?: string[]; init?(kernel: KernelInterface): Promise | void; tryResolve?(command: string): boolean; getGuestCommandPaths?(startIndex: number): ReadonlyMap; recordModuleExecution?(command: string): void; } export type DriverProcess = ManagedProcess; export type ProcessContext = Record; export class KernelError extends Error { readonly code: string; constructor(code: string, message: string) { super(message.startsWith(`${code}:`) ? message : `${code}: ${message}`); this.name = "KernelError"; this.code = code; } } function normalizePath(inputPath: string): string { if (!inputPath) return "/"; let normalized = inputPath.startsWith("/") ? inputPath : `/${inputPath}`; normalized = normalized.replace(/\/+/g, "/"); if (normalized.length > 1 && normalized.endsWith("/")) { normalized = normalized.slice(0, -1); } const parts = normalized.split("/"); const resolved: string[] = []; for (const part of parts) { if (part === "" || part === ".") continue; if (part === "..") { resolved.pop(); continue; } resolved.push(part); } return resolved.length === 0 ? "/" : `/${resolved.join("/")}`; } function dirnameVirtual(inputPath: string): string { const normalized = normalizePath(inputPath); if (normalized === "/") return "/"; const parts = normalized.split("/").filter(Boolean); return parts.length <= 1 ? "/" : `/${parts.slice(0, -1).join("/")}`; } interface FileEntry { type: "file"; data: Uint8Array; mode: number; uid: number; gid: number; nlink: number; ino: number; atimeMs: number; mtimeMs: number; ctimeMs: number; birthtimeMs: number; } interface DirectoryEntry { type: "dir"; mode: number; uid: number; gid: number; nlink: number; ino: number; atimeMs: number; mtimeMs: number; ctimeMs: number; birthtimeMs: number; } interface SymlinkEntry { type: "symlink"; target: string; mode: number; uid: number; gid: number; nlink: number; ino: number; atimeMs: number; mtimeMs: number; ctimeMs: number; birthtimeMs: number; } type MemoryEntry = FileEntry | DirectoryEntry | SymlinkEntry; let nextInode = 1; export class InMemoryFileSystem implements VirtualFileSystem { private readonly entries = new Map(); constructor() { this.entries.set("/", this.newDirectory()); } async readFile(targetPath: string): Promise { const entry = this.resolveEntry(targetPath); if (!entry || entry.type !== "file") { throw errnoError("ENOENT", `open '${targetPath}'`); } entry.atimeMs = Date.now(); return entry.data; } async readTextFile(targetPath: string): Promise { return new TextDecoder().decode(await this.readFile(targetPath)); } async readDir(targetPath: string): Promise { return (await this.readDirWithTypes(targetPath)).map((entry) => entry.name); } async readDirWithTypes(targetPath: string): Promise { const resolved = this.resolvePath(targetPath); const entry = this.entries.get(resolved); if (!entry || entry.type !== "dir") { throw errnoError("ENOENT", `scandir '${targetPath}'`); } const prefix = resolved === "/" ? "/" : `${resolved}/`; const output = new Map(); for (const [entryPath, candidate] of this.entries) { if (!entryPath.startsWith(prefix)) continue; const rest = entryPath.slice(prefix.length); if (!rest || rest.includes("/")) continue; output.set(rest, { name: rest, isDirectory: candidate.type === "dir", isSymbolicLink: candidate.type === "symlink", }); } return [...output.values()]; } async writeFile( targetPath: string, content: string | Uint8Array, ): Promise { const normalized = normalizePath(targetPath); await this.mkdir(dirnameVirtual(normalized), { recursive: true }); const data = typeof content === "string" ? new TextEncoder().encode(content) : content; const existing = this.entries.get(normalized); if (existing?.type === "file") { existing.data = data; existing.mtimeMs = Date.now(); existing.ctimeMs = Date.now(); return; } const now = Date.now(); this.entries.set(normalized, { type: "file", data, mode: S_IFREG | 0o644, uid: 0, gid: 0, nlink: 1, ino: nextInode++, atimeMs: now, mtimeMs: now, ctimeMs: now, birthtimeMs: now, }); } async createDir(targetPath: string): Promise { const normalized = normalizePath(targetPath); if (!this.entries.has(dirnameVirtual(normalized))) { throw errnoError("ENOENT", `mkdir '${targetPath}'`); } if (!this.entries.has(normalized)) { this.entries.set(normalized, this.newDirectory()); } } async mkdir( targetPath: string, options?: { recursive?: boolean }, ): Promise { const normalized = normalizePath(targetPath); if (options?.recursive === false) { return this.createDir(normalized); } let current = ""; for (const part of normalized.split("/").filter(Boolean)) { current += `/${part}`; if (!this.entries.has(current)) { this.entries.set(current, this.newDirectory()); } } } async exists(targetPath: string): Promise { try { return this.entries.has(this.resolvePath(targetPath)); } catch { return false; } } async stat(targetPath: string): Promise { const entry = this.resolveEntry(targetPath); if (!entry) throw errnoError("ENOENT", `stat '${targetPath}'`); return this.toStat(entry); } async removeFile(targetPath: string): Promise { const resolved = this.resolvePath(targetPath); const entry = this.entries.get(resolved); if (!entry || entry.type === "dir") { throw errnoError("ENOENT", `unlink '${targetPath}'`); } this.entries.delete(resolved); } async removeDir(targetPath: string): Promise { const resolved = this.resolvePath(targetPath); if (resolved === "/") { throw errnoError("EPERM", "operation not permitted"); } const entry = this.entries.get(resolved); if (!entry || entry.type !== "dir") { throw errnoError("ENOENT", `rmdir '${targetPath}'`); } const prefix = `${resolved}/`; for (const key of this.entries.keys()) { if (key.startsWith(prefix)) { throw errnoError("ENOTEMPTY", `directory not empty '${targetPath}'`); } } this.entries.delete(resolved); } async rename(oldPath: string, newPath: string): Promise { const oldResolved = this.resolvePath(oldPath); const newResolved = normalizePath(newPath); const entry = this.entries.get(oldResolved); if (!entry) throw errnoError("ENOENT", `rename '${oldPath}'`); if (!this.entries.has(dirnameVirtual(newResolved))) { throw errnoError("ENOENT", `rename '${newPath}'`); } if (entry.type !== "dir") { this.entries.set(newResolved, entry); this.entries.delete(oldResolved); return; } const prefix = `${oldResolved}/`; const moved: Array<[string, MemoryEntry]> = []; for (const candidate of this.entries) { if (candidate[0] === oldResolved || candidate[0].startsWith(prefix)) { moved.push(candidate); } } for (const [candidatePath] of moved) { this.entries.delete(candidatePath); } for (const [candidatePath, candidate] of moved) { const nextPath = candidatePath === oldResolved ? newResolved : `${newResolved}${candidatePath.slice(oldResolved.length)}`; this.entries.set(nextPath, candidate); } } async realpath(targetPath: string): Promise { return this.resolvePath(targetPath); } async symlink(target: string, linkPath: string): Promise { const normalized = normalizePath(linkPath); if (this.entries.has(normalized)) { throw errnoError("EEXIST", `symlink '${linkPath}'`); } const now = Date.now(); this.entries.set(normalized, { type: "symlink", target, mode: S_IFLNK | 0o777, uid: 0, gid: 0, nlink: 1, ino: nextInode++, atimeMs: now, mtimeMs: now, ctimeMs: now, birthtimeMs: now, }); } async readlink(targetPath: string): Promise { const normalized = normalizePath(targetPath); const entry = this.entries.get(normalized); if (!entry || entry.type !== "symlink") { throw errnoError("ENOENT", `readlink '${targetPath}'`); } return entry.target; } async lstat(targetPath: string): Promise { const entry = this.entries.get(normalizePath(targetPath)); if (!entry) throw errnoError("ENOENT", `lstat '${targetPath}'`); return this.toStat(entry); } async link(oldPath: string, newPath: string): Promise { const entry = this.resolveEntry(oldPath); if (!entry || entry.type !== "file") { throw errnoError("ENOENT", `link '${oldPath}'`); } const normalized = normalizePath(newPath); if (this.entries.has(normalized)) { throw errnoError("EEXIST", `link '${newPath}'`); } entry.nlink += 1; this.entries.set(normalized, entry); } async chmod(targetPath: string, mode: number): Promise { const entry = this.resolveEntry(targetPath); if (!entry) throw errnoError("ENOENT", `chmod '${targetPath}'`); const typeBits = mode & 0o170000; entry.mode = typeBits === 0 ? (entry.mode & 0o170000) | (mode & 0o7777) : mode; entry.ctimeMs = Date.now(); } async chown( targetPath: string, uid: number, gid: number, options?: { followSymlinks?: boolean }, ): Promise { const entry = options?.followSymlinks === false ? this.entries.get(normalizePath(targetPath)) : this.resolveEntry(targetPath); if (!entry) throw errnoError("ENOENT", `chown '${targetPath}'`); entry.uid = uid; entry.gid = gid; entry.ctimeMs = Date.now(); } async utimes( targetPath: string, atime: number, mtime: number, ): Promise { const entry = this.resolveEntry(targetPath); if (!entry) throw errnoError("ENOENT", `utimes '${targetPath}'`); entry.atimeMs = atime; entry.mtimeMs = mtime; entry.ctimeMs = Date.now(); } async truncate(targetPath: string, length: number): Promise { const entry = this.resolveEntry(targetPath); if (!entry || entry.type !== "file") { throw errnoError("ENOENT", `truncate '${targetPath}'`); } if (length < entry.data.length) { entry.data = entry.data.slice(0, length); } else if (length > entry.data.length) { const expanded = new Uint8Array(length); expanded.set(entry.data); entry.data = expanded; } entry.mtimeMs = Date.now(); entry.ctimeMs = Date.now(); } async pread( targetPath: string, offset: number, length: number, ): Promise { const entry = this.resolveEntry(targetPath); if (!entry || entry.type !== "file") { throw errnoError("ENOENT", `open '${targetPath}'`); } if (offset >= entry.data.length) return new Uint8Array(0); return entry.data.slice( offset, Math.min(offset + length, entry.data.length), ); } async pwrite( targetPath: string, offset: number, data: Uint8Array, ): Promise { const entry = this.resolveEntry(targetPath); if (!entry || entry.type !== "file") { throw errnoError("ENOENT", `open '${targetPath}'`); } const nextSize = Math.max(entry.data.length, offset + data.length); const updated = new Uint8Array(nextSize); updated.set(entry.data); updated.set(data, offset); entry.data = updated; entry.mtimeMs = Date.now(); entry.ctimeMs = Date.now(); } private resolvePath(targetPath: string, depth = 0): string { if (depth > MAX_SYMLINK_DEPTH) { throw errnoError("ELOOP", `too many symbolic links '${targetPath}'`); } const normalized = normalizePath(targetPath); const entry = this.entries.get(normalized); if (!entry) return normalized; if (entry.type === "symlink") { const target = entry.target.startsWith("/") ? entry.target : `${dirnameVirtual(normalized)}/${entry.target}`; return this.resolvePath(target, depth + 1); } return normalized; } private resolveEntry(targetPath: string): MemoryEntry | undefined { return this.entries.get(this.resolvePath(targetPath)); } private newDirectory(): DirectoryEntry { const now = Date.now(); return { type: "dir", mode: S_IFDIR | 0o755, uid: 0, gid: 0, nlink: 2, ino: nextInode++, atimeMs: now, mtimeMs: now, ctimeMs: now, birthtimeMs: now, }; } private toStat(entry: MemoryEntry): VirtualStat { const size = entry.type === "file" ? entry.data.length : 4096; return { mode: entry.mode, size, blocks: size === 0 ? 0 : Math.ceil(size / 512), dev: 1, rdev: 0, isDirectory: entry.type === "dir", isSymbolicLink: entry.type === "symlink", atimeMs: entry.atimeMs, mtimeMs: entry.mtimeMs, ctimeMs: entry.ctimeMs, birthtimeMs: entry.birthtimeMs, ino: entry.ino, nlink: entry.nlink, uid: entry.uid, gid: entry.gid, }; } } export function createInMemoryFileSystem(): InMemoryFileSystem { return new InMemoryFileSystem(); } export class NodeFileSystem implements VirtualFileSystem { readonly rootPath: string; constructor(options: { root: string }) { this.rootPath = fsSync.realpathSync(options.root); } private normalizeTarget(targetPath: string): string { const normalized = normalizePath(targetPath).replace(/^\/+/, ""); const resolved = path.resolve(this.rootPath, normalized); if ( resolved !== this.rootPath && !resolved.startsWith(`${this.rootPath}${path.sep}`) ) { throw errnoError("EACCES", `path escapes root '${targetPath}'`); } return resolved; } private toStat(stat: fsSync.Stats): VirtualStat { const posixStat = stat as fsSync.Stats & { blocks?: number; dev?: number; rdev?: number; }; return { mode: stat.mode, size: stat.size, blocks: posixStat.blocks ?? (stat.size === 0 ? 0 : Math.ceil(stat.size / 512)), dev: posixStat.dev ?? 1, rdev: posixStat.rdev ?? 0, isDirectory: stat.isDirectory(), isSymbolicLink: stat.isSymbolicLink(), atimeMs: Math.trunc(stat.atimeMs), mtimeMs: Math.trunc(stat.mtimeMs), ctimeMs: Math.trunc(stat.ctimeMs), birthtimeMs: Math.trunc(stat.birthtimeMs), ino: stat.ino, nlink: stat.nlink, uid: stat.uid, gid: stat.gid, }; } async readFile(targetPath: string): Promise { return new Uint8Array(await fs.readFile(this.normalizeTarget(targetPath))); } async readTextFile(targetPath: string): Promise { return fs.readFile(this.normalizeTarget(targetPath), "utf8"); } async readDir(targetPath: string): Promise { return fs.readdir(this.normalizeTarget(targetPath)); } async readDirWithTypes(targetPath: string): Promise { const entries = await fs.readdir(this.normalizeTarget(targetPath), { withFileTypes: true, }); return entries.map((entry) => ({ name: entry.name, isDirectory: entry.isDirectory(), isSymbolicLink: entry.isSymbolicLink(), })); } async writeFile( targetPath: string, content: string | Uint8Array, ): Promise { const resolved = this.normalizeTarget(targetPath); await fs.mkdir(path.dirname(resolved), { recursive: true }); await fs.writeFile(resolved, content); } async createDir(targetPath: string): Promise { await fs.mkdir(this.normalizeTarget(targetPath)); } async mkdir( targetPath: string, options?: { recursive?: boolean }, ): Promise { await fs.mkdir(this.normalizeTarget(targetPath), { recursive: options?.recursive ?? true, }); } async exists(targetPath: string): Promise { try { await fs.access(this.normalizeTarget(targetPath)); return true; } catch { return false; } } async stat(targetPath: string): Promise { return this.toStat(await fs.stat(this.normalizeTarget(targetPath))); } async removeFile(targetPath: string): Promise { await fs.unlink(this.normalizeTarget(targetPath)); } async removeDir(targetPath: string): Promise { await fs.rmdir(this.normalizeTarget(targetPath)); } async rename(oldPath: string, newPath: string): Promise { const nextPath = this.normalizeTarget(newPath); await fs.mkdir(path.dirname(nextPath), { recursive: true }); await fs.rename(this.normalizeTarget(oldPath), nextPath); } async realpath(targetPath: string): Promise { const real = await fs.realpath(this.normalizeTarget(targetPath)); const relative = path.relative(this.rootPath, real); return relative ? `/${relative.split(path.sep).join("/")}` : "/"; } async symlink(target: string, linkPath: string): Promise { const resolvedLink = this.normalizeTarget(linkPath); await fs.mkdir(path.dirname(resolvedLink), { recursive: true }); await fs.symlink(target, resolvedLink); } async readlink(targetPath: string): Promise { return fs.readlink(this.normalizeTarget(targetPath)); } async lstat(targetPath: string): Promise { return this.toStat(await fs.lstat(this.normalizeTarget(targetPath))); } async link(oldPath: string, newPath: string): Promise { await fs.link(this.normalizeTarget(oldPath), this.normalizeTarget(newPath)); } async chmod(targetPath: string, mode: number): Promise { await fs.chmod(this.normalizeTarget(targetPath), mode); } async chown( targetPath: string, uid: number, gid: number, options?: { followSymlinks?: boolean }, ): Promise { const normalized = this.normalizeTarget(targetPath); if (options?.followSymlinks === false) { await fs.lchown(normalized, uid, gid); } else { await fs.chown(normalized, uid, gid); } } async utimes( targetPath: string, atime: number, mtime: number, ): Promise { await fs.utimes( this.normalizeTarget(targetPath), atime / 1000, mtime / 1000, ); } async truncate(targetPath: string, length: number): Promise { await fs.truncate(this.normalizeTarget(targetPath), length); } async pread( targetPath: string, offset: number, length: number, ): Promise { const handle = await fs.open(this.normalizeTarget(targetPath), "r"); try { const buffer = Buffer.alloc(length); const { bytesRead } = await handle.read(buffer, 0, length, offset); return new Uint8Array(buffer.buffer, buffer.byteOffset, bytesRead); } finally { await handle.close(); } } async pwrite( targetPath: string, offset: number, data: Uint8Array, ): Promise { const handle = await fs.open( this.normalizeTarget(targetPath), fsSync.constants.O_WRONLY, ); try { await handle.write(data, 0, data.length, offset); } finally { await handle.close(); } } } function permissionAllows(mode: PermissionMode | undefined): boolean { return mode !== "deny"; } function globMatches(pattern: string, value: string): boolean { const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&"); const expression = escaped.replace(/\*/g, ".*"); return new RegExp(`^${expression}$`).test(value); } function envPolicyAllows( policy: EnvPermissions | undefined, name: string, ): boolean { if (policy === undefined) { return true; } if (typeof policy === "string") { return permissionAllows(policy); } let mode = policy.default ?? "deny"; for (const rule of policy.rules) { const operationsMatch = !rule.operations || rule.operations.length === 0 || rule.operations.includes("read"); const patternsMatch = !rule.patterns || rule.patterns.length === 0 || rule.patterns.some((pattern) => globMatches(pattern, name)); if (operationsMatch && patternsMatch) { mode = rule.mode; } } return permissionAllows(mode); } export const allowAllFs: FsPermissions = "allow"; export const allowAllNetwork: NetworkPermissions = "allow"; export const allowAllChildProcess: ChildProcessPermissions = "allow"; export const allowAllProcess: ProcessPermissions = "allow"; export const allowAllEnv: EnvPermissions = "allow"; export const allowAll: Permissions = { fs: allowAllFs, network: allowAllNetwork, childProcess: allowAllChildProcess, process: allowAllProcess, env: allowAllEnv, }; function normalizeFsPermissionScope( scope: FsPermissions | undefined, ): PermissionsPolicy["fs"] { if (scope === undefined || typeof scope === "string") { return scope; } return { ...scope, rules: scope.rules.map((rule) => ({ mode: rule.mode, operations: rule.operations ?? [], paths: rule.paths ?? [], })), }; } function normalizePatternPermissionScope( scope: | NetworkPermissions | ChildProcessPermissions | ProcessPermissions | EnvPermissions | BindingPermissions | undefined, ): PermissionsPolicy["network"] { if (scope === undefined || typeof scope === "string") { return scope; } return { ...scope, rules: scope.rules.map((rule) => ({ mode: rule.mode, operations: rule.operations ?? [], patterns: rule.patterns ?? [], })), }; } function normalizePermissionsPolicy( permissions: Permissions | undefined, ): PermissionsPolicy | undefined { if (!permissions) { return undefined; } return { fs: normalizeFsPermissionScope(permissions.fs), network: normalizePatternPermissionScope(permissions.network), childProcess: normalizePatternPermissionScope(permissions.childProcess), process: normalizePatternPermissionScope(permissions.process), env: normalizePatternPermissionScope(permissions.env), binding: normalizePatternPermissionScope(permissions.binding), }; } export function filterEnv( env: Record | undefined, permissions?: Permissions, ): Record { const input = env ?? {}; if (!permissions?.env) return { ...input }; const output: Record = {}; for (const [name, value] of Object.entries(input)) { if (envPolicyAllows(permissions.env, name)) { output[name] = value; } } return output; } export function createProcessScopedFileSystem( filesystem: VirtualFileSystem, ): VirtualFileSystem { return filesystem; } export async function exists( filesystem: VirtualFileSystem, targetPath: string, ): Promise { return filesystem.exists(targetPath); } export async function stat( filesystem: VirtualFileSystem, targetPath: string, ): Promise { return filesystem.stat(targetPath); } export async function rename( filesystem: VirtualFileSystem, oldPath: string, newPath: string, ): Promise { return filesystem.rename(oldPath, newPath); } export async function readDirWithTypes( filesystem: VirtualFileSystem, targetPath: string, ): Promise { return filesystem.readDirWithTypes(targetPath); } export async function mkdir( filesystem: VirtualFileSystem, targetPath: string, options?: { recursive?: boolean }, ): Promise { return filesystem.mkdir(targetPath, options); } export function createNodeHostCommandExecutor(): CommandExecutor { return { spawn() { throw new Error( "createNodeHostCommandExecutor is not supported on the native runtime path", ); }, }; } export function createKernelCommandExecutor(kernel: Kernel): CommandExecutor { return { spawn(command, args, options) { return kernel.spawn(command, args, options); }, }; } export function createKernelVfsAdapter( kernelVfs: VirtualFileSystem, ): VirtualFileSystem { return kernelVfs; } export function createHostFallbackVfs( base: VirtualFileSystem, ): VirtualFileSystem { return base; } export function isPrivateIp(host: string): boolean { return ( host === "localhost" || host === "127.0.0.1" || host.startsWith("10.") || host.startsWith("192.168.") || /^172\.(1[6-9]|2\d|3[0-1])\./.test(host) ); } export function createNodeHostNetworkAdapter(): NetworkAdapter { return createDefaultNetworkAdapter(); } export function createDefaultNetworkAdapter(): NetworkAdapter { return { async fetch(url, options) { const response = await globalThis.fetch(url, { method: options?.method ?? "GET", headers: options?.headers, body: options?.body as RequestInit["body"], }); const headers: Record = {}; response.headers.forEach((value, key) => { headers[key] = value; }); return { ok: response.ok, status: response.status, statusText: response.statusText, headers, body: await response.text(), url: response.url, redirected: response.redirected, }; }, async dnsLookup(hostname) { return { address: hostname, family: hostname.includes(":") ? 6 : 4 }; }, async httpRequest(url, options) { const response = await globalThis.fetch(url, { method: options?.method ?? "GET", headers: options?.headers, body: options?.body as RequestInit["body"], }); const headers: Record = {}; response.headers.forEach((value, key) => { headers[key] = value; }); return { status: response.status, statusText: response.statusText, headers, body: await response.text(), url: response.url, }; }, }; } export function createNodeDriver( options: NodeDriverOptions = {}, ): SystemDriver { return { filesystem: options.filesystem, network: options.networkAdapter, commandExecutor: options.commandExecutor, permissions: options.permissions, runtime: { process: options.processConfig ?? {}, os: options.osConfig ?? {}, }, }; } export class NodeExecutionDriver implements NodeRuntimeDriver { readonly network?: Pick< NetworkAdapter, "fetch" | "dnsLookup" | "httpRequest" >; constructor(private readonly options: RuntimeDriverOptions) { this.network = options.system.network; } async exec(): Promise { throw new Error( "NodeExecutionDriver is not available after the native runtime migration", ); } async run(): Promise> { throw new Error( "NodeExecutionDriver is not available after the native runtime migration", ); } dispose(): void { void this.options; } async terminate(): Promise {} } export class NodeRuntime extends NodeExecutionDriver {} export function createNodeRuntimeDriverFactory(): NodeRuntimeDriverFactory { return { createRuntimeDriver(options) { return new NodeRuntime(options); }, }; } export class ModuleAccessFileSystem extends NodeFileSystem {} export const WASMVM_COMMANDS = Object.freeze([ "sh", "bash", "grep", "egrep", "fgrep", "rg", "sed", "awk", "jq", "yq", "find", "fd", "cat", "chmod", "column", "cp", "dd", "diff", "du", "expr", "file", "head", "ln", "logname", "ls", "mkdir", "mktemp", "mv", "pathchk", "rev", "rm", "sleep", "sort", "split", "stat", "strings", "tac", "tail", "test", "[", "touch", "tree", "tsort", "whoami", "gzip", "gunzip", "zcat", "tar", "zip", "unzip", "sqlite3", "curl", "git", "git-remote-http", "git-remote-https", "env", "envsubst", "nice", "nohup", "stdbuf", "timeout", "xargs", "base32", "base64", "basenc", "basename", "comm", "cut", "dircolors", "dirname", "echo", "expand", "factor", "false", "fmt", "fold", "join", "nl", "numfmt", "od", "paste", "printenv", "printf", "ptx", "seq", "shuf", "tr", "true", "unexpand", "uniq", "wc", "yes", "b2sum", "cksum", "md5sum", "sha1sum", "sha224sum", "sha256sum", "sha384sum", "sha512sum", "sum", "link", "pwd", "readlink", "realpath", "rmdir", "shred", "tee", "truncate", "unlink", "arch", "date", "nproc", "uname", "dir", "vdir", "hostname", "hostid", "more", "sync", "tty", "chcon", "runcon", "chgrp", "chown", "chroot", "df", "groups", "id", "install", "kill", "mkfifo", "mknod", "pinky", "who", "users", "uptime", "stty", "codex", "codex-exec", ]) as readonly string[]; export type PermissionTier = "full" | "read-write" | "read-only" | "isolated"; export const DEFAULT_FIRST_PARTY_TIERS: Readonly< Record > = Object.freeze({ sh: "full", bash: "full", env: "full", timeout: "full", xargs: "full", nice: "full", nohup: "full", stdbuf: "full", codex: "full", "codex-exec": "full", git: "full", "git-remote-http": "full", "git-remote-https": "full", grep: "read-only", egrep: "read-only", fgrep: "read-only", rg: "read-only", cat: "read-only", head: "read-only", tail: "read-only", wc: "read-only", sort: "read-only", uniq: "read-only", diff: "read-only", find: "read-only", fd: "read-only", tree: "read-only", file: "read-only", du: "read-only", ls: "read-only", dir: "read-only", vdir: "read-only", strings: "read-only", stat: "read-only", rev: "read-only", column: "read-only", cut: "read-only", tr: "read-only", paste: "read-only", join: "read-only", fold: "read-only", expand: "read-only", nl: "read-only", od: "read-only", comm: "read-only", basename: "read-only", dirname: "read-only", realpath: "read-only", readlink: "read-only", pwd: "read-only", echo: "read-only", envsubst: "read-only", printf: "read-only", true: "read-only", false: "read-only", yes: "read-only", seq: "read-only", test: "read-only", "[": "read-only", expr: "read-only", factor: "read-only", date: "read-only", uname: "read-only", nproc: "read-only", whoami: "read-only", id: "read-only", groups: "read-only", base64: "read-only", md5sum: "read-only", sha256sum: "read-only", tac: "read-only", tsort: "read-only", curl: "full", sqlite3: "read-write", }); export interface WasmVmRuntimeOptions { wasmBinaryPath?: string; commandDirs?: string[]; permissions?: Record; } class NativeRuntimeDescriptor implements KernelRuntimeDriver { constructor( readonly kind: "node" | "wasmvm", readonly name: string, readonly commands: string[], readonly commandDirs?: string[], ) {} } function normalizeCommandLookup(command: string): string { return path.posix.basename(command); } interface DiscoveredWasmCommandEntry { name: string; hostPath: string; dirOffset: number; } function isWasmBinaryFile(filePath: string): boolean { try { const header = fsSync.readFileSync(filePath, { encoding: null }); return ( header.length >= 4 && header[0] === 0x00 && header[1] === 0x61 && header[2] === 0x73 && header[3] === 0x6d ); } catch { return false; } } function discoverWasmCommandEntries( commandDirs: string[], ): DiscoveredWasmCommandEntry[] { const discovered: DiscoveredWasmCommandEntry[] = []; const seen = new Set(); commandDirs.forEach((commandDir, dirOffset) => { let entries: string[]; try { entries = fsSync .readdirSync(commandDir) .sort((left, right) => left.localeCompare(right)); } catch { return; } for (const entry of entries) { if (entry.startsWith(".")) continue; if (seen.has(entry)) continue; const fullPath = path.join(commandDir, entry); if (isWasmBinaryFile(fullPath)) { seen.add(entry); discovered.push({ name: entry, hostPath: fullPath, dirOffset, }); continue; } try { const realPath = fsSync.realpathSync(fullPath); if (isWasmBinaryFile(realPath)) { seen.add(entry); discovered.push({ name: entry, hostPath: fullPath, dirOffset, }); } } catch {} } }); return discovered; } class WasmVmRuntimeDescriptor implements KernelRuntimeDriver { readonly kind = "wasmvm" as const; readonly name = "wasmvm" as const; readonly commands: string[] = []; readonly commandDirs?: string[]; readonly _commandPaths = new Map(); readonly _moduleCache = new Map(); private readonly commandDirOffsets = new Map(); constructor(options: WasmVmRuntimeOptions) { this.commandDirs = options.commandDirs && options.commandDirs.length > 0 ? [...options.commandDirs] : undefined; if (options.commandDirs && options.commandDirs.length > 0) { this.refreshDiscovery(); return; } this.commands.push(...WASMVM_COMMANDS); if (options.wasmBinaryPath) { console.warn( "createWasmVmRuntime({ wasmBinaryPath }) is deprecated; use commandDirs instead.", ); } } init(_kernel: KernelInterface): void { if (this.commandDirs && this.commandDirs.length > 0) { this.refreshDiscovery(); } } tryResolve(command: string): boolean { if (!this.commandDirs || this.commandDirs.length === 0) { return false; } const normalized = normalizeCommandLookup(command); if (this._commandPaths.has(normalized)) { return true; } this.refreshDiscovery(); return this._commandPaths.has(normalized); } getGuestCommandPaths(startIndex: number): ReadonlyMap { const guestPaths = new Map(); for (const [name] of this._commandPaths) { const dirOffset = this.commandDirOffsets.get(name); if (dirOffset === undefined) { continue; } guestPaths.set( name, `/__secure_exec/commands/${startIndex + dirOffset}/${name}`, ); } return guestPaths; } recordModuleExecution(command: string): void { const normalized = normalizeCommandLookup(command); if ( this._commandPaths.has(normalized) || ((!this.commandDirs || this.commandDirs.length === 0) && this.commands.includes(normalized)) ) { this._moduleCache.set(normalized, true); } } private refreshDiscovery(): void { if (!this.commandDirs || this.commandDirs.length === 0) { return; } const discovered = discoverWasmCommandEntries(this.commandDirs); this.commands.length = 0; this._commandPaths.clear(); this.commandDirOffsets.clear(); for (const entry of discovered) { this.commands.push(entry.name); this._commandPaths.set(entry.name, entry.hostPath); this.commandDirOffsets.set(entry.name, entry.dirOffset); } } } export function createWasmVmRuntime( options: WasmVmRuntimeOptions = {}, ): KernelRuntimeDriver { return new WasmVmRuntimeDescriptor(options); } export function createNodeRuntime(): KernelRuntimeDriver { return new NativeRuntimeDescriptor("node", "node", ["node", "npm", "npx"]); } function latestMtimeMs(targetPath: string): number { try { const stats = fsSync.statSync(targetPath); if (!stats.isDirectory()) { return stats.mtimeMs; } let latest = stats.mtimeMs; for (const entry of fsSync.readdirSync(targetPath)) { latest = Math.max(latest, latestMtimeMs(path.join(targetPath, entry))); } return latest; } catch { return 0; } } function sidecarBinaryNeedsBuild(): boolean { if (!fsSync.existsSync(SIDECAR_BINARY)) { return true; } const binaryMtime = latestMtimeMs(SIDECAR_BINARY); return SIDECAR_BUILD_INPUTS.some( (inputPath) => latestMtimeMs(inputPath) > binaryMtime, ); } function ensureNativeSidecarBinary(): string { // A published install has no in-repo Cargo workspace to build from: resolve // the prebuilt platform binary (or an explicit sidecar override). if ( process.env.AGENTOS_NATIVE_SIDECAR_BIN || process.env.AGENTOS_SIDECAR_BIN || !fsSync.existsSync(path.join(REPO_ROOT, "Cargo.toml")) ) { return resolvePublishedSidecarBinary(); } if ( ensuredSidecarBinary && fsSync.existsSync(ensuredSidecarBinary) && !sidecarBinaryNeedsBuild() ) { return ensuredSidecarBinary; } if (sidecarBinaryNeedsBuild()) { const cargoBinary = findCargoBinary(); if (cargoBinary) { execFileSync( cargoBinary, ["build", "-q", "-p", "agentos-native-sidecar"], { cwd: REPO_ROOT, stdio: "pipe", }, ); } else if (!fsSync.existsSync(SIDECAR_BINARY)) { execFileSync( resolveCargoBinary(), ["build", "-q", "-p", "agentos-native-sidecar"], { cwd: REPO_ROOT, stdio: "pipe", }, ); } } ensuredSidecarBinary = SIDECAR_BINARY; return ensuredSidecarBinary; } export function resolveNodeRuntimeSidecarBinary(): string { return ensureNativeSidecarBinary(); } function createBootstrapEntries(commandNames: string[]): RootFilesystemEntry[] { const entries: RootFilesystemEntry[] = [ { path: "/", kind: "directory", mode: 0o755, uid: 0, gid: 0, }, ...KERNEL_POSIX_BOOTSTRAP_DIRS.map((entryPath) => ({ path: entryPath, kind: "directory" as const, mode: entryPath === "/tmp" || entryPath === "/var/tmp" ? 0o1777 : entryPath === "/home/agentos" ? 0o2755 : 0o755, uid: entryPath === "/home/agentos" || entryPath === "/workspace" ? 1000 : 0, gid: entryPath === "/home/agentos" || entryPath === "/workspace" ? 1000 : 0, })), { path: "/usr/bin/env", kind: "file", mode: 0o644, uid: 0, gid: 0, content: "", encoding: "utf8", }, ]; for (const command of [...new Set(commandNames)].sort((left, right) => left.localeCompare(right), )) { entries.push({ path: `/bin/${command}`, kind: "file", mode: 0o755, uid: 0, gid: 0, content: KERNEL_COMMAND_STUB, encoding: "utf8", }); } return entries; } function mergeRootFilesystemEntries( baseEntries: RootFilesystemEntry[], overrideEntries: RootFilesystemEntry[], ): RootFilesystemEntry[] { const merged = new Map(); for (const entry of baseEntries) { merged.set(entry.path, entry); } for (const entry of overrideEntries) { merged.set(entry.path, entry); } return [...merged.values()]; } async function snapshotFilesystemEntries( filesystem: VirtualFileSystem, targetPath = "/", output: RootFilesystemEntry[] = [], options?: { passthroughDirectories?: ReadonlySet; }, ): Promise { const passthroughDirectories = options?.passthroughDirectories; const passthroughDirectory = passthroughDirectories?.has(targetPath) ?? false; const statInfo = targetPath === "/" || passthroughDirectory ? await filesystem.stat(targetPath) : await filesystem.lstat(targetPath); if (statInfo.isSymbolicLink) { output.push({ path: targetPath, kind: "symlink", mode: statInfo.mode, uid: statInfo.uid, gid: statInfo.gid, target: await filesystem.readlink(targetPath), }); return output; } if (statInfo.isDirectory) { output.push({ path: targetPath, kind: "directory", mode: statInfo.mode, uid: statInfo.uid, gid: statInfo.gid, }); if (passthroughDirectory) { return output; } const children = (await filesystem.readDirWithTypes(targetPath)) .map((entry) => entry.name) .filter((name) => name !== "." && name !== "..") .sort((left, right) => left.localeCompare(right)); for (const child of children) { const childPath = targetPath === "/" ? posixPath.join("/", child) : posixPath.join(targetPath, child); await snapshotFilesystemEntries(filesystem, childPath, output, options); } return output; } output.push({ path: targetPath, kind: "file", mode: statInfo.mode, uid: statInfo.uid, gid: statInfo.gid, content: Buffer.from(await filesystem.readFile(targetPath)).toString( "base64", ), encoding: "base64", }); return output; } async function materializeSnapshotEntriesIntoVm( client: SidecarProcess, session: AuthenticatedSession, vm: CreatedVm, entries: RootFilesystemEntry[], ): Promise { for (const entry of entries) { if (entry.path === "/") { continue; } if (entry.kind === "directory") { await client.mkdir(session, vm, entry.path, { recursive: true }); } else if (entry.kind === "file") { await client.writeFile( session, vm, entry.path, decodeRootFilesystemEntryContent(entry), ); } else { await client.symlink(session, vm, entry.target ?? "", entry.path); continue; } if (typeof entry.mode === "number") { await client.chmod(session, vm, entry.path, entry.mode); } if (typeof entry.uid === "number" && typeof entry.gid === "number") { await client.chown(session, vm, entry.path, entry.uid, entry.gid); } } } function decodeRootFilesystemEntryContent( entry: RootFilesystemEntry, ): Uint8Array { const content = entry.content ?? ""; if (entry.encoding === "base64") { return new Uint8Array(Buffer.from(content, "base64")); } return new TextEncoder().encode(content); } const NODE_FILESYSTEM_ROOT_PASSTHROUGH_DIRS = ["node_modules"] as const; function planNodeFilesystemPassthroughMounts( filesystem: VirtualFileSystem, existingMounts: readonly LocalCompatMount[], ): { mounts: LocalCompatMount[]; passthroughDirectories: ReadonlySet; } { if (!(filesystem instanceof NodeFileSystem)) { return { mounts: [], passthroughDirectories: new Set(), }; } const passthroughDirectories = new Set(); const existingGuestPaths = new Set(existingMounts.map((mount) => mount.path)); const mounts: LocalCompatMount[] = []; for (const directoryName of NODE_FILESYSTEM_ROOT_PASSTHROUGH_DIRS) { const guestPath = normalizePath(`/${directoryName}`); const hostPath = path.join(filesystem.rootPath, directoryName); let statInfo: fsSync.Stats; try { statInfo = fsSync.statSync(hostPath); } catch { continue; } if (!statInfo.isDirectory()) { continue; } passthroughDirectories.add(guestPath); if (existingGuestPaths.has(guestPath)) { continue; } mounts.push({ path: guestPath, fs: new NodeFileSystem({ root: hostPath }), readOnly: true, }); if (directoryName === "node_modules") { appendNodeModulesSymlinkTargetMounts( mounts, existingGuestPaths, guestPath, hostPath, ); } } return { mounts, passthroughDirectories, }; } function appendNodeModulesSymlinkTargetMounts( mounts: LocalCompatMount[], existingGuestPaths: Set, guestRoot: string, hostRoot: string, ): void { let canonicalHostRoot: string; try { canonicalHostRoot = fsSync.realpathSync(hostRoot); } catch { return; } const appendTarget = (guestPath: string, symlinkPath: string): void => { if (existingGuestPaths.has(guestPath)) return; try { const target = fsSync.realpathSync(symlinkPath); if (!fsSync.statSync(target).isDirectory()) return; const relativeTarget = path.relative(canonicalHostRoot, target); if ( relativeTarget === "" || (!relativeTarget.startsWith(`..${path.sep}`) && relativeTarget !== ".." && !path.isAbsolute(relativeTarget)) ) { // pnpm and similar stores deliberately use links that remain inside // node_modules. Keeping those on the root mount preserves the realpath // ancestry needed to resolve their nested dependency links. return; } mounts.push({ path: guestPath, fs: new NodeFileSystem({ root: target }), readOnly: true, }); existingGuestPaths.add(guestPath); } catch { // Broken or concurrently-removed links remain unavailable, matching Node. } }; let entries: fsSync.Dirent[]; try { entries = fsSync.readdirSync(hostRoot, { withFileTypes: true }); } catch { return; } for (const entry of entries) { if (entry.name.startsWith(".")) continue; const entryPath = path.join(hostRoot, entry.name); if (entry.isSymbolicLink()) { appendTarget(`${guestRoot}/${entry.name}`, entryPath); continue; } if (!entry.isDirectory() || !entry.name.startsWith("@")) continue; let scopedEntries: fsSync.Dirent[]; try { scopedEntries = fsSync.readdirSync(entryPath, { withFileTypes: true }); } catch { continue; } for (const scopedEntry of scopedEntries) { if (scopedEntry.name.startsWith(".") || !scopedEntry.isSymbolicLink()) { continue; } appendTarget( `${guestRoot}/${entry.name}/${scopedEntry.name}`, path.join(entryPath, scopedEntry.name), ); } } } function collectGuestCommandPaths( commandDirs: string[], startIndex = 0, ): Map { const guestPaths = new Map(); for (const entry of discoverWasmCommandEntries(commandDirs)) { if (!guestPaths.has(entry.name)) { guestPaths.set( entry.name, `/__secure_exec/commands/${startIndex + entry.dirOffset}/${entry.name}`, ); } } return guestPaths; } async function ensureCommandStubs( proxy: NativeSidecarKernelProxy, commands: Iterable, ): Promise { const rootView = proxy.createRootView(); for (const command of commands) { // `/bin` supports PATH-based test commands. `/opt/agentos/bin` mirrors // the package projection used by production VMs and is required by // upstream tools that exec private helpers via an absolute libexec path. for (const stubPath of [`/bin/${command}`, `/opt/agentos/bin/${command}`]) { await rootView.writeFile(stubPath, KERNEL_COMMAND_STUB); await rootView.chmod(stubPath, 0o755); } } } class DeferredFileSystem implements VirtualFileSystem { constructor(private readonly getFilesystem: () => VirtualFileSystem | null) {} private filesystem(): VirtualFileSystem { const filesystem = this.getFilesystem(); if (!filesystem) { throw new Error("kernel filesystem is not ready; mount a runtime first"); } return filesystem; } readFile(path: string): Promise { return this.filesystem().readFile(path); } readTextFile(path: string): Promise { return this.filesystem().readTextFile(path); } readDir(path: string): Promise { return this.filesystem().readDir(path); } readDirWithTypes(path: string): Promise { return this.filesystem().readDirWithTypes(path); } writeFile(path: string, content: string | Uint8Array): Promise { return this.filesystem().writeFile(path, content); } createDir(path: string): Promise { return this.filesystem().createDir(path); } mkdir(path: string, options?: { recursive?: boolean }): Promise { return this.filesystem().mkdir(path, options); } exists(path: string): Promise { return this.filesystem().exists(path); } stat(path: string): Promise { return this.filesystem().stat(path); } removeFile(path: string): Promise { return this.filesystem().removeFile(path); } removeDir(path: string): Promise { return this.filesystem().removeDir(path); } rename(oldPath: string, newPath: string): Promise { return this.filesystem().rename(oldPath, newPath); } realpath(path: string): Promise { return this.filesystem().realpath(path); } symlink(target: string, linkPath: string): Promise { return this.filesystem().symlink(target, linkPath); } readlink(path: string): Promise { return this.filesystem().readlink(path); } lstat(path: string): Promise { return this.filesystem().lstat(path); } link(oldPath: string, newPath: string): Promise { return this.filesystem().link(oldPath, newPath); } chmod(path: string, mode: number): Promise { return this.filesystem().chmod(path, mode); } chown( path: string, uid: number, gid: number, options?: { followSymlinks?: boolean }, ): Promise { return this.filesystem().chown(path, uid, gid, options); } utimes(path: string, atime: number, mtime: number): Promise { return this.filesystem().utimes(path, atime, mtime); } truncate(path: string, length: number): Promise { return this.filesystem().truncate(path, length); } pread(path: string, offset: number, length: number): Promise { return this.filesystem().pread(path, offset, length); } pwrite(path: string, offset: number, data: Uint8Array): Promise { return this.filesystem().pwrite(path, offset, data); } } const VIRTUAL_FILESYSTEM_METHOD_NAMES = [ "readFile", "readTextFile", "readDir", "readDirWithTypes", "writeFile", "createDir", "mkdir", "exists", "stat", "removeFile", "removeDir", "rename", "realpath", "symlink", "readlink", "lstat", "link", "chmod", "chown", "utimes", "truncate", "pread", "pwrite", ] as const; type VirtualFileSystemMethodName = (typeof VIRTUAL_FILESYSTEM_METHOD_NAMES)[number]; type BoundVirtualFileSystemMethods = Partial< Record unknown> >; interface LiveFilesystemBinding { syncFromLive(paths: readonly string[]): Promise; restore(): void; } const DEFAULT_LIVE_FILESYSTEM_SYNC_MAX_BYTES = 64 * 1024 * 1024; type LiveFilesystemSnapshotEntry = | { kind: "directory"; path: string } | { kind: "symlink"; path: string; target: string } | { kind: "file"; path: string; content: Uint8Array }; class LiveFilesystemSyncLimitError extends Error { readonly code = "ERR_AGENTOS_LIVE_FILESYSTEM_SYNC_LIMIT"; } function topLevelSyncRoot(targetPath: string): string { const normalized = normalizePath(targetPath); const [first] = normalized.split("/").filter(Boolean); return first ? `/${first}` : "/"; } function collectLiveFilesystemSyncRoots( entries: readonly RootFilesystemEntry[], ): string[] { const roots = new Set(); for (const entry of entries) { if (entry.path === "/") { continue; } roots.add(topLevelSyncRoot(entry.path)); } return [...roots].sort((left, right) => left.localeCompare(right)); } async function callBoundFilesystemMethod( methods: BoundVirtualFileSystemMethods, method: VirtualFileSystemMethodName, ...args: unknown[] ): Promise { const delegate = methods[method]; if (!delegate) { throw new Error(`filesystem method ${method} is unavailable`); } return (await delegate(...args)) as T; } async function ensureBoundParentDirectory( methods: BoundVirtualFileSystemMethods, targetPath: string, ): Promise { const parent = dirnameVirtual(targetPath); if (parent === targetPath) { return; } await callBoundFilesystemMethod(methods, "mkdir", parent, { recursive: true, }); } async function syncLiveFilesystemToBoundMethods( live: VirtualFileSystem, methods: BoundVirtualFileSystemMethods, paths: readonly string[], maxBytes: number, ): Promise { const snapshot: LiveFilesystemSnapshotEntry[] = []; const usage = { bytes: 0 }; for (const targetPath of [...new Set(paths.map(normalizePath))].sort( (left, right) => left.localeCompare(right), )) { if (!(await live.exists(targetPath).catch(() => false))) { continue; } await snapshotLiveFilesystemPath( live, targetPath, snapshot, usage, maxBytes, ); } if (usage.bytes >= maxBytes * 0.8) { process.emitWarning( `live filesystem sync retained ${usage.bytes} of ${maxBytes} bytes; ` + "raise createKernel({ maxFilesystemBytes }) before increasing VM filesystem limits", { code: "AGENTOS_LIVE_FILESYSTEM_SYNC_NEAR_LIMIT" }, ); } for (const entry of snapshot) { await applyLiveFilesystemSnapshotEntry(methods, entry); } } async function snapshotLiveFilesystemPath( live: VirtualFileSystem, targetPath: string, snapshot: LiveFilesystemSnapshotEntry[], usage: { bytes: number }, maxBytes: number, knownEntry?: Pick, ): Promise { const stat = knownEntry ?? (targetPath === "/" ? await live.stat(targetPath) : await live.lstat(targetPath)); if (stat.isSymbolicLink) { snapshot.push({ kind: "symlink", path: targetPath, target: await live.readlink(targetPath), }); return; } if (stat.isDirectory) { snapshot.push({ kind: "directory", path: targetPath }); const children = (await live.readDirWithTypes(targetPath)) .filter((entry) => entry.name !== "." && entry.name !== "..") .sort((left, right) => left.name.localeCompare(right.name)); for (const child of children) { await snapshotLiveFilesystemPath( live, targetPath === "/" ? posixPath.join("/", child.name) : posixPath.join(targetPath, child.name), snapshot, usage, maxBytes, child, ); } return; } const fileSize = "size" in stat && typeof stat.size === "number" ? stat.size : (await live.lstat(targetPath)).size; if (fileSize > maxBytes - usage.bytes) { throw new LiveFilesystemSyncLimitError( `live filesystem sync for ${targetPath} exceeds ${maxBytes} bytes ` + "(createKernel maxFilesystemBytes); raise createKernel({ maxFilesystemBytes })", ); } const content = await live.readFile(targetPath); if (content.byteLength > maxBytes - usage.bytes) { throw new LiveFilesystemSyncLimitError( `live filesystem sync for ${targetPath} exceeds ${maxBytes} bytes ` + "(createKernel maxFilesystemBytes); raise createKernel({ maxFilesystemBytes })", ); } usage.bytes += content.byteLength; snapshot.push({ kind: "file", path: targetPath, content }); } async function applyLiveFilesystemSnapshotEntry( methods: BoundVirtualFileSystemMethods, entry: LiveFilesystemSnapshotEntry, ): Promise { if (entry.kind === "directory") { await callBoundFilesystemMethod(methods, "mkdir", entry.path, { recursive: true, }); return; } await ensureBoundParentDirectory(methods, entry.path); if (entry.kind === "symlink") { await callBoundFilesystemMethod(methods, "removeFile", entry.path).catch( () => {}, ); await callBoundFilesystemMethod( methods, "symlink", entry.target, entry.path, ); return; } await callBoundFilesystemMethod( methods, "writeFile", entry.path, entry.content, ); } function bindLiveFilesystem( target: VirtualFileSystem, getFilesystem: () => VirtualFileSystem | null, maxBytes: number, ): LiveFilesystemBinding { const fallback: BoundVirtualFileSystemMethods = {}; for (const method of VIRTUAL_FILESYSTEM_METHOD_NAMES) { const candidate = (target as unknown as Record)[method]; if (typeof candidate === "function") { fallback[method] = candidate.bind(target); } } for (const method of VIRTUAL_FILESYSTEM_METHOD_NAMES) { (target as unknown as Record)[method] = ( ...args: unknown[] ) => { const filesystem = getFilesystem(); const delegate = filesystem ? (filesystem[method] as (...args: unknown[]) => unknown).bind( filesystem, ) : fallback[method]; if (!delegate) { throw new Error( `kernel filesystem is not ready; mount a runtime before calling ${method}()`, ); } return delegate(...args); }; } return { async syncFromLive(paths: readonly string[]): Promise { const filesystem = getFilesystem(); if (!filesystem) { return; } await syncLiveFilesystemToBoundMethods( filesystem, fallback, paths, maxBytes, ); }, restore(): void { for (const [method, delegate] of Object.entries(fallback)) { (target as unknown as Record)[method] = delegate; } }, }; } class NativeKernel implements Kernel { readonly env: Record; readonly cwd: string; readonly commands = new Map(); readonly processes = new Map(); readonly socketTable; readonly processTable; readonly timerTable = {}; readonly vfs: VirtualFileSystem; private client: SidecarProcess | null = null; private session: AuthenticatedSession | null = null; private vm: CreatedVm | null = null; private proxy: NativeSidecarKernelProxy | null = null; private rootFilesystem: VirtualFileSystem | null = null; private readyPromise: Promise | null = null; private readonly liveFilesystemBinding: LiveFilesystemBinding; private liveFilesystemSyncRoots: string[] = []; private readonly pendingLocalMounts: LocalCompatMount[] = []; private mountedCommandDirs: string[] = []; private readonly mountedRuntimeDrivers: KernelRuntimeDriver[] = []; private readonly runtimeDriverCommandDirStarts = new Map< KernelRuntimeDriver, number >(); private readonly loopbackExemptPorts: number[]; // Bindings registered with the VM, keyed by the callback key the sidecar // sends back on a host_callback request (the binding name). Installed lazily // on the first registerBindings call. private readonly bindingHandlers = new Map< string, (input: unknown) => unknown | Promise >(); private bindingRequestHandlerInstalled = false; constructor( private readonly options: { filesystem: VirtualFileSystem; permissions?: Permissions; env?: Record; cwd?: string; user?: VmUserConfig; sidecar?: SidecarProcess; onBootTiming?: (timing: KernelBootTiming) => void; hostNetworkAdapter?: unknown; loopbackExemptPorts?: number[]; jsRuntime?: Partial; limits?: VmLimitsConfig; mounts?: Array<{ path: string; fs: VirtualFileSystem; readOnly?: boolean; }>; syncFilesystemOnDispose?: boolean; maxFilesystemBytes?: number; }, ) { this.env = { ...(options.env ?? {}) }; this.cwd = options.cwd ?? "/workspace"; this.socketTable = { hasHostNetworkAdapter: () => Boolean(options.hostNetworkAdapter), findListener: (request: { host?: string; port?: number; path?: string; }) => this.proxy?.findListener(request) ?? null, findListenerAsync: async (request: { host?: string; port?: number; path?: string; }) => { if (this.proxy?.findListenerAsync) { return this.proxy.findListenerAsync(request); } return this.proxy?.findListener(request) ?? null; }, findBoundUdp: (request: { host?: string; port?: number }) => this.proxy?.findBoundUdp(request) ?? null, }; this.processTable = { getSignalState: (pid: number) => this.proxy?.getSignalState(pid) ?? { handlers: new Map(), }, }; this.loopbackExemptPorts = [...(options.loopbackExemptPorts ?? [])]; for (const mount of options.mounts ?? []) { this.pendingLocalMounts.push({ path: normalizePath(mount.path), fs: mount.fs, readOnly: mount.readOnly ?? false, }); } this.vfs = new DeferredFileSystem(() => this.rootFilesystem); this.liveFilesystemBinding = bindLiveFilesystem( this.options.filesystem, () => this.rootFilesystem, options.maxFilesystemBytes ?? DEFAULT_LIVE_FILESYSTEM_SYNC_MAX_BYTES, ); } get zombieTimerCount(): number { return this.proxy?.zombieTimerCount ?? 0; } async getResourceSnapshot() { if (!this.proxy) { await this.ensureReady(); } if (!this.proxy) { throw new Error("kernel is not ready"); } return this.proxy.getResourceSnapshot(); } async mount(driver: KernelRuntimeDriver): Promise { await this.ensureReady(); if (!this.proxy || !this.client || !this.session || !this.vm) { throw new Error("kernel is not ready"); } await driver.init?.(this); if (driver.kind === "node") { for (const command of driver.commands) { this.commands.set(command, "node"); } this.mountedRuntimeDrivers.push(driver); await ensureCommandStubs(this.proxy, driver.commands); return; } const commandDirs = driver.commandDirs ?? []; if (commandDirs.length === 0) { for (const command of driver.commands) { this.commands.set(command, "wasmvm"); } this.mountedRuntimeDrivers.push(driver); await ensureCommandStubs(this.proxy, driver.commands); return; } const startIndex = this.mountedCommandDirs.length; const newGuestPaths = driver.getGuestCommandPaths?.(startIndex) ?? collectGuestCommandPaths(commandDirs, startIndex); const allCommandDirs = [...this.mountedCommandDirs, ...commandDirs]; const sidecarMounts = allCommandDirs.map((commandDir, index) => serializeMountConfigForSidecar({ path: `/__secure_exec/commands/${index}`, readOnly: true, plugin: { id: "host_dir", config: { hostPath: commandDir, readOnly: true, }, }, }), ); const localMounts = this.pendingLocalMounts.map((mount) => mount.fs instanceof NodeFileSystem ? serializeMountConfigForSidecar({ path: mount.path, readOnly: mount.readOnly, plugin: { id: "host_dir", config: { hostPath: mount.fs.rootPath, readOnly: mount.readOnly, }, }, }) : serializeMountConfigForSidecar({ path: mount.path, driver: mount.fs, readOnly: mount.readOnly, }), ); await this.client.configureVm(this.session, this.vm, { mounts: [...localMounts, ...sidecarMounts], loopbackExemptPorts: this.loopbackExemptPorts, }); this.proxy.registerCommandGuestPaths(newGuestPaths); this.mountedCommandDirs.push(...commandDirs); this.mountedRuntimeDrivers.push(driver); this.runtimeDriverCommandDirStarts.set(driver, startIndex); for (const command of newGuestPaths.keys()) { this.commands.set(command, "wasmvm"); } await ensureCommandStubs(this.proxy, newGuestPaths.keys()); } async dispose(): Promise { await this.readyPromise?.catch(() => {}); let syncError: unknown; if ( this.options.syncFilesystemOnDispose !== false && this.rootFilesystem && !(this.options.filesystem instanceof NodeFileSystem) ) { try { await this.liveFilesystemBinding.syncFromLive( this.liveFilesystemSyncRoots, ); } catch (error) { syncError = error; } } try { await this.proxy?.dispose().catch(() => {}); } finally { this.proxy = null; this.rootFilesystem = null; this.client = null; this.session = null; this.vm = null; this.liveFilesystemBinding.restore(); } if (syncError) { throw syncError; } } async exec( command: string, options?: KernelExecOptions, ): Promise { await this.ensureReady(); if (!this.proxy) { throw new Error("kernel is not ready"); } return this.proxy.exec(command, options); } spawn( command: string, args: string[], options?: KernelSpawnOptions, ): ManagedProcess { if (!this.proxy) { throw new Error("kernel is not ready; await kernel.mount(...) first"); } const normalized = normalizeCommandLookup(command); const knownCommand = this.commands.has(command) || this.commands.has(normalized); if (!knownCommand && !this.tryResolveMountedCommand(command)) { throw new Error(`ENOENT: command not found: ${command}`); } const proc = this.proxy.spawn(command, args, options); const syncProcessSnapshot = () => { const snapshot = this.proxy?.processes.get(proc.pid); if (!snapshot) { return; } this.processes.set(proc.pid, { ...snapshot, args: [...snapshot.args], }); }; syncProcessSnapshot(); return { pid: proc.pid, writeStdin(data) { proc.writeStdin(data); }, closeStdin() { proc.closeStdin(); }, kill(signal) { proc.kill(signal); syncProcessSnapshot(); }, async wait() { const exitCode = await proc.wait(); syncProcessSnapshot(); return exitCode; }, get exitCode() { return proc.exitCode; }, }; } openShell(options?: OpenShellOptions): ShellHandle { if (!this.proxy) { throw new Error("kernel is not ready; await kernel.mount(...) first"); } return this.proxy.openShell(options); } async connectTerminal(options?: ConnectTerminalOptions): Promise { await this.ensureReady(); if (!this.proxy) { throw new Error("kernel is not ready"); } return this.proxy.connectTerminal(options); } mountFs( mountPath: string, filesystem: VirtualFileSystem, options?: { readOnly?: boolean }, ): void { if (!this.proxy) { this.pendingLocalMounts.push({ path: normalizePath(mountPath), fs: filesystem, readOnly: options?.readOnly ?? false, }); return; } this.proxy.mountFs(mountPath, filesystem, options); } unmountFs(mountPath: string): void { this.proxy?.unmountFs(mountPath); } async readFile(targetPath: string): Promise { await this.ensureReady(); return this.proxy!.readFile(targetPath); } async vmFetch(request: { port: number; method: string; path: string; headersJson: string; body?: string; }): Promise { await this.ensureReady(); return this.proxy!.vmFetch(request); } async registerBindings( bindings: Record, ): Promise { await this.ensureReady(); if (!this.client || !this.session || !this.vm) { throw new Error("kernel is not ready"); } // Install the dispatcher once. It routes every host_callback request the // sidecar emits to the matching registered handler and replies with a // host_callback_result frame. if (!this.bindingRequestHandlerInstalled) { this.client.setSidecarRequestHandler((request: SidecarRequestFrame) => this.dispatchBindingRequest(request), ); this.bindingRequestHandlerInstalled = true; } for (const [name, binding] of Object.entries(bindings)) { this.bindingHandlers.set(name, binding.handler); const definition: SidecarRegisteredHostCallbackDefinition = { description: binding.description, inputSchema: binding.inputSchema, ...(binding.timeoutMs !== undefined ? { timeoutMs: binding.timeoutMs } : {}), ...(binding.examples && binding.examples.length > 0 ? { examples: binding.examples.map((example) => ({ description: example.description, input: example.input, })), } : {}), }; // Register each binding as its own single-callback binding collection so the guest // can invoke it directly by name (or by any caller-provided alias). The // sidecar exposes the binding collection name as a guest command; the single // callback carries the binding's schema and gates the `binding` // permission. await this.client.registerHostCallbacks(this.session, this.vm, { name, description: binding.description, commandAliases: [name, ...(binding.commandAliases ?? [])], callbacks: { [name]: definition }, }); this.commands.set(name, "wasmvm"); for (const alias of binding.commandAliases ?? []) { this.commands.set(alias, "wasmvm"); } } } private async dispatchBindingRequest( request: SidecarRequestFrame, ): Promise { const { payload } = request; if (payload.type !== "host_callback") { throw new Error( `unsupported sidecar request for bindings: ${payload.type}`, ); } // Callback keys arrive as `${collection}:${binding}` for collection invocations // and as the bare command name otherwise. The collection and binding name are // the same here, so the registered binding name is the segment after the last // colon (or the whole key when no colon is present). const callbackKey = payload.callback_key; const bindingName = callbackKey.includes(":") ? callbackKey.slice(callbackKey.lastIndexOf(":") + 1) : callbackKey; const handler = this.bindingHandlers.get(bindingName) ?? this.bindingHandlers.get(callbackKey); if (!handler) { return { type: "host_callback_result", invocation_id: payload.invocation_id, error: `no binding registered for ${callbackKey}`, }; } try { const result = await handler(payload.input); return { type: "host_callback_result", invocation_id: payload.invocation_id, result: result === undefined ? null : result, }; } catch (error) { return { type: "host_callback_result", invocation_id: payload.invocation_id, error: error instanceof Error ? error.message : String(error), }; } } async writeFile( targetPath: string, content: string | Uint8Array, ): Promise { await this.ensureReady(); return this.proxy!.writeFile(targetPath, content); } async mkdir(targetPath: string): Promise { await this.ensureReady(); return this.proxy!.mkdir(targetPath); } async readdir(targetPath: string): Promise { await this.ensureReady(); return this.proxy!.readdir(targetPath); } async stat(targetPath: string): Promise { await this.ensureReady(); return this.proxy!.stat(targetPath); } async exists(targetPath: string): Promise { await this.ensureReady(); return this.proxy!.exists(targetPath); } async removeFile(targetPath: string): Promise { await this.ensureReady(); return this.proxy!.removeFile(targetPath); } async removeDir(targetPath: string): Promise { await this.ensureReady(); return this.proxy!.removeDir(targetPath); } async rename(oldPath: string, newPath: string): Promise { await this.ensureReady(); return this.proxy!.rename(oldPath, newPath); } async pwrite( targetPath: string, offset: number, data: Uint8Array, ): Promise { await this.ensureReady(); return this.proxy!.vfs.pwrite(targetPath, offset, data); } private tryResolveMountedCommand(command: string): boolean { const normalized = normalizeCommandLookup(command); for (const driver of this.mountedRuntimeDrivers) { if (!driver.tryResolve?.(command)) { continue; } this.commands.set(normalized, driver.kind); if (driver.kind === "wasmvm" && this.proxy) { const startIndex = this.runtimeDriverCommandDirStarts.get(driver); if (startIndex !== undefined) { const guestPaths = driver.getGuestCommandPaths?.(startIndex); if (guestPaths?.has(normalized)) { this.proxy.registerCommandGuestPaths( new Map([[normalized, guestPaths.get(normalized)!]]), ); } } } return true; } return false; } private recordModuleExecution(command: string): void { for (const driver of this.mountedRuntimeDrivers) { driver.recordModuleExecution?.(command); } } private async ensureReady(): Promise { if (!this.readyPromise) { this.readyPromise = this.initialize(); } return this.readyPromise; } private async initialize(): Promise { const createVmEnv = { ...this.env }; const requestedPermissions = this.options.permissions; const bootstrapPermissions = requestedPermissions ? normalizePermissionsPolicy(allowAll) : undefined; if (this.loopbackExemptPorts.length > 0) { createVmEnv.AGENTOS_LOOPBACK_EXEMPT_PORTS = JSON.stringify( this.loopbackExemptPorts, ); } const rootPassthroughPlan = planNodeFilesystemPassthroughMounts( this.options.filesystem, this.pendingLocalMounts, ); const snapshotEntries = await this.measureBoot("filesystem_snapshot", () => snapshotFilesystemEntries(this.options.filesystem, "/", [], { passthroughDirectories: rootPassthroughPlan.passthroughDirectories, }), ); this.liveFilesystemSyncRoots = collectLiveFilesystemSyncRoots(snapshotEntries); const rootFilesystem = { mode: "ephemeral" as const, disableDefaultBaseLayer: true, lowers: [ { kind: "snapshot" as const, entries: mergeRootFilesystemEntries( createBootstrapEntries([...NODE_RUNTIME_BOOTSTRAP_COMMANDS]), snapshotEntries, ).map((entry) => ({ ...entry, executable: entry.executable ?? false, })), }, ], bootstrapEntries: [], }; const ownsClient = this.options.sidecar === undefined; const client = this.options.sidecar ?? this.measureSyncBoot("sidecar_spawn", () => SidecarProcess.spawn({ cwd: REPO_ROOT, command: ensureNativeSidecarBinary(), args: [], gracefulExitMs: 100, forceExitMs: 100, }), ); const session = await this.measureBoot("session_open", () => client.authenticateAndOpenSession(), ); const vm = await this.measureBoot("vm_create", () => client.createVm(session, { runtime: "java_script", config: { env: createVmEnv, ...(this.options.user ? { user: this.options.user } : {}), rootFilesystem, ...(bootstrapPermissions ? { permissions: bootstrapPermissions } : {}), loopbackExemptPorts: this.loopbackExemptPorts, ...(this.options.limits ? { limits: this.options.limits } : {}), ...(this.options.jsRuntime ? { jsRuntime: this.options.jsRuntime as JsRuntimeConfig } : {}), }, }), ); await this.measureBoot("vm_ready", () => client.waitForEvent( { type: "vm_lifecycle", ownership: { scope: "vm", connection_id: session.connectionId, session_id: session.sessionId, vm_id: vm.vmId, }, state: "ready", }, 10_000, ), ); if (requestedPermissions && snapshotEntries.length > 1) { await materializeSnapshotEntriesIntoVm( client, session, vm, snapshotEntries, ); } if (rootPassthroughPlan.mounts.length > 0) { this.pendingLocalMounts.push(...rootPassthroughPlan.mounts); } if ( this.pendingLocalMounts.length > 0 || this.loopbackExemptPorts.length > 0 || requestedPermissions ) { await this.measureBoot("vm_configure", () => client.configureVm(session, vm, { mounts: this.pendingLocalMounts.map((mount) => mount.fs instanceof NodeFileSystem ? serializeMountConfigForSidecar({ path: mount.path, readOnly: mount.readOnly, plugin: { id: "host_dir", config: { hostPath: mount.fs.rootPath, readOnly: mount.readOnly, }, }, }) : serializeMountConfigForSidecar({ path: mount.path, driver: mount.fs, readOnly: mount.readOnly, }), ), permissions: requestedPermissions, loopbackExemptPorts: this.loopbackExemptPorts, }), ); } const proxy = new NativeSidecarKernelProxy({ client, session, vm, disposeClient: ownsClient, env: this.env, cwd: this.cwd, defaultExecCwd: this.options.cwd === undefined ? "/workspace" : this.cwd, localMounts: this.pendingLocalMounts, commandGuestPaths: new Map(), onWasmCommandResolved: (command) => { this.recordModuleExecution(command); }, }); this.client = client; this.session = session; this.vm = vm; this.proxy = proxy; this.rootFilesystem = proxy.createRootView(); } private async measureBoot( phase: KernelBootTimingPhase, fn: () => Promise, ): Promise { const start = performance.now(); try { return await fn(); } finally { this.options.onBootTiming?.({ phase, durationMs: performance.now() - start, }); } } private measureSyncBoot(phase: KernelBootTimingPhase, fn: () => T): T { const start = performance.now(); try { return fn(); } finally { this.options.onBootTiming?.({ phase, durationMs: performance.now() - start, }); } } } export function createKernel(options: { filesystem: VirtualFileSystem; permissions?: Permissions; env?: Record; cwd?: string; user?: VmUserConfig; sidecar?: SidecarProcess; onBootTiming?: (timing: KernelBootTiming) => void; maxProcesses?: number; hostNetworkAdapter?: unknown; loopbackExemptPorts?: number[]; jsRuntime?: Partial; limits?: VmLimitsConfig; logger?: unknown; mounts?: Array<{ path: string; fs: VirtualFileSystem; readOnly?: boolean }>; syncFilesystemOnDispose?: boolean; maxFilesystemBytes?: number; }): Kernel { return new NativeKernel(options); } function errnoError(code: string, message: string): KernelError { return new KernelError(code, `${code}: ${message}`); }