UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

147 lines • 6.22 kB
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= import { PyodideUnavailableError } from "./errors.js"; const HOST_GLOBALS = ['process', 'global', 'require', 'fetch', 'WebSocket']; const HOST_FILESYSTEMS = ['NODEFS', 'NODERAWFS', 'NATIVEFS_ASYNC']; const HOST_METHODS = ['mountNodeFS', 'mountNativeFS', 'useNodeSockFS']; const HOST_PRELOAD_METHODS = [ 'createLazyFile', 'forceLoadFile', 'createPreloadedFile', 'preloadFile', ]; /** Remove host handles, returning any names that could not be removed. */ export function stripWorkerGlobals(realm) { const left = []; for (const name of HOST_GLOBALS) { if (!(name in realm)) continue; Reflect.deleteProperty(realm, name); if (name in realm && !Reflect.defineProperty(realm, name, { value: undefined })) { left.push(name); } } return left; } function refused(name) { return () => { throw new Error(`python3: ${name} is disabled in the sandboxed worker`); }; } /** Match Pyodide's Node loader, which strips file:// before reading disk. */ function localPath(path) { const value = path.startsWith('file://') ? path.slice('file://'.length) : path; return value.includes('://') ? null : value; } /** * Restrict binary reads to the configured distribution, including real paths * behind symlinks. Capture the roots before guest code can mutate config. * * @param pyodide The loaded interpreter. */ async function guardLoadBinaryFile(pyodide) { const api = pyodide._api; const original = api?.loadBinaryFile; if (api === undefined || typeof original !== 'function') return; const nodePath = await import('node:path'); const { realpath } = await import('node:fs/promises'); const cwd = nodePath.resolve('.'); const roots = [api.config?.indexURL, api.config?.packageBaseUrl, api.config?.packageCacheDir] .map((url) => (typeof url === 'string' ? localPath(url) : null)) .filter((root) => root !== null && root.length > 0) .map((root) => nodePath.resolve(cwd, root)); const inside = (root, path) => { const tail = nodePath.relative(root, path); return tail !== '..' && !tail.startsWith(`..${nodePath.sep}`) && !nodePath.isAbsolute(tail); }; api.loadBinaryFile = (path, ...rest) => { const local = typeof path === 'string' ? localPath(path) : null; const absolute = local === null ? null : nodePath.resolve(cwd, local); const matching = absolute === null ? [] : roots.filter((root) => inside(root, absolute)); if (matching.length === 0 || absolute === null) { throw new Error(`python3: reading '${path}' is not allowed in the sandboxed worker`); } return Promise.all([ Promise.all(matching.map((root) => realpath(root))), realpath(absolute), ]).then(([canonicalRoots, canonicalPath]) => { if (!canonicalRoots.some((root) => inside(root, canonicalPath))) { throw new Error(`python3: reading '${path}' is not allowed in the sandboxed worker`); } return original.call(api, canonicalPath, ...rest); }); }; const module = pyodide._module; if (module !== undefined && typeof module.asyncLoad === 'function') { module.asyncLoad = api.loadBinaryFile; } } /** * Close the exposed Pyodide APIs that captured host filesystem handles at load. * This is worker hardening, not an isolation boundary against arbitrary JS. * * @param pyodide The loaded interpreter. */ export async function sealPyodideHostApis(pyodide) { await guardLoadBinaryFile(pyodide); const fs = pyodide.FS; const module = pyodide._module; const filesystems = fs?.filesystems; for (const table of [filesystems, module]) { if (table === undefined) continue; for (const name of HOST_FILESYSTEMS) Reflect.deleteProperty(table, name); } for (const name of HOST_PRELOAD_METHODS) { if (fs !== undefined && typeof fs[name] === 'function') fs[name] = refused(`FS.${name}()`); const exported = `FS_${name}`; if (module !== undefined && typeof module[exported] === 'function') { module[exported] = refused(`${exported}()`); } } const publicApi = pyodide; for (const name of HOST_METHODS) { if (typeof publicApi[name] === 'function') publicApi[name] = refused(`${name}()`); } const api = pyodide._api; if (api !== undefined && typeof api.initializeNodeSockFS === 'function') { api.initializeNodeSockFS = refused('initializeNodeSockFS()'); } } function denyModule(specifier) { throw new Error(`python3: module '${specifier}' cannot be loaded in the sandboxed worker`); } /** * Seal one Node worker after initialization and before guest execution. Older * Node releases without synchronous module hooks deliberately remain unsealed. * * @param pyodide The loaded interpreter. * @param realm The worker's global object. */ export async function sealWorkerRealm(pyodide, realm = globalThis) { const { registerHooks } = await import('node:module'); if (typeof registerHooks !== 'function') return; await sealPyodideHostApis(pyodide); registerHooks({ resolve: denyModule }); const left = stripWorkerGlobals(realm); if (left.length > 0) { throw new PyodideUnavailableError(`python3: could not remove worker handles: ${left.join(', ')}`); } } //# sourceMappingURL=seal.js.map