8917 lines · 296.2 KB · Rust
RawDownload
use crate::common::stable_hash64;use crate::node_import_cache::{    NodeImportCache, NodeImportCacheCleanup, NODE_IMPORT_CACHE_ASSET_ROOT_ENV,};use crate::runtime_support::{    NODE_COMPILE_CACHE_ENV, NODE_DISABLE_COMPILE_CACHE_ENV, NODE_FROZEN_TIME_ENV,    NODE_SANDBOX_ROOT_ENV,};use crate::signal::NodeSignalHandlerRegistration;use crate::v8_host::{V8RuntimeHost, V8SessionFrameReceiver, V8SessionHandle};use crate::v8_ipc::BinaryFrame;use crate::v8_runtime;use agentos_bridge::queue_tracker::{register_queue, TrackedLimit};use agentos_runtime::RuntimeContext;use agentos_v8_runtime::runtime_protocol::{RuntimeCommand, WarmSessionHint};use flume::{Receiver as EventReceiver, Sender as EventSender};use getrandom::getrandom;use serde::Deserialize;use serde::Serialize;use serde_json::{json, Value};use std::cmp::Reverse;use std::collections::{BTreeMap, BinaryHeap, HashMap, HashSet, VecDeque};use std::fmt;use std::fs::{self, File};use std::io::{BufRead, BufReader, BufWriter, Write};use std::os::fd::OwnedFd;use std::panic::{catch_unwind, AssertUnwindSafe};use std::path::{Path, PathBuf};use std::sync::{    atomic::{AtomicBool, AtomicU64, Ordering},    mpsc::{self, Receiver, SyncSender, TrySendError},    Arc, Condvar, Mutex, OnceLock,};use std::thread;use std::time::{Duration, Instant};use tokio::sync::Notify;use tokio::time; const NODE_ENTRYPOINT_ENV: &str = "AGENTOS_ENTRYPOINT";const NODE_BOOTSTRAP_ENV: &str = "AGENTOS_BOOTSTRAP_MODULE";const NODE_GUEST_ARGV_ENV: &str = "AGENTOS_GUEST_ARGV";const NODE_PREWARM_IMPORTS_ENV: &str = "AGENTOS_NODE_PREWARM_IMPORTS";const NODE_IMPORT_COMPILE_CACHE_NAMESPACE_VERSION: &str = "3";const NODE_IMPORT_CACHE_LOADER_PATH_ENV: &str = "AGENTOS_NODE_IMPORT_CACHE_LOADER_PATH";const NODE_IMPORT_CACHE_PATH_ENV: &str = "AGENTOS_NODE_IMPORT_CACHE_PATH";const NODE_KEEP_STDIN_OPEN_ENV: &str = "AGENTOS_KEEP_STDIN_OPEN";const NODE_GUEST_ENTRYPOINT_ENV: &str = "AGENTOS_GUEST_ENTRYPOINT";const NODE_GUEST_ENTRYPOINT_MODULE_MODE_ENV: &str = "AGENTOS_GUEST_ENTRYPOINT_MODULE_MODE";const NODE_GUEST_PATH_MAPPINGS_ENV: &str = "AGENTOS_GUEST_PATH_MAPPINGS";const NODE_VIRTUAL_PROCESS_EXEC_PATH_ENV: &str = "AGENTOS_VIRTUAL_PROCESS_EXEC_PATH";const NODE_VIRTUAL_PROCESS_PID_ENV: &str = "AGENTOS_VIRTUAL_PROCESS_PID";const NODE_VIRTUAL_PROCESS_PPID_ENV: &str = "AGENTOS_VIRTUAL_PROCESS_PPID";const NODE_VIRTUAL_PROCESS_UID_ENV: &str = "AGENTOS_VIRTUAL_PROCESS_UID";const NODE_VIRTUAL_PROCESS_GID_ENV: &str = "AGENTOS_VIRTUAL_PROCESS_GID";const NODE_PARENT_ALLOW_CHILD_PROCESS_ENV: &str = "AGENTOS_PARENT_NODE_ALLOW_CHILD_PROCESS";const NODE_PARENT_ALLOW_WORKER_ENV: &str = "AGENTOS_PARENT_NODE_ALLOW_WORKER";const NODE_EXTRA_FS_READ_PATHS_ENV: &str = "AGENTOS_EXTRA_FS_READ_PATHS";const NODE_EXTRA_FS_WRITE_PATHS_ENV: &str = "AGENTOS_EXTRA_FS_WRITE_PATHS";const NODE_ALLOWED_BUILTINS_ENV: &str = "AGENTOS_ALLOWED_NODE_BUILTINS";const NODE_LOOPBACK_EXEMPT_PORTS_ENV: &str = "AGENTOS_LOOPBACK_EXEMPT_PORTS";const NODE_SYNC_RPC_ENABLE_ENV: &str = "AGENTOS_NODE_SYNC_RPC_ENABLE";const NODE_SYNC_RPC_REQUEST_FD_ENV: &str = "AGENTOS_NODE_SYNC_RPC_REQUEST_FD";const NODE_SYNC_RPC_RESPONSE_FD_ENV: &str = "AGENTOS_NODE_SYNC_RPC_RESPONSE_FD";const NODE_SYNC_RPC_DATA_BYTES_ENV: &str = "AGENTOS_NODE_SYNC_RPC_DATA_BYTES";const NODE_SYNC_RPC_WAIT_TIMEOUT_MS_ENV: &str = "AGENTOS_NODE_SYNC_RPC_WAIT_TIMEOUT_MS";static NEXT_V8_SESSION_ID: AtomicU64 = AtomicU64::new(1);static JAVASCRIPT_TIMER_WHEEL: OnceLock<Arc<TimerWheel>> = OnceLock::new();static JAVASCRIPT_TIMER_WHEEL_INIT: Mutex<()> = Mutex::new(()); #[derive(Default)]struct JsStartPhaseStats {    calls: u64,    total_ns: u128,    max_ns: u128,} static JS_START_PHASES: OnceLock<Mutex<BTreeMap<String, JsStartPhaseStats>>> = OnceLock::new();static JS_EVENT_PHASES: OnceLock<Mutex<BTreeMap<String, JsStartPhaseStats>>> = OnceLock::new(); fn js_start_phases_enabled() -> bool {    std::env::var("AGENTOS_JS_START_PHASES").as_deref() == Ok("1")} fn js_event_phases_enabled() -> bool {    std::env::var("AGENTOS_JS_EVENT_PHASES").as_deref() == Ok("1")} fn record_js_start_phase(stage: &str, elapsed: Duration) {    if !js_start_phases_enabled() {        return;    }    record_js_phase_stats(        &JS_START_PHASES,        "AGENTOS_JS_START_PHASES_FILE",        stage,        elapsed,    );} fn record_js_event_phase(stage: &str, elapsed: Duration) {    if !js_event_phases_enabled() {        return;    }    record_js_phase_stats(        &JS_EVENT_PHASES,        "AGENTOS_JS_EVENT_PHASES_FILE",        stage,        elapsed,    );} fn record_js_phase_stats(    phases: &OnceLock<Mutex<BTreeMap<String, JsStartPhaseStats>>>,    path_env: &str,    stage: &str,    elapsed: Duration,) {    let phases = phases.get_or_init(|| Mutex::new(BTreeMap::new()));    let Ok(mut phases) = phases.lock() else {        return;    };    let stats = phases.entry(stage.to_string()).or_default();    stats.calls += 1;    let elapsed_ns = elapsed.as_nanos();    stats.total_ns += elapsed_ns;    stats.max_ns = stats.max_ns.max(elapsed_ns);     let Some(path) = std::env::var_os(path_env) else {        return;    };    let mut output = String::new();    for (stage, stats) in phases.iter() {        let total_us = stats.total_ns / 1_000;        let avg_us = if stats.calls == 0 {            0        } else {            total_us / u128::from(stats.calls)        };        let max_us = stats.max_ns / 1_000;        output.push_str(&format!(            "stage={stage} calls={} total_us={total_us} avg_us={avg_us} max_us={max_us}\n",            stats.calls        ));    }    let _ = fs::write(path, output);} const DEFAULT_V8_CPU_TIME_LIMIT_MS: u32 = 30_000;const DEFAULT_V8_WALL_CLOCK_LIMIT_MS: u32 = 0;const DEFAULT_NODE_IMPORT_CACHE_MATERIALIZE_TIMEOUT_MS: u64 = 30_000;const NODE_SYNC_RPC_DEFAULT_DATA_BYTES: usize = 4 * 1024 * 1024;const NODE_SYNC_RPC_DEFAULT_WAIT_TIMEOUT_MS: u64 = 30_000;const NODE_SYNC_RPC_RESPONSE_QUEUE_CAPACITY: usize = 1;const FORWARD_KERNEL_STDIN_RPC_ENV: &str = "AGENTOS_FORWARD_KERNEL_STDIN_RPC";// Defense-in-depth headroom: a transient burst of guest events (e.g. a chatty// tool/skill turn) should be absorbed by the buffer, so the producer only ever// hits backpressure under a genuinely stuck consumer rather than on every spike.const JAVASCRIPT_EVENT_CHANNEL_CAPACITY: usize = 512;const JAVASCRIPT_EVENT_PAYLOAD_LIMIT_BYTES: usize = 1024 * 1024;const JAVASCRIPT_CAPTURED_OUTPUT_LIMIT_BYTES: usize = 16 * 1024 * 1024;const KERNEL_STDIN_BUFFER_LIMIT_BYTES: usize = 16 * 1024 * 1024;const NODE_WARMUP_MARKER_VERSION: &str = "1";const NODE_WARMUP_SPECIFIERS: &[&str] = &[    "secure-exec:builtin/path",    "secure-exec:builtin/url",    "secure-exec:builtin/fs-promises",    "secure-exec:polyfill/path",]; #[derive(Debug, Default, Clone)]struct SyncBridgePhaseStats {    calls: u64,    total_us: u64,    max_us: u64,} static SYNC_BRIDGE_PHASES: OnceLock<Mutex<BTreeMap<String, SyncBridgePhaseStats>>> =    OnceLock::new();static SYNC_BRIDGE_REQUEST_ENQUEUED: OnceLock<Mutex<HashMap<u64, (String, Instant)>>> =    OnceLock::new(); fn sync_bridge_phases_enabled() -> bool {    std::env::var("AGENTOS_SYNC_BRIDGE_PHASES").as_deref() == Ok("1")} fn record_sync_bridge_phase(method: &str, stage: &str, elapsed: Duration) {    if !sync_bridge_phases_enabled() {        return;    }    let stats = SYNC_BRIDGE_PHASES.get_or_init(|| Mutex::new(BTreeMap::new()));    let Ok(mut stats) = stats.lock() else {        return;    };    let elapsed_us = elapsed.as_micros() as u64;    let key = format!("{method}:{stage}");    let entry = stats.entry(key).or_default();    entry.calls += 1;    entry.total_us = entry.total_us.wrapping_add(elapsed_us);    entry.max_us = entry.max_us.max(elapsed_us);     if let Ok(path) = std::env::var("AGENTOS_SYNC_BRIDGE_PHASES_FILE") {        let mut lines = String::new();        for (key, value) in stats.iter() {            let Some((method, stage)) = key.split_once(':') else {                continue;            };            let avg_us = value.total_us.checked_div(value.calls).unwrap_or(0);            lines.push_str(&format!(                "method={method} stage={stage} calls={} total_us={} avg_us={} max_us={}\n",                value.calls, value.total_us, avg_us, value.max_us            ));        }        let _ = fs::write(path, lines);    }} pub fn record_sync_bridge_request_enqueued(call_id: u64, method: &str) {    if !sync_bridge_phases_enabled() {        return;    }    let requests = SYNC_BRIDGE_REQUEST_ENQUEUED.get_or_init(|| Mutex::new(HashMap::new()));    let Ok(mut requests) = requests.lock() else {        return;    };    if requests.len() > 4096 {        requests.clear();    }    requests.insert(call_id, (method.to_owned(), Instant::now()));} pub fn record_sync_bridge_request_observed(call_id: u64, fallback_method: &str) {    if !sync_bridge_phases_enabled() {        return;    }    let Some(requests) = SYNC_BRIDGE_REQUEST_ENQUEUED.get() else {        return;    };    let Ok(mut requests) = requests.lock() else {        return;    };    let Some((method, started)) = requests.remove(&call_id) else {        return;    };    let method = if method.is_empty() {        fallback_method    } else {        method.as_str()    };    record_sync_bridge_phase(method, "request_service_observed", started.elapsed());}const CONTROLLED_STDERR_PREFIXES: &[&str] =    &[crate::node_import_cache::NODE_IMPORT_CACHE_METRICS_PREFIX];const RESERVED_NODE_ENV_KEYS: &[&str] = &[    NODE_BOOTSTRAP_ENV,    NODE_COMPILE_CACHE_ENV,    NODE_DISABLE_COMPILE_CACHE_ENV,    NODE_ENTRYPOINT_ENV,    NODE_EXTRA_FS_READ_PATHS_ENV,    NODE_EXTRA_FS_WRITE_PATHS_ENV,    NODE_SANDBOX_ROOT_ENV,    NODE_FROZEN_TIME_ENV,    NODE_GUEST_ENTRYPOINT_ENV,    NODE_GUEST_ENTRYPOINT_MODULE_MODE_ENV,    NODE_GUEST_ARGV_ENV,    NODE_GUEST_PATH_MAPPINGS_ENV,    NODE_VIRTUAL_PROCESS_EXEC_PATH_ENV,    NODE_VIRTUAL_PROCESS_PID_ENV,    NODE_VIRTUAL_PROCESS_PPID_ENV,    NODE_VIRTUAL_PROCESS_UID_ENV,    NODE_VIRTUAL_PROCESS_GID_ENV,    NODE_PARENT_ALLOW_CHILD_PROCESS_ENV,    NODE_PARENT_ALLOW_WORKER_ENV,    NODE_IMPORT_CACHE_ASSET_ROOT_ENV,    NODE_IMPORT_CACHE_LOADER_PATH_ENV,    NODE_IMPORT_CACHE_PATH_ENV,    NODE_KEEP_STDIN_OPEN_ENV,    NODE_ALLOWED_BUILTINS_ENV,    NODE_LOOPBACK_EXEMPT_PORTS_ENV,    NODE_SYNC_RPC_ENABLE_ENV,    NODE_SYNC_RPC_REQUEST_FD_ENV,    NODE_SYNC_RPC_RESPONSE_FD_ENV,    NODE_SYNC_RPC_DATA_BYTES_ENV,    NODE_SYNC_RPC_WAIT_TIMEOUT_MS_ENV,]; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]#[serde(tag = "type", rename_all = "snake_case")]enum NodeControlMessage {    NodeImportCacheMetrics {        metrics: serde_json::Value,    },    PythonExit {        #[serde(rename = "exitCode")]        exit_code: i32,    },    SignalState {        signal: u32,        registration: NodeSignalHandlerRegistration,    },} #[derive(Debug, Default)]struct LinePrefixFilter {    pending: Vec<u8>,} #[derive(Debug, Clone, PartialEq, Eq)]pub struct JavascriptSyncRpcRequest {    pub id: u64,    pub method: String,    pub args: Vec<Value>,    pub raw_bytes_args: HashMap<usize, Vec<u8>>,} #[derive(Debug, Deserialize)]struct JavascriptSyncRpcRequestWire {    id: u64,    method: String,    #[serde(default)]    args: Vec<Value>,} impl LinePrefixFilter {    fn filter_chunk(&mut self, chunk: &[u8], prefixes: &[&str]) -> Vec<u8> {        self.pending.extend_from_slice(chunk);        let mut filtered = Vec::new();         while let Some(newline_index) = self.pending.iter().position(|byte| *byte == b'\n') {            let line = self.pending.drain(..=newline_index).collect::<Vec<_>>();            if !has_control_prefix(&line, prefixes) {                filtered.extend_from_slice(&line);            }        }         filtered    }} fn has_control_prefix(line: &[u8], prefixes: &[&str]) -> bool {    let text = String::from_utf8_lossy(line);    let trimmed = text.trim_end_matches(['\r', '\n']);    prefixes.iter().any(|prefix| trimmed.starts_with(prefix))} #[cfg(test)]#[derive(Debug)]struct JavascriptSyncRpcResponseWriter {    sender: SyncSender<Vec<u8>>,    timeout: Duration,} #[cfg(test)]impl JavascriptSyncRpcResponseWriter {    fn new(writer: File, timeout: Duration) -> Self {        let (sender, receiver) = mpsc::sync_channel(NODE_SYNC_RPC_RESPONSE_QUEUE_CAPACITY);        spawn_javascript_sync_rpc_response_writer(writer, receiver);        Self { sender, timeout }    }     fn send(&self, payload: Vec<u8>) -> Result<(), JavascriptExecutionError> {        let started = Instant::now();        let mut payload = Some(payload);         loop {            match self                .sender                .try_send(payload.take().expect("payload should be present"))            {                Ok(()) => return Ok(()),                Err(TrySendError::Disconnected(_)) => {                    return Err(JavascriptExecutionError::RpcResponse(String::from(                        "JavaScript sync RPC response channel closed unexpectedly",                    )));                }                Err(TrySendError::Full(returned_payload)) => {                    if started.elapsed() >= self.timeout {                        return Err(JavascriptExecutionError::RpcResponse(format!(                            "timed out after {}ms while queueing JavaScript sync RPC response",                            self.timeout.as_millis()                        )));                    }                    payload = Some(returned_payload);                    thread::sleep(Duration::from_millis(5));                }            }        }    }} #[cfg(test)]impl Clone for JavascriptSyncRpcResponseWriter {    fn clone(&self) -> Self {        Self {            sender: self.sender.clone(),            timeout: self.timeout,        }    }} #[derive(Debug, Clone, Copy, PartialEq, Eq)]enum PendingSyncRpcState {    Pending(u64),    TimedOut(u64),} #[derive(Debug, Clone, Copy, PartialEq, Eq)]enum PendingSyncRpcResolution {    Pending,    TimedOut,    Missing,} #[derive(Debug, Clone, PartialEq, Eq)]pub struct CreateJavascriptContextRequest {    pub vm_id: String,    pub bootstrap_module: Option<String>,    pub compile_cache_root: Option<PathBuf>,} #[derive(Debug, Clone, PartialEq, Eq)]pub struct JavascriptContext {    pub context_id: String,    pub vm_id: String,    pub bootstrap_module: Option<String>,    pub compile_cache_dir: Option<PathBuf>,} /// Per-execution JavaScript runtime limits, carried as typed fields on the/// execution request rather than `AGENTOS_*` env vars. The sidecar populates/// these from the per-VM `VmLimits` (which originate from `CreateVmConfig` on/// the BARE wire); `None` selects the engine default. See the env-vs-wire rule/// in `crates/sidecar/CLAUDE.md`.#[derive(Debug, Clone, Default, PartialEq, Eq)]pub struct JavascriptExecutionLimits {    /// V8 heap cap in MB. `None`/`Some(0)` keeps the engine default heap.    pub v8_heap_limit_mb: Option<u32>,    /// Sync-RPC blocking-wait ceiling in ms. `None` keeps the engine default.    pub sync_rpc_wait_timeout_ms: Option<u64>,    /// Active JavaScript CPU-time budget in ms. `None` keeps the engine default;    /// `Some(0)` disables the CPU watchdog.    pub cpu_time_limit_ms: Option<u32>,    /// JavaScript wall-clock backstop in ms. `None` keeps the engine default;    /// `Some(0)` disables the wall-clock watchdog.    pub wall_clock_limit_ms: Option<u32>,    /// Timeout for materializing the per-VM Node import cache.    pub import_cache_materialize_timeout_ms: Option<u64>,    /// Maximum live JavaScript timers in this execution. `None` keeps the    /// engine default. The sidecar supplies the VM-scoped configured value.    pub max_timers: Option<usize>,    /// Maximum readiness identities delivered in one V8 turn. Sidecar VM    /// execution must supply `limits.reactor.workQuantum` here.    pub reactor_work_quantum: Option<usize>,    /// Per-call host bridge deadline. Sidecar VMs supply    /// `limits.reactor.operationDeadlineMs`; zero is invalid.    pub bridge_call_timeout_ms: Option<u64>,} /// Per-execution guest-runtime config carried as typed fields rather than/// `AGENTOS_*` env vars. The sidecar populates these from kernel state/// (`user_profile()`, `resource_limits()`) and `CreateVmConfig`; the runtime/// shim interpolates them into a `_processConfig` object the guest reads, so the/// guest's virtual identity no longer rides the ambient env channel. `None`/// keeps the guest-runtime default. See the env-vs-wire rule in/// `crates/sidecar/CLAUDE.md`.#[derive(Debug, Clone, Default, PartialEq, Eq)]pub struct GuestRuntimeConfig {    /// Virtual `process.pid`.    pub virtual_pid: Option<u64>,    /// Virtual `process.ppid`.    pub virtual_ppid: Option<u64>,    /// Virtual `process.uid` / `process.euid`.    pub virtual_uid: Option<u64>,    /// Virtual `process.gid` / `process.egid` / `process.groups`.    pub virtual_gid: Option<u64>,    /// Virtual `process.execPath`.    pub virtual_exec_path: Option<String>,    /// `os.cpus().length`.    pub os_cpu_count: Option<u64>,    /// `os.totalmem()` in bytes.    pub os_totalmem: Option<u64>,    /// `os.freemem()` in bytes.    pub os_freemem: Option<u64>,    /// `os.homedir()`.    pub os_homedir: Option<String>,    /// `os.hostname()`.    pub os_hostname: Option<String>,    /// `os.tmpdir()`.    pub os_tmpdir: Option<String>,    /// `os.type()`.    pub os_type: Option<String>,    /// `os.release()`.    pub os_release: Option<String>,    /// `os.version()`.    pub os_version: Option<String>,    /// `os.machine()`.    pub os_machine: Option<String>,    /// Default login shell.    pub os_shell: Option<String>,    /// `os.userInfo().username`.    pub os_user: Option<String>,

Lines 1–500 of 8917.