import { createHash } from "node:crypto"; import path from "node:path"; import type { AdapterContext, ComputerAction, ComputerObservation, ComputerRef, PortableFileStream, SandboxProvider, } from "@rakazo/adapter-kit"; import type { ComputerMode } from "@rakazo/contracts"; import { collectBytes, discardByteStream } from "./byte-streams.js"; export function toComputerRef(computer: { homeKey: string; kind: string; providerRef: string | null; }): ComputerRef { if (!computer.providerRef) throw new Error("computer provider reference is missing"); return { id: computer.providerRef, botId: computer.homeKey, kind: computer.kind as ComputerRef["kind"], providerRef: computer.providerRef, }; } /** * Remote computers move files as base64 JSON (the Docker supervisor accepts 16 MiB requests), so * files copied to or read from them stay below this size. Local computers have no transfer cap. */ export const REMOTE_COMPUTER_FILE_TRANSFER_MAX_BYTES = 12 * 1024 * 1024; export function computerFileTransferMaxBytes(computer: Pick): number { return computer.kind === "desktop" || computer.kind === "fake" ? Number.POSITIVE_INFINITY : REMOTE_COMPUTER_FILE_TRANSFER_MAX_BYTES; } /** Streams a file into the workspace; providers without streaming writes receive its bytes. */ export async function writeComputerFileStream( sandbox: SandboxProvider, computer: ComputerRef, file: PortableFileStream, context: AdapterContext, ): Promise { try { if (sandbox.writeFileStream) { await sandbox.writeFileStream(computer, file, context); return; } const content = await collectBytes(file.stream); await sandbox.writeFile( computer, { path: file.path, content, ...(file.executable ? { executable: true } : {}) }, context, ); } catch (error) { discardByteStream(file.stream); throw error; } } /** Opens a workspace file as a stream; providers without streaming reads return it in one chunk. */ export async function readComputerFileStream( sandbox: SandboxProvider, computer: ComputerRef, filePath: string, context: AdapterContext, options?: { maxBytes?: number }, ): Promise> { if (sandbox.readFileStream) return sandbox.readFileStream(computer, filePath, context, options); const bytes = await sandbox.readFile(computer, filePath, context, options); return new ReadableStream({ start(controller) { controller.enqueue(bytes); controller.close(); }, }); } const EMPTY_PNG = Uint8Array.from( Buffer.from( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=", "base64", ), ); export function computerObservation( image: Uint8Array, input: Omit, ): ComputerObservation { return { ...input, image, frameId: createHash("sha256").update(image).digest("hex"), capturedAt: new Date().toISOString(), }; } export function placeholderObservation(label = "ready"): ComputerObservation { return computerObservation(EMPTY_PNG, { mimeType: "image/png", width: 1, height: 1, activeWindow: { id: "placeholder", title: label }, }); } export function applyPlaceholderAction(box: { screen: string }, action: ComputerAction): void { if (action.kind === "clipboard") box.screen = action.text; else if (action.kind === "open") box.screen = `opened ${action.path}`; else if (action.kind === "launch") box.screen = `launched ${action.application}`; else box.screen = action.kind; } export function boundedComputerActions(actions: readonly ComputerAction[]): ComputerAction[] { if (actions.length > 24) throw new Error("computer action batch exceeds 24 actions"); return [...actions]; } export function clampRounded(value: number, min: number, max: number): number { return Math.min(Math.max(Math.round(value), min), max); } export function normalizeWorkspacePath(value: string): string { const normalized = value.replace(/\\/g, "/").replace(/^\/+/, ""); const rawSegments = normalized.split("/").filter(Boolean); if (rawSegments.some((segment) => segment === "..")) { throw new Error("Path escapes the computer workspace"); } const segments = rawSegments.filter((segment) => segment !== "."); return segments.join("/"); } export function workspacePath(root: string, relative: string): string { const normalized = normalizeWorkspacePath(relative); return normalized ? path.posix.join(root, normalized) : root; } export function teamBotWorkspaceDirectory(botId: string): string { return path.posix.join("bots", normalizeWorkspacePath(botId)); } const TEAM_WORKSPACE_ESCAPE = "Path escapes the bot computer workspace"; /** Bot home segment under `bots//…`, or null for shared/root paths. */ export function teamWorkspaceTargetBotId(storedPath: string): string | null { const normalized = normalizeWorkspacePath(storedPath); if (normalized === "bots" || !normalized.startsWith("bots/")) return null; const remainder = normalized.slice("bots/".length); if (!remainder) return null; const [botId] = remainder.split("/"); return botId || null; } export function assertTeamWorkspacePathAllowed( storedPath: string, allowedBotIds: ReadonlySet, ): void { const targetBotId = teamWorkspaceTargetBotId(storedPath); if (!targetBotId || allowedBotIds.has(targetBotId)) return; throw new Error(TEAM_WORKSPACE_ESCAPE); } export function assertTeamShellCommandAllowed( command: string, allowedBotIds: ReadonlySet, ): void { for (const match of command.matchAll(/\bbots\/([A-Za-z0-9_-]+)/g)) { const botId = match[1]; if (botId && !allowedBotIds.has(botId)) { throw new Error(TEAM_WORKSPACE_ESCAPE); } } } export function filterTeamWorkspaceEntries( entries: readonly T[], allowedBotIds: ReadonlySet, ): T[] { return entries.filter((entry) => { const targetBotId = teamWorkspaceTargetBotId(entry.path); return !targetBotId || allowedBotIds.has(targetBotId); }); } export function resolveBotWorkspacePath( scope: ComputerMode, botId: string, requestedPath: string, ): string { if (scope !== "team") return requestedPath; const explicitRootPath = stripVirtualWorkspaceRoot(requestedPath); const normalized = normalizeWorkspacePath(explicitRootPath ?? requestedPath); if (explicitRootPath !== null || isTeamRootPath(normalized)) return normalized; return path.posix.join(teamBotWorkspaceDirectory(botId), normalized); } export function resolveBotWorkspaceCwd( scope: ComputerMode, botId: string, requestedCwd: string | undefined, ): string | undefined { if (scope !== "team") return requestedCwd; if (!requestedCwd || requestedCwd === ".") return teamBotWorkspaceDirectory(botId); if (requestedCwd.startsWith("/")) return requestedCwd; return resolveBotWorkspacePath(scope, botId, requestedCwd); } export function displayBotWorkspacePath( scope: ComputerMode, botId: string, requestedPath: string, storedPath: string, ): string { if (scope !== "team" || !isBotRelativeRequest(requestedPath)) return storedPath; const normalized = normalizeWorkspacePath(storedPath); const botDirectory = teamBotWorkspaceDirectory(botId); if (normalized === botDirectory) return ""; return normalized.startsWith(`${botDirectory}/`) ? normalized.slice(botDirectory.length + 1) : normalized; } function isBotRelativeRequest(value: string): boolean { if (stripVirtualWorkspaceRoot(value) !== null) return false; return !isTeamRootPath(normalizeWorkspacePath(value)); } function isTeamRootPath(value: string): boolean { return ( value === "shared" || value.startsWith("shared/") || value === "project" || value.startsWith("project/") || value === "bots" || value.startsWith("bots/") ); } function stripVirtualWorkspaceRoot(value: string): string | null { const portable = value.replace(/\\/g, "/"); if (portable === "/") return ""; return portable.startsWith("/") ? portable.slice(1) : null; } export { shellQuote } from "@rakazo/core/node/desktop-runtime";