5037 lines · 155.8 KB · Rust
RawDownload
use std::collections::BTreeSet;use std::env;use std::fs;use std::io;use std::path::{Path, PathBuf};use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};use std::sync::{Arc, Mutex, OnceLock};use std::time::Duration; use agentos_runtime::RuntimeContext;use tokio::sync::Mutex as AsyncMutex;use tokio::time; pub(crate) const NODE_IMPORT_CACHE_DEBUG_ENV: &str = "AGENTOS_NODE_IMPORT_CACHE_DEBUG";pub(crate) const NODE_IMPORT_CACHE_METRICS_PREFIX: &str = "__AGENTOS_NODE_IMPORT_CACHE_METRICS__:";pub(crate) const NODE_IMPORT_CACHE_ASSET_ROOT_ENV: &str = "AGENTOS_NODE_IMPORT_CACHE_ASSET_ROOT"; const NODE_IMPORT_CACHE_PATH_ENV: &str = "AGENTOS_NODE_IMPORT_CACHE_PATH";const NODE_IMPORT_CACHE_LOADER_PATH_ENV: &str = "AGENTOS_NODE_IMPORT_CACHE_LOADER_PATH";const NODE_IMPORT_CACHE_SCHEMA_VERSION: &str = "1";const NODE_IMPORT_CACHE_LOADER_VERSION: &str = "8";// Upstream reached 104 while the reactor branch independently changed bundled// assets; use a new generation so no stale materialization survives the merge.const NODE_IMPORT_CACHE_ASSET_VERSION: &str = "105";const NODE_IMPORT_CACHE_DIR_PREFIX: &str = "agentos-node-import-cache";const DEFAULT_NODE_IMPORT_CACHE_MATERIALIZE_TIMEOUT: Duration = Duration::from_secs(30);const NODE_IMPORT_CACHE_BLOCKING_JOB_RESERVATION_BYTES: usize = 64 * 1024;const PYODIDE_DIST_DIR: &str = "pyodide-dist";const AGENTOS_BUILTIN_SPECIFIER_PREFIX: &str = "secure-exec:builtin/";const AGENTOS_POLYFILL_SPECIFIER_PREFIX: &str = "secure-exec:polyfill/";const BUNDLED_PYODIDE_MJS: &[u8] = include_bytes!("../assets/pyodide/pyodide.mjs");// Large Pyodide assets are excluded from the published crate and staged into// OUT_DIR by build.rs (copied from `assets/pyodide/` in-tree, or downloaded// from the release CDN when building the published crate).const BUNDLED_PYODIDE_ASM_JS: &[u8] =    include_bytes!(concat!(env!("OUT_DIR"), "/pyodide/pyodide.asm.js"));const BUNDLED_PYODIDE_ASM_WASM: &[u8] =    include_bytes!(concat!(env!("OUT_DIR"), "/pyodide/pyodide.asm.wasm"));const BUNDLED_PYODIDE_LOCK: &[u8] = include_bytes!("../assets/pyodide/pyodide-lock.json");const BUNDLED_PYTHON_STDLIB_ZIP: &[u8] =    include_bytes!(concat!(env!("OUT_DIR"), "/pyodide/python_stdlib.zip"));const BUNDLED_NUMPY_WHL: &[u8] = include_bytes!(concat!(    env!("OUT_DIR"),    "/pyodide/numpy-2.2.5-cp313-cp313-pyodide_2025_0_wasm32.whl"));const BUNDLED_PANDAS_WHL: &[u8] = include_bytes!(concat!(    env!("OUT_DIR"),    "/pyodide/pandas-2.3.3-cp313-cp313-pyodide_2025_0_wasm32.whl"));const BUNDLED_PYTHON_DATEUTIL_WHL: &[u8] =    include_bytes!("../assets/pyodide/python_dateutil-2.9.0.post0-py2.py3-none-any.whl");const BUNDLED_PYTZ_WHL: &[u8] =    include_bytes!("../assets/pyodide/pytz-2025.2-py2.py3-none-any.whl");const BUNDLED_SIX_WHL: &[u8] = include_bytes!("../assets/pyodide/six-1.17.0-py2.py3-none-any.whl");const BUNDLED_MICROPIP_WHL: &[u8] =    include_bytes!("../assets/pyodide/micropip-0.11.0-py3-none-any.whl");const BUNDLED_CLICK_WHL: &[u8] = include_bytes!("../assets/pyodide/click-8.3.1-py3-none-any.whl");const NODE_PYTHON_RUNNER_SOURCE: &str = include_str!("../assets/runners/python-runner.mjs"); static CLEANED_NODE_IMPORT_CACHE_ROOTS: OnceLock<Mutex<BTreeSet<PathBuf>>> = OnceLock::new();#[cfg(test)]static NODE_IMPORT_CACHE_TEST_MATERIALIZE_DELAY_MS: AtomicU64 = AtomicU64::new(0);#[cfg(test)]static NODE_IMPORT_CACHE_TEST_MATERIALIZE_DELAY_LOCK: Mutex<()> = Mutex::new(()); #[derive(Clone, Copy)]struct BundledPyodidePackageAsset {    file_name: &'static str,    bytes: &'static [u8],} const BUNDLED_PYODIDE_PACKAGE_ASSETS: &[BundledPyodidePackageAsset] = &[    BundledPyodidePackageAsset {        file_name: "numpy-2.2.5-cp313-cp313-pyodide_2025_0_wasm32.whl",        bytes: BUNDLED_NUMPY_WHL,    },    BundledPyodidePackageAsset {        file_name: "pandas-2.3.3-cp313-cp313-pyodide_2025_0_wasm32.whl",        bytes: BUNDLED_PANDAS_WHL,    },    BundledPyodidePackageAsset {        file_name: "python_dateutil-2.9.0.post0-py2.py3-none-any.whl",        bytes: BUNDLED_PYTHON_DATEUTIL_WHL,    },    BundledPyodidePackageAsset {        file_name: "pytz-2025.2-py2.py3-none-any.whl",        bytes: BUNDLED_PYTZ_WHL,    },    BundledPyodidePackageAsset {        file_name: "six-1.17.0-py2.py3-none-any.whl",        bytes: BUNDLED_SIX_WHL,    },    BundledPyodidePackageAsset {        file_name: "micropip-0.11.0-py3-none-any.whl",        bytes: BUNDLED_MICROPIP_WHL,    },    BundledPyodidePackageAsset {        file_name: "click-8.3.1-py3-none-any.whl",        bytes: BUNDLED_CLICK_WHL,    },];const NODE_IMPORT_CACHE_LOADER_TEMPLATE: &str = r#"import crypto from 'node:crypto';import fs from 'node:fs';import path from 'node:path';import { fileURLToPath, pathToFileURL } from 'node:url'; const GUEST_PATH_MAPPINGS = parseGuestPathMappings(process.env.AGENTOS_GUEST_PATH_MAPPINGS);const ALLOWED_BUILTINS = new Set(parseJsonArray(process.env.AGENTOS_ALLOWED_NODE_BUILTINS));const CACHE_PATH = process.env.__NODE_IMPORT_CACHE_PATH_ENV__;const CACHE_ROOT = CACHE_PATH ? path.dirname(CACHE_PATH) : null;const GUEST_INTERNAL_CACHE_ROOT = '/.agentos/node-import-cache';const HOST_CWD = process.cwd();const DEFAULT_GUEST_CWD =  typeof process.env.PWD === 'string' &&  process.env.PWD.startsWith('/')    ? path.posix.normalize(process.env.PWD)    : typeof (globalThis.__agentOSVirtualOs||{}).homedir === 'string' &&        (globalThis.__agentOSVirtualOs||{}).homedir.startsWith('/')      ? path.posix.normalize((globalThis.__agentOSVirtualOs||{}).homedir)    : '/root';const UNMAPPED_GUEST_PATH = '/unknown';const PROJECTED_SOURCE_CACHE_ROOT = CACHE_PATH  ? path.join(path.dirname(CACHE_PATH), 'projected-sources')  : null;const ASSET_ROOT = process.env.__NODE_IMPORT_CACHE_ASSET_ROOT_ENV__;const DEBUG_ENABLED = process.env.__NODE_IMPORT_CACHE_DEBUG_ENV__ === '1';const CONTROL_PIPE_FD = parseControlPipeFd(process.env.AGENTOS_CONTROL_PIPE_FD);const SCHEMA_VERSION = '__NODE_IMPORT_CACHE_SCHEMA_VERSION__';const LOADER_VERSION = '__NODE_IMPORT_CACHE_LOADER_VERSION__';const ASSET_VERSION = '__NODE_IMPORT_CACHE_ASSET_VERSION__';const MAX_CACHE_RECORD_ENTRIES = 512;const MAX_CACHE_KEY_BYTES = 4096;const MAX_CACHE_VALUE_BYTES = 16 * 1024;const MAX_CACHE_STATE_BYTES = 4 * 1024 * 1024;const BUILTIN_PREFIX = '__AGENTOS_BUILTIN_SPECIFIER_PREFIX__';const POLYFILL_PREFIX = '__AGENTOS_POLYFILL_SPECIFIER_PREFIX__';const FS_ASSET_SPECIFIER = `${BUILTIN_PREFIX}fs`;const FS_PROMISES_ASSET_SPECIFIER = `${BUILTIN_PREFIX}fs-promises`;const CHILD_PROCESS_ASSET_SPECIFIER = `${BUILTIN_PREFIX}child-process`;const NET_ASSET_SPECIFIER = `${BUILTIN_PREFIX}net`;const DGRAM_ASSET_SPECIFIER = `${BUILTIN_PREFIX}dgram`;const DNS_ASSET_SPECIFIER = `${BUILTIN_PREFIX}dns`;const DNS_PROMISES_ASSET_SPECIFIER = `${BUILTIN_PREFIX}dns-promises`;const HTTP_ASSET_SPECIFIER = `${BUILTIN_PREFIX}http`;const HTTP2_ASSET_SPECIFIER = `${BUILTIN_PREFIX}http2`;const HTTPS_ASSET_SPECIFIER = `${BUILTIN_PREFIX}https`;const TLS_ASSET_SPECIFIER = `${BUILTIN_PREFIX}tls`;const OS_ASSET_SPECIFIER = `${BUILTIN_PREFIX}os`;const DENIED_BUILTINS = new Set([  'child_process',  'cluster',  'dgram',  'dns',  'dns/promises',  'http',  'http2',  'https',  'inspector',  'module',  'net',  'tls',  'trace_events',  'v8',  'vm',  'worker_threads',].filter((name) => !ALLOWED_BUILTINS.has(name))); let cacheState = loadCacheState();let dirty = false;let cacheWriteError = null;const metrics = {  resolveHits: 0,  resolveMisses: 0,  packageTypeHits: 0,  packageTypeMisses: 0,  moduleFormatHits: 0,  moduleFormatMisses: 0,  sourceHits: 0,  sourceMisses: 0,}; export async function resolve(specifier, context, nextResolve) {  const guestResolvedPath = resolveGuestSpecifier(specifier, context);  if (guestResolvedPath) {    const guestUrl = pathToFileURL(guestResolvedPath).href;    const format = lookupModuleFormat(guestUrl);    flushCacheState();    emitMetrics();    return {      shortCircuit: true,      url: guestUrl,      ...(format && format !== 'builtin' ? { format } : {}),    };  }   const key = createResolutionKey(specifier, context);  const cached = cacheState.resolutions[key];   if (cached && validateResolutionEntry(cached)) {    metrics.resolveHits += 1;    const response = {      shortCircuit: true,      url: cached.resolvedUrl,    };     if (cached.format) {      response.format = cached.format;    }     flushCacheState();    emitMetrics();    return response;  }   metrics.resolveMisses += 1;   const asset = resolveSecureExecAsset(specifier);  if (asset) {    cacheState.resolutions[key] = {      kind: 'explicit-file',      resolvedUrl: asset.url,      format: 'module',      resolvedFilePath: asset.filePath,    };    dirty = true;    flushCacheState();    emitMetrics();    return {      shortCircuit: true,      url: asset.url,      format: 'module',    };  }   const builtinAsset = resolveBuiltinAsset(specifier, context);  if (builtinAsset) {    cacheState.resolutions[key] = {      kind: 'explicit-file',      resolvedUrl: builtinAsset.url,      format: 'module',      resolvedFilePath: builtinAsset.filePath,    };    dirty = true;    flushCacheState();    emitMetrics();    return {      shortCircuit: true,      url: builtinAsset.url,      format: 'module',    };  }   const deniedBuiltin = resolveDeniedBuiltin(specifier);  if (deniedBuiltin) {    cacheState.resolutions[key] = {      kind: 'explicit-file',      resolvedUrl: deniedBuiltin.url,      format: 'module',      resolvedFilePath: deniedBuiltin.filePath,    };    dirty = true;    flushCacheState();    emitMetrics();    return {      shortCircuit: true,      url: deniedBuiltin.url,      format: 'module',    };  }   const translatedContext = translateContextParentUrl(context);  let resolved;  try {    resolved = await nextResolve(specifier, translatedContext);  } catch (error) {    flushCacheState();    emitMetrics();    throw translateErrorToGuest(error);  }  const translatedUrl = translateResolvedUrlToGuest(resolved.url);  const translatedResolved =    translatedUrl === resolved.url ? resolved : { ...resolved, url: translatedUrl };  const entry = buildResolutionEntry(specifier, context, translatedResolved);  if (entry) {    cacheState.resolutions[key] = entry;    dirty = true;  }   if (entry && entry.format && resolved.format == null) {    flushCacheState();    emitMetrics();    return {      ...translatedResolved,      format: entry.format,    };  }   flushCacheState();  emitMetrics();  return translatedResolved;} export async function load(url, context, nextLoad) {  try {    const filePath = filePathFromUrl(url);    const format = lookupModuleFormat(url) ?? context.format;     if (!filePath || !format || format === 'builtin') {      return await nextLoad(url, context);    }     const projectedPackageSource = loadProjectedPackageSource(url, filePath, format);    if (projectedPackageSource != null) {      flushCacheState();      emitMetrics();      return {        shortCircuit: true,        format,        source: projectedPackageSource,      };    }     const source =      format === 'wasm'        ? fs.readFileSync(filePath)        : rewriteBuiltinImports(fs.readFileSync(filePath, 'utf8'), filePath);     return {      shortCircuit: true,      format,      source,    };  } catch (error) {    flushCacheState();    emitMetrics();    throw translateErrorToGuest(error);  }} function loadCacheState() {  if (!CACHE_PATH) {    return emptyCacheState();  }   try {    const stat = fs.statSync(CACHE_PATH);    if (!stat.isFile() || stat.size > MAX_CACHE_STATE_BYTES) {      return emptyCacheState();    }    const parsed = JSON.parse(fs.readFileSync(CACHE_PATH, 'utf8'));    if (!isCompatibleCacheState(parsed)) {      return emptyCacheState();    }     return normalizeCacheState(parsed);  } catch {    return emptyCacheState();  }} function flushCacheState() {  if (!CACHE_PATH || !dirty) {    return;  }   try {    fs.mkdirSync(path.dirname(CACHE_PATH), { recursive: true });     let merged = cacheState;    try {      const existingStat = fs.statSync(CACHE_PATH);      if (existingStat.isFile() && existingStat.size <= MAX_CACHE_STATE_BYTES) {        const existing = JSON.parse(fs.readFileSync(CACHE_PATH, 'utf8'));        if (isCompatibleCacheState(existing)) {          merged = mergeCacheStates(normalizeCacheState(existing), cacheState);        }      }    } catch {      // Ignore missing or unreadable prior state and replace it with the in-memory view.    }     merged = pruneCacheState(merged);    let serialized = JSON.stringify(merged);    if (byteLengthUtf8(serialized) > MAX_CACHE_STATE_BYTES) {      merged = pruneCacheState(merged, Math.floor(MAX_CACHE_RECORD_ENTRIES / 4));      serialized = JSON.stringify(merged);    }    if (byteLengthUtf8(serialized) > MAX_CACHE_STATE_BYTES) {      merged = emptyCacheState();      serialized = JSON.stringify(merged);    }     const tempPath = `${CACHE_PATH}.${process.pid}.${Date.now()}.tmp`;    fs.writeFileSync(tempPath, serialized);    fs.renameSync(tempPath, CACHE_PATH);    cacheState = merged;    pruneProjectedSourceFiles();    dirty = false;  } catch (error) {    cacheWriteError = error instanceof Error ? error.message : String(error);  }} function emitMetrics() {  if (!DEBUG_ENABLED) {    return;  }   const payload = cacheWriteError    ? { ...metrics, cacheWriteError }    : metrics;   emitControlMessage({ type: 'node_import_cache_metrics', metrics: payload });} function parseControlPipeFd(value) {  if (typeof value !== 'string' || value.trim() === '') {    return null;  }   const parsed = Number.parseInt(value, 10);  return Number.isInteger(parsed) && parsed >= 3 ? parsed : null;} function emitControlMessage(message) {  if (CONTROL_PIPE_FD == null) {    if (      message?.type === 'signal_state' &&      typeof process?.stdout?.write === 'function'    ) {      try {        process.stdout.write(`__AGENTOS_WASM_SIGNAL_STATE__:${JSON.stringify(message)}\n`);      } catch {        // Ignore control-channel fallback failures during teardown.      }    }    return;  }   try {    fs.writeSync(CONTROL_PIPE_FD, `${JSON.stringify(message)}\n`);  } catch {    if (      message?.type === 'signal_state' &&      typeof process?.stdout?.write === 'function'    ) {      try {        process.stdout.write(`__AGENTOS_WASM_SIGNAL_STATE__:${JSON.stringify(message)}\n`);      } catch {        // Ignore control-channel fallback failures during teardown.      }    }  }} function emptyCacheState() {  return {    schemaVersion: SCHEMA_VERSION,    loaderVersion: LOADER_VERSION,    assetVersion: ASSET_VERSION,    nodeVersion: process.version,    resolutions: {},    packageTypes: {},    moduleFormats: {},    projectedSources: {},  };} function isCompatibleCacheState(value) {  return (    isRecord(value) &&    value.schemaVersion === SCHEMA_VERSION &&    value.loaderVersion === LOADER_VERSION &&    value.assetVersion === ASSET_VERSION &&    value.nodeVersion === process.version  );} function normalizeCacheState(value) {  return pruneCacheState({    ...emptyCacheState(),    ...value,    resolutions: isRecord(value.resolutions) ? value.resolutions : {},    packageTypes: isRecord(value.packageTypes) ? value.packageTypes : {},    moduleFormats: isRecord(value.moduleFormats) ? value.moduleFormats : {},    projectedSources: isRecord(value.projectedSources) ? value.projectedSources : {},  });} function mergeCacheStates(base, current) {  return pruneCacheState({    ...emptyCacheState(),    resolutions: {      ...base.resolutions,      ...current.resolutions,    },    packageTypes: {      ...base.packageTypes,      ...current.packageTypes,

Lines 1–500 of 5037.