// Jupyter notebook canvas implementation. import { createServer } from "node:http"; import { spawn } from "node:child_process"; import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto"; import { mkdir, mkdtemp, readFile, readdir, rename, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { CanvasError, createCanvas, joinSession } from "@github/copilot-sdk/extension"; const extensionRoot = path.dirname(fileURLToPath(import.meta.url)); const servers = new Map(); const clientsByNotebook = new Map(); const notebookQueues = new Map(); const activeRuns = new Map(); const workspaceSnapshots = new Map(); const historyByNotebook = new Map(); let workspaceRoot; let notebooksDir; let checkpointsDir; let storageReady = Promise.resolve(); const notebookIdPattern = "^[A-Za-z0-9][A-Za-z0-9._-]{0,80}$"; const cellIdPattern = "^[A-Za-z0-9_-]{1,64}$"; const notebookIdRegex = new RegExp(notebookIdPattern); const cellIdRegex = new RegExp(cellIdPattern); const openInputSchema = { type: "object", additionalProperties: false, properties: { notebookId: { type: "string", pattern: notebookIdPattern }, title: { type: "string", minLength: 1, maxLength: 120 }, }, }; const notebookActionSchema = { type: "object", additionalProperties: false, properties: { notebookId: { type: "string", pattern: notebookIdPattern }, }, }; function cellIdSchema(required = true) { return { type: "object", additionalProperties: false, required: required ? ["cellId"] : [], properties: { notebookId: { type: "string", pattern: notebookIdPattern }, cellId: { type: "string", pattern: cellIdPattern }, }, }; } function textToSource(value) { return typeof value === "string" ? value : ""; } async function migrateLegacyDirectory(source, destination) { let entries; try { entries = await readdir(source, { withFileTypes: true }); } catch (error) { if (error?.code === "ENOENT") { return; } throw error; } await mkdir(destination, { recursive: true }); for (const entry of entries) { const sourcePath = path.join(source, entry.name); const destinationPath = path.join(destination, entry.name); if (entry.isDirectory()) { await migrateLegacyDirectory(sourcePath, destinationPath); continue; } if (!entry.isFile()) { continue; } try { await readFile(destinationPath); await rm(sourcePath, { force: true }); } catch (error) { if (error?.code !== "ENOENT") { throw error; } try { await rename(sourcePath, destinationPath); } catch (renameError) { if (renameError?.code !== "EXDEV") { throw renameError; } await writeFile(destinationPath, await readFile(sourcePath), { mode: 0o600 }); await rm(sourcePath, { force: true }); } } } await rm(source, { force: true, recursive: true }); } async function initializeStorage(sessionWorkspacePath) { if (!sessionWorkspacePath) { throw new Error("Notebook canvas requires session workspace storage."); } workspaceRoot = path.resolve(sessionWorkspacePath); const dataRoot = path.join(workspaceRoot, ".notebook-canvas"); notebooksDir = path.join(dataRoot, "notebooks"); checkpointsDir = path.join(dataRoot, "checkpoints"); await mkdir(dataRoot, { recursive: true }); await migrateLegacyDirectory(path.join(extensionRoot, "notebooks"), notebooksDir); await migrateLegacyDirectory(path.join(extensionRoot, "checkpoints"), checkpointsDir); } function createCellId(usedIds = new Set()) { let id; do { id = `cell-${randomUUID().slice(0, 8)}`; } while (usedIds.has(id)); return id; } function getCellId(cell, usedIds) { const candidates = [cell.id, cell.metadata?.copilotCellId]; let id = candidates.find((candidate) => ( typeof candidate === "string" && cellIdRegex.test(candidate) && (!usedIds || !usedIds.has(candidate)) )); if (!id) { id = createCellId(usedIds); } cell.id = id; cell.metadata = { ...(cell.metadata ?? {}), copilotCellId: id }; usedIds?.add(id); return id; } function normalizeSource(source) { if (Array.isArray(source)) { return source.join(""); } return typeof source === "string" ? source : ""; } function normalizeNotebook(raw, notebookId, title, createStarter = false) { const notebook = raw && typeof raw === "object" ? raw : {}; let changed = !raw || typeof raw !== "object"; const originalMetadata = notebook.metadata && typeof notebook.metadata === "object" ? notebook.metadata : {}; const originalCopilot = originalMetadata.copilot && typeof originalMetadata.copilot === "object" ? originalMetadata.copilot : {}; changed ||= notebook.nbformat !== 4; changed ||= typeof notebook.nbformat_minor !== "number"; changed ||= !originalMetadata.kernelspec || !originalMetadata.language_info; changed ||= originalCopilot.notebookId !== notebookId; changed ||= typeof originalCopilot.title !== "string"; changed ||= !Number.isInteger(originalCopilot.executionCount); changed ||= !Number.isInteger(originalCopilot.revision); notebook.nbformat = 4; notebook.nbformat_minor = typeof notebook.nbformat_minor === "number" ? notebook.nbformat_minor : 5; notebook.metadata = { ...originalMetadata, kernelspec: originalMetadata.kernelspec ?? { display_name: "Python 3", language: "python", name: "python3", }, language_info: originalMetadata.language_info ?? { name: "python", file_extension: ".py", mimetype: "text/x-python", }, copilot: { ...originalCopilot, notebookId, title: originalCopilot.title ?? title ?? "Untitled notebook", executionCount: Number(originalCopilot.executionCount ?? 0), revision: Number.isInteger(originalCopilot.revision) ? originalCopilot.revision : 0, }, }; if (!Array.isArray(notebook.cells)) { notebook.cells = []; changed = true; } const usedIds = new Set(); for (const cell of notebook.cells) { if (!["code", "markdown", "raw"].includes(cell.cell_type)) { cell.cell_type = "raw"; changed = true; } cell.metadata = cell.metadata && typeof cell.metadata === "object" ? cell.metadata : {}; const originalId = cell.id; getCellId(cell, usedIds); changed ||= originalId !== cell.id; if (Array.isArray(cell.source) || typeof cell.source !== "string") { cell.source = normalizeSource(cell.source); changed = true; } if (cell.cell_type === "code") { if (!Array.isArray(cell.outputs)) { cell.outputs = []; changed = true; } if (!Number.isInteger(cell.execution_count) && cell.execution_count !== null) { cell.execution_count = null; changed = true; } } else { if ("outputs" in cell || "execution_count" in cell) { changed = true; } delete cell.outputs; delete cell.execution_count; } } if (createStarter && notebook.cells.length === 0) { notebook.cells.push(createCell("code", "print(\"Hello from the notebook canvas\")")); changed = true; } return { notebook, changed }; } function notebookPath(notebookId) { if (typeof notebookId !== "string" || !notebookIdRegex.test(notebookId)) { throw new CanvasError("notebook_id_invalid", "Notebook ID contains unsupported characters."); } return path.join(notebooksDir, `${notebookId}.ipynb`); } function serializeNotebook(notebook) { return `${JSON.stringify(notebook, null, 2)}\n`; } function contentHash(content) { return createHash("sha256").update(content).digest("hex"); } function normalizeWorkspacePath(relativePath) { if (!workspaceRoot) { throw new CanvasError("workspace_unavailable", "This session does not expose a workspace."); } if (typeof relativePath !== "string" || relativePath.length === 0 || relativePath.length > 240) { throw new CanvasError("workspace_path_invalid", "Choose a workspace-relative .ipynb path."); } const normalized = relativePath.replaceAll("\\", "/").replace(/^\.\//, ""); if ( path.posix.isAbsolute(normalized) || normalized.split("/").some((segment) => segment === ".." || segment.length === 0) || !normalized.toLowerCase().endsWith(".ipynb") ) { throw new CanvasError("workspace_path_invalid", "Notebook paths must stay inside the workspace and end in .ipynb."); } const resolved = path.resolve(workspaceRoot, ...normalized.split("/")); const rootWithSeparator = `${path.resolve(workspaceRoot)}${path.sep}`; if (!resolved.startsWith(rootWithSeparator)) { throw new CanvasError("workspace_path_invalid", "Notebook path escapes the workspace."); } return { relative: normalized, resolved }; } async function readWorkspaceNotebook(relativePath) { const target = normalizeWorkspacePath(relativePath); const content = await readFile(target.resolved, "utf8"); let parsed; try { parsed = JSON.parse(content); } catch { throw new CanvasError("notebook_json_invalid", `${target.relative} is not a valid notebook JSON file.`); } return { ...target, content, parsed }; } async function assertWorkspaceUnchanged(notebookId, notebook) { const relativePath = notebook.metadata?.copilot?.workspacePath; if (!relativePath) { return; } const target = normalizeWorkspacePath(relativePath); const expected = workspaceSnapshots.get(notebookId) ?? contentHash(serializeNotebook(notebook)); try { const actual = contentHash(await readFile(target.resolved, "utf8")); if (actual !== expected) { throw new CanvasError( "workspace_file_conflict", `${target.relative} changed outside the canvas. Open it again or save to a different path.`, ); } } catch (error) { if (error?.code === "ENOENT") { throw new CanvasError("workspace_file_missing", `${target.relative} was removed outside the canvas.`); } throw error; } } async function writeWorkspaceDocument(relativePath, content, { overwrite = true } = {}) { const target = normalizeWorkspacePath(relativePath); if (!overwrite) { try { await readFile(target.resolved, "utf8"); throw new CanvasError("workspace_file_exists", `${target.relative} already exists.`); } catch (error) { if (error?.code !== "ENOENT") { throw error; } } } await mkdir(path.dirname(target.resolved), { recursive: true }); const temporary = path.join(path.dirname(target.resolved), `.${path.basename(target.resolved)}.${randomUUID()}.tmp`); try { await writeFile(temporary, content, { encoding: "utf8", mode: 0o600 }); await rename(temporary, target.resolved); } finally { await rm(temporary, { force: true }); } return target; } async function writeWorkspaceCopy(notebookId, notebook, { overwrite = true } = {}) { const relativePath = notebook.metadata?.copilot?.workspacePath; if (!relativePath) { return; } const content = serializeNotebook(notebook); await writeWorkspaceDocument(relativePath, content, { overwrite }); workspaceSnapshots.set(notebookId, contentHash(content)); } async function assertWorkspaceTargetAvailable(relativePath, overwrite) { const target = normalizeWorkspacePath(relativePath); if (overwrite) { return target; } try { await readFile(target.resolved, "utf8"); throw new CanvasError("workspace_file_exists", `${target.relative} already exists.`); } catch (error) { if (error?.code === "ENOENT") { return target; } throw error; } } async function listWorkspaceNotebooks() { if (!workspaceRoot) { return []; } const results = []; async function visit(directory, depth) { if (depth > 4 || results.length >= 100) { return; } const entries = await readdir(directory, { withFileTypes: true }); for (const entry of entries) { if (entry.name.startsWith(".") || ["node_modules", "dist", "build"].includes(entry.name)) { continue; } const absolute = path.join(directory, entry.name); if (entry.isDirectory()) { await visit(absolute, depth + 1); } else if (entry.isFile() && entry.name.toLowerCase().endsWith(".ipynb")) { results.push(path.relative(workspaceRoot, absolute).split(path.sep).join("/")); } } } await visit(workspaceRoot, 0); return results.sort((a, b) => a.localeCompare(b)); } function checkpointPath(notebookId, checkpointId) { if (!notebookIdRegex.test(notebookId) || !/^[A-Za-z0-9_-]{1,80}$/.test(checkpointId)) { throw new CanvasError("checkpoint_id_invalid", "Invalid checkpoint identifier."); } return path.join(checkpointsDir, notebookId, `${checkpointId}.json`); } async function createCheckpoint(notebookId, notebook, label) { const checkpointId = `cp-${Date.now()}-${randomUUID().slice(0, 6)}`; const record = { checkpointId, createdAt: new Date().toISOString(), label: textToSource(label).slice(0, 80) || "Manual checkpoint", notebook, }; const target = checkpointPath(notebookId, checkpointId); await mkdir(path.dirname(target), { recursive: true }); await writeFile(target, serializeNotebook(record), { encoding: "utf8", mode: 0o600 }); return { checkpointId, createdAt: record.createdAt, label: record.label }; } async function listCheckpoints(notebookId) { const directory = path.join(checkpointsDir, notebookId); try { const files = (await readdir(directory)).filter((name) => name.endsWith(".json")); const checkpoints = []; for (const file of files) { const record = JSON.parse(await readFile(path.join(directory, file), "utf8")); checkpoints.push({ checkpointId: record.checkpointId, createdAt: record.createdAt, label: record.label, }); } return checkpoints.sort((a, b) => b.createdAt.localeCompare(a.createdAt)).slice(0, 50); } catch (error) { if (error?.code === "ENOENT") { return []; } throw error; } } async function getAvailableRuntimes() { const runtimes = []; for (const command of ["python3", "python"]) { const version = await new Promise((resolve) => { const child = spawn(command, ["--version"], { env: { PATH: process.env.PATH ?? "/usr/bin:/bin" }, stdio: ["ignore", "pipe", "pipe"], }); let output = ""; child.stdout.on("data", (chunk) => { output += chunk; }); child.stderr.on("data", (chunk) => { output += chunk; }); child.on("error", () => resolve(null)); child.on("close", (code) => resolve(code === 0 ? output.trim() : null)); }); if (version) { runtimes.push({ id: command, label: version }); } } return runtimes; } async function applyHistory(notebookId, direction, expectedRevision) { return withNotebookLock(notebookId, async () => { const current = await loadNotebookUnlocked(notebookId); const currentRevision = Number(current.metadata.copilot.revision ?? 0); if (Number.isInteger(expectedRevision) && expectedRevision !== currentRevision) { throw new CanvasError("revision_conflict", "Notebook changed before history could be applied."); } await assertWorkspaceUnchanged(notebookId, current); const history = historyByNotebook.get(notebookId) ?? { redo: [], undo: [] }; const source = direction === "undo" ? history.undo : history.redo; const destination = direction === "undo" ? history.redo : history.undo; const snapshot = source.pop(); if (!snapshot) { throw new CanvasError("history_empty", `Nothing to ${direction}.`); } destination.push(structuredClone(current)); const workspacePath = current.metadata?.copilot?.workspacePath; if (workspacePath) { snapshot.metadata.copilot.workspacePath = workspacePath; } else { delete snapshot.metadata.copilot.workspacePath; } snapshot.metadata.copilot.revision = currentRevision + 1; historyByNotebook.set(notebookId, history); await writeNotebookUnlocked(notebookId, snapshot); await writeWorkspaceCopy(notebookId, snapshot); broadcast(notebookId, snapshot); return { notebookId, notebook: summarizeNotebook(snapshot) }; }); } function withNotebookLock(notebookId, operation) { const previous = notebookQueues.get(notebookId) ?? Promise.resolve(); const current = previous.catch(() => {}).then(operation); notebookQueues.set(notebookId, current); return current.finally(() => { if (notebookQueues.get(notebookId) === current) { notebookQueues.delete(notebookId); } }); } async function writeNotebookUnlocked(notebookId, notebook) { await mkdir(notebooksDir, { recursive: true }); const target = notebookPath(notebookId); const temporary = path.join(notebooksDir, `.${notebookId}.${randomUUID()}.tmp`); try { await writeFile(temporary, serializeNotebook(notebook), { encoding: "utf8", mode: 0o600 }); await rename(temporary, target); } finally { await rm(temporary, { force: true }); } } async function loadNotebookUnlocked(notebookId = "default", title) { await mkdir(notebooksDir, { recursive: true }); try { const raw = JSON.parse(await readFile(notebookPath(notebookId), "utf8")); const normalized = normalizeNotebook(raw, notebookId, title); if (normalized.notebook.metadata?.copilot?.workspacePath) { workspaceSnapshots.set(notebookId, contentHash(serializeNotebook(normalized.notebook))); } if (normalized.changed) { await writeNotebookUnlocked(notebookId, normalized.notebook); } return normalized.notebook; } catch (error) { if (error?.code !== "ENOENT") { throw error; } const { notebook } = normalizeNotebook({}, notebookId, title, true); await writeNotebookUnlocked(notebookId, notebook); return notebook; } } function loadNotebook(notebookId = "default", title) { return withNotebookLock(notebookId, () => loadNotebookUnlocked(notebookId, title)); } function createCell(cellType, source = "") { const normalizedCellType = ["code", "markdown", "raw"].includes(cellType) ? cellType : "code"; const cell = { id: `cell-${randomUUID().slice(0, 8)}`, cell_type: normalizedCellType, metadata: {}, source: textToSource(source), }; if (cell.cell_type === "code") { cell.execution_count = null; cell.outputs = []; } return cell; } function summarizeNotebook(notebook) { const language = notebook.metadata?.language_info?.name ?? notebook.metadata?.kernelspec?.language ?? "unknown"; return { title: notebook.metadata?.copilot?.title ?? "Untitled notebook", revision: Number(notebook.metadata?.copilot?.revision ?? 0), language, runtimeLabel: language.toLowerCase() === "python" ? "Isolated Python" : "Execution unavailable", executionMode: "stateless-run-through", workspacePath: notebook.metadata?.copilot?.workspacePath ?? null, runtime: notebook.metadata?.copilot?.runtime ?? "python3", cells: notebook.cells.map((cell) => ({ id: getCellId(cell), cellType: cell.cell_type, source: normalizeSource(cell.source), executionCount: cell.execution_count ?? null, outputs: cell.outputs ?? [], })), }; } function resolveNotebookId(ctx, input) { const fromInput = input && typeof input.notebookId === "string" ? input.notebookId : undefined; const fromBoundServer = ctx.notebookId ?? servers.get(ctx.instanceId)?.notebookId; if (fromBoundServer && fromInput && fromBoundServer !== fromInput) { throw new CanvasError("notebook_mismatch", "This canvas instance is bound to a different notebook."); } return fromBoundServer ?? fromInput ?? "default"; } function mutateNotebook(notebookId, mutator, expectedRevision, options = {}) { return withNotebookLock(notebookId, async () => { const notebook = await loadNotebookUnlocked(notebookId); const currentRevision = Number(notebook.metadata.copilot.revision ?? 0); if (Number.isInteger(expectedRevision) && expectedRevision !== currentRevision) { throw new CanvasError( "revision_conflict", `Notebook changed since revision ${expectedRevision}; current revision is ${currentRevision}. Your draft was not overwritten.`, ); } if (!options.skipWorkspacePreflight) { await assertWorkspaceUnchanged(notebookId, notebook); } const history = historyByNotebook.get(notebookId) ?? { redo: [], undo: [] }; history.undo.push(structuredClone(notebook)); history.undo = history.undo.slice(-50); history.redo = []; historyByNotebook.set(notebookId, history); const result = await mutator(notebook); notebook.metadata.copilot.revision = currentRevision + 1; await writeNotebookUnlocked(notebookId, notebook); await writeWorkspaceCopy(notebookId, notebook, { overwrite: options.workspaceOverwrite ?? true }); broadcast(notebookId, notebook); return { notebookId, notebook: summarizeNotebook(notebook), ...(result ?? {}) }; }); } function findCell(notebook, cellId) { const cell = notebook.cells.find((candidate) => getCellId(candidate) === cellId); if (!cell) { throw new CanvasError("cell_not_found", `No cell found with id ${cellId}`); } return cell; } function findCellIndex(notebook, cellId) { const index = notebook.cells.findIndex((candidate) => getCellId(candidate) === cellId); if (index === -1) { throw new CanvasError("cell_not_found", `No cell found with id ${cellId}`); } return index; } function outputFromRun(result) { const outputs = []; if (result.stdout) { outputs.push({ output_type: "stream", name: "stdout", text: result.stdout }); } if (result.stderr) { outputs.push({ output_type: "stream", name: "stderr", text: result.stderr }); } if (result.resultData && Object.keys(result.resultData).length > 0) { outputs.push({ output_type: "execute_result", execution_count: result.executionCount, data: result.resultData, metadata: {}, }); } for (const display of result.displays ?? []) { outputs.push({ output_type: "display_data", data: display, metadata: {}, }); } if (result.error) { outputs.push({ output_type: "error", ename: result.error.ename, evalue: result.error.evalue, traceback: result.error.traceback, }); } return outputs; } const pythonRunner = String.raw` import ast import base64 import contextlib import io import json import os import pathlib import sys import traceback try: import resource except ImportError: resource = None payload = json.load(sys.stdin) namespace = {} results = [] MAX_CAPTURE = 250_000 WORKSPACE = pathlib.Path.cwd().resolve() if resource is not None: try: resource.setrlimit(resource.RLIMIT_CPU, (25, 30)) resource.setrlimit(resource.RLIMIT_FSIZE, (10 * 1024 * 1024, 10 * 1024 * 1024)) resource.setrlimit(resource.RLIMIT_NOFILE, (64, 64)) except (AttributeError, OSError, ValueError): pass READ_ROOTS = [WORKSPACE] for entry in [sys.base_prefix, sys.prefix, *sys.path]: if entry: try: READ_ROOTS.append(pathlib.Path(entry).resolve()) except (OSError, TypeError): pass class LimitedBuffer(io.StringIO): def __init__(self, limit): super().__init__() self.limit = limit self.written = 0 self.truncated = False def write(self, value): value = str(value) remaining = self.limit - self.written if remaining <= 0: self.truncated = True return len(value) chunk = value[:remaining] self.written += len(chunk) if len(chunk) < len(value): self.truncated = True super().write(chunk) return len(value) def value(self): suffix = "\n[output truncated]" if self.truncated else "" return self.getvalue() + suffix def resolve_path(value): if isinstance(value, int): return None try: return pathlib.Path(value).resolve() except (OSError, TypeError, ValueError): raise PermissionError("Unable to resolve filesystem path") def under(path_value, roots): if path_value is None: return True return any(path_value == root or root in path_value.parents for root in roots) def audit(event, args): if event.startswith(("socket.", "subprocess.", "ctypes.", "pty.")) or event in { "os.system", "os.posix_spawn", "os.spawn", "os.fork", "os.forkpty" }: raise PermissionError(f"{event} is disabled in notebook execution") if event == "open": target = resolve_path(args[0]) mode = str(args[1]) if len(args) > 1 else "r" write_mode = any(flag in mode for flag in ("w", "a", "x", "+")) roots = [WORKSPACE] if write_mode else READ_ROOTS if not under(target, roots): raise PermissionError("Filesystem access outside the isolated workspace is disabled") if event in {"os.listdir", "os.scandir", "os.chdir"} and args: if not under(resolve_path(args[0]), READ_ROOTS): raise PermissionError("Directory access outside the isolated workspace is disabled") if event in { "os.remove", "os.rmdir", "os.mkdir", "os.chmod", "os.chown", "os.truncate", "os.link", "os.symlink" } and args: if not under(resolve_path(args[0]), [WORKSPACE]): raise PermissionError("Filesystem changes outside the isolated workspace are disabled") if event in {"os.rename", "os.replace"}: if any(not under(resolve_path(value), [WORKSPACE]) for value in args[:2]): raise PermissionError("Filesystem changes outside the isolated workspace are disabled") sys.addaudithook(audit) def rich_repr(value): data = {} if value is None: return data try: html_method = getattr(value, "_repr_html_", None) if callable(html_method): html = html_method() if html: data["text/html"] = str(html)[:MAX_CAPTURE] except Exception: pass try: png_method = getattr(value, "_repr_png_", None) if callable(png_method): png = png_method() if png: if isinstance(png, tuple): png = png[0] data["image/png"] = base64.b64encode(png).decode("ascii") except Exception: pass rendered = repr(value) data["text/plain"] = rendered[:MAX_CAPTURE] if len(rendered) > MAX_CAPTURE: data["text/plain"] += "\n[result truncated]" return data def capture_figures(): displays = [] pyplot = sys.modules.get("matplotlib.pyplot") if pyplot is None: return displays for number in pyplot.get_fignums(): buffer = io.BytesIO() pyplot.figure(number).savefig(buffer, format="png", bbox_inches="tight") displays.append({"image/png": base64.b64encode(buffer.getvalue()).decode("ascii")}) buffer.close() pyplot.close("all") return displays def run_cell(cell, index): stdout = LimitedBuffer(MAX_CAPTURE) stderr = LimitedBuffer(MAX_CAPTURE) result_data = {} displays = [] error = None code = cell.get("code") or "" try: tree = ast.parse(code, filename=f"", mode="exec") with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr): if tree.body and isinstance(tree.body[-1], ast.Expr): last_expr = ast.Expression(tree.body[-1].value) tree.body = tree.body[:-1] ast.fix_missing_locations(tree) ast.fix_missing_locations(last_expr) if tree.body: exec(compile(tree, f"", "exec"), namespace, namespace) value = eval(compile(last_expr, f"", "eval"), namespace, namespace) result_data = rich_repr(value) else: exec(compile(tree, f"", "exec"), namespace, namespace) displays = capture_figures() except Exception as exc: error = { "ename": exc.__class__.__name__, "evalue": str(exc), "traceback": traceback.format_exc().splitlines(), } return { "id": cell.get("id"), "stdout": stdout.value(), "stderr": stderr.value(), "resultData": result_data, "displays": displays, "error": error, } for index, cell in enumerate(payload.get("cells", [])): results.append(run_cell(cell, index)) if results[-1]["error"] is not None and payload.get("stopOnError", True): break print(json.dumps({"results": results})) `; async function runPython(payload, notebookId, preferredRuntime) { const executionDir = await mkdtemp(path.join(tmpdir(), "copilot-notebook-")); const errors = []; const commands = preferredRuntime ? [preferredRuntime] : ["python3", "python"]; try { for (const command of commands) { try { return await runPythonCommand(command, payload, notebookId, executionDir); } catch (error) { if (error?.code === "ENOENT") { errors.push(`${command}: not found`); continue; } throw error; } } throw new CanvasError("python_not_found", `Unable to run notebook cells because Python was not found (${errors.join(", ")}).`); } finally { await rm(executionDir, { recursive: true, force: true }); } } function terminateProcessTree(child, detached, signal) { try { if (detached && child.pid) { process.kill(-child.pid, signal); } else { child.kill(signal); } } catch (error) { if (error?.code !== "ESRCH") { throw error; } } } function runPythonCommand(command, payload, notebookId, executionDir) { return new Promise((resolve, reject) => { const detached = process.platform !== "win32"; const child = spawn(command, ["-I", "-c", pythonRunner], { cwd: executionDir, detached, env: { HOME: executionDir, LANG: "C.UTF-8", PATH: process.env.PATH ?? "/usr/bin:/bin", PYTHONIOENCODING: "utf-8", PYTHONUNBUFFERED: "1", TMPDIR: executionDir, }, stdio: ["pipe", "pipe", "pipe"], }); let stdout = ""; let stderr = ""; let settled = false; let forceKillTimer; const finish = (callback) => { if (settled) { return; } settled = true; clearTimeout(timeout); clearTimeout(forceKillTimer); if (activeRuns.get(notebookId)?.child === child) { activeRuns.delete(notebookId); } callback(); }; const stop = (message, code = "execution_interrupted") => { if (settled) { return; } terminateProcessTree(child, detached, "SIGTERM"); finish(() => reject(new CanvasError(code, message))); forceKillTimer = setTimeout(() => { try { terminateProcessTree(child, detached, "SIGKILL"); } catch { // The process may already have exited. } }, 1000); forceKillTimer.unref?.(); }; activeRuns.set(notebookId, { child, stop }); const timeout = setTimeout(() => { stop("Notebook execution timed out after 30 seconds.", "cell_timeout"); }, 30000); child.on("error", (error) => { finish(() => reject(error)); }); child.stdout.on("data", (chunk) => { stdout += chunk; if (Buffer.byteLength(stdout) > 2_000_000) { stop("Notebook output exceeded the 2 MB limit.", "output_too_large"); } }); child.stderr.on("data", (chunk) => { stderr += chunk; if (Buffer.byteLength(stderr) > 250_000) { stop("Notebook error output exceeded the 250 KB limit.", "output_too_large"); } }); child.on("close", (code) => { if (settled) { return; } if (code !== 0) { finish(() => reject(new CanvasError("python_failed", stderr || `Python exited with code ${code}`))); return; } try { const parsed = JSON.parse(stdout); finish(() => resolve(parsed)); } catch { finish(() => reject(new CanvasError("runner_output_invalid", "The Python runner returned invalid JSON."))); } }); child.stdin.end(JSON.stringify(payload)); }); } async function runNotebookCells(notebook, targetCellId, notebookId) { const language = String( notebook.metadata?.language_info?.name ?? notebook.metadata?.kernelspec?.language ?? "", ).toLowerCase(); if (language !== "python") { throw new CanvasError("kernel_unsupported", `Execution is only available for Python notebooks, not ${language || "unknown"} notebooks.`); } const targetIndex = targetCellId ? findCellIndex(notebook, targetCellId) : notebook.cells.length - 1; const executableCells = notebook.cells .slice(0, targetIndex + 1) .filter((cell) => cell.cell_type === "code") .map((cell) => ({ id: getCellId(cell), code: normalizeSource(cell.source) })); broadcastStatus(notebookId, { status: "running", cellId: executableCells[0]?.id ?? null, cellCount: executableCells.length, message: `Running ${executableCells.length} ${executableCells.length === 1 ? "cell" : "cells"}`, }); try { const run = await runPython( { cells: executableCells, stopOnError: true }, notebookId, notebook.metadata?.copilot?.runtime, ); const byId = new Map(run.results.map((result) => [result.id, result])); for (const cell of notebook.cells.slice(0, targetIndex + 1)) { if (cell.cell_type !== "code") { continue; } const result = byId.get(getCellId(cell)); if (!result) { continue; } const executionCount = Number(notebook.metadata.copilot.executionCount ?? 0) + 1; notebook.metadata.copilot.executionCount = executionCount; cell.execution_count = executionCount; cell.outputs = outputFromRun({ ...result, executionCount }); } } finally { broadcastStatus(notebookId, { status: "idle", cellId: null, message: "Execution finished" }); } } function addClient(notebookId, instanceId, res, entry) { let clients = clientsByNotebook.get(notebookId); if (!clients) { clients = new Set(); clientsByNotebook.set(notebookId, clients); } const client = { instanceId, res }; clients.add(client); entry.eventClients.add(res); res.on("close", () => { clients.delete(client); entry.eventClients.delete(res); if (clients.size === 0) { clientsByNotebook.delete(notebookId); } }); } function broadcast(notebookId, notebook) { const clients = clientsByNotebook.get(notebookId); if (!clients) { return; } const data = JSON.stringify(summarizeNotebook(notebook)); for (const client of clients) { if (!client.res.destroyed && !client.res.writableEnded) { client.res.write(`event: notebook\ndata: ${data}\n\n`); } } } function broadcastStatus(notebookId, status) { const clients = clientsByNotebook.get(notebookId); if (!clients) { return; } const data = JSON.stringify(status); for (const client of clients) { if (!client.res.destroyed && !client.res.writableEnded) { client.res.write(`event: execution\ndata: ${data}\n\n`); } } } async function readJson(req) { let body = ""; for await (const chunk of req) { body += chunk; if (body.length > 1_000_000) { throw new CanvasError("request_too_large", "Request body exceeded the 1 MB limit."); } } try { return body ? JSON.parse(body) : {}; } catch { throw new CanvasError("request_json_invalid", "Request body must be valid JSON."); } } function sendJson(res, status, payload) { res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff", }); res.end(JSON.stringify(payload)); } function tokenMatches(provided, expected) { if (typeof provided !== "string") { return false; } const actualBuffer = Buffer.from(provided); const expectedBuffer = Buffer.from(expected); return actualBuffer.length === expectedBuffer.length && timingSafeEqual(actualBuffer, expectedBuffer); } function requireCapability(req, requestUrl, entry) { const provided = req.headers["x-notebook-token"] ?? requestUrl.searchParams.get("token"); if (!tokenMatches(provided, entry.token)) { throw new CanvasError("request_unauthorized", "Missing or invalid canvas capability token."); } } function requireSameOrigin(req, entry) { if (req.headers.origin !== entry.origin) { throw new CanvasError("request_origin_invalid", "Notebook actions must originate from this canvas."); } } function setDocumentSecurityHeaders(res) { res.setHeader( "Content-Security-Policy", "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; base-uri 'none'; form-action 'none'", ); res.setHeader("Referrer-Policy", "no-referrer"); res.setHeader("X-Content-Type-Options", "nosniff"); } function assertString(value, name, { required = false, maxLength = 500_000, pattern } = {}) { if (value === undefined && !required) { return; } if (typeof value !== "string" || (required && value.length === 0) || value.length > maxLength) { throw new CanvasError("action_input_invalid", `${name} must be a string of at most ${maxLength} characters.`); } if (pattern && !pattern.test(value)) { throw new CanvasError("action_input_invalid", `${name} has an invalid format.`); } } function validateHttpActionPayload(payload) { if (!payload || typeof payload !== "object" || Array.isArray(payload)) { throw new CanvasError("action_input_invalid", "Action input must be a JSON object."); } const actionFields = { get_notebook: [], list_workspace_notebooks: [], save_workspace: ["expectedRevision"], save_as: ["path", "overwrite", "expectedRevision"], open_workspace: ["path", "expectedRevision"], new_notebook: ["title", "expectedRevision"], rename_workspace: ["path", "overwrite", "expectedRevision"], duplicate_workspace: ["path", "overwrite"], move_cell: ["cellId", "direction", "expectedRevision"], duplicate_cell: ["cellId", "expectedRevision"], undo: ["expectedRevision"], redo: ["expectedRevision"], get_runtimes: [], set_runtime: ["runtime", "expectedRevision"], restart_runtime: ["expectedRevision"], create_checkpoint: ["label"], list_checkpoints: [], restore_checkpoint: ["checkpointId", "expectedRevision"], set_title: ["title", "expectedRevision"], add_cell: ["afterCellId", "cellType", "source", "expectedRevision"], update_cell: ["cellId", "cellType", "source", "expectedRevision"], delete_cell: ["cellId", "expectedRevision"], run_cell: ["cellId", "expectedRevision"], run_all: ["expectedRevision"], clear_outputs: ["expectedRevision"], interrupt: [], }; const action = payload.action; if (typeof action !== "string" || !Object.hasOwn(actionFields, action)) { throw new CanvasError("unknown_action", "Unknown notebook action."); } const allowed = new Set(["action", ...actionFields[action]]); const unexpected = Object.keys(payload).find((key) => !allowed.has(key)); if (unexpected) { throw new CanvasError("action_input_invalid", `Unexpected action field: ${unexpected}`); } if (payload.expectedRevision !== undefined && (!Number.isInteger(payload.expectedRevision) || payload.expectedRevision < 0)) { throw new CanvasError("action_input_invalid", "expectedRevision must be a non-negative integer."); } assertString(payload.cellId, "cellId", { required: ["update_cell", "delete_cell", "run_cell", "move_cell", "duplicate_cell"].includes(action), maxLength: 64, pattern: cellIdRegex, }); assertString(payload.afterCellId, "afterCellId", { maxLength: 64, pattern: cellIdRegex }); assertString(payload.title, "title", { required: action === "set_title", maxLength: 120 }); assertString(payload.path, "path", { required: ["save_as", "open_workspace", "rename_workspace", "duplicate_workspace"].includes(action), maxLength: 240, }); assertString(payload.source, "source"); assertString(payload.runtime, "runtime", { required: action === "set_runtime", maxLength: 32 }); assertString(payload.label, "label", { maxLength: 80 }); assertString(payload.checkpointId, "checkpointId", { required: action === "restore_checkpoint", maxLength: 80, pattern: /^[A-Za-z0-9_-]{1,80}$/, }); if (payload.direction !== undefined && !["up", "down"].includes(payload.direction)) { throw new CanvasError("action_input_invalid", "direction must be up or down."); } if (action === "move_cell" && payload.direction === undefined) { throw new CanvasError("action_input_invalid", "direction is required."); } if (payload.overwrite !== undefined && typeof payload.overwrite !== "boolean") { throw new CanvasError("action_input_invalid", "overwrite must be a boolean."); } if (payload.cellType !== undefined && !["code", "markdown", "raw"].includes(payload.cellType)) { throw new CanvasError("action_input_invalid", "cellType must be code, markdown, or raw."); } if (action === "add_cell" && payload.cellType === undefined) { throw new CanvasError("action_input_invalid", "cellType is required."); } if (action === "update_cell" && payload.cellType === undefined && payload.source === undefined) { throw new CanvasError("action_input_invalid", "update_cell requires source or cellType."); } return { action, input: Object.fromEntries(Object.entries(payload).filter(([key]) => key !== "action")) }; } async function handleCanvasAction(ctx, action, input = {}) { await storageReady; const notebookId = resolveNotebookId(ctx, input); if (action === "get_notebook") { const notebook = await loadNotebook(notebookId); return { notebookId, path: notebookPath(notebookId), notebook: summarizeNotebook(notebook) }; } if (action === "list_workspace_notebooks") { return { notebookId, files: await listWorkspaceNotebooks() }; } if (action === "save_workspace") { return withNotebookLock(notebookId, async () => { const notebook = await loadNotebookUnlocked(notebookId); const currentRevision = Number(notebook.metadata.copilot.revision ?? 0); if (Number.isInteger(input.expectedRevision) && input.expectedRevision !== currentRevision) { throw new CanvasError("revision_conflict", "Notebook changed before it could be saved."); } if (!notebook.metadata.copilot.workspacePath) { throw new CanvasError("workspace_path_required", "Choose Save As before saving this notebook."); } await assertWorkspaceUnchanged(notebookId, notebook); await writeWorkspaceCopy(notebookId, notebook); return { notebookId, notebook: summarizeNotebook(notebook), saved: true }; }); } if (action === "save_as") { const target = await assertWorkspaceTargetAvailable(input.path, input.overwrite === true); return mutateNotebook(notebookId, (notebook) => { notebook.metadata.copilot.workspacePath = target.relative; }, input.expectedRevision, { workspaceOverwrite: true }); } if (action === "rename_workspace") { const target = await assertWorkspaceTargetAvailable(input.path, input.overwrite === true); const current = await loadNotebook(notebookId); const previousPath = current.metadata?.copilot?.workspacePath; if (!previousPath) { throw new CanvasError("workspace_path_required", "Save the notebook before renaming it."); } const result = await mutateNotebook(notebookId, (notebook) => { notebook.metadata.copilot.workspacePath = target.relative; }, input.expectedRevision, { workspaceOverwrite: true }); if (previousPath !== target.relative) { await rm(normalizeWorkspacePath(previousPath).resolved, { force: true }); } return result; } if (action === "duplicate_workspace") { const target = await assertWorkspaceTargetAvailable(input.path, input.overwrite === true); const notebook = await loadNotebook(notebookId); await assertWorkspaceUnchanged(notebookId, notebook); const duplicate = structuredClone(notebook); duplicate.metadata.copilot.workspacePath = target.relative; duplicate.metadata.copilot.title = path.basename(target.relative, ".ipynb"); await writeWorkspaceDocument(target.relative, serializeNotebook(duplicate), { overwrite: true }); return { notebookId, duplicatedPath: target.relative, files: await listWorkspaceNotebooks() }; } if (action === "open_workspace") { const source = await readWorkspaceNotebook(input.path); return withNotebookLock(notebookId, async () => { const current = await loadNotebookUnlocked(notebookId); const currentRevision = Number(current.metadata.copilot.revision ?? 0); if (Number.isInteger(input.expectedRevision) && input.expectedRevision !== currentRevision) { throw new CanvasError("revision_conflict", "Notebook changed before the selected file could be opened."); } const history = historyByNotebook.get(notebookId) ?? { redo: [], undo: [] }; history.undo.push(structuredClone(current)); history.redo = []; historyByNotebook.set(notebookId, history); const { notebook } = normalizeNotebook( source.parsed, notebookId, path.basename(source.relative, ".ipynb"), ); notebook.metadata.copilot.workspacePath = source.relative; notebook.metadata.copilot.revision = currentRevision + 1; await writeNotebookUnlocked(notebookId, notebook); await writeWorkspaceCopy(notebookId, notebook); broadcast(notebookId, notebook); return { notebookId, notebook: summarizeNotebook(notebook), openedPath: source.relative }; }); } if (action === "new_notebook") { return mutateNotebook(notebookId, (notebook) => { const fresh = normalizeNotebook({}, notebookId, input.title || "Untitled notebook", true).notebook; notebook.cells = fresh.cells; notebook.metadata = fresh.metadata; delete notebook.metadata.copilot.workspacePath; }, input.expectedRevision); } if (action === "move_cell") { return mutateNotebook(notebookId, (notebook) => { const index = findCellIndex(notebook, input.cellId); const target = input.direction === "up" ? index - 1 : index + 1; if (target < 0 || target >= notebook.cells.length) { return; } [notebook.cells[index], notebook.cells[target]] = [notebook.cells[target], notebook.cells[index]]; }, input.expectedRevision); } if (action === "duplicate_cell") { return mutateNotebook(notebookId, (notebook) => { const index = findCellIndex(notebook, input.cellId); const duplicate = structuredClone(notebook.cells[index]); duplicate.id = createCellId(new Set(notebook.cells.map((cell) => getCellId(cell)))); duplicate.metadata = { ...(duplicate.metadata ?? {}), copilotCellId: duplicate.id }; if (duplicate.cell_type === "code") { duplicate.execution_count = null; duplicate.outputs = []; } notebook.cells.splice(index + 1, 0, duplicate); return { cellId: duplicate.id }; }, input.expectedRevision); } if (action === "undo" || action === "redo") { return applyHistory(notebookId, action, input.expectedRevision); } if (action === "get_runtimes") { return { notebookId, runtimes: await getAvailableRuntimes() }; } if (action === "set_runtime") { const runtimes = await getAvailableRuntimes(); if (!runtimes.some((runtime) => runtime.id === input.runtime)) { throw new CanvasError("runtime_unavailable", "The selected Python runtime is unavailable."); } return mutateNotebook(notebookId, (notebook) => { notebook.metadata.copilot.runtime = input.runtime; }, input.expectedRevision); } if (action === "restart_runtime") { return mutateNotebook(notebookId, (notebook) => { notebook.metadata.copilot.executionCount = 0; for (const cell of notebook.cells) { if (cell.cell_type === "code") { cell.execution_count = null; cell.outputs = []; } } }, input.expectedRevision); } if (action === "create_checkpoint") { const notebook = await loadNotebook(notebookId); const checkpoint = await createCheckpoint(notebookId, notebook, input.label); return { notebookId, checkpoint, checkpoints: await listCheckpoints(notebookId) }; } if (action === "list_checkpoints") { return { notebookId, checkpoints: await listCheckpoints(notebookId) }; } if (action === "restore_checkpoint") { const record = JSON.parse(await readFile(checkpointPath(notebookId, input.checkpointId), "utf8")); return mutateNotebook(notebookId, (notebook) => { const workspacePath = notebook.metadata?.copilot?.workspacePath; notebook.cells = structuredClone(record.notebook.cells); notebook.metadata = structuredClone(record.notebook.metadata); if (workspacePath) { notebook.metadata.copilot.workspacePath = workspacePath; } else { delete notebook.metadata.copilot.workspacePath; } }, input.expectedRevision); } if (action === "interrupt") { const run = activeRuns.get(notebookId); if (!run) { return { notebookId, interrupted: false }; } run.stop("Notebook execution interrupted."); return { notebookId, interrupted: true }; } if (action === "set_title") { return mutateNotebook(notebookId, (notebook) => { notebook.metadata.copilot.title = textToSource(input.title).slice(0, 120) || "Untitled notebook"; }, input.expectedRevision); } if (action === "add_cell") { return mutateNotebook(notebookId, (notebook) => { const cell = createCell(input.cellType, input.source); if (input.afterCellId) { const index = findCellIndex(notebook, input.afterCellId); notebook.cells.splice(index + 1, 0, cell); } else { notebook.cells.push(cell); } return { cellId: getCellId(cell) }; }, input.expectedRevision); } if (action === "update_cell") { return mutateNotebook(notebookId, (notebook) => { const cell = findCell(notebook, input.cellId); if (typeof input.cellType === "string" && input.cellType !== cell.cell_type) { cell.cell_type = input.cellType; if (cell.cell_type === "code") { cell.execution_count = null; cell.outputs = []; } else { delete cell.execution_count; delete cell.outputs; } } if (typeof input.source === "string") { cell.source = input.source; } }, input.expectedRevision); } if (action === "delete_cell") { return mutateNotebook(notebookId, (notebook) => { const index = findCellIndex(notebook, input.cellId); notebook.cells.splice(index, 1); if (notebook.cells.length === 0) { notebook.cells.push(createCell("code", "")); } }, input.expectedRevision); } if (action === "run_cell") { return mutateNotebook(notebookId, async (notebook) => { await runNotebookCells(notebook, input.cellId, notebookId); }, input.expectedRevision); } if (action === "run_all") { return mutateNotebook(notebookId, async (notebook) => { await runNotebookCells(notebook, undefined, notebookId); }, input.expectedRevision); } if (action === "clear_outputs") { return mutateNotebook(notebookId, (notebook) => { for (const cell of notebook.cells) { if (cell.cell_type === "code") { cell.outputs = []; cell.execution_count = null; } } }, input.expectedRevision); } throw new CanvasError("unknown_action", `Unknown notebook action: ${action}`); } async function handleHttpAction(ctx, req, res) { const payload = validateHttpActionPayload(await readJson(req)); const result = await handleCanvasAction(ctx, payload.action, payload.input); sendJson(res, 200, result); } function escapeHtml(value) { return String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """); } function renderHtml(notebookId) { return ` Notebook
workspace / ${escapeHtml(notebookId)}.ipynb
Opening notebook
0 cells Isolated Python Autosaved internally
Open notebook
`; } async function startServer(instanceId, notebookId) { const ctx = { instanceId, notebookId }; const entry = { eventClients: new Set(), notebookId, origin: "", server: undefined, sockets: new Set(), token: randomBytes(32).toString("base64url"), url: "", }; const server = createServer(async (req, res) => { try { const requestUrl = new URL(req.url ?? "/", "http://127.0.0.1"); if (req.method === "GET" && requestUrl.pathname === "/") { setDocumentSecurityHeaders(res); res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store", }); res.end(renderHtml(notebookId)); return; } if (req.method === "GET" && requestUrl.pathname === "/favicon.ico") { res.writeHead(204, { "Cache-Control": "public, max-age=86400" }); res.end(); return; } if (req.method === "GET" && requestUrl.pathname === "/api/notebook") { requireCapability(req, requestUrl, entry); const notebook = await loadNotebook(notebookId); sendJson(res, 200, { notebookId, notebook: summarizeNotebook(notebook) }); return; } if (req.method === "GET" && requestUrl.pathname === "/download") { requireCapability(req, requestUrl, entry); const notebook = await loadNotebook(notebookId); const filename = path.basename( notebook.metadata?.copilot?.workspacePath ?? `${notebookId}.ipynb`, ).replaceAll('"', ""); res.writeHead(200, { "Content-Type": "application/x-ipynb+json; charset=utf-8", "Content-Disposition": `attachment; filename="${filename}"`, "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff", }); res.end(serializeNotebook(notebook)); return; } if (req.method === "GET" && requestUrl.pathname === "/events") { requireCapability(req, requestUrl, entry); res.writeHead(200, { "Content-Type": "text/event-stream; charset=utf-8", "Cache-Control": "no-cache", Connection: "keep-alive", "X-Content-Type-Options": "nosniff", }); addClient(notebookId, instanceId, res, entry); const notebook = await loadNotebook(notebookId); res.write(`event: notebook\ndata: ${JSON.stringify(summarizeNotebook(notebook))}\n\n`); return; } if (req.method === "POST" && requestUrl.pathname === "/api/action") { requireCapability(req, requestUrl, entry); requireSameOrigin(req, entry); if (!String(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) { throw new CanvasError("request_content_type_invalid", "Notebook actions require application/json."); } await handleHttpAction(ctx, req, res); return; } sendJson(res, 404, { message: "Not found" }); } catch (error) { const status = error?.code === "request_unauthorized" ? 401 : error?.code === "request_origin_invalid" ? 403 : error?.code === "revision_conflict" ? 409 : error instanceof CanvasError ? 400 : 500; sendJson(res, status, { code: error.code ?? "canvas_error", message: error.message }); } }); entry.server = server; server.on("connection", (socket) => { entry.sockets.add(socket); socket.on("close", () => entry.sockets.delete(socket)); }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); const address = server.address(); const port = typeof address === "object" && address ? address.port : 0; entry.origin = `http://127.0.0.1:${port}`; entry.url = `${entry.origin}/#token=${encodeURIComponent(entry.token)}`; return entry; } async function closeServerEntry(entry) { const run = activeRuns.get(entry.notebookId); run?.stop("Notebook canvas closed during execution."); for (const client of entry.eventClients) { if (!client.writableEnded) { client.write("event: close\ndata: {}\n\n"); client.end(); } } entry.eventClients.clear(); await new Promise((resolve) => { let resolved = false; const finish = () => { if (!resolved) { resolved = true; resolve(); } }; entry.server.close(finish); entry.server.closeIdleConnections?.(); const forceClose = setTimeout(() => { for (const socket of entry.sockets) { socket.destroy(); } finish(); }, 750); forceClose.unref?.(); }); } function declaredNotebookAction(name, description, properties = {}, required = []) { return { name, description, inputSchema: { type: "object", additionalProperties: false, required, properties: { notebookId: { type: "string", pattern: notebookIdPattern }, ...properties, }, }, handler: (ctx) => handleCanvasAction(ctx, name, ctx.input), }; } const productActions = [ declaredNotebookAction( "list_workspace_notebooks", "List .ipynb files available in the current workspace.", ), declaredNotebookAction( "save_workspace", "Save the notebook to its current workspace path.", ), declaredNotebookAction( "save_as", "Save a workspace copy of the notebook under a new .ipynb path.", { path: { type: "string", minLength: 1, maxLength: 240 }, overwrite: { type: "boolean" }, }, ["path"], ), declaredNotebookAction( "open_workspace", "Open a workspace .ipynb file in this canvas.", { path: { type: "string", minLength: 1, maxLength: 240 } }, ["path"], ), declaredNotebookAction( "new_notebook", "Replace the canvas with a new empty notebook.", { title: { type: "string", minLength: 1, maxLength: 120 }, }, ), declaredNotebookAction( "rename_workspace", "Move the current workspace notebook to a new .ipynb path.", { path: { type: "string", minLength: 1, maxLength: 240 }, overwrite: { type: "boolean" }, }, ["path"], ), declaredNotebookAction( "duplicate_workspace", "Create a workspace copy of the notebook at a new .ipynb path.", { path: { type: "string", minLength: 1, maxLength: 240 }, overwrite: { type: "boolean" }, }, ["path"], ), declaredNotebookAction( "move_cell", "Move a cell one position up or down.", { cellId: { type: "string", pattern: cellIdPattern }, direction: { type: "string", enum: ["up", "down"] }, }, ["cellId", "direction"], ), declaredNotebookAction( "duplicate_cell", "Duplicate a notebook cell directly below the original.", { cellId: { type: "string", pattern: cellIdPattern } }, ["cellId"], ), declaredNotebookAction("undo", "Undo the most recent notebook mutation."), declaredNotebookAction("redo", "Redo the most recently undone notebook mutation."), declaredNotebookAction("get_runtimes", "List available local Python runtimes."), declaredNotebookAction( "set_runtime", "Select the Python executable used for future notebook runs.", { runtime: { type: "string", enum: ["python3", "python"] } }, ["runtime"], ), declaredNotebookAction("restart_runtime", "Interrupt active work and clear outputs for a fresh stateless run."), declaredNotebookAction( "create_checkpoint", "Create a persistent checkpoint of the current notebook.", { label: { type: "string", maxLength: 80 } }, ), declaredNotebookAction("list_checkpoints", "List persistent checkpoints for the current notebook."), declaredNotebookAction( "restore_checkpoint", "Restore a persistent checkpoint into the current notebook.", { checkpointId: { type: "string", minLength: 1, maxLength: 80 } }, ["checkpointId"], ), ]; const canvas = createCanvas({ id: "jupyter-notebooks", displayName: "Notebook", description: "Create notebook cells and run Python code through a fresh constrained process.", inputSchema: openInputSchema, actions: [ ...productActions, { name: "get_notebook", description: "Return the current notebook cells, outputs, and backing .ipynb path.", inputSchema: notebookActionSchema, handler: (ctx) => handleCanvasAction(ctx, "get_notebook", ctx.input), }, { name: "set_title", description: "Set the notebook title.", inputSchema: { type: "object", additionalProperties: false, required: ["title"], properties: { notebookId: { type: "string", pattern: notebookIdPattern }, title: { type: "string", minLength: 1, maxLength: 120 }, }, }, handler: (ctx) => handleCanvasAction(ctx, "set_title", ctx.input), }, { name: "add_cell", description: "Add a code, Markdown, or raw cell, optionally after an existing cell.", inputSchema: { type: "object", additionalProperties: false, required: ["cellType"], properties: { notebookId: { type: "string", pattern: notebookIdPattern }, afterCellId: { type: "string", pattern: cellIdPattern }, cellType: { type: "string", enum: ["code", "markdown", "raw"] }, source: { type: "string" }, }, }, handler: (ctx) => handleCanvasAction(ctx, "add_cell", ctx.input), }, { name: "update_cell", description: "Update a cell source and optionally convert between code, Markdown, and raw.", inputSchema: { type: "object", additionalProperties: false, required: ["cellId"], properties: { notebookId: { type: "string", pattern: notebookIdPattern }, cellId: { type: "string", pattern: cellIdPattern }, cellType: { type: "string", enum: ["code", "markdown", "raw"] }, source: { type: "string" }, }, }, handler: (ctx) => handleCanvasAction(ctx, "update_cell", ctx.input), }, { name: "delete_cell", description: "Delete a notebook cell.", inputSchema: cellIdSchema(), handler: (ctx) => handleCanvasAction(ctx, "delete_cell", ctx.input), }, { name: "run_cell", description: "Run code cells from the beginning through the selected cell in a fresh constrained Python process.", inputSchema: cellIdSchema(), handler: (ctx) => handleCanvasAction(ctx, "run_cell", ctx.input), }, { name: "run_all", description: "Run all code cells in order in a fresh constrained Python process.", inputSchema: notebookActionSchema, handler: (ctx) => handleCanvasAction(ctx, "run_all", ctx.input), }, { name: "clear_outputs", description: "Clear all code cell outputs and execution counts.", inputSchema: notebookActionSchema, handler: (ctx) => handleCanvasAction(ctx, "clear_outputs", ctx.input), }, { name: "interrupt", description: "Interrupt the active Python execution for this notebook.", inputSchema: notebookActionSchema, handler: (ctx) => handleCanvasAction(ctx, "interrupt", ctx.input), }, ], open: async (ctx) => { await storageReady; const notebookId = ctx.input?.notebookId ?? "default"; await loadNotebook(notebookId, ctx.input?.title); let entry = servers.get(ctx.instanceId); if (!entry || entry.notebookId !== notebookId) { if (entry) { await closeServerEntry(entry); } entry = await startServer(ctx.instanceId, notebookId); servers.set(ctx.instanceId, entry); } return { title: "Notebook", status: "Ready", url: entry.url, }; }, onClose: async (ctx) => { const entry = servers.get(ctx.instanceId); if (entry) { servers.delete(ctx.instanceId); await closeServerEntry(entry); } }, }); const session = await joinSession({ canvases: [canvas] }); storageReady = initializeStorage(session.workspacePath); await storageReady;