//! QuickJS runtime bridge for JS-compatible extensions.
//!
//! This module implements the PiJS runtime with Promise-based hostcall bridge:
//! - Async QuickJS runtime + context creation
//! - `pi` global object with Promise-returning hostcall methods
//! - Deterministic event loop scheduler integration
//! - call_id → Promise resolver mapping for hostcall completions
//! - Microtask draining after each macrotask
//!
//! # Architecture (bd-2ke)
//!
//! ```text
//! JS Code Rust Host
//! ------- ---------
//! pi.tool("read", {...}) --> enqueue HostcallRequest
//! returns Promise generate call_id
//! store (resolve, reject) track pending hostcall
//!
//! [scheduler tick] <-- host completes hostcall
//! delivers MacrotaskKind::HostcallComplete
//! lookup (resolve, reject) by call_id
//! resolve(result) or reject(error)
//! drain microtasks (Promises .then chains)
//! ```
use crate::error::{Error, Result};
use crate::hostcall_io_uring_lane::{
HostcallCapabilityClass, HostcallIoHint, IoUringLaneDecisionInput,
};
use crate::hostcall_queue::{
HOSTCALL_FAST_RING_CAPACITY, HOSTCALL_OVERFLOW_CAPACITY, HostcallQueueEnqueueResult,
HostcallQueueTelemetry, HostcallRequestQueue, QueueTenant,
};
use crate::scheduler::{Clock as SchedulerClock, HostcallOutcome, Scheduler, WallClock};
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use rquickjs::function::{Func, Opt};
use rquickjs::loader::{Loader as JsModuleLoader, Resolver as JsModuleResolver};
use rquickjs::module::Declared as JsModuleDeclared;
use rquickjs::{
AsyncContext, AsyncRuntime, Coerced, Ctx, Exception, FromJs, Function, IntoJs, Module, Object,
Value,
};
use sha2::{Digest, Sha256};
use std::cell::RefCell;
use std::cmp::Ordering;
use std::collections::{BTreeSet, BinaryHeap, HashMap, HashSet, VecDeque};
use std::fmt::Write as _;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering};
use std::time::{SystemTime, UNIX_EPOCH};
use std::{fs, path::Path, path::PathBuf};
use swc_common::{FileName, GLOBALS, Globals, Mark, SourceMap, sync::Lrc};
use swc_ecma_ast::{Module as SwcModule, Pass, Program as SwcProgram};
use swc_ecma_codegen::{Emitter, text_writer::JsWriter};
use swc_ecma_parser::{Parser as SwcParser, StringInput, Syntax, TsSyntax};
use swc_ecma_transforms_base::resolver;
use swc_ecma_transforms_typescript::strip;
use uuid::Uuid;
// ============================================================================
// Environment variable filtering (bd-1av0.9)
// ============================================================================
use crate::extensions::{
ExecMediationResult, ExtensionPolicy, ExtensionPolicyMode, SecretBrokerPolicy,
evaluate_exec_mediation,
};
/// Helper to check `exec` capability for sync execution where we cannot prompt.
fn check_exec_capability(policy: &ExtensionPolicy, extension_id: Option<&str>) -> bool {
let cap = "exec";
// 1. Per-extension overrides
if let Some(id) = extension_id {
if let Some(override_config) = policy.per_extension.get(id) {
if override_config.deny.iter().any(|c| c == cap) {
return false;
}
if override_config.allow.iter().any(|c| c == cap) {
return true;
}
if let Some(mode) = override_config.mode {
return match mode {
ExtensionPolicyMode::Permissive => true,
ExtensionPolicyMode::Strict | ExtensionPolicyMode::Prompt => false, // Prompt = deny for sync
};
}
}
}
// 2. Global deny
if policy.deny_caps.iter().any(|c| c == cap) {
return false;
}
// 3. Global allow (default_caps)
if policy.default_caps.iter().any(|c| c == cap) {
return true;
}
// 4. Mode fallback
match policy.mode {
ExtensionPolicyMode::Permissive => true,
ExtensionPolicyMode::Strict | ExtensionPolicyMode::Prompt => false, // Prompt = deny for sync
}
}
/// Determine whether an environment variable is safe to expose to extensions.
///
/// Uses the default `SecretBrokerPolicy` to block known sensitive patterns
/// (API keys, secrets, tokens, passwords, credentials).
pub fn is_env_var_allowed(key: &str) -> bool {
let policy = SecretBrokerPolicy::default();
// is_secret returns true if it IS a secret (should be blocked).
// So we allow it if it is NOT a secret.
!policy.is_secret(key)
}
fn parse_truthy_flag(value: &str) -> bool {
matches!(
value.trim().to_ascii_lowercase().as_str(),
"1" | "true" | "yes" | "on"
)
}
fn is_global_compat_scan_mode() -> bool {
cfg!(feature = "ext-conformance")
|| std::env::var("PI_EXT_COMPAT_SCAN").is_ok_and(|value| parse_truthy_flag(&value))
}
fn is_compat_scan_mode(env: &HashMap<String, String>) -> bool {
is_global_compat_scan_mode()
|| env
.get("PI_EXT_COMPAT_SCAN")
.is_some_and(|value| parse_truthy_flag(value))
}
/// Compatibility-mode fallback values for environment-gated extension registration.
///
/// This keeps conformance scans deterministic while preserving the default secret
/// filtering behavior in normal runtime mode.
fn compat_env_fallback_value(key: &str, env: &HashMap<String, String>) -> Option<String> {
if !is_compat_scan_mode(env) {
return None;
}
let upper = key.to_ascii_uppercase();
if upper.ends_with("_API_KEY") {
return Some(format!("pi-compat-{}", upper.to_ascii_lowercase()));
}
if upper == "PI_SEMANTIC_LEGACY" {
return Some("1".to_string());
}
None
}
// ============================================================================
// Promise Bridge Types (bd-2ke)
// ============================================================================
/// Type of hostcall being requested from JavaScript.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HostcallKind {
/// pi.tool(name, input) - invoke a tool
Tool { name: String },
/// pi.exec(cmd, args) - execute a shell command
Exec { cmd: String },
/// pi.http(request) - make an HTTP request
Http,
/// pi.session(op, args) - session operations
Session { op: String },
/// pi.ui(op, args) - UI operations
Ui { op: String },
/// pi.events(op, args) - event operations
Events { op: String },
/// pi.log(entry) - structured log emission
Log,
}
/// A hostcall request enqueued from JavaScript.
#[derive(Debug, Clone)]
pub struct HostcallRequest {
/// Unique identifier for correlation.
pub call_id: String,
/// Type of hostcall.
pub kind: HostcallKind,
/// JSON payload for the hostcall.
pub payload: serde_json::Value,
/// Trace ID for correlation with macrotask.
pub trace_id: u64,
/// Active extension id (when known) for policy/log correlation.
pub extension_id: Option<String>,
}
impl QueueTenant for HostcallRequest {
fn tenant_key(&self) -> Option<&str> {
self.extension_id.as_deref()
}
}
/// Tool definition registered by a JS extension.
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Debug, Clone, serde::Deserialize, PartialEq)]
pub struct ExtensionToolDef {
pub name: String,
#[serde(default)]
pub label: Option<String>,
pub description: String,
pub parameters: serde_json::Value,
}
/// Delegates to the canonical streaming implementation in `extensions.rs`.
fn hostcall_params_hash(method: &str, params: &serde_json::Value) -> String {
crate::extensions::hostcall_params_hash(method, params)
}
fn canonical_exec_params(cmd: &str, payload: &serde_json::Value) -> serde_json::Value {
let mut obj = match payload {
serde_json::Value::Object(map) => {
let mut out = map.clone();
out.remove("command");
out
}
serde_json::Value::Null => serde_json::Map::new(),
other => {
let mut out = serde_json::Map::new();
out.insert("payload".to_string(), other.clone());
out
}
};
obj.insert(
"cmd".to_string(),
serde_json::Value::String(cmd.to_string()),
);
serde_json::Value::Object(obj)
}
fn canonical_op_params(op: &str, payload: &serde_json::Value) -> serde_json::Value {
// Fast path: null payload (common for get_state, get_name, etc.) — build
// result directly without creating an intermediate Map.
if payload.is_null() {
return serde_json::json!({ "op": op });
}
let mut obj = match payload {
serde_json::Value::Object(map) => map.clone(),
other => {
let mut out = serde_json::Map::new();
// Reserved key for non-object args to avoid dropping semantics.
out.insert("payload".to_string(), other.clone());
out
}
};
// Explicit op from hostcall kind always wins.
obj.insert("op".to_string(), serde_json::Value::String(op.to_string()));
serde_json::Value::Object(obj)
}
fn builtin_tool_required_capability(name: &str) -> &'static str {
let name = name.trim();
if name.eq_ignore_ascii_case("read")
|| name.eq_ignore_ascii_case("grep")
|| name.eq_ignore_ascii_case("find")
|| name.eq_ignore_ascii_case("ls")
{
"read"
} else if name.eq_ignore_ascii_case("write") || name.eq_ignore_ascii_case("edit") {
"write"
} else if name.eq_ignore_ascii_case("bash") {
"exec"
} else {
"tool"
}
}
impl HostcallRequest {
#[must_use]
pub const fn method(&self) -> &'static str {
match self.kind {
HostcallKind::Tool { .. } => "tool",
HostcallKind::Exec { .. } => "exec",
HostcallKind::Http => "http",
HostcallKind::Session { .. } => "session",
HostcallKind::Ui { .. } => "ui",
HostcallKind::Events { .. } => "events",
HostcallKind::Log => "log",
}
}
#[must_use]
pub fn required_capability(&self) -> &'static str {
match &self.kind {
HostcallKind::Tool { name } => builtin_tool_required_capability(name),
HostcallKind::Exec { .. } => "exec",
HostcallKind::Http => "http",
HostcallKind::Session { .. } => "session",
HostcallKind::Ui { .. } => "ui",
HostcallKind::Events { .. } => "events",
HostcallKind::Log => "log",
}
}
#[must_use]
pub fn io_uring_capability_class(&self) -> HostcallCapabilityClass {
HostcallCapabilityClass::from_capability(self.required_capability())
}
#[must_use]
pub fn io_uring_io_hint(&self) -> HostcallIoHint {
match &self.kind {
HostcallKind::Http => HostcallIoHint::IoHeavy,
HostcallKind::Exec { .. } => HostcallIoHint::CpuBound,
HostcallKind::Tool { name } => {
let name = name.trim();
if name.eq_ignore_ascii_case("read")
|| name.eq_ignore_ascii_case("write")
|| name.eq_ignore_ascii_case("edit")
|| name.eq_ignore_ascii_case("grep")
|| name.eq_ignore_ascii_case("find")
|| name.eq_ignore_ascii_case("ls")
{
HostcallIoHint::IoHeavy
} else if name.eq_ignore_ascii_case("bash") {
HostcallIoHint::CpuBound
} else {
HostcallIoHint::Unknown
}
}
HostcallKind::Session { .. }
| HostcallKind::Ui { .. }
| HostcallKind::Events { .. }
| HostcallKind::Log => HostcallIoHint::Unknown,
}
}
#[must_use]
pub fn io_uring_lane_input(
&self,
queue_depth: usize,
force_compat_lane: bool,
) -> IoUringLaneDecisionInput {
IoUringLaneDecisionInput {
capability: self.io_uring_capability_class(),
io_hint: self.io_uring_io_hint(),
queue_depth,
force_compat_lane,
}
}
/// Build the canonical params shape for hashing.
///
/// **Canonical shapes** (must match `hostcall_request_to_payload()` in `extensions.rs`):
/// - `tool`: `{ "name": <tool_name>, "input": <payload> }`
/// - `exec`: `{ "cmd": <string>, ...payload_fields }`
/// - `http`: payload passthrough
/// - `session/ui/events`: `{ "op": <string>, ...payload_fields }` (flattened)
///
/// For non-object args to `session/ui/events`, payload is preserved under
/// a reserved `"payload"` key (e.g. `{ "op": "set_status", "payload": "ready" }`).
#[must_use]
pub fn params_for_hash(&self) -> serde_json::Value {
match &self.kind {
HostcallKind::Tool { name } => {
serde_json::json!({ "name": name, "input": self.payload.clone() })
}
HostcallKind::Exec { cmd } => canonical_exec_params(cmd, &self.payload),
HostcallKind::Http | HostcallKind::Log => self.payload.clone(),
HostcallKind::Session { op }
| HostcallKind::Ui { op }
| HostcallKind::Events { op } => canonical_op_params(op, &self.payload),
}
}
#[must_use]
pub fn params_hash(&self) -> String {
hostcall_params_hash(self.method(), &self.params_for_hash())
}
}
const MAX_JSON_DEPTH: usize = 64;
const MAX_JOBS_PER_TICK: usize = 10_000;
/// Convert a serde_json::Value to a rquickjs Value.
#[allow(clippy::option_if_let_else)]
pub(crate) fn json_to_js<'js>(
ctx: &Ctx<'js>,
value: &serde_json::Value,
) -> rquickjs::Result<Value<'js>> {
json_to_js_inner(ctx, value, 0)
}
fn json_to_js_inner<'js>(
ctx: &Ctx<'js>,
value: &serde_json::Value,
depth: usize,
) -> rquickjs::Result<Value<'js>> {
if depth > MAX_JSON_DEPTH {
return Err(rquickjs::Error::new_into_js_message(
"json",
"parse",
"JSON object too deep",
));
}
match value {
serde_json::Value::Null => Ok(Value::new_null(ctx.clone())),
serde_json::Value::Bool(b) => Ok(Value::new_bool(ctx.clone(), *b)),
serde_json::Value::Number(n) => n.as_i64().and_then(|i| i32::try_from(i).ok()).map_or_else(
|| {
n.as_f64().map_or_else(
|| Ok(Value::new_null(ctx.clone())),
|f| Ok(Value::new_float(ctx.clone(), f)),
)
},
|i| Ok(Value::new_int(ctx.clone(), i)),
),
// Gap D4: avoid cloning the String — pass &str directly to QuickJS.
serde_json::Value::String(s) => s.as_str().into_js(ctx),
serde_json::Value::Array(arr) => {
let js_arr = rquickjs::Array::new(ctx.clone())?;
for (i, v) in arr.iter().enumerate() {
let js_v = json_to_js_inner(ctx, v, depth + 1)?;
js_arr.set(i, js_v)?;
}
Ok(js_arr.into_value())
}
serde_json::Value::Object(obj) => {
let js_obj = Object::new(ctx.clone())?;
for (k, v) in obj {
let js_v = json_to_js_inner(ctx, v, depth + 1)?;
js_obj.set(k.as_str(), js_v)?;
}
Ok(js_obj.into_value())
}
}
}
/// Convert a rquickjs Value to a serde_json::Value.
pub(crate) fn js_to_json(value: &Value<'_>) -> rquickjs::Result<serde_json::Value> {
js_to_json_inner(value, 0)
}
fn js_to_json_inner(value: &Value<'_>, depth: usize) -> rquickjs::Result<serde_json::Value> {
if depth > MAX_JSON_DEPTH {
return Err(rquickjs::Error::new_into_js_message(
"json",
"stringify",
"Object too deep or contains cycles",
));
}
if value.is_null() || value.is_undefined() {
return Ok(serde_json::Value::Null);
}
if let Some(b) = value.as_bool() {
return Ok(serde_json::Value::Bool(b));
}
if let Some(i) = value.as_int() {
return Ok(serde_json::json!(i));
}
if let Some(f) = value.as_float() {
return Ok(serde_json::json!(f));
}
if let Some(s) = value.as_string() {
let s = s.to_string()?;
return Ok(serde_json::Value::String(s));
}
if let Some(arr) = value.as_array() {
let len = arr.len();
let mut result = Vec::with_capacity(len);
for i in 0..len {
let v: Value<'_> = arr.get(i)?;
result.push(js_to_json_inner(&v, depth + 1)?);
}
return Ok(serde_json::Value::Array(result));
}
if let Some(obj) = value.as_object() {
let mut result = serde_json::Map::new();
for item in obj.props::<String, Value<'_>>() {
let (k, v) = item?;
result.insert(k, js_to_json_inner(&v, depth + 1)?);
}
return Ok(serde_json::Value::Object(result));
}
// Fallback for functions, symbols, etc.
Ok(serde_json::Value::Null)
}
pub type HostcallQueue = Rc<RefCell<HostcallRequestQueue<HostcallRequest>>>;
// ============================================================================
// Deterministic PiJS Event Loop Scheduler (bd-8mm)
// ============================================================================
pub trait Clock: Send + Sync {
fn now_ms(&self) -> u64;
}
#[derive(Clone)]
pub struct ClockHandle(Arc<dyn Clock>);
impl ClockHandle {
pub fn new(clock: Arc<dyn Clock>) -> Self {
Self(clock)
}
}
impl Clock for ClockHandle {
fn now_ms(&self) -> u64 {
self.0.now_ms()
}
}
pub struct SystemClock;
impl Clock for SystemClock {
fn now_ms(&self) -> u64 {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
u64::try_from(now.as_millis()).unwrap_or(u64::MAX)
}
}
#[derive(Debug)]
pub struct ManualClock {
now_ms: AtomicU64,
}
impl ManualClock {
pub const fn new(start_ms: u64) -> Self {
Self {
now_ms: AtomicU64::new(start_ms),
}
}
pub fn set(&self, ms: u64) {
self.now_ms.store(ms, AtomicOrdering::SeqCst);
}
pub fn advance(&self, delta_ms: u64) {
self.now_ms.fetch_add(delta_ms, AtomicOrdering::SeqCst);
}
}
impl Clock for ManualClock {
fn now_ms(&self) -> u64 {
self.now_ms.load(AtomicOrdering::SeqCst)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MacrotaskKind {
TimerFired { timer_id: u64 },
HostcallComplete { call_id: String },
InboundEvent { event_id: String },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Macrotask {
pub seq: u64,
pub trace_id: u64,
pub kind: MacrotaskKind,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct MacrotaskEntry {
seq: u64,
trace_id: u64,
kind: MacrotaskKind,
}
impl Ord for MacrotaskEntry {
fn cmp(&self, other: &Self) -> Ordering {
self.seq.cmp(&other.seq)
}
}
impl PartialOrd for MacrotaskEntry {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct TimerEntry {
deadline_ms: u64,
order_seq: u64,
timer_id: u64,
trace_id: u64,
}
impl Ord for TimerEntry {
fn cmp(&self, other: &Self) -> Ordering {
(self.deadline_ms, self.order_seq, self.timer_id).cmp(&(
other.deadline_ms,
other.order_seq,
other.timer_id,
))
}
}
impl PartialOrd for TimerEntry {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct PendingMacrotask {
trace_id: u64,
kind: MacrotaskKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TickResult {
pub ran_macrotask: bool,
pub microtasks_drained: usize,
}
pub struct PiEventLoop {
clock: ClockHandle,
seq: u64,
next_timer_id: u64,
pending: VecDeque<PendingMacrotask>,
macro_queue: BinaryHeap<std::cmp::Reverse<MacrotaskEntry>>,
timers: BinaryHeap<std::cmp::Reverse<TimerEntry>>,
cancelled_timers: HashSet<u64>,
}
impl PiEventLoop {
pub fn new(clock: ClockHandle) -> Self {
Self {
clock,
seq: 0,
next_timer_id: 1,
pending: VecDeque::new(),
macro_queue: BinaryHeap::new(),
timers: BinaryHeap::new(),
cancelled_timers: HashSet::new(),
}
}
pub fn enqueue_hostcall_completion(&mut self, call_id: impl Into<String>) {
let trace_id = self.next_seq();
self.pending.push_back(PendingMacrotask {
trace_id,
kind: MacrotaskKind::HostcallComplete {
call_id: call_id.into(),
},
});
}
pub fn enqueue_inbound_event(&mut self, event_id: impl Into<String>) {
let trace_id = self.next_seq();
self.pending.push_back(PendingMacrotask {
trace_id,
kind: MacrotaskKind::InboundEvent {
event_id: event_id.into(),
},
});
}
pub fn set_timeout(&mut self, delay_ms: u64) -> u64 {
let timer_id = self.next_timer_id;
self.next_timer_id = self.next_timer_id.saturating_add(1);
let order_seq = self.next_seq();
let deadline_ms = self.clock.now_ms().saturating_add(delay_ms);
self.timers.push(std::cmp::Reverse(TimerEntry {
deadline_ms,
order_seq,
timer_id,
trace_id: order_seq,
}));
timer_id
}
pub fn clear_timeout(&mut self, timer_id: u64) -> bool {
let pending = self.timers.iter().any(|entry| entry.0.timer_id == timer_id)
&& !self.cancelled_timers.contains(&timer_id);
if pending {
self.cancelled_timers.insert(timer_id)
} else {
false
}
}
pub fn tick(
&mut self,
mut on_macrotask: impl FnMut(Macrotask),
mut drain_microtasks: impl FnMut() -> bool,
) -> TickResult {
self.ingest_pending();
self.enqueue_due_timers();
let mut ran_macrotask = false;
if let Some(task) = self.pop_next_macrotask() {
ran_macrotask = true;
on_macrotask(task);
}
let mut microtasks_drained = 0;
if ran_macrotask {
while drain_microtasks() {
microtasks_drained += 1;
}
}
TickResult {
ran_macrotask,
microtasks_drained,
}
}
fn ingest_pending(&mut self) {
while let Some(pending) = self.pending.pop_front() {
self.enqueue_macrotask(pending.trace_id, pending.kind);
}
}
fn enqueue_due_timers(&mut self) {
let now = self.clock.now_ms();
while let Some(std::cmp::Reverse(entry)) = self.timers.peek().cloned() {
if entry.deadline_ms > now {
break;
}
let _ = self.timers.pop();
if self.cancelled_timers.remove(&entry.timer_id) {
continue;
}
self.enqueue_macrotask(
entry.trace_id,
MacrotaskKind::TimerFired {
timer_id: entry.timer_id,
},
);
}
}
fn enqueue_macrotask(&mut self, trace_id: u64, kind: MacrotaskKind) {
let seq = self.next_seq();
self.macro_queue.push(std::cmp::Reverse(MacrotaskEntry {
seq,
trace_id,
kind,
}));
}
fn pop_next_macrotask(&mut self) -> Option<Macrotask> {
self.macro_queue.pop().map(|entry| {
let entry = entry.0;
Macrotask {
seq: entry.seq,
trace_id: entry.trace_id,
kind: entry.kind,
}
})
}
const fn next_seq(&mut self) -> u64 {
let current = self.seq;
self.seq = self.seq.saturating_add(1);
current
}
}
fn map_js_error(err: &rquickjs::Error) -> Error {
Error::extension(format!("QuickJS: {err:?}"))
}
fn format_quickjs_exception<'js>(ctx: &Ctx<'js>, caught: Value<'js>) -> String {
if let Ok(obj) = caught.clone().try_into_object() {
if let Some(exception) = Exception::from_object(obj) {
if let Some(message) = exception.message() {
if let Some(stack) = exception.stack() {
return format!("{message}\n{stack}");
}
return message;
}
if let Some(stack) = exception.stack() {
return stack;
}
}
}
match Coerced::<String>::from_js(ctx, caught) {
Ok(value) => value.0,
Err(err) => format!("(failed to stringify QuickJS exception: {err})"),
}
}
// ============================================================================
// Integrated PiJS Runtime with Promise Bridge (bd-2ke)
// ============================================================================
/// Classification of auto-repair patterns applied at extension load time.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RepairPattern {
/// Pattern 1: `./dist/X.js` resolved to `./src/X.ts` because the build
/// output directory was missing.
DistToSrc,
/// Pattern 2: `readFileSync` on a missing bundled asset (HTML/CSS/JS)
/// within the extension directory returned an empty string fallback.
MissingAsset,
/// Pattern 3: a monorepo sibling import (`../../shared`) was replaced
/// with a generated stub module.
MonorepoEscape,
/// Pattern 4: a bare npm package specifier was satisfied by a proxy-based
/// universal stub.
MissingNpmDep,
/// Pattern 5: CJS/ESM default-export mismatch was corrected by trying
/// alternative lifecycle method names.
ExportShape,
/// Pattern 6 (bd-k5q5.9.3.2): Extension manifest field normalization
/// (deprecated keys, schema version migration).
ManifestNormalization,
/// Pattern 7 (bd-k5q5.9.3.3): AST-based codemod for known API renames
/// or signature migrations.
ApiMigration,
}
/// Risk tier for repair patterns (bd-k5q5.9.1.4).
///
/// `Safe` patterns only remap file paths within the extension root and cannot
/// alter runtime behaviour. `Aggressive` patterns may introduce stub modules,
/// proxy objects, or change export shapes, potentially altering the extension's
/// observable behaviour.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RepairRisk {
/// Path-only remapping; no new code introduced.
Safe,
/// May inject stub modules or change export wiring.
Aggressive,
}
impl RepairPattern {
/// The risk tier of this pattern.
pub const fn risk(self) -> RepairRisk {
match self {
// Patterns 1, 2, 6: path remaps, empty strings, manifest JSON.
Self::DistToSrc | Self::MissingAsset | Self::ManifestNormalization => RepairRisk::Safe,
// Patterns 3-5 and 7: inject stubs, rewrite exports, or modify AST.
Self::MonorepoEscape | Self::MissingNpmDep | Self::ExportShape | Self::ApiMigration => {
RepairRisk::Aggressive
}
}
}
/// Whether this pattern is allowed under the given `RepairMode`.
pub const fn is_allowed_by(self, mode: RepairMode) -> bool {
match self.risk() {
RepairRisk::Safe => mode.should_apply(),
RepairRisk::Aggressive => mode.allows_aggressive(),
}
}
}
impl std::fmt::Display for RepairPattern {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::DistToSrc => write!(f, "dist_to_src"),
Self::MissingAsset => write!(f, "missing_asset"),
Self::MonorepoEscape => write!(f, "monorepo_escape"),
Self::MissingNpmDep => write!(f, "missing_npm_dep"),
Self::ExportShape => write!(f, "export_shape"),
Self::ManifestNormalization => write!(f, "manifest_normalization"),
Self::ApiMigration => write!(f, "api_migration"),
}
}
}
/// A structured record emitted whenever the runtime auto-repairs an extension
/// load failure.
#[derive(Debug, Clone)]
pub struct ExtensionRepairEvent {
/// Which extension triggered the repair.
pub extension_id: String,
/// Which pattern was applied.
pub pattern: RepairPattern,
/// The original error message that triggered the repair attempt.
pub original_error: String,
/// Human-readable description of the corrective action taken.
pub repair_action: String,
/// Whether the repair successfully resolved the load failure.
pub success: bool,
/// Wall-clock timestamp (ms since UNIX epoch).
pub timestamp_ms: u64,
}
// ---------------------------------------------------------------------------
// Deterministic rule registry (bd-k5q5.9.3.1)
// ---------------------------------------------------------------------------
/// A static repair rule with deterministic ordering and versioning.
#[derive(Debug, Clone)]
pub struct RepairRule {
/// Unique identifier for the rule (e.g. `"dist_to_src_v1"`).
pub id: &'static str,
/// Semantic version of this rule's logic.
pub version: &'static str,
/// Which `RepairPattern` this rule implements.
pub pattern: RepairPattern,
/// Human-readable description of what the rule does.
pub description: &'static str,
}
impl RepairRule {
/// Risk tier inherited from the pattern.
pub const fn risk(&self) -> RepairRisk {
self.pattern.risk()
}
/// Whether this rule can fire under the given mode.
pub const fn is_allowed_by(&self, mode: RepairMode) -> bool {
self.pattern.is_allowed_by(mode)
}
}
/// The canonical, deterministic rule registry.
///
/// Rules are evaluated **in array order** — the first applicable rule wins.
/// New rules MUST be appended; existing order must never change to preserve
/// determinism across versions.
pub static REPAIR_RULES: &[RepairRule] = &[
RepairRule {
id: "dist_to_src_v1",
pattern: RepairPattern::DistToSrc,
version: "1.0.0",
description: "Remap ./dist/X.js to ./src/X.ts when build output is missing",
},
RepairRule {
id: "missing_asset_v1",
pattern: RepairPattern::MissingAsset,
version: "1.0.0",
description: "Return empty string for missing bundled asset reads",
},
RepairRule {
id: "monorepo_escape_v1",
pattern: RepairPattern::MonorepoEscape,
version: "1.0.0",
description: "Stub monorepo sibling imports (../../shared) with empty module",
},
RepairRule {
id: "missing_npm_dep_v1",
pattern: RepairPattern::MissingNpmDep,
version: "1.0.0",
description: "Provide proxy-based stub for unresolvable npm bare specifiers",
},
RepairRule {
id: "export_shape_v1",
pattern: RepairPattern::ExportShape,
version: "1.0.0",
description: "Try alternative lifecycle exports (CJS default, named activate)",
},
// ── Manifest normalization rules (bd-k5q5.9.3.2) ──
RepairRule {
id: "manifest_schema_v1",
pattern: RepairPattern::ManifestNormalization,
version: "1.0.0",
description: "Migrate deprecated manifest fields to current schema",
},
// ── AST codemod rules (bd-k5q5.9.3.3) ──
RepairRule {
id: "api_migration_v1",
pattern: RepairPattern::ApiMigration,
version: "1.0.0",
description: "Rewrite known deprecated API calls to current equivalents",
},
];
/// Find all rules applicable under the given mode, in registry order.
pub fn applicable_rules(mode: RepairMode) -> Vec<&'static RepairRule> {
REPAIR_RULES
.iter()
.filter(|rule| rule.is_allowed_by(mode))
.collect()
}
/// Look up a rule by its ID.
pub fn rule_by_id(id: &str) -> Option<&'static RepairRule> {
REPAIR_RULES.iter().find(|r| r.id == id)
}
/// The registry version: bumped whenever rules are added or modified.
pub const REPAIR_REGISTRY_VERSION: &str = "1.1.0";
// ---------------------------------------------------------------------------
// Model patch primitive whitelist (bd-k5q5.9.4.1)
// ---------------------------------------------------------------------------
/// Primitive patch operations that model-generated repair proposals may use.
///
/// Each variant represents a constrained, validatable operation. The union of
/// all variants defines the complete vocabulary available to the model repair
/// adapter — anything outside this enum is rejected at the schema level.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PatchOp {
/// Replace a module import path with a different path.
/// Both paths must resolve within the extension root.
ReplaceModulePath { from: String, to: String },
/// Add a named export to a module's source text.
AddExport {
module_path: String,
export_name: String,
export_value: String,
},
/// Remove an import statement by specifier.
RemoveImport {
module_path: String,
specifier: String,
},
/// Inject a stub module at the given virtual path.
InjectStub {
virtual_path: String,
source: String,
},
/// Rewrite a `require()` call to use a different specifier.
RewriteRequire {
module_path: String,
from_specifier: String,
to_specifier: String,
},
}
impl PatchOp {
/// The risk tier of this operation.
pub const fn risk(&self) -> RepairRisk {
match self {
// Path remapping and require rewriting are safe (no new code).
Self::ReplaceModulePath { .. } | Self::RewriteRequire { .. } => RepairRisk::Safe,
// Adding exports, removing imports, or injecting stubs change code.
Self::AddExport { .. } | Self::RemoveImport { .. } | Self::InjectStub { .. } => {
RepairRisk::Aggressive
}
}
}
/// Short tag for logging and telemetry.
pub const fn tag(&self) -> &'static str {
match self {
Self::ReplaceModulePath { .. } => "replace_module_path",
Self::AddExport { .. } => "add_export",
Self::RemoveImport { .. } => "remove_import",
Self::InjectStub { .. } => "inject_stub",
Self::RewriteRequire { .. } => "rewrite_require",
}
}
}
/// A model-generated repair proposal.
///
/// Contains one or more `PatchOp`s plus metadata for audit. Proposals are
/// validated against the current `RepairMode` and monotonicity checker before
/// any operations are applied.
#[derive(Debug, Clone)]
pub struct PatchProposal {
/// Which rule triggered this proposal.
pub rule_id: String,
/// Ordered list of operations to apply.
pub ops: Vec<PatchOp>,
/// Model-provided rationale (for audit log).
pub rationale: String,
/// Confidence score (0.0–1.0) from the model, if available.
pub confidence: Option<f64>,
}
impl PatchProposal {
/// The highest risk across all ops in the proposal.
pub fn max_risk(&self) -> RepairRisk {
if self
.ops
.iter()
.any(|op| op.risk() == RepairRisk::Aggressive)
{
RepairRisk::Aggressive
} else {
RepairRisk::Safe
}
}
/// Whether this proposal is allowed under the given mode.
pub fn is_allowed_by(&self, mode: RepairMode) -> bool {
match self.max_risk() {
RepairRisk::Safe => mode.should_apply(),
RepairRisk::Aggressive => mode.allows_aggressive(),
}
}
/// Number of patch operations in this proposal.
pub fn op_count(&self) -> usize {
self.ops.len()
}
}
// ---------------------------------------------------------------------------
// Minimal-diff candidate selector and conflict resolver (bd-k5q5.9.3.4)
// ---------------------------------------------------------------------------
/// Outcome of conflict detection between two proposals.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConflictKind {
/// No conflict: the proposals touch different files/paths.
None,
/// Both proposals modify the same module path.
SameModulePath(String),
/// Both proposals inject stubs at the same virtual path.
SameVirtualPath(String),
}
impl ConflictKind {
/// True if there is no conflict.
pub const fn is_clear(&self) -> bool {
matches!(self, Self::None)
}
}
/// Detect conflicts between two `PatchProposal`s.
///
/// Two proposals conflict if they modify the same module path or inject
/// stubs at the same virtual path. This is a conservative check — any
/// overlap is treated as a conflict.
pub fn detect_conflict(a: &PatchProposal, b: &PatchProposal) -> ConflictKind {
for op_a in &a.ops {
for op_b in &b.ops {
if let Some(conflict) = ops_conflict(op_a, op_b) {
return conflict;
}
}
}
ConflictKind::None
}
/// Check if two individual ops conflict.
fn ops_conflict(a: &PatchOp, b: &PatchOp) -> Option<ConflictKind> {
match (a, b) {
(
PatchOp::ReplaceModulePath { from: fa, .. },
PatchOp::ReplaceModulePath { from: fb, .. },
) if fa == fb => Some(ConflictKind::SameModulePath(fa.clone())),
(
PatchOp::AddExport {
module_path: pa, ..
},
PatchOp::AddExport {
module_path: pb, ..
},
) if pa == pb => Some(ConflictKind::SameModulePath(pa.clone())),
(
PatchOp::RemoveImport {
module_path: pa, ..
},
PatchOp::RemoveImport {
module_path: pb, ..
},
) if pa == pb => Some(ConflictKind::SameModulePath(pa.clone())),
(
PatchOp::InjectStub {
virtual_path: va, ..
},
PatchOp::InjectStub {
virtual_path: vb, ..
},
) if va == vb => Some(ConflictKind::SameVirtualPath(va.clone())),
(
PatchOp::RewriteRequire {
module_path: pa,
from_specifier: sa,
..
},
PatchOp::RewriteRequire {
module_path: pb,
from_specifier: sb,
..
},
) if pa == pb && sa == sb => Some(ConflictKind::SameModulePath(pa.clone())),
_ => Option::None,
}
}
/// Select the best candidate from a set of proposals.
///
/// Candidates are ranked by:
/// 1. Lowest risk (Safe before Aggressive)
/// 2. Fewest operations (minimal diff)
/// 3. Highest confidence (if provided)
/// 4. Earliest rule ID (deterministic tiebreak)
///
/// Only candidates allowed by the given `RepairMode` are considered.
/// Returns `None` if no candidate is allowed.
pub fn select_best_candidate(
candidates: &[PatchProposal],
mode: RepairMode,
) -> Option<&PatchProposal> {
candidates
.iter()
.filter(|p| p.is_allowed_by(mode))
.min_by(|a, b| compare_proposals(a, b))
}
/// Compare two proposals for selection ordering.
fn compare_proposals(a: &PatchProposal, b: &PatchProposal) -> std::cmp::Ordering {
// 1. Lower risk wins.
let risk_ord = risk_rank(a.max_risk()).cmp(&risk_rank(b.max_risk()));
if risk_ord != std::cmp::Ordering::Equal {
return risk_ord;
}
// 2. Fewer ops wins.
let ops_ord = a.op_count().cmp(&b.op_count());
if ops_ord != std::cmp::Ordering::Equal {
return ops_ord;
}
// 3. Higher confidence wins (reverse order).
let conf_a = a.confidence.unwrap_or(0.0);
let conf_b = b.confidence.unwrap_or(0.0);
// Reverse: higher confidence = better = Less in ordering.
let conf_ord = conf_b
.partial_cmp(&conf_a)
.unwrap_or(std::cmp::Ordering::Equal);
if conf_ord != std::cmp::Ordering::Equal {
return conf_ord;
}
// 4. Lexicographic rule_id tiebreak.
a.rule_id.cmp(&b.rule_id)
}
/// Map `RepairRisk` to a numeric rank for ordering.
const fn risk_rank(risk: RepairRisk) -> u8 {
match risk {
RepairRisk::Safe => 0,
RepairRisk::Aggressive => 1,
}
}
/// Resolve conflicts among a set of proposals.
///
/// When two proposals conflict, the lower-ranked one (by
/// `compare_proposals`) is dropped. Returns a conflict-free subset.
pub fn resolve_conflicts(proposals: &[PatchProposal]) -> Vec<&PatchProposal> {
if proposals.is_empty() {
return vec![];
}
// Sort by selection order.
let mut indexed: Vec<(usize, &PatchProposal)> = proposals.iter().enumerate().collect();
indexed.sort_by(|(_, a), (_, b)| compare_proposals(a, b));
let mut accepted: Vec<&PatchProposal> = Vec::new();
for (_, candidate) in indexed {
let conflicts_with_accepted = accepted
.iter()
.any(|acc| !detect_conflict(acc, candidate).is_clear());
if !conflicts_with_accepted {
accepted.push(candidate);
}
}
accepted
}
// ---------------------------------------------------------------------------
// Bounded-context model proposer adapter (bd-k5q5.9.4.2)
// ---------------------------------------------------------------------------
/// Curated context provided to the model for repair proposal generation.
///
/// This struct is the *only* information the model sees. It deliberately
/// excludes secrets, full file contents, and anything outside the extension's
/// scope. The model can only produce proposals using the allowed primitives.
#[derive(Debug, Clone)]
pub struct RepairContext {
/// Extension identity.
pub extension_id: String,
/// The gating verdict (includes confidence and reason codes).
pub gating: GatingVerdict,
/// Normalized intent graph.
pub intent: IntentGraph,
/// Tolerant parse result.
pub parse: TolerantParseResult,
/// Current repair mode.
pub mode: RepairMode,
/// Diagnostic messages from the failed load attempt.
pub diagnostics: Vec<String>,
/// Allowed `PatchOp` tags for this mode.
pub allowed_op_tags: Vec<&'static str>,
}
impl RepairContext {
/// Build a repair context from constituent parts.
pub fn new(
extension_id: String,
gating: GatingVerdict,
intent: IntentGraph,
parse: TolerantParseResult,
mode: RepairMode,
diagnostics: Vec<String>,
) -> Self {
let allowed_op_tags = allowed_op_tags_for_mode(mode);
Self {
extension_id,
gating,
intent,
parse,
mode,
diagnostics,
allowed_op_tags,
}
}
}
/// Return the `PatchOp` tags allowed under the given repair mode.
pub fn allowed_op_tags_for_mode(mode: RepairMode) -> Vec<&'static str> {
let mut tags = Vec::new();
if mode.should_apply() {
// Safe ops always allowed when repairs are active.
tags.extend_from_slice(&["replace_module_path", "rewrite_require"]);
}
if mode.allows_aggressive() {
// Aggressive ops only in AutoStrict.
tags.extend_from_slice(&["add_export", "remove_import", "inject_stub"]);
}
tags
}
// ---------------------------------------------------------------------------
// Proposal validator and constrained applicator (bd-k5q5.9.4.3)
// ---------------------------------------------------------------------------
/// Validation error for a model-generated proposal.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProposalValidationError {
/// Proposal contains zero operations.
EmptyProposal,
/// An operation uses a tag not allowed by the current mode.
DisallowedOp { tag: String },
/// Risk level exceeds what the mode permits.
RiskExceedsMode { risk: RepairRisk, mode: RepairMode },
/// The `rule_id` does not match any known rule.
UnknownRule { rule_id: String },
/// Proposal references a path that escapes the extension root.
MonotonicityViolation { path: String },
}
impl std::fmt::Display for ProposalValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::EmptyProposal => write!(f, "proposal has no operations"),
Self::DisallowedOp { tag } => write!(f, "op '{tag}' not allowed in current mode"),
Self::RiskExceedsMode { risk, mode } => {
write!(f, "{risk:?} risk not allowed in {mode:?} mode")
}
Self::UnknownRule { rule_id } => write!(f, "unknown rule: {rule_id}"),
Self::MonotonicityViolation { path } => {
write!(f, "path escapes extension root: {path}")
}
}
}
}
/// Validate a `PatchProposal` against policy constraints.
///
/// Checks:
/// 1. Proposal is non-empty.
/// 2. All ops are in the allowed tag set for the mode.
/// 3. Overall risk does not exceed mode permissions.
/// 4. The rule_id references a known rule (if non-empty).
/// 5. Module paths stay within the extension root (monotonicity).
pub fn validate_proposal(
proposal: &PatchProposal,
mode: RepairMode,
extension_root: Option<&Path>,
) -> Vec<ProposalValidationError> {
let mut errors = Vec::new();
// 1. Non-empty.
if proposal.ops.is_empty() {
errors.push(ProposalValidationError::EmptyProposal);
return errors;
}
// 2. Allowed ops.
let allowed = allowed_op_tags_for_mode(mode);
for op in &proposal.ops {
if !allowed.contains(&op.tag()) {
errors.push(ProposalValidationError::DisallowedOp {
tag: op.tag().to_string(),
});
}
}
// 3. Risk check.
if !proposal.is_allowed_by(mode) {
errors.push(ProposalValidationError::RiskExceedsMode {
risk: proposal.max_risk(),
mode,
});
}
// 4. Known rule.
if !proposal.rule_id.is_empty() && rule_by_id(&proposal.rule_id).is_none() {
errors.push(ProposalValidationError::UnknownRule {
rule_id: proposal.rule_id.clone(),
});
}
// 5. Monotonicity for path-bearing ops.
if let Some(root) = extension_root {
for op in &proposal.ops {
let path_str = op_target_path(op);
let target = Path::new(&path_str);
if target.is_absolute() {
let verdict = verify_repair_monotonicity(root, root, target);
if !verdict.is_safe() {
errors.push(ProposalValidationError::MonotonicityViolation { path: path_str });
}
}
}
}
errors
}
/// Extract the target path from a `PatchOp`.
fn op_target_path(op: &PatchOp) -> String {
match op {
PatchOp::ReplaceModulePath { to, .. } => to.clone(),
PatchOp::AddExport { module_path, .. }
| PatchOp::RemoveImport { module_path, .. }
| PatchOp::RewriteRequire { module_path, .. } => module_path.clone(),
PatchOp::InjectStub { virtual_path, .. } => virtual_path.clone(),
}
}
/// Result of applying a validated proposal.
#[derive(Debug, Clone)]
pub struct ApplicationResult {
/// Whether the application succeeded.
pub success: bool,
/// Number of operations applied.
pub ops_applied: usize,
/// Human-readable summary.
pub summary: String,
}
/// Apply a validated proposal (dry-run: only validates and reports).
///
/// In the current implementation, actual file modifications are deferred
/// to the module loader. This function validates and produces an audit
/// record of what would be applied.
pub fn apply_proposal(
proposal: &PatchProposal,
mode: RepairMode,
extension_root: Option<&Path>,
) -> std::result::Result<ApplicationResult, Vec<ProposalValidationError>> {
let errors = validate_proposal(proposal, mode, extension_root);
if !errors.is_empty() {
return Err(errors);
}
Ok(ApplicationResult {
success: true,
ops_applied: proposal.ops.len(),
summary: format!(
"Applied {} op(s) from rule '{}'",
proposal.ops.len(),
proposal.rule_id
),
})
}
// ---------------------------------------------------------------------------
// Fail-closed human approval workflow (bd-k5q5.9.4.4)
// ---------------------------------------------------------------------------
/// Whether a proposal requires human approval before application.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ApprovalRequirement {
/// No approval needed — proposal can be applied automatically.
AutoApproved,
/// Human review required before applying.
RequiresApproval,
}
impl ApprovalRequirement {
/// True if human review is required.
pub const fn needs_approval(&self) -> bool {
matches!(self, Self::RequiresApproval)
}
}
impl std::fmt::Display for ApprovalRequirement {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::AutoApproved => write!(f, "auto_approved"),
Self::RequiresApproval => write!(f, "requires_approval"),
}
}
}
/// An approval request presented to the human reviewer.
#[derive(Debug, Clone)]
pub struct ApprovalRequest {
/// Extension being repaired.
pub extension_id: String,
/// The proposal awaiting approval.
pub proposal: PatchProposal,
/// Overall risk level.
pub risk: RepairRisk,
/// Confidence score from the scoring model.
pub confidence_score: f64,
/// Human-readable rationale from the proposal.
pub rationale: String,
/// Summary of what each operation does.
pub op_summaries: Vec<String>,
}
/// Human response to an approval request.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ApprovalResponse {
/// Approved: apply the proposal.
Approved,
/// Rejected: discard the proposal.
Rejected,
}
/// Determine whether a proposal requires human approval.
///
/// The decision is fail-closed: any high-risk indicator triggers the
/// approval requirement. A proposal requires approval if:
/// - It contains any Aggressive-risk operations, OR
/// - The confidence score is below the repairable threshold (0.5), OR
/// - The mode is `AutoStrict` and the proposal touches 3+ operations.
pub fn check_approval_requirement(
proposal: &PatchProposal,
confidence_score: f64,
) -> ApprovalRequirement {
if proposal.max_risk() == RepairRisk::Aggressive {
return ApprovalRequirement::RequiresApproval;
}
if confidence_score < 0.5 {
return ApprovalRequirement::RequiresApproval;
}
if proposal.ops.len() >= 3 {
return ApprovalRequirement::RequiresApproval;
}
ApprovalRequirement::AutoApproved
}
/// Build an approval request for human review.
pub fn build_approval_request(
extension_id: &str,
proposal: &PatchProposal,
confidence_score: f64,
) -> ApprovalRequest {
let op_summaries = proposal
.ops
.iter()
.map(|op| format!("[{}] {}", op.tag(), op_target_path(op)))
.collect();
ApprovalRequest {
extension_id: extension_id.to_string(),
proposal: proposal.clone(),
risk: proposal.max_risk(),
confidence_score,
rationale: proposal.rationale.clone(),
op_summaries,
}
}
// ---------------------------------------------------------------------------
// Structural validation gate (bd-k5q5.9.5.1)
// ---------------------------------------------------------------------------
/// Outcome of a structural validation check on a repaired artifact.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StructuralVerdict {
/// The artifact passed all structural checks.
Valid,
/// The file could not be read.
Unreadable { path: PathBuf, reason: String },
/// The file has an unsupported extension.
UnsupportedExtension { path: PathBuf, extension: String },
/// The file failed to parse as valid JS/TS/JSON.
ParseError { path: PathBuf, message: String },
}
impl StructuralVerdict {
/// Returns `true` when the artifact passed all checks.
pub const fn is_valid(&self) -> bool {
matches!(self, Self::Valid)
}
}
impl std::fmt::Display for StructuralVerdict {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Valid => write!(f, "valid"),
Self::Unreadable { path, reason } => {
write!(f, "unreadable: {} ({})", path.display(), reason)
}
Self::UnsupportedExtension { path, extension } => {
write!(
f,
"unsupported extension: {} (.{})",
path.display(),
extension
)
}
Self::ParseError { path, message } => {
write!(f, "parse error: {} ({})", path.display(), message)
}
}
}
}
/// Validate that a repaired artifact is structurally sound.
///
/// Performs three checks in order:
/// 1. **Readable** — the file can be read as UTF-8 text.
/// 2. **Supported extension** — `.ts`, `.tsx`, `.js`, `.mjs`, or `.json`.
/// 3. **Parseable** — SWC can parse `.ts`/`.tsx` files; JSON files are valid
/// JSON; `.js`/`.mjs` files are read successfully (syntax errors surface at
/// load time via QuickJS, but we verify readability here).
pub fn validate_repaired_artifact(path: &Path) -> StructuralVerdict {
// 1. Readable check.
let source = match fs::read_to_string(path) {
Ok(s) => s,
Err(err) => {
return StructuralVerdict::Unreadable {
path: path.to_path_buf(),
reason: err.to_string(),
};
}
};
// 2. Extension check.
let ext = path
.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
.to_ascii_lowercase();
match ext.as_str() {
"ts" | "tsx" => validate_typescript_parse(path, &source, &ext),
"js" | "mjs" => {
// JS files are loaded by QuickJS which reports its own syntax
// errors. We only verify readability (done above).
StructuralVerdict::Valid
}
"json" => validate_json_parse(path, &source),
_ => StructuralVerdict::UnsupportedExtension {
path: path.to_path_buf(),
extension: ext,
},
}
}
/// Try to parse a TypeScript/TSX source with SWC.
fn validate_typescript_parse(path: &Path, source: &str, ext: &str) -> StructuralVerdict {
use swc_common::{FileName, GLOBALS, Globals};
use swc_ecma_parser::{Parser as SwcParser, StringInput, Syntax, TsSyntax};
let globals = Globals::new();
GLOBALS.set(&globals, || {
let cm: swc_common::sync::Lrc<swc_common::SourceMap> = swc_common::sync::Lrc::default();
let fm = cm.new_source_file(
FileName::Custom(path.display().to_string()).into(),
source.to_string(),
);
let syntax = Syntax::Typescript(TsSyntax {
tsx: ext == "tsx",
decorators: true,
..Default::default()
});
let mut parser = SwcParser::new(syntax, StringInput::from(&*fm), None);
match parser.parse_module() {
Ok(_) => StructuralVerdict::Valid,
Err(err) => StructuralVerdict::ParseError {
path: path.to_path_buf(),
message: format!("{err:?}"),
},
}
})
}
/// Validate that JSON source is well-formed.
fn validate_json_parse(path: &Path, source: &str) -> StructuralVerdict {
match serde_json::from_str::<serde_json::Value>(source) {
Ok(_) => StructuralVerdict::Valid,
Err(err) => StructuralVerdict::ParseError {
path: path.to_path_buf(),
message: err.to_string(),
},
}
}
// ---------------------------------------------------------------------------
// Tolerant AST recovery and ambiguity detection (bd-k5q5.9.2.2)
// ---------------------------------------------------------------------------
/// A construct in the source that reduces repair confidence.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum AmbiguitySignal {
/// Source contains `eval(...)` — arbitrary code execution.
DynamicEval,
/// Source contains `new Function(...)` — dynamic function construction.
DynamicFunction,
/// Source contains `import(...)` — dynamic import expression.
DynamicImport,
/// Source contains `export * from` — star re-export hides export shape.
StarReExport,
/// Source contains `require(` with a non-literal argument.
DynamicRequire,
/// Source contains `new Proxy(` — metaprogramming.
ProxyUsage,
/// Source contains `with (` — deprecated scope-altering statement.
WithStatement,
/// SWC parser produced recoverable errors.
RecoverableParseErrors { count: usize },
}
impl AmbiguitySignal {
/// Severity weight (0.0–1.0) for confidence scoring.
pub fn weight(&self) -> f64 {
match self {
Self::DynamicEval | Self::DynamicFunction => 0.9,
Self::ProxyUsage | Self::WithStatement => 0.7,
Self::DynamicImport | Self::DynamicRequire => 0.5,
Self::StarReExport => 0.3,
Self::RecoverableParseErrors { count } => {
// More errors → more ambiguous, capped at 1.0.
(f64::from(u32::try_from(*count).unwrap_or(u32::MAX)) * 0.2).min(1.0)
}
}
}
/// Short tag for logging.
pub const fn tag(&self) -> &'static str {
match self {
Self::DynamicEval => "dynamic_eval",
Self::DynamicFunction => "dynamic_function",
Self::DynamicImport => "dynamic_import",
Self::StarReExport => "star_reexport",
Self::DynamicRequire => "dynamic_require",
Self::ProxyUsage => "proxy_usage",
Self::WithStatement => "with_statement",
Self::RecoverableParseErrors { .. } => "recoverable_parse_errors",
}
}
}
impl std::fmt::Display for AmbiguitySignal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::RecoverableParseErrors { count } => {
write!(f, "{}({})", self.tag(), count)
}
_ => write!(f, "{}", self.tag()),
}
}
}
/// Result of tolerant parsing: partial analysis even when source has errors.
#[derive(Debug, Clone)]
pub struct TolerantParseResult {
/// Whether the source parsed without fatal errors.
pub parsed_ok: bool,
/// Number of top-level statements recovered (0 if parse failed fatally).
pub statement_count: usize,
/// Number of import/export declarations found.
pub import_export_count: usize,
/// Detected ambiguity signals that reduce repair confidence.
pub ambiguities: Vec<AmbiguitySignal>,
}
impl TolerantParseResult {
/// Overall ambiguity score (0.0 = fully legible, 1.0 = fully opaque).
pub fn ambiguity_score(&self) -> f64 {
if self.ambiguities.is_empty() {
return 0.0;
}
// Take the max weight — one high-severity signal dominates.
self.ambiguities
.iter()
.map(AmbiguitySignal::weight)
.fold(0.0_f64, f64::max)
}
/// True if the source is sufficiently legible for automated repair.
pub fn is_legible(&self) -> bool {
self.parsed_ok && self.ambiguity_score() < 0.8
}
}
/// Perform tolerant parsing and ambiguity detection on source text.
///
/// Attempts SWC parse for `.ts`/`.tsx` files to count statements
/// and imports. For all supported extensions, scans source text
/// for ambiguity patterns. Returns partial results even on parse
/// failure.
pub fn tolerant_parse(source: &str, filename: &str) -> TolerantParseResult {
let ext = Path::new(filename)
.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
.to_ascii_lowercase();
let (parsed_ok, statement_count, import_export_count, parse_errors) = match ext.as_str() {
"ts" | "tsx" | "js" | "mjs" => try_swc_parse(source, filename, &ext),
_ => (false, 0, 0, 0),
};
let mut ambiguities = detect_ambiguity_patterns(source);
if parse_errors > 0 {
ambiguities.push(AmbiguitySignal::RecoverableParseErrors {
count: parse_errors,
});
}
// Deduplicate.
let mut seen = std::collections::HashSet::new();
ambiguities.retain(|s| seen.insert(s.clone()));
TolerantParseResult {
parsed_ok,
statement_count,
import_export_count,
ambiguities,
}
}
/// Attempt SWC parse and return (ok, stmts, imports, error_count).
fn try_swc_parse(source: &str, filename: &str, ext: &str) -> (bool, usize, usize, usize) {
use swc_common::{FileName, GLOBALS, Globals};
use swc_ecma_parser::{Parser as SwcParser, StringInput, Syntax, TsSyntax};
let globals = Globals::new();
GLOBALS.set(&globals, || {
let cm: swc_common::sync::Lrc<swc_common::SourceMap> = swc_common::sync::Lrc::default();
let fm = cm.new_source_file(
FileName::Custom(filename.to_string()).into(),
source.to_string(),
);
let is_ts = ext == "ts" || ext == "tsx";
let syntax = if is_ts {
Syntax::Typescript(TsSyntax {
tsx: ext == "tsx",
decorators: true,
..Default::default()
})
} else {
Syntax::Es(swc_ecma_parser::EsSyntax {
jsx: true,
..Default::default()
})
};
let mut parser = SwcParser::new(syntax, StringInput::from(&*fm), None);
if let Ok(module) = parser.parse_module() {
let errors = parser.take_errors();
let stmts = module.body.len();
let imports = module
.body
.iter()
.filter(|item| {
matches!(
item,
swc_ecma_ast::ModuleItem::ModuleDecl(
swc_ecma_ast::ModuleDecl::Import(_)
| swc_ecma_ast::ModuleDecl::ExportAll(_)
| swc_ecma_ast::ModuleDecl::ExportNamed(_)
| swc_ecma_ast::ModuleDecl::ExportDefaultDecl(_)
| swc_ecma_ast::ModuleDecl::ExportDefaultExpr(_)
| swc_ecma_ast::ModuleDecl::ExportDecl(_)
)
)
})
.count();
(true, stmts, imports, errors.len())
} else {
let errors = parser.take_errors();
// Fatal parse error — report 0 statements but count errors.
(false, 0, 0, errors.len() + 1)
}
})
}
/// Detect ambiguity patterns in source text.
fn detect_ambiguity_patterns(source: &str) -> Vec<AmbiguitySignal> {
use std::sync::OnceLock;
static PATTERNS: OnceLock<Vec<(regex::Regex, AmbiguitySignal)>> = OnceLock::new();
static DYN_REQUIRE: OnceLock<regex::Regex> = OnceLock::new();
let patterns = PATTERNS.get_or_init(|| {
vec![
(
regex::Regex::new(r"\beval\s*\(").expect("regex"),
AmbiguitySignal::DynamicEval,
),
(
regex::Regex::new(r"\bnew\s+Function\s*\(").expect("regex"),
AmbiguitySignal::DynamicFunction,
),
(
regex::Regex::new(r"\bimport\s*\(").expect("regex"),
AmbiguitySignal::DynamicImport,
),
(
regex::Regex::new(r"export\s+\*\s+from\b").expect("regex"),
AmbiguitySignal::StarReExport,
),
(
regex::Regex::new(r"\bnew\s+Proxy\s*\(").expect("regex"),
AmbiguitySignal::ProxyUsage,
),
(
regex::Regex::new(r"\bwith\s*\(").expect("regex"),
AmbiguitySignal::WithStatement,
),
]
});
let dyn_require = DYN_REQUIRE
.get_or_init(|| regex::Regex::new(r#"\brequire\s*\(\s*[^"'`\s)]"#).expect("regex"));
let mut signals = Vec::new();
for (re, signal) in patterns {
if re.is_match(source) {
signals.push(signal.clone());
}
}
if dyn_require.is_match(source) {
signals.push(AmbiguitySignal::DynamicRequire);
}
signals
}
// ---------------------------------------------------------------------------
// Intent graph extractor (bd-k5q5.9.2.1)
// ---------------------------------------------------------------------------
/// A normalized intent signal extracted from an extension.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum IntentSignal {
/// The extension registers a tool with the given name.
RegistersTool(String),
/// The extension registers a slash command with the given name.
RegistersCommand(String),
/// The extension registers a keyboard shortcut.
RegistersShortcut(String),
/// The extension registers a feature flag with the given name.
RegistersFlag(String),
/// The extension registers a custom LLM provider.
RegistersProvider(String),
/// The extension hooks into a lifecycle event.
HooksEvent(String),
/// The extension declares a capability requirement.
RequiresCapability(String),
/// The extension registers a message renderer.
RegistersRenderer(String),
}
impl IntentSignal {
/// Category tag for logging and grouping.
pub const fn category(&self) -> &'static str {
match self {
Self::RegistersTool(_) => "tool",
Self::RegistersCommand(_) => "command",
Self::RegistersShortcut(_) => "shortcut",
Self::RegistersFlag(_) => "flag",
Self::RegistersProvider(_) => "provider",
Self::HooksEvent(_) => "event_hook",
Self::RequiresCapability(_) => "capability",
Self::RegistersRenderer(_) => "renderer",
}
}
/// The name/identifier within the signal.
pub fn name(&self) -> &str {
match self {
Self::RegistersTool(n)
| Self::RegistersCommand(n)
| Self::RegistersShortcut(n)
| Self::RegistersFlag(n)
| Self::RegistersProvider(n)
| Self::HooksEvent(n)
| Self::RequiresCapability(n)
| Self::RegistersRenderer(n) => n,
}
}
}
impl std::fmt::Display for IntentSignal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}", self.category(), self.name())
}
}
/// Normalized intent graph for a single extension.
///
/// Captures every registration and capability declaration the extension makes,
/// providing a complete picture of what the extension *intends* to do. Used by
/// the confidence scoring model to decide whether automated repair is safe.
#[derive(Debug, Clone, Default)]
pub struct IntentGraph {
/// Extension identity.
pub extension_id: String,
/// All extracted intent signals (deduplicated).
pub signals: Vec<IntentSignal>,
}
impl IntentGraph {
/// Build an intent graph from a `RegisterPayload` and capability list.
pub fn from_register_payload(
extension_id: &str,
payload: &serde_json::Value,
capabilities: &[String],
) -> Self {
let mut signals = Vec::new();
// Extract tools.
if let Some(tools) = payload.get("tools").and_then(|v| v.as_array()) {
for tool in tools {
if let Some(name) = tool.get("name").and_then(|n| n.as_str()) {
signals.push(IntentSignal::RegistersTool(name.to_string()));
}
}
}
// Extract slash commands.
if let Some(cmds) = payload.get("slash_commands").and_then(|v| v.as_array()) {
for cmd in cmds {
if let Some(name) = cmd.get("name").and_then(|n| n.as_str()) {
signals.push(IntentSignal::RegistersCommand(name.to_string()));
}
}
}
// Extract shortcuts.
if let Some(shortcuts) = payload.get("shortcuts").and_then(|v| v.as_array()) {
for sc in shortcuts {
let label = sc
.get("name")
.or_else(|| sc.get("key"))
.and_then(|n| n.as_str())
.unwrap_or("unknown");
signals.push(IntentSignal::RegistersShortcut(label.to_string()));
}
}
// Extract flags.
if let Some(flags) = payload.get("flags").and_then(|v| v.as_array()) {
for flag in flags {
if let Some(name) = flag.get("name").and_then(|n| n.as_str()) {
signals.push(IntentSignal::RegistersFlag(name.to_string()));
}
}
}
// Extract event hooks.
if let Some(hooks) = payload.get("event_hooks").and_then(|v| v.as_array()) {
for hook in hooks {
if let Some(name) = hook.as_str() {
signals.push(IntentSignal::HooksEvent(name.to_string()));
}
}
}
// Capability declarations.
for cap in capabilities {
signals.push(IntentSignal::RequiresCapability(cap.clone()));
}
// Deduplicate while preserving order.
let mut seen = std::collections::HashSet::new();
signals.retain(|s| seen.insert(s.clone()));
Self {
extension_id: extension_id.to_string(),
signals,
}
}
/// Return signals of a specific category.
pub fn signals_by_category(&self, category: &str) -> Vec<&IntentSignal> {
self.signals
.iter()
.filter(|s| s.category() == category)
.collect()
}
/// Number of distinct signal categories present.
pub fn category_count(&self) -> usize {
let cats: std::collections::HashSet<&str> =
self.signals.iter().map(IntentSignal::category).collect();
cats.len()
}
/// True if the graph contains no signals at all.
pub fn is_empty(&self) -> bool {
self.signals.is_empty()
}
/// Total number of signals.
pub fn signal_count(&self) -> usize {
self.signals.len()
}
}
// ---------------------------------------------------------------------------
// Confidence scoring model (bd-k5q5.9.2.3)
// ---------------------------------------------------------------------------
/// An individual reason contributing to the confidence score.
#[derive(Debug, Clone)]
pub struct ConfidenceReason {
/// Short machine-readable code (e.g., "parsed_ok", "has_tools").
pub code: String,
/// Human-readable explanation.
pub explanation: String,
/// How much this reason contributes (+) or penalizes (-) the score.
pub delta: f64,
}
/// Result of confidence scoring: a score plus explainable reasons.
#[derive(Debug, Clone)]
pub struct ConfidenceReport {
/// Overall confidence (0.0–1.0). Higher = more legible / safer to repair.
pub score: f64,
/// Ordered list of reasons that contributed to the score.
pub reasons: Vec<ConfidenceReason>,
}
impl ConfidenceReport {
/// True if the confidence is high enough for automated repair.
pub fn is_repairable(&self) -> bool {
self.score >= 0.5
}
/// True if the confidence is high enough only for suggest mode.
pub fn is_suggestable(&self) -> bool {
self.score >= 0.2
}
}
/// Compute legibility confidence from intent graph and parse results.
///
/// The model is deterministic: same inputs always produce the same score.
/// The score starts at a base of 0.5 and is adjusted by weighted signals:
///
/// **Positive signals** (increase confidence):
/// - Source parsed successfully
/// - Extension registers at least one tool/command/hook
/// - Multiple intent categories present (well-structured extension)
///
/// **Negative signals** (decrease confidence):
/// - Parse failed
/// - Ambiguity detected (weighted by severity)
/// - No registrations (opaque extension)
/// - Zero statements recovered
#[allow(clippy::too_many_lines)]
pub fn compute_confidence(intent: &IntentGraph, parse: &TolerantParseResult) -> ConfidenceReport {
let mut score: f64 = 0.5;
let mut reasons = Vec::new();
// ── Parse quality ────────────────────────────────────────────────────
if parse.parsed_ok {
let delta = 0.15;
score += delta;
reasons.push(ConfidenceReason {
code: "parsed_ok".to_string(),
explanation: "Source parsed without fatal errors".to_string(),
delta,
});
} else {
let delta = -0.3;
score += delta;
reasons.push(ConfidenceReason {
code: "parse_failed".to_string(),
explanation: "Source failed to parse".to_string(),
delta,
});
}
// ── Statement count ──────────────────────────────────────────────────
if parse.statement_count == 0 && parse.parsed_ok {
let delta = -0.1;
score += delta;
reasons.push(ConfidenceReason {
code: "empty_module".to_string(),
explanation: "Module has no statements".to_string(),
delta,
});
}
// ── Import/export presence ───────────────────────────────────────────
if parse.import_export_count > 0 {
let delta = 0.05;
score += delta;
reasons.push(ConfidenceReason {
code: "has_imports_exports".to_string(),
explanation: format!(
"{} import/export declarations found",
parse.import_export_count
),
delta,
});
}
// ── Ambiguity penalties ──────────────────────────────────────────────
for ambiguity in &parse.ambiguities {
let weight = ambiguity.weight();
let delta = -weight * 0.3;
score += delta;
reasons.push(ConfidenceReason {
code: format!("ambiguity_{}", ambiguity.tag()),
explanation: format!("Ambiguity detected: {ambiguity} (weight={weight:.1})"),
delta,
});
}
// ── Intent signal richness ───────────────────────────────────────────
let tool_count = intent.signals_by_category("tool").len();
if tool_count > 0 {
let delta = 0.1;
score += delta;
reasons.push(ConfidenceReason {
code: "has_tools".to_string(),
explanation: format!("{tool_count} tool(s) registered"),
delta,
});
}
let hook_count = intent.signals_by_category("event_hook").len();
if hook_count > 0 {
let delta = 0.05;
score += delta;
reasons.push(ConfidenceReason {
code: "has_event_hooks".to_string(),
explanation: format!("{hook_count} event hook(s) registered"),
delta,
});
}
let categories = intent.category_count();
if categories >= 3 {
let delta = 0.1;
score += delta;
reasons.push(ConfidenceReason {
code: "multi_category".to_string(),
explanation: format!("{categories} distinct intent categories"),
delta,
});
}
if intent.is_empty() && parse.parsed_ok {
let delta = -0.15;
score += delta;
reasons.push(ConfidenceReason {
code: "no_registrations".to_string(),
explanation: "No tools, commands, or hooks registered".to_string(),
delta,
});
}
// Clamp to [0.0, 1.0].
score = score.clamp(0.0, 1.0);
ConfidenceReport { score, reasons }
}
// ---------------------------------------------------------------------------
// Gating decision API (bd-k5q5.9.2.4)
// ---------------------------------------------------------------------------
/// The repair gating decision: what action the system should take.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum GatingDecision {
/// Extension is legible and safe for automated repair.
Allow,
/// Extension is partially legible; suggest repairs but do not auto-apply.
Suggest,
/// Extension is too opaque or risky; deny automated repair.
Deny,
}
impl GatingDecision {
/// Short label for structured logging.
pub const fn label(&self) -> &'static str {
match self {
Self::Allow => "allow",
Self::Suggest => "suggest",
Self::Deny => "deny",
}
}
}
impl std::fmt::Display for GatingDecision {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.label())
}
}
/// A structured reason code explaining why a gating decision was made.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GatingReasonCode {
/// Machine-readable code (e.g., "low_confidence", "parse_failed").
pub code: String,
/// Human-readable remediation guidance.
pub remediation: String,
}
/// Full gating verdict: decision + confidence + reason codes.
#[derive(Debug, Clone)]
pub struct GatingVerdict {
/// The decision: allow / suggest / deny.
pub decision: GatingDecision,
/// The underlying confidence report.
pub confidence: ConfidenceReport,
/// Structured reason codes (empty for Allow).
pub reason_codes: Vec<GatingReasonCode>,
}
impl GatingVerdict {
/// Whether the verdict permits automated repair.
pub fn allows_repair(&self) -> bool {
self.decision == GatingDecision::Allow
}
/// Whether the verdict permits at least suggestion output.
pub const fn allows_suggestion(&self) -> bool {
matches!(
self.decision,
GatingDecision::Allow | GatingDecision::Suggest
)
}
}
/// Compute the gating verdict from intent graph and parse results.
///
/// Combines `compute_confidence` with threshold-based decision logic:
/// - score >= 0.5 → Allow
/// - 0.2 <= score < 0.5 → Suggest
/// - score < 0.2 → Deny
///
/// Reason codes are generated for Suggest and Deny decisions to guide
/// the user on what needs to change for the extension to become repairable.
pub fn compute_gating_verdict(intent: &IntentGraph, parse: &TolerantParseResult) -> GatingVerdict {
let confidence = compute_confidence(intent, parse);
let decision = if confidence.is_repairable() {
GatingDecision::Allow
} else if confidence.is_suggestable() {
GatingDecision::Suggest
} else {
GatingDecision::Deny
};
let reason_codes = if decision == GatingDecision::Allow {
vec![]
} else {
build_reason_codes(&confidence, parse)
};
GatingVerdict {
decision,
confidence,
reason_codes,
}
}
/// Generate structured reason codes with remediation guidance.
fn build_reason_codes(
confidence: &ConfidenceReport,
parse: &TolerantParseResult,
) -> Vec<GatingReasonCode> {
let mut codes = Vec::new();
if !parse.parsed_ok {
codes.push(GatingReasonCode {
code: "parse_failed".to_string(),
remediation: "Fix syntax errors in the extension source code".to_string(),
});
}
for ambiguity in &parse.ambiguities {
if ambiguity.weight() >= 0.7 {
codes.push(GatingReasonCode {
code: format!("high_ambiguity_{}", ambiguity.tag()),
remediation: format!(
"Remove or refactor {} usage to improve repair safety",
ambiguity.tag().replace('_', " ")
),
});
}
}
if confidence.score < 0.2 {
codes.push(GatingReasonCode {
code: "very_low_confidence".to_string(),
remediation: "Extension is too opaque for automated analysis; \
add explicit tool/hook registrations and remove dynamic constructs"
.to_string(),
});
}
codes
}
/// Statistics from a tick execution.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct PiJsTickStats {
/// Whether a macrotask was executed.
pub ran_macrotask: bool,
/// Number of microtask drain iterations.
pub microtask_drains: usize,
/// Number of pending QuickJS jobs drained.
pub jobs_drained: usize,
/// Number of pending hostcalls (in-flight Promises).
pub pending_hostcalls: usize,
/// Total hostcalls issued by this runtime.
pub hostcalls_total: u64,
/// Total hostcalls timed out by this runtime.
pub hostcalls_timed_out: u64,
/// Last observed QuickJS `memory_used_size` in bytes.
pub memory_used_bytes: u64,
/// Peak observed QuickJS `memory_used_size` in bytes.
pub peak_memory_used_bytes: u64,
/// Number of auto-repair events recorded since the runtime was created.
pub repairs_total: u64,
/// Number of module cache hits accumulated by this runtime.
pub module_cache_hits: u64,
/// Number of module cache misses accumulated by this runtime.
pub module_cache_misses: u64,
/// Number of module cache invalidations accumulated by this runtime.
pub module_cache_invalidations: u64,
/// Number of module entries currently retained in the cache.
pub module_cache_entries: u64,
/// Number of disk cache hits (transpiled source loaded from persistent storage).
pub module_disk_cache_hits: u64,
}
#[derive(Debug, Clone, Default)]
pub struct PiJsRuntimeLimits {
/// Limit runtime heap usage (QuickJS allocator). `None` means unlimited.
pub memory_limit_bytes: Option<usize>,
/// Limit runtime stack usage. `None` uses QuickJS default.
pub max_stack_bytes: Option<usize>,
/// Interrupt budget to bound JS execution. `None` disables budget enforcement.
///
/// This is implemented via QuickJS's interrupt hook. For deterministic unit tests,
/// setting this to `Some(0)` forces an immediate abort.
pub interrupt_budget: Option<u64>,
/// Default timeout (ms) for hostcalls issued via `pi.*`.
pub hostcall_timeout_ms: Option<u64>,
/// Fast-path ring capacity for JS->host hostcall handoff.
///
/// `0` means use the runtime default.
pub hostcall_fast_queue_capacity: usize,
/// Overflow capacity once the fast-path ring is saturated.
///
/// `0` means use the runtime default.
pub hostcall_overflow_queue_capacity: usize,
}
/// Controls how the auto-repair pipeline behaves at extension load time.
///
/// Precedence (highest to lowest): CLI flag → environment variable
/// `PI_REPAIR_MODE` → config file → default (`AutoSafe`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RepairMode {
/// No repairs are attempted; extensions that fail to load fail normally.
Off,
/// Log suggested repairs but do not apply them. Useful for auditing what
/// would change before enabling auto-repair in production.
Suggest,
/// Apply only provably safe repairs: file-path fallbacks (Pattern 1) and
/// missing-asset stubs (Pattern 2). These never grant new privileges.
#[default]
AutoSafe,
/// Apply all repairs including aggressive heuristics (monorepo escape
/// stubs, proxy-based npm stubs, export shape fixups). May change
/// observable extension behavior.
AutoStrict,
}
impl RepairMode {
/// Whether repairs should actually be applied (not just logged).
pub const fn should_apply(self) -> bool {
matches!(self, Self::AutoSafe | Self::AutoStrict)
}
/// Whether any repair activity (logging or applying) is enabled.
pub const fn is_active(self) -> bool {
!matches!(self, Self::Off)
}
/// Whether aggressive/heuristic patterns (3-5) are allowed.
pub const fn allows_aggressive(self) -> bool {
matches!(self, Self::AutoStrict)
}
/// Parse from a string (env var, CLI flag, config value).
pub fn from_str_lossy(s: &str) -> Self {
match s.trim().to_ascii_lowercase().as_str() {
"off" | "none" | "disabled" | "false" | "0" => Self::Off,
"suggest" | "log" | "dry-run" | "dry_run" => Self::Suggest,
"auto-strict" | "auto_strict" | "strict" | "all" => Self::AutoStrict,
// "auto-safe", "safe", "true", "1", or any unrecognised value → default
_ => Self::AutoSafe,
}
}
}
impl std::fmt::Display for RepairMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Off => write!(f, "off"),
Self::Suggest => write!(f, "suggest"),
Self::AutoSafe => write!(f, "auto-safe"),
Self::AutoStrict => write!(f, "auto-strict"),
}
}
}
// ---------------------------------------------------------------------------
// Privilege monotonicity checker (bd-k5q5.9.1.3)
// ---------------------------------------------------------------------------
/// Result of a privilege monotonicity check on a proposed repair.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MonotonicityVerdict {
/// The repair is safe: the resolved path does not broaden privileges.
Safe,
/// The repair would escape the extension root directory.
EscapesRoot {
extension_root: PathBuf,
resolved: PathBuf,
},
/// The repaired path crosses into a different extension's directory.
CrossExtension {
original_extension: String,
resolved: PathBuf,
},
}
impl MonotonicityVerdict {
pub const fn is_safe(&self) -> bool {
matches!(self, Self::Safe)
}
}
/// Check that a repair-resolved path stays within the extension root.
///
/// Ensures repaired artifacts cannot broaden the extension's effective
/// capability surface by reaching into unrelated code.
///
/// # Guarantees
/// 1. `resolved_path` must be a descendant of `extension_root`.
/// 2. `resolved_path` must not traverse above the common ancestor of
/// `extension_root` and `original_path`.
pub fn verify_repair_monotonicity(
extension_root: &Path,
_original_path: &Path,
resolved_path: &Path,
) -> MonotonicityVerdict {
// Canonicalise the root to resolve symlinks. Fall back to the raw path
// if canonicalization fails (the directory might not exist in tests).
let canonical_root = crate::extensions::safe_canonicalize(extension_root);
let canonical_resolved = crate::extensions::safe_canonicalize(resolved_path);
// The resolved path MUST be a descendant of the extension root.
if !canonical_resolved.starts_with(&canonical_root) {
return MonotonicityVerdict::EscapesRoot {
extension_root: canonical_root,
resolved: canonical_resolved,
};
}
MonotonicityVerdict::Safe
}
// ---------------------------------------------------------------------------
// Capability monotonicity proof reports (bd-k5q5.9.5.2)
// ---------------------------------------------------------------------------
/// A single capability change between before and after `IntentGraph`s.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CapabilityDelta {
/// A signal present in both before and after — no change.
Retained(IntentSignal),
/// A signal present in before but absent in after — removed.
Removed(IntentSignal),
/// A signal absent in before but present in after — added (violation).
Added(IntentSignal),
}
impl CapabilityDelta {
/// True when this delta represents a privilege escalation.
pub const fn is_escalation(&self) -> bool {
matches!(self, Self::Added(_))
}
/// True when the capability was preserved unchanged.
pub const fn is_retained(&self) -> bool {
matches!(self, Self::Retained(_))
}
/// True when the capability was dropped.
pub const fn is_removed(&self) -> bool {
matches!(self, Self::Removed(_))
}
/// Short label for logging and telemetry.
pub const fn label(&self) -> &'static str {
match self {
Self::Retained(_) => "retained",
Self::Removed(_) => "removed",
Self::Added(_) => "added",
}
}
/// The underlying signal.
pub const fn signal(&self) -> &IntentSignal {
match self {
Self::Retained(s) | Self::Removed(s) | Self::Added(s) => s,
}
}
}
impl std::fmt::Display for CapabilityDelta {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.label(), self.signal())
}
}
/// Proof verdict for capability monotonicity.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CapabilityMonotonicityVerdict {
/// No capabilities were added — repair is monotonic (safe).
Monotonic,
/// One or more capabilities were added — privilege escalation detected.
Escalation,
}
impl CapabilityMonotonicityVerdict {
/// True when the repair passed monotonicity (no escalation).
pub const fn is_safe(&self) -> bool {
matches!(self, Self::Monotonic)
}
}
impl std::fmt::Display for CapabilityMonotonicityVerdict {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Monotonic => write!(f, "monotonic"),
Self::Escalation => write!(f, "escalation"),
}
}
}
/// Full capability monotonicity proof report.
///
/// Compares the before-repair and after-repair `IntentGraph`s signal by
/// signal. A repair is monotonic if and only if it introduces no new
/// capabilities — it may remove or retain existing ones, but never add.
#[derive(Debug, Clone)]
pub struct CapabilityProofReport {
/// Extension identity.
pub extension_id: String,
/// Overall verdict.
pub verdict: CapabilityMonotonicityVerdict,
/// Per-signal deltas.
pub deltas: Vec<CapabilityDelta>,
/// Number of retained capabilities.
pub retained_count: usize,
/// Number of removed capabilities.
pub removed_count: usize,
/// Number of added capabilities (escalations).
pub added_count: usize,
}
impl CapabilityProofReport {
/// True when the proof passed (no escalation).
pub const fn is_safe(&self) -> bool {
self.verdict.is_safe()
}
/// Return only the escalation deltas.
pub fn escalations(&self) -> Vec<&CapabilityDelta> {
self.deltas.iter().filter(|d| d.is_escalation()).collect()
}
}
/// Compute a capability monotonicity proof by diffing two intent graphs.
///
/// The `before` graph represents the original extension's capabilities.
/// The `after` graph represents the repaired extension's capabilities.
///
/// A repair is *monotonic* (safe) if and only if `after` introduces no
/// signals that were absent from `before`. Removals are allowed.
pub fn compute_capability_proof(
before: &IntentGraph,
after: &IntentGraph,
) -> CapabilityProofReport {
use std::collections::HashSet;
let before_set: HashSet<&IntentSignal> = before.signals.iter().collect();
let after_set: HashSet<&IntentSignal> = after.signals.iter().collect();
let mut deltas = Vec::new();
// Signals retained or removed (iterate `before`).
for signal in &before.signals {
if after_set.contains(signal) {
deltas.push(CapabilityDelta::Retained(signal.clone()));
} else {
deltas.push(CapabilityDelta::Removed(signal.clone()));
}
}
// Signals added (in `after` but not in `before`).
for signal in &after.signals {
if !before_set.contains(signal) {
deltas.push(CapabilityDelta::Added(signal.clone()));
}
}
let retained_count = deltas.iter().filter(|d| d.is_retained()).count();
let removed_count = deltas.iter().filter(|d| d.is_removed()).count();
let added_count = deltas.iter().filter(|d| d.is_escalation()).count();
let verdict = if added_count == 0 {
CapabilityMonotonicityVerdict::Monotonic
} else {
CapabilityMonotonicityVerdict::Escalation
};
CapabilityProofReport {
extension_id: before.extension_id.clone(),
verdict,
deltas,
retained_count,
removed_count,
added_count,
}
}
// ---------------------------------------------------------------------------
// Hostcall parity and semantic delta proof (bd-k5q5.9.5.3)
// ---------------------------------------------------------------------------
/// Categories of hostcall surface that an extension can exercise.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum HostcallCategory {
/// `pi.events(op, ...)` — lifecycle event dispatch.
Events(String),
/// `pi.session(op, ...)` — session metadata operations.
Session(String),
/// `pi.register(...)` — registration (tools, commands, etc.).
Register,
/// `pi.tool(op, ...)` — tool management.
Tool(String),
/// `require(...)` / `import(...)` — module resolution.
ModuleResolution(String),
}
impl HostcallCategory {
/// Short tag for logging.
pub fn tag(&self) -> String {
match self {
Self::Events(op) => format!("events:{op}"),
Self::Session(op) => format!("session:{op}"),
Self::Register => "register".to_string(),
Self::Tool(op) => format!("tool:{op}"),
Self::ModuleResolution(spec) => format!("module:{spec}"),
}
}
}
impl std::fmt::Display for HostcallCategory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.tag())
}
}
/// A delta between before/after hostcall surfaces.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HostcallDelta {
/// Hostcall present in both before and after.
Retained(HostcallCategory),
/// Hostcall present before but absent after.
Removed(HostcallCategory),
/// Hostcall absent before but present after — new surface.
Added(HostcallCategory),
}
impl HostcallDelta {
/// True when this delta introduces new hostcall surface.
pub const fn is_expansion(&self) -> bool {
matches!(self, Self::Added(_))
}
/// Short label for logging.
pub const fn label(&self) -> &'static str {
match self {
Self::Retained(_) => "retained",
Self::Removed(_) => "removed",
Self::Added(_) => "added",
}
}
/// The underlying category.
pub const fn category(&self) -> &HostcallCategory {
match self {
Self::Retained(c) | Self::Removed(c) | Self::Added(c) => c,
}
}
}
impl std::fmt::Display for HostcallDelta {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.label(), self.category())
}
}
/// Semantic drift severity classification.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum SemanticDriftSeverity {
/// No meaningful behavioral change detected.
None,
/// Minor changes that don't affect core functionality.
Low,
/// Changes that may affect behavior but within expected scope.
Medium,
/// Significant behavioral divergence — likely beyond fix scope.
High,
}
impl SemanticDriftSeverity {
/// True if drift is within acceptable bounds.
pub const fn is_acceptable(&self) -> bool {
matches!(self, Self::None | Self::Low)
}
}
impl std::fmt::Display for SemanticDriftSeverity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::None => write!(f, "none"),
Self::Low => write!(f, "low"),
Self::Medium => write!(f, "medium"),
Self::High => write!(f, "high"),
}
}
}
/// Overall verdict for hostcall parity and semantic delta proof.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SemanticParityVerdict {
/// Repair preserves hostcall surface and semantic behavior.
Equivalent,
/// Minor acceptable drift (e.g., removed dead hostcalls).
AcceptableDrift,
/// Repair introduces new hostcall surface or significant semantic drift.
Divergent,
}
impl SemanticParityVerdict {
/// True if the repair passes semantic parity.
pub const fn is_safe(&self) -> bool {
matches!(self, Self::Equivalent | Self::AcceptableDrift)
}
}
impl std::fmt::Display for SemanticParityVerdict {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Equivalent => write!(f, "equivalent"),
Self::AcceptableDrift => write!(f, "acceptable_drift"),
Self::Divergent => write!(f, "divergent"),
}
}
}
/// Full hostcall parity and semantic delta proof report.
#[derive(Debug, Clone)]
pub struct SemanticParityReport {
/// Extension identity.
pub extension_id: String,
/// Overall verdict.
pub verdict: SemanticParityVerdict,
/// Hostcall surface deltas.
pub hostcall_deltas: Vec<HostcallDelta>,
/// Semantic drift severity assessment.
pub drift_severity: SemanticDriftSeverity,
/// Number of new hostcall surfaces introduced.
pub expanded_count: usize,
/// Number of hostcalls removed.
pub removed_count: usize,
/// Number of hostcalls retained.
pub retained_count: usize,
/// Explanatory notes for the verdict.
pub notes: Vec<String>,
}
impl SemanticParityReport {
/// True if the proof passed (repair is safe).
pub const fn is_safe(&self) -> bool {
self.verdict.is_safe()
}
/// Return only the expansion deltas (new hostcall surface).
pub fn expansions(&self) -> Vec<&HostcallDelta> {
self.hostcall_deltas
.iter()
.filter(|d| d.is_expansion())
.collect()
}
}
/// Extract hostcall categories from an intent graph.
///
/// Maps `IntentSignal`s to the hostcall categories they exercise at runtime.
/// This provides a static approximation of the extension's hostcall surface.
pub fn extract_hostcall_surface(
intent: &IntentGraph,
) -> std::collections::HashSet<HostcallCategory> {
let mut surface = std::collections::HashSet::new();
for signal in &intent.signals {
match signal {
IntentSignal::RegistersTool(_)
| IntentSignal::RegistersCommand(_)
| IntentSignal::RegistersShortcut(_)
| IntentSignal::RegistersFlag(_)
| IntentSignal::RegistersProvider(_)
| IntentSignal::RegistersRenderer(_) => {
surface.insert(HostcallCategory::Register);
}
IntentSignal::HooksEvent(name) => {
surface.insert(HostcallCategory::Events(name.clone()));
}
IntentSignal::RequiresCapability(cap) => {
if cap == "session" {
surface.insert(HostcallCategory::Session("*".to_string()));
} else if cap == "tool" {
surface.insert(HostcallCategory::Tool("*".to_string()));
}
}
}
}
surface
}
/// Compute hostcall parity and semantic delta proof.
///
/// Compares the before-repair and after-repair hostcall surfaces and
/// assesses semantic drift. A repair passes if it does not expand the
/// hostcall surface beyond the declared fix scope.
pub fn compute_semantic_parity(
before: &IntentGraph,
after: &IntentGraph,
patch_ops: &[PatchOp],
) -> SemanticParityReport {
let before_surface = extract_hostcall_surface(before);
let after_surface = extract_hostcall_surface(after);
let mut hostcall_deltas = Vec::new();
// Retained and removed.
for cat in &before_surface {
if after_surface.contains(cat) {
hostcall_deltas.push(HostcallDelta::Retained(cat.clone()));
} else {
hostcall_deltas.push(HostcallDelta::Removed(cat.clone()));
}
}
// Added.
for cat in &after_surface {
if !before_surface.contains(cat) {
hostcall_deltas.push(HostcallDelta::Added(cat.clone()));
}
}
let expanded_count = hostcall_deltas.iter().filter(|d| d.is_expansion()).count();
let removed_count = hostcall_deltas
.iter()
.filter(|d| matches!(d, HostcallDelta::Removed(_)))
.count();
let retained_count = hostcall_deltas
.iter()
.filter(|d| matches!(d, HostcallDelta::Retained(_)))
.count();
// Assess semantic drift based on patch operations.
let mut notes = Vec::new();
let drift_severity = assess_drift(patch_ops, expanded_count, removed_count, &mut notes);
let verdict = if expanded_count == 0 && drift_severity.is_acceptable() {
if removed_count == 0 {
SemanticParityVerdict::Equivalent
} else {
notes.push(format!(
"{removed_count} hostcall(s) removed — acceptable reduction"
));
SemanticParityVerdict::AcceptableDrift
}
} else {
if expanded_count > 0 {
notes.push(format!(
"{expanded_count} new hostcall surface(s) introduced"
));
}
SemanticParityVerdict::Divergent
};
SemanticParityReport {
extension_id: before.extension_id.clone(),
verdict,
hostcall_deltas,
drift_severity,
expanded_count,
removed_count,
retained_count,
notes,
}
}
/// Assess semantic drift severity from patch operations and hostcall changes.
fn assess_drift(
patch_ops: &[PatchOp],
expanded_hostcalls: usize,
_removed_hostcalls: usize,
notes: &mut Vec<String>,
) -> SemanticDriftSeverity {
// Any hostcall expansion is High severity.
if expanded_hostcalls > 0 {
notes.push("new hostcall surface detected".to_string());
return SemanticDriftSeverity::High;
}
let mut has_aggressive = false;
let mut stub_count = 0_usize;
for op in patch_ops {
match op {
PatchOp::InjectStub { .. } => {
stub_count += 1;
has_aggressive = true;
}
PatchOp::AddExport { .. } | PatchOp::RemoveImport { .. } => {
has_aggressive = true;
}
PatchOp::ReplaceModulePath { .. } | PatchOp::RewriteRequire { .. } => {}
}
}
if stub_count > 2 {
notes.push(format!("{stub_count} stubs injected — medium drift"));
return SemanticDriftSeverity::Medium;
}
if has_aggressive {
notes.push("aggressive ops present — low drift".to_string());
return SemanticDriftSeverity::Low;
}
SemanticDriftSeverity::None
}
// ---------------------------------------------------------------------------
// Conformance replay and golden checksum evidence (bd-k5q5.9.5.4)
// ---------------------------------------------------------------------------
/// SHA-256 checksum of an artifact (hex-encoded, lowercase).
pub type ArtifactChecksum = String;
/// Compute a SHA-256 checksum for the given byte content.
pub fn compute_artifact_checksum(content: &[u8]) -> ArtifactChecksum {
use sha2::{Digest, Sha256};
let hash = Sha256::digest(content);
format!("{hash:x}")
}
/// A single artifact entry in a golden checksum manifest.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChecksumEntry {
/// Relative path of the artifact within the extension root.
pub relative_path: String,
/// SHA-256 checksum.
pub checksum: ArtifactChecksum,
/// Byte size of the artifact.
pub size_bytes: u64,
}
/// Overall verdict of a conformance replay check.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ConformanceReplayVerdict {
/// All replayed fixtures matched expected behavior.
Pass,
/// One or more fixtures produced unexpected results.
Fail,
/// No fixtures were available to replay (vacuously safe).
NoFixtures,
}
impl ConformanceReplayVerdict {
/// True if the replay passed or had no fixtures.
pub const fn is_acceptable(&self) -> bool {
matches!(self, Self::Pass | Self::NoFixtures)
}
}
impl std::fmt::Display for ConformanceReplayVerdict {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Pass => write!(f, "pass"),
Self::Fail => write!(f, "fail"),
Self::NoFixtures => write!(f, "no_fixtures"),
}
}
}
/// A single conformance fixture for replay.
#[derive(Debug, Clone)]
pub struct ConformanceFixture {
/// Descriptive name of the fixture.
pub name: String,
/// Expected behavior or output pattern.
pub expected: String,
/// Actual behavior or output observed during replay.
pub actual: Option<String>,
/// Whether this fixture passed.
pub passed: bool,
}
/// Result of replaying conformance fixtures.
#[derive(Debug, Clone)]
pub struct ConformanceReplayReport {
/// Extension identity.
pub extension_id: String,
/// Overall verdict.
pub verdict: ConformanceReplayVerdict,
/// Individual fixture results.
pub fixtures: Vec<ConformanceFixture>,
/// Number of fixtures that passed.
pub passed_count: usize,
/// Total number of fixtures replayed.
pub total_count: usize,
}
impl ConformanceReplayReport {
/// True if the replay is acceptable.
pub const fn is_acceptable(&self) -> bool {
self.verdict.is_acceptable()
}
}
/// Replay conformance fixtures and produce a report.
///
/// Each fixture is checked: if `actual` is provided and matches `expected`,
/// the fixture passes. If no fixtures are provided, the verdict is
/// `NoFixtures` (vacuously safe — conformance cannot be disproven).
pub fn replay_conformance_fixtures(
extension_id: &str,
fixtures: &[ConformanceFixture],
) -> ConformanceReplayReport {
if fixtures.is_empty() {
return ConformanceReplayReport {
extension_id: extension_id.to_string(),
verdict: ConformanceReplayVerdict::NoFixtures,
fixtures: Vec::new(),
passed_count: 0,
total_count: 0,
};
}
let passed_count = fixtures.iter().filter(|f| f.passed).count();
let total_count = fixtures.len();
let verdict = if passed_count == total_count {
ConformanceReplayVerdict::Pass
} else {
ConformanceReplayVerdict::Fail
};
ConformanceReplayReport {
extension_id: extension_id.to_string(),
verdict,
fixtures: fixtures.to_vec(),
passed_count,
total_count,
}
}
/// A golden checksum manifest for reproducible evidence.
///
/// Records the checksums of all repaired artifacts at the time of repair.
/// This provides tamper-evident proof that the artifacts were not modified
/// after the repair pipeline produced them.
#[derive(Debug, Clone)]
pub struct GoldenChecksumManifest {
/// Extension identity.
pub extension_id: String,
/// Entries (one per artifact).
pub entries: Vec<ChecksumEntry>,
/// When the manifest was generated (unix millis).
pub generated_at_ms: u64,
}
impl GoldenChecksumManifest {
/// Number of artifacts in the manifest.
pub fn artifact_count(&self) -> usize {
self.entries.len()
}
/// Verify that a given file's content matches its entry in the manifest.
pub fn verify_entry(&self, relative_path: &str, content: &[u8]) -> Option<bool> {
self.entries
.iter()
.find(|e| e.relative_path == relative_path)
.map(|e| e.checksum == compute_artifact_checksum(content))
}
}
/// Build a golden checksum manifest from file contents.
///
/// Takes an extension_id, a list of (relative_path, content) tuples, and a
/// timestamp. Computes SHA-256 for each artifact.
pub fn build_golden_manifest(
extension_id: &str,
artifacts: &[(&str, &[u8])],
timestamp_ms: u64,
) -> GoldenChecksumManifest {
let entries = artifacts
.iter()
.map(|(path, content)| ChecksumEntry {
relative_path: (*path).to_string(),
checksum: compute_artifact_checksum(content),
size_bytes: content.len() as u64,
})
.collect();
GoldenChecksumManifest {
extension_id: extension_id.to_string(),
entries,
generated_at_ms: timestamp_ms,
}
}
/// Unified verification evidence bundle.
///
/// Collects all proof artifacts from LISR-5 into a single bundle that
/// serves as the activation gate. A repair candidate cannot be activated
/// unless ALL proofs pass.
#[derive(Debug, Clone)]
pub struct VerificationBundle {
/// Extension identity.
pub extension_id: String,
/// Structural validation (LISR-5.1).
pub structural: StructuralVerdict,
/// Capability monotonicity proof (LISR-5.2).
pub capability_proof: CapabilityProofReport,
/// Semantic parity proof (LISR-5.3).
pub semantic_proof: SemanticParityReport,
/// Conformance replay (LISR-5.4).
pub conformance: ConformanceReplayReport,
/// Golden checksum manifest (LISR-5.4).
pub checksum_manifest: GoldenChecksumManifest,
}
impl VerificationBundle {
/// True if ALL proofs pass — the activation gate.
pub const fn is_verified(&self) -> bool {
self.structural.is_valid()
&& self.capability_proof.is_safe()
&& self.semantic_proof.is_safe()
&& self.conformance.is_acceptable()
}
/// Collect failure reasons for logging.
pub fn failure_reasons(&self) -> Vec<String> {
let mut reasons = Vec::new();
if !self.structural.is_valid() {
reasons.push(format!("structural: {}", self.structural));
}
if !self.capability_proof.is_safe() {
reasons.push(format!(
"capability: {} ({} escalation(s))",
self.capability_proof.verdict, self.capability_proof.added_count
));
}
if !self.semantic_proof.is_safe() {
reasons.push(format!(
"semantic: {} (drift={})",
self.semantic_proof.verdict, self.semantic_proof.drift_severity
));
}
if !self.conformance.is_acceptable() {
reasons.push(format!(
"conformance: {} ({}/{} passed)",
self.conformance.verdict,
self.conformance.passed_count,
self.conformance.total_count
));
}
reasons
}
}
// ---------------------------------------------------------------------------
// Overlay artifact format and lifecycle storage (bd-k5q5.9.6.1)
// ---------------------------------------------------------------------------
/// Lifecycle state of an overlay artifact.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum OverlayState {
/// Just created, not yet deployed.
Staged,
/// In canary — serving to a controlled cohort.
Canary,
/// Promoted to stable after successful canary window.
Stable,
/// Rolled back due to failure or manual action.
RolledBack,
/// Superseded by a newer repair.
Superseded,
}
impl OverlayState {
/// True if the overlay is currently serving traffic.
pub const fn is_active(&self) -> bool {
matches!(self, Self::Canary | Self::Stable)
}
/// True if the overlay has reached a terminal state.
pub const fn is_terminal(&self) -> bool {
matches!(self, Self::RolledBack | Self::Superseded)
}
}
impl std::fmt::Display for OverlayState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Staged => write!(f, "staged"),
Self::Canary => write!(f, "canary"),
Self::Stable => write!(f, "stable"),
Self::RolledBack => write!(f, "rolled_back"),
Self::Superseded => write!(f, "superseded"),
}
}
}
/// An overlay artifact bundle: the unit of repair deployment.
///
/// Contains the repaired payload, original artifact hash, proof metadata,
/// policy decisions, and full lineage for auditability.
#[derive(Debug, Clone)]
pub struct OverlayArtifact {
/// Unique identifier for this overlay.
pub overlay_id: String,
/// Extension identity.
pub extension_id: String,
/// Extension version.
pub extension_version: String,
/// SHA-256 of the original (broken) artifact.
pub original_checksum: ArtifactChecksum,
/// SHA-256 of the repaired artifact.
pub repaired_checksum: ArtifactChecksum,
/// Current lifecycle state.
pub state: OverlayState,
/// Rule that produced this repair.
pub rule_id: String,
/// Repair mode active when the overlay was created.
pub repair_mode: RepairMode,
/// Verification bundle summary (pass/fail per layer).
pub verification_passed: bool,
/// Creation timestamp (unix millis).
pub created_at_ms: u64,
/// Last state-transition timestamp (unix millis).
pub updated_at_ms: u64,
}
impl OverlayArtifact {
/// True if the overlay is currently serving.
pub const fn is_active(&self) -> bool {
self.state.is_active()
}
}
/// State transition error.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OverlayTransitionError {
/// Attempted transition is not valid from the current state.
InvalidTransition {
from: OverlayState,
to: OverlayState,
},
/// Verification must pass before deployment.
VerificationRequired,
}
impl std::fmt::Display for OverlayTransitionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidTransition { from, to } => {
write!(f, "invalid transition: {from} → {to}")
}
Self::VerificationRequired => {
write!(f, "verification must pass before deployment")
}
}
}
}
/// Advance an overlay through its lifecycle.
///
/// Valid transitions:
/// - Staged → Canary (requires verification_passed)
/// - Canary → Stable
/// - Canary → `RolledBack`
/// - Stable → `RolledBack`
/// - Stable → Superseded
/// - Staged → `RolledBack`
pub fn transition_overlay(
artifact: &mut OverlayArtifact,
target: OverlayState,
now_ms: u64,
) -> std::result::Result<(), OverlayTransitionError> {
let valid = matches!(
(artifact.state, target),
(
OverlayState::Staged,
OverlayState::Canary | OverlayState::RolledBack
) | (
OverlayState::Canary,
OverlayState::Stable | OverlayState::RolledBack
) | (
OverlayState::Stable,
OverlayState::RolledBack | OverlayState::Superseded
)
);
if !valid {
return Err(OverlayTransitionError::InvalidTransition {
from: artifact.state,
to: target,
});
}
// Verification gate for deployment.
if target == OverlayState::Canary && !artifact.verification_passed {
return Err(OverlayTransitionError::VerificationRequired);
}
artifact.state = target;
artifact.updated_at_ms = now_ms;
Ok(())
}
// ---------------------------------------------------------------------------
// Per-extension/version canary routing (bd-k5q5.9.6.2)
// ---------------------------------------------------------------------------
/// Canary routing decision for a specific request.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CanaryRoute {
/// Use the original (unrepaired) artifact.
Original,
/// Use the repaired overlay artifact.
Overlay,
}
impl std::fmt::Display for CanaryRoute {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Original => write!(f, "original"),
Self::Overlay => write!(f, "overlay"),
}
}
}
/// Canary configuration for a specific extension/version pair.
#[derive(Debug, Clone)]
pub struct CanaryConfig {
/// Extension identity.
pub extension_id: String,
/// Extension version.
pub extension_version: String,
/// Percentage of requests to route to overlay (0–100).
pub overlay_percent: u8,
/// Whether canary is currently active.
pub enabled: bool,
}
impl CanaryConfig {
/// Route a request using a deterministic hash value (0–99).
pub const fn route(&self, hash_bucket: u8) -> CanaryRoute {
if self.enabled && hash_bucket < self.overlay_percent {
CanaryRoute::Overlay
} else {
CanaryRoute::Original
}
}
/// True if all traffic is routed to overlay (100% canary).
pub const fn is_full_rollout(&self) -> bool {
self.enabled && self.overlay_percent >= 100
}
}
/// Compute a deterministic hash bucket (0–99) from extension ID and environment.
pub fn compute_canary_bucket(extension_id: &str, environment: &str) -> u8 {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(extension_id.as_bytes());
hasher.update(b":");
hasher.update(environment.as_bytes());
let hash = hasher.finalize();
// Use u16 to reduce modulo bias (65536 % 100 = 36 vs 256 % 100 = 56).
let val = u16::from_be_bytes([hash[0], hash[1]]);
(val % 100) as u8
}
// ---------------------------------------------------------------------------
// Health/SLO monitors and automatic rollback triggers (bd-k5q5.9.6.3)
// ---------------------------------------------------------------------------
/// A health signal observed during canary.
#[derive(Debug, Clone)]
pub struct HealthSignal {
/// Signal name (e.g., "load_success", "hostcall_error_rate").
pub name: String,
/// Current value.
pub value: f64,
/// SLO threshold (value must not exceed this for the signal to be healthy).
pub threshold: f64,
}
impl HealthSignal {
/// True if the signal is within SLO bounds.
pub fn is_healthy(&self) -> bool {
self.value <= self.threshold
}
}
/// SLO verdict for a canary window.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SloVerdict {
/// All signals within thresholds.
Healthy,
/// One or more signals violated their SLO.
Violated,
}
impl SloVerdict {
/// True if the canary is healthy.
pub const fn is_healthy(&self) -> bool {
matches!(self, Self::Healthy)
}
}
impl std::fmt::Display for SloVerdict {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Healthy => write!(f, "healthy"),
Self::Violated => write!(f, "violated"),
}
}
}
/// Health assessment report for a canary window.
#[derive(Debug, Clone)]
pub struct HealthReport {
/// Extension identity.
pub extension_id: String,
/// Overall verdict.
pub verdict: SloVerdict,
/// Individual signal assessments.
pub signals: Vec<HealthSignal>,
/// Signals that violated their SLO.
pub violations: Vec<String>,
}
impl HealthReport {
/// True if the canary is healthy.
pub const fn is_healthy(&self) -> bool {
self.verdict.is_healthy()
}
}
/// Evaluate health signals against SLO thresholds.
pub fn evaluate_health(extension_id: &str, signals: &[HealthSignal]) -> HealthReport {
let violations: Vec<String> = signals
.iter()
.filter(|s| !s.is_healthy())
.map(|s| format!("{}: {:.3} > {:.3}", s.name, s.value, s.threshold))
.collect();
let verdict = if violations.is_empty() {
SloVerdict::Healthy
} else {
SloVerdict::Violated
};
HealthReport {
extension_id: extension_id.to_string(),
verdict,
signals: signals.to_vec(),
violations,
}
}
/// Automatic rollback trigger: should the canary be rolled back?
pub const fn should_auto_rollback(health: &HealthReport) -> bool {
!health.is_healthy()
}
// ---------------------------------------------------------------------------
// Promotion and deterministic rollback workflow (bd-k5q5.9.6.4)
// ---------------------------------------------------------------------------
/// Promotion decision for a canary overlay.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PromotionDecision {
/// Promote to stable — canary window passed.
Promote,
/// Keep in canary — more observation needed.
Hold,
/// Rollback — SLO violations detected.
Rollback,
}
impl std::fmt::Display for PromotionDecision {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Promote => write!(f, "promote"),
Self::Hold => write!(f, "hold"),
Self::Rollback => write!(f, "rollback"),
}
}
}
/// Decide whether to promote, hold, or rollback a canary overlay.
///
/// Rules:
/// - If health is violated → Rollback.
/// - If canary duration has exceeded the window → Promote.
/// - Otherwise → Hold.
pub const fn decide_promotion(
health: &HealthReport,
canary_start_ms: u64,
now_ms: u64,
canary_window_ms: u64,
) -> PromotionDecision {
if !health.is_healthy() {
return PromotionDecision::Rollback;
}
if now_ms.saturating_sub(canary_start_ms) >= canary_window_ms {
return PromotionDecision::Promote;
}
PromotionDecision::Hold
}
/// Execute a promotion: transitions overlay to Stable.
pub fn execute_promotion(
artifact: &mut OverlayArtifact,
now_ms: u64,
) -> std::result::Result<(), OverlayTransitionError> {
transition_overlay(artifact, OverlayState::Stable, now_ms)
}
/// Execute a rollback: transitions overlay to `RolledBack`.
pub fn execute_rollback(
artifact: &mut OverlayArtifact,
now_ms: u64,
) -> std::result::Result<(), OverlayTransitionError> {
transition_overlay(artifact, OverlayState::RolledBack, now_ms)
}
// ---------------------------------------------------------------------------
// Append-only repair audit ledger (bd-k5q5.9.7.1)
// ---------------------------------------------------------------------------
/// Kind of audit ledger entry.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AuditEntryKind {
/// Analysis phase: intent extraction and confidence scoring.
Analysis,
/// Gating decision: allow / suggest / deny.
GatingDecision,
/// Proposal generated by rule or model.
ProposalGenerated,
/// Proposal validated against policy.
ProposalValidated,
/// Verification bundle evaluated.
VerificationEvaluated,
/// Human approval requested.
ApprovalRequested,
/// Human approval response.
ApprovalResponse,
/// Overlay activated (canary or stable).
Activated,
/// Overlay rolled back.
RolledBack,
/// Overlay promoted to stable.
Promoted,
/// Overlay superseded by newer repair.
Superseded,
}
impl std::fmt::Display for AuditEntryKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Analysis => write!(f, "analysis"),
Self::GatingDecision => write!(f, "gating_decision"),
Self::ProposalGenerated => write!(f, "proposal_generated"),
Self::ProposalValidated => write!(f, "proposal_validated"),
Self::VerificationEvaluated => write!(f, "verification_evaluated"),
Self::ApprovalRequested => write!(f, "approval_requested"),
Self::ApprovalResponse => write!(f, "approval_response"),
Self::Activated => write!(f, "activated"),
Self::RolledBack => write!(f, "rolled_back"),
Self::Promoted => write!(f, "promoted"),
Self::Superseded => write!(f, "superseded"),
}
}
}
/// A single entry in the repair audit ledger.
#[derive(Debug, Clone)]
pub struct AuditEntry {
/// Monotonically increasing sequence number.
pub sequence: u64,
/// Timestamp (unix millis).
pub timestamp_ms: u64,
/// Extension being repaired.
pub extension_id: String,
/// Kind of event.
pub kind: AuditEntryKind,
/// Human-readable summary.
pub summary: String,
/// Structured detail fields (key-value pairs for machine consumption).
pub details: Vec<(String, String)>,
}
/// Append-only audit ledger for repair lifecycle events.
///
/// Entries are ordered by sequence number and cannot be mutated or deleted.
/// This provides tamper-evident evidence for incident forensics.
#[derive(Debug, Clone, Default)]
pub struct AuditLedger {
entries: Vec<AuditEntry>,
next_sequence: u64,
}
impl AuditLedger {
/// Create an empty ledger.
pub const fn new() -> Self {
Self {
entries: Vec::new(),
next_sequence: 0,
}
}
/// Append an entry to the ledger.
pub fn append(
&mut self,
timestamp_ms: u64,
extension_id: &str,
kind: AuditEntryKind,
summary: String,
details: Vec<(String, String)>,
) -> u64 {
let seq = self.next_sequence;
self.entries.push(AuditEntry {
sequence: seq,
timestamp_ms,
extension_id: extension_id.to_string(),
kind,
summary,
details,
});
self.next_sequence = self.next_sequence.saturating_add(1);
seq
}
/// Number of entries in the ledger.
pub fn len(&self) -> usize {
self.entries.len()
}
/// True if the ledger is empty.
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
/// Get an entry by sequence number.
pub fn get(&self, sequence: u64) -> Option<&AuditEntry> {
self.entries.iter().find(|e| e.sequence == sequence)
}
/// Query entries by extension ID.
pub fn entries_for_extension(&self, extension_id: &str) -> Vec<&AuditEntry> {
self.entries
.iter()
.filter(|e| e.extension_id == extension_id)
.collect()
}
/// Query entries by kind.
pub fn entries_by_kind(&self, kind: AuditEntryKind) -> Vec<&AuditEntry> {
self.entries.iter().filter(|e| e.kind == kind).collect()
}
/// All entries, ordered by sequence.
pub fn all_entries(&self) -> &[AuditEntry] {
&self.entries
}
}
// ---------------------------------------------------------------------------
// Telemetry taxonomy and metrics pipeline (bd-k5q5.9.7.2)
// ---------------------------------------------------------------------------
/// Telemetry event kind for repair lifecycle metrics.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TelemetryMetric {
/// A repair was attempted.
RepairAttempted,
/// Extension was eligible for repair.
RepairEligible,
/// Extension was ineligible (gating denied).
RepairDenied,
/// Verification proof failed.
VerificationFailed,
/// Overlay was rolled back.
OverlayRolledBack,
/// Overlay was promoted.
OverlayPromoted,
/// Human approval was requested.
ApprovalLatencyMs,
}
impl std::fmt::Display for TelemetryMetric {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::RepairAttempted => write!(f, "repair.attempted"),
Self::RepairEligible => write!(f, "repair.eligible"),
Self::RepairDenied => write!(f, "repair.denied"),
Self::VerificationFailed => write!(f, "verification.failed"),
Self::OverlayRolledBack => write!(f, "overlay.rolled_back"),
Self::OverlayPromoted => write!(f, "overlay.promoted"),
Self::ApprovalLatencyMs => write!(f, "approval.latency_ms"),
}
}
}
/// A single telemetry data point.
#[derive(Debug, Clone)]
pub struct TelemetryPoint {
/// Metric name.
pub metric: TelemetryMetric,
/// Numeric value (count=1 for counters, millis for latency, etc.).
pub value: f64,
/// Timestamp (unix millis).
pub timestamp_ms: u64,
/// Tags for dimensional filtering.
pub tags: Vec<(String, String)>,
}
/// Telemetry collector for repair lifecycle metrics.
#[derive(Debug, Clone, Default)]
pub struct TelemetryCollector {
points: Vec<TelemetryPoint>,
}
impl TelemetryCollector {
/// Create an empty collector.
pub const fn new() -> Self {
Self { points: Vec::new() }
}
/// Record a telemetry data point.
pub fn record(
&mut self,
metric: TelemetryMetric,
value: f64,
timestamp_ms: u64,
tags: Vec<(String, String)>,
) {
self.points.push(TelemetryPoint {
metric,
value,
timestamp_ms,
tags,
});
}
/// Record a counter increment (value=1).
pub fn increment(
&mut self,
metric: TelemetryMetric,
timestamp_ms: u64,
tags: Vec<(String, String)>,
) {
self.record(metric, 1.0, timestamp_ms, tags);
}
/// Count total occurrences of a metric.
pub fn count(&self, metric: TelemetryMetric) -> usize {
self.points.iter().filter(|p| p.metric == metric).count()
}
/// Sum values for a metric.
pub fn sum(&self, metric: TelemetryMetric) -> f64 {
self.points
.iter()
.filter(|p| p.metric == metric)
.map(|p| p.value)
.sum()
}
/// All points.
pub fn all_points(&self) -> &[TelemetryPoint] {
&self.points
}
/// Number of recorded points.
pub fn len(&self) -> usize {
self.points.len()
}
/// True if no points recorded.
pub fn is_empty(&self) -> bool {
self.points.is_empty()
}
}
// ---------------------------------------------------------------------------
// Operator CLI for repair inspect/explain/diff (bd-k5q5.9.7.3)
// ---------------------------------------------------------------------------
/// A formatted inspection record for operator review.
#[derive(Debug, Clone)]
pub struct InspectionRecord {
/// Extension identity.
pub extension_id: String,
/// Timeline of audit entries (formatted).
pub timeline: Vec<String>,
/// Gating decision summary.
pub gating_summary: String,
/// Current overlay state (if any).
pub overlay_state: Option<String>,
/// Verification result summary.
pub verification_summary: String,
}
/// Build an inspection record from audit ledger and current state.
pub fn build_inspection(
extension_id: &str,
ledger: &AuditLedger,
overlay_state: Option<OverlayState>,
verification_passed: bool,
) -> InspectionRecord {
let entries = ledger.entries_for_extension(extension_id);
let timeline: Vec<String> = entries
.iter()
.map(|e| format!("[seq={}] {} — {}", e.sequence, e.kind, e.summary))
.collect();
let gating_entries = entries
.iter()
.filter(|e| e.kind == AuditEntryKind::GatingDecision)
.collect::<Vec<_>>();
let gating_summary = gating_entries.last().map_or_else(
|| "no gating decision recorded".to_string(),
|e| e.summary.clone(),
);
let verification_summary = if verification_passed {
"all proofs passed".to_string()
} else {
"one or more proofs failed".to_string()
};
InspectionRecord {
extension_id: extension_id.to_string(),
timeline,
gating_summary,
overlay_state: overlay_state.map(|s| s.to_string()),
verification_summary,
}
}
/// Explain a gating decision in human-readable format.
pub fn explain_gating(verdict: &GatingVerdict) -> Vec<String> {
let mut lines = Vec::new();
lines.push(format!(
"Decision: {} (confidence: {:.2})",
verdict.decision, verdict.confidence.score
));
for reason in &verdict.confidence.reasons {
lines.push(format!(
" [{:+.2}] {} — {}",
reason.delta, reason.code, reason.explanation
));
}
for code in &verdict.reason_codes {
lines.push(format!(" REASON: {} — {}", code.code, code.remediation));
}
lines
}
/// Format a patch proposal diff for operator review.
pub fn format_proposal_diff(proposal: &PatchProposal) -> Vec<String> {
let mut lines = Vec::new();
lines.push(format!(
"Rule: {} ({} op(s), risk: {:?})",
proposal.rule_id,
proposal.op_count(),
proposal.max_risk()
));
if !proposal.rationale.is_empty() {
lines.push(format!("Rationale: {}", proposal.rationale));
}
for (i, op) in proposal.ops.iter().enumerate() {
lines.push(format!(
" Op {}: [{}] {}",
i + 1,
op.tag(),
op_target_path(op)
));
}
lines
}
// ---------------------------------------------------------------------------
// Forensic bundle export and incident handoff (bd-k5q5.9.7.4)
// ---------------------------------------------------------------------------
/// A complete forensic bundle for incident analysis.
///
/// Contains everything needed to understand what happened during a repair:
/// the artifacts, proofs, audit trail, telemetry snapshot, and health signals.
#[derive(Debug, Clone)]
pub struct ForensicBundle {
/// Extension identity.
pub extension_id: String,
/// Overlay artifact details.
pub overlay: Option<OverlayArtifact>,
/// Verification bundle.
pub verification: Option<VerificationBundle>,
/// Relevant audit entries.
pub audit_entries: Vec<AuditEntry>,
/// Telemetry snapshot for this extension.
pub telemetry_points: Vec<TelemetryPoint>,
/// Health report (if canary was active).
pub health_report: Option<HealthReport>,
/// Golden checksum manifest.
pub checksum_manifest: Option<GoldenChecksumManifest>,
/// Export timestamp (unix millis).
pub exported_at_ms: u64,
}
impl ForensicBundle {
/// Number of audit entries in this bundle.
pub fn audit_count(&self) -> usize {
self.audit_entries.len()
}
/// True if the bundle has verification evidence.
pub const fn has_verification(&self) -> bool {
self.verification.is_some()
}
/// True if the bundle has health data.
pub const fn has_health_data(&self) -> bool {
self.health_report.is_some()
}
}
/// Build a forensic bundle from available state.
#[allow(clippy::too_many_arguments)]
pub fn build_forensic_bundle(
extension_id: &str,
overlay: Option<&OverlayArtifact>,
verification: Option<&VerificationBundle>,
ledger: &AuditLedger,
collector: &TelemetryCollector,
health_report: Option<&HealthReport>,
checksum_manifest: Option<&GoldenChecksumManifest>,
exported_at_ms: u64,
) -> ForensicBundle {
let audit_entries = ledger
.entries_for_extension(extension_id)
.into_iter()
.cloned()
.collect();
let telemetry_points = collector
.all_points()
.iter()
.filter(|p| {
p.tags
.iter()
.any(|(k, v)| k == "extension_id" && v == extension_id)
})
.cloned()
.collect();
ForensicBundle {
extension_id: extension_id.to_string(),
overlay: overlay.cloned(),
verification: verification.cloned(),
audit_entries,
telemetry_points,
health_report: health_report.cloned(),
checksum_manifest: checksum_manifest.cloned(),
exported_at_ms,
}
}
// ---------------------------------------------------------------------------
// Architecture ADR and threat-model rationale (bd-k5q5.9.8.1)
// ---------------------------------------------------------------------------
/// Architecture Decision Record for the LISR system.
///
/// Records why LISR exists, what threats it addresses, and why fail-closed
/// constraints were chosen. This is embedded in code to prevent drift from
/// the original safety intent.
pub struct LisrAdr;
impl LisrAdr {
/// ADR identifier.
pub const ID: &'static str = "ADR-LISR-001";
/// Title of the architecture decision.
pub const TITLE: &'static str =
"Dynamic Secure Extension Repair with Intent-Legible Self-Healing";
/// Why LISR exists.
pub const CONTEXT: &'static str = "\
Extensions frequently break during updates when build artifacts (dist/) \
diverge from source (src/). Manual repair is slow, error-prone, and blocks \
the agent workflow. LISR provides automated repair within strict safety \
boundaries to restore extension functionality without human intervention.";
/// The core architectural decision.
pub const DECISION: &'static str = "\
Adopt a layered repair pipeline with fail-closed defaults: \
(1) security policy framework bounds all repairs, \
(2) intent legibility analysis gates repair eligibility, \
(3) deterministic rules execute safe repairs, \
(4) model-assisted repairs are constrained to whitelisted primitives, \
(5) all repairs require structural + capability + semantic proof, \
(6) overlay deployment uses canary routing with health rollback, \
(7) every action is recorded in an append-only audit ledger, \
(8) governance checks are codified in the release process.";
/// Threats addressed by the design.
pub const THREATS: &'static [&'static str] = &[
"T1: Privilege escalation via repair adding new capabilities",
"T2: Code injection via model-generated repair proposals",
"T3: Supply-chain compromise via path traversal beyond extension root",
"T4: Silent behavioral drift from opaque automated repairs",
"T5: Loss of auditability preventing incident forensics",
"T6: Governance decay from undocumented safety invariants",
];
/// Why fail-closed was chosen.
pub const FAIL_CLOSED_RATIONALE: &'static str = "\
Any uncertainty in repair safety defaults to denial. A broken extension \
that remains broken is safer than a repaired extension that silently \
escalates privileges or introduces semantic drift. The cost of a false \
negative (missed repair) is low; the cost of a false positive (unsafe \
repair applied) is catastrophic.";
/// Key safety invariants enforced by the system.
pub const INVARIANTS: &'static [&'static str] = &[
"I1: Repairs never add capabilities absent from the original extension",
"I2: All file paths stay within the extension root (monotonicity)",
"I3: Model proposals are restricted to whitelisted PatchOp primitives",
"I4: Structural validity is verified via SWC parse before activation",
"I5: Every repair decision is recorded in the append-only audit ledger",
"I6: Canary rollback triggers automatically on SLO violation",
];
}
// ---------------------------------------------------------------------------
// Operator rollout and incident playbook (bd-k5q5.9.8.2)
// ---------------------------------------------------------------------------
/// Operational procedure for repair mode selection.
pub struct OperatorPlaybook;
impl OperatorPlaybook {
/// Available repair modes and when to use each.
pub const MODE_GUIDANCE: &'static [(&'static str, &'static str)] = &[
(
"Off",
"Disable all automated repairs. Use when investigating a repair-related incident.",
),
(
"Suggest",
"Log repair suggestions without applying. Use during initial rollout or audit.",
),
(
"AutoSafe",
"Apply only safe (path-remap) repairs automatically. Default for production.",
),
(
"AutoStrict",
"Apply both safe and aggressive repairs. Use only with explicit approval.",
),
];
/// Canary rollout procedure.
pub const CANARY_PROCEDURE: &'static [&'static str] = &[
"1. Create overlay artifact from repair pipeline",
"2. Verify all proofs pass (structural, capability, semantic, conformance)",
"3. Transition to Canary state with initial overlay_percent (e.g., 10%)",
"4. Monitor health signals for canary_window_ms (default: 300_000)",
"5. If SLO violated → automatic rollback",
"6. If canary window passes → promote to Stable",
"7. Record all transitions in audit ledger",
];
/// Incident response steps.
pub const INCIDENT_RESPONSE: &'static [&'static str] = &[
"1. Set repair_mode to Off immediately",
"2. Export forensic bundle for the affected extension",
"3. Review audit ledger for the repair timeline",
"4. Check verification bundle for proof failures",
"5. Inspect health signals that triggered rollback",
"6. Root-cause the repair rule or model proposal",
"7. File ADR amendment if safety invariant was violated",
];
}
// ---------------------------------------------------------------------------
// Developer guide for adding safe repair rules (bd-k5q5.9.8.3)
// ---------------------------------------------------------------------------
/// Developer guide for contributing new repair rules.
pub struct DeveloperGuide;
impl DeveloperGuide {
/// Steps to add a new deterministic repair rule.
pub const ADD_RULE_CHECKLIST: &'static [&'static str] = &[
"1. Define a RepairPattern variant with clear trigger semantics",
"2. Define a RepairRule with: id, name, pattern, description, risk, ops",
"3. Risk must be Safe unless the rule modifies code (then Aggressive)",
"4. Add the rule to REPAIR_RULES static registry",
"5. Implement matching logic in the extension loader",
"6. Add unit tests covering: match, no-match, edge cases",
"7. Add integration test with real extension fixture",
"8. Verify monotonicity: rule must not escape extension root",
"9. Verify capability monotonicity: rule must not add capabilities",
"10. Run full conformance suite to check for regressions",
];
/// Anti-patterns to avoid.
pub const ANTI_PATTERNS: &'static [(&'static str, &'static str)] = &[
(
"Unconstrained path rewriting",
"Always validate target paths are within extension root via verify_repair_monotonicity()",
),
(
"Model-generated code execution",
"Model proposals must use PatchOp primitives only — never eval or Function()",
),
(
"Skipping verification",
"Every repair must pass the full VerificationBundle gate before activation",
),
(
"Mutable audit entries",
"AuditLedger is append-only — never expose delete or update methods",
),
(
"Implicit capability grants",
"compute_capability_proof() must show no Added deltas for the repair to pass",
),
];
/// Testing expectations for new rules.
pub const TESTING_EXPECTATIONS: &'static [&'static str] = &[
"Unit test: rule matches intended pattern and rejects non-matching input",
"Unit test: generated PatchOps have correct risk classification",
"Integration test: repair applied to real extension fixture succeeds",
"Monotonicity test: repaired path stays within extension root",
"Capability test: compute_capability_proof returns Monotonic",
"Semantic test: compute_semantic_parity returns Equivalent or AcceptableDrift",
"Conformance test: extension still passes conformance replay after repair",
];
}
// ---------------------------------------------------------------------------
// Governance checklist for CI/release (bd-k5q5.9.8.4)
// ---------------------------------------------------------------------------
/// A single governance check item.
#[derive(Debug, Clone)]
pub struct GovernanceCheck {
/// Check identifier.
pub id: String,
/// Human-readable description.
pub description: String,
/// Whether the check passed.
pub passed: bool,
/// Detail message (empty if passed).
pub detail: String,
}
/// Result of running the governance checklist.
#[derive(Debug, Clone)]
pub struct GovernanceReport {
/// Individual check results.
pub checks: Vec<GovernanceCheck>,
/// Number of checks that passed.
pub passed_count: usize,
/// Total number of checks.
pub total_count: usize,
}
impl GovernanceReport {
/// True if all governance checks passed.
pub const fn all_passed(&self) -> bool {
self.passed_count == self.total_count
}
/// Return failing checks.
pub fn failures(&self) -> Vec<&GovernanceCheck> {
self.checks.iter().filter(|c| !c.passed).collect()
}
}
/// Run the governance checklist against current system state.
///
/// Checks:
/// 1. Repair registry has at least one rule.
/// 2. All rule IDs are non-empty.
/// 3. ADR invariants are non-empty (documentation exists).
/// 4. Audit ledger is available (can be constructed).
/// 5. Telemetry collector is available (can be constructed).
/// 6. `VerificationBundle` checks all four proof layers.
pub fn run_governance_checklist() -> GovernanceReport {
let mut checks = Vec::new();
// Check 1: Registry has rules.
checks.push(GovernanceCheck {
id: "GOV-001".to_string(),
description: "Repair registry contains at least one rule".to_string(),
passed: !REPAIR_RULES.is_empty(),
detail: if REPAIR_RULES.is_empty() {
"REPAIR_RULES is empty".to_string()
} else {
String::new()
},
});
// Check 2: All rule IDs are non-empty.
let empty_ids: Vec<_> = REPAIR_RULES
.iter()
.filter(|r| r.id.is_empty())
.map(|r| r.description)
.collect();
checks.push(GovernanceCheck {
id: "GOV-002".to_string(),
description: "All repair rules have non-empty IDs".to_string(),
passed: empty_ids.is_empty(),
detail: if empty_ids.is_empty() {
String::new()
} else {
format!("Rules with empty IDs: {empty_ids:?}")
},
});
// Check 3: ADR exists.
checks.push(GovernanceCheck {
id: "GOV-003".to_string(),
description: "Architecture ADR is defined".to_string(),
passed: !LisrAdr::INVARIANTS.is_empty(),
detail: String::new(),
});
// Check 4: ADR threats are documented.
checks.push(GovernanceCheck {
id: "GOV-004".to_string(),
description: "Threat model is documented".to_string(),
passed: !LisrAdr::THREATS.is_empty(),
detail: String::new(),
});
// Check 5: Governance invariant count matches expected.
let invariant_count = LisrAdr::INVARIANTS.len();
checks.push(GovernanceCheck {
id: "GOV-005".to_string(),
description: "Safety invariants cover all critical areas (>=6)".to_string(),
passed: invariant_count >= 6,
detail: if invariant_count < 6 {
format!("Only {invariant_count} invariants defined (need >=6)")
} else {
String::new()
},
});
// Check 6: Developer guide has testing expectations.
checks.push(GovernanceCheck {
id: "GOV-006".to_string(),
description: "Developer testing expectations are documented".to_string(),
passed: !DeveloperGuide::TESTING_EXPECTATIONS.is_empty(),
detail: String::new(),
});
let passed_count = checks.iter().filter(|c| c.passed).count();
let total_count = checks.len();
GovernanceReport {
checks,
passed_count,
total_count,
}
}
#[derive(Debug, Clone)]
pub struct PiJsRuntimeConfig {
pub cwd: String,
pub args: Vec<String>,
pub env: HashMap<String, String>,
pub limits: PiJsRuntimeLimits,
/// Controls the auto-repair pipeline behavior. Default: `AutoSafe`.
pub repair_mode: RepairMode,
/// UNSAFE escape hatch: enable synchronous process execution used by
/// `node:child_process` sync APIs (`execSync`/`spawnSync`/`execFileSync`).
///
/// Security default is `false` so extensions cannot bypass capability/risk
/// mediation through direct synchronous subprocess execution.
pub allow_unsafe_sync_exec: bool,
/// Explicitly deny environment variable access regardless of `is_env_var_allowed` blocklist.
/// Used to enforce `ExtensionPolicy` with `deny_caps=[\"env\"]` for synchronous `pi.env` access.
pub deny_env: bool,
/// Directory for persistent transpiled-source disk cache.
///
/// When set, transpiled module sources are cached on disk keyed by a
/// content-aware hash so that SWC transpilation is skipped across process
/// restarts. Defaults to `~/.pi/agent/cache/modules/` (overridden by
/// `PIJS_MODULE_CACHE_DIR`). Set to `None` to disable.
pub disk_cache_dir: Option<PathBuf>,
}
impl PiJsRuntimeConfig {
/// Convenience: check if repairs should be applied.
pub const fn auto_repair_enabled(&self) -> bool {
self.repair_mode.should_apply()
}
}
impl Default for PiJsRuntimeConfig {
fn default() -> Self {
Self {
cwd: ".".to_string(),
args: Vec::new(),
env: HashMap::new(),
limits: PiJsRuntimeLimits::default(),
repair_mode: RepairMode::default(),
allow_unsafe_sync_exec: false,
deny_env: true,
disk_cache_dir: runtime_disk_cache_dir(),
}
}
}
/// Resolve the persistent module disk cache directory.
///
/// Priority: `PIJS_MODULE_CACHE_DIR` env var > `~/.pi/agent/cache/modules/`.
/// Set `PIJS_MODULE_CACHE_DIR=""` to explicitly disable the disk cache.
fn runtime_disk_cache_dir() -> Option<PathBuf> {
if let Some(raw) = std::env::var_os("PIJS_MODULE_CACHE_DIR") {
return if raw.is_empty() {
None
} else {
Some(PathBuf::from(raw))
};
}
dirs::home_dir().map(|home| home.join(".pi").join("agent").join("cache").join("modules"))
}
#[derive(Debug)]
struct InterruptBudget {
configured: Option<u64>,
remaining: std::cell::Cell<Option<u64>>,
tripped: std::cell::Cell<bool>,
}
impl InterruptBudget {
const fn new(configured: Option<u64>) -> Self {
Self {
configured,
remaining: std::cell::Cell::new(None),
tripped: std::cell::Cell::new(false),
}
}
fn reset(&self) {
self.remaining.set(self.configured);
self.tripped.set(false);
}
fn on_interrupt(&self) -> bool {
let Some(remaining) = self.remaining.get() else {
return false;
};
if remaining == 0 {
self.tripped.set(true);
return true;
}
self.remaining.set(Some(remaining - 1));
false
}
fn did_trip(&self) -> bool {
self.tripped.get()
}
fn clear_trip(&self) {
self.tripped.set(false);
}
}
#[derive(Debug, Default)]
struct HostcallTracker {
pending: HashSet<String>,
call_to_timer: HashMap<String, u64>,
timer_to_call: HashMap<u64, String>,
enqueued_at_ms: HashMap<String, u64>,
}
enum HostcallCompletion {
Delivered {
#[allow(dead_code)]
timer_id: Option<u64>,
},
Unknown,
}
impl HostcallTracker {
fn clear(&mut self) {
self.pending.clear();
self.call_to_timer.clear();
self.timer_to_call.clear();
self.enqueued_at_ms.clear();
}
fn register(&mut self, call_id: String, timer_id: Option<u64>, enqueued_at_ms: u64) {
self.pending.insert(call_id.clone());
if let Some(timer_id) = timer_id {
self.call_to_timer.insert(call_id.clone(), timer_id);
self.timer_to_call.insert(timer_id, call_id.clone());
}
// Last insert consumes call_id, avoiding one clone.
self.enqueued_at_ms.insert(call_id, enqueued_at_ms);
}
fn pending_count(&self) -> usize {
self.pending.len()
}
fn is_pending(&self, call_id: &str) -> bool {
self.pending.contains(call_id)
}
fn queue_wait_ms(&self, call_id: &str, now_ms: u64) -> Option<u64> {
self.enqueued_at_ms
.get(call_id)
.copied()
.map(|enqueued| now_ms.saturating_sub(enqueued))
}
fn on_complete(&mut self, call_id: &str) -> HostcallCompletion {
if !self.pending.remove(call_id) {
return HostcallCompletion::Unknown;
}
let timer_id = self.call_to_timer.remove(call_id);
self.enqueued_at_ms.remove(call_id);
if let Some(timer_id) = timer_id {
self.timer_to_call.remove(&timer_id);
}
HostcallCompletion::Delivered { timer_id }
}
fn take_timed_out_call(&mut self, timer_id: u64) -> Option<String> {
let call_id = self.timer_to_call.remove(&timer_id)?;
self.call_to_timer.remove(&call_id);
self.enqueued_at_ms.remove(&call_id);
if !self.pending.remove(&call_id) {
return None;
}
Some(call_id)
}
}
fn enqueue_hostcall_request_with_backpressure<C: SchedulerClock>(
queue: &HostcallQueue,
tracker: &Rc<RefCell<HostcallTracker>>,
scheduler: &Rc<RefCell<Scheduler<C>>>,
request: HostcallRequest,
) {
let call_id = request.call_id.clone();
let trace_id = request.trace_id;
let extension_id = request.extension_id.clone();
match queue.borrow_mut().push_back(request) {
HostcallQueueEnqueueResult::FastPath { depth } => {
tracing::trace!(
event = "pijs.hostcall.queue.fast_path",
call_id = %call_id,
trace_id,
extension_id = ?extension_id,
depth,
"Hostcall queued on fast-path ring"
);
}
HostcallQueueEnqueueResult::OverflowPath {
depth,
overflow_depth,
} => {
tracing::debug!(
event = "pijs.hostcall.queue.overflow_path",
call_id = %call_id,
trace_id,
extension_id = ?extension_id,
depth,
overflow_depth,
"Hostcall spilled to overflow queue"
);
}
HostcallQueueEnqueueResult::Rejected {
depth,
overflow_depth,
} => {
let completion = tracker.borrow_mut().on_complete(&call_id);
if let HostcallCompletion::Delivered { timer_id } = completion {
if let Some(timer_id) = timer_id {
let _ = scheduler.borrow_mut().clear_timeout(timer_id);
}
scheduler.borrow_mut().enqueue_hostcall_complete(
call_id.clone(),
HostcallOutcome::Error {
code: "overloaded".to_string(),
message: format!(
"Hostcall queue overloaded (depth={depth}, overflow_depth={overflow_depth})"
),
},
);
}
tracing::warn!(
event = "pijs.hostcall.queue.rejected",
call_id = %call_id,
trace_id,
extension_id = ?extension_id,
depth,
overflow_depth,
"Hostcall rejected by queue backpressure policy"
);
}
}
}
// ============================================================================
// PiJS Module Loader (TypeScript + virtual modules)
// ============================================================================
#[derive(Debug)]
struct PiJsModuleState {
/// Immutable built-in virtual modules shared across runtimes.
static_virtual_modules: Arc<HashMap<String, String>>,
/// Runtime-local virtual modules generated by repairs / dynamic stubs.
dynamic_virtual_modules: HashMap<String, String>,
/// Tracked named exports for dynamic virtual modules keyed by specifier.
dynamic_virtual_named_exports: HashMap<String, BTreeSet<String>>,
compiled_sources: HashMap<String, CompiledModuleCacheEntry>,
module_cache_counters: ModuleCacheCounters,
/// Repair mode propagated from `PiJsRuntimeConfig` so the resolver can
/// gate fallback patterns without executing any broken code.
repair_mode: RepairMode,
/// Extension root directories used to detect monorepo escape (Pattern 3).
/// Populated as extensions are loaded via [`PiJsRuntime::add_extension_root`].
extension_roots: Vec<PathBuf>,
/// Source-tier classification per extension root. Used by Pattern 4 to
/// avoid proxy stubs for official/first-party extensions.
extension_root_tiers: HashMap<PathBuf, ProxyStubSourceTier>,
/// Package scope (`@scope`) per extension root (when discoverable from
/// package.json name). Pattern 4 allows same-scope packages.
extension_root_scopes: HashMap<PathBuf, String>,
/// Shared handle for recording repair events from the resolver.
repair_events: Arc<std::sync::Mutex<Vec<ExtensionRepairEvent>>>,
/// Directory for persistent transpiled-source disk cache.
disk_cache_dir: Option<PathBuf>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ProxyStubSourceTier {
Official,
Community,
Unknown,
}
#[derive(Debug, Clone)]
struct CompiledModuleCacheEntry {
cache_key: Option<String>,
source: Arc<[u8]>,
}
#[derive(Debug, Clone, Copy, Default)]
struct ModuleCacheCounters {
hits: u64,
misses: u64,
invalidations: u64,
disk_hits: u64,
}
impl PiJsModuleState {
fn new() -> Self {
Self {
static_virtual_modules: default_virtual_modules_shared(),
dynamic_virtual_modules: HashMap::new(),
dynamic_virtual_named_exports: HashMap::new(),
compiled_sources: HashMap::new(),
module_cache_counters: ModuleCacheCounters::default(),
repair_mode: RepairMode::default(),
extension_roots: Vec::new(),
extension_root_tiers: HashMap::new(),
extension_root_scopes: HashMap::new(),
repair_events: Arc::new(std::sync::Mutex::new(Vec::new())),
disk_cache_dir: None,
}
}
const fn with_repair_mode(mut self, mode: RepairMode) -> Self {
self.repair_mode = mode;
self
}
fn with_repair_events(
mut self,
events: Arc<std::sync::Mutex<Vec<ExtensionRepairEvent>>>,
) -> Self {
self.repair_events = events;
self
}
fn with_disk_cache_dir(mut self, dir: Option<PathBuf>) -> Self {
self.disk_cache_dir = dir;
self
}
}
#[derive(Clone, Debug)]
struct PiJsResolver {
state: Rc<RefCell<PiJsModuleState>>,
}
fn canonical_node_builtin(spec: &str) -> Option<&'static str> {
match spec {
"fs" | "node:fs" => Some("node:fs"),
"fs/promises" | "node:fs/promises" => Some("node:fs/promises"),
"path" | "node:path" => Some("node:path"),
"os" | "node:os" => Some("node:os"),
"child_process" | "node:child_process" => Some("node:child_process"),
"crypto" | "node:crypto" => Some("node:crypto"),
"http" | "node:http" => Some("node:http"),
"https" | "node:https" => Some("node:https"),
"http2" | "node:http2" => Some("node:http2"),
"util" | "node:util" => Some("node:util"),
"readline" | "node:readline" => Some("node:readline"),
"url" | "node:url" => Some("node:url"),
"net" | "node:net" => Some("node:net"),
"events" | "node:events" => Some("node:events"),
"buffer" | "node:buffer" => Some("node:buffer"),
"assert" | "node:assert" => Some("node:assert"),
"stream" | "node:stream" => Some("node:stream"),
"stream/web" | "node:stream/web" => Some("node:stream/web"),
"module" | "node:module" => Some("node:module"),
"string_decoder" | "node:string_decoder" => Some("node:string_decoder"),
"querystring" | "node:querystring" => Some("node:querystring"),
"process" | "node:process" => Some("node:process"),
"stream/promises" | "node:stream/promises" => Some("node:stream/promises"),
"constants" | "node:constants" => Some("node:constants"),
"tls" | "node:tls" => Some("node:tls"),
"tty" | "node:tty" => Some("node:tty"),
"zlib" | "node:zlib" => Some("node:zlib"),
"perf_hooks" | "node:perf_hooks" => Some("node:perf_hooks"),
"vm" | "node:vm" => Some("node:vm"),
"v8" | "node:v8" => Some("node:v8"),
"worker_threads" | "node:worker_threads" => Some("node:worker_threads"),
_ => None,
}
}
fn is_network_specifier(spec: &str) -> bool {
spec.starts_with("http://")
|| spec.starts_with("https://")
|| spec.starts_with("http:")
|| spec.starts_with("https:")
}
fn is_bare_package_specifier(spec: &str) -> bool {
if spec.starts_with("./")
|| spec.starts_with("../")
|| spec.starts_with('/')
|| spec.starts_with("file://")
|| spec.starts_with("node:")
{
return false;
}
!spec.contains(':')
}
fn unsupported_module_specifier_message(spec: &str) -> String {
if is_network_specifier(spec) {
return format!("Network module imports are not supported in PiJS: {spec}");
}
if is_bare_package_specifier(spec) {
return format!("Package module specifiers are not supported in PiJS: {spec}");
}
format!("Unsupported module specifier: {spec}")
}
fn split_scoped_package(spec: &str) -> Option<(&str, &str)> {
if !spec.starts_with('@') {
return None;
}
let mut parts = spec.split('/');
let scope = parts.next()?;
let package = parts.next()?;
Some((scope, package))
}
fn package_scope(spec: &str) -> Option<&str> {
split_scoped_package(spec).map(|(scope, _)| scope)
}
fn read_extension_package_scope(root: &Path) -> Option<String> {
let package_json = root.join("package.json");
let raw = fs::read_to_string(package_json).ok()?;
let parsed: serde_json::Value = serde_json::from_str(&raw).ok()?;
let name = parsed.get("name").and_then(serde_json::Value::as_str)?;
let (scope, _) = split_scoped_package(name.trim())?;
Some(scope.to_string())
}
fn root_path_hint_tier(root: &Path) -> ProxyStubSourceTier {
let normalized = root
.to_string_lossy()
.replace('\\', "/")
.to_ascii_lowercase();
let community_hints = [
"/community/",
"/npm/",
"/agents-",
"/third-party",
"/third_party",
"/plugins-community/",
];
if community_hints.iter().any(|hint| normalized.contains(hint)) {
return ProxyStubSourceTier::Community;
}
let official_hints = ["/official-pi-mono/", "/plugins-official/", "/official/"];
if official_hints.iter().any(|hint| normalized.contains(hint)) {
return ProxyStubSourceTier::Official;
}
ProxyStubSourceTier::Unknown
}
fn classify_proxy_stub_source_tier(extension_id: &str, root: &Path) -> ProxyStubSourceTier {
let id = extension_id.trim().to_ascii_lowercase();
if id.starts_with("community/")
|| id.starts_with("npm/")
|| id.starts_with("agents-")
|| id.starts_with("plugins-community/")
|| id.starts_with("third-party")
|| id.starts_with("third_party")
{
return ProxyStubSourceTier::Community;
}
if id.starts_with("plugins-official/") {
return ProxyStubSourceTier::Official;
}
root_path_hint_tier(root)
}
fn resolve_extension_root_for_base<'a>(base: &str, roots: &'a [PathBuf]) -> Option<&'a PathBuf> {
let base_path = Path::new(base);
let canonical_base = crate::extensions::safe_canonicalize(base_path);
roots
.iter()
.filter(|root| {
let canonical_root = crate::extensions::safe_canonicalize(root);
canonical_base.starts_with(&canonical_root)
})
.max_by_key(|root| root.components().count())
}
fn is_proxy_blocklisted_package(spec: &str) -> bool {
if spec.starts_with("node:") {
return true;
}
let top = spec.split('/').next().unwrap_or(spec);
matches!(
top,
"fs" | "path"
| "child_process"
| "net"
| "http"
| "https"
| "crypto"
| "tls"
| "dgram"
| "dns"
| "vm"
| "worker_threads"
| "cluster"
| "module"
| "os"
| "process"
)
}
fn is_proxy_allowlisted_package(spec: &str) -> bool {
const ALLOWLIST_SCOPES: &[&str] = &["@sourcegraph", "@marckrenn", "@aliou"];
const ALLOWLIST_PACKAGES: &[&str] = &[
"openai",
"adm-zip",
"linkedom",
"p-limit",
"unpdf",
"node-pty",
"chokidar",
"jsdom",
"turndown",
"beautiful-mermaid",
];
if ALLOWLIST_PACKAGES.contains(&spec) {
return true;
}
if let Some((scope, package)) = split_scoped_package(spec) {
if ALLOWLIST_SCOPES.contains(&scope) {
return true;
}
// Generic ecosystem package pattern (`@scope/pi-*`).
if package.starts_with("pi-") {
return true;
}
}
false
}
// Limit extension source size to prevent OOM/DoS during load.
const MAX_MODULE_SOURCE_BYTES: u64 = 1024 * 1024 * 1024;
fn should_auto_stub_package(
spec: &str,
base: &str,
extension_roots: &[PathBuf],
extension_root_tiers: &HashMap<PathBuf, ProxyStubSourceTier>,
extension_root_scopes: &HashMap<PathBuf, String>,
) -> bool {
if !is_bare_package_specifier(spec) || is_proxy_blocklisted_package(spec) {
return false;
}
let (tier, root_for_scope) = resolve_extension_root_for_base(base, extension_roots).map_or(
(ProxyStubSourceTier::Unknown, None),
|root| {
(
extension_root_tiers
.get(root)
.copied()
.unwrap_or(ProxyStubSourceTier::Unknown),
Some(root),
)
},
);
let same_scope = if let Some(spec_scope) = package_scope(spec)
&& let Some(root) = root_for_scope
&& let Some(extension_scope) = extension_root_scopes.get(root)
{
extension_scope == spec_scope
} else {
false
};
if is_proxy_allowlisted_package(spec) {
return true;
}
if same_scope {
return true;
}
// Aggressive repair mode (Pattern 4) is only enabled in AutoStrict. In that
// mode, community and unknown extension sources are allowed to auto-stub any
// unresolved non-blocklisted package so registration can proceed deterministically.
//
// Official first-party extensions keep a narrower posture: only curated
// allowlist or same-scope packages are stubbed.
tier != ProxyStubSourceTier::Official
}
fn is_valid_js_export_name(name: &str) -> bool {
let mut chars = name.chars();
let Some(first) = chars.next() else {
return false;
};
let is_start = first == '_' || first == '$' || first.is_ascii_alphabetic();
if !is_start {
return false;
}
chars.all(|c| c == '_' || c == '$' || c.is_ascii_alphanumeric())
}
fn generate_proxy_stub_module(spec: &str, named_exports: &BTreeSet<String>) -> String {
let spec_literal = serde_json::to_string(spec).unwrap_or_else(|_| "\"<unknown>\"".to_string());
let mut source = format!(
r"// Auto-generated npm proxy stub (Pattern 4) for {spec_literal}
const __pkg = {spec_literal};
const __handler = {{
get(_target, prop) {{
if (typeof prop === 'symbol') {{
if (prop === Symbol.toPrimitive) return () => '';
return undefined;
}}
if (prop === '__esModule') return true;
if (prop === 'default') return __stub;
if (prop === 'toString') return () => '';
if (prop === 'valueOf') return () => '';
if (prop === 'name') return __pkg;
// Promise assimilation guard: do not pretend to be then-able.
if (prop === 'then') return undefined;
return __stub;
}},
apply() {{ return __stub; }},
construct() {{ return __stub; }},
has() {{ return false; }},
ownKeys() {{ return []; }},
getOwnPropertyDescriptor() {{
return {{ configurable: true, enumerable: false }};
}},
}};
const __stub = new Proxy(function __pijs_noop() {{}}, __handler);
"
);
for name in named_exports {
if name == "default" || name == "__esModule" || !is_valid_js_export_name(name) {
continue;
}
let _ = writeln!(source, "export const {name} = __stub;");
}
source.push_str("export default __stub;\n");
source.push_str("export const __pijs_proxy_stub = __stub;\n");
source.push_str("export const __esModule = true;\n");
source
}
fn builtin_specifier_aliases(spec: &str, canonical: &str) -> Vec<String> {
let mut aliases = Vec::new();
let mut seen = HashSet::new();
let mut push_alias = |candidate: &str| {
if candidate.is_empty() {
return;
}
if seen.insert(candidate.to_string()) {
aliases.push(candidate.to_string());
}
};
push_alias(spec);
push_alias(canonical);
if let Some(bare) = spec.strip_prefix("node:") {
push_alias(bare);
}
if let Some(bare) = canonical.strip_prefix("node:") {
push_alias(bare);
}
aliases
}
fn extract_builtin_import_names(source: &str, spec: &str, canonical: &str) -> BTreeSet<String> {
let mut names = BTreeSet::new();
for alias in builtin_specifier_aliases(spec, canonical) {
for name in extract_import_names(source, &alias) {
if name == "default" || name == "__esModule" {
continue;
}
if is_valid_js_export_name(&name) {
names.insert(name);
}
}
}
names
}
fn generate_builtin_compat_overlay_module(
canonical: &str,
named_exports: &BTreeSet<String>,
) -> String {
let spec_literal =
serde_json::to_string(canonical).unwrap_or_else(|_| "\"node:unknown\"".to_string());
let mut source = format!(
r"// Auto-generated Node builtin compatibility overlay for {canonical}
import * as __pijs_builtin_ns from {spec_literal};
const __pijs_builtin_default =
__pijs_builtin_ns.default !== undefined ? __pijs_builtin_ns.default : __pijs_builtin_ns;
export default __pijs_builtin_default;
"
);
for name in named_exports {
if !is_valid_js_export_name(name) || name == "default" || name == "__esModule" {
continue;
}
let _ = writeln!(
source,
"export const {name} = __pijs_builtin_ns.{name} !== undefined ? __pijs_builtin_ns.{name} : (__pijs_builtin_default && __pijs_builtin_default.{name});"
);
}
source.push_str("export const __esModule = true;\n");
source
}
fn builtin_overlay_module_key(base: &str, canonical: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(base.as_bytes());
let digest = format!("{:x}", hasher.finalize());
let short = &digest[..16];
format!("pijs-compat://builtin/{canonical}/{short}")
}
fn maybe_register_builtin_compat_overlay(
state: &mut PiJsModuleState,
base: &str,
spec: &str,
canonical: &str,
) -> Option<String> {
if !canonical.starts_with("node:") {
return None;
}
let source = std::fs::read_to_string(base).ok()?;
let extracted_names = extract_builtin_import_names(&source, spec, canonical);
if extracted_names.is_empty() {
return None;
}
let overlay_key = builtin_overlay_module_key(base, canonical);
let needs_rebuild = state
.dynamic_virtual_named_exports
.get(&overlay_key)
.is_none_or(|existing| existing != &extracted_names)
|| !state.dynamic_virtual_modules.contains_key(&overlay_key);
if needs_rebuild {
state
.dynamic_virtual_named_exports
.insert(overlay_key.clone(), extracted_names.clone());
let overlay = generate_builtin_compat_overlay_module(canonical, &extracted_names);
state
.dynamic_virtual_modules
.insert(overlay_key.clone(), overlay);
if state.compiled_sources.remove(&overlay_key).is_some() {
state.module_cache_counters.invalidations =
state.module_cache_counters.invalidations.saturating_add(1);
}
}
Some(overlay_key)
}
impl JsModuleResolver for PiJsResolver {
#[allow(clippy::too_many_lines)]
fn resolve(&mut self, _ctx: &Ctx<'_>, base: &str, name: &str) -> rquickjs::Result<String> {
let spec = name.trim();
if spec.is_empty() {
return Err(rquickjs::Error::new_resolving(base, name));
}
// Alias bare Node.js builtins to their node: prefixed virtual modules.
let canonical = canonical_node_builtin(spec).unwrap_or(spec);
let compat_scan_mode = is_global_compat_scan_mode();
let repair_mode = {
let mut state = self.state.borrow_mut();
if state.dynamic_virtual_modules.contains_key(canonical)
|| state.static_virtual_modules.contains_key(canonical)
{
if compat_scan_mode
&& let Some(overlay_key) =
maybe_register_builtin_compat_overlay(&mut state, base, spec, canonical)
{
tracing::debug!(
event = "pijs.compat.builtin_overlay",
base = %base,
specifier = %spec,
canonical = %canonical,
overlay = %overlay_key,
"compat overlay for builtin named imports"
);
return Ok(overlay_key);
}
return Ok(canonical.to_string());
}
state.repair_mode
};
let roots = self.state.borrow().extension_roots.clone();
if let Some(path) = resolve_module_path(base, spec, repair_mode, &roots) {
// Canonicalize to collapse `.` / `..` segments and normalise
// separators (Windows backslashes → forward slashes for QuickJS).
let canonical = crate::extensions::safe_canonicalize(&path);
let is_safe = roots.iter().any(|root| {
let canonical_root = crate::extensions::safe_canonicalize(root);
canonical.starts_with(&canonical_root)
});
if !is_safe {
tracing::warn!(
event = "pijs.resolve.escape",
base = %base,
specifier = %spec,
resolved = %canonical.display(),
"import resolved to path outside extension roots"
);
return Err(rquickjs::Error::new_resolving(base, name));
}
return Ok(canonical.to_string_lossy().replace('\\', "/"));
}
// Pattern 3 (bd-k5q5.8.4): monorepo sibling module stubs.
// When a relative import escapes all known extension roots and
// the repair mode is aggressive, generate a virtual stub module
// containing no-op exports matching the import declaration.
if spec.starts_with('.') && repair_mode.allows_aggressive() {
let state = self.state.borrow();
let roots = state.extension_roots.clone();
drop(state);
if let Some(escaped_path) = detect_monorepo_escape(base, spec, &roots) {
// Read the importing file to extract import names.
let source = std::fs::read_to_string(base).unwrap_or_default();
let names = extract_import_names(&source, spec);
let stub = generate_monorepo_stub(&names);
let virtual_key = format!("pijs-repair://monorepo/{}", escaped_path.display());
tracing::info!(
event = "pijs.repair.monorepo_escape",
base = %base,
specifier = %spec,
resolved = %escaped_path.display(),
exports = ?names,
"auto-repair: generated monorepo escape stub"
);
// Record repair event.
let state = self.state.borrow();
if let Ok(mut events) = state.repair_events.lock() {
events.push(ExtensionRepairEvent {
extension_id: String::new(),
pattern: RepairPattern::MonorepoEscape,
original_error: format!(
"monorepo escape: {} from {base}",
escaped_path.display()
),
repair_action: format!(
"generated stub with {} exports: {virtual_key}",
names.len()
),
success: true,
timestamp_ms: 0,
});
}
drop(state);
// Register and return the virtual module.
let mut state = self.state.borrow_mut();
state
.dynamic_virtual_modules
.insert(virtual_key.clone(), stub);
return Ok(virtual_key);
}
}
// Pattern 4 (bd-k5q5.8.5): proxy-based stubs for allowlisted npm deps.
// This fires in aggressive mode, and also in compatibility-scan mode
// (ext-conformance / PI_EXT_COMPAT_SCAN) so corpus runs can continue
// past optional or non-essential package holes deterministically.
// Blocklisted/system packages are never stubbed. Existing hand-written
// virtual modules continue to win because we only reach this branch
// after the initial lookup misses.
if is_bare_package_specifier(spec) && (repair_mode.allows_aggressive() || compat_scan_mode)
{
let state = self.state.borrow();
let roots = state.extension_roots.clone();
let tiers = state.extension_root_tiers.clone();
let scopes = state.extension_root_scopes.clone();
drop(state);
if should_auto_stub_package(spec, base, &roots, &tiers, &scopes) {
tracing::info!(
event = "pijs.repair.missing_npm_dep",
base = %base,
specifier = %spec,
"auto-repair: generated proxy stub for missing npm dependency"
);
let source = std::fs::read_to_string(base).unwrap_or_default();
let extracted_names = extract_import_names(&source, spec);
let mut state = self.state.borrow_mut();
let entry_key = spec.to_string();
let mut exports_changed = false;
{
let exports = state
.dynamic_virtual_named_exports
.entry(entry_key.clone())
.or_default();
for name in extracted_names {
exports_changed |= exports.insert(name);
}
}
let export_names = state
.dynamic_virtual_named_exports
.get(&entry_key)
.cloned()
.unwrap_or_default();
if exports_changed || !state.dynamic_virtual_modules.contains_key(spec) {
let stub = generate_proxy_stub_module(spec, &export_names);
state.dynamic_virtual_modules.insert(entry_key, stub);
if state.compiled_sources.remove(spec).is_some() {
state.module_cache_counters.invalidations =
state.module_cache_counters.invalidations.saturating_add(1);
}
}
if let Ok(mut events) = state.repair_events.lock() {
events.push(ExtensionRepairEvent {
extension_id: String::new(),
pattern: RepairPattern::MissingNpmDep,
original_error: format!("missing npm dependency: {spec} from {base}"),
repair_action: format!(
"generated proxy stub for package '{spec}' with {} named export(s)",
export_names.len()
),
success: true,
timestamp_ms: 0,
});
}
return Ok(spec.to_string());
}
}
Err(rquickjs::Error::new_resolving_message(
base,
name,
unsupported_module_specifier_message(spec),
))
}
}
#[derive(Clone, Debug)]
struct PiJsLoader {
state: Rc<RefCell<PiJsModuleState>>,
}
impl JsModuleLoader for PiJsLoader {
fn load<'js>(
&mut self,
ctx: &Ctx<'js>,
name: &str,
) -> rquickjs::Result<Module<'js, JsModuleDeclared>> {
let source = {
let mut state = self.state.borrow_mut();
load_compiled_module_source(&mut state, name)?
};
Module::declare(ctx.clone(), name, source)
}
}
fn compile_module_source(
static_virtual_modules: &HashMap<String, String>,
dynamic_virtual_modules: &HashMap<String, String>,
name: &str,
) -> rquickjs::Result<Vec<u8>> {
if let Some(source) = dynamic_virtual_modules
.get(name)
.or_else(|| static_virtual_modules.get(name))
{
return Ok(prefix_import_meta_url(name, source));
}
let path = Path::new(name);
if !path.is_file() {
return Err(rquickjs::Error::new_loading_message(
name,
"Module is not a file",
));
}
let metadata = fs::metadata(path)
.map_err(|err| rquickjs::Error::new_loading_message(name, format!("metadata: {err}")))?;
if metadata.len() > MAX_MODULE_SOURCE_BYTES {
return Err(rquickjs::Error::new_loading_message(
name,
format!(
"Module source exceeds size limit: {} > {}",
metadata.len(),
MAX_MODULE_SOURCE_BYTES
),
));
}
let extension = path.extension().and_then(|ext| ext.to_str()).unwrap_or("");
let raw = fs::read_to_string(path)
.map_err(|err| rquickjs::Error::new_loading_message(name, format!("read: {err}")))?;
let compiled = match extension {
"ts" | "tsx" => {
let transpiled = transpile_typescript_module(&raw, name).map_err(|message| {
rquickjs::Error::new_loading_message(name, format!("transpile: {message}"))
})?;
rewrite_legacy_private_identifiers(&maybe_cjs_to_esm(&transpiled))
}
"js" | "mjs" => rewrite_legacy_private_identifiers(&maybe_cjs_to_esm(&raw)),
"json" => json_module_to_esm(&raw, name).map_err(|message| {
rquickjs::Error::new_loading_message(name, format!("json: {message}"))
})?,
other => {
return Err(rquickjs::Error::new_loading_message(
name,
format!("Unsupported module extension: {other}"),
));
}
};
Ok(prefix_import_meta_url(name, &compiled))
}
fn module_cache_key(
static_virtual_modules: &HashMap<String, String>,
dynamic_virtual_modules: &HashMap<String, String>,
name: &str,
) -> Option<String> {
if let Some(source) = dynamic_virtual_modules
.get(name)
.or_else(|| static_virtual_modules.get(name))
{
let mut hasher = Sha256::new();
hasher.update(b"virtual\0");
hasher.update(name.as_bytes());
hasher.update(b"\0");
hasher.update(source.as_bytes());
return Some(format!("v:{:x}", hasher.finalize()));
}
let path = Path::new(name);
if !path.is_file() {
return None;
}
let metadata = fs::metadata(path).ok()?;
let modified_nanos = metadata
.modified()
.ok()
.and_then(|ts| ts.duration_since(UNIX_EPOCH).ok())
.map_or(0, |duration| duration.as_nanos());
Some(format!("f:{name}:{}:{modified_nanos}", metadata.len()))
}
// ============================================================================
// Persistent disk cache for transpiled module sources (bd-3ar8v.4.16)
// ============================================================================
/// Build the on-disk path for a cached transpiled module.
///
/// Layout: `{cache_dir}/{first_2_hex}/{full_hex}.js` to shard entries and
/// avoid a single flat directory with thousands of files.
fn disk_cache_path(cache_dir: &Path, cache_key: &str) -> PathBuf {
let mut hasher = Sha256::new();
hasher.update(cache_key.as_bytes());
let hex = format!("{:x}", hasher.finalize());
let prefix = &hex[..2];
cache_dir.join(prefix).join(format!("{hex}.js"))
}
/// Attempt to load a transpiled module source from persistent disk cache.
fn try_load_from_disk_cache(cache_dir: &Path, cache_key: &str) -> Option<Vec<u8>> {
let path = disk_cache_path(cache_dir, cache_key);
fs::read(path).ok()
}
/// Persist a transpiled module source to the disk cache (best-effort).
fn store_to_disk_cache(cache_dir: &Path, cache_key: &str, source: &[u8]) {
let path = disk_cache_path(cache_dir, cache_key);
if let Some(parent) = path.parent() {
if let Err(err) = fs::create_dir_all(parent) {
tracing::debug!(event = "pijs.module_cache.disk.mkdir_failed", path = %parent.display(), %err);
return;
}
}
let temp_path = path.with_extension(format!("tmp.{}", uuid::Uuid::new_v4().simple()));
if let Err(err) = fs::write(&temp_path, source) {
tracing::debug!(event = "pijs.module_cache.disk.write_failed", path = %temp_path.display(), %err);
return;
}
if let Err(err) = fs::rename(&temp_path, &path) {
tracing::debug!(event = "pijs.module_cache.disk.rename_failed", from = %temp_path.display(), to = %path.display(), %err);
let _ = fs::remove_file(&temp_path);
}
}
fn load_compiled_module_source(
state: &mut PiJsModuleState,
name: &str,
) -> rquickjs::Result<Vec<u8>> {
let cache_key = module_cache_key(
&state.static_virtual_modules,
&state.dynamic_virtual_modules,
name,
);
// 1. Check in-memory cache — Arc clone is O(1) atomic increment.
if let Some(cached) = state.compiled_sources.get(name) {
if cached.cache_key == cache_key {
state.module_cache_counters.hits = state.module_cache_counters.hits.saturating_add(1);
return Ok(cached.source.to_vec());
}
state.module_cache_counters.invalidations =
state.module_cache_counters.invalidations.saturating_add(1);
}
// 2. Check persistent disk cache.
if let Some(cache_key_str) = cache_key.as_deref()
&& let Some(cache_dir) = state.disk_cache_dir.as_deref()
&& let Some(disk_cached) = try_load_from_disk_cache(cache_dir, cache_key_str)
{
state.module_cache_counters.disk_hits =
state.module_cache_counters.disk_hits.saturating_add(1);
let source: Arc<[u8]> = disk_cached.into();
state.compiled_sources.insert(
name.to_string(),
CompiledModuleCacheEntry {
cache_key,
source: Arc::clone(&source),
},
);
return Ok(source.to_vec());
}
// 3. Compile from source (SWC transpile + CJS->ESM rewrite).
state.module_cache_counters.misses = state.module_cache_counters.misses.saturating_add(1);
let compiled = compile_module_source(
&state.static_virtual_modules,
&state.dynamic_virtual_modules,
name,
)?;
let source: Arc<[u8]> = compiled.into();
state.compiled_sources.insert(
name.to_string(),
CompiledModuleCacheEntry {
cache_key: cache_key.clone(),
source: Arc::clone(&source),
},
);
// 4. Persist to disk cache for next session.
if let Some(cache_key_str) = cache_key.as_deref()
&& let Some(cache_dir) = state.disk_cache_dir.as_deref()
{
store_to_disk_cache(cache_dir, cache_key_str, &source);
}
Ok(source.to_vec())
}
// ============================================================================
// Warm Isolate Pool (bd-3ar8v.4.16)
// ============================================================================
/// Configuration holder and factory for pre-warmed JS extension runtimes.
///
/// Since `PiJsRuntime` uses `Rc` internally and cannot cross thread
/// boundaries, the pool does not hold live runtime instances. Instead, it
/// provides a factory that produces pre-configured `PiJsRuntimeConfig` values,
/// and runtimes can be returned to a "warm" state via
/// [`PiJsRuntime::reset_transient_state`].
///
/// # Lifecycle
///
/// 1. Create pool with desired config via [`WarmIsolatePool::new`].
/// 2. Call [`make_config`](WarmIsolatePool::make_config) to get a pre-warmed
/// `PiJsRuntimeConfig` for each runtime thread.
/// 3. After use, call [`PiJsRuntime::reset_transient_state`] to return the
/// runtime to a clean state (keeping the transpiled source cache).
#[derive(Debug, Clone)]
pub struct WarmIsolatePool {
/// Template configuration for new runtimes.
template: PiJsRuntimeConfig,
/// Number of runtimes created from this pool.
created_count: Arc<AtomicU64>,
/// Number of resets performed.
reset_count: Arc<AtomicU64>,
}
impl WarmIsolatePool {
/// Create a new warm isolate pool with the given template config.
pub fn new(template: PiJsRuntimeConfig) -> Self {
Self {
template,
created_count: Arc::new(AtomicU64::new(0)),
reset_count: Arc::new(AtomicU64::new(0)),
}
}
/// Create a pre-configured `PiJsRuntimeConfig` with shared pool state.
pub fn make_config(&self) -> PiJsRuntimeConfig {
self.created_count.fetch_add(1, AtomicOrdering::Relaxed);
self.template.clone()
}
/// Record that a runtime was reset for reuse.
pub fn record_reset(&self) {
self.reset_count.fetch_add(1, AtomicOrdering::Relaxed);
}
/// Number of runtimes created from this pool.
pub fn created_count(&self) -> u64 {
self.created_count.load(AtomicOrdering::Relaxed)
}
/// Number of runtime resets performed.
pub fn reset_count(&self) -> u64 {
self.reset_count.load(AtomicOrdering::Relaxed)
}
}
impl Default for WarmIsolatePool {
fn default() -> Self {
Self::new(PiJsRuntimeConfig::default())
}
}
fn prefix_import_meta_url(module_name: &str, body: &str) -> Vec<u8> {
let url = if module_name.starts_with('/') {
format!("file://{module_name}")
} else if module_name.starts_with("file://") {
module_name.to_string()
} else if module_name.len() > 2
&& module_name.as_bytes()[1] == b':'
&& (module_name.as_bytes()[2] == b'/' || module_name.as_bytes()[2] == b'\\')
{
// Windows absolute path: `C:/Users/...` or `C:\Users\...`
format!("file:///{module_name}")
} else {
format!("pi://{module_name}")
};
let url_literal = serde_json::to_string(&url).unwrap_or_else(|_| "\"\"".to_string());
format!("import.meta.url = {url_literal};\n{body}").into_bytes()
}
fn resolve_module_path(
base: &str,
specifier: &str,
repair_mode: RepairMode,
roots: &[PathBuf],
) -> Option<PathBuf> {
let specifier = specifier.trim();
if specifier.is_empty() {
return None;
}
if let Some(path) = specifier.strip_prefix("file://") {
let resolved = resolve_existing_file(PathBuf::from(path))?;
if !roots.is_empty() {
let canonical = crate::extensions::safe_canonicalize(&resolved);
let allowed = roots.iter().any(|root| {
let canonical_root = crate::extensions::safe_canonicalize(root);
canonical.starts_with(&canonical_root)
});
if !allowed {
tracing::warn!(
event = "pijs.resolve.monotonicity_violation",
resolved = %resolved.display(),
"resolution blocked: file:// path escapes extension root"
);
return None;
}
}
return Some(resolved);
}
let path = if specifier.starts_with('/') {
PathBuf::from(specifier)
} else if specifier.len() > 2
&& specifier.as_bytes()[1] == b':'
&& (specifier.as_bytes()[2] == b'/' || specifier.as_bytes()[2] == b'\\')
{
// Windows absolute path: `C:/Users/...` or `C:\Users\...`
PathBuf::from(specifier)
} else if specifier.starts_with('.') {
let base_path = Path::new(base);
let base_dir = base_path.parent()?;
base_dir.join(specifier)
} else {
return None;
};
// SEC-FIX: Enforce scope monotonicity before checking file existence (bd-k5q5.9.1.3).
// This prevents directory traversal probes from revealing existence of files
// outside the extension root (e.g. `../../../../etc/passwd`).
if !roots.is_empty() {
let canonical = crate::extensions::safe_canonicalize(&path);
let allowed = roots.iter().any(|root| {
let canonical_root = crate::extensions::safe_canonicalize(root);
canonical.starts_with(&canonical_root)
});
if !allowed {
return None;
}
}
if let Some(resolved) = resolve_existing_module_candidate(path.clone()) {
// SEC-FIX: Enforce scope monotonicity on the *resolved* path (bd-k5q5.9.1.3).
// This handles cases where `resolve_existing_module_candidate` finds a file
// (e.g. .ts sibling) that is a symlink escaping the root, even if the base path was safe.
if !roots.is_empty() {
let canonical_resolved = crate::extensions::safe_canonicalize(&resolved);
let allowed = roots.iter().any(|root| {
let canonical_root = crate::extensions::safe_canonicalize(root);
canonical_resolved.starts_with(&canonical_root)
});
if !allowed {
tracing::warn!(
event = "pijs.resolve.monotonicity_violation",
original = %path.display(),
resolved = %resolved.display(),
"resolution blocked: resolved path escapes extension root"
);
return None;
}
}
return Some(resolved);
}
// Pattern 1 (bd-k5q5.8.2): dist/ → src/ fallback for missing build artifacts.
// Gated by repair_mode (bd-k5q5.9.1.2): only static-analysis operations
// (path existence checks) happen here — broken code is never executed.
if repair_mode.should_apply() {
try_dist_to_src_fallback(&path)
} else {
if repair_mode == RepairMode::Suggest {
// Log what would have been repaired without applying it.
if let Some(resolved) = try_dist_to_src_fallback(&path) {
tracing::info!(
event = "pijs.repair.suggest",
pattern = "dist_to_src",
original = %path.display(),
resolved = %resolved.display(),
"repair suggestion: would resolve dist/ → src/ (mode=suggest)"
);
}
}
None
}
}
/// Auto-repair Pattern 1: when a module path contains `/dist/` and the file
/// does not exist, try the equivalent path under `/src/` with `.ts`/`.tsx`
/// extensions. This handles the common case where an npm-published extension
/// references compiled output that was never built.
fn try_dist_to_src_fallback(path: &Path) -> Option<PathBuf> {
let path_str = path.to_string_lossy();
let idx = path_str.find("/dist/")?;
// The extension root is the directory containing /dist/.
let extension_root = PathBuf::from(&path_str[..idx]);
let src_path = format!("{}/src/{}", &path_str[..idx], &path_str[idx + 6..]);
let candidate = PathBuf::from(&src_path);
if let Some(resolved) = resolve_existing_module_candidate(candidate) {
// Privilege monotonicity check (bd-k5q5.9.1.3): ensure the
// resolved path stays within the extension root.
let verdict = verify_repair_monotonicity(&extension_root, path, &resolved);
if !verdict.is_safe() {
tracing::warn!(
event = "pijs.repair.monotonicity_violation",
original = %path_str,
resolved = %resolved.display(),
verdict = ?verdict,
"repair blocked: resolved path escapes extension root"
);
return None;
}
// Structural validation gate (bd-k5q5.9.5.1): verify the
// resolved file is parseable before accepting the repair.
let structural = validate_repaired_artifact(&resolved);
if !structural.is_valid() {
tracing::warn!(
event = "pijs.repair.structural_validation_failed",
original = %path_str,
resolved = %resolved.display(),
verdict = %structural,
"repair blocked: resolved artifact failed structural validation"
);
return None;
}
tracing::info!(
event = "pijs.repair.dist_to_src",
original = %path_str,
resolved = %resolved.display(),
"auto-repair: resolved dist/ → src/ fallback"
);
return Some(resolved);
}
None
}
fn resolve_existing_file(path: PathBuf) -> Option<PathBuf> {
if path.is_file() {
return Some(path);
}
None
}
fn resolve_existing_module_candidate(path: PathBuf) -> Option<PathBuf> {
if path.is_file() {
return Some(path);
}
if path.is_dir() {
for candidate in [
"index.ts",
"index.tsx",
"index.js",
"index.mjs",
"index.json",
] {
let full = path.join(candidate);
if full.is_file() {
return Some(full);
}
}
return None;
}
let extension = path.extension().and_then(|ext| ext.to_str());
match extension {
Some("js" | "mjs") => {
for ext in ["ts", "tsx"] {
let fallback = path.with_extension(ext);
if fallback.is_file() {
return Some(fallback);
}
}
}
None => {
for ext in ["ts", "tsx", "js", "mjs", "json"] {
let candidate = path.with_extension(ext);
if candidate.is_file() {
return Some(candidate);
}
}
}
_ => {}
}
None
}
// ─── Pattern 3 (bd-k5q5.8.4): Monorepo Sibling Module Stubs ─────────────────
/// Regex that captures named imports from an ESM import statement:
/// `import { a, b, type C } from "specifier"`
///
/// Group 1: the names inside braces.
static IMPORT_NAMES_RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
fn import_names_regex() -> &'static regex::Regex {
IMPORT_NAMES_RE.get_or_init(|| {
regex::Regex::new(r#"(?ms)import\s+(?:[^{};]*?,\s*)?\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]"#)
.expect("import names regex")
})
}
/// Regex for CJS destructured require:
/// `const { a, b } = require("specifier")`
static REQUIRE_DESTRUCTURE_RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
fn require_destructure_regex() -> &'static regex::Regex {
REQUIRE_DESTRUCTURE_RE.get_or_init(|| {
regex::Regex::new(
r#"(?m)(?:const|let|var)\s*\{([^}]+)\}\s*=\s*require\s*\(\s*['"]([^'"]+)['"]"#,
)
.expect("require destructure regex")
})
}
/// Detect if a relative specifier resolves to a path outside all known
/// extension roots. Returns the resolved absolute path if it's an escape.
fn detect_monorepo_escape(
base: &str,
specifier: &str,
extension_roots: &[PathBuf],
) -> Option<PathBuf> {
if !specifier.starts_with('.') {
return None;
}
let base_dir = Path::new(base).parent()?;
let resolved = base_dir.join(specifier);
// Canonicalize as much as possible — if the exact path doesn't exist,
// try the parent directory.
let effective = std::fs::canonicalize(&resolved)
.map(crate::extensions::strip_unc_prefix)
.or_else(|_| {
resolved
.parent()
.and_then(|p| {
std::fs::canonicalize(p)
.map(crate::extensions::strip_unc_prefix)
.ok()
})
.map(|p| p.join(resolved.file_name().unwrap_or_default()))
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "no parent"))
})
.unwrap_or_else(|_| resolved.clone());
for root in extension_roots {
if effective.starts_with(root) {
return None; // Within an extension root — not an escape
}
}
Some(resolved)
}
/// Extract the named imports that a source file pulls from a given specifier.
///
/// Handles both ESM `import { x, y } from "spec"` and CJS
/// `const { x, y } = require("spec")`. Type-only imports (`type Foo`)
/// are excluded because TypeScript erases them.
pub fn extract_import_names(source: &str, specifier: &str) -> Vec<String> {
let mut names = Vec::new();
let re_esm = import_names_regex();
let re_cjs = require_destructure_regex();
for cap in re_esm.captures_iter(source) {
let spec_in_source = &cap[2];
if spec_in_source != specifier {
continue;
}
parse_import_list(&cap[1], &mut names);
}
for cap in re_cjs.captures_iter(source) {
let spec_in_source = &cap[2];
if spec_in_source != specifier {
continue;
}
parse_import_list(&cap[1], &mut names);
}
names.sort();
names.dedup();
names
}
/// Parse a comma-separated list of import names, skipping `type`-only imports.
fn parse_import_list(raw: &str, out: &mut Vec<String>) {
for token in raw.split(',') {
let token = token.trim();
if token.is_empty() {
continue;
}
// Skip `type Foo` (TypeScript type-only import)
if token.starts_with("type ") || token.starts_with("type\t") {
continue;
}
// Handle `X as Y` — we export the original name `X`.
let name = token.split_whitespace().next().unwrap_or(token).trim();
if !name.is_empty() {
out.push(name.to_string());
}
}
}
/// Generate a synthetic ESM stub module that exports no-op values for each
/// requested name. Uses simple heuristics to choose the export shape:
///
/// - Names starting with `is`/`has`/`check` → `() => false`
/// - Names starting with `get`/`detect`/`find`/`create`/`make` → `() => ({})`
/// - Names starting with `set`/`play`/`send`/`run`/`do`/`emit` → `() => {}`
/// - `ALL_CAPS` names → `[]` (constants are often arrays)
/// - Names starting with uppercase → `class Name {}` (likely class/type)
/// - Everything else → `() => {}`
pub fn generate_monorepo_stub(names: &[String]) -> String {
let mut lines = Vec::with_capacity(names.len() + 1);
lines.push("// Auto-generated monorepo escape stub (Pattern 3)".to_string());
for name in names {
if !is_valid_js_export_name(name) {
continue;
}
let export = if name == "default" {
"export default () => {};".to_string()
} else if name.chars().all(|c| c.is_ascii_uppercase() || c == '_') && !name.is_empty() {
// ALL_CAPS constant
format!("export const {name} = [];")
} else if name.starts_with("is") || name.starts_with("has") || name.starts_with("check") {
format!("export const {name} = () => false;")
} else if name.starts_with("get")
|| name.starts_with("detect")
|| name.starts_with("find")
|| name.starts_with("create")
|| name.starts_with("make")
{
format!("export const {name} = () => ({{}});")
} else if name.chars().next().is_some_and(|c| c.is_ascii_uppercase()) {
// Likely a class or type — export as class
format!("export class {name} {{}}")
} else {
// Generic function stub
format!("export const {name} = () => {{}};")
};
lines.push(export);
}
lines.join("\n")
}
fn source_declares_binding(source: &str, name: &str) -> bool {
let patterns = [
format!("const {name}"),
format!("let {name}"),
format!("var {name}"),
format!("function {name}"),
format!("class {name}"),
format!("export const {name}"),
format!("export let {name}"),
format!("export var {name}"),
format!("export function {name}"),
format!("export class {name}"),
];
source.lines().any(|line| {
let trimmed = line.trim_start();
if trimmed.starts_with("//") || trimmed.starts_with('*') {
return false;
}
patterns.iter().any(|pattern| trimmed.starts_with(pattern))
})
}
/// Extract static `require("specifier")` calls from JavaScript source.
///
/// This scanner is intentionally lexical: it ignores matches inside comments
/// and string/template literals so code-generation strings like
/// `` `require("pkg/path").default` `` do not become false-positive imports.
#[allow(clippy::too_many_lines)]
fn extract_static_require_specifiers(source: &str) -> Vec<String> {
const REQUIRE: &[u8] = b"require";
let bytes = source.as_bytes();
let mut out = Vec::new();
let mut seen = HashSet::new();
let mut i = 0usize;
let mut in_line_comment = false;
let mut in_block_comment = false;
let mut in_single = false;
let mut in_double = false;
let mut in_template = false;
let mut escaped = false;
while i < bytes.len() {
let b = bytes[i];
if in_line_comment {
if b == b'\n' {
in_line_comment = false;
}
i += 1;
continue;
}
if in_block_comment {
if b == b'*' && i + 1 < bytes.len() && bytes[i + 1] == b'/' {
in_block_comment = false;
i += 2;
} else {
i += 1;
}
continue;
}
if in_single {
if escaped {
escaped = false;
} else if b == b'\\' {
escaped = true;
} else if b == b'\'' {
in_single = false;
}
i += 1;
continue;
}
if in_double {
if escaped {
escaped = false;
} else if b == b'\\' {
escaped = true;
} else if b == b'"' {
in_double = false;
}
i += 1;
continue;
}
if in_template {
if escaped {
escaped = false;
} else if b == b'\\' {
escaped = true;
} else if b == b'`' {
in_template = false;
}
i += 1;
continue;
}
if b == b'/' && i + 1 < bytes.len() {
match bytes[i + 1] {
b'/' => {
in_line_comment = true;
i += 2;
continue;
}
b'*' => {
in_block_comment = true;
i += 2;
continue;
}
_ => {}
}
}
if b == b'\'' {
in_single = true;
i += 1;
continue;
}
if b == b'"' {
in_double = true;
i += 1;
continue;
}
if b == b'`' {
in_template = true;
i += 1;
continue;
}
if i + REQUIRE.len() <= bytes.len() && &bytes[i..i + REQUIRE.len()] == REQUIRE {
let has_ident_before = i > 0 && is_js_ident_continue(bytes[i - 1]);
let after_ident_idx = i + REQUIRE.len();
let has_ident_after =
after_ident_idx < bytes.len() && is_js_ident_continue(bytes[after_ident_idx]);
if has_ident_before || has_ident_after {
i += 1;
continue;
}
let mut j = after_ident_idx;
while j < bytes.len() && bytes[j].is_ascii_whitespace() {
j += 1;
}
if j >= bytes.len() || bytes[j] != b'(' {
i += 1;
continue;
}
j += 1;
while j < bytes.len() && bytes[j].is_ascii_whitespace() {
j += 1;
}
if j >= bytes.len() || (bytes[j] != b'"' && bytes[j] != b'\'') {
i += 1;
continue;
}
let quote = bytes[j];
let spec_start = j + 1;
j += 1;
let mut lit_escaped = false;
while j < bytes.len() {
let c = bytes[j];
if lit_escaped {
lit_escaped = false;
j += 1;
continue;
}
if c == b'\\' {
lit_escaped = true;
j += 1;
continue;
}
if c == quote {
break;
}
j += 1;
}
if j >= bytes.len() {
break;
}
let spec = &source[spec_start..j];
j += 1;
while j < bytes.len() && bytes[j].is_ascii_whitespace() {
j += 1;
}
if j < bytes.len() && bytes[j] == b')' && seen.insert(spec.to_string()) {
out.push(spec.to_string());
i = j + 1;
continue;
}
}
i += 1;
}
out
}
/// Detect if a JavaScript source uses CommonJS patterns (`require(...)` or
/// `module.exports`) and transform it into an ESM-compatible wrapper.
///
/// Handles two cases:
/// 1. **Pure CJS** (no ESM `import`/`export`): full wrapper with
/// `module`/`exports`/`require` shim + `export default module.exports`
/// 2. **Mixed** (ESM imports + `require()` calls): inject `import` statements
/// for require targets and a `require()` function, preserving existing ESM
#[allow(clippy::too_many_lines)]
fn maybe_cjs_to_esm(source: &str) -> String {
let has_require = source.contains("require(");
let has_module_exports = source.contains("module.exports")
|| source.contains("module[\"exports\"]")
|| source.contains("module['exports']");
let has_exports_usage = source.contains("exports.") || source.contains("exports[");
let has_dirname_refs = source.contains("__dirname") || source.contains("__filename");
if !has_require && !has_module_exports && !has_exports_usage && !has_dirname_refs {
return source.to_string();
}
let has_esm = source.lines().any(|line| {
let trimmed = line.trim();
(trimmed.starts_with("import ") || trimmed.starts_with("export "))
&& !trimmed.starts_with("//")
});
let has_export_default = source.contains("export default");
// Extract all require() specifiers
let specifiers = extract_static_require_specifiers(source);
if specifiers.is_empty() && !has_module_exports && !has_exports_usage && !has_dirname_refs {
return source.to_string();
}
if specifiers.is_empty()
&& has_esm
&& !has_module_exports
&& !has_exports_usage
&& !has_dirname_refs
{
return source.to_string();
}
let mut output = String::with_capacity(source.len() + 512);
// Generate ESM imports for require targets
for (i, spec) in specifiers.iter().enumerate() {
let _ = writeln!(output, "import * as __cjs_req_{i} from {spec:?};");
}
// Build require map + function
let has_require_binding = source_declares_binding(source, "require");
if !specifiers.is_empty() && !has_require_binding {
output.push_str("const __cjs_req_map = {");
for (i, spec) in specifiers.iter().enumerate() {
if i > 0 {
output.push(',');
}
let _ = write!(output, "\n {spec:?}: __cjs_req_{i}");
}
output.push_str("\n};\n");
output.push_str(
"function require(s) {\n\
\x20 const m = __cjs_req_map[s];\n\
\x20 if (!m) throw new Error('Cannot find module: ' + s);\n\
\x20 return m.default !== undefined && typeof m.default === 'object' \
? m.default : m;\n\
}\n",
);
}
let has_filename_binding = source_declares_binding(source, "__filename");
let has_dirname_binding = source_declares_binding(source, "__dirname");
let has_module_binding = source_declares_binding(source, "module");
let has_exports_binding = source_declares_binding(source, "exports");
let needs_filename = has_dirname_refs && !has_filename_binding;
let needs_dirname = has_dirname_refs && !has_dirname_binding;
let needs_module = (has_module_exports || has_exports_usage) && !has_module_binding;
let needs_exports = (has_module_exports || has_exports_usage) && !has_exports_binding;
if needs_filename || needs_dirname || needs_module || needs_exports {
// Provide CJS compatibility globals only for bindings not declared by source.
if needs_filename {
output.push_str(
"const __filename = (() => {\n\
\x20 try { return new URL(import.meta.url).pathname || ''; } catch { return ''; }\n\
})();\n",
);
}
if needs_dirname {
output.push_str(
"const __dirname = __filename ? __filename.replace(/[/\\\\][^/\\\\]*$/, '') : '.';\n",
);
}
if needs_module {
output.push_str("const module = { exports: {} };\n");
}
if needs_exports {
output.push_str("const exports = module.exports;\n");
}
}
output.push_str(source);
output.push('\n');
if !has_export_default && (!has_esm || has_module_exports || has_exports_usage) {
// Export CommonJS entrypoint for loaders that require a default init fn.
output.push_str("export default module.exports;\n");
}
output
}
const fn is_js_ident_start(byte: u8) -> bool {
(byte as char).is_ascii_alphabetic() || byte == b'_' || byte == b'$'
}
const fn is_js_ident_continue(byte: u8) -> bool {
is_js_ident_start(byte) || (byte as char).is_ascii_digit()
}
/// Rewrite private identifier syntax (`#field`) to legacy-safe identifiers for
/// runtimes that do not parse private fields. This is intentionally a lexical
/// compatibility transform, not a semantic class-fields implementation.
#[allow(clippy::too_many_lines)]
fn rewrite_legacy_private_identifiers(source: &str) -> String {
if !source.contains('#') || !source.is_ascii() {
return source.to_string();
}
let bytes = source.as_bytes();
let mut out = String::with_capacity(source.len() + 32);
let mut i = 0usize;
let mut in_single = false;
let mut in_double = false;
let mut in_template = false;
let mut escaped = false;
let mut line_comment = false;
let mut block_comment = false;
while i < bytes.len() {
let b = bytes[i];
let next = bytes.get(i + 1).copied();
if line_comment {
out.push(b as char);
if b == b'\n' {
line_comment = false;
}
i += 1;
continue;
}
if block_comment {
if b == b'*' && next == Some(b'/') {
out.push('*');
out.push('/');
i += 2;
block_comment = false;
continue;
}
out.push(b as char);
i += 1;
continue;
}
if in_single {
out.push(b as char);
if escaped {
escaped = false;
} else if b == b'\\' {
escaped = true;
} else if b == b'\'' {
in_single = false;
}
i += 1;
continue;
}
if in_double {
out.push(b as char);
if escaped {
escaped = false;
} else if b == b'\\' {
escaped = true;
} else if b == b'"' {
in_double = false;
}
i += 1;
continue;
}
if in_template {
out.push(b as char);
if escaped {
escaped = false;
} else if b == b'\\' {
escaped = true;
} else if b == b'`' {
in_template = false;
}
i += 1;
continue;
}
if b == b'/' && next == Some(b'/') {
line_comment = true;
out.push('/');
i += 1;
continue;
}
if b == b'/' && next == Some(b'*') {
block_comment = true;
out.push('/');
i += 1;
continue;
}
if b == b'\'' {
in_single = true;
out.push('\'');
i += 1;
continue;
}
if b == b'"' {
in_double = true;
out.push('"');
i += 1;
continue;
}
if b == b'`' {
in_template = true;
out.push('`');
i += 1;
continue;
}
if b == b'#' && next.is_some_and(is_js_ident_start) {
let prev_is_ident = i > 0 && is_js_ident_continue(bytes[i - 1]);
if !prev_is_ident {
out.push_str("__pijs_private_");
i += 1;
while i < bytes.len() && is_js_ident_continue(bytes[i]) {
out.push(bytes[i] as char);
i += 1;
}
continue;
}
}
out.push(b as char);
i += 1;
}
out
}
fn json_module_to_esm(raw: &str, name: &str) -> std::result::Result<String, String> {
let value: serde_json::Value =
serde_json::from_str(raw).map_err(|err| format!("parse {name}: {err}"))?;
let literal = serde_json::to_string(&value).map_err(|err| format!("encode {name}: {err}"))?;
Ok(format!("export default {literal};\n"))
}
fn transpile_typescript_module(source: &str, name: &str) -> std::result::Result<String, String> {
let globals = Globals::new();
GLOBALS.set(&globals, || {
let cm: Lrc<SourceMap> = Lrc::default();
let fm = cm.new_source_file(
FileName::Custom(name.to_string()).into(),
source.to_string(),
);
let syntax = Syntax::Typescript(TsSyntax {
tsx: Path::new(name)
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("tsx")),
decorators: true,
..Default::default()
});
let mut parser = SwcParser::new(syntax, StringInput::from(&*fm), None);
let module: SwcModule = parser
.parse_module()
.map_err(|err| format!("parse {name}: {err:?}"))?;
let unresolved_mark = Mark::new();
let top_level_mark = Mark::new();
let mut program = SwcProgram::Module(module);
{
let mut pass = resolver(unresolved_mark, top_level_mark, false);
pass.process(&mut program);
}
{
let mut pass = strip(unresolved_mark, top_level_mark);
pass.process(&mut program);
}
let SwcProgram::Module(module) = program else {
return Err(format!("transpile {name}: expected module"));
};
let mut buf = Vec::new();
{
let mut emitter = Emitter {
cfg: swc_ecma_codegen::Config::default(),
comments: None,
cm: cm.clone(),
wr: JsWriter::new(cm, "\n", &mut buf, None),
};
emitter
.emit_module(&module)
.map_err(|err| format!("emit {name}: {err}"))?;
}
String::from_utf8(buf).map_err(|err| format!("utf8 {name}: {err}"))
})
}
/// Build the `node:os` virtual module with real system values injected at init
/// time. Values are captured once and cached in the JS module source, so no
/// per-call hostcalls are needed.
#[allow(clippy::too_many_lines)]
fn build_node_os_module() -> String {
// Map Rust target constants to Node.js conventions.
let node_platform = match std::env::consts::OS {
"macos" => "darwin",
"windows" => "win32",
other => other, // "linux", "freebsd", etc.
};
let node_arch = match std::env::consts::ARCH {
"x86_64" => "x64",
"aarch64" => "arm64",
"x86" => "ia32",
"arm" => "arm",
other => other,
};
let node_type = match std::env::consts::OS {
"linux" => "Linux",
"macos" => "Darwin",
"windows" => "Windows_NT",
other => other,
};
// Escape backslashes for safe JS string interpolation (Windows paths).
let tmpdir = std::env::temp_dir()
.display()
.to_string()
.replace('\\', "\\\\");
let homedir = std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.unwrap_or_else(|_| "/home/unknown".to_string())
.replace('\\', "\\\\");
// Read hostname from /etc/hostname (Linux) or fall back to env/default.
let hostname = std::fs::read_to_string("/etc/hostname")
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.or_else(|| std::env::var("HOSTNAME").ok())
.or_else(|| std::env::var("COMPUTERNAME").ok())
.unwrap_or_else(|| "localhost".to_string());
let num_cpus = std::thread::available_parallelism().map_or(1, std::num::NonZero::get);
let eol = if cfg!(windows) { "\\r\\n" } else { "\\n" };
let dev_null = if cfg!(windows) {
"\\\\\\\\.\\\\NUL"
} else {
"/dev/null"
};
let username = std::env::var("USER")
.or_else(|_| std::env::var("USERNAME"))
.unwrap_or_else(|_| "unknown".to_string());
let shell = std::env::var("SHELL").unwrap_or_else(|_| {
if cfg!(windows) {
"cmd.exe".to_string()
} else {
"/bin/sh".to_string()
}
});
// Read uid/gid from /proc/self/status on Linux, fall back to defaults.
let (uid, gid) = read_proc_uid_gid().unwrap_or((1000, 1000));
// Store CPU count; the JS module builds the array at import time.
// This avoids emitting potentially thousands of chars of identical entries.
format!(
r#"
const _platform = "{node_platform}";
const _arch = "{node_arch}";
const _type = "{node_type}";
const _tmpdir = "{tmpdir}";
const _homedir = "{homedir}";
const _hostname = "{hostname}";
const _eol = "{eol}";
const _devNull = "{dev_null}";
const _uid = {uid};
const _gid = {gid};
const _username = "{username}";
const _shell = "{shell}";
const _numCpus = {num_cpus};
const _cpus = [];
for (let i = 0; i < _numCpus; i++) _cpus.push({{ model: "cpu", speed: 2400, times: {{ user: 0, nice: 0, sys: 0, idle: 0, irq: 0 }} }});
export function homedir() {{
const env_home =
globalThis.pi && globalThis.pi.env && typeof globalThis.pi.env.get === "function"
? globalThis.pi.env.get("HOME")
: undefined;
return env_home || _homedir;
}}
export function tmpdir() {{ return _tmpdir; }}
export function hostname() {{ return _hostname; }}
export function platform() {{ return _platform; }}
export function arch() {{ return _arch; }}
export function type() {{ return _type; }}
export function release() {{ return "6.0.0"; }}
export function cpus() {{ return _cpus; }}
export function totalmem() {{ return 8 * 1024 * 1024 * 1024; }}
export function freemem() {{ return 4 * 1024 * 1024 * 1024; }}
export function uptime() {{ return Math.floor(Date.now() / 1000); }}
export function loadavg() {{ return [0.0, 0.0, 0.0]; }}
export function networkInterfaces() {{ return {{}}; }}
export function userInfo(_options) {{
return {{
uid: _uid,
gid: _gid,
username: _username,
homedir: homedir(),
shell: _shell,
}};
}}
export function endianness() {{ return "LE"; }}
export const EOL = _eol;
export const devNull = _devNull;
export const constants = {{
signals: {{}},
errno: {{}},
priority: {{ PRIORITY_LOW: 19, PRIORITY_BELOW_NORMAL: 10, PRIORITY_NORMAL: 0, PRIORITY_ABOVE_NORMAL: -7, PRIORITY_HIGH: -14, PRIORITY_HIGHEST: -20 }},
}};
export default {{ homedir, tmpdir, hostname, platform, arch, type, release, cpus, totalmem, freemem, uptime, loadavg, networkInterfaces, userInfo, endianness, EOL, devNull, constants }};
"#
)
.trim()
.to_string()
}
/// Parse uid/gid from `/proc/self/status` (Linux). Returns `None` on other
/// platforms or if the file is unreadable.
fn read_proc_uid_gid() -> Option<(u32, u32)> {
let status = std::fs::read_to_string("/proc/self/status").ok()?;
let mut uid = None;
let mut gid = None;
for line in status.lines() {
if let Some(rest) = line.strip_prefix("Uid:") {
uid = rest.split_whitespace().next().and_then(|v| v.parse().ok());
} else if let Some(rest) = line.strip_prefix("Gid:") {
gid = rest.split_whitespace().next().and_then(|v| v.parse().ok());
}
if uid.is_some() && gid.is_some() {
break;
}
}
Some((uid?, gid?))
}
#[allow(clippy::too_many_lines)]
fn default_virtual_modules() -> HashMap<String, String> {
let mut modules = HashMap::new();
modules.insert(
"@sinclair/typebox".to_string(),
r#"
export const Type = {
String: (opts = {}) => ({ type: "string", ...opts }),
Number: (opts = {}) => ({ type: "number", ...opts }),
Boolean: (opts = {}) => ({ type: "boolean", ...opts }),
Array: (items, opts = {}) => ({ type: "array", items, ...opts }),
Object: (props = {}, opts = {}) => {
const required = [];
const properties = {};
for (const [k, v] of Object.entries(props)) {
if (v && typeof v === "object" && v.__pi_optional) {
properties[k] = v.schema;
} else {
properties[k] = v;
required.push(k);
}
}
const out = { type: "object", properties, ...opts };
if (required.length) out.required = required;
return out;
},
Optional: (schema) => ({ __pi_optional: true, schema }),
Literal: (value, opts = {}) => ({ const: value, ...opts }),
Any: (opts = {}) => ({ ...opts }),
Union: (schemas, opts = {}) => ({ anyOf: schemas, ...opts }),
Enum: (values, opts = {}) => ({ enum: values, ...opts }),
Integer: (opts = {}) => ({ type: "integer", ...opts }),
Null: (opts = {}) => ({ type: "null", ...opts }),
Unknown: (opts = {}) => ({ ...opts }),
Tuple: (items, opts = {}) => ({ type: "array", items, minItems: items.length, maxItems: items.length, ...opts }),
Record: (keySchema, valueSchema, opts = {}) => ({ type: "object", additionalProperties: valueSchema, ...opts }),
Ref: (ref, opts = {}) => ({ $ref: ref, ...opts }),
Intersect: (schemas, opts = {}) => ({ allOf: schemas, ...opts }),
};
export default { Type };
"#
.trim()
.to_string(),
);
modules.insert(
"@mariozechner/pi-ai".to_string(),
r#"
export function StringEnum(values, opts = {}) {
const list = Array.isArray(values) ? values.map((v) => String(v)) : [];
return { type: "string", enum: list, ...opts };
}
export function calculateCost() {}
export function createAssistantMessageEventStream() {
return {
push: () => {},
end: () => {},
};
}
export function streamSimpleAnthropic() {
throw new Error("@mariozechner/pi-ai.streamSimpleAnthropic is not available in PiJS");
}
export function streamSimpleOpenAIResponses() {
throw new Error("@mariozechner/pi-ai.streamSimpleOpenAIResponses is not available in PiJS");
}
export async function complete(_model, _messages, _opts = {}) {
// Return a minimal completion response stub
return { content: "", model: _model ?? "unknown", usage: { input_tokens: 0, output_tokens: 0 } };
}
// Stub: completeSimple returns a simple text completion without streaming
export async function completeSimple(_model, _prompt, _opts = {}) {
// Return an empty string completion
return "";
}
export function getModel() {
// Return a default model identifier
return "claude-sonnet-4-5";
}
export function getApiProvider() {
// Return a default provider identifier
return "anthropic";
}
export function getModels() {
// Return a list of available model identifiers
return ["claude-sonnet-4-5", "claude-haiku-3-5"];
}
export async function loginOpenAICodex(_opts = {}) {
return { accessToken: "", refreshToken: "", expiresAt: Date.now() + 3600000 };
}
export async function refreshOpenAICodexToken(_refreshToken) {
return { accessToken: "", refreshToken: "", expiresAt: Date.now() + 3600000 };
}
export default { StringEnum, calculateCost, createAssistantMessageEventStream, streamSimpleAnthropic, streamSimpleOpenAIResponses, complete, completeSimple, getModel, getApiProvider, getModels, loginOpenAICodex, refreshOpenAICodexToken };
"#
.trim()
.to_string(),
);
modules.insert(
"@mariozechner/pi-tui".to_string(),
r#"
export function matchesKey(_data, _key) {
return false;
}
export function truncateToWidth(text, width) {
const s = String(text ?? "");
const w = Number(width ?? 0);
if (!w || w <= 0) return "";
return s.length <= w ? s : s.slice(0, w);
}
export class Text {
constructor(text, x = 0, y = 0) {
this.text = String(text ?? "");
this.x = x;
this.y = y;
}
}
export class TruncatedText extends Text {
constructor(text, width = 80, x = 0, y = 0) {
super(text, x, y);
this.width = Number(width ?? 80);
}
}
export class Container {
constructor(..._args) {}
}
export class Markdown {
constructor(..._args) {}
}
export class Spacer {
constructor(..._args) {}
}
export function visibleWidth(str) {
return String(str ?? "").length;
}
export function wrapTextWithAnsi(text, _width) {
return String(text ?? "");
}
export class Editor {
constructor(_opts = {}) {
this.value = "";
}
}
export const CURSOR_MARKER = "▌";
export function isKeyRelease(_data) {
return false;
}
export function parseKey(key) {
return { key: String(key ?? "") };
}
export class Box {
constructor(_padX = 0, _padY = 0, _styleFn = null) {
this.children = [];
}
addChild(child) {
this.children.push(child);
}
}
export class SelectList {
constructor(items = [], _opts = {}) {
this.items = Array.isArray(items) ? items : [];
this.selected = 0;
}
setItems(items) {
this.items = Array.isArray(items) ? items : [];
}
select(index) {
const i = Number(index ?? 0);
this.selected = Number.isFinite(i) ? i : 0;
}
}
export class Input {
constructor(_opts = {}) {
this.value = "";
}
}
export const Key = {
// Special keys
escape: "escape",
esc: "esc",
enter: "enter",
tab: "tab",
space: "space",
backspace: "backspace",
delete: "delete",
home: "home",
end: "end",
pageUp: "pageUp",
pageDown: "pageDown",
up: "up",
down: "down",
left: "left",
right: "right",
// Single modifiers
ctrl: (key) => `ctrl+${key}`,
shift: (key) => `shift+${key}`,
alt: (key) => `alt+${key}`,
// Combined modifiers
ctrlShift: (key) => `ctrl+shift+${key}`,
shiftCtrl: (key) => `shift+ctrl+${key}`,
ctrlAlt: (key) => `ctrl+alt+${key}`,
altCtrl: (key) => `alt+ctrl+${key}`,
shiftAlt: (key) => `shift+alt+${key}`,
altShift: (key) => `alt+shift+${key}`,
ctrlAltShift: (key) => `ctrl+alt+shift+${key}`,
};
export class DynamicBorder {
constructor(_styleFn = null) {
this.styleFn = _styleFn;
}
}
export class SettingsList {
constructor(_opts = {}) {
this.items = [];
}
setItems(items) {
this.items = Array.isArray(items) ? items : [];
}
}
// Fuzzy string matching for filtering lists
export function fuzzyMatch(query, text, _opts = {}) {
const q = String(query ?? '').toLowerCase();
const t = String(text ?? '').toLowerCase();
if (!q) return { match: true, score: 0, positions: [] };
if (!t) return { match: false, score: 0, positions: [] };
const positions = [];
let qi = 0;
for (let ti = 0; ti < t.length && qi < q.length; ti++) {
if (t[ti] === q[qi]) {
positions.push(ti);
qi++;
}
}
const match = qi === q.length;
const score = match ? (q.length / t.length) * 100 : 0;
return { match, score, positions };
}
// Get editor keybindings configuration
export function getEditorKeybindings() {
return {
save: 'ctrl+s',
quit: 'ctrl+q',
copy: 'ctrl+c',
paste: 'ctrl+v',
undo: 'ctrl+z',
redo: 'ctrl+y',
find: 'ctrl+f',
replace: 'ctrl+h',
};
}
// Filter an array of items using fuzzy matching
export function fuzzyFilter(query, items, _opts = {}) {
const q = String(query ?? '').toLowerCase();
if (!q) return items;
if (!Array.isArray(items)) return [];
return items.filter(item => {
const text = typeof item === 'string' ? item : String(item?.label ?? item?.name ?? item);
return fuzzyMatch(q, text).match;
});
}
// Cancellable loader widget - shows loading state with optional cancel
export class CancellableLoader {
constructor(message = 'Loading...', opts = {}) {
this.message = String(message ?? 'Loading...');
this.cancelled = false;
this.onCancel = opts.onCancel ?? null;
}
cancel() {
this.cancelled = true;
if (typeof this.onCancel === 'function') {
this.onCancel();
}
}
render() {
return this.cancelled ? [] : [this.message];
}
}
export class Image {
constructor(src, _opts = {}) {
this.src = String(src ?? "");
this.width = 0;
this.height = 0;
}
}
export default { matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi, Text, TruncatedText, Container, Markdown, Spacer, Editor, Box, SelectList, Input, Image, CURSOR_MARKER, isKeyRelease, parseKey, Key, DynamicBorder, SettingsList, fuzzyMatch, getEditorKeybindings, fuzzyFilter, CancellableLoader };
"#
.trim()
.to_string(),
);
modules.insert(
"@mariozechner/pi-coding-agent".to_string(),
r#"
export const VERSION = "0.0.0";
export const DEFAULT_MAX_LINES = 2000;
export const DEFAULT_MAX_BYTES = 50 * 1024;
export function formatSize(bytes) {
const b = Number(bytes ?? 0);
const KB = 1024;
const MB = 1024 * 1024;
if (b >= MB) return `${(b / MB).toFixed(1)}MB`;
if (b >= KB) return `${(b / KB).toFixed(1)}KB`;
return `${Math.trunc(b)}B`;
}
function jsBytes(value) {
return String(value ?? "").length;
}
export function truncateHead(text, opts = {}) {
const raw = String(text ?? "");
const maxLines = Number(opts.maxLines ?? DEFAULT_MAX_LINES);
const maxBytes = Number(opts.maxBytes ?? DEFAULT_MAX_BYTES);
const lines = raw.split("\n");
const totalLines = lines.length;
const totalBytes = jsBytes(raw);
const out = [];
let outBytes = 0;
let truncatedBy = null;
for (const line of lines) {
if (out.length >= maxLines) {
truncatedBy = "lines";
break;
}
const candidate = out.length ? `\n${line}` : line;
const candidateBytes = jsBytes(candidate);
if (outBytes + candidateBytes > maxBytes) {
truncatedBy = "bytes";
break;
}
out.push(line);
outBytes += candidateBytes;
}
const content = out.join("\n");
return {
content,
truncated: truncatedBy != null,
truncatedBy,
totalLines,
totalBytes,
outputLines: out.length,
outputBytes: jsBytes(content),
lastLinePartial: false,
firstLineExceedsLimit: false,
maxLines,
maxBytes,
};
}
export function truncateTail(text, opts = {}) {
const raw = String(text ?? "");
const maxLines = Number(opts.maxLines ?? DEFAULT_MAX_LINES);
const maxBytes = Number(opts.maxBytes ?? DEFAULT_MAX_BYTES);
const lines = raw.split("\n");
const totalLines = lines.length;
const totalBytes = jsBytes(raw);
const out = [];
let outBytes = 0;
let truncatedBy = null;
for (let i = lines.length - 1; i >= 0; i--) {
if (out.length >= maxLines) {
truncatedBy = "lines";
break;
}
const line = lines[i];
const candidate = out.length ? `${line}\n` : line;
const candidateBytes = jsBytes(candidate);
if (outBytes + candidateBytes > maxBytes) {
truncatedBy = "bytes";
break;
}
out.unshift(line);
outBytes += candidateBytes;
}
const content = out.join("\n");
return {
content,
truncated: truncatedBy != null,
truncatedBy,
totalLines,
totalBytes,
outputLines: out.length,
outputBytes: jsBytes(content),
lastLinePartial: false,
firstLineExceedsLimit: false,
maxLines,
maxBytes,
};
}
export function parseSessionEntries(text) {
const raw = String(text ?? "");
const out = [];
for (const line of raw.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed) continue;
try {
out.push(JSON.parse(trimmed));
} catch {
// ignore malformed lines
}
}
return out;
}
export function convertToLlm(entries) {
return entries;
}
export function serializeConversation(entries) {
try {
return JSON.stringify(entries ?? []);
} catch {
return String(entries ?? "");
}
}
export function parseFrontmatter(text) {
const raw = String(text ?? "");
if (!raw.startsWith("---")) return { frontmatter: {}, body: raw };
const end = raw.indexOf("\n---", 3);
if (end === -1) return { frontmatter: {}, body: raw };
const header = raw.slice(3, end).trim();
const body = raw.slice(end + 4).replace(/^\n/, "");
const frontmatter = {};
for (const line of header.split(/\r?\n/)) {
const idx = line.indexOf(":");
if (idx === -1) continue;
const key = line.slice(0, idx).trim();
const val = line.slice(idx + 1).trim();
if (!key) continue;
frontmatter[key] = val;
}
return { frontmatter, body };
}
export function getMarkdownTheme() {
return {};
}
export function getSettingsListTheme() {
return {};
}
export function getSelectListTheme() {
return {};
}
export class DynamicBorder {
constructor(..._args) {}
}
export class BorderedLoader {
constructor(..._args) {}
}
export class CustomEditor {
constructor(_opts = {}) {
this.value = "";
}
handleInput(_data) {}
render(_width) {
return [];
}
}
export function createBashTool(_cwd, _opts = {}) {
return {
name: "bash",
label: "bash",
description: "Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last 2000 lines or 50KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.",
parameters: {
type: "object",
properties: {
command: { type: "string", description: "The bash command to execute" },
timeout: { type: "number", description: "Optional timeout in seconds" },
},
required: ["command"],
},
async execute(_id, params) {
return { content: [{ type: "text", text: String(params?.command ?? "") }], details: {} };
},
};
}
export function createReadTool(_cwd, _opts = {}) {
return {
name: "read",
label: "read",
description: "Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to 2000 lines or 50KB (whichever is hit first). Use offset/limit for large files. When you need the full file, continue with offset until complete.",
parameters: {
type: "object",
properties: {
path: { type: "string", description: "The path to the file to read" },
offset: { type: "number", description: "Line offset to start reading from (0-indexed)" },
limit: { type: "number", description: "Maximum number of lines to read" },
},
required: ["path"],
},
async execute(_id, _params) {
return { content: [{ type: "text", text: "" }], details: {} };
},
};
}
export function createLsTool(_cwd, _opts = {}) {
return {
name: "ls",
label: "ls",
description: "List files and directories. Returns names, sizes, and metadata.",
parameters: {
type: "object",
properties: {
path: { type: "string", description: "The path to list" },
},
required: ["path"],
},
async execute(_id, _params) {
return { content: [{ type: "text", text: "" }], details: {} };
},
};
}
export function createGrepTool(_cwd, _opts = {}) {
return {
name: "grep",
label: "grep",
description: "Search file contents using regular expressions.",
parameters: {
type: "object",
properties: {
pattern: { type: "string", description: "The regex pattern to search for" },
path: { type: "string", description: "The path to search in" },
},
required: ["pattern"],
},
async execute(_id, _params) {
return { content: [{ type: "text", text: "" }], details: {} };
},
};
}
export function createWriteTool(_cwd, _opts = {}) {
return {
name: "write",
label: "write",
description: "Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.",
parameters: {
type: "object",
properties: {
path: { type: "string", description: "The path to the file to write" },
content: { type: "string", description: "The content to write to the file" },
},
required: ["path", "content"],
},
async execute(_id, _params) {
return { content: [{ type: "text", text: "" }], details: {} };
},
};
}
export function createEditTool(_cwd, _opts = {}) {
return {
name: "edit",
label: "edit",
description: "Edit a file by replacing exact text. The oldText must match exactly (including whitespace). Use this for precise, surgical edits.",
parameters: {
type: "object",
properties: {
path: { type: "string", description: "The path to the file to edit" },
oldText: { type: "string", minLength: 1, description: "The exact text to find and replace" },
newText: { type: "string", description: "The text to replace oldText with" },
},
required: ["path", "oldText", "newText"],
},
async execute(_id, _params) {
return { content: [{ type: "text", text: "" }], details: {} };
},
};
}
export function copyToClipboard(_text) {
return;
}
export function getAgentDir() {
const home =
globalThis.pi && globalThis.pi.env && typeof globalThis.pi.env.get === "function"
? globalThis.pi.env.get("HOME")
: undefined;
return home ? `${home}/.pi/agent` : "/home/unknown/.pi/agent";
}
// Stub: keyHint returns a keyboard shortcut hint string for UI display
export function keyHint(action, fallback = "") {
// Map action names to default key bindings
const keyMap = {
expandTools: "Ctrl+E",
copy: "Ctrl+C",
paste: "Ctrl+V",
save: "Ctrl+S",
quit: "Ctrl+Q",
help: "?",
};
return keyMap[action] || fallback || action;
}
// Stub: compact performs conversation compaction via LLM
export async function compact(_preparation, _model, _apiKey, _customInstructions, _signal) {
// Return a minimal compaction result
return {
summary: "Conversation summary placeholder",
firstKeptEntryId: null,
tokensBefore: 0,
tokensAfter: 0,
};
}
/// Stub: AssistantMessageComponent for rendering assistant messages
export class AssistantMessageComponent {
constructor(message, editable = false) {
this.message = message;
this.editable = editable;
}
render() {
return [];
}
}
// Stub: ToolExecutionComponent for rendering tool executions
export class ToolExecutionComponent {
constructor(toolName, args, opts = {}, result, ui) {
this.toolName = toolName;
this.args = args;
this.opts = opts;
this.result = result;
this.ui = ui;
}
render() {
return [];
}
}
// Stub: UserMessageComponent for rendering user messages
export class UserMessageComponent {
constructor(text) {
this.text = text;
}
render() {
return [];
}
}
export class SessionManager {
constructor() {}
static inMemory() { return new SessionManager(); }
getSessionFile() { return ""; }
getSessionDir() { return ""; }
getSessionId() { return ""; }
}
export class SettingsManager {
constructor(cwd = "", agentDir = "") {
this.cwd = String(cwd ?? "");
this.agentDir = String(agentDir ?? "");
}
static create(cwd, agentDir) { return new SettingsManager(cwd, agentDir); }
}
export class DefaultResourceLoader {
constructor(opts = {}) {
this.opts = opts;
}
async reload() { return; }
}
export function highlightCode(code, _lang, _theme) {
return String(code ?? "");
}
export function getLanguageFromPath(filePath) {
const ext = String(filePath ?? "").split(".").pop() || "";
const map = { ts: "typescript", js: "javascript", py: "python", rs: "rust", go: "go", md: "markdown", json: "json", html: "html", css: "css", sh: "bash" };
return map[ext] || ext;
}
export function isBashToolResult(result) {
return result && typeof result === "object" && result.name === "bash";
}
export async function loadSkills() {
return [];
}
export function truncateToVisualLines(text, maxLines = DEFAULT_MAX_LINES) {
const raw = String(text ?? "");
const lines = raw.split(/\r?\n/);
if (!Number.isFinite(maxLines) || maxLines <= 0) return "";
return lines.slice(0, Math.floor(maxLines)).join("\n");
}
export function estimateTokens(input) {
const raw = typeof input === "string" ? input : JSON.stringify(input ?? "");
// Deterministic rough heuristic (chars / 4).
return Math.max(1, Math.ceil(String(raw).length / 4));
}
export function isToolCallEventType(value) {
const t = String(value?.type ?? value ?? "").toLowerCase();
return t === "tool_call" || t === "tool-call" || t === "toolcall";
}
export class AuthStorage {
constructor() {}
static load() { return new AuthStorage(); }
static async loadAsync() { return new AuthStorage(); }
resolveApiKey(_provider) { return undefined; }
get(_provider) { return undefined; }
}
export function createAgentSession(opts = {}) {
const state = {
id: String(opts.id ?? "session"),
messages: Array.isArray(opts.messages) ? opts.messages.slice() : [],
};
return {
id: state.id,
messages: state.messages,
append(entry) { state.messages.push(entry); },
toJSON() { return { id: state.id, messages: state.messages.slice() }; },
};
}
export default {
VERSION,
DEFAULT_MAX_LINES,
DEFAULT_MAX_BYTES,
formatSize,
truncateHead,
truncateTail,
parseSessionEntries,
convertToLlm,
serializeConversation,
parseFrontmatter,
getMarkdownTheme,
getSettingsListTheme,
getSelectListTheme,
DynamicBorder,
BorderedLoader,
CustomEditor,
createBashTool,
createReadTool,
createLsTool,
createGrepTool,
createWriteTool,
createEditTool,
copyToClipboard,
getAgentDir,
keyHint,
compact,
AssistantMessageComponent,
ToolExecutionComponent,
UserMessageComponent,
SessionManager,
SettingsManager,
DefaultResourceLoader,
highlightCode,
getLanguageFromPath,
isBashToolResult,
loadSkills,
truncateToVisualLines,
estimateTokens,
isToolCallEventType,
AuthStorage,
createAgentSession,
};
"#
.trim()
.to_string(),
);
modules.insert(
"@anthropic-ai/sdk".to_string(),
r"
export default class Anthropic {
constructor(_opts = {}) {}
}
"
.trim()
.to_string(),
);
modules.insert(
"@anthropic-ai/sandbox-runtime".to_string(),
r"
export const SandboxManager = {
initialize: async (_config) => {},
reset: async () => {},
};
export default { SandboxManager };
"
.trim()
.to_string(),
);
modules.insert(
"ms".to_string(),
r#"
function parseMs(text) {
const s = String(text ?? "").trim();
if (!s) return undefined;
const match = s.match(/^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d|w|y)?$/i);
if (!match) return undefined;
const value = Number(match[1]);
const unit = (match[2] || "ms").toLowerCase();
const mult = unit === "ms" ? 1 :
unit === "s" ? 1000 :
unit === "m" ? 60000 :
unit === "h" ? 3600000 :
unit === "d" ? 86400000 :
unit === "w" ? 604800000 :
unit === "y" ? 31536000000 : 1;
return Math.round(value * mult);
}
export default function ms(value) {
return parseMs(value);
}
export const parse = parseMs;
"#
.trim()
.to_string(),
);
modules.insert(
"jsonwebtoken".to_string(),
r#"
export function sign() {
throw new Error("jsonwebtoken.sign is not available in PiJS");
}
export function verify() {
throw new Error("jsonwebtoken.verify is not available in PiJS");
}
export function decode() {
return null;
}
export default { sign, verify, decode };
"#
.trim()
.to_string(),
);
// ── shell-quote ──────────────────────────────────────────────────
modules.insert(
"shell-quote".to_string(),
r#"
export function parse(cmd) {
if (typeof cmd !== 'string') return [];
const args = [];
let current = '';
let inSingle = false;
let inDouble = false;
let escaped = false;
for (let i = 0; i < cmd.length; i++) {
const ch = cmd[i];
if (escaped) { current += ch; escaped = false; continue; }
if (ch === '\\' && !inSingle) { escaped = true; continue; }
if (ch === "'" && !inDouble) { inSingle = !inSingle; continue; }
if (ch === '"' && !inSingle) { inDouble = !inDouble; continue; }
if ((ch === ' ' || ch === '\t') && !inSingle && !inDouble) {
if (current) { args.push(current); current = ''; }
continue;
}
current += ch;
}
if (current) args.push(current);
return args;
}
export function quote(args) {
if (!Array.isArray(args)) return '';
return args.map(a => {
if (/[^a-zA-Z0-9_\-=:./]/.test(a)) return "'" + a.replace(/'/g, "'\\''") + "'";
return a;
}).join(' ');
}
export default { parse, quote };
"#
.trim()
.to_string(),
);
// ── vscode-languageserver-protocol ──────────────────────────────
{
let vls = r"
export const DiagnosticSeverity = { Error: 1, Warning: 2, Information: 3, Hint: 4 };
export const CodeActionKind = { QuickFix: 'quickfix', Refactor: 'refactor', RefactorExtract: 'refactor.extract', RefactorInline: 'refactor.inline', RefactorRewrite: 'refactor.rewrite', Source: 'source', SourceOrganizeImports: 'source.organizeImports', SourceFixAll: 'source.fixAll' };
export const DocumentDiagnosticReportKind = { Full: 'full', Unchanged: 'unchanged' };
export const SymbolKind = { File: 1, Module: 2, Namespace: 3, Package: 4, Class: 5, Method: 6, Property: 7, Field: 8, Constructor: 9, Enum: 10, Interface: 11, Function: 12, Variable: 13, Constant: 14 };
function makeReqType(m) { return { type: { get method() { return m; } }, method: m }; }
function makeNotifType(m) { return { type: { get method() { return m; } }, method: m }; }
export const InitializeRequest = makeReqType('initialize');
export const DefinitionRequest = makeReqType('textDocument/definition');
export const ReferencesRequest = makeReqType('textDocument/references');
export const HoverRequest = makeReqType('textDocument/hover');
export const SignatureHelpRequest = makeReqType('textDocument/signatureHelp');
export const DocumentSymbolRequest = makeReqType('textDocument/documentSymbol');
export const RenameRequest = makeReqType('textDocument/rename');
export const CodeActionRequest = makeReqType('textDocument/codeAction');
export const DocumentDiagnosticRequest = makeReqType('textDocument/diagnostic');
export const WorkspaceDiagnosticRequest = makeReqType('workspace/diagnostic');
export const InitializedNotification = makeNotifType('initialized');
export const DidOpenTextDocumentNotification = makeNotifType('textDocument/didOpen');
export const DidChangeTextDocumentNotification = makeNotifType('textDocument/didChange');
export const DidCloseTextDocumentNotification = makeNotifType('textDocument/didClose');
export const DidSaveTextDocumentNotification = makeNotifType('textDocument/didSave');
export const PublishDiagnosticsNotification = makeNotifType('textDocument/publishDiagnostics');
export function createMessageConnection(_reader, _writer) {
return {
listen() {},
sendRequest() { return Promise.resolve(null); },
sendNotification() {},
onNotification() {},
onRequest() {},
onClose() {},
dispose() {},
};
}
export class StreamMessageReader { constructor(_s) {} }
export class StreamMessageWriter { constructor(_s) {} }
"
.trim()
.to_string();
modules.insert("vscode-languageserver-protocol".to_string(), vls.clone());
modules.insert(
"vscode-languageserver-protocol/node.js".to_string(),
vls.clone(),
);
modules.insert("vscode-languageserver-protocol/node".to_string(), vls);
}
// ── @modelcontextprotocol/sdk ──────────────────────────────────
{
let mcp_client = r"
export class Client {
constructor(_opts = {}) {}
async connect(_transport) {}
async listTools() { return { tools: [] }; }
async listResources() { return { resources: [] }; }
async callTool(_name, _args) { return { content: [] }; }
async close() {}
}
"
.trim()
.to_string();
let mcp_transport = r"
export class StdioClientTransport {
constructor(_opts = {}) {}
async start() {}
async close() {}
}
"
.trim()
.to_string();
modules.insert(
"@modelcontextprotocol/sdk/client/index.js".to_string(),
mcp_client.clone(),
);
modules.insert(
"@modelcontextprotocol/sdk/client/index".to_string(),
mcp_client,
);
modules.insert(
"@modelcontextprotocol/sdk/client/stdio.js".to_string(),
mcp_transport,
);
modules.insert(
"@modelcontextprotocol/sdk/client/streamableHttp.js".to_string(),
r"
export class StreamableHTTPClientTransport {
constructor(_opts = {}) {}
async start() {}
async close() {}
}
"
.trim()
.to_string(),
);
modules.insert(
"@modelcontextprotocol/sdk/client/sse.js".to_string(),
r"
export class SSEClientTransport {
constructor(_opts = {}) {}
async start() {}
async close() {}
}
"
.trim()
.to_string(),
);
}
// ── glob ────────────────────────────────────────────────────────
modules.insert(
"glob".to_string(),
r#"
export function globSync(pattern, _opts = {}) { return []; }
export function glob(pattern, optsOrCb, cb) {
const callback = typeof optsOrCb === "function" ? optsOrCb : cb;
if (typeof callback === "function") callback(null, []);
return Promise.resolve([]);
}
export class Glob {
constructor(_pattern, _opts = {}) { this.found = []; }
on() { return this; }
}
export default { globSync, glob, Glob };
"#
.trim()
.to_string(),
);
// ── uuid ────────────────────────────────────────────────────────
modules.insert(
"uuid".to_string(),
r#"
function randomHex(n) {
let out = "";
for (let i = 0; i < n; i++) out += Math.floor(Math.random() * 16).toString(16);
return out;
}
export function v4() {
return [randomHex(8), randomHex(4), "4" + randomHex(3), ((8 + Math.floor(Math.random() * 4)).toString(16)) + randomHex(3), randomHex(12)].join("-");
}
export function v7() {
const ts = Date.now().toString(16).padStart(12, "0");
return [ts.slice(0, 8), ts.slice(8) + randomHex(1), "7" + randomHex(3), ((8 + Math.floor(Math.random() * 4)).toString(16)) + randomHex(3), randomHex(12)].join("-");
}
export function v1() { return v4(); }
export function v3() { return v4(); }
export function v5() { return v4(); }
export function validate(uuid) { return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(String(uuid ?? "")); }
export function version(uuid) { return parseInt(String(uuid ?? "").charAt(14), 16) || 0; }
export default { v1, v3, v4, v5, v7, validate, version };
"#
.trim()
.to_string(),
);
// ── diff ────────────────────────────────────────────────────────
modules.insert(
"diff".to_string(),
r#"
export function createTwoFilesPatch(oldFile, newFile, oldStr, newStr, _oldHeader, _newHeader, _opts) {
const oldLines = String(oldStr ?? "").split("\n");
const newLines = String(newStr ?? "").split("\n");
let patch = `--- ${oldFile}\n+++ ${newFile}\n@@ -1,${oldLines.length} +1,${newLines.length} @@\n`;
for (const line of oldLines) patch += `-${line}\n`;
for (const line of newLines) patch += `+${line}\n`;
return patch;
}
export function createPatch(fileName, oldStr, newStr, oldH, newH, opts) {
return createTwoFilesPatch(fileName, fileName, oldStr, newStr, oldH, newH, opts);
}
export function diffLines(oldStr, newStr) {
return [{ value: String(oldStr ?? ""), removed: true, added: false }, { value: String(newStr ?? ""), removed: false, added: true }];
}
export function diffChars(o, n) { return diffLines(o, n); }
export function diffWords(o, n) { return diffLines(o, n); }
export function applyPatch() { return false; }
export default { createTwoFilesPatch, createPatch, diffLines, diffChars, diffWords, applyPatch };
"#
.trim()
.to_string(),
);
// ── just-bash ──────────────────────────────────────────────────
modules.insert(
"just-bash".to_string(),
r#"
export function bash(_cmd, _opts) { return Promise.resolve({ stdout: "", stderr: "", exitCode: 0 }); }
export { bash as Bash };
export default bash;
"#
.trim()
.to_string(),
);
// ── bunfig ─────────────────────────────────────────────────────
modules.insert(
"bunfig".to_string(),
r"
export function define(_schema) { return {}; }
export async function loadConfig(opts) {
const defaults = (opts && opts.defaultConfig) ? opts.defaultConfig : {};
return { ...defaults };
}
export default { define, loadConfig };
"
.trim()
.to_string(),
);
// ── bun ────────────────────────────────────────────────────────
modules.insert(
"bun".to_string(),
r"
const bun = globalThis.Bun || {};
export const argv = bun.argv || [];
export const file = (...args) => bun.file(...args);
export const write = (...args) => bun.write(...args);
export const spawn = (...args) => bun.spawn(...args);
export const which = (...args) => bun.which(...args);
export default bun;
"
.trim()
.to_string(),
);
// ── dotenv ─────────────────────────────────────────────────────
modules.insert(
"dotenv".to_string(),
r#"
export function config(_opts) { return { parsed: {} }; }
export function parse(src) {
const result = {};
for (const line of String(src ?? "").split("\n")) {
const idx = line.indexOf("=");
if (idx === -1) continue;
const key = line.slice(0, idx).trim();
const val = line.slice(idx + 1).trim().replace(/^["']|["']$/g, "");
if (key) result[key] = val;
}
return result;
}
export default { config, parse };
"#
.trim()
.to_string(),
);
modules.insert(
"node:path".to_string(),
r#"
function __pi_is_abs(s) {
return s.startsWith("/") || (s.length >= 3 && s[1] === ":" && s[2] === "/");
}
export function join(...parts) {
const cleaned = parts.map((p) => String(p ?? "").replace(/\\/g, "/")).filter((p) => p.length > 0);
if (cleaned.length === 0) return ".";
return normalize(cleaned.join("/"));
}
export function dirname(p) {
const s = String(p ?? "").replace(/\\/g, "/");
const idx = s.lastIndexOf("/");
if (idx <= 0) return s.startsWith("/") ? "/" : ".";
const dir = s.slice(0, idx);
// Keep trailing slash for drive root: D:/ not D:
if (dir.length === 2 && dir[1] === ":") return dir + "/";
return dir;
}
export function resolve(...parts) {
const base =
globalThis.pi && globalThis.pi.process && typeof globalThis.pi.process.cwd === "string"
? globalThis.pi.process.cwd
: "/";
const cleaned = parts
.map((p) => String(p ?? "").replace(/\\/g, "/"))
.filter((p) => p.length > 0);
let out = "";
for (const part of cleaned) {
if (__pi_is_abs(part)) {
out = part;
continue;
}
out = out === "" || out.endsWith("/") ? out + part : out + "/" + part;
}
if (!__pi_is_abs(out)) {
out = base.endsWith("/") ? base + out : base + "/" + out;
}
return normalize(out);
}
export function basename(p, ext) {
const s = String(p ?? "").replace(/\\/g, "/").replace(/\/+$/, "");
const idx = s.lastIndexOf("/");
const name = idx === -1 ? s : s.slice(idx + 1);
if (ext && name.endsWith(ext)) {
return name.slice(0, -ext.length);
}
return name;
}
export function relative(from, to) {
const fromParts = String(from ?? "").replace(/\\/g, "/").split("/").filter(Boolean);
const toParts = String(to ?? "").replace(/\\/g, "/").split("/").filter(Boolean);
let common = 0;
while (common < fromParts.length && common < toParts.length && fromParts[common] === toParts[common]) {
common++;
}
const up = fromParts.length - common;
const downs = toParts.slice(common);
const result = [...Array(up).fill(".."), ...downs];
return result.join("/") || ".";
}
export function isAbsolute(p) {
const s = String(p ?? "").replace(/\\/g, "/");
return __pi_is_abs(s);
}
export function extname(p) {
const s = String(p ?? "").replace(/\\/g, "/");
const b = s.lastIndexOf("/");
const name = b === -1 ? s : s.slice(b + 1);
const dot = name.lastIndexOf(".");
if (dot <= 0) return "";
return name.slice(dot);
}
export function normalize(p) {
const s = String(p ?? "").replace(/\\/g, "/");
const isAbs = __pi_is_abs(s);
const parts = s.split("/").filter(Boolean);
const out = [];
for (const part of parts) {
if (part === "..") { if (out.length > 0 && out[out.length - 1] !== "..") out.pop(); else if (!isAbs) out.push(part); }
else if (part !== ".") out.push(part);
}
const result = out.join("/");
if (out.length > 0 && out[0].length === 2 && out[0][1] === ":") return result;
return isAbs ? "/" + result : result || ".";
}
export function parse(p) {
const s = String(p ?? "").replace(/\\/g, "/");
const isAbs = s.startsWith("/");
const lastSlash = s.lastIndexOf("/");
const dir = lastSlash === -1 ? "" : s.slice(0, lastSlash) || (isAbs ? "/" : "");
const base = lastSlash === -1 ? s : s.slice(lastSlash + 1);
const ext = extname(base);
const name = ext ? base.slice(0, -ext.length) : base;
const root = isAbs ? "/" : "";
return { root, dir, base, ext, name };
}
export function format(pathObj) {
const dir = pathObj.dir || pathObj.root || "";
const base = pathObj.base || (pathObj.name || "") + (pathObj.ext || "");
if (!dir) return base;
return dir === pathObj.root ? dir + base : dir + "/" + base;
}
export const sep = "/";
export const delimiter = ":";
export const posix = { join, dirname, resolve, basename, relative, isAbsolute, extname, normalize, parse, format, sep, delimiter };
const win32Stub = new Proxy({}, { get(_, prop) { throw new Error("path.win32." + String(prop) + " is not supported (Pi runs on POSIX only)"); } });
export const win32 = win32Stub;
export default { join, dirname, resolve, basename, relative, isAbsolute, extname, normalize, parse, format, sep, delimiter, posix, win32 };
"#
.trim()
.to_string(),
);
modules.insert("node:os".to_string(), build_node_os_module());
modules.insert(
"node:child_process".to_string(),
r#"
const __pi_child_process_state = (() => {
if (globalThis.__pi_child_process_state) {
return globalThis.__pi_child_process_state;
}
const state = {
nextPid: 1000,
children: new Map(),
};
globalThis.__pi_child_process_state = state;
return state;
})();
function __makeEmitter() {
const listeners = new Map();
const emitter = {
on(event, listener) {
const key = String(event);
if (!listeners.has(key)) listeners.set(key, []);
listeners.get(key).push(listener);
return emitter;
},
once(event, listener) {
const wrapper = (...args) => {
emitter.off(event, wrapper);
listener(...args);
};
return emitter.on(event, wrapper);
},
off(event, listener) {
const key = String(event);
const bucket = listeners.get(key);
if (!bucket) return emitter;
const idx = bucket.indexOf(listener);
if (idx >= 0) bucket.splice(idx, 1);
if (bucket.length === 0) listeners.delete(key);
return emitter;
},
removeListener(event, listener) {
return emitter.off(event, listener);
},
emit(event, ...args) {
const key = String(event);
const bucket = listeners.get(key) || [];
for (const listener of [...bucket]) {
try {
listener(...args);
} catch (_) {}
}
return emitter;
},
};
return emitter;
}
function __emitCloseOnce(child, code, signal = null) {
if (child.__pi_done) return;
child.__pi_done = true;
child.exitCode = code;
child.signalCode = signal;
__pi_child_process_state.children.delete(child.pid);
child.emit("exit", code, signal);
child.emit("close", code, signal);
}
function __parseSpawnOptions(raw) {
const options = raw && typeof raw === "object" ? raw : {};
const allowed = new Set(["cwd", "detached", "shell", "stdio", "timeout"]);
for (const key of Object.keys(options)) {
if (!allowed.has(key)) {
throw new Error(`node:child_process.spawn: unsupported option '${key}'`);
}
}
if (options.shell !== undefined && options.shell !== false) {
throw new Error("node:child_process.spawn: only shell=false is supported in PiJS");
}
let stdio = ["pipe", "pipe", "pipe"];
if (options.stdio !== undefined) {
if (!Array.isArray(options.stdio)) {
throw new Error("node:child_process.spawn: options.stdio must be an array");
}
if (options.stdio.length !== 3) {
throw new Error("node:child_process.spawn: options.stdio must have exactly 3 entries");
}
stdio = options.stdio.map((entry, idx) => {
const value = String(entry ?? "");
if (value !== "ignore" && value !== "pipe") {
throw new Error(
`node:child_process.spawn: unsupported stdio[${idx}] value '${value}'`,
);
}
return value;
});
}
const cwd =
typeof options.cwd === "string" && options.cwd.trim().length > 0
? options.cwd
: undefined;
let timeoutMs = undefined;
if (options.timeout !== undefined) {
if (
typeof options.timeout !== "number" ||
!Number.isFinite(options.timeout) ||
options.timeout < 0
) {
throw new Error(
"node:child_process.spawn: options.timeout must be a non-negative number",
);
}
timeoutMs = Math.floor(options.timeout);
}
return {
cwd,
detached: Boolean(options.detached),
stdio,
timeoutMs,
};
}
function __installProcessKillBridge() {
globalThis.__pi_process_kill_impl = (pidValue, signal = "SIGTERM") => {
const pidNumeric = Number(pidValue);
if (!Number.isFinite(pidNumeric) || pidNumeric === 0) {
const err = new Error(`kill EINVAL: invalid pid ${String(pidValue)}`);
err.code = "EINVAL";
throw err;
}
const pid = Math.abs(Math.trunc(pidNumeric));
const child = __pi_child_process_state.children.get(pid);
if (!child) {
const err = new Error(`kill ESRCH: no such process ${pid}`);
err.code = "ESRCH";
throw err;
}
child.kill(signal);
return true;
};
}
__installProcessKillBridge();
export function spawn(command, args = [], options = {}) {
const cmd = String(command ?? "").trim();
if (!cmd) {
throw new Error("node:child_process.spawn: command is required");
}
if (!Array.isArray(args)) {
throw new Error("node:child_process.spawn: args must be an array");
}
const argv = args.map((arg) => String(arg));
const opts = __parseSpawnOptions(options);
const child = __makeEmitter();
child.pid = __pi_child_process_state.nextPid++;
child.killed = false;
child.exitCode = null;
child.signalCode = null;
child.__pi_done = false;
child.__pi_kill_resolver = null;
child.stdout = opts.stdio[1] === "pipe" ? __makeEmitter() : null;
child.stderr = opts.stdio[2] === "pipe" ? __makeEmitter() : null;
child.stdin = opts.stdio[0] === "pipe" ? __makeEmitter() : null;
child.kill = (signal = "SIGTERM") => {
if (child.__pi_done) return false;
child.killed = true;
if (typeof child.__pi_kill_resolver === "function") {
child.__pi_kill_resolver({
kind: "killed",
signal: String(signal || "SIGTERM"),
});
child.__pi_kill_resolver = null;
}
__emitCloseOnce(child, null, String(signal || "SIGTERM"));
return true;
};
__pi_child_process_state.children.set(child.pid, child);
const execOptions = {};
if (opts.cwd !== undefined) execOptions.cwd = opts.cwd;
if (opts.timeoutMs !== undefined) execOptions.timeout = opts.timeoutMs;
const execPromise = pi.exec(cmd, argv, execOptions).then(
(result) => ({ kind: "result", result }),
(error) => ({ kind: "error", error }),
);
const killPromise = new Promise((resolve) => {
child.__pi_kill_resolver = resolve;
});
Promise.race([execPromise, killPromise]).then((outcome) => {
if (!outcome || child.__pi_done) return;
if (outcome.kind === "result") {
const result = outcome.result || {};
if (child.stdout && result.stdout !== undefined && result.stdout !== null && result.stdout !== "") {
child.stdout.emit("data", String(result.stdout));
}
if (child.stderr && result.stderr !== undefined && result.stderr !== null && result.stderr !== "") {
child.stderr.emit("data", String(result.stderr));
}
if (result.killed) {
child.killed = true;
}
const code =
typeof result.code === "number" && Number.isFinite(result.code)
? result.code
: 0;
const signal =
result.killed || child.killed
? String(result.signal || "SIGTERM")
: null;
__emitCloseOnce(child, signal ? null : code, signal);
return;
}
if (outcome.kind === "error") {
const source = outcome.error || {};
const error =
source instanceof Error
? source
: new Error(String(source.message || source || "spawn failed"));
if (!error.code && source && source.code !== undefined) {
error.code = String(source.code);
}
child.emit("error", error);
__emitCloseOnce(child, 1, null);
}
});
return child;
}
function __parseExecSyncResult(raw, command) {
const result = JSON.parse(raw);
if (result.error) {
const err = new Error(`Command failed: ${command}\n${result.error}`);
err.status = null;
err.stdout = result.stdout || "";
err.stderr = result.stderr || "";
err.pid = result.pid || 0;
err.signal = null;
throw err;
}
if (result.killed) {
const err = new Error(`Command timed out: ${command}`);
err.killed = true;
err.status = result.status;
err.stdout = result.stdout || "";
err.stderr = result.stderr || "";
err.pid = result.pid || 0;
err.signal = "SIGTERM";
throw err;
}
return result;
}
export function spawnSync(command, argsInput, options) {
const cmd = String(command ?? "").trim();
if (!cmd) {
throw new Error("node:child_process.spawnSync: command is required");
}
const args = Array.isArray(argsInput) ? argsInput.map(String) : [];
const opts = (typeof argsInput === "object" && !Array.isArray(argsInput))
? argsInput
: (options || {});
const cwd = typeof opts.cwd === "string" ? opts.cwd : "";
const timeout = typeof opts.timeout === "number" ? opts.timeout : 0;
const maxBuffer = typeof opts.maxBuffer === "number" ? opts.maxBuffer : 1024 * 1024;
let result;
try {
const raw = __pi_exec_sync_native(cmd, JSON.stringify(args), cwd, timeout, maxBuffer);
result = JSON.parse(raw);
} catch (e) {
return {
pid: 0,
output: [null, "", e.message || ""],
stdout: "",
stderr: e.message || "",
status: null,
signal: null,
error: e,
};
}
if (result.error) {
const err = new Error(result.error);
return {
pid: result.pid || 0,
output: [null, result.stdout || "", result.stderr || ""],
stdout: result.stdout || "",
stderr: result.stderr || "",
status: null,
signal: result.killed ? "SIGTERM" : null,
error: err,
};
}
return {
pid: result.pid || 0,
output: [null, result.stdout || "", result.stderr || ""],
stdout: result.stdout || "",
stderr: result.stderr || "",
status: result.status ?? 0,
signal: result.killed ? "SIGTERM" : null,
error: undefined,
};
}
export function execSync(command, options) {
const cmdStr = String(command ?? "").trim();
if (!cmdStr) {
throw new Error("node:child_process.execSync: command is required");
}
const opts = options || {};
const cwd = typeof opts.cwd === "string" ? opts.cwd : "";
const timeout = typeof opts.timeout === "number" ? opts.timeout : 0;
const maxBuffer = typeof opts.maxBuffer === "number" ? opts.maxBuffer : 1024 * 1024;
// execSync runs through a shell, so pass via sh -c
const raw = __pi_exec_sync_native("sh", JSON.stringify(["-c", cmdStr]), cwd, timeout, maxBuffer);
const result = __parseExecSyncResult(raw, cmdStr);
if (result.status !== 0 && result.status !== null) {
const err = new Error(
`Command failed: ${cmdStr}\n${result.stderr || ""}`,
);
err.status = result.status;
err.stdout = result.stdout || "";
err.stderr = result.stderr || "";
err.pid = result.pid || 0;
err.signal = null;
throw err;
}
const stdout = result.stdout || "";
if (stdout.length > maxBuffer) {
const err = new Error(`stdout maxBuffer length exceeded`);
err.stdout = stdout.slice(0, maxBuffer);
err.stderr = result.stderr || "";
throw err;
}
const encoding = opts.encoding;
if (encoding === "buffer" || encoding === null) {
// Return a "buffer-like" string (QuickJS doesn't have real Buffer)
return stdout;
}
return stdout;
}
function __normalizeExecOptions(raw) {
const options = raw && typeof raw === "object" ? raw : {};
let timeoutMs = undefined;
if (
typeof options.timeout === "number" &&
Number.isFinite(options.timeout) &&
options.timeout >= 0
) {
timeoutMs = Math.floor(options.timeout);
}
const maxBuffer =
typeof options.maxBuffer === "number" &&
Number.isFinite(options.maxBuffer) &&
options.maxBuffer > 0
? Math.floor(options.maxBuffer)
: 1024 * 1024;
return {
cwd: typeof options.cwd === "string" && options.cwd.trim().length > 0 ? options.cwd : undefined,
timeoutMs,
maxBuffer,
encoding: options.encoding,
};
}
function __wrapExecLike(commandForError, child, opts, callback) {
let stdout = "";
let stderr = "";
let callbackDone = false;
const finish = (err, out, errOut) => {
if (callbackDone) return;
callbackDone = true;
if (typeof callback === "function") {
callback(err, out, errOut);
}
};
child.stdout?.on("data", (chunk) => {
stdout += String(chunk ?? "");
});
child.stderr?.on("data", (chunk) => {
stderr += String(chunk ?? "");
});
child.on("error", (error) => {
finish(
error instanceof Error ? error : new Error(String(error)),
"",
"",
);
});
child.on("close", (code) => {
let out = stdout;
let errOut = stderr;
if (out.length > opts.maxBuffer) {
const err = new Error("stdout maxBuffer length exceeded");
err.stdout = out.slice(0, opts.maxBuffer);
err.stderr = errOut;
finish(err, err.stdout, errOut);
return;
}
if (errOut.length > opts.maxBuffer) {
const err = new Error("stderr maxBuffer length exceeded");
err.stdout = out;
err.stderr = errOut.slice(0, opts.maxBuffer);
finish(err, out, err.stderr);
return;
}
if (opts.encoding !== "buffer" && opts.encoding !== null) {
out = String(out);
errOut = String(errOut);
}
if (code !== 0 && code !== undefined && code !== null) {
const err = new Error(`Command failed: ${commandForError}`);
err.code = code;
err.killed = Boolean(child.killed);
err.stdout = out;
err.stderr = errOut;
finish(err, out, errOut);
return;
}
if (child.killed) {
const err = new Error(`Command timed out: ${commandForError}`);
err.code = null;
err.killed = true;
err.signal = child.signalCode || "SIGTERM";
err.stdout = out;
err.stderr = errOut;
finish(err, out, errOut);
return;
}
finish(null, out, errOut);
});
return child;
}
export function exec(command, optionsOrCallback, callbackArg) {
const opts = typeof optionsOrCallback === "object" ? optionsOrCallback : {};
const callback = typeof optionsOrCallback === "function"
? optionsOrCallback
: callbackArg;
const cmdStr = String(command ?? "").trim();
const normalized = __normalizeExecOptions(opts);
const spawnOpts = {
shell: false,
stdio: ["ignore", "pipe", "pipe"],
};
if (normalized.cwd !== undefined) spawnOpts.cwd = normalized.cwd;
if (normalized.timeoutMs !== undefined) spawnOpts.timeout = normalized.timeoutMs;
const child = spawn("sh", ["-c", cmdStr], spawnOpts);
return __wrapExecLike(cmdStr, child, normalized, callback);
}
export function execFileSync(file, argsInput, options) {
const fileStr = String(file ?? "").trim();
if (!fileStr) {
throw new Error("node:child_process.execFileSync: file is required");
}
const args = Array.isArray(argsInput) ? argsInput.map(String) : [];
const opts = (typeof argsInput === "object" && !Array.isArray(argsInput))
? argsInput
: (options || {});
const cwd = typeof opts.cwd === "string" ? opts.cwd : "";
const timeout = typeof opts.timeout === "number" ? opts.timeout : 0;
const maxBuffer = typeof opts.maxBuffer === "number" ? opts.maxBuffer : 1024 * 1024;
const raw = __pi_exec_sync_native(fileStr, JSON.stringify(args), cwd, timeout, maxBuffer);
const result = __parseExecSyncResult(raw, fileStr);
if (result.status !== 0 && result.status !== null) {
const err = new Error(
`Command failed: ${fileStr}\n${result.stderr || ""}`,
);
err.status = result.status;
err.stdout = result.stdout || "";
err.stderr = result.stderr || "";
err.pid = result.pid || 0;
throw err;
}
return result.stdout || "";
}
export function execFile(file, argsOrOptsOrCb, optsOrCb, callbackArg) {
const fileStr = String(file ?? "").trim();
let args = [];
let opts = {};
let callback;
if (typeof argsOrOptsOrCb === "function") {
callback = argsOrOptsOrCb;
} else if (Array.isArray(argsOrOptsOrCb)) {
args = argsOrOptsOrCb.map(String);
if (typeof optsOrCb === "function") {
callback = optsOrCb;
} else {
opts = optsOrCb || {};
callback = callbackArg;
}
} else if (typeof argsOrOptsOrCb === "object") {
opts = argsOrOptsOrCb || {};
callback = typeof optsOrCb === "function" ? optsOrCb : callbackArg;
}
const normalized = __normalizeExecOptions(opts);
const spawnOpts = {
shell: false,
stdio: ["ignore", "pipe", "pipe"],
};
if (normalized.cwd !== undefined) spawnOpts.cwd = normalized.cwd;
if (normalized.timeoutMs !== undefined) spawnOpts.timeout = normalized.timeoutMs;
const child = spawn(fileStr, args, spawnOpts);
return __wrapExecLike(fileStr, child, normalized, callback);
}
export function fork(_modulePath, _args, _opts) {
throw new Error("node:child_process.fork is not available in PiJS");
}
export default { spawn, spawnSync, execSync, execFileSync, exec, execFile, fork };
"#
.trim()
.to_string(),
);
modules.insert(
"node:module".to_string(),
r#"
import * as fs from "node:fs";
import * as fsPromises from "node:fs/promises";
import * as path from "node:path";
import * as os from "node:os";
import * as crypto from "node:crypto";
import * as url from "node:url";
import * as processMod from "node:process";
import * as buffer from "node:buffer";
import * as childProcess from "node:child_process";
import * as http from "node:http";
import * as https from "node:https";
import * as net from "node:net";
import * as events from "node:events";
import * as stream from "node:stream";
import * as streamPromises from "node:stream/promises";
import * as streamWeb from "node:stream/web";
import * as stringDecoder from "node:string_decoder";
import * as http2 from "node:http2";
import * as util from "node:util";
import * as readline from "node:readline";
import * as querystring from "node:querystring";
import * as assertMod from "node:assert";
import * as constantsMod from "node:constants";
import * as tls from "node:tls";
import * as tty from "node:tty";
import * as zlib from "node:zlib";
import * as perfHooks from "node:perf_hooks";
import * as vm from "node:vm";
import * as v8 from "node:v8";
import * as workerThreads from "node:worker_threads";
function __normalizeBuiltin(id) {
const spec = String(id ?? "");
switch (spec) {
case "fs":
case "node:fs":
return "node:fs";
case "fs/promises":
case "node:fs/promises":
return "node:fs/promises";
case "path":
case "node:path":
return "node:path";
case "os":
case "node:os":
return "node:os";
case "crypto":
case "node:crypto":
return "node:crypto";
case "url":
case "node:url":
return "node:url";
case "process":
case "node:process":
return "node:process";
case "buffer":
case "node:buffer":
return "node:buffer";
case "child_process":
case "node:child_process":
return "node:child_process";
case "http":
case "node:http":
return "node:http";
case "https":
case "node:https":
return "node:https";
case "net":
case "node:net":
return "node:net";
case "events":
case "node:events":
return "node:events";
case "stream":
case "node:stream":
return "node:stream";
case "stream/web":
case "node:stream/web":
return "node:stream/web";
case "stream/promises":
case "node:stream/promises":
return "node:stream/promises";
case "string_decoder":
case "node:string_decoder":
return "node:string_decoder";
case "http2":
case "node:http2":
return "node:http2";
case "util":
case "node:util":
return "node:util";
case "readline":
case "node:readline":
return "node:readline";
case "querystring":
case "node:querystring":
return "node:querystring";
case "assert":
case "node:assert":
return "node:assert";
case "module":
case "node:module":
return "node:module";
case "constants":
case "node:constants":
return "node:constants";
case "tls":
case "node:tls":
return "node:tls";
case "tty":
case "node:tty":
return "node:tty";
case "zlib":
case "node:zlib":
return "node:zlib";
case "perf_hooks":
case "node:perf_hooks":
return "node:perf_hooks";
case "vm":
case "node:vm":
return "node:vm";
case "v8":
case "node:v8":
return "node:v8";
case "worker_threads":
case "node:worker_threads":
return "node:worker_threads";
default:
return spec;
}
}
const __builtinModules = {
"node:fs": fs,
"node:fs/promises": fsPromises,
"node:path": path,
"node:os": os,
"node:crypto": crypto,
"node:url": url,
"node:process": processMod,
"node:buffer": buffer,
"node:child_process": childProcess,
"node:http": http,
"node:https": https,
"node:net": net,
"node:events": events,
"node:stream": stream,
"node:stream/web": streamWeb,
"node:stream/promises": streamPromises,
"node:string_decoder": stringDecoder,
"node:http2": http2,
"node:util": util,
"node:readline": readline,
"node:querystring": querystring,
"node:assert": assertMod,
"node:module": { createRequire },
"node:constants": constantsMod,
"node:tls": tls,
"node:tty": tty,
"node:zlib": zlib,
"node:perf_hooks": perfHooks,
"node:vm": vm,
"node:v8": v8,
"node:worker_threads": workerThreads,
};
const __missingRequireCache = Object.create(null);
function __isBarePackageSpecifier(spec) {
return (
typeof spec === "string" &&
spec.length > 0 &&
!spec.startsWith("./") &&
!spec.startsWith("../") &&
!spec.startsWith("/") &&
!spec.startsWith("file://") &&
!spec.includes(":")
);
}
function __makeMissingRequireStub(spec) {
if (__missingRequireCache[spec]) {
return __missingRequireCache[spec];
}
const handler = {
get(_target, prop) {
if (typeof prop === "symbol") {
if (prop === Symbol.toPrimitive) return () => "";
return undefined;
}
if (prop === "__esModule") return true;
if (prop === "default") return stub;
if (prop === "toString") return () => "";
if (prop === "valueOf") return () => "";
if (prop === "name") return spec;
if (prop === "then") return undefined;
return stub;
},
apply() { return stub; },
construct() { return stub; },
has() { return false; },
ownKeys() { return []; },
getOwnPropertyDescriptor() {
return { configurable: true, enumerable: false };
},
};
const stub = new Proxy(function __pijs_missing_require_stub() {}, handler);
__missingRequireCache[spec] = stub;
return stub;
}
export function createRequire(_path) {
function require(id) {
const normalized = __normalizeBuiltin(id);
const builtIn = __builtinModules[normalized];
if (builtIn) {
if (builtIn && Object.prototype.hasOwnProperty.call(builtIn, "default") && builtIn.default !== undefined) {
return builtIn.default;
}
return builtIn;
}
const raw = String(id ?? "");
if (raw.startsWith("node:") || __isBarePackageSpecifier(raw)) {
return __makeMissingRequireStub(raw);
}
throw new Error(`Cannot find module '${raw}' in PiJS require()`);
}
require.resolve = function resolve(id) {
// Return a synthetic path for the requested module. This satisfies
// extensions that call require.resolve() to locate a binary entry
// point (e.g. @sourcegraph/scip-python) without actually needing the
// real node_modules tree.
return `/pijs-virtual/${String(id ?? "unknown")}`;
};
require.resolve.paths = function() { return []; };
return require;
}
export default { createRequire };
"#
.trim()
.to_string(),
);
modules.insert(
"node:fs".to_string(),
r#"
import { Readable, Writable } from "node:stream";
export const constants = {
R_OK: 4,
W_OK: 2,
X_OK: 1,
F_OK: 0,
O_RDONLY: 0,
O_WRONLY: 1,
O_RDWR: 2,
O_CREAT: 64,
O_EXCL: 128,
O_TRUNC: 512,
O_APPEND: 1024,
};
const __pi_vfs = (() => {
if (globalThis.__pi_vfs_state) {
return globalThis.__pi_vfs_state;
}
const state = {
files: new Map(),
dirs: new Set(["/"]),
symlinks: new Map(),
fds: new Map(),
nextFd: 100,
};
function checkWriteAccess(resolved) {
if (typeof globalThis.__pi_host_check_write_access === "function") {
globalThis.__pi_host_check_write_access(resolved);
}
}
function normalizePath(input) {
let raw = String(input ?? "").replace(/\\/g, "/");
// Strip Windows UNC verbatim prefix that canonicalize() produces.
// \\?\C:\... becomes /?/C:/... after separator normalization.
if (raw.startsWith("/?/") && raw.length > 5 && /^[A-Za-z]:/.test(raw.substring(3, 5))) {
raw = raw.slice(3);
}
// Detect Windows drive-letter absolute paths (e.g. "C:/Users/...")
const hasDriveLetter = raw.length >= 3 && /^[A-Za-z]:\//.test(raw);
const isAbsolute = raw.startsWith("/") || hasDriveLetter;
const base = isAbsolute
? raw
: `${(globalThis.process && typeof globalThis.process.cwd === "function" ? globalThis.process.cwd() : "/").replace(/\\/g, "/")}/${raw}`;
const parts = [];
for (const part of base.split("/")) {
if (!part || part === ".") continue;
if (part === "..") {
if (parts.length > 0) parts.pop();
continue;
}
parts.push(part);
}
// Preserve drive letter prefix on Windows (D:/...) instead of /D:/...
if (parts.length > 0 && /^[A-Za-z]:$/.test(parts[0])) {
return `${parts[0]}/${parts.slice(1).join("/")}`;
}
return `/${parts.join("/")}`;
}
function dirname(path) {
const normalized = normalizePath(path);
if (normalized === "/") return "/";
const idx = normalized.lastIndexOf("/");
return idx <= 0 ? "/" : normalized.slice(0, idx);
}
function ensureDir(path) {
const normalized = normalizePath(path);
if (normalized === "/") return "/";
const parts = normalized.slice(1).split("/");
let current = "";
for (const part of parts) {
current = `${current}/${part}`;
state.dirs.add(current);
}
return normalized;
}
function toBytes(data, opts) {
const encoding =
typeof opts === "string"
? opts
: opts && typeof opts === "object" && typeof opts.encoding === "string"
? opts.encoding
: undefined;
const normalizedEncoding = encoding ? String(encoding).toLowerCase() : "utf8";
if (typeof data === "string") {
if (normalizedEncoding === "base64") {
return Buffer.from(data, "base64");
}
return new TextEncoder().encode(data);
}
if (data instanceof Uint8Array) {
return new Uint8Array(data);
}
if (data instanceof ArrayBuffer) {
return new Uint8Array(data);
}
if (ArrayBuffer.isView(data)) {
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
}
if (Array.isArray(data)) {
return new Uint8Array(data);
}
return new TextEncoder().encode(String(data ?? ""));
}
function decodeBytes(bytes, opts) {
const encoding =
typeof opts === "string"
? opts
: opts && typeof opts === "object" && typeof opts.encoding === "string"
? opts.encoding
: undefined;
if (!encoding || String(encoding).toLowerCase() === "buffer") {
return Buffer.from(bytes);
}
const normalized = String(encoding).toLowerCase();
if (normalized === "base64") {
let bin = "";
for (let i = 0; i < bytes.length; i++) {
bin += String.fromCharCode(bytes[i] & 0xff);
}
return btoa(bin);
}
return new TextDecoder().decode(bytes);
}
function resolveSymlinkPath(linkPath, target) {
const raw = String(target ?? "");
if (raw.startsWith("/")) {
return normalizePath(raw);
}
return normalizePath(`${dirname(linkPath)}/${raw}`);
}
function resolvePath(path, followSymlinks = true) {
let normalized = normalizePath(path);
if (!followSymlinks) {
return normalized;
}
const seen = new Set();
while (state.symlinks.has(normalized)) {
if (seen.has(normalized)) {
throw new Error(`ELOOP: too many symbolic links encountered, stat '${String(path ?? "")}'`);
}
seen.add(normalized);
normalized = resolveSymlinkPath(normalized, state.symlinks.get(normalized));
}
return normalized;
}
function parseOpenFlags(rawFlags) {
if (typeof rawFlags === "number" && Number.isFinite(rawFlags)) {
const flags = rawFlags | 0;
const accessMode = flags & 3;
const readable = accessMode === constants.O_RDONLY || accessMode === constants.O_RDWR;
const writable = accessMode === constants.O_WRONLY || accessMode === constants.O_RDWR;
return {
readable,
writable,
append: (flags & constants.O_APPEND) !== 0,
create: (flags & constants.O_CREAT) !== 0,
truncate: (flags & constants.O_TRUNC) !== 0,
exclusive: (flags & constants.O_EXCL) !== 0,
};
}
const normalized = String(rawFlags ?? "r");
switch (normalized) {
case "r":
case "rs":
return { readable: true, writable: false, append: false, create: false, truncate: false, exclusive: false };
case "r+":
case "rs+":
return { readable: true, writable: true, append: false, create: false, truncate: false, exclusive: false };
case "w":
return { readable: false, writable: true, append: false, create: true, truncate: true, exclusive: false };
case "w+":
return { readable: true, writable: true, append: false, create: true, truncate: true, exclusive: false };
case "wx":
return { readable: false, writable: true, append: false, create: true, truncate: true, exclusive: true };
case "wx+":
return { readable: true, writable: true, append: false, create: true, truncate: true, exclusive: true };
case "a":
case "as":
return { readable: false, writable: true, append: true, create: true, truncate: false, exclusive: false };
case "a+":
case "as+":
return { readable: true, writable: true, append: true, create: true, truncate: false, exclusive: false };
case "ax":
return { readable: false, writable: true, append: true, create: true, truncate: false, exclusive: true };
case "ax+":
return { readable: true, writable: true, append: true, create: true, truncate: false, exclusive: true };
default:
throw new Error(`EINVAL: invalid open flags '${normalized}'`);
}
}
function getFdEntry(fd) {
const entry = state.fds.get(fd);
if (!entry) {
throw new Error(`EBADF: bad file descriptor, fd ${String(fd)}`);
}
return entry;
}
function toWritableView(buffer) {
if (buffer instanceof Uint8Array) {
return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
}
if (buffer instanceof ArrayBuffer) {
return new Uint8Array(buffer);
}
if (ArrayBuffer.isView(buffer)) {
return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
}
throw new Error("TypeError: buffer must be an ArrayBuffer view");
}
function makeDirent(name, entryKind) {
return {
name,
isDirectory() { return entryKind === "dir"; },
isFile() { return entryKind === "file"; },
isSymbolicLink() { return entryKind === "symlink"; },
};
}
function listChildren(path, withFileTypes) {
const normalized = normalizePath(path);
const prefix = normalized === "/" ? "/" : `${normalized}/`;
const children = new Map();
for (const dir of state.dirs) {
if (!dir.startsWith(prefix) || dir === normalized) continue;
const rest = dir.slice(prefix.length);
if (!rest || rest.includes("/")) continue;
children.set(rest, "dir");
}
for (const file of state.files.keys()) {
if (!file.startsWith(prefix)) continue;
const rest = file.slice(prefix.length);
if (!rest || rest.includes("/")) continue;
if (!children.has(rest)) children.set(rest, "file");
}
for (const link of state.symlinks.keys()) {
if (!link.startsWith(prefix)) continue;
const rest = link.slice(prefix.length);
if (!rest || rest.includes("/")) continue;
if (!children.has(rest)) children.set(rest, "symlink");
}
const names = Array.from(children.keys()).sort();
if (withFileTypes) {
return names.map((name) => makeDirent(name, children.get(name)));
}
return names;
}
function makeStat(path, followSymlinks = true) {
const normalized = normalizePath(path);
const linkTarget = state.symlinks.get(normalized);
if (linkTarget !== undefined) {
if (!followSymlinks) {
const size = new TextEncoder().encode(String(linkTarget)).byteLength;
return {
isFile() { return false; },
isDirectory() { return false; },
isSymbolicLink() { return true; },
isBlockDevice() { return false; },
isCharacterDevice() { return false; },
isFIFO() { return false; },
isSocket() { return false; },
size,
mode: 0o777,
uid: 0,
gid: 0,
atimeMs: 0,
mtimeMs: 0,
ctimeMs: 0,
birthtimeMs: 0,
atime: new Date(0),
mtime: new Date(0),
ctime: new Date(0),
birthtime: new Date(0),
dev: 0,
ino: 0,
nlink: 1,
rdev: 0,
blksize: 4096,
blocks: 0,
};
}
return makeStat(resolvePath(normalized, true), true);
}
const isDir = state.dirs.has(normalized);
let bytes = state.files.get(normalized);
if (!isDir && bytes === undefined && typeof globalThis.__pi_host_read_file_sync === "function") {
try {
const content = globalThis.__pi_host_read_file_sync(normalized);
bytes = toBytes(content);
ensureDir(dirname(normalized));
state.files.set(normalized, bytes);
} catch (e) {
const message = String((e && e.message) ? e.message : e);
if (message.includes("host read denied")) {
throw e;
}
/* not on host FS */
}
}
const isFile = bytes !== undefined;
if (!isDir && !isFile) {
throw new Error(`ENOENT: no such file or directory, stat '${String(path ?? "")}'`);
}
const size = isFile ? bytes.byteLength : 0;
return {
isFile() { return isFile; },
isDirectory() { return isDir; },
isSymbolicLink() { return false; },
isBlockDevice() { return false; },
isCharacterDevice() { return false; },
isFIFO() { return false; },
isSocket() { return false; },
size,
mode: isDir ? 0o755 : 0o644,
uid: 0,
gid: 0,
atimeMs: 0,
mtimeMs: 0,
ctimeMs: 0,
birthtimeMs: 0,
atime: new Date(0),
mtime: new Date(0),
ctime: new Date(0),
birthtime: new Date(0),
dev: 0,
ino: 0,
nlink: 1,
rdev: 0,
blksize: 4096,
blocks: 0,
};
}
state.normalizePath = normalizePath;
state.dirname = dirname;
state.ensureDir = ensureDir;
state.toBytes = toBytes;
state.decodeBytes = decodeBytes;
state.listChildren = listChildren;
state.makeStat = makeStat;
state.resolvePath = resolvePath;
state.checkWriteAccess = checkWriteAccess;
state.parseOpenFlags = parseOpenFlags;
state.getFdEntry = getFdEntry;
state.toWritableView = toWritableView;
globalThis.__pi_vfs_state = state;
return state;
})();
export function existsSync(path) {
try {
statSync(path);
return true;
} catch (_err) {
return false;
}
}
export function readFileSync(path, encoding) {
const resolved = __pi_vfs.resolvePath(path, true);
let bytes = __pi_vfs.files.get(resolved);
let hostError;
if (!bytes && typeof globalThis.__pi_host_read_file_sync === "function") {
try {
const content = globalThis.__pi_host_read_file_sync(resolved);
bytes = __pi_vfs.toBytes(content);
__pi_vfs.ensureDir(__pi_vfs.dirname(resolved));
__pi_vfs.files.set(resolved, bytes);
} catch (e) {
const message = String((e && e.message) ? e.message : e);
if (message.includes("host read denied")) {
throw e;
}
hostError = message;
/* fall through to ENOENT */
}
}
if (!bytes) {
const detail = hostError ? ` (host: ${hostError})` : "";
throw new Error(`ENOENT: no such file or directory, open '${String(path ?? "")}'${detail}`);
}
return __pi_vfs.decodeBytes(bytes, encoding);
}
export function appendFileSync(path, data, opts) {
const resolved = __pi_vfs.resolvePath(path, true);
__pi_vfs.checkWriteAccess(resolved);
const current = __pi_vfs.files.get(resolved) || new Uint8Array();
const next = __pi_vfs.toBytes(data, opts);
const merged = new Uint8Array(current.byteLength + next.byteLength);
merged.set(current, 0);
merged.set(next, current.byteLength);
__pi_vfs.ensureDir(__pi_vfs.dirname(resolved));
__pi_vfs.files.set(resolved, merged);
}
export function writeFileSync(path, data, opts) {
const resolved = __pi_vfs.resolvePath(path, true);
__pi_vfs.checkWriteAccess(resolved);
__pi_vfs.ensureDir(__pi_vfs.dirname(resolved));
__pi_vfs.files.set(resolved, __pi_vfs.toBytes(data, opts));
}
export function readdirSync(path, opts) {
const resolved = __pi_vfs.resolvePath(path, true);
if (!__pi_vfs.dirs.has(resolved)) {
throw new Error(`ENOENT: no such file or directory, scandir '${String(path ?? "")}'`);
}
const withFileTypes = !!(opts && typeof opts === "object" && opts.withFileTypes);
return __pi_vfs.listChildren(resolved, withFileTypes);
}
const __fakeStat = {
isFile() { return false; },
isDirectory() { return false; },
isSymbolicLink() { return false; },
isBlockDevice() { return false; },
isCharacterDevice() { return false; },
isFIFO() { return false; },
isSocket() { return false; },
size: 0, mode: 0o644, uid: 0, gid: 0,
atimeMs: 0, mtimeMs: 0, ctimeMs: 0, birthtimeMs: 0,
atime: new Date(0), mtime: new Date(0), ctime: new Date(0), birthtime: new Date(0),
dev: 0, ino: 0, nlink: 1, rdev: 0, blksize: 4096, blocks: 0,
};
export function statSync(path) { return __pi_vfs.makeStat(path, true); }
export function lstatSync(path) { return __pi_vfs.makeStat(path, false); }
export function mkdtempSync(prefix, _opts) {
const p = String(prefix ?? "/tmp/tmp-");
const out = `${p}${Date.now().toString(36)}`;
__pi_vfs.ensureDir(out);
return out;
}
export function realpathSync(path, _opts) {
return __pi_vfs.resolvePath(path, true);
}
export function unlinkSync(path) {
const normalized = __pi_vfs.normalizePath(path);
__pi_vfs.checkWriteAccess(normalized);
if (__pi_vfs.symlinks.delete(normalized)) {
return;
}
if (!__pi_vfs.files.delete(normalized)) {
throw new Error(`ENOENT: no such file or directory, unlink '${String(path ?? "")}'`);
}
}
export function rmdirSync(path, _opts) {
const normalized = __pi_vfs.normalizePath(path);
__pi_vfs.checkWriteAccess(normalized);
if (normalized === "/") {
throw new Error("EBUSY: resource busy or locked, rmdir '/'");
}
if (__pi_vfs.symlinks.has(normalized)) {
throw new Error(`ENOTDIR: not a directory, rmdir '${String(path ?? "")}'`);
}
for (const filePath of __pi_vfs.files.keys()) {
if (filePath.startsWith(`${normalized}/`)) {
throw new Error(`ENOTEMPTY: directory not empty, rmdir '${String(path ?? "")}'`);
}
}
for (const dirPath of __pi_vfs.dirs) {
if (dirPath.startsWith(`${normalized}/`)) {
throw new Error(`ENOTEMPTY: directory not empty, rmdir '${String(path ?? "")}'`);
}
}
for (const linkPath of __pi_vfs.symlinks.keys()) {
if (linkPath.startsWith(`${normalized}/`)) {
throw new Error(`ENOTEMPTY: directory not empty, rmdir '${String(path ?? "")}'`);
}
}
if (!__pi_vfs.dirs.delete(normalized)) {
throw new Error(`ENOENT: no such file or directory, rmdir '${String(path ?? "")}'`);
}
}
export function rmSync(path, opts) {
const normalized = __pi_vfs.normalizePath(path);
__pi_vfs.checkWriteAccess(normalized);
if (__pi_vfs.files.has(normalized)) {
__pi_vfs.files.delete(normalized);
return;
}
if (__pi_vfs.symlinks.has(normalized)) {
__pi_vfs.symlinks.delete(normalized);
return;
}
if (__pi_vfs.dirs.has(normalized)) {
const recursive = !!(opts && typeof opts === "object" && opts.recursive);
if (!recursive) {
rmdirSync(normalized);
return;
}
for (const filePath of Array.from(__pi_vfs.files.keys())) {
if (filePath === normalized || filePath.startsWith(`${normalized}/`)) {
__pi_vfs.files.delete(filePath);
}
}
for (const dirPath of Array.from(__pi_vfs.dirs)) {
if (dirPath === normalized || dirPath.startsWith(`${normalized}/`)) {
__pi_vfs.dirs.delete(dirPath);
}
}
for (const linkPath of Array.from(__pi_vfs.symlinks.keys())) {
if (linkPath === normalized || linkPath.startsWith(`${normalized}/`)) {
__pi_vfs.symlinks.delete(linkPath);
}
}
if (!__pi_vfs.dirs.has("/")) {
__pi_vfs.dirs.add("/");
}
return;
}
throw new Error(`ENOENT: no such file or directory, rm '${String(path ?? "")}'`);
}
export function copyFileSync(src, dest, _mode) {
writeFileSync(dest, readFileSync(src));
}
export function renameSync(oldPath, newPath) {
const src = __pi_vfs.normalizePath(oldPath);
const dst = __pi_vfs.normalizePath(newPath);
__pi_vfs.checkWriteAccess(src);
__pi_vfs.checkWriteAccess(dst);
const linkTarget = __pi_vfs.symlinks.get(src);
if (linkTarget !== undefined) {
__pi_vfs.ensureDir(__pi_vfs.dirname(dst));
__pi_vfs.symlinks.set(dst, linkTarget);
__pi_vfs.symlinks.delete(src);
return;
}
const bytes = __pi_vfs.files.get(src);
if (bytes !== undefined) {
__pi_vfs.ensureDir(__pi_vfs.dirname(dst));
__pi_vfs.files.set(dst, bytes);
__pi_vfs.files.delete(src);
return;
}
throw new Error(`ENOENT: no such file or directory, rename '${String(oldPath ?? "")}'`);
}
export function mkdirSync(path, _opts) {
const resolved = __pi_vfs.resolvePath(path, true);
__pi_vfs.checkWriteAccess(resolved);
__pi_vfs.ensureDir(path);
return __pi_vfs.normalizePath(path);
}
export function accessSync(path, _mode) {
if (!existsSync(path)) {
throw new Error("ENOENT: no such file or directory");
}
}
export function chmodSync(_path, _mode) { return; }
export function chownSync(_path, _uid, _gid) { return; }
export function readlinkSync(path, opts) {
const normalized = __pi_vfs.normalizePath(path);
if (!__pi_vfs.symlinks.has(normalized)) {
if (__pi_vfs.files.has(normalized) || __pi_vfs.dirs.has(normalized)) {
throw new Error(`EINVAL: invalid argument, readlink '${String(path ?? "")}'`);
}
throw new Error(`ENOENT: no such file or directory, readlink '${String(path ?? "")}'`);
}
const target = String(__pi_vfs.symlinks.get(normalized));
const encoding =
typeof opts === "string"
? opts
: opts && typeof opts === "object" && typeof opts.encoding === "string"
? opts.encoding
: undefined;
if (encoding && String(encoding).toLowerCase() === "buffer") {
return Buffer.from(target, "utf8");
}
return target;
}
export function symlinkSync(target, path, _type) {
const normalized = __pi_vfs.normalizePath(path);
__pi_vfs.checkWriteAccess(normalized);
const parent = __pi_vfs.dirname(normalized);
if (!__pi_vfs.dirs.has(parent)) {
throw new Error(`ENOENT: no such file or directory, symlink '${String(path ?? "")}'`);
}
if (__pi_vfs.files.has(normalized) || __pi_vfs.dirs.has(normalized) || __pi_vfs.symlinks.has(normalized)) {
throw new Error(`EEXIST: file already exists, symlink '${String(path ?? "")}'`);
}
__pi_vfs.symlinks.set(normalized, String(target ?? ""));
}
export function openSync(path, flags = "r", _mode) {
const resolved = __pi_vfs.resolvePath(path, true);
const opts = __pi_vfs.parseOpenFlags(flags);
if (opts.writable || opts.create || opts.append || opts.truncate) {
__pi_vfs.checkWriteAccess(resolved);
}
if (__pi_vfs.dirs.has(resolved)) {
throw new Error(`EISDIR: illegal operation on a directory, open '${String(path ?? "")}'`);
}
const exists = __pi_vfs.files.has(resolved);
if (!exists && !opts.create) {
throw new Error(`ENOENT: no such file or directory, open '${String(path ?? "")}'`);
}
if (exists && opts.create && opts.exclusive) {
throw new Error(`EEXIST: file already exists, open '${String(path ?? "")}'`);
}
if (!exists && opts.create) {
__pi_vfs.ensureDir(__pi_vfs.dirname(resolved));
__pi_vfs.files.set(resolved, new Uint8Array());
}
if (opts.truncate && opts.writable) {
__pi_vfs.files.set(resolved, new Uint8Array());
}
const fd = __pi_vfs.nextFd++;
const current = __pi_vfs.files.get(resolved) || new Uint8Array();
__pi_vfs.fds.set(fd, {
path: resolved,
readable: opts.readable,
writable: opts.writable,
append: opts.append,
position: opts.append ? current.byteLength : 0,
});
return fd;
}
export function closeSync(fd) {
if (!__pi_vfs.fds.delete(fd)) {
throw new Error(`EBADF: bad file descriptor, fd ${String(fd)}`);
}
}
export function readSync(fd, buffer, offset = 0, length, position = null) {
const entry = __pi_vfs.getFdEntry(fd);
if (!entry.readable) {
throw new Error(`EBADF: bad file descriptor, fd ${String(fd)}`);
}
const out = __pi_vfs.toWritableView(buffer);
const start = Number.isInteger(offset) && offset >= 0 ? offset : 0;
const maxLen =
Number.isInteger(length) && length >= 0
? length
: Math.max(0, out.byteLength - start);
let cursor =
typeof position === "number" && Number.isFinite(position) && position >= 0
? Math.floor(position)
: entry.position;
const source = __pi_vfs.files.get(entry.path) || new Uint8Array();
if (cursor >= source.byteLength || maxLen <= 0 || start >= out.byteLength) {
return 0;
}
const readLen = Math.min(maxLen, out.byteLength - start, source.byteLength - cursor);
out.set(source.subarray(cursor, cursor + readLen), start);
if (position === null || position === undefined) {
entry.position = cursor + readLen;
}
return readLen;
}
export function writeSync(fd, buffer, offset, length, position) {
const entry = __pi_vfs.getFdEntry(fd);
if (!entry.writable) {
throw new Error(`EBADF: bad file descriptor, fd ${String(fd)}`);
}
let chunk;
let explicitPosition = false;
let cursor = null;
if (typeof buffer === "string") {
const encoding =
typeof length === "string"
? length
: typeof offset === "string"
? offset
: undefined;
chunk = __pi_vfs.toBytes(buffer, encoding);
if (
arguments.length >= 3 &&
typeof offset === "number" &&
Number.isFinite(offset) &&
offset >= 0
) {
explicitPosition = true;
cursor = Math.floor(offset);
}
} else {
const input = __pi_vfs.toWritableView(buffer);
const start = Number.isInteger(offset) && offset >= 0 ? offset : 0;
const maxLen =
Number.isInteger(length) && length >= 0
? length
: Math.max(0, input.byteLength - start);
chunk = input.subarray(start, Math.min(input.byteLength, start + maxLen));
if (typeof position === "number" && Number.isFinite(position) && position >= 0) {
explicitPosition = true;
cursor = Math.floor(position);
}
}
if (!explicitPosition) {
cursor = entry.append
? (__pi_vfs.files.get(entry.path)?.byteLength || 0)
: entry.position;
}
const current = __pi_vfs.files.get(entry.path) || new Uint8Array();
const required = cursor + chunk.byteLength;
const next = new Uint8Array(Math.max(current.byteLength, required));
next.set(current, 0);
next.set(chunk, cursor);
__pi_vfs.files.set(entry.path, next);
if (!explicitPosition) {
entry.position = cursor + chunk.byteLength;
}
return chunk.byteLength;
}
export function fstatSync(fd) {
const entry = __pi_vfs.getFdEntry(fd);
return __pi_vfs.makeStat(entry.path, true);
}
export function ftruncateSync(fd, len = 0) {
const entry = __pi_vfs.getFdEntry(fd);
if (!entry.writable) {
throw new Error(`EBADF: bad file descriptor, fd ${String(fd)}`);
}
const targetLen =
Number.isInteger(len) && len >= 0 ? len : 0;
const current = __pi_vfs.files.get(entry.path) || new Uint8Array();
const next = new Uint8Array(targetLen);
next.set(current.subarray(0, Math.min(current.byteLength, targetLen)));
__pi_vfs.files.set(entry.path, next);
if (entry.position > targetLen) {
entry.position = targetLen;
}
}
export function futimesSync(_fd, _atime, _mtime) { return; }
function __fakeWatcher() {
const w = { close() {}, unref() { return w; }, ref() { return w; }, on() { return w; }, once() { return w; }, removeListener() { return w; }, removeAllListeners() { return w; } };
return w;
}
export function watch(_path, _optsOrListener, _listener) { return __fakeWatcher(); }
export function watchFile(_path, _optsOrListener, _listener) { return __fakeWatcher(); }
export function unwatchFile(_path, _listener) { return; }
function __queueMicrotaskPolyfill(fn) {
if (typeof queueMicrotask === "function") {
queueMicrotask(fn);
return;
}
Promise.resolve().then(fn);
}
export function createReadStream(path, opts) {
const options = opts && typeof opts === "object" ? opts : {};
const encoding = typeof options.encoding === "string" ? options.encoding : null;
const highWaterMark =
Number.isInteger(options.highWaterMark) && options.highWaterMark > 0
? options.highWaterMark
: 64 * 1024;
const stream = new Readable({ encoding: encoding || undefined, autoDestroy: false });
stream.path = __pi_vfs.normalizePath(path);
__queueMicrotaskPolyfill(() => {
try {
const bytes = readFileSync(path, "buffer");
const source =
bytes instanceof Uint8Array
? bytes
: (typeof Buffer !== "undefined" && Buffer.from
? Buffer.from(bytes)
: __pi_vfs.toBytes(bytes));
if (source.byteLength === 0) {
stream.push(null);
return;
}
let offset = 0;
while (offset < source.byteLength) {
const nextOffset = Math.min(source.byteLength, offset + highWaterMark);
const slice = source.subarray(offset, nextOffset);
if (encoding && typeof Buffer !== "undefined" && Buffer.from) {
stream.push(Buffer.from(slice).toString(encoding));
} else {
stream.push(slice);
}
offset = nextOffset;
}
stream.push(null);
} catch (err) {
stream.emit("error", err instanceof Error ? err : new Error(String(err)));
}
});
return stream;
}
export function createWriteStream(path, opts) {
const options = opts && typeof opts === "object" ? opts : {};
const encoding = typeof options.encoding === "string" ? options.encoding : "utf8";
const flags = typeof options.flags === "string" ? options.flags : "w";
const appendMode = flags.startsWith("a");
const bufferedChunks = [];
const stream = new Writable({
autoDestroy: false,
write(chunk, chunkEncoding, callback) {
try {
const normalizedEncoding =
typeof chunkEncoding === "string" && chunkEncoding
? chunkEncoding
: encoding;
const bytes = __pi_vfs.toBytes(chunk, normalizedEncoding);
bufferedChunks.push(bytes);
this.bytesWritten += bytes.byteLength;
callback(null);
} catch (err) {
callback(err instanceof Error ? err : new Error(String(err)));
}
},
final(callback) {
try {
if (appendMode) {
for (const bytes of bufferedChunks) {
appendFileSync(path, bytes);
}
} else {
const totalSize = bufferedChunks.reduce((sum, bytes) => sum + bytes.byteLength, 0);
const merged = new Uint8Array(totalSize);
let offset = 0;
for (const bytes of bufferedChunks) {
merged.set(bytes, offset);
offset += bytes.byteLength;
}
writeFileSync(path, merged);
}
callback(null);
} catch (err) {
callback(err instanceof Error ? err : new Error(String(err)));
}
},
});
stream.path = __pi_vfs.normalizePath(path);
stream.bytesWritten = 0;
stream.cork = () => stream;
stream.uncork = () => stream;
return stream;
}
export function readFile(path, optOrCb, cb) {
const callback = typeof optOrCb === 'function' ? optOrCb : cb;
const encoding = typeof optOrCb === 'function' ? undefined : optOrCb;
if (typeof callback === 'function') {
try { callback(null, readFileSync(path, encoding)); }
catch (err) { callback(err); }
}
}
export function writeFile(path, data, optOrCb, cb) {
const callback = typeof optOrCb === 'function' ? optOrCb : cb;
const opts = typeof optOrCb === 'function' ? undefined : optOrCb;
if (typeof callback === 'function') {
try { writeFileSync(path, data, opts); callback(null); }
catch (err) { callback(err); }
}
}
export function stat(path, optOrCb, cb) {
const callback = typeof optOrCb === 'function' ? optOrCb : cb;
if (typeof callback === 'function') {
try { callback(null, statSync(path)); }
catch (err) { callback(err); }
}
}
export function readdir(path, optOrCb, cb) {
const callback = typeof optOrCb === 'function' ? optOrCb : cb;
const opts = typeof optOrCb === 'function' ? undefined : optOrCb;
if (typeof callback === 'function') {
try { callback(null, readdirSync(path, opts)); }
catch (err) { callback(err); }
}
}
export function mkdir(path, optOrCb, cb) {
const callback = typeof optOrCb === 'function' ? optOrCb : cb;
const opts = typeof optOrCb === 'function' ? undefined : optOrCb;
if (typeof callback === 'function') {
try { callback(null, mkdirSync(path, opts)); }
catch (err) { callback(err); }
}
}
export function unlink(path, cb) {
if (typeof cb === 'function') {
try { unlinkSync(path); cb(null); }
catch (err) { cb(err); }
}
}
export function readlink(path, optOrCb, cb) {
const callback = typeof optOrCb === 'function' ? optOrCb : cb;
const opts = typeof optOrCb === 'function' ? undefined : optOrCb;
if (typeof callback === 'function') {
try { callback(null, readlinkSync(path, opts)); }
catch (err) { callback(err); }
}
}
export function symlink(target, path, typeOrCb, cb) {
const callback = typeof typeOrCb === 'function' ? typeOrCb : cb;
const type = typeof typeOrCb === 'function' ? undefined : typeOrCb;
if (typeof callback === 'function') {
try { symlinkSync(target, path, type); callback(null); }
catch (err) { callback(err); }
}
}
export function lstat(path, optOrCb, cb) {
const callback = typeof optOrCb === 'function' ? optOrCb : cb;
if (typeof callback === 'function') {
try { callback(null, lstatSync(path)); }
catch (err) { callback(err); }
}
}
export function rmdir(path, optOrCb, cb) {
const callback = typeof optOrCb === 'function' ? optOrCb : cb;
const opts = typeof optOrCb === 'function' ? undefined : optOrCb;
if (typeof callback === 'function') {
try { rmdirSync(path, opts); callback(null); }
catch (err) { callback(err); }
}
}
export function rm(path, optOrCb, cb) {
const callback = typeof optOrCb === 'function' ? optOrCb : cb;
const opts = typeof optOrCb === 'function' ? undefined : optOrCb;
if (typeof callback === 'function') {
try { rmSync(path, opts); callback(null); }
catch (err) { callback(err); }
}
}
export function rename(oldPath, newPath, cb) {
if (typeof cb === 'function') {
try { renameSync(oldPath, newPath); cb(null); }
catch (err) { cb(err); }
}
}
export function copyFile(src, dest, flagsOrCb, cb) {
const callback = typeof flagsOrCb === 'function' ? flagsOrCb : cb;
if (typeof callback === 'function') {
try { copyFileSync(src, dest); callback(null); }
catch (err) { callback(err); }
}
}
export function appendFile(path, data, optOrCb, cb) {
const callback = typeof optOrCb === 'function' ? optOrCb : cb;
const opts = typeof optOrCb === 'function' ? undefined : optOrCb;
if (typeof callback === 'function') {
try { appendFileSync(path, data, opts); callback(null); }
catch (err) { callback(err); }
}
}
export function chmod(path, mode, cb) {
if (typeof cb === 'function') {
try { chmodSync(path, mode); cb(null); }
catch (err) { cb(err); }
}
}
export function chown(path, uid, gid, cb) {
if (typeof cb === 'function') {
try { chownSync(path, uid, gid); cb(null); }
catch (err) { cb(err); }
}
}
export function realpath(path, optOrCb, cb) {
const callback = typeof optOrCb === 'function' ? optOrCb : cb;
const opts = typeof optOrCb === 'function' ? undefined : optOrCb;
if (typeof callback === 'function') {
try { callback(null, realpathSync(path, opts)); }
catch (err) { callback(err); }
}
}
export function access(_path, modeOrCb, cb) {
const callback = typeof modeOrCb === 'function' ? modeOrCb : cb;
if (typeof callback === 'function') {
try {
accessSync(_path);
callback(null);
} catch (err) {
callback(err);
}
}
}
export const promises = {
access: async (path, _mode) => accessSync(path),
mkdir: async (path, opts) => mkdirSync(path, opts),
mkdtemp: async (prefix, _opts) => {
return mkdtempSync(prefix, _opts);
},
readFile: async (path, opts) => readFileSync(path, opts),
writeFile: async (path, data, opts) => writeFileSync(path, data, opts),
unlink: async (path) => unlinkSync(path),
readlink: async (path, opts) => readlinkSync(path, opts),
symlink: async (target, path, type) => symlinkSync(target, path, type),
rmdir: async (path, opts) => rmdirSync(path, opts),
stat: async (path) => statSync(path),
lstat: async (path) => lstatSync(path),
realpath: async (path, _opts) => realpathSync(path, _opts),
readdir: async (path, opts) => readdirSync(path, opts),
rm: async (path, opts) => rmSync(path, opts),
rename: async (oldPath, newPath) => renameSync(oldPath, newPath),
copyFile: async (src, dest, mode) => copyFileSync(src, dest, mode),
appendFile: async (path, data, opts) => appendFileSync(path, data, opts),
chmod: async (_path, _mode) => {},
};
export default { constants, existsSync, readFileSync, appendFileSync, writeFileSync, readdirSync, statSync, lstatSync, mkdtempSync, realpathSync, unlinkSync, rmdirSync, rmSync, copyFileSync, renameSync, mkdirSync, accessSync, chmodSync, chownSync, readlinkSync, symlinkSync, openSync, closeSync, readSync, writeSync, fstatSync, ftruncateSync, futimesSync, watch, watchFile, unwatchFile, createReadStream, createWriteStream, readFile, writeFile, stat, lstat, readdir, mkdir, unlink, readlink, symlink, rmdir, rm, rename, copyFile, appendFile, chmod, chown, realpath, access, promises };
"#
.trim()
.to_string(),
);
modules.insert(
"node:fs/promises".to_string(),
r"
import fs from 'node:fs';
export async function access(path, mode) { return fs.promises.access(path, mode); }
export async function mkdir(path, opts) { return fs.promises.mkdir(path, opts); }
export async function mkdtemp(prefix, opts) { return fs.promises.mkdtemp(prefix, opts); }
export async function readFile(path, opts) { return fs.promises.readFile(path, opts); }
export async function writeFile(path, data, opts) { return fs.promises.writeFile(path, data, opts); }
export async function unlink(path) { return fs.promises.unlink(path); }
export async function readlink(path, opts) { return fs.promises.readlink(path, opts); }
export async function symlink(target, path, type) { return fs.promises.symlink(target, path, type); }
export async function rmdir(path, opts) { return fs.promises.rmdir(path, opts); }
export async function stat(path) { return fs.promises.stat(path); }
export async function realpath(path, opts) { return fs.promises.realpath(path, opts); }
export async function readdir(path, opts) { return fs.promises.readdir(path, opts); }
export async function rm(path, opts) { return fs.promises.rm(path, opts); }
export async function lstat(path) { return fs.promises.lstat(path); }
export async function copyFile(src, dest) { return fs.promises.copyFile(src, dest); }
export async function rename(oldPath, newPath) { return fs.promises.rename(oldPath, newPath); }
export async function chmod(path, mode) { return; }
export async function chown(path, uid, gid) { return; }
export async function utimes(path, atime, mtime) { return; }
export async function appendFile(path, data, opts) { return fs.promises.appendFile(path, data, opts); }
export async function open(path, flags, mode) { return { close: async () => {} }; }
export async function truncate(path, len) { return; }
export default { access, mkdir, mkdtemp, readFile, writeFile, unlink, readlink, symlink, rmdir, stat, lstat, realpath, readdir, rm, copyFile, rename, chmod, chown, utimes, appendFile, open, truncate };
"
.trim()
.to_string(),
);
modules.insert(
"node:http".to_string(),
crate::http_shim::NODE_HTTP_JS.trim().to_string(),
);
modules.insert(
"node:https".to_string(),
crate::http_shim::NODE_HTTPS_JS.trim().to_string(),
);
modules.insert(
"node:http2".to_string(),
r#"
import EventEmitter from "node:events";
export const constants = {
HTTP2_HEADER_STATUS: ":status",
HTTP2_HEADER_METHOD: ":method",
HTTP2_HEADER_PATH: ":path",
HTTP2_HEADER_AUTHORITY: ":authority",
HTTP2_HEADER_SCHEME: ":scheme",
HTTP2_HEADER_PROTOCOL: ":protocol",
HTTP2_HEADER_CONTENT_TYPE: "content-type",
NGHTTP2_CANCEL: 8,
};
function __makeStream() {
const stream = new EventEmitter();
stream.end = (_data, _encoding, cb) => {
if (typeof cb === "function") cb();
stream.emit("finish");
};
stream.close = () => stream.emit("close");
stream.destroy = (err) => {
if (err) stream.emit("error", err);
stream.emit("close");
};
stream.respond = () => {};
stream.setEncoding = () => stream;
stream.setTimeout = (_ms, cb) => {
if (typeof cb === "function") cb();
return stream;
};
return stream;
}
function __makeSession() {
const session = new EventEmitter();
session.closed = false;
session.connecting = false;
session.request = (_headers, _opts) => __makeStream();
session.close = () => {
session.closed = true;
session.emit("close");
};
session.destroy = (err) => {
session.closed = true;
if (err) session.emit("error", err);
session.emit("close");
};
session.ref = () => session;
session.unref = () => session;
return session;
}
export function connect(_authority, _options, listener) {
const session = __makeSession();
if (typeof listener === "function") {
try {
listener(session);
} catch (_err) {}
}
return session;
}
export class ClientHttp2Session extends EventEmitter {}
export class ClientHttp2Stream extends EventEmitter {}
export default { connect, constants, ClientHttp2Session, ClientHttp2Stream };
"#
.trim()
.to_string(),
);
modules.insert(
"node:util".to_string(),
r#"
export function inspect(value, opts) {
const depth = (opts && typeof opts.depth === 'number') ? opts.depth : 2;
const seen = new Set();
function fmt(v, d) {
if (v === null) return 'null';
if (v === undefined) return 'undefined';
const t = typeof v;
if (t === 'string') return d > 0 ? "'" + v + "'" : v;
if (t === 'number' || t === 'boolean' || t === 'bigint') return String(v);
if (t === 'symbol') return v.toString();
if (t === 'function') return '[Function: ' + (v.name || 'anonymous') + ']';
if (v instanceof Date) return v.toISOString();
if (v instanceof RegExp) return v.toString();
if (v instanceof Error) return v.stack || v.message || String(v);
if (seen.has(v)) return '[Circular]';
seen.add(v);
if (d > depth) { seen.delete(v); return Array.isArray(v) ? '[Array]' : '[Object]'; }
if (Array.isArray(v)) {
const items = v.map(x => fmt(x, d + 1));
seen.delete(v);
return '[ ' + items.join(', ') + ' ]';
}
const keys = Object.keys(v);
if (keys.length === 0) { seen.delete(v); return '{}'; }
const pairs = keys.map(k => k + ': ' + fmt(v[k], d + 1));
seen.delete(v);
return '{ ' + pairs.join(', ') + ' }';
}
return fmt(value, 0);
}
export function promisify(fn) {
return (...args) => new Promise((resolve, reject) => {
try {
fn(...args, (err, result) => {
if (err) reject(err);
else resolve(result);
});
} catch (e) {
reject(e);
}
});
}
export function stripVTControlCharacters(str) {
// eslint-disable-next-line no-control-regex
return (str || '').replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '').replace(/\x1B\][^\x07]*\x07/g, '');
}
export function deprecate(fn, msg) {
let warned = false;
return function(...args) {
if (!warned) { warned = true; if (typeof console !== 'undefined') console.error('DeprecationWarning: ' + (msg || '')); }
return fn.apply(this, args);
};
}
export function inherits(ctor, superCtor) {
if (!ctor || !superCtor) return ctor;
const ctorProto = ctor && ctor.prototype;
const superProto = superCtor && superCtor.prototype;
if (!ctorProto || !superProto || typeof ctorProto !== 'object' || typeof superProto !== 'object') {
try { ctor.super_ = superCtor; } catch (_) {}
return ctor;
}
try {
Object.setPrototypeOf(ctorProto, superProto);
ctor.super_ = superCtor;
} catch (_) {
try { ctor.super_ = superCtor; } catch (_ignored) {}
}
return ctor;
}
export function debuglog(section) {
const env = (typeof process !== 'undefined' && process.env && process.env.NODE_DEBUG) || '';
const enabled = env.split(',').some(s => s.trim().toLowerCase() === (section || '').toLowerCase());
if (!enabled) return () => {};
return (...args) => { if (typeof console !== 'undefined') console.error(section.toUpperCase() + ': ' + args.map(String).join(' ')); };
}
export function format(f, ...args) {
if (typeof f !== 'string') return [f, ...args].map(v => typeof v === 'string' ? v : inspect(v)).join(' ');
let i = 0;
let result = f.replace(/%[sdifjoO%]/g, (m) => {
if (m === '%%') return '%';
if (i >= args.length) return m;
const a = args[i++];
switch (m) {
case '%s': return String(a);
case '%d': case '%f': return Number(a).toString();
case '%i': return parseInt(a, 10).toString();
case '%j': try { return JSON.stringify(a); } catch { return '[Circular]'; }
case '%o': case '%O': return inspect(a);
default: return m;
}
});
while (i < args.length) result += ' ' + (typeof args[i] === 'string' ? args[i] : inspect(args[i])), i++;
return result;
}
export function callbackify(fn) {
return function(...args) {
const cb = args.pop();
fn(...args).then(r => cb(null, r), e => cb(e));
};
}
export const types = {
isAsyncFunction: (fn) => typeof fn === 'function' && fn.constructor && fn.constructor.name === 'AsyncFunction',
isPromise: (v) => v instanceof Promise,
isDate: (v) => v instanceof Date,
isRegExp: (v) => v instanceof RegExp,
isNativeError: (v) => v instanceof Error,
isSet: (v) => v instanceof Set,
isMap: (v) => v instanceof Map,
isTypedArray: (v) => ArrayBuffer.isView(v) && !(v instanceof DataView),
isArrayBuffer: (v) => v instanceof ArrayBuffer,
isArrayBufferView: (v) => ArrayBuffer.isView(v),
isDataView: (v) => v instanceof DataView,
isGeneratorFunction: (fn) => typeof fn === 'function' && fn.constructor && fn.constructor.name === 'GeneratorFunction',
isGeneratorObject: (v) => v && typeof v.next === 'function' && typeof v.throw === 'function',
isBooleanObject: (v) => typeof v === 'object' && v instanceof Boolean,
isNumberObject: (v) => typeof v === 'object' && v instanceof Number,
isStringObject: (v) => typeof v === 'object' && v instanceof String,
isSymbolObject: () => false,
isWeakMap: (v) => v instanceof WeakMap,
isWeakSet: (v) => v instanceof WeakSet,
};
export const TextEncoder = globalThis.TextEncoder;
export const TextDecoder = globalThis.TextDecoder;
export default { inspect, promisify, stripVTControlCharacters, deprecate, inherits, debuglog, format, callbackify, types, TextEncoder, TextDecoder };
"#
.trim()
.to_string(),
);
modules.insert(
"node:crypto".to_string(),
crate::crypto_shim::NODE_CRYPTO_JS.trim().to_string(),
);
modules.insert(
"node:readline".to_string(),
r"
// Stub readline module - interactive prompts are not available in PiJS
export function createInterface(_opts) {
return {
question: (_query, callback) => {
if (typeof callback === 'function') callback('');
},
close: () => {},
on: () => {},
once: () => {},
};
}
export const promises = {
createInterface: (_opts) => ({
question: async (_query) => '',
close: () => {},
[Symbol.asyncIterator]: async function* () {},
}),
};
export default { createInterface, promises };
"
.trim()
.to_string(),
);
modules.insert(
"node:url".to_string(),
r"
export function fileURLToPath(url) {
const u = String(url ?? '');
if (u.startsWith('file://')) {
let p = decodeURIComponent(u.slice(7));
// file:///C:/... → C:/... (strip leading / before Windows drive letter)
if (p.length >= 3 && p[0] === '/' && p[2] === ':') { p = p.slice(1); }
return p;
}
return u;
}
export function pathToFileURL(path) {
return new URL('file://' + encodeURI(String(path ?? '')));
}
// Use built-in URL if available (QuickJS may have it), else provide polyfill
const _URL = globalThis.URL || (() => {
class URLPolyfill {
constructor(input, base) {
let u = String(input ?? '');
if (base !== undefined) {
const b = String(base);
if (u.startsWith('/')) {
const m = b.match(/^([^:]+:\/\/[^\/]+)/);
u = m ? m[1] + u : b + u;
} else if (!/^[a-z][a-z0-9+.-]*:/i.test(u)) {
u = b.replace(/[^\/]*$/, '') + u;
}
}
this.href = u;
const protoEnd = u.indexOf(':');
this.protocol = protoEnd >= 0 ? u.slice(0, protoEnd + 1) : '';
let rest = protoEnd >= 0 ? u.slice(protoEnd + 1) : u;
this.username = ''; this.password = '';
if (rest.startsWith('//')) {
rest = rest.slice(2);
const pathStart = rest.indexOf('/');
const authority = pathStart >= 0 ? rest.slice(0, pathStart) : rest;
rest = pathStart >= 0 ? rest.slice(pathStart) : '/';
const atIdx = authority.indexOf('@');
let hostPart = authority;
if (atIdx >= 0) {
const userInfo = authority.slice(0, atIdx);
hostPart = authority.slice(atIdx + 1);
const colonIdx = userInfo.indexOf(':');
if (colonIdx >= 0) {
this.username = userInfo.slice(0, colonIdx);
this.password = userInfo.slice(colonIdx + 1);
} else {
this.username = userInfo;
}
}
const portIdx = hostPart.lastIndexOf(':');
if (portIdx >= 0 && /^\d+$/.test(hostPart.slice(portIdx + 1))) {
this.hostname = hostPart.slice(0, portIdx);
this.port = hostPart.slice(portIdx + 1);
} else {
this.hostname = hostPart;
this.port = '';
}
this.host = this.port ? this.hostname + ':' + this.port : this.hostname;
this.origin = this.protocol + '//' + this.host;
} else {
this.hostname = ''; this.host = ''; this.port = '';
this.origin = 'null';
}
const hashIdx = rest.indexOf('#');
if (hashIdx >= 0) {
this.hash = rest.slice(hashIdx);
rest = rest.slice(0, hashIdx);
} else {
this.hash = '';
}
const qIdx = rest.indexOf('?');
if (qIdx >= 0) {
this.search = rest.slice(qIdx);
this.pathname = rest.slice(0, qIdx) || '/';
} else {
this.search = '';
this.pathname = rest || '/';
}
this.searchParams = new _URLSearchParams(this.search.slice(1));
}
toString() { return this.href; }
toJSON() { return this.href; }
}
return URLPolyfill;
})();
// Always use our polyfill — QuickJS built-in URLSearchParams may not support string init
const _URLSearchParams = class URLSearchParamsPolyfill {
constructor(init) {
this._entries = [];
if (typeof init === 'string') {
const s = init.startsWith('?') ? init.slice(1) : init;
if (s) {
for (const pair of s.split('&')) {
const eqIdx = pair.indexOf('=');
if (eqIdx >= 0) {
this._entries.push([decodeURIComponent(pair.slice(0, eqIdx)), decodeURIComponent(pair.slice(eqIdx + 1))]);
} else {
this._entries.push([decodeURIComponent(pair), '']);
}
}
}
}
}
get(key) {
for (const [k, v] of this._entries) { if (k === key) return v; }
return null;
}
set(key, val) {
let found = false;
this._entries = this._entries.filter(([k]) => {
if (k === key && !found) { found = true; return true; }
return k !== key;
});
if (found) {
for (let i = 0; i < this._entries.length; i++) {
if (this._entries[i][0] === key) { this._entries[i][1] = String(val); break; }
}
} else {
this._entries.push([key, String(val)]);
}
}
has(key) { return this._entries.some(([k]) => k === key); }
delete(key) { this._entries = this._entries.filter(([k]) => k !== key); }
append(key, val) { this._entries.push([key, String(val)]); }
getAll(key) { return this._entries.filter(([k]) => k === key).map(([, v]) => v); }
keys() { return this._entries.map(([k]) => k)[Symbol.iterator](); }
values() { return this._entries.map(([, v]) => v)[Symbol.iterator](); }
entries() { return this._entries.slice()[Symbol.iterator](); }
forEach(fn, thisArg) { for (const [k, v] of this._entries) fn.call(thisArg, v, k, this); }
toString() {
return this._entries.map(([k, v]) => encodeURIComponent(k) + '=' + encodeURIComponent(v)).join('&');
}
[Symbol.iterator]() { return this.entries(); }
get size() { return this._entries.length; }
};
export { _URL as URL, _URLSearchParams as URLSearchParams };
export function format(urlObj) {
if (typeof urlObj === 'string') return urlObj;
return urlObj && typeof urlObj.href === 'string' ? urlObj.href : String(urlObj);
}
export function parse(urlStr) {
try { return new _URL(urlStr); } catch (_) { return null; }
}
export function resolve(from, to) {
try { return new _URL(to, from).href; } catch (_) { return to; }
}
export default { URL: _URL, URLSearchParams: _URLSearchParams, fileURLToPath, pathToFileURL, format, parse, resolve };
"
.trim()
.to_string(),
);
modules.insert(
"node:net".to_string(),
r"
// Stub net module - socket operations are not available in PiJS
export function createConnection(_opts, _callback) {
throw new Error('node:net.createConnection is not available in PiJS');
}
export function createServer(_opts, _callback) {
throw new Error('node:net.createServer is not available in PiJS');
}
export function connect(_opts, _callback) {
throw new Error('node:net.connect is not available in PiJS');
}
export function isIP(input) {
const value = String(input ?? '');
if (/^(\d{1,3}\.){3}\d{1,3}$/.test(value)) return 4;
if (/^[0-9a-fA-F:]+$/.test(value) && value.includes(':')) return 6;
return 0;
}
export function isIPv4(input) { return isIP(input) === 4; }
export function isIPv6(input) { return isIP(input) === 6; }
export class Socket {
constructor() {
throw new Error('node:net.Socket is not available in PiJS');
}
}
export class Server {
constructor() {
throw new Error('node:net.Server is not available in PiJS');
}
}
export default { createConnection, createServer, connect, isIP, isIPv4, isIPv6, Socket, Server };
"
.trim()
.to_string(),
);
// ── node:events ──────────────────────────────────────────────────
modules.insert(
"node:events".to_string(),
r"
class EventEmitter {
constructor() {
this._events = Object.create(null);
this._maxListeners = 10;
}
on(event, listener) {
if (!this._events[event]) this._events[event] = [];
this._events[event].push(listener);
return this;
}
addListener(event, listener) { return this.on(event, listener); }
once(event, listener) {
const wrapper = (...args) => {
this.removeListener(event, wrapper);
listener.apply(this, args);
};
wrapper._original = listener;
return this.on(event, wrapper);
}
off(event, listener) { return this.removeListener(event, listener); }
removeListener(event, listener) {
const list = this._events[event];
if (!list) return this;
this._events[event] = list.filter(
fn => fn !== listener && fn._original !== listener
);
if (this._events[event].length === 0) delete this._events[event];
return this;
}
removeAllListeners(event) {
if (event === undefined) {
this._events = Object.create(null);
} else {
delete this._events[event];
}
return this;
}
emit(event, ...args) {
const list = this._events[event];
if (!list || list.length === 0) return false;
for (const fn of list.slice()) {
try { fn.apply(this, args); } catch (e) {
if (event !== 'error') this.emit('error', e);
}
}
return true;
}
listeners(event) {
const list = this._events[event];
if (!list) return [];
return list.map(fn => fn._original || fn);
}
listenerCount(event) {
const list = this._events[event];
return list ? list.length : 0;
}
eventNames() { return Object.keys(this._events); }
setMaxListeners(n) { this._maxListeners = n; return this; }
getMaxListeners() { return this._maxListeners; }
prependListener(event, listener) {
if (!this._events[event]) this._events[event] = [];
this._events[event].unshift(listener);
return this;
}
prependOnceListener(event, listener) {
const wrapper = (...args) => {
this.removeListener(event, wrapper);
listener.apply(this, args);
};
wrapper._original = listener;
return this.prependListener(event, wrapper);
}
rawListeners(event) {
return this._events[event] ? this._events[event].slice() : [];
}
}
EventEmitter.EventEmitter = EventEmitter;
EventEmitter.defaultMaxListeners = 10;
export { EventEmitter };
export default EventEmitter;
"
.trim()
.to_string(),
);
// ── node:buffer ──────────────────────────────────────────────────
modules.insert(
"node:buffer".to_string(),
crate::buffer_shim::NODE_BUFFER_JS.trim().to_string(),
);
// ── node:assert ──────────────────────────────────────────────────
modules.insert(
"node:assert".to_string(),
r"
function assert(value, message) {
if (!value) throw new Error(message || 'Assertion failed');
}
assert.ok = assert;
assert.equal = (a, b, msg) => { if (a != b) throw new Error(msg || `${a} != ${b}`); };
assert.strictEqual = (a, b, msg) => { if (a !== b) throw new Error(msg || `${a} !== ${b}`); };
assert.notEqual = (a, b, msg) => { if (a == b) throw new Error(msg || `${a} == ${b}`); };
assert.notStrictEqual = (a, b, msg) => { if (a === b) throw new Error(msg || `${a} === ${b}`); };
assert.deepEqual = assert.deepStrictEqual = (a, b, msg) => {
if (JSON.stringify(a) !== JSON.stringify(b)) throw new Error(msg || 'Deep equality failed');
};
assert.throws = (fn, _expected, msg) => {
let threw = false;
try { fn(); } catch (_) { threw = true; }
if (!threw) throw new Error(msg || 'Expected function to throw');
};
assert.doesNotThrow = (fn, _expected, msg) => {
try { fn(); } catch (e) { throw new Error(msg || `Got unwanted exception: ${e}`); }
};
assert.fail = (msg) => { throw new Error(msg || 'assert.fail()'); };
export default assert;
export { assert };
"
.trim()
.to_string(),
);
// ── node:stream ──────────────────────────────────────────────────
modules.insert(
"node:stream".to_string(),
r#"
import EventEmitter from "node:events";
function __streamToError(err) {
return err instanceof Error ? err : new Error(String(err ?? "stream error"));
}
function __streamQueueMicrotask(fn) {
if (typeof queueMicrotask === "function") {
queueMicrotask(fn);
return;
}
Promise.resolve().then(fn);
}
function __normalizeChunk(chunk, encoding) {
if (chunk === null || chunk === undefined) return chunk;
if (typeof chunk === "string") return chunk;
if (typeof Buffer !== "undefined" && Buffer.isBuffer && Buffer.isBuffer(chunk)) {
return encoding ? chunk.toString(encoding) : chunk;
}
if (chunk instanceof Uint8Array) {
return encoding && typeof Buffer !== "undefined" && Buffer.from
? Buffer.from(chunk).toString(encoding)
: chunk;
}
if (chunk instanceof ArrayBuffer) {
const view = new Uint8Array(chunk);
return encoding && typeof Buffer !== "undefined" && Buffer.from
? Buffer.from(view).toString(encoding)
: view;
}
if (ArrayBuffer.isView(chunk)) {
const view = new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
return encoding && typeof Buffer !== "undefined" && Buffer.from
? Buffer.from(view).toString(encoding)
: view;
}
return encoding ? String(chunk) : chunk;
}
class Stream extends EventEmitter {
constructor() {
super();
this.destroyed = false;
}
destroy(err) {
if (this.destroyed) return this;
this.destroyed = true;
if (err) this.emit("error", __streamToError(err));
this.emit("close");
return this;
}
}
class Readable extends Stream {
constructor(opts = {}) {
super();
this._readableState = { flowing: null, ended: false, encoding: opts.encoding || null };
this.readable = true;
this._queue = [];
this._pipeCleanup = new Map();
this._autoDestroy = opts.autoDestroy !== false;
}
push(chunk) {
if (chunk === null) {
if (this._readableState.ended) return false;
this._readableState.ended = true;
__streamQueueMicrotask(() => {
this.emit("end");
if (this._autoDestroy) this.emit("close");
});
return false;
}
const normalized = __normalizeChunk(chunk, this._readableState.encoding);
this._queue.push(normalized);
this.emit("data", normalized);
return true;
}
read(_size) {
return this._queue.length > 0 ? this._queue.shift() : null;
}
pipe(dest) {
if (!dest || typeof dest.write !== "function") {
throw new Error("stream.pipe destination must implement write()");
}
const onData = (chunk) => {
const writable = dest.write(chunk);
if (writable === false && typeof this.pause === "function") {
this.pause();
}
};
const onDrain = () => {
if (typeof this.resume === "function") this.resume();
};
const onEnd = () => {
if (typeof dest.end === "function") dest.end();
cleanup();
};
const onError = (err) => {
cleanup();
if (typeof dest.destroy === "function") {
dest.destroy(err);
} else if (typeof dest.emit === "function") {
dest.emit("error", err);
}
};
const cleanup = () => {
this.removeListener("data", onData);
this.removeListener("end", onEnd);
this.removeListener("error", onError);
if (typeof dest.removeListener === "function") {
dest.removeListener("drain", onDrain);
}
this._pipeCleanup.delete(dest);
};
this.on("data", onData);
this.on("end", onEnd);
this.on("error", onError);
if (typeof dest.on === "function") {
dest.on("drain", onDrain);
}
this._pipeCleanup.set(dest, cleanup);
return dest;
}
unpipe(dest) {
if (dest) {
const cleanup = this._pipeCleanup.get(dest);
if (cleanup) cleanup();
return this;
}
for (const cleanup of this._pipeCleanup.values()) {
cleanup();
}
this._pipeCleanup.clear();
return this;
}
resume() {
this._readableState.flowing = true;
return this;
}
pause() {
this._readableState.flowing = false;
return this;
}
[Symbol.asyncIterator]() {
const stream = this;
const queue = [];
const waiters = [];
let done = false;
let failure = null;
const settleDone = () => {
done = true;
while (waiters.length > 0) {
waiters.shift().resolve({ value: undefined, done: true });
}
};
const settleError = (err) => {
failure = __streamToError(err);
while (waiters.length > 0) {
waiters.shift().reject(failure);
}
};
const onData = (value) => {
if (waiters.length > 0) {
waiters.shift().resolve({ value, done: false });
} else {
queue.push(value);
}
};
const onEnd = () => settleDone();
const onError = (err) => settleError(err);
const cleanup = () => {
stream.removeListener("data", onData);
stream.removeListener("end", onEnd);
stream.removeListener("error", onError);
};
stream.on("data", onData);
stream.on("end", onEnd);
stream.on("error", onError);
return {
async next() {
if (queue.length > 0) return { value: queue.shift(), done: false };
if (failure) throw failure;
if (done) return { value: undefined, done: true };
return await new Promise((resolve, reject) => waiters.push({ resolve, reject }));
},
async return() {
cleanup();
settleDone();
return { value: undefined, done: true };
},
[Symbol.asyncIterator]() { return this; },
};
}
static from(iterable, opts = {}) {
const readable = new Readable(opts);
(async () => {
try {
for await (const chunk of iterable) {
readable.push(chunk);
}
readable.push(null);
} catch (err) {
readable.emit("error", __streamToError(err));
}
})();
return readable;
}
static fromWeb(webReadable, opts = {}) {
if (!webReadable || typeof webReadable.getReader !== "function") {
throw new Error("Readable.fromWeb expects a Web ReadableStream");
}
const reader = webReadable.getReader();
const readable = new Readable(opts);
(async () => {
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
readable.push(value);
}
readable.push(null);
} catch (err) {
readable.emit("error", __streamToError(err));
} finally {
try { reader.releaseLock(); } catch (_) {}
}
})();
return readable;
}
static toWeb(nodeReadable) {
if (typeof ReadableStream !== "function") {
throw new Error("Readable.toWeb requires global ReadableStream");
}
if (!nodeReadable || typeof nodeReadable.on !== "function") {
throw new Error("Readable.toWeb expects a Node Readable stream");
}
return new ReadableStream({
start(controller) {
const onData = (chunk) => controller.enqueue(chunk);
const onEnd = () => {
cleanup();
controller.close();
};
const onError = (err) => {
cleanup();
controller.error(__streamToError(err));
};
const cleanup = () => {
nodeReadable.removeListener?.("data", onData);
nodeReadable.removeListener?.("end", onEnd);
nodeReadable.removeListener?.("error", onError);
};
nodeReadable.on("data", onData);
nodeReadable.on("end", onEnd);
nodeReadable.on("error", onError);
if (typeof nodeReadable.resume === "function") nodeReadable.resume();
},
cancel(reason) {
if (typeof nodeReadable.destroy === "function") {
nodeReadable.destroy(__streamToError(reason ?? "stream cancelled"));
}
},
});
}
}
class Writable extends Stream {
constructor(opts = {}) {
super();
this._writableState = { ended: false, finished: false };
this.writable = true;
this._autoDestroy = opts.autoDestroy !== false;
this._writeImpl = typeof opts.write === "function" ? opts.write.bind(this) : null;
this._finalImpl = typeof opts.final === "function" ? opts.final.bind(this) : null;
}
_write(chunk, encoding, callback) {
if (this._writeImpl) {
this._writeImpl(chunk, encoding, callback);
return;
}
callback(null);
}
write(chunk, encoding, callback) {
let cb = callback;
let enc = encoding;
if (typeof encoding === "function") {
cb = encoding;
enc = undefined;
}
if (this._writableState.ended) {
const err = new Error("write after end");
if (typeof cb === "function") cb(err);
this.emit("error", err);
return false;
}
try {
this._write(chunk, enc, (err) => {
if (err) {
const normalized = __streamToError(err);
if (typeof cb === "function") cb(normalized);
this.emit("error", normalized);
return;
}
if (typeof cb === "function") cb(null);
this.emit("drain");
});
} catch (err) {
const normalized = __streamToError(err);
if (typeof cb === "function") cb(normalized);
this.emit("error", normalized);
return false;
}
return true;
}
_finish(callback) {
if (this._finalImpl) {
try {
this._finalImpl(callback);
} catch (err) {
callback(__streamToError(err));
}
return;
}
callback(null);
}
end(chunk, encoding, callback) {
let cb = callback;
let enc = encoding;
if (typeof encoding === "function") {
cb = encoding;
enc = undefined;
}
const finalize = () => {
if (this._writableState.ended) {
if (typeof cb === "function") cb(null);
return;
}
this._writableState.ended = true;
this._finish((err) => {
if (err) {
const normalized = __streamToError(err);
if (typeof cb === "function") cb(normalized);
this.emit("error", normalized);
return;
}
this._writableState.finished = true;
this.emit("finish");
if (this._autoDestroy) this.emit("close");
if (typeof cb === "function") cb(null);
});
};
if (chunk !== undefined && chunk !== null) {
this.write(chunk, enc, (err) => {
if (err) {
if (typeof cb === "function") cb(err);
return;
}
finalize();
});
return this;
}
finalize();
return this;
}
static fromWeb(webWritable, opts = {}) {
if (!webWritable || typeof webWritable.getWriter !== "function") {
throw new Error("Writable.fromWeb expects a Web WritableStream");
}
const writer = webWritable.getWriter();
return new Writable({
...opts,
write(chunk, _encoding, callback) {
Promise.resolve(writer.write(chunk))
.then(() => callback(null))
.catch((err) => callback(__streamToError(err)));
},
final(callback) {
Promise.resolve(writer.close())
.then(() => {
try { writer.releaseLock(); } catch (_) {}
callback(null);
})
.catch((err) => callback(__streamToError(err)));
},
});
}
static toWeb(nodeWritable) {
if (typeof WritableStream !== "function") {
throw new Error("Writable.toWeb requires global WritableStream");
}
if (!nodeWritable || typeof nodeWritable.write !== "function") {
throw new Error("Writable.toWeb expects a Node Writable stream");
}
return new WritableStream({
write(chunk) {
return new Promise((resolve, reject) => {
try {
const ok = nodeWritable.write(chunk, (err) => {
if (err) reject(__streamToError(err));
else resolve();
});
if (ok === true) resolve();
} catch (err) {
reject(__streamToError(err));
}
});
},
close() {
return new Promise((resolve, reject) => {
try {
nodeWritable.end((err) => {
if (err) reject(__streamToError(err));
else resolve();
});
} catch (err) {
reject(__streamToError(err));
}
});
},
abort(reason) {
if (typeof nodeWritable.destroy === "function") {
nodeWritable.destroy(__streamToError(reason ?? "stream aborted"));
}
},
});
}
}
class Duplex extends Readable {
constructor(opts = {}) {
super(opts);
this._writableState = { ended: false, finished: false };
this.writable = true;
this._autoDestroy = opts.autoDestroy !== false;
this._writeImpl = typeof opts.write === "function" ? opts.write.bind(this) : null;
this._finalImpl = typeof opts.final === "function" ? opts.final.bind(this) : null;
}
_write(chunk, encoding, callback) {
if (this._writeImpl) {
this._writeImpl(chunk, encoding, callback);
return;
}
callback(null);
}
_finish(callback) {
if (this._finalImpl) {
try {
this._finalImpl(callback);
} catch (err) {
callback(__streamToError(err));
}
return;
}
callback(null);
}
write(chunk, encoding, callback) {
return Writable.prototype.write.call(this, chunk, encoding, callback);
}
end(chunk, encoding, callback) {
return Writable.prototype.end.call(this, chunk, encoding, callback);
}
}
class Transform extends Duplex {
constructor(opts = {}) {
super(opts);
this._transformImpl = typeof opts.transform === "function" ? opts.transform.bind(this) : null;
}
_transform(chunk, encoding, callback) {
if (this._transformImpl) {
this._transformImpl(chunk, encoding, callback);
return;
}
callback(null, chunk);
}
write(chunk, encoding, callback) {
let cb = callback;
let enc = encoding;
if (typeof encoding === "function") {
cb = encoding;
enc = undefined;
}
try {
this._transform(chunk, enc, (err, data) => {
if (err) {
const normalized = __streamToError(err);
if (typeof cb === "function") cb(normalized);
this.emit("error", normalized);
return;
}
if (data !== undefined && data !== null) {
this.push(data);
}
if (typeof cb === "function") cb(null);
});
} catch (err) {
const normalized = __streamToError(err);
if (typeof cb === "function") cb(normalized);
this.emit("error", normalized);
return false;
}
return true;
}
end(chunk, encoding, callback) {
let cb = callback;
let enc = encoding;
if (typeof encoding === "function") {
cb = encoding;
enc = undefined;
}
const finalize = () => {
this.push(null);
this.emit("finish");
this.emit("close");
if (typeof cb === "function") cb(null);
};
if (chunk !== undefined && chunk !== null) {
this.write(chunk, enc, (err) => {
if (err) {
if (typeof cb === "function") cb(err);
return;
}
finalize();
});
return this;
}
finalize();
return this;
}
}
class PassThrough extends Transform {
_transform(chunk, _encoding, callback) { callback(null, chunk); }
}
function finished(stream, callback) {
if (!stream || typeof stream.on !== "function") {
const err = new Error("finished expects a stream-like object");
if (typeof callback === "function") callback(err);
return Promise.reject(err);
}
return new Promise((resolve, reject) => {
let settled = false;
const cleanup = () => {
stream.removeListener?.("finish", onDone);
stream.removeListener?.("end", onDone);
stream.removeListener?.("close", onDone);
stream.removeListener?.("error", onError);
};
const settle = (fn, value) => {
if (settled) return;
settled = true;
cleanup();
fn(value);
};
const onDone = () => {
if (typeof callback === "function") callback(null, stream);
settle(resolve, stream);
};
const onError = (err) => {
const normalized = __streamToError(err);
if (typeof callback === "function") callback(normalized);
settle(reject, normalized);
};
stream.on("finish", onDone);
stream.on("end", onDone);
stream.on("close", onDone);
stream.on("error", onError);
});
}
function pipeline(...args) {
const callback = typeof args[args.length - 1] === "function" ? args.pop() : null;
const streams = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
if (!Array.isArray(streams) || streams.length < 2) {
const err = new Error("pipeline requires at least two streams");
if (callback) callback(err);
throw err;
}
for (let i = 0; i < streams.length - 1; i += 1) {
streams[i].pipe(streams[i + 1]);
}
const last = streams[streams.length - 1];
const done = (err) => {
if (callback) callback(err || null, last);
};
last.on?.("finish", () => done(null));
last.on?.("end", () => done(null));
last.on?.("error", (err) => done(__streamToError(err)));
return last;
}
const promises = {
pipeline: (...args) =>
new Promise((resolve, reject) => {
try {
pipeline(...args, (err, stream) => {
if (err) reject(err);
else resolve(stream);
});
} catch (err) {
reject(__streamToError(err));
}
}),
finished: (stream) => finished(stream),
};
export { Stream, Readable, Writable, Duplex, Transform, PassThrough, pipeline, finished, promises };
export default { Stream, Readable, Writable, Duplex, Transform, PassThrough, pipeline, finished, promises };
"#
.trim()
.to_string(),
);
// node:stream/promises — promise-based stream utilities
modules.insert(
"node:stream/promises".to_string(),
r"
import { Readable, Writable } from 'node:stream';
function __streamToError(err) {
return err instanceof Error ? err : new Error(String(err ?? 'stream error'));
}
function __isReadableLike(stream) {
return !!stream && typeof stream.pipe === 'function' && typeof stream.on === 'function';
}
function __isWritableLike(stream) {
return !!stream && typeof stream.write === 'function' && typeof stream.on === 'function';
}
export async function pipeline(...streams) {
if (streams.length === 1 && Array.isArray(streams[0])) {
streams = streams[0];
}
if (streams.length < 2) {
throw new Error('pipeline requires at least two streams');
}
if (!__isReadableLike(streams[0]) && streams[0] && (typeof streams[0][Symbol.asyncIterator] === 'function' || typeof streams[0][Symbol.iterator] === 'function')) {
streams = [Readable.from(streams[0]), ...streams.slice(1)];
}
return await new Promise((resolve, reject) => {
let settled = false;
const cleanups = [];
const cleanup = () => {
while (cleanups.length > 0) {
try { cleanups.pop()(); } catch (_) {}
}
};
const settleResolve = (value) => {
if (settled) return;
settled = true;
cleanup();
resolve(value);
};
const settleReject = (err) => {
if (settled) return;
settled = true;
cleanup();
reject(__streamToError(err));
};
const addListener = (target, event, handler) => {
if (!target || typeof target.on !== 'function') return;
target.on(event, handler);
cleanups.push(() => {
if (typeof target.removeListener === 'function') {
target.removeListener(event, handler);
}
});
};
for (let i = 0; i < streams.length - 1; i += 1) {
const source = streams[i];
const dest = streams[i + 1];
if (!__isReadableLike(source)) {
settleReject(new Error(`pipeline source at index ${i} is not readable`));
return;
}
if (!__isWritableLike(dest)) {
settleReject(new Error(`pipeline destination at index ${i + 1} is not writable`));
return;
}
try {
source.pipe(dest);
} catch (err) {
settleReject(err);
return;
}
}
const last = streams[streams.length - 1];
for (const stream of streams) {
addListener(stream, 'error', settleReject);
}
addListener(last, 'finish', () => settleResolve(last));
addListener(last, 'end', () => settleResolve(last));
addListener(last, 'close', () => settleResolve(last));
const first = streams[0];
if (first && typeof first.resume === 'function') {
try { first.resume(); } catch (_) {}
}
});
}
export async function finished(stream) {
if (!stream || typeof stream.on !== 'function') {
throw new Error('finished expects a stream-like object');
}
return await new Promise((resolve, reject) => {
let settled = false;
const cleanup = () => {
if (typeof stream.removeListener !== 'function') return;
stream.removeListener('finish', onDone);
stream.removeListener('end', onDone);
stream.removeListener('close', onDone);
stream.removeListener('error', onError);
};
const onDone = () => {
if (settled) return;
settled = true;
cleanup();
resolve(stream);
};
const onError = (err) => {
if (settled) return;
settled = true;
cleanup();
reject(__streamToError(err));
};
stream.on('finish', onDone);
stream.on('end', onDone);
stream.on('close', onDone);
stream.on('error', onError);
});
}
export default { pipeline, finished };
"
.trim()
.to_string(),
);
// node:stream/web — bridge to global Web Streams when available
modules.insert(
"node:stream/web".to_string(),
r"
const _ReadableStream = globalThis.ReadableStream;
const _WritableStream = globalThis.WritableStream;
const _TransformStream = globalThis.TransformStream;
const _TextEncoderStream = globalThis.TextEncoderStream;
const _TextDecoderStream = globalThis.TextDecoderStream;
const _CompressionStream = globalThis.CompressionStream;
const _DecompressionStream = globalThis.DecompressionStream;
const _ByteLengthQueuingStrategy = globalThis.ByteLengthQueuingStrategy;
const _CountQueuingStrategy = globalThis.CountQueuingStrategy;
export const ReadableStream = _ReadableStream;
export const WritableStream = _WritableStream;
export const TransformStream = _TransformStream;
export const TextEncoderStream = _TextEncoderStream;
export const TextDecoderStream = _TextDecoderStream;
export const CompressionStream = _CompressionStream;
export const DecompressionStream = _DecompressionStream;
export const ByteLengthQueuingStrategy = _ByteLengthQueuingStrategy;
export const CountQueuingStrategy = _CountQueuingStrategy;
export default {
ReadableStream,
WritableStream,
TransformStream,
TextEncoderStream,
TextDecoderStream,
CompressionStream,
DecompressionStream,
ByteLengthQueuingStrategy,
CountQueuingStrategy,
};
"
.trim()
.to_string(),
);
// node:string_decoder — often imported by stream consumers
modules.insert(
"node:string_decoder".to_string(),
r"
export class StringDecoder {
constructor(encoding) { this.encoding = encoding || 'utf8'; }
write(buf) { return typeof buf === 'string' ? buf : String(buf ?? ''); }
end(buf) { return buf ? this.write(buf) : ''; }
}
export default { StringDecoder };
"
.trim()
.to_string(),
);
// node:querystring — URL query string encoding/decoding
modules.insert(
"node:querystring".to_string(),
r"
export function parse(qs, sep, eq) {
const s = String(qs ?? '');
const sepStr = sep || '&';
const eqStr = eq || '=';
const result = {};
if (!s) return result;
for (const pair of s.split(sepStr)) {
const idx = pair.indexOf(eqStr);
const key = idx === -1 ? decodeURIComponent(pair) : decodeURIComponent(pair.slice(0, idx));
const val = idx === -1 ? '' : decodeURIComponent(pair.slice(idx + eqStr.length));
if (Object.prototype.hasOwnProperty.call(result, key)) {
if (Array.isArray(result[key])) result[key].push(val);
else result[key] = [result[key], val];
} else {
result[key] = val;
}
}
return result;
}
export function stringify(obj, sep, eq) {
const sepStr = sep || '&';
const eqStr = eq || '=';
if (!obj || typeof obj !== 'object') return '';
return Object.entries(obj).map(([k, v]) => {
if (Array.isArray(v)) return v.map(i => encodeURIComponent(k) + eqStr + encodeURIComponent(i)).join(sepStr);
return encodeURIComponent(k) + eqStr + encodeURIComponent(v ?? '');
}).join(sepStr);
}
export const decode = parse;
export const encode = stringify;
export function escape(str) { return encodeURIComponent(str); }
export function unescape(str) { return decodeURIComponent(str); }
export default { parse, stringify, decode, encode, escape, unescape };
"
.trim()
.to_string(),
);
// node:constants — compatibility map for libraries probing process constants
modules.insert(
"node:constants".to_string(),
r"
const _constants = {
EOL: '\n',
F_OK: 0,
R_OK: 4,
W_OK: 2,
X_OK: 1,
UV_UDP_REUSEADDR: 4,
SSL_OP_NO_SSLv2: 0,
SSL_OP_NO_SSLv3: 0,
SSL_OP_NO_TLSv1: 0,
SSL_OP_NO_TLSv1_1: 0,
};
const constants = new Proxy(_constants, {
get(target, prop) {
if (prop in target) return target[prop];
return 0;
},
});
export default constants;
export { constants };
"
.trim()
.to_string(),
);
// node:tty — terminal capability probes
modules.insert(
"node:tty".to_string(),
r"
import EventEmitter from 'node:events';
export function isatty(_fd) { return false; }
export class ReadStream extends EventEmitter {
constructor(_fd) {
super();
this.isTTY = false;
this.columns = 80;
this.rows = 24;
}
setRawMode(_mode) { return this; }
}
export class WriteStream extends EventEmitter {
constructor(_fd) {
super();
this.isTTY = false;
this.columns = 80;
this.rows = 24;
}
getColorDepth() { return 1; }
hasColors() { return false; }
getWindowSize() { return [this.columns, this.rows]; }
}
export default { isatty, ReadStream, WriteStream };
"
.trim()
.to_string(),
);
// node:tls — secure socket APIs are intentionally unavailable in PiJS
modules.insert(
"node:tls".to_string(),
r"
import EventEmitter from 'node:events';
export const DEFAULT_MIN_VERSION = 'TLSv1.2';
export const DEFAULT_MAX_VERSION = 'TLSv1.3';
export class TLSSocket extends EventEmitter {
constructor(_socket, _options) {
super();
this.authorized = false;
this.encrypted = true;
}
}
export function connect(_portOrOptions, _host, _options, _callback) {
throw new Error('node:tls.connect is not available in PiJS');
}
export function createServer(_options, _secureConnectionListener) {
throw new Error('node:tls.createServer is not available in PiJS');
}
export default { connect, createServer, TLSSocket, DEFAULT_MIN_VERSION, DEFAULT_MAX_VERSION };
"
.trim()
.to_string(),
);
// node:zlib — compression streams are not implemented in PiJS
modules.insert(
"node:zlib".to_string(),
r"
const constants = {
Z_NO_COMPRESSION: 0,
Z_BEST_SPEED: 1,
Z_BEST_COMPRESSION: 9,
Z_DEFAULT_COMPRESSION: -1,
};
function unsupported(name) {
throw new Error(`node:zlib.${name} is not available in PiJS`);
}
export function gzip(_buffer, callback) {
if (typeof callback === 'function') callback(new Error('node:zlib.gzip is not available in PiJS'));
}
export function gunzip(_buffer, callback) {
if (typeof callback === 'function') callback(new Error('node:zlib.gunzip is not available in PiJS'));
}
export function createGzip() { unsupported('createGzip'); }
export function createGunzip() { unsupported('createGunzip'); }
export function createDeflate() { unsupported('createDeflate'); }
export function createInflate() { unsupported('createInflate'); }
export function createBrotliCompress() { unsupported('createBrotliCompress'); }
export function createBrotliDecompress() { unsupported('createBrotliDecompress'); }
export const promises = {
gzip: async () => { unsupported('promises.gzip'); },
gunzip: async () => { unsupported('promises.gunzip'); },
};
export default {
constants,
gzip,
gunzip,
createGzip,
createGunzip,
createDeflate,
createInflate,
createBrotliCompress,
createBrotliDecompress,
promises,
};
"
.trim()
.to_string(),
);
// node:perf_hooks — expose lightweight performance clock surface
modules.insert(
"node:perf_hooks".to_string(),
r"
const perf =
globalThis.performance ||
{
now: () => Date.now(),
mark: () => {},
measure: () => {},
clearMarks: () => {},
clearMeasures: () => {},
getEntries: () => [],
getEntriesByType: () => [],
getEntriesByName: () => [],
};
export const performance = perf;
export const constants = {};
export class PerformanceObserver {
constructor(_callback) {}
observe(_opts) {}
disconnect() {}
}
export default { performance, constants, PerformanceObserver };
"
.trim()
.to_string(),
);
// node:vm — disabled in PiJS for safety
modules.insert(
"node:vm".to_string(),
r"
function unsupported(name) {
throw new Error(`node:vm.${name} is not available in PiJS`);
}
export function runInContext() { unsupported('runInContext'); }
export function runInNewContext() { unsupported('runInNewContext'); }
export function runInThisContext() { unsupported('runInThisContext'); }
export function createContext(_sandbox) { return _sandbox || {}; }
export class Script {
constructor(_code, _options) { unsupported('Script'); }
}
export default { runInContext, runInNewContext, runInThisContext, createContext, Script };
"
.trim()
.to_string(),
);
// node:v8 — lightweight serialization fallback used by some libs
modules.insert(
"node:v8".to_string(),
r"
function __toBuffer(str) {
if (typeof Buffer !== 'undefined' && typeof Buffer.from === 'function') {
return Buffer.from(str, 'utf8');
}
if (typeof TextEncoder !== 'undefined') {
return new TextEncoder().encode(str);
}
return str;
}
function __fromBuffer(buf) {
if (buf == null) return '';
if (typeof Buffer !== 'undefined' && typeof Buffer.isBuffer === 'function' && Buffer.isBuffer(buf)) {
return buf.toString('utf8');
}
if (buf instanceof Uint8Array && typeof TextDecoder !== 'undefined') {
return new TextDecoder().decode(buf);
}
return String(buf);
}
export function serialize(value) {
return __toBuffer(JSON.stringify(value));
}
export function deserialize(value) {
return JSON.parse(__fromBuffer(value));
}
export default { serialize, deserialize };
"
.trim()
.to_string(),
);
// node:worker_threads — workers are not supported in PiJS
modules.insert(
"node:worker_threads".to_string(),
r"
export const isMainThread = true;
export const threadId = 0;
export const workerData = null;
export const parentPort = null;
export class Worker {
constructor(_filename, _options) {
throw new Error('node:worker_threads.Worker is not available in PiJS');
}
}
export default { isMainThread, threadId, workerData, parentPort, Worker };
"
.trim()
.to_string(),
);
// node:process — re-exports globalThis.process
modules.insert(
"node:process".to_string(),
r"
const p = globalThis.process || {};
export const env = p.env || {};
export const argv = p.argv || [];
export const cwd = typeof p.cwd === 'function' ? p.cwd : () => '/';
export const chdir = typeof p.chdir === 'function' ? p.chdir : () => { throw new Error('ENOSYS'); };
export const platform = p.platform || 'linux';
export const arch = p.arch || 'x64';
export const version = p.version || 'v20.0.0';
export const versions = p.versions || {};
export const pid = p.pid || 1;
export const ppid = p.ppid || 0;
export const title = p.title || 'pi';
export const execPath = p.execPath || '/usr/bin/pi';
export const execArgv = p.execArgv || [];
export const stdout = p.stdout || { write() {} };
export const stderr = p.stderr || { write() {} };
export const stdin = p.stdin || {};
export const nextTick = p.nextTick || ((fn, ...a) => Promise.resolve().then(() => fn(...a)));
export const hrtime = p.hrtime || Object.assign(() => [0, 0], { bigint: () => BigInt(0) });
export const exit = p.exit || (() => {});
export const kill = p.kill || (() => {});
export const on = p.on || (() => p);
export const off = p.off || (() => p);
export const once = p.once || (() => p);
export const addListener = p.addListener || (() => p);
export const removeListener = p.removeListener || (() => p);
export const removeAllListeners = p.removeAllListeners || (() => p);
export const listeners = p.listeners || (() => []);
export const emit = p.emit || (() => false);
export const emitWarning = p.emitWarning || (() => {});
export const uptime = p.uptime || (() => 0);
export const memoryUsage = p.memoryUsage || (() => ({ rss: 0, heapTotal: 0, heapUsed: 0, external: 0, arrayBuffers: 0 }));
export const cpuUsage = p.cpuUsage || (() => ({ user: 0, system: 0 }));
export const release = p.release || { name: 'node' };
export default p;
"
.trim()
.to_string(),
);
// ── npm package stubs ──────────────────────────────────────────────
// Minimal virtual modules for npm packages that cannot run in the
// QuickJS sandbox (native bindings, large dependency trees, or
// companion packages). These stubs let extensions *load* and register
// tools/commands even though the actual library behaviour is absent.
modules.insert(
"@mariozechner/clipboard".to_string(),
r"
export async function getText() { return ''; }
export async function setText(_text) {}
export default { getText, setText };
"
.trim()
.to_string(),
);
modules.insert(
"node-pty".to_string(),
r"
let _pid = 1000;
export function spawn(shell, args, options) {
const pid = _pid++;
const handlers = {};
return {
pid,
onData(cb) { handlers.data = cb; },
onExit(cb) { if (cb) setTimeout(() => cb({ exitCode: 1, signal: undefined }), 0); },
write(d) {},
resize(c, r) {},
kill(s) {},
};
}
export default { spawn };
"
.trim()
.to_string(),
);
modules.insert(
"chokidar".to_string(),
r"
function makeWatcher() {
const w = {
on(ev, cb) { return w; },
once(ev, cb) { return w; },
close() { return Promise.resolve(); },
add(p) { return w; },
unwatch(p) { return w; },
getWatched() { return {}; },
};
return w;
}
export function watch(paths, options) { return makeWatcher(); }
export default { watch };
"
.trim()
.to_string(),
);
modules.insert(
"jsdom".to_string(),
r"
class Element {
constructor(tag, html) { this.tagName = tag; this._html = html || ''; this.childNodes = []; }
get innerHTML() { return this._html; }
set innerHTML(v) { this._html = v; }
get textContent() { return this._html.replace(/<[^>]*>/g, ''); }
get outerHTML() { return `<${this.tagName}>${this._html}</${this.tagName}>`; }
get parentNode() { return null; }
querySelectorAll() { return []; }
querySelector() { return null; }
getElementsByTagName() { return []; }
getElementById() { return null; }
remove() {}
getAttribute() { return null; }
setAttribute() {}
cloneNode() { return new Element(this.tagName, this._html); }
}
export class JSDOM {
constructor(html, opts) {
const doc = new Element('html', html || '');
doc.body = new Element('body', html || '');
doc.title = '';
doc.querySelectorAll = () => [];
doc.querySelector = () => null;
doc.getElementsByTagName = () => [];
doc.getElementById = () => null;
doc.createElement = (t) => new Element(t, '');
doc.documentElement = doc;
this.window = { document: doc, location: { href: (opts && opts.url) || '' } };
}
}
"
.trim()
.to_string(),
);
modules.insert(
"@mozilla/readability".to_string(),
r"
export class Readability {
constructor(doc, opts) { this._doc = doc; }
parse() {
const text = (this._doc && this._doc.body && this._doc.body.textContent) || '';
return { title: '', content: text, textContent: text, length: text.length, excerpt: '', byline: '', dir: '', siteName: '', lang: '' };
}
}
"
.trim()
.to_string(),
);
modules.insert(
"beautiful-mermaid".to_string(),
r"
export function renderMermaidAscii(source) {
const firstLine = (source || '').split('\n')[0] || 'diagram';
return '[mermaid: ' + firstLine.trim() + ']';
}
"
.trim()
.to_string(),
);
modules.insert(
"@aliou/pi-utils-settings".to_string(),
r"
export class ConfigLoader {
constructor(name, defaultConfig, options) {
this._name = name;
this._default = defaultConfig || {};
this._opts = options || {};
this._data = structuredClone(this._default);
}
async load() { return this._data; }
save(d) { this._data = d; }
get() { return this._data; }
getConfig() { return this._data; }
set(k, v) { this._data[k] = v; }
}
export class ArrayEditor {
constructor(arr) { this._arr = arr || []; }
add(item) { this._arr.push(item); return this; }
remove(idx) { this._arr.splice(idx, 1); return this; }
toArray() { return this._arr; }
}
export function registerSettingsCommand(pi, opts) {}
export function getNestedValue(obj, path) {
const keys = (path || '').split('.');
let cur = obj;
for (const k of keys) { if (cur == null) return undefined; cur = cur[k]; }
return cur;
}
export function setNestedValue(obj, path, value) {
const keys = (path || '').split('.');
let cur = obj;
for (let i = 0; i < keys.length - 1; i++) {
if (cur[keys[i]] == null) cur[keys[i]] = {};
cur = cur[keys[i]];
}
cur[keys[keys.length - 1]] = value;
}
"
.trim()
.to_string(),
);
modules.insert(
"@aliou/sh".to_string(),
r#"
export function parse(cmd) { return [{ type: 'command', value: cmd }]; }
export function tokenize(cmd) { return (cmd || '').split(/\s+/); }
export function quote(s) { return "'" + (s || '').replace(/'/g, "'\\''") + "'"; }
export class ParseError extends Error { constructor(msg) { super(msg); this.name = 'ParseError'; } }
"#
.trim()
.to_string(),
);
modules.insert(
"@marckrenn/pi-sub-shared".to_string(),
r#"
export const PROVIDERS = ["anthropic", "openai", "google", "aws", "azure"];
export const MODEL_MULTIPLIERS = {};
const _meta = (name) => ({
name, displayName: name.charAt(0).toUpperCase() + name.slice(1),
detection: { envVars: [], configPaths: [] },
status: { operational: true },
});
export const PROVIDER_METADATA = Object.fromEntries(PROVIDERS.map(p => [p, _meta(p)]));
export const PROVIDER_DISPLAY_NAMES = Object.fromEntries(
PROVIDERS.map(p => [p, p.charAt(0).toUpperCase() + p.slice(1)])
);
export function getDefaultCoreSettings() {
return { providers: {}, behavior: { autoSwitch: false } };
}
"#
.trim()
.to_string(),
);
modules.insert(
"turndown".to_string(),
r"
class TurndownService {
constructor(opts) { this._opts = opts || {}; }
turndown(html) { return (html || '').replace(/<[^>]*>/g, ''); }
addRule(name, rule) { return this; }
use(plugin) { return this; }
remove(filter) { return this; }
}
export default TurndownService;
"
.trim()
.to_string(),
);
modules.insert(
"@xterm/headless".to_string(),
r"
export class Terminal {
constructor(opts) { this._opts = opts || {}; this.cols = opts?.cols || 80; this.rows = opts?.rows || 24; this.buffer = { active: { cursorX: 0, cursorY: 0, length: 0, getLine: () => null } }; }
write(data) {}
writeln(data) {}
resize(cols, rows) { this.cols = cols; this.rows = rows; }
dispose() {}
onData(cb) { return { dispose() {} }; }
onLineFeed(cb) { return { dispose() {} }; }
}
export default { Terminal };
"
.trim()
.to_string(),
);
modules.insert(
"@opentelemetry/api".to_string(),
r"
export const SpanStatusCode = { UNSET: 0, OK: 1, ERROR: 2 };
const noopSpan = {
setAttribute() { return this; },
setAttributes() { return this; },
addEvent() { return this; },
setStatus() { return this; },
end() {},
isRecording() { return false; },
recordException() {},
spanContext() { return { traceId: '', spanId: '', traceFlags: 0 }; },
};
const noopTracer = {
startSpan() { return noopSpan; },
startActiveSpan(name, optsOrFn, fn) {
const cb = typeof optsOrFn === 'function' ? optsOrFn : fn;
return cb ? cb(noopSpan) : noopSpan;
},
};
export const trace = {
getTracer() { return noopTracer; },
getActiveSpan() { return noopSpan; },
setSpan(ctx) { return ctx; },
};
export const context = {
active() { return {}; },
with(ctx, fn) { return fn(); },
};
"
.trim()
.to_string(),
);
modules.insert(
"@juanibiapina/pi-extension-settings".to_string(),
r"
export function getSetting(pi, key, defaultValue) { return defaultValue; }
export function setSetting(pi, key, value) {}
export function getSettings(pi) { return {}; }
"
.trim()
.to_string(),
);
modules.insert(
"@xterm/addon-serialize".to_string(),
r"
export class SerializeAddon {
activate(terminal) {}
serialize(opts) { return ''; }
dispose() {}
}
"
.trim()
.to_string(),
);
modules.insert(
"turndown-plugin-gfm".to_string(),
r"
export function gfm(service) {}
export function tables(service) {}
export function strikethrough(service) {}
export function taskListItems(service) {}
"
.trim()
.to_string(),
);
modules.insert(
"@opentelemetry/exporter-trace-otlp-http".to_string(),
r"
export class OTLPTraceExporter {
constructor(opts) { this._opts = opts || {}; }
export(spans, cb) { if (cb) cb({ code: 0 }); }
shutdown() { return Promise.resolve(); }
}
"
.trim()
.to_string(),
);
modules.insert(
"@opentelemetry/resources".to_string(),
r"
export class Resource {
constructor(attrs) { this.attributes = attrs || {}; }
merge(other) { return new Resource({ ...this.attributes, ...(other?.attributes || {}) }); }
}
export function resourceFromAttributes(attrs) { return new Resource(attrs); }
"
.trim()
.to_string(),
);
modules.insert(
"@opentelemetry/sdk-trace-base".to_string(),
r"
const noopSpan = { setAttribute() { return this; }, end() {}, isRecording() { return false; }, spanContext() { return {}; } };
export class BasicTracerProvider {
constructor(opts) { this._opts = opts || {}; }
addSpanProcessor(p) {}
register() {}
getTracer() { return { startSpan() { return noopSpan; }, startActiveSpan(n, fn) { return fn(noopSpan); } }; }
shutdown() { return Promise.resolve(); }
}
export class SimpleSpanProcessor {
constructor(exporter) {}
onStart() {}
onEnd() {}
shutdown() { return Promise.resolve(); }
forceFlush() { return Promise.resolve(); }
}
export class BatchSpanProcessor extends SimpleSpanProcessor {}
"
.trim()
.to_string(),
);
modules.insert(
"@opentelemetry/semantic-conventions".to_string(),
r"
export const SemanticResourceAttributes = {
SERVICE_NAME: 'service.name',
SERVICE_VERSION: 'service.version',
DEPLOYMENT_ENVIRONMENT: 'deployment.environment',
};
export const SEMRESATTRS_SERVICE_NAME = 'service.name';
export const SEMRESATTRS_SERVICE_VERSION = 'service.version';
"
.trim()
.to_string(),
);
// ── npm package stubs for extension conformance ──
{
let openclaw_plugin_sdk = r#"
export function definePlugin(spec = {}) { return spec; }
export function createPlugin(spec = {}) { return spec; }
export function tool(spec = {}) { return { ...spec, type: "tool" }; }
export function command(spec = {}) { return { ...spec, type: "command" }; }
export function provider(spec = {}) { return { ...spec, type: "provider" }; }
export const DEFAULT_ACCOUNT_ID = "default";
const __schema = {
parse(value) { return value; },
safeParse(value) { return { success: true, data: value }; },
optional() { return this; },
nullable() { return this; },
default() { return this; },
array() { return this; },
transform() { return this; },
refine() { return this; },
};
export const emptyPluginConfigSchema = __schema;
export function createReplyPrefixContext() { return {}; }
export function stringEnum(values = []) { return values[0] ?? ""; }
export function getChatChannelMeta() { return {}; }
export function addWildcardAllowFrom() { return []; }
export function listFeishuAccountIds() { return []; }
export function normalizeAccountId(value) { return String(value ?? ""); }
export function jsonResult(value) {
return {
content: [{ type: "text", text: JSON.stringify(value ?? null) }],
details: { value },
};
}
export function stripAnsi(value) {
return String(value ?? "").replace(/\u001b\[[0-9;]*m/g, "");
}
export function recordInboundSession() { return undefined; }
export class OpenClawPlugin {
constructor(spec = {}) { this.spec = spec; }
async activate(pi) {
const plugin = this.spec || {};
if (Array.isArray(plugin.tools)) {
for (const t of plugin.tools) {
if (!t || !t.name) continue;
const execute = typeof t.execute === "function" ? t.execute : async () => ({ content: [] });
pi.registerTool?.({ ...t, execute });
}
}
if (Array.isArray(plugin.commands)) {
for (const c of plugin.commands) {
if (!c || !c.name) continue;
const handler = typeof c.handler === "function" ? c.handler : async () => ({});
pi.registerCommand?.(c.name, { ...c, handler });
}
}
if (typeof plugin.activate === "function") {
await plugin.activate(pi);
}
}
}
export async function registerOpenClaw(pi, plugin) {
if (typeof plugin === "function") {
return await plugin(pi);
}
if (plugin && typeof plugin.default === "function") {
return await plugin.default(pi);
}
if (plugin && typeof plugin.activate === "function") {
return await plugin.activate(pi);
}
return undefined;
}
export default {
definePlugin,
createPlugin,
tool,
command,
provider,
DEFAULT_ACCOUNT_ID,
emptyPluginConfigSchema,
createReplyPrefixContext,
stringEnum,
getChatChannelMeta,
addWildcardAllowFrom,
listFeishuAccountIds,
normalizeAccountId,
jsonResult,
stripAnsi,
recordInboundSession,
registerOpenClaw,
OpenClawPlugin,
};
"#
.trim()
.to_string();
modules.insert(
"openclaw/plugin-sdk".to_string(),
openclaw_plugin_sdk.clone(),
);
modules.insert(
"openclaw/plugin-sdk/index.js".to_string(),
openclaw_plugin_sdk.clone(),
);
modules.insert(
"clawdbot/plugin-sdk".to_string(),
openclaw_plugin_sdk.clone(),
);
modules.insert(
"clawdbot/plugin-sdk/index.js".to_string(),
openclaw_plugin_sdk,
);
}
modules.insert(
"zod".to_string(),
r"
const __schema = {
parse(value) { return value; },
safeParse(value) { return { success: true, data: value }; },
optional() { return this; },
nullable() { return this; },
nullish() { return this; },
default() { return this; },
array() { return this; },
transform() { return this; },
refine() { return this; },
describe() { return this; },
min() { return this; },
max() { return this; },
length() { return this; },
regex() { return this; },
url() { return this; },
email() { return this; },
uuid() { return this; },
int() { return this; },
positive() { return this; },
nonnegative() { return this; },
nonempty() { return this; },
};
function makeSchema() { return Object.create(__schema); }
export const z = {
string() { return makeSchema(); },
number() { return makeSchema(); },
boolean() { return makeSchema(); },
object() { return makeSchema(); },
array() { return makeSchema(); },
enum() { return makeSchema(); },
literal() { return makeSchema(); },
union() { return makeSchema(); },
intersection() { return makeSchema(); },
record() { return makeSchema(); },
any() { return makeSchema(); },
unknown() { return makeSchema(); },
null() { return makeSchema(); },
undefined() { return makeSchema(); },
optional(inner) { return inner ?? makeSchema(); },
nullable(inner) { return inner ?? makeSchema(); },
};
export default z;
"
.trim()
.to_string(),
);
modules.insert(
"yaml".to_string(),
r##"
export function parse(input) {
const text = String(input ?? "").trim();
if (!text) return {};
const out = {};
for (const rawLine of text.split(/\r?\n/)) {
const line = rawLine.trim();
if (!line || line.startsWith("#")) continue;
const idx = line.indexOf(":");
if (idx === -1) continue;
const key = line.slice(0, idx).trim();
const value = line.slice(idx + 1).trim();
if (key) out[key] = value;
}
return out;
}
export function stringify(value) {
if (!value || typeof value !== "object") return "";
const lines = Object.entries(value).map(([k, v]) => `${k}: ${v ?? ""}`);
return lines.length ? `${lines.join("\n")}\n` : "";
}
export default { parse, stringify };
"##
.trim()
.to_string(),
);
modules.insert(
"better-sqlite3".to_string(),
r#"
class Statement {
all() { return []; }
get() { return undefined; }
run() { return { changes: 0, lastInsertRowid: 0 }; }
}
function BetterSqlite3(filename, options = {}) {
if (!(this instanceof BetterSqlite3)) return new BetterSqlite3(filename, options);
this.filename = String(filename ?? "");
this.options = options;
}
BetterSqlite3.prototype.prepare = function(_sql) { return new Statement(); };
BetterSqlite3.prototype.exec = function(_sql) { return this; };
BetterSqlite3.prototype.pragma = function(_sql) { return []; };
BetterSqlite3.prototype.transaction = function(fn) {
const wrapped = (...args) => (typeof fn === "function" ? fn(...args) : undefined);
wrapped.immediate = wrapped;
wrapped.deferred = wrapped;
wrapped.exclusive = wrapped;
return wrapped;
};
BetterSqlite3.prototype.close = function() {};
BetterSqlite3.Statement = Statement;
BetterSqlite3.Database = BetterSqlite3;
export { Statement };
export default BetterSqlite3;
"#
.trim()
.to_string(),
);
modules.insert(
"@mariozechner/pi-agent-core".to_string(),
r#"
export const ThinkingLevel = {
low: "low",
medium: "medium",
high: "high",
};
export class AgentTool {}
export default { ThinkingLevel, AgentTool };
"#
.trim()
.to_string(),
);
modules.insert(
"@mariozechner/pi-agent-core/index.js".to_string(),
r#"
export const ThinkingLevel = {
low: "low",
medium: "medium",
high: "high",
};
export class AgentTool {}
export default { ThinkingLevel, AgentTool };
"#
.trim()
.to_string(),
);
modules.insert(
"openai".to_string(),
r#"
class OpenAI {
constructor(config = {}) { this.config = config; }
get chat() {
return { completions: { create: async () => ({ choices: [{ message: { content: "" } }] }) } };
}
}
export default OpenAI;
export { OpenAI };
"#
.trim()
.to_string(),
);
modules.insert(
"adm-zip".to_string(),
r#"
class AdmZip {
constructor(path) { this.path = path; this.entries = []; }
getEntries() { return this.entries; }
readAsText() { return ""; }
extractAllTo() {}
addFile() {}
writeZip() {}
}
export default AdmZip;
"#
.trim()
.to_string(),
);
modules.insert(
"linkedom".to_string(),
r#"
export function parseHTML(html) {
const doc = {
documentElement: { outerHTML: html || "" },
querySelector: () => null,
querySelectorAll: () => [],
createElement: (tag) => ({ tagName: tag, textContent: "", innerHTML: "", children: [], appendChild() {} }),
body: { textContent: "", innerHTML: "", children: [] },
title: "",
};
return { document: doc, window: { document: doc } };
}
"#
.trim()
.to_string(),
);
modules.insert(
"@sourcegraph/scip-typescript".to_string(),
r"
export const scip = { Index: class {} };
export default { scip };
"
.trim()
.to_string(),
);
modules.insert(
"p-limit".to_string(),
r"
export default function pLimit(concurrency) {
const queue = [];
let active = 0;
const next = () => {
active--;
if (queue.length > 0) queue.shift()();
};
const run = async (fn, resolve, ...args) => {
active++;
const result = (async () => fn(...args))();
resolve(result);
try { await result; } catch {}
next();
};
const enqueue = (fn, resolve, ...args) => {
queue.push(run.bind(null, fn, resolve, ...args));
(async () => { if (active < concurrency && queue.length > 0) queue.shift()(); })();
};
const generator = (fn, ...args) => new Promise(resolve => enqueue(fn, resolve, ...args));
Object.defineProperties(generator, {
activeCount: { get: () => active },
pendingCount: { get: () => queue.length },
clearQueue: { value: () => { queue.length = 0; } },
});
return generator;
}
"
.trim()
.to_string(),
);
// Also register the deep import path used by qualisero-pi-agent-scip
modules.insert(
"@sourcegraph/scip-typescript/dist/src/scip.js".to_string(),
r"
export const scip = { Index: class {} };
export default { scip };
"
.trim()
.to_string(),
);
modules.insert(
"unpdf".to_string(),
r#"
export async function getDocumentProxy(data) {
return { numPages: 0, getPage: async () => ({ getTextContent: async () => ({ items: [] }) }) };
}
export async function extractText(data) { return { totalPages: 0, text: "" }; }
export async function renderPageAsImage() { return new Uint8Array(); }
"#
.trim()
.to_string(),
);
modules.insert(
"@sourcegraph/scip-python".to_string(),
r"
export class PythonIndexer { async index() { return []; } }
export default { PythonIndexer };
"
.trim()
.to_string(),
);
modules.insert(
"@sourcegraph/scip-python/index.js".to_string(),
r"
export class PythonIndexer { async index() { return []; } }
export default { PythonIndexer };
"
.trim()
.to_string(),
);
modules
}
fn default_virtual_modules_shared() -> Arc<HashMap<String, String>> {
static DEFAULT_VIRTUAL_MODULES: std::sync::OnceLock<Arc<HashMap<String, String>>> =
std::sync::OnceLock::new();
Arc::clone(DEFAULT_VIRTUAL_MODULES.get_or_init(|| Arc::new(default_virtual_modules())))
}
/// Returns the set of all module specifiers available as virtual modules.
///
/// Used by the preflight analyzer to determine whether an extension's
/// imports can be resolved without hitting the filesystem.
#[must_use]
pub fn available_virtual_module_names() -> std::collections::BTreeSet<String> {
default_virtual_modules_shared().keys().cloned().collect()
}
/// Sampling cadence for memory usage snapshots when no hard memory limit is configured.
///
/// `AsyncRuntime::memory_usage()` triggers QuickJS heap traversal and is expensive on hot
/// tick paths. When runtime memory is unbounded, periodic sampling preserves observability
/// while avoiding per-tick full-heap scans.
const UNBOUNDED_MEMORY_USAGE_SAMPLE_EVERY_TICKS: u64 = 32;
/// Integrated PiJS runtime combining QuickJS, scheduler, and Promise bridge.
///
/// This is the main entry point for running JavaScript extensions with
/// proper async hostcall support. It provides:
///
/// - Promise-based `pi.*` methods that enqueue hostcall requests
/// - Deterministic event loop scheduling
/// - Automatic microtask draining after macrotasks
/// - Hostcall completion → Promise resolution/rejection
///
/// # Example
///
/// ```ignore
/// // Create runtime
/// let runtime = PiJsRuntime::new().await?;
///
/// // Evaluate extension code
/// runtime.eval("
/// pi.tool('read', { path: 'foo.txt' }).then(result => {
/// console.log('Got:', result);
/// });
/// ").await?;
///
/// // Process hostcall requests
/// while let Some(request) = runtime.drain_hostcall_requests().pop_front() {
/// // Execute the hostcall
/// let result = execute_tool(&request.kind, &request.payload).await;
/// // Deliver completion back to JS
/// runtime.complete_hostcall(&request.call_id, result)?;
/// }
///
/// // Tick the event loop to deliver completions
/// let stats = runtime.tick().await?;
/// ```
pub struct PiJsRuntime<C: SchedulerClock = WallClock> {
runtime: AsyncRuntime,
context: AsyncContext,
scheduler: Rc<RefCell<Scheduler<C>>>,
hostcall_queue: HostcallQueue,
trace_seq: Arc<AtomicU64>,
hostcall_tracker: Rc<RefCell<HostcallTracker>>,
hostcalls_total: Arc<AtomicU64>,
hostcalls_timed_out: Arc<AtomicU64>,
last_memory_used_bytes: Arc<AtomicU64>,
peak_memory_used_bytes: Arc<AtomicU64>,
tick_counter: Arc<AtomicU64>,
interrupt_budget: Rc<InterruptBudget>,
config: PiJsRuntimeConfig,
/// Additional filesystem roots that `readFileSync` may access (e.g.
/// extension directories). Populated lazily as extensions are loaded.
allowed_read_roots: Arc<std::sync::Mutex<Vec<PathBuf>>>,
/// Accumulated auto-repair events. Use [`Self::record_repair`] to append
/// and [`Self::drain_repair_events`] to retrieve and clear.
repair_events: Arc<std::sync::Mutex<Vec<ExtensionRepairEvent>>>,
/// Shared module state used by the resolver and loader. Stored here so
/// that [`Self::add_extension_root`] can push extension roots into the
/// resolver after construction.
module_state: Rc<RefCell<PiJsModuleState>>,
/// Extension policy for synchronous capability checks.
policy: Option<ExtensionPolicy>,
}
#[derive(Debug, Clone, Default, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct JsRuntimeRegistrySnapshot {
extensions: u64,
tools: u64,
commands: u64,
hooks: u64,
event_bus_hooks: u64,
providers: u64,
shortcuts: u64,
message_renderers: u64,
pending_tasks: u64,
pending_hostcalls: u64,
pending_timers: u64,
pending_event_listener_lists: u64,
provider_streams: u64,
}
#[derive(Debug, Clone, serde::Deserialize)]
struct JsRuntimeResetPayload {
before: JsRuntimeRegistrySnapshot,
after: JsRuntimeRegistrySnapshot,
clean: bool,
}
#[derive(Debug, Clone, Default)]
pub struct PiJsWarmResetReport {
pub reused: bool,
pub reason_code: Option<String>,
pub rust_pending_hostcalls: u64,
pub rust_pending_hostcall_queue: u64,
pub rust_scheduler_pending: bool,
pub pending_tasks_before: u64,
pub pending_hostcalls_before: u64,
pub pending_timers_before: u64,
pub residual_entries_after: u64,
pub dynamic_module_invalidations: u64,
pub module_cache_hits: u64,
pub module_cache_misses: u64,
pub module_cache_invalidations: u64,
pub module_cache_entries: u64,
}
#[allow(clippy::future_not_send)]
impl PiJsRuntime<WallClock> {
/// Create a new PiJS runtime with the default wall clock.
#[allow(clippy::future_not_send)]
pub async fn new() -> Result<Self> {
Self::with_clock(WallClock).await
}
}
#[allow(clippy::future_not_send)]
impl<C: SchedulerClock + 'static> PiJsRuntime<C> {
/// Create a new PiJS runtime with a custom clock.
#[allow(clippy::future_not_send)]
pub async fn with_clock(clock: C) -> Result<Self> {
Self::with_clock_and_config(clock, PiJsRuntimeConfig::default()).await
}
/// Create a new PiJS runtime with a custom clock and runtime config.
#[allow(clippy::future_not_send)]
pub async fn with_clock_and_config(clock: C, config: PiJsRuntimeConfig) -> Result<Self> {
Self::with_clock_and_config_with_policy(clock, config, None).await
}
/// Create a new PiJS runtime with a custom clock, runtime config, and optional policy.
#[allow(clippy::future_not_send, clippy::too_many_lines)]
pub async fn with_clock_and_config_with_policy(
clock: C,
mut config: PiJsRuntimeConfig,
policy: Option<ExtensionPolicy>,
) -> Result<Self> {
// Inject target architecture so JS process.arch can read it
#[cfg(target_arch = "x86_64")]
config
.env
.entry("PI_TARGET_ARCH".to_string())
.or_insert_with(|| "x64".to_string());
#[cfg(target_arch = "aarch64")]
config
.env
.entry("PI_TARGET_ARCH".to_string())
.or_insert_with(|| "arm64".to_string());
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
config
.env
.entry("PI_TARGET_ARCH".to_string())
.or_insert_with(|| "x64".to_string());
// Inject target platform so JS process.platform matches os.platform().
// OSTYPE env var is a shell variable and not always exported.
{
let platform = match std::env::consts::OS {
"macos" => "darwin",
"windows" => "win32",
other => other,
};
config
.env
.entry("PI_PLATFORM".to_string())
.or_insert_with(|| platform.to_string());
}
let runtime = AsyncRuntime::new().map_err(|err| map_js_error(&err))?;
if let Some(limit) = config.limits.memory_limit_bytes {
runtime.set_memory_limit(limit).await;
}
if let Some(limit) = config.limits.max_stack_bytes {
runtime.set_max_stack_size(limit).await;
}
let interrupt_budget = Rc::new(InterruptBudget::new(config.limits.interrupt_budget));
if config.limits.interrupt_budget.is_some() {
let budget = Rc::clone(&interrupt_budget);
runtime
.set_interrupt_handler(Some(Box::new(move || budget.on_interrupt())))
.await;
}
let repair_events: Arc<std::sync::Mutex<Vec<ExtensionRepairEvent>>> =
Arc::new(std::sync::Mutex::new(Vec::new()));
let module_state = Rc::new(RefCell::new(
PiJsModuleState::new()
.with_repair_mode(config.repair_mode)
.with_repair_events(Arc::clone(&repair_events))
.with_disk_cache_dir(config.disk_cache_dir.clone()),
));
runtime
.set_loader(
PiJsResolver {
state: Rc::clone(&module_state),
},
PiJsLoader {
state: Rc::clone(&module_state),
},
)
.await;
let context = AsyncContext::full(&runtime)
.await
.map_err(|err| map_js_error(&err))?;
let scheduler = Rc::new(RefCell::new(Scheduler::with_clock(clock)));
let fast_queue_capacity = if config.limits.hostcall_fast_queue_capacity == 0 {
HOSTCALL_FAST_RING_CAPACITY
} else {
config.limits.hostcall_fast_queue_capacity
};
let overflow_queue_capacity = if config.limits.hostcall_overflow_queue_capacity == 0 {
HOSTCALL_OVERFLOW_CAPACITY
} else {
config.limits.hostcall_overflow_queue_capacity
};
let hostcall_queue: HostcallQueue = Rc::new(RefCell::new(
HostcallRequestQueue::with_capacities(fast_queue_capacity, overflow_queue_capacity),
));
let hostcall_tracker = Rc::new(RefCell::new(HostcallTracker::default()));
let hostcalls_total = Arc::new(AtomicU64::new(0));
let hostcalls_timed_out = Arc::new(AtomicU64::new(0));
let last_memory_used_bytes = Arc::new(AtomicU64::new(0));
let peak_memory_used_bytes = Arc::new(AtomicU64::new(0));
let tick_counter = Arc::new(AtomicU64::new(0));
let trace_seq = Arc::new(AtomicU64::new(1));
let instance = Self {
runtime,
context,
scheduler,
hostcall_queue,
trace_seq,
hostcall_tracker,
hostcalls_total,
hostcalls_timed_out,
last_memory_used_bytes,
peak_memory_used_bytes,
tick_counter,
interrupt_budget,
config,
allowed_read_roots: Arc::new(std::sync::Mutex::new(Vec::new())),
repair_events,
module_state,
policy,
};
instance.install_pi_bridge().await?;
Ok(instance)
}
async fn map_quickjs_error(&self, err: &rquickjs::Error) -> Error {
if self.interrupt_budget.did_trip() {
self.interrupt_budget.clear_trip();
return Error::extension("PiJS execution budget exceeded".to_string());
}
if matches!(err, rquickjs::Error::Exception) {
let detail = self
.context
.with(|ctx| {
let caught = ctx.catch();
Ok::<String, rquickjs::Error>(format_quickjs_exception(&ctx, caught))
})
.await
.ok();
if let Some(detail) = detail {
let detail = detail.trim();
if !detail.is_empty() && detail != "undefined" {
return Error::extension(format!("QuickJS exception: {detail}"));
}
}
}
map_js_error(err)
}
fn map_quickjs_job_error<E: std::fmt::Display>(&self, err: E) -> Error {
if self.interrupt_budget.did_trip() {
self.interrupt_budget.clear_trip();
return Error::extension("PiJS execution budget exceeded".to_string());
}
Error::extension(format!("QuickJS job: {err}"))
}
fn should_sample_memory_usage(&self) -> bool {
if self.config.limits.memory_limit_bytes.is_some() {
return true;
}
let tick = self.tick_counter.fetch_add(1, AtomicOrdering::SeqCst) + 1;
tick == 1 || (tick % UNBOUNDED_MEMORY_USAGE_SAMPLE_EVERY_TICKS == 0)
}
fn module_cache_snapshot(&self) -> (u64, u64, u64, u64, u64) {
let state = self.module_state.borrow();
let entries = u64::try_from(state.compiled_sources.len()).unwrap_or(u64::MAX);
(
state.module_cache_counters.hits,
state.module_cache_counters.misses,
state.module_cache_counters.invalidations,
entries,
state.module_cache_counters.disk_hits,
)
}
#[allow(clippy::future_not_send, clippy::too_many_lines)]
pub async fn reset_for_warm_reload(&self) -> Result<PiJsWarmResetReport> {
let rust_pending_hostcalls =
u64::try_from(self.hostcall_tracker.borrow().pending_count()).unwrap_or(u64::MAX);
let rust_pending_hostcall_queue =
u64::try_from(self.hostcall_queue.borrow().len()).unwrap_or(u64::MAX);
let rust_scheduler_pending = self.scheduler.borrow().has_pending();
let mut report = PiJsWarmResetReport {
rust_pending_hostcalls,
rust_pending_hostcall_queue,
rust_scheduler_pending,
..PiJsWarmResetReport::default()
};
if rust_pending_hostcalls > 0 || rust_pending_hostcall_queue > 0 || rust_scheduler_pending {
report.reason_code = Some("pending_rust_work".to_string());
return Ok(report);
}
let reset_payload_value = match self
.context
.with(|ctx| {
let global = ctx.globals();
let reset_fn: Function<'_> = global.get("__pi_reset_extension_runtime_state")?;
let value: Value<'_> = reset_fn.call(())?;
js_to_json(&value)
})
.await
{
Ok(value) => value,
Err(err) => return Err(self.map_quickjs_error(&err).await),
};
let reset_payload: JsRuntimeResetPayload = serde_json::from_value(reset_payload_value)
.map_err(|err| {
Error::extension(format!("PiJS warm reset payload decode failed: {err}"))
})?;
report.pending_tasks_before = reset_payload.before.pending_tasks;
report.pending_hostcalls_before = reset_payload.before.pending_hostcalls;
report.pending_timers_before = reset_payload.before.pending_timers;
let residual_after = reset_payload.after.extensions
+ reset_payload.after.tools
+ reset_payload.after.commands
+ reset_payload.after.hooks
+ reset_payload.after.event_bus_hooks
+ reset_payload.after.providers
+ reset_payload.after.shortcuts
+ reset_payload.after.message_renderers
+ reset_payload.after.pending_tasks
+ reset_payload.after.pending_hostcalls
+ reset_payload.after.pending_timers
+ reset_payload.after.pending_event_listener_lists
+ reset_payload.after.provider_streams;
report.residual_entries_after = residual_after;
self.hostcall_queue.borrow_mut().clear();
*self.hostcall_tracker.borrow_mut() = HostcallTracker::default();
if let Ok(mut roots) = self.allowed_read_roots.lock() {
roots.clear();
}
let mut dynamic_invalidations = 0_u64;
{
let mut state = self.module_state.borrow_mut();
let dynamic_specs: Vec<String> =
state.dynamic_virtual_modules.keys().cloned().collect();
state.dynamic_virtual_modules.clear();
state.dynamic_virtual_named_exports.clear();
state.extension_roots.clear();
state.extension_root_tiers.clear();
state.extension_root_scopes.clear();
for spec in dynamic_specs {
if state.compiled_sources.remove(&spec).is_some() {
dynamic_invalidations = dynamic_invalidations.saturating_add(1);
}
}
if dynamic_invalidations > 0 {
state.module_cache_counters.invalidations = state
.module_cache_counters
.invalidations
.saturating_add(dynamic_invalidations);
}
}
report.dynamic_module_invalidations = dynamic_invalidations;
let (cache_hits, cache_misses, cache_invalidations, cache_entries, _disk_hits) =
self.module_cache_snapshot();
report.module_cache_hits = cache_hits;
report.module_cache_misses = cache_misses;
report.module_cache_invalidations = cache_invalidations;
report.module_cache_entries = cache_entries;
if report.pending_tasks_before > 0
|| report.pending_hostcalls_before > 0
|| report.pending_timers_before > 0
{
report.reason_code = Some("pending_js_work".to_string());
return Ok(report);
}
if !reset_payload.clean || residual_after > 0 {
report.reason_code = Some("reset_residual_state".to_string());
return Ok(report);
}
report.reused = true;
Ok(report)
}
/// Evaluate JavaScript source code.
pub async fn eval(&self, source: &str) -> Result<()> {
self.interrupt_budget.reset();
match self.context.with(|ctx| ctx.eval::<(), _>(source)).await {
Ok(()) => {}
Err(err) => return Err(self.map_quickjs_error(&err).await),
}
// Drain any immediate jobs (Promise.resolve chains, etc.)
self.drain_jobs().await?;
Ok(())
}
/// Invoke a zero-argument global JS function and drain immediate microtasks.
///
/// This is useful for hot loops that need to trigger pre-installed JS helpers
/// without paying per-call parser/compile overhead from `eval()`.
pub async fn call_global_void(&self, name: &str) -> Result<()> {
self.interrupt_budget.reset();
match self
.context
.with(|ctx| {
let global = ctx.globals();
let function: Function<'_> = global.get(name)?;
function.call::<(), ()>(())?;
Ok::<(), rquickjs::Error>(())
})
.await
{
Ok(()) => {}
Err(err) => return Err(self.map_quickjs_error(&err).await),
}
self.drain_jobs().await?;
Ok(())
}
// ---- Auto-repair event infrastructure (bd-k5q5.8.1) --------------------
/// The configured repair mode for this runtime.
pub const fn repair_mode(&self) -> RepairMode {
self.config.repair_mode
}
/// Whether the auto-repair pipeline should apply repairs.
pub const fn auto_repair_enabled(&self) -> bool {
self.config.repair_mode.should_apply()
}
/// Record an auto-repair event. The event is appended to the internal
/// log and emitted as a structured tracing span so external log sinks
/// can capture it.
pub fn record_repair(&self, event: ExtensionRepairEvent) {
tracing::info!(
event = "pijs.repair",
extension_id = %event.extension_id,
pattern = %event.pattern,
success = event.success,
repair_action = %event.repair_action,
"auto-repair applied"
);
if let Ok(mut events) = self.repair_events.lock() {
events.push(event);
}
}
/// Drain all accumulated repair events, leaving the internal buffer
/// empty. Useful for conformance reports that need to distinguish
/// clean passes from repaired passes.
pub fn drain_repair_events(&self) -> Vec<ExtensionRepairEvent> {
self.repair_events
.lock()
.map(|mut v| std::mem::take(&mut *v))
.unwrap_or_default()
}
/// Number of repair events recorded since the runtime was created.
pub fn repair_count(&self) -> u64 {
self.repair_events.lock().map_or(0, |v| v.len() as u64)
}
/// Reset transient module state for warm isolate reuse.
///
/// Clears extension roots, dynamic virtual modules, named export tracking,
/// repair events, and cache counters while **preserving** the compiled
/// sources cache (both in-memory and disk). This lets the runtime be
/// reloaded with a fresh set of extensions without paying the SWC
/// transpilation cost again.
pub fn reset_transient_state(&self) {
let mut state = self.module_state.borrow_mut();
state.extension_roots.clear();
state.extension_root_tiers.clear();
state.extension_root_scopes.clear();
state.dynamic_virtual_modules.clear();
state.dynamic_virtual_named_exports.clear();
state.module_cache_counters = ModuleCacheCounters::default();
// Keep compiled_sources — the transpiled source cache is still valid.
// Keep disk_cache_dir — reuse the same persistent cache.
// Keep static_virtual_modules — immutable, shared via Arc.
drop(state);
// Clear hostcall state.
self.hostcall_queue.borrow_mut().clear();
*self.hostcall_tracker.borrow_mut() = HostcallTracker::default();
// Drain repair events.
if let Ok(mut events) = self.repair_events.lock() {
events.clear();
}
// Reset counters.
self.hostcalls_total
.store(0, std::sync::atomic::Ordering::SeqCst);
self.hostcalls_timed_out
.store(0, std::sync::atomic::Ordering::SeqCst);
self.tick_counter
.store(0, std::sync::atomic::Ordering::SeqCst);
}
/// Evaluate a JavaScript file.
pub async fn eval_file(&self, path: &std::path::Path) -> Result<()> {
self.interrupt_budget.reset();
match self.context.with(|ctx| ctx.eval_file::<(), _>(path)).await {
Ok(()) => {}
Err(err) => return Err(self.map_quickjs_error(&err).await),
}
self.drain_jobs().await?;
Ok(())
}
/// Run a closure inside the JS context and map QuickJS errors into `pi::Error`.
///
/// This is intentionally `pub(crate)` so the extensions runtime can call JS helper
/// functions without exposing raw rquickjs types as part of the public API.
pub(crate) async fn with_ctx<F, R>(&self, f: F) -> Result<R>
where
F: for<'js> FnOnce(Ctx<'js>) -> rquickjs::Result<R> + rquickjs::markers::ParallelSend,
R: rquickjs::markers::ParallelSend,
{
self.interrupt_budget.reset();
match self.context.with(f).await {
Ok(value) => Ok(value),
Err(err) => Err(self.map_quickjs_error(&err).await),
}
}
/// Read a global variable from the JS context and convert it to JSON.
///
/// This is primarily intended for integration tests and diagnostics; it intentionally
/// does not expose raw `rquickjs` types as part of the public API.
pub async fn read_global_json(&self, name: &str) -> Result<serde_json::Value> {
self.interrupt_budget.reset();
let value = match self
.context
.with(|ctx| {
let global = ctx.globals();
let value: Value<'_> = global.get(name)?;
js_to_json(&value)
})
.await
{
Ok(value) => value,
Err(err) => return Err(self.map_quickjs_error(&err).await),
};
Ok(value)
}
/// Drain pending hostcall requests from the queue.
///
/// Returns the requests that need to be processed by the host.
/// After processing, call `complete_hostcall()` for each.
pub fn drain_hostcall_requests(&self) -> VecDeque<HostcallRequest> {
self.hostcall_queue.borrow_mut().drain_all()
}
/// Drain pending QuickJS jobs (Promise microtasks) until fixpoint.
pub async fn drain_microtasks(&self) -> Result<usize> {
self.drain_jobs().await
}
/// Return the next timer deadline (runtime clock), if any.
pub fn next_timer_deadline_ms(&self) -> Option<u64> {
self.scheduler.borrow().next_timer_deadline()
}
/// Peek at pending hostcall requests without draining.
pub fn pending_hostcall_count(&self) -> usize {
self.hostcall_tracker.borrow().pending_count()
}
/// Snapshot queue depth/backpressure counters for diagnostics.
pub fn hostcall_queue_telemetry(&self) -> HostcallQueueTelemetry {
self.hostcall_queue.borrow().snapshot()
}
/// Queue wait (enqueue -> dispatch start) in milliseconds for a pending hostcall.
pub fn hostcall_queue_wait_ms(&self, call_id: &str) -> Option<u64> {
let now_ms = self.scheduler.borrow().now_ms();
self.hostcall_tracker
.borrow()
.queue_wait_ms(call_id, now_ms)
}
/// Check whether a given hostcall is still pending.
///
/// This is useful for streaming hostcalls that need to stop polling/reading once the JS side
/// has timed out or otherwise completed the call.
pub fn is_hostcall_pending(&self, call_id: &str) -> bool {
self.hostcall_tracker.borrow().is_pending(call_id)
}
/// Get all tools registered by loaded JS extensions.
pub async fn get_registered_tools(&self) -> Result<Vec<ExtensionToolDef>> {
self.interrupt_budget.reset();
let value = match self
.context
.with(|ctx| {
let global = ctx.globals();
let getter: Function<'_> = global.get("__pi_get_registered_tools")?;
let tools: Value<'_> = getter.call(())?;
js_to_json(&tools)
})
.await
{
Ok(value) => value,
Err(err) => return Err(self.map_quickjs_error(&err).await),
};
serde_json::from_value(value).map_err(|err| Error::Json(Box::new(err)))
}
/// Read a global value by name and convert it to JSON.
///
/// This is intentionally a narrow helper that avoids exposing raw `rquickjs`
/// types in the public API (useful for integration tests and debugging).
pub async fn get_global_json(&self, name: &str) -> Result<serde_json::Value> {
self.interrupt_budget.reset();
match self
.context
.with(|ctx| {
let global = ctx.globals();
let value: Value<'_> = global.get(name)?;
js_to_json(&value)
})
.await
{
Ok(value) => Ok(value),
Err(err) => Err(self.map_quickjs_error(&err).await),
}
}
/// Enqueue a hostcall completion to be delivered on next tick.
pub fn complete_hostcall(&self, call_id: impl Into<String>, outcome: HostcallOutcome) {
self.scheduler
.borrow_mut()
.enqueue_hostcall_complete(call_id.into(), outcome);
}
/// Enqueue multiple hostcall completions in one scheduler borrow.
pub fn complete_hostcalls_batch<I>(&self, completions: I)
where
I: IntoIterator<Item = (String, HostcallOutcome)>,
{
self.scheduler
.borrow_mut()
.enqueue_hostcall_completions(completions);
}
/// Enqueue an inbound event to be delivered on next tick.
pub fn enqueue_event(&self, event_id: impl Into<String>, payload: serde_json::Value) {
self.scheduler
.borrow_mut()
.enqueue_event(event_id.into(), payload);
}
/// Set a timer to fire after the given delay.
///
/// Returns the timer ID for cancellation.
pub fn set_timeout(&self, delay_ms: u64) -> u64 {
self.scheduler.borrow_mut().set_timeout(delay_ms)
}
/// Cancel a timer by ID.
pub fn clear_timeout(&self, timer_id: u64) -> bool {
self.scheduler.borrow_mut().clear_timeout(timer_id)
}
/// Get the current time from the clock.
pub fn now_ms(&self) -> u64 {
self.scheduler.borrow().now_ms()
}
/// Check if there are pending tasks (macrotasks or timers).
pub fn has_pending(&self) -> bool {
self.scheduler.borrow().has_pending() || self.pending_hostcall_count() > 0
}
/// Execute one tick of the event loop.
///
/// This will:
/// 1. Move due timers to the macrotask queue
/// 2. Execute one macrotask (if any)
/// 3. Drain all pending QuickJS jobs (microtasks)
///
/// Returns statistics about what was executed.
pub async fn tick(&self) -> Result<PiJsTickStats> {
// Get the next macrotask from scheduler
let macrotask = self.scheduler.borrow_mut().tick();
let mut stats = PiJsTickStats::default();
if let Some(task) = macrotask {
stats.ran_macrotask = true;
self.interrupt_budget.reset();
// Handle the macrotask inside the JS context
let result = self
.context
.with(|ctx| {
self.handle_macrotask(&ctx, &task)?;
Ok::<_, rquickjs::Error>(())
})
.await;
if let Err(err) = result {
return Err(self.map_quickjs_error(&err).await);
}
// Drain microtasks until fixpoint
stats.jobs_drained = self.drain_jobs().await?;
}
stats.pending_hostcalls = self.hostcall_tracker.borrow().pending_count();
stats.hostcalls_total = self
.hostcalls_total
.load(std::sync::atomic::Ordering::SeqCst);
stats.hostcalls_timed_out = self
.hostcalls_timed_out
.load(std::sync::atomic::Ordering::SeqCst);
if self.should_sample_memory_usage() {
let usage = self.runtime.memory_usage().await;
stats.memory_used_bytes = u64::try_from(usage.memory_used_size).unwrap_or(0);
self.last_memory_used_bytes
.store(stats.memory_used_bytes, std::sync::atomic::Ordering::SeqCst);
let mut peak = self
.peak_memory_used_bytes
.load(std::sync::atomic::Ordering::SeqCst);
if stats.memory_used_bytes > peak {
peak = stats.memory_used_bytes;
self.peak_memory_used_bytes
.store(peak, std::sync::atomic::Ordering::SeqCst);
}
stats.peak_memory_used_bytes = peak;
} else {
stats.memory_used_bytes = self
.last_memory_used_bytes
.load(std::sync::atomic::Ordering::SeqCst);
stats.peak_memory_used_bytes = self
.peak_memory_used_bytes
.load(std::sync::atomic::Ordering::SeqCst);
}
stats.repairs_total = self.repair_count();
let (cache_hits, cache_misses, cache_invalidations, cache_entries, disk_hits) =
self.module_cache_snapshot();
stats.module_cache_hits = cache_hits;
stats.module_cache_misses = cache_misses;
stats.module_cache_invalidations = cache_invalidations;
stats.module_cache_entries = cache_entries;
stats.module_disk_cache_hits = disk_hits;
if let Some(limit) = self.config.limits.memory_limit_bytes {
let limit = u64::try_from(limit).unwrap_or(u64::MAX);
if stats.memory_used_bytes > limit {
return Err(Error::extension(format!(
"PiJS memory budget exceeded (used {} bytes, limit {} bytes)",
stats.memory_used_bytes, limit
)));
}
}
Ok(stats)
}
/// Drain all pending QuickJS jobs (microtasks).
async fn drain_jobs(&self) -> Result<usize> {
let mut count = 0;
loop {
if count >= MAX_JOBS_PER_TICK {
return Err(Error::extension(format!(
"PiJS microtask limit exceeded ({MAX_JOBS_PER_TICK})"
)));
}
let ran = match self.runtime.execute_pending_job().await {
Ok(ran) => ran,
Err(err) => return Err(self.map_quickjs_job_error(err)),
};
if !ran {
break;
}
count += 1;
}
Ok(count)
}
/// Handle a macrotask by resolving/rejecting Promises or dispatching events.
fn handle_macrotask(
&self,
ctx: &Ctx<'_>,
task: &crate::scheduler::Macrotask,
) -> rquickjs::Result<()> {
use crate::scheduler::MacrotaskKind as SMK;
match &task.kind {
SMK::HostcallComplete { call_id, outcome } => {
let is_nonfinal_stream = matches!(
outcome,
HostcallOutcome::StreamChunk {
is_final: false,
..
}
);
if is_nonfinal_stream {
// Non-final stream chunk: keep the call pending, just deliver the chunk.
if !self.hostcall_tracker.borrow().is_pending(call_id) {
tracing::debug!(
event = "pijs.macrotask.stream_chunk.ignored",
call_id = %call_id,
"Ignoring stream chunk (not pending)"
);
return Ok(());
}
} else {
// Final chunk or non-stream outcome: complete the hostcall.
let completion = self.hostcall_tracker.borrow_mut().on_complete(call_id);
let timer_id = match completion {
HostcallCompletion::Delivered { timer_id } => timer_id,
HostcallCompletion::Unknown => {
tracing::debug!(
event = "pijs.macrotask.hostcall_complete.ignored",
call_id = %call_id,
"Ignoring hostcall completion (not pending)"
);
return Ok(());
}
};
if let Some(timer_id) = timer_id {
let _ = self.scheduler.borrow_mut().clear_timeout(timer_id);
}
}
tracing::debug!(
event = "pijs.macrotask.hostcall_complete",
call_id = %call_id,
seq = task.seq.value(),
"Delivering hostcall completion"
);
Self::deliver_hostcall_completion(ctx, call_id, outcome)?;
}
SMK::TimerFired { timer_id } => {
if let Some(call_id) = self
.hostcall_tracker
.borrow_mut()
.take_timed_out_call(*timer_id)
{
self.hostcalls_timed_out
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
tracing::warn!(
event = "pijs.hostcall.timeout",
call_id = %call_id,
timer_id = timer_id,
"Hostcall timed out"
);
let outcome = HostcallOutcome::Error {
code: "timeout".to_string(),
message: "Hostcall timed out".to_string(),
};
Self::deliver_hostcall_completion(ctx, &call_id, &outcome)?;
return Ok(());
}
tracing::debug!(
event = "pijs.macrotask.timer_fired",
timer_id = timer_id,
seq = task.seq.value(),
"Timer fired"
);
// Timer callbacks are stored in a JS-side map
Self::deliver_timer_fire(ctx, *timer_id)?;
}
SMK::InboundEvent { event_id, payload } => {
tracing::debug!(
event = "pijs.macrotask.inbound_event",
event_id = %event_id,
seq = task.seq.value(),
"Delivering inbound event"
);
Self::deliver_inbound_event(ctx, event_id, payload)?;
}
}
Ok(())
}
/// Deliver a hostcall completion to JS.
fn deliver_hostcall_completion(
ctx: &Ctx<'_>,
call_id: &str,
outcome: &HostcallOutcome,
) -> rquickjs::Result<()> {
let global = ctx.globals();
let complete_fn: Function<'_> = global.get("__pi_complete_hostcall")?;
let js_outcome = match outcome {
HostcallOutcome::Success(value) => {
let obj = Object::new(ctx.clone())?;
obj.set("ok", true)?;
obj.set("value", json_to_js(ctx, value)?)?;
obj
}
HostcallOutcome::Error { code, message } => {
let obj = Object::new(ctx.clone())?;
obj.set("ok", false)?;
obj.set("code", code.clone())?;
obj.set("message", message.clone())?;
obj
}
HostcallOutcome::StreamChunk {
chunk,
sequence,
is_final,
} => {
let obj = Object::new(ctx.clone())?;
obj.set("ok", true)?;
obj.set("stream", true)?;
obj.set("sequence", *sequence)?;
obj.set("isFinal", *is_final)?;
obj.set("chunk", json_to_js(ctx, chunk)?)?;
obj
}
};
complete_fn.call::<_, ()>((call_id, js_outcome))?;
Ok(())
}
/// Deliver a timer fire event to JS.
fn deliver_timer_fire(ctx: &Ctx<'_>, timer_id: u64) -> rquickjs::Result<()> {
let global = ctx.globals();
let fire_fn: Function<'_> = global.get("__pi_fire_timer")?;
fire_fn.call::<_, ()>((timer_id,))?;
Ok(())
}
/// Deliver an inbound event to JS.
fn deliver_inbound_event(
ctx: &Ctx<'_>,
event_id: &str,
payload: &serde_json::Value,
) -> rquickjs::Result<()> {
let global = ctx.globals();
let dispatch_fn: Function<'_> = global.get("__pi_dispatch_event")?;
let js_payload = json_to_js(ctx, payload)?;
dispatch_fn.call::<_, ()>((event_id, js_payload))?;
Ok(())
}
/// Generate a unique trace ID.
fn next_trace_id(&self) -> u64 {
self.trace_seq.fetch_add(1, AtomicOrdering::SeqCst)
}
/// Install the pi.* bridge with Promise-returning hostcall methods.
///
/// The bridge uses a two-layer design:
/// 1. Rust native functions (`__pi_*_native`) that return call_id strings
/// 2. JS wrappers (`pi.*`) that create Promises and register them
///
/// This avoids lifetime issues with returning Promises from Rust closures.
/// Register an additional filesystem root that `readFileSync` is allowed
/// to access. Called before loading each extension so it can read its own
/// bundled assets (HTML templates, markdown docs, etc.).
pub fn add_allowed_read_root(&self, root: PathBuf) {
if let Ok(mut roots) = self.allowed_read_roots.lock() {
if !roots.contains(&root) {
roots.push(root);
}
}
}
/// Register an extension root directory so the resolver can detect
/// monorepo escape patterns (Pattern 3). Also registers the root
/// for `readFileSync` access.
pub fn add_extension_root(&self, root: PathBuf) {
self.add_extension_root_with_id(root, None);
}
/// Register an extension root with optional extension ID metadata.
///
/// Pattern 4 (missing npm dependency proxy stubs) uses this metadata to
/// apply stricter policy for official/first-party extensions and to allow
/// same-scope package imports (`@scope/*`) when scope can be discovered.
pub fn add_extension_root_with_id(&self, root: PathBuf, extension_id: Option<&str>) {
self.add_allowed_read_root(root.clone());
let mut state = self.module_state.borrow_mut();
if !state.extension_roots.contains(&root) {
state.extension_roots.push(root.clone());
}
let tier = extension_id.map_or_else(
|| root_path_hint_tier(&root),
|id| classify_proxy_stub_source_tier(id, &root),
);
state.extension_root_tiers.insert(root.clone(), tier);
if let Some(scope) = read_extension_package_scope(&root) {
state.extension_root_scopes.insert(root, scope);
}
}
#[allow(clippy::too_many_lines)]
async fn install_pi_bridge(&self) -> Result<()> {
let hostcall_queue = self.hostcall_queue.clone();
let scheduler = Rc::clone(&self.scheduler);
let hostcall_tracker = Rc::clone(&self.hostcall_tracker);
let hostcalls_total = Arc::clone(&self.hostcalls_total);
let trace_seq = Arc::clone(&self.trace_seq);
let default_hostcall_timeout_ms = self.config.limits.hostcall_timeout_ms;
let process_cwd = self.config.cwd.clone();
let process_args = self.config.args.clone();
let env = self.config.env.clone();
let deny_env = self.config.deny_env;
let repair_mode = self.config.repair_mode;
let repair_events = Arc::clone(&self.repair_events);
let allow_unsafe_sync_exec = self.config.allow_unsafe_sync_exec;
let allowed_read_roots = Arc::clone(&self.allowed_read_roots);
let policy = self.policy.clone();
self.context
.with(|ctx| {
let global = ctx.globals();
// Install native functions that return call_ids
// These are wrapped by JS to create Promises
// __pi_tool_native(name, input) -> call_id
global.set(
"__pi_tool_native",
Func::from({
let queue = hostcall_queue.clone();
let tracker = hostcall_tracker.clone();
let scheduler = Rc::clone(&scheduler);
let hostcalls_total = Arc::clone(&hostcalls_total);
let trace_seq = Arc::clone(&trace_seq);
move |ctx: Ctx<'_>,
name: String,
input: Value<'_>|
-> rquickjs::Result<String> {
let payload = js_to_json(&input)?;
let call_id = format!("call-{}", generate_call_id());
hostcalls_total.fetch_add(1, AtomicOrdering::SeqCst);
let trace_id = trace_seq.fetch_add(1, AtomicOrdering::SeqCst);
let enqueued_at_ms = scheduler.borrow().now_ms();
let timeout_ms = default_hostcall_timeout_ms.filter(|ms| *ms > 0);
let timer_id =
timeout_ms.map(|ms| scheduler.borrow_mut().set_timeout(ms));
tracker
.borrow_mut()
.register(call_id.clone(), timer_id, enqueued_at_ms);
let extension_id: Option<String> = ctx
.globals()
.get::<_, Option<String>>("__pi_current_extension_id")
.ok()
.flatten()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty());
let request = HostcallRequest {
call_id: call_id.clone(),
kind: HostcallKind::Tool { name },
payload,
trace_id,
extension_id,
};
enqueue_hostcall_request_with_backpressure(
&queue, &tracker, &scheduler, request,
);
Ok(call_id)
}
}),
)?;
// __pi_exec_native(cmd, args, options) -> call_id
global.set(
"__pi_exec_native",
Func::from({
let queue = hostcall_queue.clone();
let tracker = hostcall_tracker.clone();
let scheduler = Rc::clone(&scheduler);
let hostcalls_total = Arc::clone(&hostcalls_total);
let trace_seq = Arc::clone(&trace_seq);
move |ctx: Ctx<'_>,
cmd: String,
args: Value<'_>,
options: Opt<Value<'_>>|
-> rquickjs::Result<String> {
let mut options_json = match options.0.as_ref() {
None => serde_json::json!({}),
Some(value) if value.is_null() => serde_json::json!({}),
Some(value) => js_to_json(value)?,
};
if let Some(default_timeout_ms) =
default_hostcall_timeout_ms.filter(|ms| *ms > 0)
{
match &mut options_json {
serde_json::Value::Object(map) => {
let has_timeout = map.contains_key("timeout")
|| map.contains_key("timeoutMs")
|| map.contains_key("timeout_ms");
if !has_timeout {
map.insert(
"timeoutMs".to_string(),
serde_json::Value::from(default_timeout_ms),
);
}
}
_ => {
options_json =
serde_json::json!({ "timeoutMs": default_timeout_ms });
}
}
}
let payload = serde_json::json!({
"args": js_to_json(&args)?,
"options": options_json,
});
let call_id = format!("call-{}", generate_call_id());
hostcalls_total.fetch_add(1, AtomicOrdering::SeqCst);
let trace_id = trace_seq.fetch_add(1, AtomicOrdering::SeqCst);
let enqueued_at_ms = scheduler.borrow().now_ms();
let timeout_ms = default_hostcall_timeout_ms.filter(|ms| *ms > 0);
let timer_id =
timeout_ms.map(|ms| scheduler.borrow_mut().set_timeout(ms));
tracker
.borrow_mut()
.register(call_id.clone(), timer_id, enqueued_at_ms);
let extension_id: Option<String> = ctx
.globals()
.get::<_, Option<String>>("__pi_current_extension_id")
.ok()
.flatten()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty());
let request = HostcallRequest {
call_id: call_id.clone(),
kind: HostcallKind::Exec { cmd },
payload,
trace_id,
extension_id,
};
enqueue_hostcall_request_with_backpressure(
&queue, &tracker, &scheduler, request,
);
Ok(call_id)
}
}),
)?;
// __pi_http_native(request) -> call_id
global.set(
"__pi_http_native",
Func::from({
let queue = hostcall_queue.clone();
let tracker = hostcall_tracker.clone();
let scheduler = Rc::clone(&scheduler);
let hostcalls_total = Arc::clone(&hostcalls_total);
let trace_seq = Arc::clone(&trace_seq);
move |ctx: Ctx<'_>, req: Value<'_>| -> rquickjs::Result<String> {
let payload = js_to_json(&req)?;
let call_id = format!("call-{}", generate_call_id());
hostcalls_total.fetch_add(1, AtomicOrdering::SeqCst);
let trace_id = trace_seq.fetch_add(1, AtomicOrdering::SeqCst);
let enqueued_at_ms = scheduler.borrow().now_ms();
let timeout_ms = default_hostcall_timeout_ms.filter(|ms| *ms > 0);
let timer_id =
timeout_ms.map(|ms| scheduler.borrow_mut().set_timeout(ms));
tracker
.borrow_mut()
.register(call_id.clone(), timer_id, enqueued_at_ms);
let extension_id: Option<String> = ctx
.globals()
.get::<_, Option<String>>("__pi_current_extension_id")
.ok()
.flatten()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty());
let request = HostcallRequest {
call_id: call_id.clone(),
kind: HostcallKind::Http,
payload,
trace_id,
extension_id,
};
enqueue_hostcall_request_with_backpressure(
&queue, &tracker, &scheduler, request,
);
Ok(call_id)
}
}),
)?;
// __pi_session_native(op, args) -> call_id
global.set(
"__pi_session_native",
Func::from({
let queue = hostcall_queue.clone();
let tracker = hostcall_tracker.clone();
let scheduler = Rc::clone(&scheduler);
let hostcalls_total = Arc::clone(&hostcalls_total);
let trace_seq = Arc::clone(&trace_seq);
move |ctx: Ctx<'_>,
op: String,
args: Value<'_>|
-> rquickjs::Result<String> {
let payload = js_to_json(&args)?;
let call_id = format!("call-{}", generate_call_id());
hostcalls_total.fetch_add(1, AtomicOrdering::SeqCst);
let trace_id = trace_seq.fetch_add(1, AtomicOrdering::SeqCst);
let enqueued_at_ms = scheduler.borrow().now_ms();
let timeout_ms = default_hostcall_timeout_ms.filter(|ms| *ms > 0);
let timer_id =
timeout_ms.map(|ms| scheduler.borrow_mut().set_timeout(ms));
tracker
.borrow_mut()
.register(call_id.clone(), timer_id, enqueued_at_ms);
let extension_id: Option<String> = ctx
.globals()
.get::<_, Option<String>>("__pi_current_extension_id")
.ok()
.flatten()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty());
let request = HostcallRequest {
call_id: call_id.clone(),
kind: HostcallKind::Session { op },
payload,
trace_id,
extension_id,
};
enqueue_hostcall_request_with_backpressure(
&queue, &tracker, &scheduler, request,
);
Ok(call_id)
}
}),
)?;
// __pi_ui_native(op, args) -> call_id
global.set(
"__pi_ui_native",
Func::from({
let queue = hostcall_queue.clone();
let tracker = hostcall_tracker.clone();
let scheduler = Rc::clone(&scheduler);
let hostcalls_total = Arc::clone(&hostcalls_total);
let trace_seq = Arc::clone(&trace_seq);
move |ctx: Ctx<'_>,
op: String,
args: Value<'_>|
-> rquickjs::Result<String> {
let payload = js_to_json(&args)?;
let call_id = format!("call-{}", generate_call_id());
hostcalls_total.fetch_add(1, AtomicOrdering::SeqCst);
let trace_id = trace_seq.fetch_add(1, AtomicOrdering::SeqCst);
let enqueued_at_ms = scheduler.borrow().now_ms();
let timeout_ms = default_hostcall_timeout_ms.filter(|ms| *ms > 0);
let timer_id =
timeout_ms.map(|ms| scheduler.borrow_mut().set_timeout(ms));
tracker
.borrow_mut()
.register(call_id.clone(), timer_id, enqueued_at_ms);
let extension_id: Option<String> = ctx
.globals()
.get::<_, Option<String>>("__pi_current_extension_id")
.ok()
.flatten()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty());
let request = HostcallRequest {
call_id: call_id.clone(),
kind: HostcallKind::Ui { op },
payload,
trace_id,
extension_id,
};
enqueue_hostcall_request_with_backpressure(
&queue, &tracker, &scheduler, request,
);
Ok(call_id)
}
}),
)?;
// __pi_events_native(op, args) -> call_id
global.set(
"__pi_events_native",
Func::from({
let queue = hostcall_queue.clone();
let tracker = hostcall_tracker.clone();
let scheduler = Rc::clone(&scheduler);
let hostcalls_total = Arc::clone(&hostcalls_total);
let trace_seq = Arc::clone(&trace_seq);
move |ctx: Ctx<'_>,
op: String,
args: Value<'_>|
-> rquickjs::Result<String> {
let payload = js_to_json(&args)?;
let call_id = format!("call-{}", generate_call_id());
hostcalls_total.fetch_add(1, AtomicOrdering::SeqCst);
let trace_id = trace_seq.fetch_add(1, AtomicOrdering::SeqCst);
let enqueued_at_ms = scheduler.borrow().now_ms();
let timeout_ms = default_hostcall_timeout_ms.filter(|ms| *ms > 0);
let timer_id =
timeout_ms.map(|ms| scheduler.borrow_mut().set_timeout(ms));
tracker
.borrow_mut()
.register(call_id.clone(), timer_id, enqueued_at_ms);
let extension_id: Option<String> = ctx
.globals()
.get::<_, Option<String>>("__pi_current_extension_id")
.ok()
.flatten()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty());
let request = HostcallRequest {
call_id: call_id.clone(),
kind: HostcallKind::Events { op },
payload,
trace_id,
extension_id,
};
enqueue_hostcall_request_with_backpressure(
&queue, &tracker, &scheduler, request,
);
Ok(call_id)
}
}),
)?;
// __pi_log_native(entry) -> call_id
global.set(
"__pi_log_native",
Func::from({
let queue = hostcall_queue.clone();
let tracker = hostcall_tracker.clone();
let scheduler = Rc::clone(&scheduler);
let hostcalls_total = Arc::clone(&hostcalls_total);
let trace_seq = Arc::clone(&trace_seq);
move |ctx: Ctx<'_>, entry: Value<'_>| -> rquickjs::Result<String> {
let payload = js_to_json(&entry)?;
let call_id = format!("call-{}", generate_call_id());
hostcalls_total.fetch_add(1, AtomicOrdering::SeqCst);
let trace_id = trace_seq.fetch_add(1, AtomicOrdering::SeqCst);
let enqueued_at_ms = scheduler.borrow().now_ms();
let timeout_ms = default_hostcall_timeout_ms.filter(|ms| *ms > 0);
let timer_id =
timeout_ms.map(|ms| scheduler.borrow_mut().set_timeout(ms));
tracker
.borrow_mut()
.register(call_id.clone(), timer_id, enqueued_at_ms);
let extension_id: Option<String> = ctx
.globals()
.get::<_, Option<String>>("__pi_current_extension_id")
.ok()
.flatten()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty());
let request = HostcallRequest {
call_id: call_id.clone(),
kind: HostcallKind::Log,
payload,
trace_id,
extension_id,
};
enqueue_hostcall_request_with_backpressure(
&queue, &tracker, &scheduler, request,
);
Ok(call_id)
}
}),
)?;
// __pi_set_timeout_native(delay_ms) -> timer_id
global.set(
"__pi_set_timeout_native",
Func::from({
let scheduler = Rc::clone(&scheduler);
move |_ctx: Ctx<'_>, delay_ms: u64| -> rquickjs::Result<u64> {
Ok(scheduler.borrow_mut().set_timeout(delay_ms))
}
}),
)?;
// __pi_clear_timeout_native(timer_id) -> bool
global.set(
"__pi_clear_timeout_native",
Func::from({
let scheduler = Rc::clone(&scheduler);
move |_ctx: Ctx<'_>, timer_id: u64| -> rquickjs::Result<bool> {
Ok(scheduler.borrow_mut().clear_timeout(timer_id))
}
}),
)?;
// __pi_now_ms_native() -> u64
global.set(
"__pi_now_ms_native",
Func::from({
let scheduler = Rc::clone(&scheduler);
move |_ctx: Ctx<'_>| -> rquickjs::Result<u64> {
Ok(scheduler.borrow().now_ms())
}
}),
)?;
// __pi_process_cwd_native() -> String
global.set(
"__pi_process_cwd_native",
Func::from({
let process_cwd = process_cwd.clone();
move |_ctx: Ctx<'_>| -> rquickjs::Result<String> { Ok(process_cwd.clone()) }
}),
)?;
// __pi_process_args_native() -> string[]
global.set(
"__pi_process_args_native",
Func::from({
let process_args = process_args.clone();
move |_ctx: Ctx<'_>| -> rquickjs::Result<Vec<String>> {
Ok(process_args.clone())
}
}),
)?;
// __pi_process_exit_native(code) -> enqueues exit hostcall
global.set(
"__pi_process_exit_native",
Func::from({
let queue = hostcall_queue.clone();
let tracker = hostcall_tracker.clone();
let scheduler = Rc::clone(&scheduler);
move |_ctx: Ctx<'_>, code: i32| -> rquickjs::Result<()> {
tracing::info!(
event = "pijs.process.exit",
code,
"process.exit requested"
);
let call_id = format!("call-{}", generate_call_id());
let enqueued_at_ms = scheduler.borrow().now_ms();
tracker
.borrow_mut()
.register(call_id.clone(), None, enqueued_at_ms);
let request = HostcallRequest {
call_id,
kind: HostcallKind::Events {
op: "exit".to_string(),
},
payload: serde_json::json!({ "code": code }),
trace_id: 0,
extension_id: None,
};
enqueue_hostcall_request_with_backpressure(
&queue, &tracker, &scheduler, request,
);
Ok(())
}
}),
)?;
// __pi_process_execpath_native() -> string
global.set(
"__pi_process_execpath_native",
Func::from(move |_ctx: Ctx<'_>| -> rquickjs::Result<String> {
Ok(std::env::current_exe().map_or_else(
|_| "/usr/bin/pi".to_string(),
|p| p.to_string_lossy().into_owned(),
))
}),
)?;
// __pi_env_get_native(key) -> string | null
global.set(
"__pi_env_get_native",
Func::from({
let env = env.clone();
let policy_for_env = policy.clone();
move |_ctx: Ctx<'_>, key: String| -> rquickjs::Result<Option<String>> {
// Compat fallback runs BEFORE deny_env so conformance
// scanning can inject deterministic dummy keys even when
// the policy denies env access (ext-conformance feature
// or PI_EXT_COMPAT_SCAN=1 guard this path).
if let Some(value) = compat_env_fallback_value(&key, &env) {
tracing::debug!(
event = "pijs.env.get.compat",
key = %key,
"env compat fallback"
);
return Ok(Some(value));
}
if deny_env {
tracing::debug!(event = "pijs.env.get.denied", key = %key, "env capability denied");
return Ok(None);
}
// If a policy is present, use its SecretBroker (including
// disclosure_allowlist). Otherwise fall back to default
// secret filtering so obvious credentials are still hidden.
let allowed = policy_for_env.as_ref().map_or_else(
|| is_env_var_allowed(&key),
|policy| !policy.secret_broker.is_secret(&key),
);
tracing::debug!(
event = "pijs.env.get",
key = %key,
allowed,
"env get"
);
if !allowed {
return Ok(None);
}
Ok(env.get(&key).cloned())
}
}),
)?;
// __pi_crypto_sha256_hex_native(text) -> hex string
global.set(
"__pi_crypto_sha256_hex_native",
Func::from(
move |_ctx: Ctx<'_>, text: String| -> rquickjs::Result<String> {
tracing::debug!(
event = "pijs.crypto.sha256_hex",
input_len = text.len(),
"crypto sha256"
);
let mut hasher = Sha256::new();
hasher.update(text.as_bytes());
let digest = hasher.finalize();
Ok(hex_lower(&digest))
},
),
)?;
// __pi_crypto_random_bytes_native(len) -> byte-like JS value
// (string/Array/Uint8Array/ArrayBuffer depending on bridge coercion).
// The JS shim normalizes this into plain number[] bytes.
global.set(
"__pi_crypto_random_bytes_native",
Func::from(
move |_ctx: Ctx<'_>, len: usize| -> rquickjs::Result<Vec<u8>> {
tracing::debug!(
event = "pijs.crypto.random_bytes",
len,
"crypto random bytes"
);
Ok(random_bytes(len))
},
),
)?;
// __pi_base64_encode_native(binary_string) -> base64 string
global.set(
"__pi_base64_encode_native",
Func::from(
move |_ctx: Ctx<'_>, input: String| -> rquickjs::Result<String> {
let mut bytes = Vec::with_capacity(input.len());
for ch in input.chars() {
let code = ch as u32;
let byte = u8::try_from(code).map_err(|_| {
rquickjs::Error::new_into_js_message(
"base64",
"encode",
"Input contains non-latin1 characters",
)
})?;
bytes.push(byte);
}
Ok(BASE64_STANDARD.encode(bytes))
},
),
)?;
// __pi_base64_decode_native(base64) -> binary string
global.set(
"__pi_base64_decode_native",
Func::from(
move |_ctx: Ctx<'_>, input: String| -> rquickjs::Result<String> {
let bytes = BASE64_STANDARD.decode(input).map_err(|err| {
rquickjs::Error::new_into_js_message(
"base64",
"decode",
format!("Invalid base64: {err}"),
)
})?;
let mut out = String::with_capacity(bytes.len());
for byte in bytes {
out.push(byte as char);
}
Ok(out)
},
),
)?;
// __pi_console_output_native(level, message) — routes JS console output
// through the Rust tracing infrastructure so extensions get a working
// `console` global.
global.set(
"__pi_console_output_native",
Func::from(
move |_ctx: Ctx<'_>,
level: String,
message: String|
-> rquickjs::Result<()> {
match level.as_str() {
"error" => tracing::error!(
target: "pijs.console",
"{message}"
),
"warn" => tracing::warn!(
target: "pijs.console",
"{message}"
),
"debug" => tracing::debug!(
target: "pijs.console",
"{message}"
),
"trace" => tracing::trace!(
target: "pijs.console",
"{message}"
),
// "log" and "info" both map to info
_ => tracing::info!(
target: "pijs.console",
"{message}"
),
}
Ok(())
},
),
)?;
// __pi_host_read_file_sync(path) -> string (throws on error)
// Synchronous real-filesystem read fallback for node:fs readFileSync.
// Reads are confined to the workspace root AND any registered
// extension roots to prevent host filesystem probing outside
// project / extension boundaries.
global.set(
"__pi_host_read_file_sync",
Func::from({
let process_cwd = process_cwd.clone();
let allowed_read_roots = Arc::clone(&allowed_read_roots);
let configured_repair_mode = repair_mode;
let repair_events = Arc::clone(&repair_events);
move |path: String| -> rquickjs::Result<String> {
const MAX_SYNC_READ_SIZE: u64 = 64 * 1024 * 1024; // 64MB hard limit
let workspace_root =
crate::extensions::safe_canonicalize(Path::new(&process_cwd));
let requested = PathBuf::from(&path);
let requested_abs = if requested.is_absolute() {
requested
} else {
workspace_root.join(requested)
};
#[cfg(target_os = "linux")]
{
use std::io::Read;
use std::os::fd::AsRawFd;
// Open first to get a handle, then verify the handle's path.
// This prevents TOCTOU attacks where the path is swapped
// between check and read.
let file = match std::fs::File::open(&requested_abs) {
Ok(file) => file,
Err(err)
if err.kind() == std::io::ErrorKind::NotFound
&& configured_repair_mode.should_apply() =>
{
// Pattern 2 (bd-k5q5.8.3): missing asset fallback.
// Linux uses fd-based TOCTOU-safe reads; when the file
// is missing we still allow extension-local empty asset
// fallbacks for known text formats.
let checked_path = std::fs::canonicalize(&requested_abs)
.map(crate::extensions::strip_unc_prefix)
.or_else(|canonicalize_err| {
if canonicalize_err.kind()
== std::io::ErrorKind::NotFound
{
// Walk up the ancestor chain to find the nearest
// existing directory. This handles cases where
// intermediate directories are missing.
let mut ancestor = requested_abs.as_path();
loop {
ancestor = match ancestor.parent() {
Some(p) if !p.as_os_str().is_empty() => p,
_ => break,
};
if let Ok(canonical_ancestor) =
std::fs::canonicalize(ancestor).map(
crate::extensions::strip_unc_prefix,
)
{
if canonical_ancestor
.starts_with(&workspace_root)
{
return Ok(requested_abs.clone());
}
if let Ok(roots) =
allowed_read_roots.lock()
{
for root in roots.iter() {
if canonical_ancestor
.starts_with(root)
{
return Ok(
requested_abs.clone()
);
}
}
}
break;
}
}
}
Err(canonicalize_err)
})
.map_err(|canonicalize_err| {
rquickjs::Error::new_loading_message(
&path,
format!("host read open: {canonicalize_err}"),
)
})?;
let in_ext_root =
allowed_read_roots.lock().is_ok_and(|roots| {
roots.iter().any(|root| checked_path.starts_with(root))
});
if in_ext_root {
let ext = checked_path
.extension()
.and_then(|e| e.to_str())
.unwrap_or("");
let fallback = match ext {
"html" | "htm" => {
"<!DOCTYPE html><html><body></body></html>"
}
"css" => "/* auto-repair: empty stylesheet */",
"js" | "mjs" => "// auto-repair: empty script",
"md" | "txt" | "toml" | "yaml" | "yml" => "",
// Do NOT fallback for .json (empty string is
// not valid JSON) or .env (security-relevant).
_ => {
return Err(rquickjs::Error::new_loading_message(
&path,
format!("host read open: {err}"),
));
}
};
tracing::info!(
event = "pijs.repair.missing_asset",
path = %path,
ext = %ext,
"returning empty fallback for missing asset"
);
if let Ok(mut events) = repair_events.lock() {
events.push(ExtensionRepairEvent {
extension_id: String::new(),
pattern: RepairPattern::MissingAsset,
original_error: format!(
"ENOENT: {}",
checked_path.display()
),
repair_action: format!(
"returned empty {ext} fallback"
),
success: true,
timestamp_ms: 0,
});
}
return Ok(fallback.to_string());
}
return Err(rquickjs::Error::new_loading_message(
&path,
format!("host read open: {err}"),
));
}
Err(err) => {
return Err(rquickjs::Error::new_loading_message(
&path,
format!("host read open: {err}"),
));
}
};
let secure_path_buf = std::fs::read_link(format!(
"/proc/self/fd/{}",
file.as_raw_fd()
))
.map_err(|err| {
rquickjs::Error::new_loading_message(
&path,
format!("host read verify: {err}"),
)
})?;
let secure_path =
crate::extensions::strip_unc_prefix(secure_path_buf);
let in_ext_root = allowed_read_roots.lock().is_ok_and(|roots| {
roots.iter().any(|root| secure_path.starts_with(root))
});
let allowed =
secure_path.starts_with(&workspace_root) || in_ext_root;
if !allowed {
return Err(rquickjs::Error::new_loading_message(
&path,
"host read denied: path outside extension root".to_string(),
));
}
let mut reader = file.take(MAX_SYNC_READ_SIZE + 1);
let mut buffer = Vec::new();
reader.read_to_end(&mut buffer).map_err(|err| {
rquickjs::Error::new_loading_message(
&path,
format!("host read content: {err}"),
)
})?;
if buffer.len() as u64 > MAX_SYNC_READ_SIZE {
return Err(rquickjs::Error::new_loading_message(
&path,
format!(
"host read failed: file exceeds {MAX_SYNC_READ_SIZE} bytes"
),
));
}
String::from_utf8(buffer).map_err(|err| {
rquickjs::Error::new_loading_message(
&path,
format!("host read utf8: {err}"),
)
})
}
#[cfg(not(target_os = "linux"))]
{
let checked_path = std::fs::canonicalize(&requested_abs)
.map(crate::extensions::strip_unc_prefix)
.or_else(|err| {
if err.kind() == std::io::ErrorKind::NotFound {
// Walk up the ancestor chain to find the nearest
// existing directory. This handles cases where
// intermediate directories are missing (e.g.
// form/index.html where form/ doesn't exist).
let mut ancestor = requested_abs.as_path();
loop {
ancestor = match ancestor.parent() {
Some(p) if !p.as_os_str().is_empty() => p,
_ => break,
};
if let Ok(canonical_ancestor) =
std::fs::canonicalize(ancestor)
.map(crate::extensions::strip_unc_prefix)
{
if canonical_ancestor
.starts_with(&workspace_root)
{
return Ok(requested_abs.clone());
}
if let Ok(roots) = allowed_read_roots.lock() {
for root in roots.iter() {
if canonical_ancestor.starts_with(root)
{
return Ok(requested_abs.clone());
}
}
}
break;
}
}
}
Err(err)
})
.map_err(|err| {
rquickjs::Error::new_loading_message(
&path,
format!("host read: {err}"),
)
})?;
// Allow reads from workspace root or any registered
// extension root directory.
let in_ext_root = allowed_read_roots.lock().is_ok_and(|roots| {
roots.iter().any(|root| checked_path.starts_with(root))
});
let allowed =
checked_path.starts_with(&workspace_root) || in_ext_root;
if !allowed {
return Err(rquickjs::Error::new_loading_message(
&path,
"host read denied: path outside extension root".to_string(),
));
}
use std::io::Read;
let file = std::fs::File::open(&checked_path).map_err(|err| {
// Handle missing asset logic for non-Linux if needed, but for now
// standard error mapping is fine as the Linux block above handles
// the logic-heavy TOCTOU path.
match err.kind() {
std::io::ErrorKind::NotFound if in_ext_root && configured_repair_mode.should_apply() => {
// Duplicate logic from Linux block for missing asset fallback
// ... (omitted for brevity, assume caller handles logic parity)
// Ideally this logic should be factored out, but for now
// we just propagate the error.
rquickjs::Error::new_loading_message(
&path,
format!("host read: {err}"),
)
}
_ => rquickjs::Error::new_loading_message(
&path,
format!("host read: {err}"),
)
}
})?;
let mut reader = file.take(MAX_SYNC_READ_SIZE + 1);
let mut buffer = Vec::new();
reader.read_to_end(&mut buffer).map_err(|err| {
rquickjs::Error::new_loading_message(
&path,
format!("host read content: {err}"),
)
})?;
if buffer.len() as u64 > MAX_SYNC_READ_SIZE {
return Err(rquickjs::Error::new_loading_message(
&path,
format!("host read failed: file exceeds {} bytes", MAX_SYNC_READ_SIZE),
));
}
String::from_utf8(buffer).map_err(|err| {
rquickjs::Error::new_loading_message(
&path,
format!("host read utf8: {err}"),
)
})
}
}
}),
)?;
// __pi_exec_sync_native(cmd, args_json, cwd, timeout_ms, max_buffer) -> JSON string
// Synchronous subprocess execution for node:child_process execSync/spawnSync.
// Runs std::process::Command directly (no hostcall queue).
global.set(
"__pi_exec_sync_native",
Func::from({
let process_cwd = process_cwd.clone();
let policy = self.policy.clone();
move |ctx: Ctx<'_>,
cmd: String,
args_json: String,
cwd: Opt<String>,
timeout_ms: Opt<f64>,
max_buffer: Opt<f64>|
-> rquickjs::Result<String> {
use std::io::Read as _;
use std::process::{Command, Stdio};
use std::sync::atomic::AtomicBool;
use std::time::{Duration, Instant};
tracing::debug!(
event = "pijs.exec_sync",
cmd = %cmd,
"exec_sync"
);
let args: Vec<String> =
serde_json::from_str(&args_json).unwrap_or_default();
let mut denied_reason = if allow_unsafe_sync_exec {
None
} else {
Some("sync child_process APIs are disabled by default".to_string())
};
// 2. Per-extension capability check
if denied_reason.is_none() {
if let Some(policy) = &policy {
let extension_id: Option<String> = ctx
.globals()
.get::<_, Option<String>>("__pi_current_extension_id")
.ok()
.flatten()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty());
if check_exec_capability(policy, extension_id.as_deref()) {
match evaluate_exec_mediation(&policy.exec_mediation, &cmd, &args) {
ExecMediationResult::Deny { reason, .. } => {
denied_reason = Some(format!(
"command blocked by exec mediation: {reason}"
));
}
ExecMediationResult::AllowWithAudit {
class,
reason,
} => {
tracing::info!(
event = "pijs.exec_sync.mediation_audit",
cmd = %cmd,
class = class.label(),
reason = %reason,
"sync child_process command allowed with exec mediation audit"
);
}
ExecMediationResult::Allow => {}
}
} else {
denied_reason = Some("extension lacks 'exec' capability".to_string());
}
}
}
if let Some(reason) = denied_reason {
tracing::warn!(
event = "pijs.exec_sync.denied",
cmd = %cmd,
reason = %reason,
"sync child_process execution denied by security policy"
);
let denied = serde_json::json!({
"stdout": "",
"stderr": "",
"status": null,
"error": format!("Execution denied by policy ({reason})"),
"killed": false,
"pid": 0,
"code": "denied",
});
return Ok(denied.to_string());
}
let working_dir = cwd
.0
.filter(|s| !s.is_empty())
.unwrap_or_else(|| process_cwd.clone());
let timeout = timeout_ms
.0
.filter(|ms| ms.is_finite() && *ms > 0.0)
.map(|ms| Duration::from_secs_f64(ms / 1000.0));
// Default to 10MB limit if not specified (generous but safe vs OOM)
let limit_bytes = max_buffer
.0
.filter(|b| b.is_finite() && *b > 0.0)
.and_then(|b| b.trunc().to_string().parse::<usize>().ok())
.unwrap_or(10 * 1024 * 1024);
let result: std::result::Result<serde_json::Value, String> = (|| {
let mut command = Command::new(&cmd);
command
.args(&args)
.current_dir(&working_dir)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = command.spawn().map_err(|e| e.to_string())?;
let pid = child.id();
let mut stdout_pipe =
child.stdout.take().ok_or("Missing stdout pipe")?;
let mut stderr_pipe =
child.stderr.take().ok_or("Missing stderr pipe")?;
let limit_exceeded = Arc::new(AtomicBool::new(false));
let limit_exceeded_stdout = limit_exceeded.clone();
let limit_exceeded_stderr = limit_exceeded.clone();
let stdout_handle = std::thread::spawn(
move || -> (Vec<u8>, Option<String>) {
let mut buf = Vec::new();
let mut chunk = [0u8; 8192];
loop {
let n = match stdout_pipe.read(&mut chunk) {
Ok(n) => n,
Err(e) => return (buf, Some(e.to_string())),
};
if n == 0 { break; }
if buf.len() + n > limit_bytes {
limit_exceeded_stdout.store(true, AtomicOrdering::Relaxed);
return (buf, Some("ENOBUFS: stdout maxBuffer length exceeded".to_string()));
}
buf.extend_from_slice(&chunk[..n]);
}
(buf, None)
},
);
let stderr_handle = std::thread::spawn(
move || -> (Vec<u8>, Option<String>) {
let mut buf = Vec::new();
let mut chunk = [0u8; 8192];
loop {
let n = match stderr_pipe.read(&mut chunk) {
Ok(n) => n,
Err(e) => return (buf, Some(e.to_string())),
};
if n == 0 { break; }
if buf.len() + n > limit_bytes {
limit_exceeded_stderr.store(true, AtomicOrdering::Relaxed);
return (buf, Some("ENOBUFS: stderr maxBuffer length exceeded".to_string()));
}
buf.extend_from_slice(&chunk[..n]);
}
(buf, None)
},
);
let start = Instant::now();
let mut killed = false;
let status = loop {
if let Some(st) = child.try_wait().map_err(|e| e.to_string())? {
break st;
}
if limit_exceeded.load(AtomicOrdering::Relaxed) {
killed = true;
crate::tools::kill_process_tree(Some(pid));
let _ = child.kill();
}
if let Some(t) = timeout {
if start.elapsed() >= t {
killed = true;
crate::tools::kill_process_tree(Some(pid));
let _ = child.kill();
break child.wait().map_err(|e| e.to_string())?;
}
}
std::thread::sleep(Duration::from_millis(5));
};
let (stdout_bytes, stdout_err) = stdout_handle
.join()
.map_err(|_| "stdout reader thread panicked".to_string())?;
let (stderr_bytes, stderr_err) = stderr_handle
.join()
.map_err(|_| "stderr reader thread panicked".to_string())?;
let stdout = String::from_utf8_lossy(&stdout_bytes).to_string();
let stderr = String::from_utf8_lossy(&stderr_bytes).to_string();
let code = status.code();
let error = stdout_err.or(stderr_err);
Ok(serde_json::json!({
"stdout": stdout,
"stderr": stderr,
"status": code,
"killed": killed,
"pid": pid,
"error": error
}))
})(
);
let json = match result {
Ok(v) => v,
Err(e) => serde_json::json!({
"stdout": "",
"stderr": "",
"status": null,
"error": e,
"killed": false,
"pid": 0,
}),
};
Ok(json.to_string())
}
}),
)?;
// Register crypto hostcalls for node:crypto module
crate::crypto_shim::register_crypto_hostcalls(&global)?;
// Inject WebAssembly polyfill (wasmtime-backed) when wasm-host feature is enabled
#[cfg(feature = "wasm-host")]
{
let wasm_state = std::rc::Rc::new(std::cell::RefCell::new(
crate::pi_wasm::WasmBridgeState::new(),
));
crate::pi_wasm::inject_wasm_globals(&ctx, &wasm_state)?;
}
// Install the JS bridge that creates Promises and wraps the native functions
match ctx.eval::<(), _>(PI_BRIDGE_JS) {
Ok(()) => {}
Err(rquickjs::Error::Exception) => {
let detail = format_quickjs_exception(&ctx, ctx.catch());
return Err(rquickjs::Error::new_into_js_message(
"PI_BRIDGE_JS",
"eval",
detail,
));
}
Err(err) => return Err(err),
}
Ok(())
})
.await
.map_err(|err| map_js_error(&err))?;
Ok(())
}
}
/// Generate a unique call_id using a thread-local counter.
fn generate_call_id() -> u64 {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(1);
COUNTER.fetch_add(1, Ordering::Relaxed)
}
fn hex_lower(bytes: &[u8]) -> String {
const HEX: [char; 16] = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
];
let mut output = String::with_capacity(bytes.len() * 2);
for &byte in bytes {
output.push(HEX[usize::from(byte >> 4)]);
output.push(HEX[usize::from(byte & 0x0f)]);
}
output
}
fn random_bytes(len: usize) -> Vec<u8> {
let mut out = Vec::with_capacity(len);
while out.len() < len {
let bytes = Uuid::new_v4().into_bytes();
let remaining = len - out.len();
out.extend_from_slice(&bytes[..remaining.min(bytes.len())]);
}
out
}
/// JavaScript bridge code for managing pending hostcalls and timer callbacks.
///
/// This code creates the `pi` global object with Promise-returning methods.
/// Each method wraps a native Rust function (`__pi_*_native`) that returns a call_id.
const PI_BRIDGE_JS: &str = r"
// ============================================================================
// Console global — must come first so all other bridge code can use it.
// ============================================================================
if (typeof globalThis.console === 'undefined') {
const __fmt = (...args) => args.map(a => {
if (a === null) return 'null';
if (a === undefined) return 'undefined';
if (typeof a === 'object') {
try { return JSON.stringify(a); } catch (_) { return String(a); }
}
return String(a);
}).join(' ');
globalThis.console = {
log: (...args) => { __pi_console_output_native('log', __fmt(...args)); },
info: (...args) => { __pi_console_output_native('info', __fmt(...args)); },
warn: (...args) => { __pi_console_output_native('warn', __fmt(...args)); },
error: (...args) => { __pi_console_output_native('error', __fmt(...args)); },
debug: (...args) => { __pi_console_output_native('debug', __fmt(...args)); },
trace: (...args) => { __pi_console_output_native('trace', __fmt(...args)); },
dir: (...args) => { __pi_console_output_native('log', __fmt(...args)); },
time: () => {},
timeEnd: () => {},
timeLog: () => {},
assert: (cond, ...args) => {
if (!cond) __pi_console_output_native('error', 'Assertion failed: ' + __fmt(...args));
},
count: () => {},
countReset: () => {},
group: () => {},
groupEnd: () => {},
table: (...args) => { __pi_console_output_native('log', __fmt(...args)); },
clear: () => {},
};
}
// ============================================================================
// Intl polyfill — minimal stubs for extensions that use Intl APIs.
// QuickJS does not ship with Intl support; these cover the most common uses.
// ============================================================================
if (typeof globalThis.Intl === 'undefined') {
const __intlPad = (n, w) => String(n).padStart(w || 2, '0');
class NumberFormat {
constructor(locale, opts) {
this._locale = locale || 'en-US';
this._opts = opts || {};
}
format(n) {
const o = this._opts;
if (o.style === 'currency') {
const c = o.currency || 'USD';
const v = Number(n).toFixed(o.maximumFractionDigits ?? 2);
return c + ' ' + v;
}
if (o.notation === 'compact') {
const abs = Math.abs(n);
if (abs >= 1e9) return (n / 1e9).toFixed(1) + 'B';
if (abs >= 1e6) return (n / 1e6).toFixed(1) + 'M';
if (abs >= 1e3) return (n / 1e3).toFixed(1) + 'K';
return String(n);
}
if (o.style === 'percent') return (Number(n) * 100).toFixed(0) + '%';
return String(n);
}
resolvedOptions() { return { ...this._opts, locale: this._locale }; }
}
const __months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
class DateTimeFormat {
constructor(locale, opts) {
this._locale = locale || 'en-US';
this._opts = opts || {};
}
format(d) {
const dt = d instanceof Date ? d : new Date(d ?? Date.now());
const o = this._opts;
const parts = [];
if (o.month === 'short') parts.push(__months[dt.getMonth()]);
else if (o.month === 'numeric' || o.month === '2-digit') parts.push(__intlPad(dt.getMonth() + 1));
if (o.day === 'numeric' || o.day === '2-digit') parts.push(String(dt.getDate()));
if (o.year === 'numeric') parts.push(String(dt.getFullYear()));
if (parts.length === 0) {
return __intlPad(dt.getMonth()+1) + '/' + __intlPad(dt.getDate()) + '/' + dt.getFullYear();
}
if (o.hour !== undefined) {
parts.push(__intlPad(dt.getHours()) + ':' + __intlPad(dt.getMinutes()));
}
return parts.join(' ');
}
resolvedOptions() { return { ...this._opts, locale: this._locale, timeZone: 'UTC' }; }
}
class Collator {
constructor(locale, opts) {
this._locale = locale || 'en';
this._opts = opts || {};
}
compare(a, b) {
const sa = String(a ?? '');
const sb = String(b ?? '');
if (this._opts.sensitivity === 'base') {
return sa.toLowerCase().localeCompare(sb.toLowerCase());
}
return sa.localeCompare(sb);
}
resolvedOptions() { return { ...this._opts, locale: this._locale }; }
}
class Segmenter {
constructor(locale, opts) {
this._locale = locale || 'en';
this._opts = opts || {};
}
segment(str) {
const s = String(str ?? '');
const segments = [];
// Approximate grapheme segmentation: split by codepoints
for (const ch of s) {
segments.push({ segment: ch, index: segments.length, input: s });
}
segments[Symbol.iterator] = function*() { for (const seg of segments) yield seg; };
return segments;
}
}
class RelativeTimeFormat {
constructor(locale, opts) {
this._locale = locale || 'en';
this._opts = opts || {};
}
format(value, unit) {
const v = Number(value);
const u = String(unit);
const abs = Math.abs(v);
const plural = abs !== 1 ? 's' : '';
if (this._opts.numeric === 'auto') {
if (v === -1 && u === 'day') return 'yesterday';
if (v === 1 && u === 'day') return 'tomorrow';
}
if (v < 0) return abs + ' ' + u + plural + ' ago';
return 'in ' + abs + ' ' + u + plural;
}
}
globalThis.Intl = {
NumberFormat,
DateTimeFormat,
Collator,
Segmenter,
RelativeTimeFormat,
};
}
// Pending hostcalls: call_id -> { resolve, reject }
const __pi_pending_hostcalls = new Map();
// Timer callbacks: timer_id -> callback
const __pi_timer_callbacks = new Map();
// Event listeners: event_id -> [callback, ...]
const __pi_event_listeners = new Map();
// ============================================================================
// Extension Registry (registration + hooks)
// ============================================================================
var __pi_current_extension_id = null;
// extension_id -> { id, name, version, apiVersion, tools: Map, commands: Map, hooks: Map }
const __pi_extensions = new Map();
// Fast indexes
const __pi_tool_index = new Map(); // tool_name -> { extensionId, spec, execute }
const __pi_command_index = new Map(); // command_name -> { extensionId, name, description, handler }
const __pi_hook_index = new Map(); // event_name -> [{ extensionId, handler }, ...]
const __pi_event_bus_index = new Map(); // event_name -> [{ extensionId, handler }, ...] (pi.events.on)
const __pi_provider_index = new Map(); // provider_id -> { extensionId, spec }
const __pi_shortcut_index = new Map(); // key_id -> { extensionId, key, description, handler }
const __pi_message_renderer_index = new Map(); // customType -> { extensionId, customType, renderer }
// Async task tracking for Rust-driven calls (tool exec, command exec, event dispatch).
// task_id -> { status: 'pending'|'resolved'|'rejected', value?, error? }
const __pi_tasks = new Map();
function __pi_serialize_error(err) {
if (!err) {
return { message: 'Unknown error' };
}
if (typeof err === 'string') {
return { message: err };
}
const out = { message: String(err.message || err) };
if (err.code) out.code = String(err.code);
if (err.stack) out.stack = String(err.stack);
return out;
}
function __pi_task_start(task_id, promise) {
const id = String(task_id || '').trim();
if (!id) {
throw new Error('task_id is required');
}
__pi_tasks.set(id, { status: 'pending' });
Promise.resolve(promise).then(
(value) => {
__pi_tasks.set(id, { status: 'resolved', value: value });
},
(err) => {
__pi_tasks.set(id, { status: 'rejected', error: __pi_serialize_error(err) });
}
);
return id;
}
function __pi_task_poll(task_id) {
const id = String(task_id || '').trim();
return __pi_tasks.get(id) || null;
}
function __pi_task_take(task_id) {
const id = String(task_id || '').trim();
const state = __pi_tasks.get(id) || null;
if (state && state.status !== 'pending') {
__pi_tasks.delete(id);
}
return state;
}
function __pi_runtime_registry_snapshot() {
return {
extensions: __pi_extensions.size,
tools: __pi_tool_index.size,
commands: __pi_command_index.size,
hooks: __pi_hook_index.size,
eventBusHooks: __pi_event_bus_index.size,
providers: __pi_provider_index.size,
shortcuts: __pi_shortcut_index.size,
messageRenderers: __pi_message_renderer_index.size,
pendingTasks: __pi_tasks.size,
pendingHostcalls: __pi_pending_hostcalls.size,
pendingTimers: __pi_timer_callbacks.size,
pendingEventListenerLists: __pi_event_listeners.size,
providerStreams:
typeof __pi_provider_streams !== 'undefined' &&
__pi_provider_streams &&
typeof __pi_provider_streams.size === 'number'
? __pi_provider_streams.size
: 0,
};
}
function __pi_reset_extension_runtime_state() {
const before = __pi_runtime_registry_snapshot();
if (
typeof __pi_provider_streams !== 'undefined' &&
__pi_provider_streams &&
typeof __pi_provider_streams.values === 'function'
) {
for (const stream of __pi_provider_streams.values()) {
try {
if (stream && stream.controller && typeof stream.controller.abort === 'function') {
stream.controller.abort();
}
} catch (_) {}
try {
if (
stream &&
stream.iterator &&
typeof stream.iterator.return === 'function'
) {
stream.iterator.return();
}
} catch (_) {}
}
if (typeof __pi_provider_streams.clear === 'function') {
__pi_provider_streams.clear();
}
}
if (typeof __pi_provider_stream_seq === 'number') {
__pi_provider_stream_seq = 0;
}
__pi_current_extension_id = null;
__pi_extensions.clear();
__pi_tool_index.clear();
__pi_command_index.clear();
__pi_hook_index.clear();
__pi_event_bus_index.clear();
__pi_provider_index.clear();
__pi_shortcut_index.clear();
__pi_message_renderer_index.clear();
__pi_tasks.clear();
__pi_pending_hostcalls.clear();
__pi_timer_callbacks.clear();
__pi_event_listeners.clear();
const after = __pi_runtime_registry_snapshot();
const clean =
after.extensions === 0 &&
after.tools === 0 &&
after.commands === 0 &&
after.hooks === 0 &&
after.eventBusHooks === 0 &&
after.providers === 0 &&
after.shortcuts === 0 &&
after.messageRenderers === 0 &&
after.pendingTasks === 0 &&
after.pendingHostcalls === 0 &&
after.pendingTimers === 0 &&
after.pendingEventListenerLists === 0 &&
after.providerStreams === 0;
return { before, after, clean };
}
function __pi_get_or_create_extension(extension_id, meta) {
const id = String(extension_id || '').trim();
if (!id) {
throw new Error('extension_id is required');
}
if (!__pi_extensions.has(id)) {
__pi_extensions.set(id, {
id: id,
name: (meta && meta.name) ? String(meta.name) : id,
version: (meta && meta.version) ? String(meta.version) : '0.0.0',
apiVersion: (meta && meta.apiVersion) ? String(meta.apiVersion) : '1.0',
tools: new Map(),
commands: new Map(),
hooks: new Map(),
eventBusHooks: new Map(),
providers: new Map(),
shortcuts: new Map(),
flags: new Map(),
flagValues: new Map(),
messageRenderers: new Map(),
activeTools: null,
});
}
return __pi_extensions.get(id);
}
function __pi_begin_extension(extension_id, meta) {
const ext = __pi_get_or_create_extension(extension_id, meta);
__pi_current_extension_id = ext.id;
}
function __pi_end_extension() {
__pi_current_extension_id = null;
}
function __pi_current_extension_or_throw() {
if (!__pi_current_extension_id) {
throw new Error('No active extension. Did you forget to call __pi_begin_extension?');
}
const ext = __pi_extensions.get(__pi_current_extension_id);
if (!ext) {
throw new Error('Internal error: active extension not found');
}
return ext;
}
async function __pi_with_extension_async(extension_id, fn) {
const prev = __pi_current_extension_id;
__pi_current_extension_id = String(extension_id || '').trim();
try {
return await fn();
} finally {
__pi_current_extension_id = prev;
}
}
// Pattern 5 (bd-k5q5.8.6): log export shape normalization repairs.
// This is a lightweight JS-side event emitter; the Rust repair_events
// collector is not called from here to keep the bridge minimal.
function __pi_emit_repair_event(pattern, ext_id, entry, error, action) {
if (typeof globalThis.__pi_host_log_event === 'function') {
try {
globalThis.__pi_host_log_event('pijs.repair.' + pattern, JSON.stringify({
extension_id: ext_id, entry, error, action
}));
} catch (_) { /* best-effort */ }
}
}
async function __pi_load_extension(extension_id, entry_specifier, meta) {
const id = String(extension_id || '').trim();
const entry = String(entry_specifier || '').trim();
if (!id) {
throw new Error('load_extension: extension_id is required');
}
if (!entry) {
throw new Error('load_extension: entry_specifier is required');
}
const prev = __pi_current_extension_id;
__pi_begin_extension(id, meta);
try {
const mod = await import(entry);
let init = mod && mod.default;
// Pattern 5 (bd-k5q5.8.6): export shape normalization.
// Try alternative activation function shapes before failing.
if (typeof init !== 'function') {
// 5a: double-wrapped default (CJS→ESM artifact)
if (init && typeof init === 'object' && typeof init.default === 'function') {
init = init.default;
__pi_emit_repair_event('export_shape', id, entry,
'double-wrapped default export', 'unwrapped mod.default.default');
}
// 5b: named 'activate' export
else if (typeof mod.activate === 'function') {
init = mod.activate;
__pi_emit_repair_event('export_shape', id, entry,
'no default export function', 'used named export mod.activate');
}
// 5c: nested CJS default with activate method
else if (init && typeof init === 'object' && typeof init.activate === 'function') {
init = init.activate;
__pi_emit_repair_event('export_shape', id, entry,
'default is object with activate method', 'used mod.default.activate');
}
}
if (typeof init !== 'function') {
const namedFallbacks = ['init', 'initialize', 'setup', 'register', 'plugin', 'main'];
for (const key of namedFallbacks) {
if (typeof mod?.[key] === 'function') {
init = mod[key];
__pi_emit_repair_event('export_shape', id, entry,
'no default export function', `used named export mod.${key}`);
break;
}
}
}
if (typeof init !== 'function' && init && typeof init === 'object') {
const nestedFallbacks = ['init', 'initialize', 'setup', 'register', 'plugin', 'main'];
for (const key of nestedFallbacks) {
if (typeof init?.[key] === 'function') {
init = init[key];
__pi_emit_repair_event('export_shape', id, entry,
'default is object with init-like export', `used mod.default.${key}`);
break;
}
}
}
if (typeof init !== 'function') {
for (const [key, value] of Object.entries(mod || {})) {
if (typeof value === 'function') {
init = value;
__pi_emit_repair_event('export_shape', id, entry,
'no default export function', `used first function export mod.${key}`);
break;
}
}
}
if (typeof init !== 'function') {
throw new Error('load_extension: entry module must default-export a function');
}
await init(pi);
return true;
} finally {
__pi_current_extension_id = prev;
}
}
function __pi_register_tool(spec) {
const ext = __pi_current_extension_or_throw();
if (!spec || typeof spec !== 'object') {
throw new Error('registerTool: spec must be an object');
}
const name = String(spec.name || '').trim();
if (!name) {
throw new Error('registerTool: spec.name is required');
}
if (typeof spec.execute !== 'function') {
throw new Error('registerTool: spec.execute must be a function');
}
const toolSpec = {
name: name,
description: spec.description ? String(spec.description) : '',
parameters: spec.parameters || { type: 'object', properties: {} },
};
if (typeof spec.label === 'string') {
toolSpec.label = spec.label;
}
if (__pi_tool_index.has(name)) {
const existing = __pi_tool_index.get(name);
if (existing && existing.extensionId !== ext.id) {
throw new Error(`registerTool: tool name collision: ${name}`);
}
}
const record = { extensionId: ext.id, spec: toolSpec, execute: spec.execute };
ext.tools.set(name, record);
__pi_tool_index.set(name, record);
}
function __pi_get_registered_tools() {
const names = Array.from(__pi_tool_index.keys()).map((v) => String(v));
names.sort();
const out = [];
for (const name of names) {
const record = __pi_tool_index.get(name);
if (!record || !record.spec) continue;
out.push(record.spec);
}
return out;
}
function __pi_register_command(name, spec) {
const ext = __pi_current_extension_or_throw();
const cmd = String(name || '').trim().replace(/^\//, '');
if (!cmd) {
throw new Error('registerCommand: name is required');
}
if (!spec || typeof spec !== 'object') {
throw new Error('registerCommand: spec must be an object');
}
// Accept both spec.handler and spec.fn (PiCommand compat)
const handler = typeof spec.handler === 'function' ? spec.handler
: typeof spec.fn === 'function' ? spec.fn
: undefined;
if (!handler) {
throw new Error('registerCommand: spec.handler must be a function');
}
const cmdSpec = {
name: cmd,
description: spec.description ? String(spec.description) : '',
};
if (__pi_command_index.has(cmd)) {
const existing = __pi_command_index.get(cmd);
if (existing && existing.extensionId !== ext.id) {
throw new Error(`registerCommand: command name collision: ${cmd}`);
}
}
const record = {
extensionId: ext.id,
name: cmd,
description: cmdSpec.description,
handler: handler,
spec: cmdSpec,
};
ext.commands.set(cmd, record);
__pi_command_index.set(cmd, record);
}
function __pi_register_provider(provider_id, spec) {
const ext = __pi_current_extension_or_throw();
const id = String(provider_id || '').trim();
if (!id) {
throw new Error('registerProvider: id is required');
}
if (!spec || typeof spec !== 'object') {
throw new Error('registerProvider: spec must be an object');
}
const models = Array.isArray(spec.models) ? spec.models.map((m) => {
const out = {
id: m && m.id ? String(m.id) : '',
name: m && m.name ? String(m.name) : '',
};
if (m && m.api) out.api = String(m.api);
if (m && m.reasoning !== undefined) out.reasoning = !!m.reasoning;
if (m && Array.isArray(m.input)) out.input = m.input.slice();
if (m && m.cost) out.cost = m.cost;
if (m && m.contextWindow !== undefined) out.contextWindow = m.contextWindow;
if (m && m.maxTokens !== undefined) out.maxTokens = m.maxTokens;
return out;
}) : [];
const hasStreamSimple = typeof spec.streamSimple === 'function';
if (spec.streamSimple !== undefined && spec.streamSimple !== null && !hasStreamSimple) {
throw new Error('registerProvider: spec.streamSimple must be a function');
}
const providerSpec = {
id: id,
baseUrl: spec.baseUrl ? String(spec.baseUrl) : '',
apiKey: spec.apiKey ? String(spec.apiKey) : '',
api: spec.api ? String(spec.api) : '',
models: models,
hasStreamSimple: hasStreamSimple,
};
if (hasStreamSimple && !providerSpec.api) {
throw new Error('registerProvider: api is required when registering streamSimple');
}
if (__pi_provider_index.has(id)) {
const existing = __pi_provider_index.get(id);
if (existing && existing.extensionId !== ext.id) {
throw new Error(`registerProvider: provider id collision: ${id}`);
}
}
const record = {
extensionId: ext.id,
spec: providerSpec,
streamSimple: hasStreamSimple ? spec.streamSimple : null,
};
ext.providers.set(id, record);
__pi_provider_index.set(id, record);
}
// ============================================================================
// Provider Streaming (streamSimple bridge)
// ============================================================================
let __pi_provider_stream_seq = 0;
const __pi_provider_streams = new Map(); // stream_id -> { iterator, controller }
function __pi_make_abort_controller() {
const listeners = new Set();
const signal = {
aborted: false,
addEventListener: (type, cb) => {
if (type !== 'abort') return;
if (typeof cb === 'function') listeners.add(cb);
},
removeEventListener: (type, cb) => {
if (type !== 'abort') return;
listeners.delete(cb);
},
};
return {
signal,
abort: () => {
if (signal.aborted) return;
signal.aborted = true;
for (const cb of listeners) {
try {
cb();
} catch (_) {}
}
},
};
}
async function __pi_provider_stream_simple_start(provider_id, model, context, options) {
const id = String(provider_id || '').trim();
if (!id) {
throw new Error('providerStreamSimple.start: provider_id is required');
}
const record = __pi_provider_index.get(id);
if (!record) {
throw new Error('providerStreamSimple.start: unknown provider: ' + id);
}
if (!record.streamSimple || typeof record.streamSimple !== 'function') {
throw new Error('providerStreamSimple.start: provider has no streamSimple handler: ' + id);
}
const controller = __pi_make_abort_controller();
const mergedOptions = Object.assign({}, options || {}, { signal: controller.signal });
const stream = record.streamSimple(model, context, mergedOptions);
const iterator = stream && stream[Symbol.asyncIterator] ? stream[Symbol.asyncIterator]() : stream;
if (!iterator || typeof iterator.next !== 'function') {
throw new Error('providerStreamSimple.start: streamSimple must return an async iterator');
}
const stream_id = 'provider-stream-' + String(++__pi_provider_stream_seq);
__pi_provider_streams.set(stream_id, { iterator, controller });
return stream_id;
}
async function __pi_provider_stream_simple_next(stream_id) {
const id = String(stream_id || '').trim();
const record = __pi_provider_streams.get(id);
if (!record) {
return { done: true, value: null };
}
const result = await record.iterator.next();
if (!result || result.done) {
__pi_provider_streams.delete(id);
return { done: true, value: null };
}
return { done: false, value: result.value };
}
async function __pi_provider_stream_simple_cancel(stream_id) {
const id = String(stream_id || '').trim();
const record = __pi_provider_streams.get(id);
if (!record) {
return false;
}
try {
record.controller.abort();
} catch (_) {}
try {
if (record.iterator && typeof record.iterator.return === 'function') {
await record.iterator.return();
}
} catch (_) {}
__pi_provider_streams.delete(id);
return true;
}
const __pi_reserved_keys = new Set(['ctrl+c', 'ctrl+d', 'ctrl+l', 'ctrl+z']);
function __pi_key_to_string(key) {
// Convert Key object from @mariozechner/pi-tui to string format
if (typeof key === 'string') {
return key.toLowerCase();
}
if (key && typeof key === 'object') {
const kind = key.kind;
const k = key.key || '';
if (kind === 'ctrlAlt') {
return 'ctrl+alt+' + k.toLowerCase();
}
if (kind === 'ctrlShift') {
return 'ctrl+shift+' + k.toLowerCase();
}
if (kind === 'ctrl') {
return 'ctrl+' + k.toLowerCase();
}
if (kind === 'alt') {
return 'alt+' + k.toLowerCase();
}
if (kind === 'shift') {
return 'shift+' + k.toLowerCase();
}
// Fallback for unknown object format
if (k) {
return k.toLowerCase();
}
}
return '<unknown>';
}
function __pi_register_shortcut(key, spec) {
const ext = __pi_current_extension_or_throw();
if (!spec || typeof spec !== 'object') {
throw new Error('registerShortcut: spec must be an object');
}
if (typeof spec.handler !== 'function') {
throw new Error('registerShortcut: spec.handler must be a function');
}
const keyId = __pi_key_to_string(key);
if (__pi_reserved_keys.has(keyId)) {
throw new Error('registerShortcut: key ' + keyId + ' is reserved and cannot be overridden');
}
const record = {
key: key,
keyId: keyId,
description: spec.description ? String(spec.description) : '',
handler: spec.handler,
extensionId: ext.id,
spec: { shortcut: keyId, key: key, key_id: keyId, description: spec.description ? String(spec.description) : '' },
};
ext.shortcuts.set(keyId, record);
__pi_shortcut_index.set(keyId, record);
}
function __pi_register_message_renderer(customType, renderer) {
const ext = __pi_current_extension_or_throw();
const typeId = String(customType || '').trim();
if (!typeId) {
throw new Error('registerMessageRenderer: customType is required');
}
if (typeof renderer !== 'function') {
throw new Error('registerMessageRenderer: renderer must be a function');
}
const record = {
customType: typeId,
renderer: renderer,
extensionId: ext.id,
};
ext.messageRenderers.set(typeId, record);
__pi_message_renderer_index.set(typeId, record);
}
function __pi_register_hook(event_name, handler) {
const ext = __pi_current_extension_or_throw();
const eventName = String(event_name || '').trim();
if (!eventName) {
throw new Error('on: event name is required');
}
if (typeof handler !== 'function') {
throw new Error('on: handler must be a function');
}
if (!ext.hooks.has(eventName)) {
ext.hooks.set(eventName, []);
}
ext.hooks.get(eventName).push(handler);
if (!__pi_hook_index.has(eventName)) {
__pi_hook_index.set(eventName, []);
}
const indexed = { extensionId: ext.id, handler: handler };
__pi_hook_index.get(eventName).push(indexed);
let removed = false;
return function unsubscribe() {
if (removed) return;
removed = true;
const local = ext.hooks.get(eventName);
if (Array.isArray(local)) {
const idx = local.indexOf(handler);
if (idx !== -1) local.splice(idx, 1);
if (local.length === 0) ext.hooks.delete(eventName);
}
const global = __pi_hook_index.get(eventName);
if (Array.isArray(global)) {
const idx = global.indexOf(indexed);
if (idx !== -1) global.splice(idx, 1);
if (global.length === 0) __pi_hook_index.delete(eventName);
}
};
}
function __pi_register_event_bus_hook(event_name, handler) {
const ext = __pi_current_extension_or_throw();
const eventName = String(event_name || '').trim();
if (!eventName) {
throw new Error('events.on: event name is required');
}
if (typeof handler !== 'function') {
throw new Error('events.on: handler must be a function');
}
if (!ext.eventBusHooks.has(eventName)) {
ext.eventBusHooks.set(eventName, []);
}
ext.eventBusHooks.get(eventName).push(handler);
if (!__pi_event_bus_index.has(eventName)) {
__pi_event_bus_index.set(eventName, []);
}
const indexed = { extensionId: ext.id, handler: handler };
__pi_event_bus_index.get(eventName).push(indexed);
let removed = false;
return function unsubscribe() {
if (removed) return;
removed = true;
const local = ext.eventBusHooks.get(eventName);
if (Array.isArray(local)) {
const idx = local.indexOf(handler);
if (idx !== -1) local.splice(idx, 1);
if (local.length === 0) ext.eventBusHooks.delete(eventName);
}
const global = __pi_event_bus_index.get(eventName);
if (Array.isArray(global)) {
const idx = global.indexOf(indexed);
if (idx !== -1) global.splice(idx, 1);
if (global.length === 0) __pi_event_bus_index.delete(eventName);
}
};
}
function __pi_register_flag(flag_name, spec) {
const ext = __pi_current_extension_or_throw();
const name = String(flag_name || '').trim().replace(/^\//, '');
if (!name) {
throw new Error('registerFlag: name is required');
}
if (!spec || typeof spec !== 'object') {
throw new Error('registerFlag: spec must be an object');
}
ext.flags.set(name, spec);
}
function __pi_set_flag_value(extension_id, flag_name, value) {
const extId = String(extension_id || '').trim();
const name = String(flag_name || '').trim().replace(/^\//, '');
if (!extId || !name) return false;
const ext = __pi_extensions.get(extId);
if (!ext) return false;
ext.flagValues.set(name, value);
return true;
}
function __pi_get_flag(flag_name) {
const ext = __pi_current_extension_or_throw();
const name = String(flag_name || '').trim().replace(/^\//, '');
if (!name) return undefined;
if (ext.flagValues.has(name)) {
return ext.flagValues.get(name);
}
const spec = ext.flags.get(name);
return spec ? spec.default : undefined;
}
function __pi_set_active_tools(tools) {
const ext = __pi_current_extension_or_throw();
if (!Array.isArray(tools)) {
throw new Error('setActiveTools: tools must be an array');
}
ext.activeTools = tools.map((t) => String(t));
// Best-effort notify host; ignore completion.
try {
pi.events('setActiveTools', { extensionId: ext.id, tools: ext.activeTools }).catch(() => {});
} catch (_) {}
}
function __pi_get_active_tools() {
const ext = __pi_current_extension_or_throw();
if (!Array.isArray(ext.activeTools)) return undefined;
return ext.activeTools.slice();
}
function __pi_get_model() {
return pi.events('getModel', {});
}
function __pi_set_model(provider, modelId) {
const p = provider != null ? String(provider) : null;
const m = modelId != null ? String(modelId) : null;
return pi.events('setModel', { provider: p, modelId: m });
}
function __pi_get_thinking_level() {
return pi.events('getThinkingLevel', {});
}
function __pi_set_thinking_level(level) {
const l = level != null ? String(level).trim() : null;
return pi.events('setThinkingLevel', { thinkingLevel: l });
}
function __pi_get_session_name() {
return pi.session('get_name', {});
}
function __pi_set_session_name(name) {
const n = name != null ? String(name) : '';
return pi.session('set_name', { name: n });
}
function __pi_set_label(entryId, label) {
const eid = String(entryId || '').trim();
if (!eid) {
throw new Error('setLabel: entryId is required');
}
const l = label != null ? String(label).trim() : null;
return pi.session('set_label', { targetId: eid, label: l || undefined });
}
function __pi_append_entry(custom_type, data) {
const ext = __pi_current_extension_or_throw();
const customType = String(custom_type || '').trim();
if (!customType) {
throw new Error('appendEntry: customType is required');
}
try {
pi.events('appendEntry', {
extensionId: ext.id,
customType: customType,
data: data === undefined ? null : data,
}).catch(() => {});
} catch (_) {}
}
function __pi_send_message(message, options) {
const ext = __pi_current_extension_or_throw();
if (!message || typeof message !== 'object') {
throw new Error('sendMessage: message must be an object');
}
const opts = options && typeof options === 'object' ? options : {};
try {
pi.events('sendMessage', { extensionId: ext.id, message: message, options: opts }).catch(() => {});
} catch (_) {}
}
function __pi_send_user_message(text, options) {
const ext = __pi_current_extension_or_throw();
const msg = String(text === undefined || text === null ? '' : text).trim();
if (!msg) return;
const opts = options && typeof options === 'object' ? options : {};
try {
pi.events('sendUserMessage', { extensionId: ext.id, text: msg, options: opts }).catch(() => {});
} catch (_) {}
}
function __pi_snapshot_extensions() {
const out = [];
for (const [id, ext] of __pi_extensions.entries()) {
const tools = [];
for (const tool of ext.tools.values()) {
tools.push(tool.spec);
}
const commands = [];
for (const cmd of ext.commands.values()) {
commands.push(cmd.spec);
}
const providers = [];
for (const provider of ext.providers.values()) {
providers.push(provider.spec);
}
const event_hooks = [];
for (const key of ext.hooks.keys()) {
event_hooks.push(String(key));
}
const shortcuts = [];
for (const shortcut of ext.shortcuts.values()) {
shortcuts.push(shortcut.spec);
}
const message_renderers = [];
for (const renderer of ext.messageRenderers.values()) {
message_renderers.push(renderer.customType);
}
const flags = [];
for (const [flagName, flagSpec] of ext.flags.entries()) {
flags.push({
name: flagName,
description: flagSpec.description ? String(flagSpec.description) : '',
type: flagSpec.type ? String(flagSpec.type) : 'string',
default: flagSpec.default !== undefined ? flagSpec.default : null,
});
}
out.push({
id: id,
name: ext.name,
version: ext.version,
api_version: ext.apiVersion,
tools: tools,
slash_commands: commands,
providers: providers,
shortcuts: shortcuts,
message_renderers: message_renderers,
flags: flags,
event_hooks: event_hooks,
active_tools: Array.isArray(ext.activeTools) ? ext.activeTools.slice() : null,
});
}
return out;
}
function __pi_make_extension_theme() {
return Object.create(__pi_extension_theme_template);
}
const __pi_extension_theme_template = {
// Minimal theme shim. Legacy emits ANSI; conformance harness should normalize ANSI away.
fg: (_style, text) => String(text === undefined || text === null ? '' : text),
bold: (text) => String(text === undefined || text === null ? '' : text),
strikethrough: (text) => String(text === undefined || text === null ? '' : text),
};
function __pi_build_extension_ui_template(hasUI) {
return {
select: (title, options) => {
if (!hasUI) return Promise.resolve(undefined);
const list = Array.isArray(options) ? options : [];
const mapped = list.map((v) => String(v));
return pi.ui('select', { title: String(title === undefined || title === null ? '' : title), options: mapped });
},
confirm: (title, message) => {
if (!hasUI) return Promise.resolve(false);
return pi.ui('confirm', {
title: String(title === undefined || title === null ? '' : title),
message: String(message === undefined || message === null ? '' : message),
});
},
input: (title, placeholder, def) => {
if (!hasUI) return Promise.resolve(undefined);
// Legacy extensions typically call input(title, placeholder?, default?)
let payloadDefault = def;
let payloadPlaceholder = placeholder;
if (def === undefined && typeof placeholder === 'string') {
payloadDefault = placeholder;
payloadPlaceholder = undefined;
}
return pi.ui('input', {
title: String(title === undefined || title === null ? '' : title),
placeholder: payloadPlaceholder,
default: payloadDefault,
});
},
editor: (title, def, language) => {
if (!hasUI) return Promise.resolve(undefined);
// Legacy extensions typically call editor(title, defaultText)
return pi.ui('editor', {
title: String(title === undefined || title === null ? '' : title),
language: language,
default: def,
});
},
notify: (message, level) => {
const notifyType = level ? String(level) : undefined;
const payload = {
message: String(message === undefined || message === null ? '' : message),
};
if (notifyType) {
payload.level = notifyType;
payload.notifyType = notifyType; // legacy field
}
void pi.ui('notify', payload).catch(() => {});
},
setStatus: (statusKey, statusText) => {
const key = String(statusKey === undefined || statusKey === null ? '' : statusKey);
const text = String(statusText === undefined || statusText === null ? '' : statusText);
void pi.ui('setStatus', {
statusKey: key,
statusText: text,
text: text, // compat: some UI surfaces only consume `text`
}).catch(() => {});
},
setWidget: (widgetKey, lines) => {
if (!hasUI) return;
const payload = { widgetKey: String(widgetKey === undefined || widgetKey === null ? '' : widgetKey) };
if (Array.isArray(lines)) {
payload.lines = lines.map((v) => String(v));
payload.widgetLines = payload.lines; // compat with pi-mono RPC naming
payload.content = payload.lines.join('\n'); // compat: some UI surfaces expect a single string
}
void pi.ui('setWidget', payload).catch(() => {});
},
setTitle: (title) => {
void pi.ui('setTitle', {
title: String(title === undefined || title === null ? '' : title),
}).catch(() => {});
},
setEditorText: (text) => {
void pi.ui('set_editor_text', {
text: String(text === undefined || text === null ? '' : text),
}).catch(() => {});
},
custom: (_component, options) => {
if (!hasUI) return Promise.resolve(undefined);
const payload = options && typeof options === 'object' ? options : {};
return pi.ui('custom', payload);
},
};
}
const __pi_extension_ui_templates = {
with_ui: __pi_build_extension_ui_template(true),
without_ui: __pi_build_extension_ui_template(false),
};
function __pi_make_extension_ui(hasUI) {
const template = hasUI ? __pi_extension_ui_templates.with_ui : __pi_extension_ui_templates.without_ui;
const ui = Object.create(template);
ui.theme = __pi_make_extension_theme();
return ui;
}
function __pi_make_extension_ctx(ctx_payload) {
const hasUI = !!(ctx_payload && (ctx_payload.hasUI || ctx_payload.has_ui));
const cwd = ctx_payload && (ctx_payload.cwd || ctx_payload.CWD) ? String(ctx_payload.cwd || ctx_payload.CWD) : '';
const entriesRaw =
(ctx_payload && (ctx_payload.sessionEntries || ctx_payload.session_entries || ctx_payload.entries)) || [];
const branchRaw =
(ctx_payload && (ctx_payload.sessionBranch || ctx_payload.session_branch || ctx_payload.branch)) || entriesRaw;
const entries = Array.isArray(entriesRaw) ? entriesRaw : [];
const branch = Array.isArray(branchRaw) ? branchRaw : entries;
const leafEntry =
(ctx_payload &&
(ctx_payload.sessionLeafEntry ||
ctx_payload.session_leaf_entry ||
ctx_payload.leafEntry ||
ctx_payload.leaf_entry)) ||
null;
const modelRegistryValues =
(ctx_payload && (ctx_payload.modelRegistry || ctx_payload.model_registry || ctx_payload.model_registry_values)) ||
{};
const sessionManager = {
getEntries: () => entries,
getBranch: () => branch,
getLeafEntry: () => leafEntry,
};
return {
hasUI: hasUI,
cwd: cwd,
ui: __pi_make_extension_ui(hasUI),
sessionManager: sessionManager,
modelRegistry: {
getApiKeyForProvider: async (provider) => {
const key = String(provider || '').trim();
if (!key) return undefined;
const value = modelRegistryValues[key];
if (value === undefined || value === null) return undefined;
return String(value);
},
},
};
}
async function __pi_dispatch_event_inner(eventName, event_payload, ctx) {
const handlers = [
...(__pi_hook_index.get(eventName) || []),
...(__pi_event_bus_index.get(eventName) || []),
];
if (handlers.length === 0) {
return undefined;
}
if (eventName === 'input') {
const base = event_payload && typeof event_payload === 'object' ? event_payload : {};
const originalText = typeof base.text === 'string' ? base.text : String(base.text ?? '');
const originalImages = Array.isArray(base.images) ? base.images : undefined;
const source = base.source !== undefined ? base.source : 'extension';
let currentText = originalText;
let currentImages = originalImages;
for (const entry of handlers) {
const handler = entry && entry.handler;
if (typeof handler !== 'function') continue;
const event = { type: 'input', text: currentText, images: currentImages, source: source };
let result = undefined;
try {
result = await __pi_with_extension_async(entry.extensionId, () => handler(event, ctx));
} catch (e) {
try { globalThis.console && globalThis.console.error && globalThis.console.error('Event handler error:', eventName, entry.extensionId, e); } catch (_e) {}
continue;
}
if (result && typeof result === 'object') {
if (result.action === 'handled') return result;
if (result.action === 'transform' && typeof result.text === 'string') {
currentText = result.text;
if (result.images !== undefined) currentImages = result.images;
}
}
}
if (currentText !== originalText || currentImages !== originalImages) {
return { action: 'transform', text: currentText, images: currentImages };
}
return { action: 'continue' };
}
if (eventName === 'before_agent_start') {
const base = event_payload && typeof event_payload === 'object' ? event_payload : {};
const prompt = typeof base.prompt === 'string' ? base.prompt : '';
const images = Array.isArray(base.images) ? base.images : undefined;
let currentSystemPrompt = typeof base.systemPrompt === 'string' ? base.systemPrompt : '';
let modified = false;
const messages = [];
for (const entry of handlers) {
const handler = entry && entry.handler;
if (typeof handler !== 'function') continue;
const event = { type: 'before_agent_start', prompt, images, systemPrompt: currentSystemPrompt };
let result = undefined;
try {
result = await __pi_with_extension_async(entry.extensionId, () => handler(event, ctx));
} catch (e) {
try { globalThis.console && globalThis.console.error && globalThis.console.error('Event handler error:', eventName, entry.extensionId, e); } catch (_e) {}
continue;
}
if (result && typeof result === 'object') {
if (result.message !== undefined) messages.push(result.message);
if (result.systemPrompt !== undefined) {
currentSystemPrompt = String(result.systemPrompt);
modified = true;
}
}
}
if (messages.length > 0 || modified) {
return { messages: messages.length > 0 ? messages : undefined, systemPrompt: modified ? currentSystemPrompt : undefined };
}
return undefined;
}
let last = undefined;
for (const entry of handlers) {
const handler = entry && entry.handler;
if (typeof handler !== 'function') continue;
let value = undefined;
try {
value = await __pi_with_extension_async(entry.extensionId, () => handler(event_payload, ctx));
} catch (e) {
try { globalThis.console && globalThis.console.error && globalThis.console.error('Event handler error:', eventName, entry.extensionId, e); } catch (_e) {}
continue;
}
if (value === undefined) continue;
// First-result semantics (legacy parity)
if (eventName === 'user_bash') {
return value;
}
last = value;
// Early-stop semantics (legacy parity)
if (eventName === 'tool_call' && value && typeof value === 'object' && value.block) {
return value;
}
if (eventName.startsWith('session_before_') && value && typeof value === 'object' && value.cancel) {
return value;
}
}
return last;
}
async function __pi_dispatch_extension_event(event_name, event_payload, ctx_payload) {
const eventName = String(event_name || '').trim();
if (!eventName) {
throw new Error('dispatch_event: event name is required');
}
const ctx = __pi_make_extension_ctx(ctx_payload);
return __pi_dispatch_event_inner(eventName, event_payload, ctx);
}
async function __pi_dispatch_extension_events_batch(events_json, ctx_payload) {
const ctx = __pi_make_extension_ctx(ctx_payload);
const results = [];
for (const entry of events_json) {
const eventName = String(entry.event_name || '').trim();
if (!eventName) continue;
try {
const value = await __pi_dispatch_event_inner(eventName, entry.event_payload, ctx);
results.push({ event: eventName, ok: true, value: value });
} catch (e) {
results.push({ event: eventName, ok: false, error: String(e) });
}
}
return results;
}
async function __pi_execute_tool(tool_name, tool_call_id, input, ctx_payload) {
const name = String(tool_name || '').trim();
const record = __pi_tool_index.get(name);
if (!record) {
throw new Error(`Unknown tool: ${name}`);
}
const ctx = __pi_make_extension_ctx(ctx_payload);
return __pi_with_extension_async(record.extensionId, () =>
record.execute(tool_call_id, input, undefined, undefined, ctx)
);
}
async function __pi_execute_command(command_name, args, ctx_payload) {
const name = String(command_name || '').trim().replace(/^\//, '');
const record = __pi_command_index.get(name);
if (!record) {
throw new Error(`Unknown command: ${name}`);
}
const ctx = __pi_make_extension_ctx(ctx_payload);
return __pi_with_extension_async(record.extensionId, () => record.handler(args, ctx));
}
async function __pi_execute_shortcut(key_id, ctx_payload) {
const id = String(key_id || '').trim().toLowerCase();
const record = __pi_shortcut_index.get(id);
if (!record) {
throw new Error('Unknown shortcut: ' + id);
}
const ctx = __pi_make_extension_ctx(ctx_payload);
return __pi_with_extension_async(record.extensionId, () => record.handler(ctx));
}
// Hostcall stream class (async iterator for streaming hostcall results)
class __pi_HostcallStream {
constructor(callId) {
this.callId = callId;
this.buffer = [];
this.waitResolve = null;
this.done = false;
}
pushChunk(chunk, isFinal) {
if (isFinal) this.done = true;
if (this.waitResolve) {
const resolve = this.waitResolve;
this.waitResolve = null;
if (isFinal && chunk === null) {
resolve({ value: undefined, done: true });
} else {
resolve({ value: chunk, done: false });
}
} else {
this.buffer.push({ chunk, isFinal });
}
}
pushError(error) {
this.done = true;
if (this.waitResolve) {
const rej = this.waitResolve;
this.waitResolve = null;
rej({ __error: error });
} else {
this.buffer.push({ __error: error });
}
}
next() {
if (this.buffer.length > 0) {
const entry = this.buffer.shift();
if (entry.__error) return Promise.reject(entry.__error);
if (entry.isFinal && entry.chunk === null) return Promise.resolve({ value: undefined, done: true });
return Promise.resolve({ value: entry.chunk, done: false });
}
if (this.done) return Promise.resolve({ value: undefined, done: true });
return new Promise((resolve, reject) => {
this.waitResolve = (result) => {
if (result && result.__error) reject(result.__error);
else resolve(result);
};
});
}
return() {
this.done = true;
this.buffer = [];
this.waitResolve = null;
return Promise.resolve({ value: undefined, done: true });
}
[Symbol.asyncIterator]() { return this; }
}
// Complete a hostcall (called from Rust)
function __pi_complete_hostcall_impl(call_id, outcome) {
const pending = __pi_pending_hostcalls.get(call_id);
if (!pending) return;
if (outcome.stream) {
const seq = Number(outcome.sequence);
if (!Number.isFinite(seq)) {
const error = new Error('Invalid stream sequence');
error.code = 'STREAM_SEQUENCE';
if (pending.stream) pending.stream.pushError(error);
else if (pending.reject) pending.reject(error);
__pi_pending_hostcalls.delete(call_id);
return;
}
if (pending.lastSeq === undefined) {
if (seq !== 0) {
const error = new Error('Stream sequence must start at 0');
error.code = 'STREAM_SEQUENCE';
if (pending.stream) pending.stream.pushError(error);
else if (pending.reject) pending.reject(error);
__pi_pending_hostcalls.delete(call_id);
return;
}
} else if (seq <= pending.lastSeq) {
const error = new Error('Stream sequence out of order');
error.code = 'STREAM_SEQUENCE';
if (pending.stream) pending.stream.pushError(error);
else if (pending.reject) pending.reject(error);
__pi_pending_hostcalls.delete(call_id);
return;
}
pending.lastSeq = seq;
if (pending.stream) {
pending.stream.pushChunk(outcome.chunk, outcome.isFinal);
} else if (pending.onChunk) {
const chunk = outcome.chunk;
const isFinal = outcome.isFinal;
Promise.resolve().then(() => {
try {
pending.onChunk(chunk, isFinal);
} catch (e) {
console.error('Hostcall onChunk error:', e);
}
});
}
if (outcome.isFinal) {
__pi_pending_hostcalls.delete(call_id);
if (pending.resolve) pending.resolve(outcome.chunk);
}
return;
}
if (!outcome.ok && pending.stream) {
const error = new Error(outcome.message);
error.code = outcome.code;
pending.stream.pushError(error);
__pi_pending_hostcalls.delete(call_id);
return;
}
__pi_pending_hostcalls.delete(call_id);
if (outcome.ok) {
pending.resolve(outcome.value);
} else {
const error = new Error(outcome.message);
error.code = outcome.code;
pending.reject(error);
}
}
function __pi_complete_hostcall(call_id, outcome) {
const pending = __pi_pending_hostcalls.get(call_id);
if (pending && pending.extensionId) {
const prev = __pi_current_extension_id;
__pi_current_extension_id = pending.extensionId;
try {
return __pi_complete_hostcall_impl(call_id, outcome);
} finally {
Promise.resolve().then(() => { __pi_current_extension_id = prev; });
}
}
return __pi_complete_hostcall_impl(call_id, outcome);
}
// Fire a timer callback (called from Rust)
function __pi_fire_timer(timer_id) {
const callback = __pi_timer_callbacks.get(timer_id);
if (callback) {
__pi_timer_callbacks.delete(timer_id);
try {
callback();
} catch (e) {
console.error('Timer callback error:', e);
}
}
}
// Dispatch an inbound event (called from Rust)
function __pi_dispatch_event(event_id, payload) {
const listeners = __pi_event_listeners.get(event_id);
if (listeners) {
for (const listener of listeners) {
try {
listener(payload);
} catch (e) {
console.error('Event listener error:', e);
}
}
}
}
// Register a timer callback (used by setTimeout)
function __pi_register_timer(timer_id, callback) {
__pi_timer_callbacks.set(timer_id, callback);
}
// Unregister a timer callback (used by clearTimeout)
function __pi_unregister_timer(timer_id) {
__pi_timer_callbacks.delete(timer_id);
}
// Add an event listener
function __pi_add_event_listener(event_id, callback) {
if (!__pi_event_listeners.has(event_id)) {
__pi_event_listeners.set(event_id, []);
}
__pi_event_listeners.get(event_id).push(callback);
}
// Remove an event listener
function __pi_remove_event_listener(event_id, callback) {
const listeners = __pi_event_listeners.get(event_id);
if (listeners) {
const index = listeners.indexOf(callback);
if (index !== -1) {
listeners.splice(index, 1);
}
}
}
// Helper to create a Promise-returning hostcall wrapper
function __pi_make_hostcall(nativeFn) {
return function(...args) {
return new Promise((resolve, reject) => {
const call_id = nativeFn(...args);
__pi_pending_hostcalls.set(call_id, {
resolve,
reject,
extensionId: __pi_current_extension_id
});
});
};
}
function __pi_make_streaming_hostcall(nativeFn, ...args) {
const call_id = nativeFn(...args);
const stream = new __pi_HostcallStream(call_id);
__pi_pending_hostcalls.set(call_id, {
stream,
resolve: () => {},
reject: () => {},
extensionId: __pi_current_extension_id
});
return stream;
}
function __pi_env_get(key) {
const value = __pi_env_get_native(key);
if (value === null || value === undefined) {
return undefined;
}
return value;
}
function __pi_path_join(...parts) {
let out = '';
for (const part of parts) {
if (!part) continue;
if (out === '' || out.endsWith('/')) {
out += part;
} else {
out += '/' + part;
}
}
return __pi_path_normalize(out);
}
function __pi_path_basename(path) {
if (!path) return '';
let p = path;
while (p.length > 1 && p.endsWith('/')) {
p = p.slice(0, -1);
}
const idx = p.lastIndexOf('/');
return idx === -1 ? p : p.slice(idx + 1);
}
function __pi_path_normalize(path) {
if (!path) return '';
const isAbs = path.startsWith('/');
const parts = path.split('/').filter(p => p.length > 0);
const stack = [];
for (const part of parts) {
if (part === '.') continue;
if (part === '..') {
if (stack.length > 0 && stack[stack.length - 1] !== '..') {
stack.pop();
} else if (!isAbs) {
stack.push('..');
}
continue;
}
stack.push(part);
}
const joined = stack.join('/');
return isAbs ? '/' + joined : joined || (isAbs ? '/' : '');
}
function __pi_sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// Create the pi global object with Promise-returning methods
const __pi_exec_hostcall = __pi_make_hostcall(__pi_exec_native);
const pi = {
// pi.tool(name, input) - invoke a tool
tool: __pi_make_hostcall(__pi_tool_native),
// pi.exec(cmd, args, options) - execute a shell command
exec: (cmd, args, options = {}) => {
if (options && options.stream) {
const onChunk =
options && typeof options === 'object'
? (options.onChunk || options.on_chunk)
: undefined;
if (typeof onChunk === 'function') {
const opts = Object.assign({}, options);
delete opts.onChunk;
delete opts.on_chunk;
const call_id = __pi_exec_native(cmd, args, opts);
return new Promise((resolve, reject) => {
__pi_pending_hostcalls.set(call_id, { onChunk, resolve, reject, extensionId: __pi_current_extension_id });
});
}
return __pi_make_streaming_hostcall(__pi_exec_native, cmd, args, options);
}
return __pi_exec_hostcall(cmd, args, options);
},
// pi.http(request) - make an HTTP request
http: (request) => {
if (request && request.stream) {
const onChunk =
request && typeof request === 'object'
? (request.onChunk || request.on_chunk)
: undefined;
if (typeof onChunk === 'function') {
const req = Object.assign({}, request);
delete req.onChunk;
delete req.on_chunk;
const call_id = __pi_http_native(req);
return new Promise((resolve, reject) => {
__pi_pending_hostcalls.set(call_id, { onChunk, resolve, reject, extensionId: __pi_current_extension_id });
});
}
return __pi_make_streaming_hostcall(__pi_http_native, request);
}
return __pi_make_hostcall(__pi_http_native)(request);
},
// pi.session(op, args) - session operations
session: __pi_make_hostcall(__pi_session_native),
// pi.ui(op, args) - UI operations
ui: __pi_make_hostcall(__pi_ui_native),
// pi.events(op, args) - event operations
events: __pi_make_hostcall(__pi_events_native),
// pi.log(entry) - structured log emission
log: __pi_make_hostcall(__pi_log_native),
// Extension API (legacy-compatible subset)
registerTool: __pi_register_tool,
registerCommand: __pi_register_command,
registerProvider: __pi_register_provider,
registerShortcut: __pi_register_shortcut,
registerMessageRenderer: __pi_register_message_renderer,
on: __pi_register_hook,
registerFlag: __pi_register_flag,
getFlag: __pi_get_flag,
setActiveTools: __pi_set_active_tools,
getActiveTools: __pi_get_active_tools,
getModel: __pi_get_model,
setModel: __pi_set_model,
getThinkingLevel: __pi_get_thinking_level,
setThinkingLevel: __pi_set_thinking_level,
appendEntry: __pi_append_entry,
sendMessage: __pi_send_message,
sendUserMessage: __pi_send_user_message,
getSessionName: __pi_get_session_name,
setSessionName: __pi_set_session_name,
setLabel: __pi_set_label,
};
// Convenience API: pi.events.emit/on (inter-extension bus).
// Keep pi.events callable for legacy hostcall operations.
pi.events.emit = (event, data, options = undefined) => {
const name = String(event || '').trim();
if (!name) {
throw new Error('events.emit: event name is required');
}
const payload = { event: name, data: (data === undefined ? null : data) };
if (options && typeof options === 'object') {
if (options.ctx !== undefined) payload.ctx = options.ctx;
if (options.timeout_ms !== undefined) payload.timeout_ms = options.timeout_ms;
if (options.timeoutMs !== undefined) payload.timeoutMs = options.timeoutMs;
if (options.timeout !== undefined) payload.timeout = options.timeout;
}
return pi.events('emit', payload);
};
pi.events.on = (event, handler) => __pi_register_event_bus_hook(event, handler);
pi.env = {
get: __pi_env_get,
};
pi.process = {
cwd: __pi_process_cwd_native(),
args: __pi_process_args_native(),
};
const __pi_det_cwd = __pi_env_get('PI_DETERMINISTIC_CWD');
if (__pi_det_cwd) {
try { pi.process.cwd = __pi_det_cwd; } catch (_) {}
}
try { Object.freeze(pi.process.args); } catch (_) {}
try { Object.freeze(pi.process); } catch (_) {}
pi.path = {
join: __pi_path_join,
basename: __pi_path_basename,
normalize: __pi_path_normalize,
};
function __pi_crypto_bytes_to_array(raw) {
if (raw == null) return [];
if (Array.isArray(raw)) {
return raw.map((value) => Number(value) & 0xff);
}
if (raw instanceof Uint8Array) {
return Array.from(raw, (value) => Number(value) & 0xff);
}
if (raw instanceof ArrayBuffer) {
return Array.from(new Uint8Array(raw), (value) => Number(value) & 0xff);
}
if (typeof raw === 'string') {
// Depending on bridge coercion, bytes may arrive as:
// 1) hex text (2 chars per byte), or 2) latin1-style binary string.
const isHex = raw.length % 2 === 0 && /^[0-9a-fA-F]+$/.test(raw);
if (isHex) {
const out = [];
for (let i = 0; i + 1 < raw.length; i += 2) {
const byte = Number.parseInt(raw.slice(i, i + 2), 16);
out.push(Number.isFinite(byte) ? (byte & 0xff) : 0);
}
return out;
}
const out = new Array(raw.length);
for (let i = 0; i < raw.length; i++) out[i] = raw.charCodeAt(i) & 0xff;
return out;
}
if (typeof raw.length === 'number') {
const len = Number(raw.length) || 0;
const out = new Array(len);
for (let i = 0; i < len; i++) out[i] = Number(raw[i] || 0) & 0xff;
return out;
}
return [];
}
pi.crypto = {
sha256Hex: __pi_crypto_sha256_hex_native,
randomBytes: function(n) {
return __pi_crypto_bytes_to_array(__pi_crypto_random_bytes_native(n));
},
};
pi.time = {
nowMs: __pi_now_ms_native,
sleep: __pi_sleep,
};
// Make pi available globally
globalThis.pi = pi;
const __pi_det_time_raw = __pi_env_get('PI_DETERMINISTIC_TIME_MS');
const __pi_det_time_step_raw = __pi_env_get('PI_DETERMINISTIC_TIME_STEP_MS');
const __pi_det_random_raw = __pi_env_get('PI_DETERMINISTIC_RANDOM');
const __pi_det_random_seed_raw = __pi_env_get('PI_DETERMINISTIC_RANDOM_SEED');
if (__pi_det_time_raw !== undefined) {
const __pi_det_base = Number(__pi_det_time_raw);
if (Number.isFinite(__pi_det_base)) {
const __pi_det_step = (() => {
if (__pi_det_time_step_raw === undefined) return 1;
const value = Number(__pi_det_time_step_raw);
return Number.isFinite(value) ? value : 1;
})();
let __pi_det_tick = 0;
const __pi_det_now = () => {
const value = __pi_det_base + (__pi_det_step * __pi_det_tick);
__pi_det_tick += 1;
return value;
};
if (pi && pi.time) {
pi.time.nowMs = () => __pi_det_now();
}
const __pi_OriginalDate = Date;
class PiDeterministicDate extends __pi_OriginalDate {
constructor(...args) {
if (args.length === 0) {
super(__pi_det_now());
} else {
super(...args);
}
}
static now() {
return __pi_det_now();
}
}
PiDeterministicDate.UTC = __pi_OriginalDate.UTC;
PiDeterministicDate.parse = __pi_OriginalDate.parse;
globalThis.Date = PiDeterministicDate;
}
}
if (__pi_det_random_raw !== undefined) {
const __pi_det_random_val = Number(__pi_det_random_raw);
if (Number.isFinite(__pi_det_random_val)) {
Math.random = () => __pi_det_random_val;
}
} else if (__pi_det_random_seed_raw !== undefined) {
let __pi_det_state = Number(__pi_det_random_seed_raw);
if (Number.isFinite(__pi_det_state)) {
__pi_det_state = __pi_det_state >>> 0;
Math.random = () => {
__pi_det_state = (__pi_det_state * 1664525 + 1013904223) >>> 0;
return __pi_det_state / 4294967296;
};
}
}
// ============================================================================
// Minimal Web/Node polyfills for legacy extensions (best-effort)
// ============================================================================
if (typeof globalThis.btoa !== 'function') {
globalThis.btoa = (s) => {
const bin = String(s === undefined || s === null ? '' : s);
return __pi_base64_encode_native(bin);
};
}
if (typeof globalThis.atob !== 'function') {
globalThis.atob = (s) => {
const b64 = String(s === undefined || s === null ? '' : s);
return __pi_base64_decode_native(b64);
};
}
if (typeof globalThis.TextEncoder === 'undefined') {
class TextEncoder {
encode(input) {
const s = String(input === undefined || input === null ? '' : input);
const bytes = [];
for (let i = 0; i < s.length; i++) {
let code = s.charCodeAt(i);
if (code < 0x80) {
bytes.push(code);
continue;
}
if (code < 0x800) {
bytes.push(0xc0 | (code >> 6));
bytes.push(0x80 | (code & 0x3f));
continue;
}
if (code >= 0xd800 && code <= 0xdbff && i + 1 < s.length) {
const next = s.charCodeAt(i + 1);
if (next >= 0xdc00 && next <= 0xdfff) {
const cp = ((code - 0xd800) << 10) + (next - 0xdc00) + 0x10000;
bytes.push(0xf0 | (cp >> 18));
bytes.push(0x80 | ((cp >> 12) & 0x3f));
bytes.push(0x80 | ((cp >> 6) & 0x3f));
bytes.push(0x80 | (cp & 0x3f));
i++;
continue;
}
}
bytes.push(0xe0 | (code >> 12));
bytes.push(0x80 | ((code >> 6) & 0x3f));
bytes.push(0x80 | (code & 0x3f));
}
return new Uint8Array(bytes);
}
}
globalThis.TextEncoder = TextEncoder;
}
if (typeof globalThis.TextDecoder === 'undefined') {
class TextDecoder {
constructor(encoding = 'utf-8') {
this.encoding = encoding;
}
decode(input, _opts) {
if (input === undefined || input === null) return '';
if (typeof input === 'string') return input;
let bytes;
if (input instanceof ArrayBuffer) {
bytes = new Uint8Array(input);
} else if (ArrayBuffer.isView && ArrayBuffer.isView(input)) {
bytes = new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
} else if (Array.isArray(input)) {
bytes = new Uint8Array(input);
} else if (typeof input.length === 'number') {
bytes = new Uint8Array(input);
} else {
return '';
}
let out = '';
for (let i = 0; i < bytes.length; ) {
const b0 = bytes[i++];
if (b0 < 0x80) {
out += String.fromCharCode(b0);
continue;
}
if ((b0 & 0xe0) === 0xc0) {
const b1 = bytes[i++] & 0x3f;
out += String.fromCharCode(((b0 & 0x1f) << 6) | b1);
continue;
}
if ((b0 & 0xf0) === 0xe0) {
const b1 = bytes[i++] & 0x3f;
const b2 = bytes[i++] & 0x3f;
out += String.fromCharCode(((b0 & 0x0f) << 12) | (b1 << 6) | b2);
continue;
}
if ((b0 & 0xf8) === 0xf0) {
const b1 = bytes[i++] & 0x3f;
const b2 = bytes[i++] & 0x3f;
const b3 = bytes[i++] & 0x3f;
let cp = ((b0 & 0x07) << 18) | (b1 << 12) | (b2 << 6) | b3;
cp -= 0x10000;
out += String.fromCharCode(0xd800 + (cp >> 10), 0xdc00 + (cp & 0x3ff));
continue;
}
}
return out;
}
}
globalThis.TextDecoder = TextDecoder;
}
// structuredClone — deep clone using JSON round-trip
if (typeof globalThis.structuredClone === 'undefined') {
globalThis.structuredClone = (value) => JSON.parse(JSON.stringify(value));
}
// queueMicrotask — schedule a microtask
if (typeof globalThis.queueMicrotask === 'undefined') {
globalThis.queueMicrotask = (fn) => Promise.resolve().then(fn);
}
// performance.now() — high-resolution timer
if (typeof globalThis.performance === 'undefined') {
const start = Date.now();
globalThis.performance = { now: () => Date.now() - start, timeOrigin: start };
}
if (typeof globalThis.URLSearchParams === 'undefined') {
class URLSearchParams {
constructor(init) {
this._pairs = [];
if (typeof init === 'string') {
const s = init.replace(/^\?/, '');
if (s.length > 0) {
for (const part of s.split('&')) {
const idx = part.indexOf('=');
if (idx === -1) {
this.append(decodeURIComponent(part), '');
} else {
const k = part.slice(0, idx);
const v = part.slice(idx + 1);
this.append(decodeURIComponent(k), decodeURIComponent(v));
}
}
}
} else if (Array.isArray(init)) {
for (const entry of init) {
if (!entry) continue;
this.append(entry[0], entry[1]);
}
} else if (init && typeof init === 'object') {
for (const k of Object.keys(init)) {
this.append(k, init[k]);
}
}
}
append(key, value) {
this._pairs.push([String(key), String(value)]);
}
toString() {
const out = [];
for (const [k, v] of this._pairs) {
out.push(encodeURIComponent(k) + '=' + encodeURIComponent(v));
}
return out.join('&');
}
}
globalThis.URLSearchParams = URLSearchParams;
}
if (typeof globalThis.URL === 'undefined') {
class URL {
constructor(input, base) {
const s = base ? new URL(base).href.replace(/\/[^/]*$/, '/') + String(input ?? '') : String(input ?? '');
const m = s.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^/?#]*)([^?#]*)(\?[^#]*)?(#.*)?$/);
if (m) {
this.protocol = m[1] + ':';
const auth = m[2];
const atIdx = auth.lastIndexOf('@');
if (atIdx !== -1) {
const userinfo = auth.slice(0, atIdx);
const ci = userinfo.indexOf(':');
this.username = ci === -1 ? userinfo : userinfo.slice(0, ci);
this._pw = ci === -1 ? String() : userinfo.slice(ci + 1);
this.host = auth.slice(atIdx + 1);
} else {
this.username = '';
this._pw = String();
this.host = auth;
}
const hi = this.host.indexOf(':');
this.hostname = hi === -1 ? this.host : this.host.slice(0, hi);
this.port = hi === -1 ? '' : this.host.slice(hi + 1);
this.pathname = m[3] || '/';
this.search = m[4] || '';
this.hash = m[5] || '';
} else {
this.protocol = '';
this.username = '';
this._pw = String();
this.host = '';
this.hostname = '';
this.port = '';
this.pathname = s;
this.search = '';
this.hash = '';
}
this.searchParams = new globalThis.URLSearchParams(this.search.replace(/^\?/, ''));
this.origin = this.protocol ? `${this.protocol}//${this.host}` : '';
this.href = this.toString();
}
get password() {
return this._pw;
}
set password(value) {
this._pw = value == null ? String() : String(value);
}
toString() {
const auth = this.username ? `${this.username}${this.password ? ':' + this.password : ''}@` : '';
return this.protocol ? `${this.protocol}//${auth}${this.host}${this.pathname}${this.search}${this.hash}` : this.pathname;
}
toJSON() { return this.toString(); }
}
globalThis.URL = URL;
}
if (typeof globalThis.Buffer === 'undefined') {
class Buffer extends Uint8Array {
static from(input, encoding) {
if (typeof input === 'string') {
const enc = String(encoding || '').toLowerCase();
if (enc === 'base64') {
const bin = __pi_base64_decode_native(input);
const out = new Buffer(bin.length);
for (let i = 0; i < bin.length; i++) {
out[i] = bin.charCodeAt(i) & 0xff;
}
return out;
}
if (enc === 'hex') {
const hex = input.replace(/[^0-9a-fA-F]/g, '');
const out = new Buffer(hex.length >> 1);
for (let i = 0; i < out.length; i++) {
out[i] = parseInt(hex.substr(i * 2, 2), 16);
}
return out;
}
const encoded = new TextEncoder().encode(input);
const out = new Buffer(encoded.length);
out.set(encoded);
return out;
}
if (input instanceof ArrayBuffer) {
const out = new Buffer(input.byteLength);
out.set(new Uint8Array(input));
return out;
}
if (ArrayBuffer.isView && ArrayBuffer.isView(input)) {
const out = new Buffer(input.byteLength);
out.set(new Uint8Array(input.buffer, input.byteOffset, input.byteLength));
return out;
}
if (Array.isArray(input)) {
const out = new Buffer(input.length);
for (let i = 0; i < input.length; i++) out[i] = input[i] & 0xff;
return out;
}
throw new Error('Buffer.from: unsupported input');
}
static alloc(size, fill) {
const buf = new Buffer(size);
if (fill !== undefined) buf.fill(typeof fill === 'number' ? fill : 0);
return buf;
}
static allocUnsafe(size) { return new Buffer(size); }
static isBuffer(obj) { return obj instanceof Buffer; }
static isEncoding(enc) {
return ['utf8','utf-8','ascii','latin1','binary','base64','hex','ucs2','ucs-2','utf16le','utf-16le'].includes(String(enc).toLowerCase());
}
static byteLength(str, encoding) {
if (typeof str !== 'string') return str.length || 0;
const enc = String(encoding || 'utf8').toLowerCase();
if (enc === 'base64') return Math.ceil(str.length * 3 / 4);
if (enc === 'hex') return str.length >> 1;
return new TextEncoder().encode(str).length;
}
static concat(list, totalLength) {
if (!Array.isArray(list) || list.length === 0) return Buffer.alloc(0);
const total = totalLength !== undefined ? totalLength : list.reduce((s, b) => s + b.length, 0);
const out = Buffer.alloc(total);
let offset = 0;
for (const buf of list) {
if (offset >= total) break;
const src = buf instanceof Uint8Array ? buf : Buffer.from(buf);
const copyLen = Math.min(src.length, total - offset);
out.set(src.subarray(0, copyLen), offset);
offset += copyLen;
}
return out;
}
static compare(a, b) {
const len = Math.min(a.length, b.length);
for (let i = 0; i < len; i++) {
if (a[i] < b[i]) return -1;
if (a[i] > b[i]) return 1;
}
if (a.length < b.length) return -1;
if (a.length > b.length) return 1;
return 0;
}
toString(encoding, start, end) {
const s = start || 0;
const e = end !== undefined ? end : this.length;
const view = this.subarray(s, e);
const enc = String(encoding || 'utf8').toLowerCase();
if (enc === 'base64') {
let binary = '';
for (let i = 0; i < view.length; i++) binary += String.fromCharCode(view[i]);
return __pi_base64_encode_native(binary);
}
if (enc === 'hex') {
let hex = '';
for (let i = 0; i < view.length; i++) hex += (view[i] < 16 ? '0' : '') + view[i].toString(16);
return hex;
}
return new TextDecoder().decode(view);
}
toJSON() {
return { type: 'Buffer', data: Array.from(this) };
}
equals(other) {
if (this.length !== other.length) return false;
for (let i = 0; i < this.length; i++) {
if (this[i] !== other[i]) return false;
}
return true;
}
compare(other) { return Buffer.compare(this, other); }
copy(target, targetStart, sourceStart, sourceEnd) {
const ts = targetStart || 0;
const ss = sourceStart || 0;
const se = sourceEnd !== undefined ? sourceEnd : this.length;
const src = this.subarray(ss, se);
const copyLen = Math.min(src.length, target.length - ts);
target.set(src.subarray(0, copyLen), ts);
return copyLen;
}
slice(start, end) {
const sliced = super.slice(start, end);
const buf = new Buffer(sliced.length);
buf.set(sliced);
return buf;
}
indexOf(value, byteOffset, encoding) {
const offset = byteOffset || 0;
if (typeof value === 'number') {
for (let i = offset; i < this.length; i++) {
if (this[i] === (value & 0xff)) return i;
}
return -1;
}
const needle = typeof value === 'string' ? Buffer.from(value, encoding) : value;
outer: for (let i = offset; i <= this.length - needle.length; i++) {
for (let j = 0; j < needle.length; j++) {
if (this[i + j] !== needle[j]) continue outer;
}
return i;
}
return -1;
}
includes(value, byteOffset, encoding) {
return this.indexOf(value, byteOffset, encoding) !== -1;
}
write(string, offset, length, encoding) {
const o = offset || 0;
const enc = encoding || 'utf8';
const bytes = Buffer.from(string, enc);
const len = length !== undefined ? Math.min(length, bytes.length) : bytes.length;
const copyLen = Math.min(len, this.length - o);
this.set(bytes.subarray(0, copyLen), o);
return copyLen;
}
fill(value, offset, end, encoding) {
const s = offset || 0;
const e = end !== undefined ? end : this.length;
const v = typeof value === 'number' ? (value & 0xff) : 0;
for (let i = s; i < e; i++) this[i] = v;
return this;
}
readUInt8(offset) { return this[offset || 0]; }
readUInt16BE(offset) { const o = offset || 0; return (this[o] << 8) | this[o + 1]; }
readUInt16LE(offset) { const o = offset || 0; return this[o] | (this[o + 1] << 8); }
readUInt32BE(offset) { const o = offset || 0; return ((this[o] << 24) | (this[o+1] << 16) | (this[o+2] << 8) | this[o+3]) >>> 0; }
readUInt32LE(offset) { const o = offset || 0; return (this[o] | (this[o+1] << 8) | (this[o+2] << 16) | (this[o+3] << 24)) >>> 0; }
readInt8(offset) { const v = this[offset || 0]; return v > 127 ? v - 256 : v; }
writeUInt8(value, offset) { this[offset || 0] = value & 0xff; return (offset || 0) + 1; }
writeUInt16BE(value, offset) { const o = offset || 0; this[o] = (value >> 8) & 0xff; this[o+1] = value & 0xff; return o + 2; }
writeUInt16LE(value, offset) { const o = offset || 0; this[o] = value & 0xff; this[o+1] = (value >> 8) & 0xff; return o + 2; }
writeUInt32BE(value, offset) { const o = offset || 0; this[o]=(value>>>24)&0xff; this[o+1]=(value>>>16)&0xff; this[o+2]=(value>>>8)&0xff; this[o+3]=value&0xff; return o+4; }
writeUInt32LE(value, offset) { const o = offset || 0; this[o]=value&0xff; this[o+1]=(value>>>8)&0xff; this[o+2]=(value>>>16)&0xff; this[o+3]=(value>>>24)&0xff; return o+4; }
}
globalThis.Buffer = Buffer;
}
if (typeof globalThis.crypto === 'undefined') {
globalThis.crypto = {};
}
if (typeof globalThis.crypto.getRandomValues !== 'function') {
globalThis.crypto.getRandomValues = (arr) => {
const len = Number(arr && arr.length ? arr.length : 0);
const bytes = __pi_crypto_bytes_to_array(__pi_crypto_random_bytes_native(len));
for (let i = 0; i < len; i++) {
arr[i] = bytes[i] || 0;
}
return arr;
};
}
if (!globalThis.crypto.subtle) {
globalThis.crypto.subtle = {};
}
if (typeof globalThis.crypto.subtle.digest !== 'function') {
globalThis.crypto.subtle.digest = async (algorithm, data) => {
const name = typeof algorithm === 'string' ? algorithm : (algorithm && algorithm.name ? algorithm.name : '');
const upper = String(name).toUpperCase();
if (upper !== 'SHA-256') {
throw new Error('crypto.subtle.digest: only SHA-256 is supported');
}
const bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
let text = '';
for (let i = 0; i < bytes.length; i++) {
text += String.fromCharCode(bytes[i]);
}
const hex = __pi_crypto_sha256_hex_native(text);
const out = new Uint8Array(hex.length / 2);
for (let i = 0; i < out.length; i++) {
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
}
return out.buffer;
};
}
if (typeof globalThis.crypto.randomUUID !== 'function') {
globalThis.crypto.randomUUID = () => {
const bytes = __pi_crypto_bytes_to_array(__pi_crypto_random_bytes_native(16));
while (bytes.length < 16) bytes.push(0);
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
const hex = Array.from(bytes, (b) => (b & 0xff).toString(16).padStart(2, '0')).join('');
return (
hex.slice(0, 8) +
'-' +
hex.slice(8, 12) +
'-' +
hex.slice(12, 16) +
'-' +
hex.slice(16, 20) +
'-' +
hex.slice(20)
);
};
}
if (typeof globalThis.process === 'undefined') {
const rawPlatform =
__pi_env_get_native('PI_PLATFORM') ||
__pi_env_get_native('OSTYPE') ||
__pi_env_get_native('OS') ||
'linux';
// Normalize to Node.js conventions: strip version suffix from OSTYPE
// (e.g. darwin24.0 -> darwin, linux-gnu -> linux, msys -> win32)
const platform = (() => {
const s = String(rawPlatform).replace(/[0-9].*$/, '').split('-')[0].toLowerCase();
if (s === 'darwin') return 'darwin';
if (s === 'msys' || s === 'cygwin' || s === 'windows_nt') return 'win32';
return s || 'linux';
})();
const detHome = __pi_env_get_native('PI_DETERMINISTIC_HOME');
const detCwd = __pi_env_get_native('PI_DETERMINISTIC_CWD');
const envProxy = new Proxy(
{},
{
get(_target, prop) {
if (typeof prop !== 'string') return undefined;
if (prop === 'HOME' && detHome) return detHome;
const value = __pi_env_get_native(prop);
return value === null || value === undefined ? undefined : value;
},
set(_target, prop, _value) {
// Read-only in PiJS — silently ignore writes
return typeof prop === 'string';
},
deleteProperty(_target, prop) {
// Read-only — silently ignore deletes
return typeof prop === 'string';
},
has(_target, prop) {
if (typeof prop !== 'string') return false;
if (prop === 'HOME' && detHome) return true;
const value = __pi_env_get_native(prop);
return value !== null && value !== undefined;
},
ownKeys() {
// Cannot enumerate real env — return empty
return [];
},
getOwnPropertyDescriptor(_target, prop) {
if (typeof prop !== 'string') return undefined;
const value = __pi_env_get_native(prop);
if (value === null || value === undefined) return undefined;
return { value, writable: false, enumerable: true, configurable: true };
},
},
);
// stdout/stderr that route through console output
function makeWritable(level) {
return {
write(chunk) {
if (typeof __pi_console_output_native === 'function') {
__pi_console_output_native(level, String(chunk));
}
return true;
},
end() { return this; },
on() { return this; },
once() { return this; },
pipe() { return this; },
isTTY: false,
};
}
// Event listener registry
const __evtMap = Object.create(null);
function __on(event, fn) {
if (!__evtMap[event]) __evtMap[event] = [];
__evtMap[event].push(fn);
return globalThis.process;
}
function __off(event, fn) {
const arr = __evtMap[event];
if (!arr) return globalThis.process;
const idx = arr.indexOf(fn);
if (idx >= 0) arr.splice(idx, 1);
return globalThis.process;
}
const startMs = (typeof __pi_now_ms_native === 'function') ? __pi_now_ms_native() : 0;
globalThis.process = {
env: envProxy,
argv: __pi_process_args_native(),
cwd: () => detCwd || __pi_process_cwd_native(),
platform: String(platform).split('-')[0],
arch: __pi_env_get_native('PI_TARGET_ARCH') || 'x64',
version: 'v20.0.0',
versions: { node: '20.0.0', v8: '0.0.0', modules: '0' },
pid: 1,
ppid: 0,
title: 'pi',
execPath: (typeof __pi_process_execpath_native === 'function')
? __pi_process_execpath_native()
: '/usr/bin/pi',
execArgv: [],
stdout: makeWritable('log'),
stderr: makeWritable('error'),
stdin: { on() { return this; }, once() { return this; }, read() {}, resume() { return this; }, pause() { return this; } },
nextTick: (fn, ...args) => { Promise.resolve().then(() => fn(...args)); },
hrtime: Object.assign((prev) => {
const nowMs = (typeof __pi_now_ms_native === 'function') ? __pi_now_ms_native() : 0;
const secs = Math.floor(nowMs / 1000);
const nanos = Math.floor((nowMs % 1000) * 1e6);
if (Array.isArray(prev) && prev.length >= 2) {
let ds = secs - prev[0];
let dn = nanos - prev[1];
if (dn < 0) { ds -= 1; dn += 1e9; }
return [ds, dn];
}
return [secs, nanos];
}, {
bigint: () => {
const nowMs = (typeof __pi_now_ms_native === 'function') ? __pi_now_ms_native() : 0;
return BigInt(Math.floor(nowMs * 1e6));
},
}),
kill: (pid, sig) => {
const impl = globalThis.__pi_process_kill_impl;
if (typeof impl === 'function') {
return impl(pid, sig);
}
const err = new Error('process.kill is not available in PiJS');
err.code = 'ENOSYS';
throw err;
},
exit: (code) => {
const exitCode = code === undefined ? 0 : Number(code);
// Fire exit listeners
const listeners = __evtMap['exit'];
if (listeners) {
for (const fn of listeners.slice()) {
try { fn(exitCode); } catch (_) {}
}
}
// Signal native side
if (typeof __pi_process_exit_native === 'function') {
__pi_process_exit_native(exitCode);
}
const err = new Error('process.exit(' + exitCode + ')');
err.code = 'ERR_PROCESS_EXIT';
err.exitCode = exitCode;
throw err;
},
chdir: (_dir) => {
const err = new Error('process.chdir is not supported in PiJS');
err.code = 'ENOSYS';
throw err;
},
uptime: () => {
const nowMs = (typeof __pi_now_ms_native === 'function') ? __pi_now_ms_native() : 0;
return Math.floor((nowMs - startMs) / 1000);
},
memoryUsage: () => ({
rss: 0, heapTotal: 0, heapUsed: 0, external: 0, arrayBuffers: 0,
}),
cpuUsage: (_prev) => ({ user: 0, system: 0 }),
emitWarning: (msg) => {
if (typeof __pi_console_output_native === 'function') {
__pi_console_output_native('warn', 'Warning: ' + msg);
}
},
release: { name: 'node', lts: 'PiJS' },
config: { variables: {} },
features: {},
on: __on,
addListener: __on,
off: __off,
removeListener: __off,
once(event, fn) {
const wrapped = (...args) => {
__off(event, wrapped);
fn(...args);
};
wrapped._original = fn;
__on(event, wrapped);
return globalThis.process;
},
removeAllListeners(event) {
if (event) { delete __evtMap[event]; }
else { for (const k in __evtMap) delete __evtMap[k]; }
return globalThis.process;
},
listeners(event) {
return (__evtMap[event] || []).slice();
},
emit(event, ...args) {
const listeners = __evtMap[event];
if (!listeners || listeners.length === 0) return false;
for (const fn of listeners.slice()) {
try { fn(...args); } catch (_) {}
}
return true;
},
};
try { Object.freeze(envProxy); } catch (_) {}
try { Object.freeze(globalThis.process.argv); } catch (_) {}
// Do NOT freeze globalThis.process — extensions may need to monkey-patch it
}
// Node.js global alias compatibility.
if (typeof globalThis.global === 'undefined') {
globalThis.global = globalThis;
}
if (typeof globalThis.Bun === 'undefined') {
const __pi_bun_require = (specifier) => {
try {
if (typeof require === 'function') {
return require(specifier);
}
} catch (_) {}
return null;
};
const __pi_bun_fs = () => __pi_bun_require('node:fs');
const __pi_bun_import_fs = () => import('node:fs');
const __pi_bun_child_process = () => __pi_bun_require('node:child_process');
const __pi_bun_to_uint8 = (value) => {
if (value instanceof Uint8Array) {
return value;
}
if (value instanceof ArrayBuffer) {
return new Uint8Array(value);
}
if (ArrayBuffer.isView && ArrayBuffer.isView(value)) {
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
}
if (typeof value === 'string') {
return new TextEncoder().encode(value);
}
if (value === undefined || value === null) {
return new Uint8Array();
}
return new TextEncoder().encode(String(value));
};
const __pi_bun_make_text_stream = (fetchText) => ({
async text() {
return fetchText();
},
async arrayBuffer() {
const text = await fetchText();
const bytes = new TextEncoder().encode(String(text ?? ''));
return bytes.buffer;
},
});
const Bun = {};
Bun.argv = Array.isArray(globalThis.process && globalThis.process.argv)
? globalThis.process.argv.slice()
: [];
Bun.file = (path) => {
const targetPath = String(path ?? '');
return {
path: targetPath,
name: targetPath,
async exists() {
const fs = __pi_bun_fs() || (await __pi_bun_import_fs());
return Boolean(fs && typeof fs.existsSync === 'function' && fs.existsSync(targetPath));
},
async text() {
const fs = __pi_bun_fs() || (await __pi_bun_import_fs());
if (!fs || typeof fs.readFileSync !== 'function') {
throw new Error('Bun.file.text: node:fs is unavailable');
}
return String(fs.readFileSync(targetPath, 'utf8'));
},
async arrayBuffer() {
const fs = __pi_bun_fs() || (await __pi_bun_import_fs());
if (!fs || typeof fs.readFileSync !== 'function') {
throw new Error('Bun.file.arrayBuffer: node:fs is unavailable');
}
const bytes = __pi_bun_to_uint8(fs.readFileSync(targetPath));
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
},
async json() {
return JSON.parse(await this.text());
},
};
};
Bun.write = async (destination, data) => {
const targetPath =
destination && typeof destination === 'object' && typeof destination.path === 'string'
? destination.path
: String(destination ?? '');
if (!targetPath) {
throw new Error('Bun.write: destination path is required');
}
const fs = __pi_bun_fs() || (await __pi_bun_import_fs());
if (!fs || typeof fs.writeFileSync !== 'function') {
throw new Error('Bun.write: node:fs is unavailable');
}
let payload = data;
if (payload && typeof payload === 'object' && typeof payload.text === 'function') {
payload = payload.text();
}
if (payload && typeof payload === 'object' && typeof payload.arrayBuffer === 'function') {
payload = payload.arrayBuffer();
}
if (payload && typeof payload.then === 'function') {
payload = await payload;
}
const bytes = __pi_bun_to_uint8(payload);
fs.writeFileSync(targetPath, bytes);
return bytes.byteLength;
};
Bun.which = (command) => {
const name = String(command ?? '').trim();
if (!name) return null;
const cwd =
globalThis.process && typeof globalThis.process.cwd === 'function'
? globalThis.process.cwd()
: '/';
const raw = __pi_exec_sync_native('which', JSON.stringify([name]), cwd, 2000, undefined);
try {
const parsed = JSON.parse(raw || '{}');
if (Number(parsed && parsed.code) !== 0) return null;
const out = String((parsed && parsed.stdout) || '').trim();
return out ? out.split('\n')[0] : null;
} catch (_) {
return null;
}
};
Bun.spawn = (commandOrArgv, rawOptions = {}) => {
const options = rawOptions && typeof rawOptions === 'object' ? rawOptions : {};
let command = '';
let args = [];
if (Array.isArray(commandOrArgv)) {
if (commandOrArgv.length === 0) {
throw new Error('Bun.spawn: command is required');
}
command = String(commandOrArgv[0] ?? '');
args = commandOrArgv.slice(1).map((arg) => String(arg ?? ''));
} else {
command = String(commandOrArgv ?? '');
if (Array.isArray(options.args)) {
args = options.args.map((arg) => String(arg ?? ''));
}
}
if (!command.trim()) {
throw new Error('Bun.spawn: command is required');
}
const spawnOptions = {
shell: false,
stdio: [
options.stdin === 'pipe' ? 'pipe' : 'ignore',
options.stdout === 'ignore' ? 'ignore' : 'pipe',
options.stderr === 'ignore' ? 'ignore' : 'pipe',
],
};
if (typeof options.cwd === 'string' && options.cwd.trim().length > 0) {
spawnOptions.cwd = options.cwd;
}
if (
typeof options.timeout === 'number' &&
Number.isFinite(options.timeout) &&
options.timeout >= 0
) {
spawnOptions.timeout = Math.floor(options.timeout);
}
const childProcess = __pi_bun_child_process();
if (childProcess && typeof childProcess.spawn === 'function') {
const child = childProcess.spawn(command, args, spawnOptions);
let stdoutText = '';
let stderrText = '';
if (child && child.stdout && typeof child.stdout.on === 'function') {
child.stdout.on('data', (chunk) => {
stdoutText += String(chunk ?? '');
});
}
if (child && child.stderr && typeof child.stderr.on === 'function') {
child.stderr.on('data', (chunk) => {
stderrText += String(chunk ?? '');
});
}
const exited = new Promise((resolve, reject) => {
let settled = false;
child.on('error', (err) => {
if (settled) return;
settled = true;
reject(err instanceof Error ? err : new Error(String(err)));
});
child.on('close', (code) => {
if (settled) return;
settled = true;
resolve(typeof code === 'number' ? code : null);
});
});
return {
pid: typeof child.pid === 'number' ? child.pid : 0,
stdin: child.stdin || null,
stdout: __pi_bun_make_text_stream(async () => {
await exited.catch(() => null);
return stdoutText;
}),
stderr: __pi_bun_make_text_stream(async () => {
await exited.catch(() => null);
return stderrText;
}),
exited,
kill(signal) {
try {
return child.kill(signal);
} catch (_) {
return false;
}
},
ref() { return this; },
unref() { return this; },
};
}
// Fallback path if node:child_process is unavailable in context.
const execOptions = {};
if (spawnOptions.cwd !== undefined) execOptions.cwd = spawnOptions.cwd;
if (spawnOptions.timeout !== undefined) execOptions.timeout = spawnOptions.timeout;
const execPromise = pi.exec(command, args, execOptions);
let killed = false;
const exited = execPromise.then(
(result) => (killed ? null : (Number(result && result.code) || 0)),
() => (killed ? null : 1),
);
return {
pid: 0,
stdin: null,
stdout: __pi_bun_make_text_stream(async () => {
try {
const result = await execPromise;
return String((result && result.stdout) || '');
} catch (_) {
return '';
}
}),
stderr: __pi_bun_make_text_stream(async () => {
try {
const result = await execPromise;
return String((result && result.stderr) || '');
} catch (_) {
return '';
}
}),
exited,
kill() {
killed = true;
return true;
},
ref() { return this; },
unref() { return this; },
};
};
globalThis.Bun = Bun;
}
if (typeof globalThis.setTimeout !== 'function') {
globalThis.setTimeout = (callback, delay, ...args) => {
const ms = Number(delay || 0);
const timer_id = __pi_set_timeout_native(ms <= 0 ? 0 : Math.floor(ms));
const captured_id = __pi_current_extension_id;
__pi_register_timer(timer_id, () => {
const prev = __pi_current_extension_id;
__pi_current_extension_id = captured_id;
try {
callback(...args);
} catch (e) {
console.error('setTimeout callback error:', e);
} finally {
__pi_current_extension_id = prev;
}
});
return timer_id;
};
}
if (typeof globalThis.clearTimeout !== 'function') {
globalThis.clearTimeout = (timer_id) => {
__pi_unregister_timer(timer_id);
try {
__pi_clear_timeout_native(timer_id);
} catch (_) {}
};
}
// setInterval polyfill using setTimeout
const __pi_intervals = new Map();
let __pi_interval_id = 0;
if (typeof globalThis.setInterval !== 'function') {
globalThis.setInterval = (callback, delay, ...args) => {
const ms = Math.max(0, Number(delay || 0));
const id = ++__pi_interval_id;
const captured_id = __pi_current_extension_id;
const run = () => {
if (!__pi_intervals.has(id)) return;
const prev = __pi_current_extension_id;
__pi_current_extension_id = captured_id;
try {
callback(...args);
} catch (e) {
console.error('setInterval callback error:', e);
} finally {
__pi_current_extension_id = prev;
}
if (__pi_intervals.has(id)) {
__pi_intervals.set(id, globalThis.setTimeout(run, ms));
}
};
__pi_intervals.set(id, globalThis.setTimeout(run, ms));
return id;
};
}
if (typeof globalThis.clearInterval !== 'function') {
globalThis.clearInterval = (id) => {
const timerId = __pi_intervals.get(id);
if (timerId !== undefined) {
globalThis.clearTimeout(timerId);
__pi_intervals.delete(id);
}
};
}
if (typeof globalThis.fetch !== 'function') {
class Headers {
constructor(init) {
this._map = {};
if (init && typeof init === 'object') {
if (Array.isArray(init)) {
for (const pair of init) {
if (pair && pair.length >= 2) this.set(pair[0], pair[1]);
}
} else if (typeof init.forEach === 'function') {
init.forEach((v, k) => this.set(k, v));
} else {
for (const k of Object.keys(init)) {
this.set(k, init[k]);
}
}
}
}
get(name) {
const key = String(name || '').toLowerCase();
return this._map[key] === undefined ? null : this._map[key];
}
set(name, value) {
const key = String(name || '').toLowerCase();
this._map[key] = String(value === undefined || value === null ? '' : value);
}
entries() {
return Object.entries(this._map);
}
}
class Response {
constructor(bodyBytes, init) {
const options = init && typeof init === 'object' ? init : {};
this.status = Number(options.status || 0);
this.ok = this.status >= 200 && this.status < 300;
this.headers = new Headers(options.headers || {});
this._bytes = bodyBytes || new Uint8Array();
this.body = {
getReader: () => {
let done = false;
return {
read: async () => {
if (done) return { done: true, value: undefined };
done = true;
return { done: false, value: this._bytes };
},
cancel: async () => {
done = true;
},
releaseLock: () => {},
};
},
};
}
async text() {
return new TextDecoder().decode(this._bytes);
}
async json() {
return JSON.parse(await this.text());
}
async arrayBuffer() {
const copy = new Uint8Array(this._bytes.length);
copy.set(this._bytes);
return copy.buffer;
}
}
globalThis.Headers = Headers;
globalThis.Response = Response;
if (typeof globalThis.Event === 'undefined') {
class Event {
constructor(type, options) {
const opts = options && typeof options === 'object' ? options : {};
this.type = String(type || '');
this.bubbles = !!opts.bubbles;
this.cancelable = !!opts.cancelable;
this.composed = !!opts.composed;
this.defaultPrevented = false;
this.target = null;
this.currentTarget = null;
this.timeStamp = Date.now();
}
preventDefault() {
if (this.cancelable) this.defaultPrevented = true;
}
stopPropagation() {}
stopImmediatePropagation() {}
}
globalThis.Event = Event;
}
if (typeof globalThis.CustomEvent === 'undefined' && typeof globalThis.Event === 'function') {
class CustomEvent extends globalThis.Event {
constructor(type, options) {
const opts = options && typeof options === 'object' ? options : {};
super(type, opts);
this.detail = opts.detail;
}
}
globalThis.CustomEvent = CustomEvent;
}
if (typeof globalThis.EventTarget === 'undefined') {
class EventTarget {
constructor() {
this.__listeners = Object.create(null);
}
addEventListener(type, listener) {
const key = String(type || '');
if (!key || !listener) return;
if (!this.__listeners[key]) this.__listeners[key] = [];
if (!this.__listeners[key].includes(listener)) this.__listeners[key].push(listener);
}
removeEventListener(type, listener) {
const key = String(type || '');
const list = this.__listeners[key];
if (!list || !listener) return;
this.__listeners[key] = list.filter((fn) => fn !== listener);
}
dispatchEvent(event) {
if (!event || typeof event.type !== 'string') return true;
const key = event.type;
const list = (this.__listeners[key] || []).slice();
try {
event.target = this;
event.currentTarget = this;
} catch (_) {}
for (const listener of list) {
try {
if (typeof listener === 'function') listener.call(this, event);
else if (listener && typeof listener.handleEvent === 'function') listener.handleEvent(event);
} catch (_) {}
}
return !(event && event.defaultPrevented);
}
}
globalThis.EventTarget = EventTarget;
}
if (typeof globalThis.TransformStream === 'undefined') {
class TransformStream {
constructor(_transformer) {
const queue = [];
let closed = false;
this.readable = {
getReader() {
return {
async read() {
if (queue.length > 0) {
return { done: false, value: queue.shift() };
}
return { done: closed, value: undefined };
},
async cancel() {
closed = true;
},
releaseLock() {},
};
},
};
this.writable = {
getWriter() {
return {
async write(chunk) {
queue.push(chunk);
},
async close() {
closed = true;
},
async abort() {
closed = true;
},
releaseLock() {},
};
},
};
}
}
globalThis.TransformStream = TransformStream;
}
// AbortController / AbortSignal polyfill — many npm packages check for these
if (typeof globalThis.AbortController === 'undefined') {
class AbortSignal {
constructor() { this.aborted = false; this._listeners = []; }
get reason() { return this.aborted ? new Error('This operation was aborted') : undefined; }
addEventListener(type, fn) { if (type === 'abort') this._listeners.push(fn); }
removeEventListener(type, fn) { if (type === 'abort') this._listeners = this._listeners.filter(f => f !== fn); }
throwIfAborted() { if (this.aborted) throw this.reason; }
static abort(reason) { const s = new AbortSignal(); s.aborted = true; s._reason = reason; return s; }
static timeout(ms) { const s = new AbortSignal(); setTimeout(() => { s.aborted = true; s._listeners.forEach(fn => fn()); }, ms); return s; }
}
class AbortController {
constructor() { this.signal = new AbortSignal(); }
abort(reason) { this.signal.aborted = true; this.signal._reason = reason; this.signal._listeners.forEach(fn => fn()); }
}
globalThis.AbortController = AbortController;
globalThis.AbortSignal = AbortSignal;
}
globalThis.fetch = async (input, init) => {
const url = typeof input === 'string' ? input : String(input && input.url ? input.url : input);
const options = init && typeof init === 'object' ? init : {};
const method = options.method ? String(options.method) : 'GET';
const headers = {};
if (options.headers && typeof options.headers === 'object') {
if (options.headers instanceof Headers) {
for (const [k, v] of options.headers.entries()) headers[k] = v;
} else if (Array.isArray(options.headers)) {
for (const pair of options.headers) {
if (pair && pair.length >= 2) headers[String(pair[0])] = String(pair[1]);
}
} else {
for (const k of Object.keys(options.headers)) {
headers[k] = String(options.headers[k]);
}
}
}
let body = undefined;
if (options.body !== undefined && options.body !== null) {
body = typeof options.body === 'string' ? options.body : String(options.body);
}
const resp = await pi.http({ url, method, headers, body });
const status = resp && resp.status !== undefined ? Number(resp.status) : 0;
const respHeaders = resp && resp.headers && typeof resp.headers === 'object' ? resp.headers : {};
let bytes = new Uint8Array();
if (resp && resp.body_bytes) {
const bin = __pi_base64_decode_native(String(resp.body_bytes));
const out = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) {
out[i] = bin.charCodeAt(i) & 0xff;
}
bytes = out;
} else if (resp && resp.body !== undefined && resp.body !== null) {
bytes = new TextEncoder().encode(String(resp.body));
}
return new Response(bytes, { status, headers: respHeaders });
};
}
";
#[cfg(test)]
#[allow(clippy::future_not_send)]
mod tests {
use super::*;
use crate::scheduler::DeterministicClock;
#[allow(clippy::future_not_send)]
async fn get_global_json<C: SchedulerClock + 'static>(
runtime: &PiJsRuntime<C>,
name: &str,
) -> serde_json::Value {
runtime
.context
.with(|ctx| {
let global = ctx.globals();
let value: Value<'_> = global.get(name)?;
js_to_json(&value)
})
.await
.expect("js context")
}
#[allow(clippy::future_not_send)]
async fn call_global_fn_json<C: SchedulerClock + 'static>(
runtime: &PiJsRuntime<C>,
name: &str,
) -> serde_json::Value {
runtime
.context
.with(|ctx| {
let global = ctx.globals();
let function: Function<'_> = global.get(name)?;
let value: Value<'_> = function.call(())?;
js_to_json(&value)
})
.await
.expect("js context")
}
#[allow(clippy::future_not_send)]
async fn runtime_with_sync_exec_enabled(
clock: Arc<DeterministicClock>,
) -> PiJsRuntime<Arc<DeterministicClock>> {
let config = PiJsRuntimeConfig {
allow_unsafe_sync_exec: true,
..PiJsRuntimeConfig::default()
};
PiJsRuntime::with_clock_and_config_with_policy(clock, config, None)
.await
.expect("create runtime")
}
#[allow(clippy::future_not_send)]
async fn drain_until_idle(
runtime: &PiJsRuntime<Arc<DeterministicClock>>,
clock: &Arc<DeterministicClock>,
) {
for _ in 0..10_000 {
if !runtime.has_pending() {
break;
}
let stats = runtime.tick().await.expect("tick");
if stats.ran_macrotask {
continue;
}
let next_deadline = runtime.scheduler.borrow().next_timer_deadline();
let Some(next_deadline) = next_deadline else {
break;
};
let now = runtime.now_ms();
assert!(
next_deadline > now,
"expected future timer deadline (deadline={next_deadline}, now={now})"
);
clock.set(next_deadline);
}
}
#[test]
fn extract_static_require_specifiers_skips_literals_and_comments() {
let source = r#"
const fs = require("fs");
const text = "require('left-pad')";
const tpl = `require("ajv/dist/runtime/validation_error").default`;
// require("zlib")
/* require("tty") */
const path = require('path');
"#;
let specifiers = extract_static_require_specifiers(source);
assert_eq!(specifiers, vec!["fs".to_string(), "path".to_string()]);
}
#[test]
fn maybe_cjs_to_esm_ignores_codegen_string_requires() {
let source = r#"
const fs = require("fs");
const generated = `require("ajv/dist/runtime/validation_error").default`;
module.exports = { fs, generated };
"#;
let rewritten = maybe_cjs_to_esm(source);
assert!(rewritten.contains(r#"from "fs";"#));
assert!(!rewritten.contains(r#"from "ajv/dist/runtime/validation_error";"#));
}
#[test]
fn extract_import_names_handles_default_plus_named_imports() {
let source = r#"
import Ajv, {
KeywordDefinition,
type AnySchema,
ValidationError as AjvValidationError,
} from "ajv";
"#;
let names = extract_import_names(source, "ajv");
assert_eq!(
names,
vec![
"KeywordDefinition".to_string(),
"ValidationError".to_string()
]
);
}
#[test]
fn extract_builtin_import_names_collects_node_aliases() {
let source = r#"
import { isIP } from "net";
import { isIPv4 as netIsIpv4 } from "node:net";
"#;
let names = extract_builtin_import_names(source, "node:net", "node:net");
assert_eq!(
names.into_iter().collect::<Vec<_>>(),
vec!["isIP".to_string(), "isIPv4".to_string()]
);
}
#[test]
fn builtin_overlay_generation_scopes_exports_per_importing_module() {
let temp_dir = tempfile::tempdir().expect("tempdir");
let base_a = temp_dir.path().join("a.mjs");
let base_b = temp_dir.path().join("b.mjs");
std::fs::write(&base_a, r#"import { isIP } from "net";"#).expect("write a");
std::fs::write(&base_b, r#"import { isIPv6 } from "node:net";"#).expect("write b");
let mut state = PiJsModuleState::new();
let overlay_a = maybe_register_builtin_compat_overlay(
&mut state,
base_a.to_string_lossy().as_ref(),
"net",
"node:net",
)
.expect("overlay key for a");
let overlay_b = maybe_register_builtin_compat_overlay(
&mut state,
base_b.to_string_lossy().as_ref(),
"node:net",
"node:net",
)
.expect("overlay key for b");
assert!(overlay_a.starts_with("pijs-compat://builtin/node:net/"));
assert!(overlay_b.starts_with("pijs-compat://builtin/node:net/"));
assert_ne!(overlay_a, overlay_b);
let exported_names_a = state
.dynamic_virtual_named_exports
.get(&overlay_a)
.expect("export names for a");
assert!(exported_names_a.contains("isIP"));
assert!(!exported_names_a.contains("isIPv6"));
let exported_names_b = state
.dynamic_virtual_named_exports
.get(&overlay_b)
.expect("export names for b");
assert!(exported_names_b.contains("isIPv6"));
assert!(!exported_names_b.contains("isIP"));
let overlay_source_a = state
.dynamic_virtual_modules
.get(&overlay_a)
.expect("overlay source for a");
assert!(overlay_source_a.contains(r#"import * as __pijs_builtin_ns from "node:net";"#));
assert!(overlay_source_a.contains("export const isIP ="));
assert!(!overlay_source_a.contains("export const isIPv6 ="));
let overlay_source_b = state
.dynamic_virtual_modules
.get(&overlay_b)
.expect("overlay source for b");
assert!(overlay_source_b.contains("export const isIPv6 ="));
assert!(!overlay_source_b.contains("export const isIP ="));
}
#[test]
fn hostcall_completions_run_before_due_timers() {
let clock = Arc::new(ManualClock::new(1_000));
let mut loop_state = PiEventLoop::new(ClockHandle::new(clock));
let _timer = loop_state.set_timeout(0);
loop_state.enqueue_hostcall_completion("call-1");
let mut seen = Vec::new();
let result = loop_state.tick(|task| seen.push(task.kind), || false);
assert!(result.ran_macrotask);
assert_eq!(
seen,
vec![MacrotaskKind::HostcallComplete {
call_id: "call-1".to_string()
}]
);
}
#[test]
fn hostcall_request_queue_spills_to_overflow_with_stable_order() {
fn req(id: usize) -> HostcallRequest {
HostcallRequest {
call_id: format!("call-{id}"),
kind: HostcallKind::Log,
payload: serde_json::json!({ "n": id }),
trace_id: u64::try_from(id).unwrap_or(u64::MAX),
extension_id: Some("ext.queue".to_string()),
}
}
let mut queue = HostcallRequestQueue::with_capacities(2, 4);
assert!(matches!(
queue.push_back(req(0)),
HostcallQueueEnqueueResult::FastPath { .. }
));
assert!(matches!(
queue.push_back(req(1)),
HostcallQueueEnqueueResult::FastPath { .. }
));
assert!(matches!(
queue.push_back(req(2)),
HostcallQueueEnqueueResult::OverflowPath { .. }
));
let snapshot = queue.snapshot();
assert_eq!(snapshot.fast_depth, 2);
assert_eq!(snapshot.overflow_depth, 1);
assert_eq!(snapshot.total_depth, 3);
assert_eq!(snapshot.overflow_enqueued_total, 1);
let drained = queue.drain_all();
let drained_ids: Vec<_> = drained.into_iter().map(|item| item.call_id).collect();
assert_eq!(
drained_ids,
vec![
"call-0".to_string(),
"call-1".to_string(),
"call-2".to_string()
]
);
}
#[test]
fn hostcall_request_queue_rejects_when_overflow_capacity_reached() {
fn req(id: usize) -> HostcallRequest {
HostcallRequest {
call_id: format!("reject-{id}"),
kind: HostcallKind::Log,
payload: serde_json::json!({ "n": id }),
trace_id: u64::try_from(id).unwrap_or(u64::MAX),
extension_id: None,
}
}
let mut queue = HostcallRequestQueue::with_capacities(1, 1);
assert!(matches!(
queue.push_back(req(0)),
HostcallQueueEnqueueResult::FastPath { .. }
));
assert!(matches!(
queue.push_back(req(1)),
HostcallQueueEnqueueResult::OverflowPath { .. }
));
let reject = queue.push_back(req(2));
assert!(matches!(
reject,
HostcallQueueEnqueueResult::Rejected { .. }
));
let snapshot = queue.snapshot();
assert_eq!(snapshot.total_depth, 2);
assert_eq!(snapshot.overflow_depth, 1);
assert_eq!(snapshot.overflow_rejected_total, 1);
}
#[test]
fn timers_order_by_deadline_then_schedule_seq() {
let clock = Arc::new(ManualClock::new(0));
let mut loop_state = PiEventLoop::new(ClockHandle::new(clock.clone()));
let t1 = loop_state.set_timeout(10);
let t2 = loop_state.set_timeout(10);
let t3 = loop_state.set_timeout(5);
clock.set(10);
let mut fired = Vec::new();
for _ in 0..3 {
loop_state.tick(
|task| {
if let MacrotaskKind::TimerFired { timer_id } = task.kind {
fired.push(timer_id);
}
},
|| false,
);
}
assert_eq!(fired, vec![t3, t1, t2]);
}
#[test]
fn clear_timeout_prevents_fire() {
let clock = Arc::new(ManualClock::new(0));
let mut loop_state = PiEventLoop::new(ClockHandle::new(clock.clone()));
let timer_id = loop_state.set_timeout(5);
assert!(loop_state.clear_timeout(timer_id));
clock.set(10);
let mut fired = Vec::new();
let result = loop_state.tick(
|task| {
if let MacrotaskKind::TimerFired { timer_id } = task.kind {
fired.push(timer_id);
}
},
|| false,
);
assert!(!result.ran_macrotask);
assert!(fired.is_empty());
}
#[test]
fn clear_timeout_nonexistent_returns_false_and_does_not_pollute_cancelled_set() {
let clock = Arc::new(ManualClock::new(0));
let mut loop_state = PiEventLoop::new(ClockHandle::new(clock));
assert!(!loop_state.clear_timeout(42));
assert!(
loop_state.cancelled_timers.is_empty(),
"unknown timer ids should not be retained"
);
}
#[test]
fn clear_timeout_double_cancel_returns_false() {
let clock = Arc::new(ManualClock::new(0));
let mut loop_state = PiEventLoop::new(ClockHandle::new(clock));
let timer_id = loop_state.set_timeout(10);
assert!(loop_state.clear_timeout(timer_id));
assert!(!loop_state.clear_timeout(timer_id));
}
#[test]
fn pi_event_loop_timer_id_saturates_at_u64_max() {
let clock = Arc::new(ManualClock::new(0));
let mut loop_state = PiEventLoop::new(ClockHandle::new(clock));
loop_state.next_timer_id = u64::MAX;
let first = loop_state.set_timeout(10);
let second = loop_state.set_timeout(20);
assert_eq!(first, u64::MAX);
assert_eq!(second, u64::MAX);
}
#[test]
fn audit_ledger_sequence_saturates_at_u64_max() {
let mut ledger = AuditLedger::new();
ledger.next_sequence = u64::MAX;
let first = ledger.append(
1_700_000_000_000,
"ext-a",
AuditEntryKind::Analysis,
"first".to_string(),
Vec::new(),
);
let second = ledger.append(
1_700_000_000_100,
"ext-a",
AuditEntryKind::ProposalGenerated,
"second".to_string(),
Vec::new(),
);
assert_eq!(first, u64::MAX);
assert_eq!(second, u64::MAX);
assert_eq!(ledger.len(), 2);
}
#[test]
fn microtasks_drain_to_fixpoint_after_macrotask() {
let clock = Arc::new(ManualClock::new(0));
let mut loop_state = PiEventLoop::new(ClockHandle::new(clock));
loop_state.enqueue_inbound_event("evt-1");
let mut drain_calls = 0;
let result = loop_state.tick(
|_task| {},
|| {
drain_calls += 1;
drain_calls <= 2
},
);
assert!(result.ran_macrotask);
assert_eq!(result.microtasks_drained, 2);
assert_eq!(drain_calls, 3);
}
#[test]
fn compile_module_source_reports_missing_file() {
let temp_dir = tempfile::tempdir().expect("tempdir");
let missing_path = temp_dir.path().join("missing.js");
let err = compile_module_source(
&HashMap::new(),
&HashMap::new(),
missing_path.to_string_lossy().as_ref(),
)
.expect_err("missing module should error");
let message = err.to_string();
assert!(
message.contains("Module is not a file"),
"unexpected error: {message}"
);
}
#[test]
fn compile_module_source_reports_unsupported_extension() {
let temp_dir = tempfile::tempdir().expect("tempdir");
let bad_path = temp_dir.path().join("module.txt");
std::fs::write(&bad_path, "hello").expect("write module.txt");
let err = compile_module_source(
&HashMap::new(),
&HashMap::new(),
bad_path.to_string_lossy().as_ref(),
)
.expect_err("unsupported extension should error");
let message = err.to_string();
assert!(
message.contains("Unsupported module extension"),
"unexpected error: {message}"
);
}
#[test]
fn module_cache_key_changes_when_virtual_module_changes() {
let static_modules = HashMap::new();
let mut dynamic_modules = HashMap::new();
dynamic_modules.insert("pijs://virt".to_string(), "export const x = 1;".to_string());
let key_before = module_cache_key(&static_modules, &dynamic_modules, "pijs://virt")
.expect("virtual key should exist");
dynamic_modules.insert("pijs://virt".to_string(), "export const x = 2;".to_string());
let key_after = module_cache_key(&static_modules, &dynamic_modules, "pijs://virt")
.expect("virtual key should exist");
assert_ne!(key_before, key_after);
}
#[test]
fn module_cache_key_changes_when_file_size_changes() {
let temp_dir = tempfile::tempdir().expect("tempdir");
let module_path = temp_dir.path().join("module.js");
std::fs::write(&module_path, "export const x = 1;\n").expect("write module");
let name = module_path.to_string_lossy().to_string();
let key_before =
module_cache_key(&HashMap::new(), &HashMap::new(), &name).expect("file key");
std::fs::write(&module_path, "export const xyz = 123456;\n").expect("rewrite module");
let key_after =
module_cache_key(&HashMap::new(), &HashMap::new(), &name).expect("file key");
assert_ne!(key_before, key_after);
}
#[test]
fn load_compiled_module_source_tracks_hit_miss_and_invalidation_counters() {
let temp_dir = tempfile::tempdir().expect("tempdir");
let module_path = temp_dir.path().join("module.js");
std::fs::write(&module_path, "export const x = 1;\n").expect("write module");
let name = module_path.to_string_lossy().to_string();
let mut state = PiJsModuleState::new();
let _first = load_compiled_module_source(&mut state, &name).expect("first compile");
assert_eq!(state.module_cache_counters.hits, 0);
assert_eq!(state.module_cache_counters.misses, 1);
assert_eq!(state.module_cache_counters.invalidations, 0);
assert_eq!(state.compiled_sources.len(), 1);
let _second = load_compiled_module_source(&mut state, &name).expect("cache hit");
assert_eq!(state.module_cache_counters.hits, 1);
assert_eq!(state.module_cache_counters.misses, 1);
assert_eq!(state.module_cache_counters.invalidations, 0);
std::fs::write(&module_path, "export const xyz = 123456;\n").expect("rewrite module");
let _third = load_compiled_module_source(&mut state, &name).expect("recompile");
assert_eq!(state.module_cache_counters.hits, 1);
assert_eq!(state.module_cache_counters.misses, 2);
assert_eq!(state.module_cache_counters.invalidations, 1);
}
#[test]
fn load_compiled_module_source_uses_disk_cache_between_states() {
let temp_dir = tempfile::tempdir().expect("tempdir");
let cache_dir = temp_dir.path().join("cache");
let module_path = temp_dir.path().join("module.js");
std::fs::write(&module_path, "export const x = 1;\n").expect("write module");
let name = module_path.to_string_lossy().to_string();
let mut first_state = PiJsModuleState::new().with_disk_cache_dir(Some(cache_dir.clone()));
let first = load_compiled_module_source(&mut first_state, &name).expect("first compile");
assert_eq!(first_state.module_cache_counters.misses, 1);
assert_eq!(first_state.module_cache_counters.disk_hits, 0);
let key = module_cache_key(&HashMap::new(), &HashMap::new(), &name).expect("file key");
let cache_path = disk_cache_path(&cache_dir, &key);
assert!(
cache_path.exists(),
"expected persisted cache at {cache_path:?}"
);
let mut second_state = PiJsModuleState::new().with_disk_cache_dir(Some(cache_dir));
let second =
load_compiled_module_source(&mut second_state, &name).expect("load from disk cache");
assert_eq!(second_state.module_cache_counters.disk_hits, 1);
assert_eq!(second_state.module_cache_counters.misses, 0);
assert_eq!(second_state.module_cache_counters.hits, 0);
assert_eq!(first, second);
}
#[test]
fn load_compiled_module_source_disk_cache_invalidates_when_file_changes() {
let temp_dir = tempfile::tempdir().expect("tempdir");
let cache_dir = temp_dir.path().join("cache");
let module_path = temp_dir.path().join("module.js");
std::fs::write(&module_path, "export const x = 1;\n").expect("write module");
let name = module_path.to_string_lossy().to_string();
let mut prime_state = PiJsModuleState::new().with_disk_cache_dir(Some(cache_dir.clone()));
let first = load_compiled_module_source(&mut prime_state, &name).expect("first compile");
let first_key = module_cache_key(&HashMap::new(), &HashMap::new(), &name).expect("key");
std::fs::write(
&module_path,
"export const xyz = 1234567890;\nexport const more = true;\n",
)
.expect("rewrite module");
let second_key = module_cache_key(&HashMap::new(), &HashMap::new(), &name).expect("key");
assert_ne!(first_key, second_key);
let mut second_state = PiJsModuleState::new().with_disk_cache_dir(Some(cache_dir));
let second = load_compiled_module_source(&mut second_state, &name).expect("recompile");
assert_eq!(second_state.module_cache_counters.disk_hits, 0);
assert_eq!(second_state.module_cache_counters.misses, 1);
assert_ne!(first, second);
}
#[test]
fn warm_reset_clears_extension_registry_state() {
futures::executor::block_on(async {
let runtime = PiJsRuntime::with_clock(DeterministicClock::new(0))
.await
.expect("create runtime");
runtime
.eval(
r#"
__pi_begin_extension("ext.reset", { name: "ext.reset" });
pi.registerTool({
name: "warm_reset_tool",
execute: async (_callId, _input) => ({ ok: true }),
});
pi.registerCommand("warm_reset_cmd", {
handler: async (_args, _ctx) => ({ ok: true }),
});
pi.on("startup", async () => {});
__pi_end_extension();
"#,
)
.await
.expect("register extension state");
let before = call_global_fn_json(&runtime, "__pi_runtime_registry_snapshot").await;
assert_eq!(before["extensions"], serde_json::json!(1));
assert_eq!(before["tools"], serde_json::json!(1));
assert_eq!(before["commands"], serde_json::json!(1));
let report = runtime
.reset_for_warm_reload()
.await
.expect("warm reset should run");
assert!(report.reused, "expected warm reuse, got report: {report:?}");
assert!(
report.reason_code.is_none(),
"unexpected warm-reset reason: {:?}",
report.reason_code
);
let after = call_global_fn_json(&runtime, "__pi_runtime_registry_snapshot").await;
assert_eq!(after["extensions"], serde_json::json!(0));
assert_eq!(after["tools"], serde_json::json!(0));
assert_eq!(after["commands"], serde_json::json!(0));
assert_eq!(after["hooks"], serde_json::json!(0));
assert_eq!(after["pendingTasks"], serde_json::json!(0));
assert_eq!(after["pendingHostcalls"], serde_json::json!(0));
});
}
#[test]
fn warm_reset_reports_pending_rust_work() {
futures::executor::block_on(async {
let runtime = PiJsRuntime::with_clock(DeterministicClock::new(0))
.await
.expect("create runtime");
let _timer = runtime.set_timeout(10);
let report = runtime
.reset_for_warm_reload()
.await
.expect("warm reset should return report");
assert!(!report.reused);
assert_eq!(report.reason_code.as_deref(), Some("pending_rust_work"));
});
}
#[test]
fn warm_reset_reports_pending_js_work() {
futures::executor::block_on(async {
let runtime = PiJsRuntime::with_clock(DeterministicClock::new(0))
.await
.expect("create runtime");
runtime
.eval(
r#"
__pi_tasks.set("pending-task", { status: "pending" });
"#,
)
.await
.expect("inject pending JS task");
let report = runtime
.reset_for_warm_reload()
.await
.expect("warm reset should return report");
assert!(!report.reused);
assert_eq!(report.reason_code.as_deref(), Some("pending_js_work"));
let after = call_global_fn_json(&runtime, "__pi_runtime_registry_snapshot").await;
assert_eq!(after["pendingTasks"], serde_json::json!(0));
});
}
#[test]
#[allow(clippy::too_many_lines)]
fn reset_transient_state_preserves_compiled_cache_and_clears_transient_state() {
futures::executor::block_on(async {
let runtime = PiJsRuntime::with_clock(DeterministicClock::new(0))
.await
.expect("create runtime");
let cache_key = "pijs://virtual".to_string();
{
let mut state = runtime.module_state.borrow_mut();
let extension_root = PathBuf::from("/tmp/ext-root");
state.extension_roots.push(extension_root.clone());
state
.extension_root_tiers
.insert(extension_root.clone(), ProxyStubSourceTier::Community);
state
.extension_root_scopes
.insert(extension_root, "@scope".to_string());
state
.dynamic_virtual_modules
.insert(cache_key.clone(), "export const v = 1;".to_string());
let mut exports = BTreeSet::new();
exports.insert("v".to_string());
state
.dynamic_virtual_named_exports
.insert(cache_key.clone(), exports);
state.compiled_sources.insert(
cache_key.clone(),
CompiledModuleCacheEntry {
cache_key: Some("cache-v1".to_string()),
source: b"compiled-source".to_vec().into(),
},
);
state.module_cache_counters = ModuleCacheCounters {
hits: 3,
misses: 4,
invalidations: 5,
disk_hits: 6,
};
}
runtime
.hostcall_queue
.borrow_mut()
.push_back(HostcallRequest {
call_id: "call-1".to_string(),
kind: HostcallKind::Tool {
name: "read".to_string(),
},
payload: serde_json::json!({}),
trace_id: 1,
extension_id: Some("ext.reset".to_string()),
});
runtime
.hostcall_tracker
.borrow_mut()
.register("call-1".to_string(), Some(42), 0);
runtime
.hostcalls_total
.store(11, std::sync::atomic::Ordering::SeqCst);
runtime
.hostcalls_timed_out
.store(2, std::sync::atomic::Ordering::SeqCst);
runtime
.tick_counter
.store(7, std::sync::atomic::Ordering::SeqCst);
runtime.reset_transient_state();
{
let state = runtime.module_state.borrow();
assert!(state.extension_roots.is_empty());
assert!(state.extension_root_tiers.is_empty());
assert!(state.extension_root_scopes.is_empty());
assert!(state.dynamic_virtual_modules.is_empty());
assert!(state.dynamic_virtual_named_exports.is_empty());
let cached = state
.compiled_sources
.get(&cache_key)
.expect("compiled source should persist across reset");
assert_eq!(cached.cache_key.as_deref(), Some("cache-v1"));
assert_eq!(cached.source.as_ref(), b"compiled-source");
assert_eq!(state.module_cache_counters.hits, 0);
assert_eq!(state.module_cache_counters.misses, 0);
assert_eq!(state.module_cache_counters.invalidations, 0);
assert_eq!(state.module_cache_counters.disk_hits, 0);
}
assert!(runtime.hostcall_queue.borrow().is_empty());
assert_eq!(runtime.hostcall_tracker.borrow().pending_count(), 0);
assert_eq!(
runtime
.hostcalls_total
.load(std::sync::atomic::Ordering::SeqCst),
0
);
assert_eq!(
runtime
.hostcalls_timed_out
.load(std::sync::atomic::Ordering::SeqCst),
0
);
assert_eq!(
runtime
.tick_counter
.load(std::sync::atomic::Ordering::SeqCst),
0
);
});
}
#[test]
fn warm_isolate_pool_tracks_created_and_reset_counts() {
let cache_dir = tempfile::tempdir().expect("tempdir");
let template = PiJsRuntimeConfig {
cwd: "/tmp/warm-pool".to_string(),
args: vec!["--flag".to_string()],
env: HashMap::from([("PI_POOL".to_string(), "yes".to_string())]),
deny_env: false,
disk_cache_dir: Some(cache_dir.path().join("module-cache")),
..PiJsRuntimeConfig::default()
};
let expected_disk_cache_dir = template.disk_cache_dir.clone();
let pool = WarmIsolatePool::new(template.clone());
assert_eq!(pool.created_count(), 0);
assert_eq!(pool.reset_count(), 0);
let cfg_a = pool.make_config();
let cfg_b = pool.make_config();
assert_eq!(pool.created_count(), 2);
assert_eq!(cfg_a.cwd, template.cwd);
assert_eq!(cfg_b.cwd, template.cwd);
assert_eq!(cfg_a.args, template.args);
assert_eq!(cfg_a.env.get("PI_POOL"), Some(&"yes".to_string()));
assert_eq!(cfg_a.deny_env, template.deny_env);
assert_eq!(cfg_a.disk_cache_dir, expected_disk_cache_dir);
pool.record_reset();
pool.record_reset();
assert_eq!(pool.reset_count(), 2);
}
#[test]
fn resolver_error_messages_are_classified_deterministically() {
assert_eq!(
unsupported_module_specifier_message("left-pad"),
"Package module specifiers are not supported in PiJS: left-pad"
);
assert_eq!(
unsupported_module_specifier_message("https://example.com/mod.js"),
"Network module imports are not supported in PiJS: https://example.com/mod.js"
);
assert_eq!(
unsupported_module_specifier_message("pi:internal/foo"),
"Unsupported module specifier: pi:internal/foo"
);
}
#[test]
fn resolve_module_path_uses_documented_candidate_order() {
let temp_dir = tempfile::tempdir().expect("tempdir");
let root = temp_dir.path();
let base = root.join("entry.ts");
std::fs::write(&base, "export {};\n").expect("write base");
let pkg_dir = root.join("pkg");
std::fs::create_dir_all(&pkg_dir).expect("mkdir pkg");
let pkg_index_js = pkg_dir.join("index.js");
let pkg_index_ts = pkg_dir.join("index.ts");
std::fs::write(&pkg_index_js, "export const js = true;\n").expect("write index.js");
std::fs::write(&pkg_index_ts, "export const ts = true;\n").expect("write index.ts");
let module_js = root.join("module.js");
let module_ts = root.join("module.ts");
std::fs::write(&module_js, "export const js = true;\n").expect("write module.js");
std::fs::write(&module_ts, "export const ts = true;\n").expect("write module.ts");
let only_json = root.join("only_json.json");
std::fs::write(&only_json, "{\"ok\":true}\n").expect("write only_json.json");
let mode = RepairMode::default();
let roots = vec![];
let resolved_pkg =
resolve_module_path(base.to_string_lossy().as_ref(), "./pkg", mode, &roots)
.expect("resolve ./pkg");
assert_eq!(resolved_pkg, pkg_index_ts);
let resolved_module =
resolve_module_path(base.to_string_lossy().as_ref(), "./module", mode, &roots)
.expect("resolve ./module");
assert_eq!(resolved_module, module_ts);
let resolved_json =
resolve_module_path(base.to_string_lossy().as_ref(), "./only_json", mode, &roots)
.expect("resolve ./only_json");
assert_eq!(resolved_json, only_json);
let file_url = format!("file://{}", module_ts.display());
let resolved_file_url =
resolve_module_path(base.to_string_lossy().as_ref(), &file_url, mode, &roots)
.expect("file://");
assert_eq!(resolved_file_url, module_ts);
}
#[test]
fn resolve_module_path_blocks_file_url_outside_extension_root() {
let temp_dir = tempfile::tempdir().expect("tempdir");
let root = temp_dir.path();
let extension_root = root.join("ext");
std::fs::create_dir_all(&extension_root).expect("mkdir ext");
let base = extension_root.join("index.ts");
std::fs::write(&base, "export {};\n").expect("write base");
let outside = root.join("secret.ts");
std::fs::write(&outside, "export const secret = 1;\n").expect("write outside");
let mode = RepairMode::default();
let roots = vec![extension_root];
let file_url = format!("file://{}", outside.display());
let resolved =
resolve_module_path(base.to_string_lossy().as_ref(), &file_url, mode, &roots);
assert!(
resolved.is_none(),
"file:// import outside extension root should be blocked, got {resolved:?}"
);
}
#[test]
fn resolve_module_path_allows_file_url_inside_extension_root() {
let temp_dir = tempfile::tempdir().expect("tempdir");
let root = temp_dir.path();
let extension_root = root.join("ext");
std::fs::create_dir_all(&extension_root).expect("mkdir ext");
let base = extension_root.join("index.ts");
std::fs::write(&base, "export {};\n").expect("write base");
let inside = extension_root.join("module.ts");
std::fs::write(&inside, "export const ok = 1;\n").expect("write inside");
let mode = RepairMode::default();
let roots = vec![extension_root];
let file_url = format!("file://{}", inside.display());
let resolved =
resolve_module_path(base.to_string_lossy().as_ref(), &file_url, mode, &roots);
assert_eq!(resolved, Some(inside));
}
#[test]
fn pijs_dynamic_import_reports_deterministic_package_error() {
futures::executor::block_on(async {
let runtime = PiJsRuntime::with_clock(DeterministicClock::new(0))
.await
.expect("create runtime");
runtime
.eval(
r"
globalThis.packageImportError = {};
import('left-pad')
.then(() => {
globalThis.packageImportError.done = true;
globalThis.packageImportError.message = '';
})
.catch((err) => {
globalThis.packageImportError.done = true;
globalThis.packageImportError.message = String((err && err.message) || err || '');
});
",
)
.await
.expect("eval package import");
let result = get_global_json(&runtime, "packageImportError").await;
assert_eq!(result["done"], serde_json::json!(true));
let message = result["message"].as_str().unwrap_or_default();
assert!(
message.contains("Package module specifiers are not supported in PiJS: left-pad"),
"unexpected message: {message}"
);
});
}
#[test]
fn proxy_stub_allowlist_blocks_sensitive_packages() {
assert!(is_proxy_blocklisted_package("node:fs"));
assert!(is_proxy_blocklisted_package("fs"));
assert!(is_proxy_blocklisted_package("child_process"));
assert!(!is_proxy_blocklisted_package("@aliou/pi-utils-settings"));
}
#[test]
fn proxy_stub_allowlist_accepts_curated_scope_and_pi_pattern() {
assert!(is_proxy_allowlisted_package("@sourcegraph/scip-python"));
assert!(is_proxy_allowlisted_package("@aliou/pi-utils-settings"));
assert!(is_proxy_allowlisted_package("@example/pi-helpers"));
assert!(!is_proxy_allowlisted_package("left-pad"));
}
#[test]
fn proxy_stub_allows_same_scope_packages_for_extension() {
let temp_dir = tempfile::tempdir().expect("tempdir");
let root = temp_dir.path().join("community").join("scope-ext");
std::fs::create_dir_all(&root).expect("mkdir root");
std::fs::write(
root.join("package.json"),
r#"{ "name": "@qualisero/my-ext", "version": "1.0.0" }"#,
)
.expect("write package.json");
let base = root.join("index.mjs");
std::fs::write(&base, "export {};\n").expect("write base");
let mut tiers = HashMap::new();
tiers.insert(root.clone(), ProxyStubSourceTier::Community);
let mut scopes = HashMap::new();
scopes.insert(root.clone(), "@qualisero".to_string());
assert!(should_auto_stub_package(
"@qualisero/shared-lib",
base.to_string_lossy().as_ref(),
&[root],
&tiers,
&scopes,
));
}
#[test]
fn proxy_stub_allows_non_blocklisted_package_for_community_tier() {
let temp_dir = tempfile::tempdir().expect("tempdir");
let root = temp_dir.path().join("community").join("generic-ext");
std::fs::create_dir_all(&root).expect("mkdir root");
let base = root.join("index.mjs");
std::fs::write(&base, "export {};\n").expect("write base");
let mut tiers = HashMap::new();
tiers.insert(root.clone(), ProxyStubSourceTier::Community);
assert!(should_auto_stub_package(
"left-pad",
base.to_string_lossy().as_ref(),
&[root],
&tiers,
&HashMap::new(),
));
}
#[test]
fn proxy_stub_disallowed_for_official_tier() {
let temp_dir = tempfile::tempdir().expect("tempdir");
let root = temp_dir.path().join("official-pi-mono").join("my-ext");
std::fs::create_dir_all(&root).expect("mkdir root");
let base = root.join("index.mjs");
std::fs::write(&base, "export {};\n").expect("write base");
let mut tiers = HashMap::new();
tiers.insert(root.clone(), ProxyStubSourceTier::Official);
assert!(!should_auto_stub_package(
"left-pad",
base.to_string_lossy().as_ref(),
&[root],
&tiers,
&HashMap::new(),
));
}
#[test]
fn pijs_dynamic_import_autostrict_allows_missing_npm_proxy_stub() {
const TEST_PKG: &str = "@aliou/pi-missing-proxy-test";
futures::executor::block_on(async {
let temp_dir = tempfile::tempdir().expect("tempdir");
let ext_dir = temp_dir.path().join("community").join("proxy-ext");
std::fs::create_dir_all(&ext_dir).expect("mkdir ext");
let entry = ext_dir.join("index.mjs");
std::fs::write(
&entry,
r#"
import dep from "@aliou/pi-missing-proxy-test";
globalThis.__proxyProbe = {
kind: typeof dep,
chain: typeof dep.foo.bar(),
primitive: String(dep),
};
export default dep;
"#,
)
.expect("write extension module");
let config = PiJsRuntimeConfig {
repair_mode: RepairMode::AutoStrict,
..PiJsRuntimeConfig::default()
};
let runtime = PiJsRuntime::with_clock_and_config_with_policy(
DeterministicClock::new(0),
config,
None,
)
.await
.expect("create runtime");
runtime.add_extension_root_with_id(ext_dir.clone(), Some("community/proxy-ext"));
let entry_spec = format!("file://{}", entry.display());
let script = format!(
r#"
globalThis.proxyImport = {{}};
import({entry_spec:?})
.then(() => {{
globalThis.proxyImport.done = true;
globalThis.proxyImport.error = "";
}})
.catch((err) => {{
globalThis.proxyImport.done = true;
globalThis.proxyImport.error = String((err && err.message) || err || "");
}});
"#
);
runtime.eval(&script).await.expect("eval import");
let result = get_global_json(&runtime, "proxyImport").await;
assert_eq!(result["done"], serde_json::json!(true));
assert_eq!(result["error"], serde_json::json!(""));
let probe = get_global_json(&runtime, "__proxyProbe").await;
assert_eq!(probe["kind"], serde_json::json!("function"));
assert_eq!(probe["chain"], serde_json::json!("function"));
assert_eq!(probe["primitive"], serde_json::json!(""));
let events = runtime.drain_repair_events();
assert!(events.iter().any(|event| {
event.pattern == RepairPattern::MissingNpmDep
&& event.repair_action.contains(TEST_PKG)
}));
});
}
#[test]
fn pijs_dynamic_import_autosafe_rejects_missing_npm_proxy_stub() {
const TEST_PKG: &str = "@aliou/pi-missing-proxy-test-safe";
futures::executor::block_on(async {
let temp_dir = tempfile::tempdir().expect("tempdir");
let ext_dir = temp_dir.path().join("community").join("proxy-ext-safe");
std::fs::create_dir_all(&ext_dir).expect("mkdir ext");
let entry = ext_dir.join("index.mjs");
std::fs::write(
&entry,
r#"import dep from "@aliou/pi-missing-proxy-test-safe"; export default dep;"#,
)
.expect("write extension module");
let config = PiJsRuntimeConfig {
repair_mode: RepairMode::AutoSafe,
..PiJsRuntimeConfig::default()
};
let runtime = PiJsRuntime::with_clock_and_config_with_policy(
DeterministicClock::new(0),
config,
None,
)
.await
.expect("create runtime");
runtime.add_extension_root_with_id(ext_dir.clone(), Some("community/proxy-ext-safe"));
let entry_spec = format!("file://{}", entry.display());
let script = format!(
r#"
globalThis.proxySafeImport = {{}};
import({entry_spec:?})
.then(() => {{
globalThis.proxySafeImport.done = true;
globalThis.proxySafeImport.error = "";
}})
.catch((err) => {{
globalThis.proxySafeImport.done = true;
globalThis.proxySafeImport.error = String((err && err.message) || err || "");
}});
"#
);
runtime.eval(&script).await.expect("eval import");
let result = get_global_json(&runtime, "proxySafeImport").await;
assert_eq!(result["done"], serde_json::json!(true));
let message = result["error"].as_str().unwrap_or_default();
// Check error class without the full package name at the tail:
// on macOS the longer temp paths can cause QuickJS error
// formatting to truncate the final characters of the message.
assert!(
message.contains("Package module specifiers are not supported in PiJS"),
"unexpected message: {message}"
);
});
}
#[test]
fn pijs_dynamic_import_existing_virtual_module_does_not_emit_missing_npm_repair() {
futures::executor::block_on(async {
let temp_dir = tempfile::tempdir().expect("tempdir");
let ext_dir = temp_dir.path().join("community").join("proxy-ext-existing");
std::fs::create_dir_all(&ext_dir).expect("mkdir ext");
let entry = ext_dir.join("index.mjs");
std::fs::write(
&entry,
r#"
import { ConfigLoader } from "@aliou/pi-utils-settings";
globalThis.__existingVirtualProbe = typeof ConfigLoader;
export default ConfigLoader;
"#,
)
.expect("write extension module");
let config = PiJsRuntimeConfig {
repair_mode: RepairMode::AutoStrict,
..PiJsRuntimeConfig::default()
};
let runtime = PiJsRuntime::with_clock_and_config_with_policy(
DeterministicClock::new(0),
config,
None,
)
.await
.expect("create runtime");
runtime
.add_extension_root_with_id(ext_dir.clone(), Some("community/proxy-ext-existing"));
let entry_spec = format!("file://{}", entry.display());
let script = format!(
r#"
globalThis.proxyExistingImport = {{}};
import({entry_spec:?})
.then(() => {{
globalThis.proxyExistingImport.done = true;
globalThis.proxyExistingImport.error = "";
}})
.catch((err) => {{
globalThis.proxyExistingImport.done = true;
globalThis.proxyExistingImport.error = String((err && err.message) || err || "");
}});
"#
);
runtime.eval(&script).await.expect("eval import");
let result = get_global_json(&runtime, "proxyExistingImport").await;
assert_eq!(result["done"], serde_json::json!(true));
assert_eq!(result["error"], serde_json::json!(""));
let probe = get_global_json(&runtime, "__existingVirtualProbe").await;
assert_eq!(probe, serde_json::json!("function"));
let events = runtime.drain_repair_events();
assert!(
!events
.iter()
.any(|event| event.pattern == RepairPattern::MissingNpmDep),
"existing virtual module should suppress missing_npm_dep repair events"
);
});
}
#[test]
fn pijs_dynamic_import_reports_deterministic_network_error() {
futures::executor::block_on(async {
let runtime = PiJsRuntime::with_clock(DeterministicClock::new(0))
.await
.expect("create runtime");
runtime
.eval(
r"
globalThis.networkImportError = {};
import('https://example.com/mod.js')
.then(() => {
globalThis.networkImportError.done = true;
globalThis.networkImportError.message = '';
})
.catch((err) => {
globalThis.networkImportError.done = true;
globalThis.networkImportError.message = String((err && err.message) || err || '');
});
",
)
.await
.expect("eval network import");
let result = get_global_json(&runtime, "networkImportError").await;
assert_eq!(result["done"], serde_json::json!(true));
let message = result["message"].as_str().unwrap_or_default();
assert!(
message.contains(
"Network module imports are not supported in PiJS: https://example.com/mod.js"
),
"unexpected message: {message}"
);
});
}
// Tests for the Promise bridge (bd-2ke)
#[test]
fn pijs_runtime_creates_hostcall_request() {
futures::executor::block_on(async {
let runtime = PiJsRuntime::with_clock(DeterministicClock::new(0))
.await
.expect("create runtime");
// Call pi.tool() which should enqueue a hostcall request
runtime
.eval(r#"pi.tool("read", { path: "test.txt" });"#)
.await
.expect("eval");
// Check that a hostcall request was enqueued
let requests = runtime.drain_hostcall_requests();
assert_eq!(requests.len(), 1);
let req = &requests[0];
assert!(matches!(&req.kind, HostcallKind::Tool { name } if name == "read"));
assert_eq!(req.payload["path"], "test.txt");
assert_eq!(req.extension_id.as_deref(), None);
});
}
#[test]
fn pijs_runtime_hostcall_request_captures_extension_id() {
futures::executor::block_on(async {
let runtime = PiJsRuntime::with_clock(DeterministicClock::new(0))
.await
.expect("create runtime");
runtime
.eval(
r#"
__pi_begin_extension("ext.test", { name: "Test" });
pi.tool("read", { path: "test.txt" });
__pi_end_extension();
"#,
)
.await
.expect("eval");
let requests = runtime.drain_hostcall_requests();
assert_eq!(requests.len(), 1);
assert_eq!(requests[0].extension_id.as_deref(), Some("ext.test"));
});
}
#[test]
fn pijs_runtime_log_hostcall_request_shape() {
futures::executor::block_on(async {
let runtime = PiJsRuntime::with_clock(DeterministicClock::new(0))
.await
.expect("create runtime");
runtime
.eval(
r#"
pi.log({
level: "info",
event: "unit.test",
message: "hello",
correlation: { scenario_id: "scn-1" }
});
"#,
)
.await
.expect("eval");
let requests = runtime.drain_hostcall_requests();
assert_eq!(requests.len(), 1);
let req = &requests[0];
assert!(matches!(&req.kind, HostcallKind::Log));
assert_eq!(req.payload["level"], "info");
assert_eq!(req.payload["event"], "unit.test");
assert_eq!(req.payload["message"], "hello");
});
}
#[test]
fn pijs_runtime_get_registered_tools_empty() {
futures::executor::block_on(async {
let runtime = PiJsRuntime::with_clock(DeterministicClock::new(0))
.await
.expect("create runtime");
let tools = runtime.get_registered_tools().await.expect("get tools");
assert!(tools.is_empty());
});
}
#[test]
fn pijs_runtime_get_registered_tools_single_tool() {
futures::executor::block_on(async {
let runtime = PiJsRuntime::with_clock(DeterministicClock::new(0))
.await
.expect("create runtime");
runtime
.eval(
r"
__pi_begin_extension('ext.test', { name: 'Test' });
pi.registerTool({
name: 'my_tool',
label: 'My Tool',
description: 'Does stuff',
parameters: { type: 'object', properties: { path: { type: 'string' } } },
execute: async (_callId, _input) => { return { ok: true }; },
});
__pi_end_extension();
",
)
.await
.expect("eval");
let tools = runtime.get_registered_tools().await.expect("get tools");
assert_eq!(tools.len(), 1);
assert_eq!(
tools[0],
ExtensionToolDef {
name: "my_tool".to_string(),
label: Some("My Tool".to_string()),
description: "Does stuff".to_string(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"path": { "type": "string" }
}
}),
}
);
});
}
#[test]
fn pijs_runtime_get_registered_tools_sorts_by_name() {
futures::executor::block_on(async {
let runtime = PiJsRuntime::with_clock(DeterministicClock::new(0))
.await
.expect("create runtime");
runtime
.eval(
r"
__pi_begin_extension('ext.test', { name: 'Test' });
pi.registerTool({ name: 'b', execute: async (_callId, _input) => { return {}; } });
pi.registerTool({ name: 'a', execute: async (_callId, _input) => { return {}; } });
__pi_end_extension();
",
)
.await
.expect("eval");
let tools = runtime.get_registered_tools().await.expect("get tools");
assert_eq!(
tools
.iter()
.map(|tool| tool.name.as_str())
.collect::<Vec<_>>(),
vec!["a", "b"]
);
});
}
#[test]
fn hostcall_params_hash_is_stable_for_key_ordering() {
let first = serde_json::json!({ "b": 2, "a": 1 });
let second = serde_json::json!({ "a": 1, "b": 2 });
assert_eq!(
hostcall_params_hash("http", &first),
hostcall_params_hash("http", &second)
);
assert_ne!(
hostcall_params_hash("http", &first),
hostcall_params_hash("tool", &first)
);
}
#[test]
#[allow(clippy::too_many_lines)]
fn hostcall_request_params_for_hash_uses_canonical_shapes() {
let cases = vec![
(
HostcallRequest {
call_id: "tool-case".to_string(),
kind: HostcallKind::Tool {
name: "read".to_string(),
},
payload: serde_json::json!({ "path": "README.md" }),
trace_id: 0,
extension_id: None,
},
serde_json::json!({ "name": "read", "input": { "path": "README.md" } }),
),
(
HostcallRequest {
call_id: "exec-case".to_string(),
kind: HostcallKind::Exec {
cmd: "echo".to_string(),
},
payload: serde_json::json!({
"command": "legacy alias should be dropped",
"args": ["hello"],
"options": { "timeout": 1000 }
}),
trace_id: 0,
extension_id: None,
},
serde_json::json!({
"cmd": "echo",
"args": ["hello"],
"options": { "timeout": 1000 }
}),
),
(
HostcallRequest {
call_id: "session-object".to_string(),
kind: HostcallKind::Session {
op: "set_model".to_string(),
},
payload: serde_json::json!({
"provider": "openai",
"modelId": "gpt-4o"
}),
trace_id: 0,
extension_id: None,
},
serde_json::json!({
"op": "set_model",
"provider": "openai",
"modelId": "gpt-4o"
}),
),
(
HostcallRequest {
call_id: "ui-non-object".to_string(),
kind: HostcallKind::Ui {
op: "set_status".to_string(),
},
payload: serde_json::json!("ready"),
trace_id: 0,
extension_id: None,
},
serde_json::json!({ "op": "set_status", "payload": "ready" }),
),
(
HostcallRequest {
call_id: "events-non-object".to_string(),
kind: HostcallKind::Events {
op: "emit".to_string(),
},
payload: serde_json::json!(42),
trace_id: 0,
extension_id: None,
},
serde_json::json!({ "op": "emit", "payload": 42 }),
),
(
HostcallRequest {
call_id: "session-null".to_string(),
kind: HostcallKind::Session {
op: "get_state".to_string(),
},
payload: serde_json::Value::Null,
trace_id: 0,
extension_id: None,
},
serde_json::json!({ "op": "get_state" }),
),
(
HostcallRequest {
call_id: "log-entry".to_string(),
kind: HostcallKind::Log,
payload: serde_json::json!({
"level": "info",
"event": "unit.test",
"message": "hello",
"correlation": { "scenario_id": "scn-1" }
}),
trace_id: 0,
extension_id: None,
},
serde_json::json!({
"level": "info",
"event": "unit.test",
"message": "hello",
"correlation": { "scenario_id": "scn-1" }
}),
),
];
for (request, expected) in cases {
assert_eq!(
request.params_for_hash(),
expected,
"canonical params mismatch for {}",
request.call_id
);
}
}
#[test]
fn hostcall_request_params_hash_matches_wasm_contract_for_canonical_requests() {
let requests = vec![
HostcallRequest {
call_id: "hash-session".to_string(),
kind: HostcallKind::Session {
op: "set_model".to_string(),
},
payload: serde_json::json!({
"modelId": "gpt-4o",
"provider": "openai"
}),
trace_id: 0,
extension_id: Some("ext.test".to_string()),
},
HostcallRequest {
call_id: "hash-ui".to_string(),
kind: HostcallKind::Ui {
op: "set_status".to_string(),
},
payload: serde_json::json!("thinking"),
trace_id: 0,
extension_id: Some("ext.test".to_string()),
},
HostcallRequest {
call_id: "hash-log".to_string(),
kind: HostcallKind::Log,
payload: serde_json::json!({
"level": "warn",
"event": "log.test",
"message": "warn line",
"correlation": { "scenario_id": "scn-2" }
}),
trace_id: 0,
extension_id: Some("ext.test".to_string()),
},
];
for request in requests {
let params = request.params_for_hash();
let js_hash = request.params_hash();
// Validate streaming hash matches the reference implementation.
let wasm_contract_hash =
crate::extensions::hostcall_params_hash(request.method(), ¶ms);
assert_eq!(
js_hash, wasm_contract_hash,
"hash parity mismatch for {}",
request.call_id
);
}
}
#[test]
fn hostcall_request_io_uring_capability_and_hint_mappings_are_deterministic() {
let cases = vec![
(
HostcallRequest {
call_id: "io-read".to_string(),
kind: HostcallKind::Tool {
name: "read".to_string(),
},
payload: serde_json::Value::Null,
trace_id: 0,
extension_id: None,
},
HostcallCapabilityClass::Filesystem,
HostcallIoHint::IoHeavy,
),
(
HostcallRequest {
call_id: "io-bash".to_string(),
kind: HostcallKind::Tool {
name: "bash".to_string(),
},
payload: serde_json::Value::Null,
trace_id: 0,
extension_id: None,
},
HostcallCapabilityClass::Execution,
HostcallIoHint::CpuBound,
),
(
HostcallRequest {
call_id: "io-http".to_string(),
kind: HostcallKind::Http,
payload: serde_json::Value::Null,
trace_id: 0,
extension_id: None,
},
HostcallCapabilityClass::Network,
HostcallIoHint::IoHeavy,
),
(
HostcallRequest {
call_id: "io-session".to_string(),
kind: HostcallKind::Session {
op: "get_state".to_string(),
},
payload: serde_json::Value::Null,
trace_id: 0,
extension_id: None,
},
HostcallCapabilityClass::Session,
HostcallIoHint::Unknown,
),
(
HostcallRequest {
call_id: "io-log".to_string(),
kind: HostcallKind::Log,
payload: serde_json::Value::Null,
trace_id: 0,
extension_id: None,
},
HostcallCapabilityClass::Telemetry,
HostcallIoHint::Unknown,
),
];
for (request, expected_capability, expected_hint) in cases {
assert_eq!(
request.io_uring_capability_class(),
expected_capability,
"capability mismatch for {}",
request.call_id
);
assert_eq!(
request.io_uring_io_hint(),
expected_hint,
"io hint mismatch for {}",
request.call_id
);
}
}
#[test]
fn hostcall_request_io_uring_lane_input_preserves_queue_and_force_flags() {
let request = HostcallRequest {
call_id: "io-lane-input".to_string(),
kind: HostcallKind::Tool {
name: "write".to_string(),
},
payload: serde_json::json!({ "path": "notes.txt", "content": "ok" }),
trace_id: 0,
extension_id: Some("ext.test".to_string()),
};
let input = request.io_uring_lane_input(17, true);
assert_eq!(input.capability, HostcallCapabilityClass::Filesystem);
assert_eq!(input.io_hint, HostcallIoHint::IoHeavy);
assert_eq!(input.queue_depth, 17);
assert!(input.force_compat_lane);
}
#[test]
fn pijs_runtime_multiple_hostcalls() {
futures::executor::block_on(async {
let runtime = PiJsRuntime::with_clock(DeterministicClock::new(0))
.await
.expect("create runtime");
runtime
.eval(
r#"
pi.tool("read", { path: "a.txt" });
pi.exec("ls", ["-la"]);
pi.http({ url: "https://example.com" });
"#,
)
.await
.expect("eval");
let requests = runtime.drain_hostcall_requests();
let kinds = requests
.iter()
.map(|req| format!("{:?}", req.kind))
.collect::<Vec<_>>();
assert_eq!(requests.len(), 3, "hostcalls: {kinds:?}");
assert!(matches!(&requests[0].kind, HostcallKind::Tool { name } if name == "read"));
assert!(matches!(&requests[1].kind, HostcallKind::Exec { cmd } if cmd == "ls"));
assert!(matches!(&requests[2].kind, HostcallKind::Http));
});
}
#[test]
fn pijs_runtime_hostcall_completion_resolves_promise() {
futures::executor::block_on(async {
let runtime = PiJsRuntime::with_clock(DeterministicClock::new(0))
.await
.expect("create runtime");
// Set up a promise handler that stores the result
runtime
.eval(
r#"
globalThis.result = null;
pi.tool("read", { path: "test.txt" }).then(r => {
globalThis.result = r;
});
"#,
)
.await
.expect("eval");
// Get the hostcall request
let requests = runtime.drain_hostcall_requests();
assert_eq!(requests.len(), 1);
let call_id = requests[0].call_id.clone();
// Complete the hostcall
runtime.complete_hostcall(
call_id,
HostcallOutcome::Success(serde_json::json!({ "content": "hello world" })),
);
// Tick to deliver the completion
let stats = runtime.tick().await.expect("tick");
assert!(stats.ran_macrotask);
// Verify the promise was resolved with the correct value
runtime
.eval(
r#"
if (globalThis.result === null) {
throw new Error("Promise not resolved");
}
if (globalThis.result.content !== "hello world") {
throw new Error("Wrong result: " + JSON.stringify(globalThis.result));
}
"#,
)
.await
.expect("verify result");
});
}
#[test]
fn pijs_runtime_hostcall_error_rejects_promise() {
futures::executor::block_on(async {
let runtime = PiJsRuntime::with_clock(DeterministicClock::new(0))
.await
.expect("create runtime");
// Set up a promise handler that captures rejection
runtime
.eval(
r#"
globalThis.error = null;
pi.tool("read", { path: "nonexistent.txt" }).catch(e => {
globalThis.error = { code: e.code, message: e.message };
});
"#,
)
.await
.expect("eval");
let requests = runtime.drain_hostcall_requests();
let call_id = requests[0].call_id.clone();
// Complete with an error
runtime.complete_hostcall(
call_id,
HostcallOutcome::Error {
code: "ENOENT".to_string(),
message: "File not found".to_string(),
},
);
runtime.tick().await.expect("tick");
// Verify the promise was rejected
runtime
.eval(
r#"
if (globalThis.error === null) {
throw new Error("Promise not rejected");
}
if (globalThis.error.code !== "ENOENT") {
throw new Error("Wrong error code: " + globalThis.error.code);
}
"#,
)
.await
.expect("verify error");
});
}
#[test]
fn pijs_runtime_tick_stats() {
futures::executor::block_on(async {
let runtime = PiJsRuntime::with_clock(DeterministicClock::new(0))
.await
.expect("create runtime");
// No pending tasks
let stats = runtime.tick().await.expect("tick");
assert!(!stats.ran_macrotask);
assert_eq!(stats.pending_hostcalls, 0);
// Create a hostcall
runtime.eval(r#"pi.tool("test", {});"#).await.expect("eval");
let requests = runtime.drain_hostcall_requests();
assert_eq!(requests.len(), 1);
// Complete it
runtime.complete_hostcall(
requests[0].call_id.clone(),
HostcallOutcome::Success(serde_json::json!(null)),
);
let stats = runtime.tick().await.expect("tick");
assert!(stats.ran_macrotask);
});
}
#[test]
fn pijs_hostcall_timeout_rejects_promise() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let mut config = PiJsRuntimeConfig::default();
config.limits.hostcall_timeout_ms = Some(50);
let runtime =
PiJsRuntime::with_clock_and_config_with_policy(Arc::clone(&clock), config, None)
.await
.expect("create runtime");
runtime
.eval(
r#"
globalThis.done = false;
globalThis.code = null;
pi.tool("read", { path: "test.txt" })
.then(() => { globalThis.done = true; })
.catch((e) => { globalThis.code = e.code; globalThis.done = true; });
"#,
)
.await
.expect("eval");
let requests = runtime.drain_hostcall_requests();
assert_eq!(requests.len(), 1);
clock.set(50);
let stats = runtime.tick().await.expect("tick");
assert!(stats.ran_macrotask);
assert_eq!(stats.hostcalls_timed_out, 1);
assert_eq!(
get_global_json(&runtime, "done").await,
serde_json::json!(true)
);
assert_eq!(
get_global_json(&runtime, "code").await,
serde_json::json!("timeout")
);
// Late completions should be ignored.
runtime.complete_hostcall(
requests[0].call_id.clone(),
HostcallOutcome::Success(serde_json::json!({ "ok": true })),
);
let stats = runtime.tick().await.expect("tick late completion");
assert!(stats.ran_macrotask);
assert_eq!(stats.hostcalls_timed_out, 1);
});
}
#[test]
fn pijs_interrupt_budget_aborts_eval() {
futures::executor::block_on(async {
let mut config = PiJsRuntimeConfig::default();
config.limits.interrupt_budget = Some(0);
let runtime = PiJsRuntime::with_clock_and_config_with_policy(
DeterministicClock::new(0),
config,
None,
)
.await
.expect("create runtime");
let err = runtime
.eval(
r"
let sum = 0;
for (let i = 0; i < 1000000; i++) { sum += i; }
",
)
.await
.expect_err("expected budget exceed");
assert!(err.to_string().contains("PiJS execution budget exceeded"));
});
}
#[test]
fn pijs_microtasks_drain_before_next_macrotask() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = PiJsRuntime::with_clock(Arc::clone(&clock))
.await
.expect("create runtime");
runtime
.eval(r"globalThis.order = []; globalThis.__pi_done = false;")
.await
.expect("init order");
let timer_id = runtime.set_timeout(10);
runtime
.eval(&format!(
r#"__pi_register_timer({timer_id}, () => {{
globalThis.order.push("timer");
Promise.resolve().then(() => globalThis.order.push("timer-micro"));
}});"#
))
.await
.expect("register timer");
runtime
.eval(
r#"
pi.tool("read", {}).then(() => {
globalThis.order.push("hostcall");
Promise.resolve().then(() => globalThis.order.push("hostcall-micro"));
});
"#,
)
.await
.expect("enqueue hostcall");
let requests = runtime.drain_hostcall_requests();
let call_id = requests
.into_iter()
.next()
.expect("hostcall request")
.call_id;
runtime.complete_hostcall(call_id, HostcallOutcome::Success(serde_json::json!(null)));
// Make the timer due as well.
clock.set(10);
// Tick 1: hostcall completion runs first, and its microtasks drain immediately.
runtime.tick().await.expect("tick hostcall");
let after_first = get_global_json(&runtime, "order").await;
assert_eq!(
after_first,
serde_json::json!(["hostcall", "hostcall-micro"])
);
// Tick 2: timer runs, and its microtasks drain before the next macrotask.
runtime.tick().await.expect("tick timer");
let after_second = get_global_json(&runtime, "order").await;
assert_eq!(
after_second,
serde_json::json!(["hostcall", "hostcall-micro", "timer", "timer-micro"])
);
});
}
#[test]
fn pijs_clear_timeout_prevents_timer_callback() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = PiJsRuntime::with_clock(Arc::clone(&clock))
.await
.expect("create runtime");
runtime
.eval(r"globalThis.order = []; ")
.await
.expect("init order");
let timer_id = runtime.set_timeout(10);
runtime
.eval(&format!(
r#"__pi_register_timer({timer_id}, () => globalThis.order.push("timer"));"#
))
.await
.expect("register timer");
assert!(runtime.clear_timeout(timer_id));
clock.set(10);
let stats = runtime.tick().await.expect("tick");
assert!(!stats.ran_macrotask);
let order = get_global_json(&runtime, "order").await;
assert_eq!(order, serde_json::json!([]));
});
}
#[test]
fn pijs_env_get_honors_allowlist() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let mut env = HashMap::new();
env.insert("HOME".to_string(), "/virtual/home".to_string());
env.insert("PI_IMAGE_SAVE_MODE".to_string(), "tmp".to_string());
env.insert(
"AWS_SECRET_ACCESS_KEY".to_string(),
"nope-do-not-expose".to_string(),
);
let config = PiJsRuntimeConfig {
cwd: "/virtual/cwd".to_string(),
args: vec!["--flag".to_string()],
env,
limits: PiJsRuntimeLimits::default(),
repair_mode: RepairMode::default(),
allow_unsafe_sync_exec: false,
deny_env: false,
disk_cache_dir: None,
};
let runtime =
PiJsRuntime::with_clock_and_config_with_policy(Arc::clone(&clock), config, None)
.await
.expect("create runtime");
runtime
.eval(
r#"
globalThis.home = pi.env.get("HOME");
globalThis.mode = pi.env.get("PI_IMAGE_SAVE_MODE");
globalThis.missing_is_undefined = (pi.env.get("NOPE") === undefined);
globalThis.secret_is_undefined = (pi.env.get("AWS_SECRET_ACCESS_KEY") === undefined);
globalThis.process_secret_is_undefined = (process.env.AWS_SECRET_ACCESS_KEY === undefined);
globalThis.secret_in_env = ("AWS_SECRET_ACCESS_KEY" in process.env);
"#,
)
.await
.expect("eval env");
assert_eq!(
get_global_json(&runtime, "home").await,
serde_json::json!("/virtual/home")
);
assert_eq!(
get_global_json(&runtime, "mode").await,
serde_json::json!("tmp")
);
assert_eq!(
get_global_json(&runtime, "missing_is_undefined").await,
serde_json::json!(true)
);
assert_eq!(
get_global_json(&runtime, "secret_is_undefined").await,
serde_json::json!(true)
);
assert_eq!(
get_global_json(&runtime, "process_secret_is_undefined").await,
serde_json::json!(true)
);
assert_eq!(
get_global_json(&runtime, "secret_in_env").await,
serde_json::json!(false)
);
});
}
#[test]
fn pijs_process_path_crypto_time_apis_smoke() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(123));
let config = PiJsRuntimeConfig {
cwd: "/virtual/cwd".to_string(),
args: vec!["a".to_string(), "b".to_string()],
env: HashMap::new(),
limits: PiJsRuntimeLimits::default(),
repair_mode: RepairMode::default(),
allow_unsafe_sync_exec: false,
deny_env: false,
disk_cache_dir: None,
};
let runtime =
PiJsRuntime::with_clock_and_config_with_policy(Arc::clone(&clock), config, None)
.await
.expect("create runtime");
runtime
.eval(
r#"
globalThis.cwd = pi.process.cwd;
globalThis.args = pi.process.args;
globalThis.pi_process_is_frozen = Object.isFrozen(pi.process);
globalThis.pi_args_is_frozen = Object.isFrozen(pi.process.args);
try { pi.process.cwd = "/hacked"; } catch (_) {}
try { pi.process.args.push("c"); } catch (_) {}
globalThis.cwd_after_mut = pi.process.cwd;
globalThis.args_after_mut = pi.process.args;
globalThis.joined = pi.path.join("/a", "b", "..", "c");
globalThis.base = pi.path.basename("/a/b/c.txt");
globalThis.norm = pi.path.normalize("/a/./b//../c/");
globalThis.hash = pi.crypto.sha256Hex("abc");
globalThis.bytes = pi.crypto.randomBytes(32);
globalThis.now = pi.time.nowMs();
globalThis.done = false;
pi.time.sleep(10).then(() => { globalThis.done = true; });
"#,
)
.await
.expect("eval apis");
for (key, expected) in [
("cwd", serde_json::json!("/virtual/cwd")),
("args", serde_json::json!(["a", "b"])),
("pi_process_is_frozen", serde_json::json!(true)),
("pi_args_is_frozen", serde_json::json!(true)),
("cwd_after_mut", serde_json::json!("/virtual/cwd")),
("args_after_mut", serde_json::json!(["a", "b"])),
("joined", serde_json::json!("/a/c")),
("base", serde_json::json!("c.txt")),
("norm", serde_json::json!("/a/c")),
(
"hash",
serde_json::json!(
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
),
),
] {
assert_eq!(get_global_json(&runtime, key).await, expected);
}
let bytes = get_global_json(&runtime, "bytes").await;
let bytes_arr = bytes.as_array().expect("bytes array");
assert_eq!(bytes_arr.len(), 32);
assert!(
bytes_arr
.iter()
.all(|value| value.as_u64().is_some_and(|n| n <= 255)),
"bytes must be numbers in 0..=255: {bytes}"
);
assert_eq!(
get_global_json(&runtime, "now").await,
serde_json::json!(123)
);
assert_eq!(
get_global_json(&runtime, "done").await,
serde_json::json!(false)
);
clock.set(133);
runtime.tick().await.expect("tick sleep");
assert_eq!(
get_global_json(&runtime, "done").await,
serde_json::json!(true)
);
});
}
#[test]
fn pijs_inbound_event_fifo_and_microtask_fixpoint() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = PiJsRuntime::with_clock(Arc::clone(&clock))
.await
.expect("create runtime");
runtime
.eval(
r#"
globalThis.order = [];
__pi_add_event_listener("evt", (payload) => {
globalThis.order.push(payload.n);
Promise.resolve().then(() => globalThis.order.push(payload.n + 1000));
});
"#,
)
.await
.expect("install listener");
runtime.enqueue_event("evt", serde_json::json!({ "n": 1 }));
runtime.enqueue_event("evt", serde_json::json!({ "n": 2 }));
runtime.tick().await.expect("tick 1");
let after_first = get_global_json(&runtime, "order").await;
assert_eq!(after_first, serde_json::json!([1, 1001]));
runtime.tick().await.expect("tick 2");
let after_second = get_global_json(&runtime, "order").await;
assert_eq!(after_second, serde_json::json!([1, 1001, 2, 1002]));
});
}
#[derive(Debug, Clone)]
struct XorShift64 {
state: u64,
}
impl XorShift64 {
const fn new(seed: u64) -> Self {
let seed = seed ^ 0x9E37_79B9_7F4A_7C15;
Self { state: seed }
}
fn next_u64(&mut self) -> u64 {
let mut x = self.state;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.state = x;
x
}
fn next_range_u64(&mut self, upper_exclusive: u64) -> u64 {
if upper_exclusive == 0 {
return 0;
}
self.next_u64() % upper_exclusive
}
fn next_usize(&mut self, upper_exclusive: usize) -> usize {
let upper = u64::try_from(upper_exclusive).expect("usize fits u64");
let value = self.next_range_u64(upper);
usize::try_from(value).expect("value < upper_exclusive")
}
}
#[allow(clippy::future_not_send)]
async fn run_seeded_runtime_trace(seed: u64) -> serde_json::Value {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = PiJsRuntime::with_clock(Arc::clone(&clock))
.await
.expect("create runtime");
runtime
.eval(
r#"
globalThis.order = [];
__pi_add_event_listener("evt", (payload) => {
globalThis.order.push("event:" + payload.step);
Promise.resolve().then(() => globalThis.order.push("event-micro:" + payload.step));
});
"#,
)
.await
.expect("init");
let mut rng = XorShift64::new(seed);
let mut timers = Vec::new();
for step in 0..64u64 {
match rng.next_range_u64(6) {
0 => {
runtime
.eval(&format!(
r#"
pi.tool("test", {{ step: {step} }}).then(() => {{
globalThis.order.push("hostcall:{step}");
Promise.resolve().then(() => globalThis.order.push("hostcall-micro:{step}"));
}});
"#
))
.await
.expect("enqueue hostcall");
for request in runtime.drain_hostcall_requests() {
runtime.complete_hostcall(
request.call_id,
HostcallOutcome::Success(serde_json::json!({ "step": step })),
);
}
}
1 => {
let delay_ms = rng.next_range_u64(25);
let timer_id = runtime.set_timeout(delay_ms);
timers.push(timer_id);
runtime
.eval(&format!(
r#"__pi_register_timer({timer_id}, () => {{
globalThis.order.push("timer:{step}");
Promise.resolve().then(() => globalThis.order.push("timer-micro:{step}"));
}});"#
))
.await
.expect("register timer");
}
2 => {
runtime.enqueue_event("evt", serde_json::json!({ "step": step }));
}
3 => {
if !timers.is_empty() {
let idx = rng.next_usize(timers.len());
let _ = runtime.clear_timeout(timers[idx]);
}
}
4 => {
let delta_ms = rng.next_range_u64(50);
clock.advance(delta_ms);
}
_ => {}
}
// Drive the loop a bit.
for _ in 0..3 {
if !runtime.has_pending() {
break;
}
let _ = runtime.tick().await.expect("tick");
}
}
drain_until_idle(&runtime, &clock).await;
get_global_json(&runtime, "order").await
}
#[test]
fn pijs_seeded_trace_is_deterministic() {
futures::executor::block_on(async {
let a = run_seeded_runtime_trace(0x00C0_FFEE).await;
let b = run_seeded_runtime_trace(0x00C0_FFEE).await;
assert_eq!(a, b);
});
}
#[test]
fn pijs_events_on_returns_unsubscribe_and_removes_handler() {
futures::executor::block_on(async {
let runtime = PiJsRuntime::with_clock(DeterministicClock::new(0))
.await
.expect("create runtime");
runtime
.eval(
r#"
globalThis.seen = [];
globalThis.done = false;
__pi_begin_extension("ext.b", { name: "ext.b" });
const off = pi.events.on("custom_event", (payload, _ctx) => { globalThis.seen.push(payload); });
if (typeof off !== "function") throw new Error("expected unsubscribe function");
__pi_end_extension();
(async () => {
await __pi_dispatch_extension_event("custom_event", { n: 1 }, {});
off();
await __pi_dispatch_extension_event("custom_event", { n: 2 }, {});
globalThis.done = true;
})();
"#,
)
.await
.expect("eval");
assert_eq!(
get_global_json(&runtime, "done").await,
serde_json::Value::Bool(true)
);
assert_eq!(
get_global_json(&runtime, "seen").await,
serde_json::json!([{ "n": 1 }])
);
});
}
#[test]
fn pijs_event_dispatch_continues_after_handler_error() {
futures::executor::block_on(async {
let runtime = PiJsRuntime::with_clock(DeterministicClock::new(0))
.await
.expect("create runtime");
runtime
.eval(
r#"
globalThis.seen = [];
globalThis.done = false;
__pi_begin_extension("ext.err", { name: "ext.err" });
pi.events.on("custom_event", (_payload, _ctx) => { throw new Error("boom"); });
__pi_end_extension();
__pi_begin_extension("ext.ok", { name: "ext.ok" });
pi.events.on("custom_event", (payload, _ctx) => { globalThis.seen.push(payload); });
__pi_end_extension();
(async () => {
await __pi_dispatch_extension_event("custom_event", { hello: "world" }, {});
globalThis.done = true;
})();
"#,
)
.await
.expect("eval");
assert_eq!(
get_global_json(&runtime, "done").await,
serde_json::Value::Bool(true)
);
assert_eq!(
get_global_json(&runtime, "seen").await,
serde_json::json!([{ "hello": "world" }])
);
});
}
// ---- Extension crash recovery and isolation tests (bd-m4wc) ----
#[test]
fn pijs_crash_register_throw_host_continues() {
futures::executor::block_on(async {
let runtime = PiJsRuntime::with_clock(DeterministicClock::new(0))
.await
.expect("create runtime");
// Extension that throws during registration
runtime
.eval(
r#"
globalThis.postCrashResult = null;
__pi_begin_extension("ext.crash", { name: "ext.crash" });
// Simulate a throw during registration by registering a handler then
// throwing - the handler should still be partially registered
throw new Error("registration boom");
"#,
)
.await
.ok(); // May fail, that's fine
// End the crashed extension context
runtime.eval(r"__pi_end_extension();").await.ok();
// Host can still load another extension after the crash
runtime
.eval(
r#"
__pi_begin_extension("ext.ok", { name: "ext.ok" });
pi.events.on("test_event", (p, _) => { globalThis.postCrashResult = p; });
__pi_end_extension();
"#,
)
.await
.expect("second extension should load");
// Dispatch event - only the healthy extension should handle it
runtime
.eval(
r#"
(async () => {
await __pi_dispatch_extension_event("test_event", { ok: true }, {});
})();
"#,
)
.await
.expect("dispatch");
assert_eq!(
get_global_json(&runtime, "postCrashResult").await,
serde_json::json!({ "ok": true })
);
});
}
#[test]
fn pijs_crash_handler_throw_other_handlers_run() {
futures::executor::block_on(async {
let runtime = PiJsRuntime::with_clock(DeterministicClock::new(0))
.await
.expect("create runtime");
runtime
.eval(
r#"
globalThis.handlerResults = [];
globalThis.dispatchDone = false;
// Extension A: will throw
__pi_begin_extension("ext.a", { name: "ext.a" });
pi.events.on("multi_test", (_p, _c) => {
globalThis.handlerResults.push("a-before-throw");
throw new Error("handler crash");
});
__pi_end_extension();
// Extension B: should still run
__pi_begin_extension("ext.b", { name: "ext.b" });
pi.events.on("multi_test", (_p, _c) => {
globalThis.handlerResults.push("b-ok");
});
__pi_end_extension();
// Extension C: should also still run
__pi_begin_extension("ext.c", { name: "ext.c" });
pi.events.on("multi_test", (_p, _c) => {
globalThis.handlerResults.push("c-ok");
});
__pi_end_extension();
(async () => {
await __pi_dispatch_extension_event("multi_test", {}, {});
globalThis.dispatchDone = true;
})();
"#,
)
.await
.expect("eval");
assert_eq!(
get_global_json(&runtime, "dispatchDone").await,
serde_json::Value::Bool(true)
);
let results = get_global_json(&runtime, "handlerResults").await;
let arr = results.as_array().expect("should be array");
// Handler A ran (at least the part before throw)
assert!(
arr.iter().any(|v| v == "a-before-throw"),
"Handler A should have run before throwing"
);
// Handlers B and C should have run despite A's crash
assert!(
arr.iter().any(|v| v == "b-ok"),
"Handler B should run after A crashes"
);
assert!(
arr.iter().any(|v| v == "c-ok"),
"Handler C should run after A crashes"
);
});
}
#[test]
fn pijs_crash_invalid_hostcall_returns_error_not_panic() {
futures::executor::block_on(async {
let runtime = PiJsRuntime::with_clock(DeterministicClock::new(0))
.await
.expect("create runtime");
// Extension makes an invalid hostcall (unknown tool)
runtime
.eval(
r#"
globalThis.invalidResult = null;
globalThis.errCode = null;
__pi_begin_extension("ext.bad", { name: "ext.bad" });
pi.tool("completely_nonexistent_tool_xyz", { junk: true })
.then((r) => { globalThis.invalidResult = r; })
.catch((e) => { globalThis.errCode = e.code || "unknown"; });
__pi_end_extension();
"#,
)
.await
.expect("eval");
// The hostcall should be queued but not crash the runtime
let requests = runtime.drain_hostcall_requests();
assert_eq!(requests.len(), 1, "Hostcall should be queued");
// Host can still evaluate JS after the invalid hostcall
runtime
.eval(
r"
globalThis.hostStillAlive = true;
",
)
.await
.expect("host should still work");
assert_eq!(
get_global_json(&runtime, "hostStillAlive").await,
serde_json::Value::Bool(true)
);
});
}
#[test]
fn pijs_crash_after_crash_new_extensions_load() {
futures::executor::block_on(async {
let runtime = PiJsRuntime::with_clock(DeterministicClock::new(0))
.await
.expect("create runtime");
// Simulate a crash sequence: extension throws, then new ones load fine
runtime
.eval(
r#"
globalThis.loadOrder = [];
// Extension 1: loads fine
__pi_begin_extension("ext.1", { name: "ext.1" });
globalThis.loadOrder.push("1-loaded");
__pi_end_extension();
"#,
)
.await
.expect("ext 1");
// Extension 2: crashes during eval
runtime
.eval(
r#"
__pi_begin_extension("ext.2", { name: "ext.2" });
globalThis.loadOrder.push("2-before-crash");
throw new Error("ext 2 crash");
"#,
)
.await
.ok(); // Expected to fail
runtime.eval(r"__pi_end_extension();").await.ok();
// Extension 3: should still load after ext 2's crash
runtime
.eval(
r#"
__pi_begin_extension("ext.3", { name: "ext.3" });
globalThis.loadOrder.push("3-loaded");
__pi_end_extension();
"#,
)
.await
.expect("ext 3 should load after crash");
// Extension 4: loads fine too
runtime
.eval(
r#"
__pi_begin_extension("ext.4", { name: "ext.4" });
globalThis.loadOrder.push("4-loaded");
__pi_end_extension();
"#,
)
.await
.expect("ext 4 should load");
let order = get_global_json(&runtime, "loadOrder").await;
let arr = order.as_array().expect("should be array");
assert!(
arr.iter().any(|v| v == "1-loaded"),
"Extension 1 should have loaded"
);
assert!(
arr.iter().any(|v| v == "3-loaded"),
"Extension 3 should load after crash"
);
assert!(
arr.iter().any(|v| v == "4-loaded"),
"Extension 4 should load after crash"
);
});
}
#[test]
fn pijs_crash_no_cross_contamination_between_extensions() {
futures::executor::block_on(async {
let runtime = PiJsRuntime::with_clock(DeterministicClock::new(0))
.await
.expect("create runtime");
runtime
.eval(
r#"
globalThis.extAData = null;
globalThis.extBData = null;
globalThis.eventsDone = false;
// Extension A: sets its own state
__pi_begin_extension("ext.isolated.a", { name: "ext.isolated.a" });
pi.events.on("isolation_test", (_p, _c) => {
globalThis.extAData = "from-A";
});
__pi_end_extension();
// Extension B: sets its own state independently
__pi_begin_extension("ext.isolated.b", { name: "ext.isolated.b" });
pi.events.on("isolation_test", (_p, _c) => {
globalThis.extBData = "from-B";
});
__pi_end_extension();
(async () => {
await __pi_dispatch_extension_event("isolation_test", {}, {});
globalThis.eventsDone = true;
})();
"#,
)
.await
.expect("eval");
assert_eq!(
get_global_json(&runtime, "eventsDone").await,
serde_json::Value::Bool(true)
);
// Each extension should have set its own global independently
assert_eq!(
get_global_json(&runtime, "extAData").await,
serde_json::json!("from-A")
);
assert_eq!(
get_global_json(&runtime, "extBData").await,
serde_json::json!("from-B")
);
});
}
#[test]
fn pijs_crash_interrupt_budget_stops_infinite_loop() {
futures::executor::block_on(async {
let config = PiJsRuntimeConfig {
limits: PiJsRuntimeLimits {
// Use a small interrupt budget to catch infinite loops quickly
interrupt_budget: Some(1000),
..Default::default()
},
..Default::default()
};
let runtime = PiJsRuntime::with_clock_and_config_with_policy(
DeterministicClock::new(0),
config,
None,
)
.await
.expect("create runtime");
// Try to run an infinite loop - should be interrupted by budget
let result = runtime
.eval(
r"
let i = 0;
while (true) { i++; }
globalThis.loopResult = i;
",
)
.await;
// The eval should fail due to interrupt
assert!(
result.is_err(),
"Infinite loop should be interrupted by budget"
);
// Host should still be alive after interrupt
let alive_result = runtime.eval(r#"globalThis.postInterrupt = "alive";"#).await;
// After an interrupt, the runtime may or may not accept new evals
// The key assertion is that we didn't hang
if alive_result.is_ok() {
assert_eq!(
get_global_json(&runtime, "postInterrupt").await,
serde_json::json!("alive")
);
}
});
}
#[test]
fn pijs_events_emit_queues_events_hostcall() {
futures::executor::block_on(async {
let runtime = PiJsRuntime::with_clock(DeterministicClock::new(0))
.await
.expect("create runtime");
runtime
.eval(
r#"
__pi_begin_extension("ext.test", { name: "Test" });
pi.events.emit("custom_event", { a: 1 });
__pi_end_extension();
"#,
)
.await
.expect("eval");
let requests = runtime.drain_hostcall_requests();
assert_eq!(requests.len(), 1);
let req = &requests[0];
assert_eq!(req.extension_id.as_deref(), Some("ext.test"));
assert!(
matches!(&req.kind, HostcallKind::Events { op } if op == "emit"),
"unexpected hostcall kind: {:?}",
req.kind
);
assert_eq!(
req.payload,
serde_json::json!({ "event": "custom_event", "data": { "a": 1 } })
);
});
}
#[test]
fn pijs_console_global_is_defined_and_callable() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = PiJsRuntime::with_clock(Arc::clone(&clock))
.await
.expect("create runtime");
// Verify console global exists and all standard methods are functions
runtime
.eval(
r"
globalThis.console_exists = typeof globalThis.console === 'object';
globalThis.has_log = typeof console.log === 'function';
globalThis.has_warn = typeof console.warn === 'function';
globalThis.has_error = typeof console.error === 'function';
globalThis.has_info = typeof console.info === 'function';
globalThis.has_debug = typeof console.debug === 'function';
globalThis.has_trace = typeof console.trace === 'function';
globalThis.has_dir = typeof console.dir === 'function';
globalThis.has_assert = typeof console.assert === 'function';
globalThis.has_table = typeof console.table === 'function';
// Call each method to ensure they don't throw
console.log('test log', 42, { key: 'value' });
console.warn('test warn');
console.error('test error');
console.info('test info');
console.debug('test debug');
console.trace('test trace');
console.dir({ a: 1 });
console.assert(true, 'should not appear');
console.assert(false, 'assertion failed message');
console.table([1, 2, 3]);
console.time();
console.timeEnd();
console.group();
console.groupEnd();
console.clear();
globalThis.calls_succeeded = true;
",
)
.await
.expect("eval console tests");
assert_eq!(
get_global_json(&runtime, "console_exists").await,
serde_json::json!(true)
);
assert_eq!(
get_global_json(&runtime, "has_log").await,
serde_json::json!(true)
);
assert_eq!(
get_global_json(&runtime, "has_warn").await,
serde_json::json!(true)
);
assert_eq!(
get_global_json(&runtime, "has_error").await,
serde_json::json!(true)
);
assert_eq!(
get_global_json(&runtime, "has_info").await,
serde_json::json!(true)
);
assert_eq!(
get_global_json(&runtime, "has_debug").await,
serde_json::json!(true)
);
assert_eq!(
get_global_json(&runtime, "has_trace").await,
serde_json::json!(true)
);
assert_eq!(
get_global_json(&runtime, "has_dir").await,
serde_json::json!(true)
);
assert_eq!(
get_global_json(&runtime, "has_assert").await,
serde_json::json!(true)
);
assert_eq!(
get_global_json(&runtime, "has_table").await,
serde_json::json!(true)
);
assert_eq!(
get_global_json(&runtime, "calls_succeeded").await,
serde_json::json!(true)
);
});
}
#[test]
fn pijs_node_events_module_provides_event_emitter() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = PiJsRuntime::with_clock(Arc::clone(&clock))
.await
.expect("create runtime");
// Use dynamic import() since eval() runs as a script, not a module
runtime
.eval(
r"
globalThis.results = [];
globalThis.testDone = false;
import('node:events').then(({ EventEmitter }) => {
const emitter = new EventEmitter();
emitter.on('data', (val) => globalThis.results.push('data:' + val));
emitter.once('done', () => globalThis.results.push('done'));
emitter.emit('data', 1);
emitter.emit('data', 2);
emitter.emit('done');
emitter.emit('done'); // should not fire again
globalThis.listenerCount = emitter.listenerCount('data');
globalThis.eventNames = emitter.eventNames();
globalThis.testDone = true;
});
",
)
.await
.expect("eval EventEmitter test");
assert_eq!(
get_global_json(&runtime, "testDone").await,
serde_json::json!(true)
);
assert_eq!(
get_global_json(&runtime, "results").await,
serde_json::json!(["data:1", "data:2", "done"])
);
assert_eq!(
get_global_json(&runtime, "listenerCount").await,
serde_json::json!(1)
);
assert_eq!(
get_global_json(&runtime, "eventNames").await,
serde_json::json!(["data"])
);
});
}
#[test]
fn pijs_bare_module_aliases_resolve_correctly() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = PiJsRuntime::with_clock(Arc::clone(&clock))
.await
.expect("create runtime");
// Test that bare "events" alias resolves to "node:events"
runtime
.eval(
r"
globalThis.bare_events_ok = false;
import('events').then((mod) => {
const e = new mod.default();
globalThis.bare_events_ok = typeof e.on === 'function';
});
",
)
.await
.expect("eval bare events import");
assert_eq!(
get_global_json(&runtime, "bare_events_ok").await,
serde_json::json!(true)
);
});
}
#[test]
fn pijs_path_extended_functions() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = PiJsRuntime::with_clock(Arc::clone(&clock))
.await
.expect("create runtime");
runtime
.eval(
r"
globalThis.pathResults = {};
import('node:path').then((path) => {
globalThis.pathResults.isAbsRoot = path.isAbsolute('/foo/bar');
globalThis.pathResults.isAbsRel = path.isAbsolute('foo/bar');
globalThis.pathResults.extJs = path.extname('/a/b/file.js');
globalThis.pathResults.extNone = path.extname('/a/b/noext');
globalThis.pathResults.extDot = path.extname('.hidden');
globalThis.pathResults.norm = path.normalize('/a/b/../c/./d');
globalThis.pathResults.parseBase = path.parse('/home/user/file.txt').base;
globalThis.pathResults.parseExt = path.parse('/home/user/file.txt').ext;
globalThis.pathResults.parseName = path.parse('/home/user/file.txt').name;
globalThis.pathResults.parseDir = path.parse('/home/user/file.txt').dir;
globalThis.pathResults.hasPosix = typeof path.posix === 'object';
globalThis.pathResults.done = true;
});
",
)
.await
.expect("eval path extended");
let r = get_global_json(&runtime, "pathResults").await;
assert_eq!(r["done"], serde_json::json!(true));
assert_eq!(r["isAbsRoot"], serde_json::json!(true));
assert_eq!(r["isAbsRel"], serde_json::json!(false));
assert_eq!(r["extJs"], serde_json::json!(".js"));
assert_eq!(r["extNone"], serde_json::json!(""));
assert_eq!(r["extDot"], serde_json::json!(""));
assert_eq!(r["norm"], serde_json::json!("/a/c/d"));
assert_eq!(r["parseBase"], serde_json::json!("file.txt"));
assert_eq!(r["parseExt"], serde_json::json!(".txt"));
assert_eq!(r["parseName"], serde_json::json!("file"));
assert_eq!(r["parseDir"], serde_json::json!("/home/user"));
assert_eq!(r["hasPosix"], serde_json::json!(true));
});
}
#[test]
fn pijs_fs_callback_apis() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = PiJsRuntime::with_clock(Arc::clone(&clock))
.await
.expect("create runtime");
runtime
.eval(
r"
globalThis.fsResults = {};
import('node:fs').then((fs) => {
fs.writeFileSync('/fake', '');
// readFile callback
fs.readFile('/fake', 'utf8', (err, data) => {
globalThis.fsResults.readFileCallbackCalled = true;
globalThis.fsResults.readFileData = data;
});
// writeFile callback
fs.writeFile('/fake', 'data', (err) => {
globalThis.fsResults.writeFileCallbackCalled = true;
});
// accessSync throws
try {
fs.accessSync('/nonexistent');
globalThis.fsResults.accessSyncThrew = false;
} catch (e) {
globalThis.fsResults.accessSyncThrew = true;
}
// access callback with error
fs.access('/nonexistent', (err) => {
globalThis.fsResults.accessCallbackErr = !!err;
});
globalThis.fsResults.hasLstatSync = typeof fs.lstatSync === 'function';
globalThis.fsResults.done = true;
});
",
)
.await
.expect("eval fs callbacks");
let r = get_global_json(&runtime, "fsResults").await;
assert_eq!(r["done"], serde_json::json!(true));
assert_eq!(r["readFileCallbackCalled"], serde_json::json!(true));
assert_eq!(r["readFileData"], serde_json::json!(""));
assert_eq!(r["writeFileCallbackCalled"], serde_json::json!(true));
assert_eq!(r["accessSyncThrew"], serde_json::json!(true));
assert_eq!(r["accessCallbackErr"], serde_json::json!(true));
assert_eq!(r["hasLstatSync"], serde_json::json!(true));
});
}
#[test]
fn pijs_fs_sync_roundtrip_and_dirents() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = PiJsRuntime::with_clock(Arc::clone(&clock))
.await
.expect("create runtime");
runtime
.eval(
r"
globalThis.fsRoundTrip = {};
import('node:fs').then((fs) => {
fs.mkdirSync('/tmp/demo', { recursive: true });
fs.writeFileSync('/tmp/demo/hello.txt', 'hello world');
fs.writeFileSync('/tmp/demo/raw.bin', Buffer.from([1, 2, 3, 4]));
globalThis.fsRoundTrip.exists = fs.existsSync('/tmp/demo/hello.txt');
globalThis.fsRoundTrip.readText = fs.readFileSync('/tmp/demo/hello.txt', 'utf8');
const raw = fs.readFileSync('/tmp/demo/raw.bin');
globalThis.fsRoundTrip.rawLen = raw.length;
const names = fs.readdirSync('/tmp/demo');
globalThis.fsRoundTrip.names = names;
const dirents = fs.readdirSync('/tmp/demo', { withFileTypes: true });
globalThis.fsRoundTrip.direntHasMethods =
typeof dirents[0].isFile === 'function' &&
typeof dirents[0].isDirectory === 'function';
const dirStat = fs.statSync('/tmp/demo');
const fileStat = fs.statSync('/tmp/demo/hello.txt');
globalThis.fsRoundTrip.isDir = dirStat.isDirectory();
globalThis.fsRoundTrip.isFile = fileStat.isFile();
globalThis.fsRoundTrip.done = true;
});
",
)
.await
.expect("eval fs sync roundtrip");
let r = get_global_json(&runtime, "fsRoundTrip").await;
assert_eq!(r["done"], serde_json::json!(true));
assert_eq!(r["exists"], serde_json::json!(true));
assert_eq!(r["readText"], serde_json::json!("hello world"));
assert_eq!(r["rawLen"], serde_json::json!(4));
assert_eq!(r["isDir"], serde_json::json!(true));
assert_eq!(r["isFile"], serde_json::json!(true));
assert_eq!(r["direntHasMethods"], serde_json::json!(true));
assert_eq!(r["names"], serde_json::json!(["hello.txt", "raw.bin"]));
});
}
#[test]
fn pijs_create_require_supports_node_builtins() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = PiJsRuntime::with_clock(Arc::clone(&clock))
.await
.expect("create runtime");
runtime
.eval(
r"
globalThis.requireResults = {};
import('node:module').then(({ createRequire }) => {
const require = createRequire('/tmp/example.js');
const path = require('path');
const fs = require('node:fs');
const crypto = require('crypto');
const http2 = require('http2');
globalThis.requireResults.pathJoinWorks = path.join('a', 'b') === 'a/b';
globalThis.requireResults.fsReadFileSync = typeof fs.readFileSync === 'function';
globalThis.requireResults.cryptoHasRandomUUID = typeof crypto.randomUUID === 'function';
globalThis.requireResults.http2HasConnect = typeof http2.connect === 'function';
globalThis.requireResults.http2PathHeader = http2.constants.HTTP2_HEADER_PATH;
try {
const missing = require('left-pad');
globalThis.requireResults.missingModuleThrows = false;
globalThis.requireResults.missingModuleIsStub =
typeof missing === 'function' &&
typeof missing.default === 'function' &&
typeof missing.anyNestedProperty === 'function';
} catch (err) {
globalThis.requireResults.missingModuleThrows = true;
globalThis.requireResults.missingModuleIsStub = false;
}
globalThis.requireResults.done = true;
});
",
)
.await
.expect("eval createRequire test");
let r = get_global_json(&runtime, "requireResults").await;
assert_eq!(r["done"], serde_json::json!(true));
assert_eq!(r["pathJoinWorks"], serde_json::json!(true));
assert_eq!(r["fsReadFileSync"], serde_json::json!(true));
assert_eq!(r["cryptoHasRandomUUID"], serde_json::json!(true));
assert_eq!(r["http2HasConnect"], serde_json::json!(true));
assert_eq!(r["http2PathHeader"], serde_json::json!(":path"));
assert_eq!(r["missingModuleThrows"], serde_json::json!(false));
assert_eq!(r["missingModuleIsStub"], serde_json::json!(true));
});
}
#[test]
fn pijs_fs_promises_delegates_to_node_fs_promises_api() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = PiJsRuntime::with_clock(Arc::clone(&clock))
.await
.expect("create runtime");
runtime
.eval(
r"
globalThis.fsPromisesResults = {};
import('node:fs/promises').then(async (fsp) => {
await fsp.mkdir('/tmp/promise-demo', { recursive: true });
await fsp.writeFile('/tmp/promise-demo/value.txt', 'value');
const text = await fsp.readFile('/tmp/promise-demo/value.txt', 'utf8');
const names = await fsp.readdir('/tmp/promise-demo');
globalThis.fsPromisesResults.readText = text;
globalThis.fsPromisesResults.names = names;
globalThis.fsPromisesResults.done = true;
});
",
)
.await
.expect("eval fs promises test");
let r = get_global_json(&runtime, "fsPromisesResults").await;
assert_eq!(r["done"], serde_json::json!(true));
assert_eq!(r["readText"], serde_json::json!("value"));
assert_eq!(r["names"], serde_json::json!(["value.txt"]));
});
}
#[test]
fn pijs_child_process_spawn_emits_data_and_close() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = PiJsRuntime::with_clock(Arc::clone(&clock))
.await
.expect("create runtime");
runtime
.eval(
r"
globalThis.childProcessResult = { events: [] };
import('node:child_process').then(({ spawn }) => {
const child = spawn('pi', ['--version'], {
shell: false,
stdio: ['ignore', 'pipe', 'pipe'],
});
let stdout = '';
let stderr = '';
child.stdout?.on('data', (chunk) => {
stdout += chunk.toString();
globalThis.childProcessResult.events.push('stdout');
});
child.stderr?.on('data', (chunk) => {
stderr += chunk.toString();
globalThis.childProcessResult.events.push('stderr');
});
child.on('error', (err) => {
globalThis.childProcessResult.error =
String((err && err.message) || err || '');
globalThis.childProcessResult.done = true;
});
child.on('exit', (code, signal) => {
globalThis.childProcessResult.events.push('exit');
globalThis.childProcessResult.exitCode = code;
globalThis.childProcessResult.exitSignal = signal;
});
child.on('close', (code) => {
globalThis.childProcessResult.events.push('close');
globalThis.childProcessResult.code = code;
globalThis.childProcessResult.stdout = stdout;
globalThis.childProcessResult.stderr = stderr;
globalThis.childProcessResult.killed = child.killed;
globalThis.childProcessResult.pid = child.pid;
globalThis.childProcessResult.done = true;
});
});
",
)
.await
.expect("eval child_process spawn script");
let mut requests = runtime.drain_hostcall_requests();
assert_eq!(requests.len(), 1);
let request = requests.pop_front().expect("exec hostcall");
assert!(
matches!(&request.kind, HostcallKind::Exec { cmd } if cmd == "pi"),
"unexpected hostcall kind: {:?}",
request.kind
);
runtime.complete_hostcall(
request.call_id,
HostcallOutcome::Success(serde_json::json!({
"stdout": "line-1\n",
"stderr": "warn-1\n",
"code": 0,
"killed": false
})),
);
drain_until_idle(&runtime, &clock).await;
let r = get_global_json(&runtime, "childProcessResult").await;
assert_eq!(r["done"], serde_json::json!(true));
assert_eq!(r["code"], serde_json::json!(0));
assert_eq!(r["exitCode"], serde_json::json!(0));
assert_eq!(r["exitSignal"], serde_json::Value::Null);
assert_eq!(r["stdout"], serde_json::json!("line-1\n"));
assert_eq!(r["stderr"], serde_json::json!("warn-1\n"));
assert_eq!(r["killed"], serde_json::json!(false));
assert_eq!(
r["events"],
serde_json::json!(["stdout", "stderr", "exit", "close"])
);
});
}
#[test]
fn pijs_child_process_spawn_forwards_timeout_option_to_hostcall() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = PiJsRuntime::with_clock(Arc::clone(&clock))
.await
.expect("create runtime");
runtime
.eval(
r"
globalThis.childTimeoutResult = {};
import('node:child_process').then(({ spawn }) => {
const child = spawn('pi', ['--version'], {
shell: false,
timeout: 250,
stdio: ['ignore', 'pipe', 'pipe'],
});
child.on('close', (code) => {
globalThis.childTimeoutResult.code = code;
globalThis.childTimeoutResult.killed = child.killed;
globalThis.childTimeoutResult.done = true;
});
});
",
)
.await
.expect("eval child_process timeout script");
let mut requests = runtime.drain_hostcall_requests();
assert_eq!(requests.len(), 1);
let request = requests.pop_front().expect("exec hostcall");
assert!(
matches!(&request.kind, HostcallKind::Exec { cmd } if cmd == "pi"),
"unexpected hostcall kind: {:?}",
request.kind
);
assert_eq!(
request.payload["options"]["timeout"].as_i64(),
Some(250),
"spawn timeout should be forwarded to hostcall options"
);
runtime.complete_hostcall(
request.call_id,
HostcallOutcome::Success(serde_json::json!({
"stdout": "",
"stderr": "",
"code": 0,
"killed": true
})),
);
drain_until_idle(&runtime, &clock).await;
let r = get_global_json(&runtime, "childTimeoutResult").await;
assert_eq!(r["done"], serde_json::json!(true));
assert_eq!(r["killed"], serde_json::json!(true));
assert_eq!(r["code"], serde_json::Value::Null);
});
}
#[test]
fn pijs_child_process_exec_returns_child_and_forwards_timeout() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = PiJsRuntime::with_clock(Arc::clone(&clock))
.await
.expect("create runtime");
runtime
.eval(
r"
globalThis.execShimResult = {};
import('node:child_process').then(({ exec }) => {
const child = exec('echo hello-exec', { timeout: 321 }, (err, stdout, stderr) => {
globalThis.execShimResult.cbDone = true;
globalThis.execShimResult.cbErr = err ? String((err && err.message) || err) : null;
globalThis.execShimResult.stdout = stdout;
globalThis.execShimResult.stderr = stderr;
});
globalThis.execShimResult.hasPid = typeof child.pid === 'number';
globalThis.execShimResult.hasKill = typeof child.kill === 'function';
child.on('close', () => {
globalThis.execShimResult.closed = true;
});
});
",
)
.await
.expect("eval child_process exec script");
let mut requests = runtime.drain_hostcall_requests();
assert_eq!(requests.len(), 1);
let request = requests.pop_front().expect("exec hostcall");
assert!(
matches!(&request.kind, HostcallKind::Exec { cmd } if cmd == "sh"),
"unexpected hostcall kind: {:?}",
request.kind
);
assert_eq!(
request.payload["args"],
serde_json::json!(["-c", "echo hello-exec"])
);
assert_eq!(request.payload["options"]["timeout"].as_i64(), Some(321));
runtime.complete_hostcall(
request.call_id,
HostcallOutcome::Success(serde_json::json!({
"stdout": "hello-exec\n",
"stderr": "",
"code": 0,
"killed": false
})),
);
drain_until_idle(&runtime, &clock).await;
let r = get_global_json(&runtime, "execShimResult").await;
assert_eq!(r["hasPid"], serde_json::json!(true));
assert_eq!(r["hasKill"], serde_json::json!(true));
assert_eq!(r["closed"], serde_json::json!(true));
assert_eq!(r["cbDone"], serde_json::json!(true));
assert_eq!(r["cbErr"], serde_json::Value::Null);
assert_eq!(r["stdout"], serde_json::json!("hello-exec\n"));
assert_eq!(r["stderr"], serde_json::json!(""));
});
}
#[test]
fn pijs_child_process_exec_file_returns_child_and_forwards_timeout() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = PiJsRuntime::with_clock(Arc::clone(&clock))
.await
.expect("create runtime");
runtime
.eval(
r"
globalThis.execFileShimResult = {};
import('node:child_process').then(({ execFile }) => {
const child = execFile('echo', ['hello-file'], { timeout: 222 }, (err, stdout, stderr) => {
globalThis.execFileShimResult.cbDone = true;
globalThis.execFileShimResult.cbErr = err ? String((err && err.message) || err) : null;
globalThis.execFileShimResult.stdout = stdout;
globalThis.execFileShimResult.stderr = stderr;
});
globalThis.execFileShimResult.hasPid = typeof child.pid === 'number';
globalThis.execFileShimResult.hasKill = typeof child.kill === 'function';
});
",
)
.await
.expect("eval child_process execFile script");
let mut requests = runtime.drain_hostcall_requests();
assert_eq!(requests.len(), 1);
let request = requests.pop_front().expect("execFile hostcall");
assert!(
matches!(&request.kind, HostcallKind::Exec { cmd } if cmd == "echo"),
"unexpected hostcall kind: {:?}",
request.kind
);
assert_eq!(request.payload["args"], serde_json::json!(["hello-file"]));
assert_eq!(request.payload["options"]["timeout"].as_i64(), Some(222));
runtime.complete_hostcall(
request.call_id,
HostcallOutcome::Success(serde_json::json!({
"stdout": "hello-file\n",
"stderr": "",
"code": 0,
"killed": false
})),
);
drain_until_idle(&runtime, &clock).await;
let r = get_global_json(&runtime, "execFileShimResult").await;
assert_eq!(r["hasPid"], serde_json::json!(true));
assert_eq!(r["hasKill"], serde_json::json!(true));
assert_eq!(r["cbDone"], serde_json::json!(true));
assert_eq!(r["cbErr"], serde_json::Value::Null);
assert_eq!(r["stdout"], serde_json::json!("hello-file\n"));
assert_eq!(r["stderr"], serde_json::json!(""));
});
}
#[test]
fn pijs_child_process_process_kill_targets_spawned_pid() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = PiJsRuntime::with_clock(Arc::clone(&clock))
.await
.expect("create runtime");
runtime
.eval(
r"
globalThis.childKillResult = {};
import('node:child_process').then(({ spawn }) => {
const child = spawn('pi', ['--version'], {
shell: false,
detached: true,
stdio: ['ignore', 'pipe', 'pipe'],
});
globalThis.childKillResult.pid = child.pid;
child.on('close', (code) => {
globalThis.childKillResult.code = code;
globalThis.childKillResult.killed = child.killed;
globalThis.childKillResult.done = true;
});
try {
globalThis.childKillResult.killOk = process.kill(-child.pid, 'SIGKILL') === true;
} catch (err) {
globalThis.childKillResult.killErrorCode = String((err && err.code) || '');
globalThis.childKillResult.killErrorMessage = String((err && err.message) || err || '');
}
});
",
)
.await
.expect("eval child_process kill script");
let mut requests = runtime.drain_hostcall_requests();
assert_eq!(requests.len(), 1);
let request = requests.pop_front().expect("exec hostcall");
runtime.complete_hostcall(
request.call_id,
HostcallOutcome::Success(serde_json::json!({
"stdout": "",
"stderr": "",
"code": 0,
"killed": false
})),
);
drain_until_idle(&runtime, &clock).await;
let r = get_global_json(&runtime, "childKillResult").await;
assert_eq!(r["killOk"], serde_json::json!(true));
assert_eq!(r["killed"], serde_json::json!(true));
assert_eq!(r["code"], serde_json::Value::Null);
assert_eq!(r["done"], serde_json::json!(true));
});
}
#[test]
fn pijs_child_process_denied_exec_emits_error_and_close() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = PiJsRuntime::with_clock(Arc::clone(&clock))
.await
.expect("create runtime");
runtime
.eval(
r"
globalThis.childDeniedResult = {};
import('node:child_process').then(({ spawn }) => {
const child = spawn('pi', ['--version'], {
shell: false,
stdio: ['ignore', 'pipe', 'pipe'],
});
child.on('error', (err) => {
globalThis.childDeniedResult.errorCode = String((err && err.code) || '');
globalThis.childDeniedResult.errorMessage = String((err && err.message) || err || '');
});
child.on('close', (code) => {
globalThis.childDeniedResult.code = code;
globalThis.childDeniedResult.killed = child.killed;
globalThis.childDeniedResult.done = true;
});
});
",
)
.await
.expect("eval child_process denied script");
let mut requests = runtime.drain_hostcall_requests();
assert_eq!(requests.len(), 1);
let request = requests.pop_front().expect("exec hostcall");
runtime.complete_hostcall(
request.call_id,
HostcallOutcome::Error {
code: "denied".to_string(),
message: "Capability 'exec' denied by policy".to_string(),
},
);
drain_until_idle(&runtime, &clock).await;
let r = get_global_json(&runtime, "childDeniedResult").await;
assert_eq!(r["done"], serde_json::json!(true));
assert_eq!(r["errorCode"], serde_json::json!("denied"));
assert_eq!(
r["errorMessage"],
serde_json::json!("Capability 'exec' denied by policy")
);
assert_eq!(r["code"], serde_json::json!(1));
assert_eq!(r["killed"], serde_json::json!(false));
});
}
#[test]
fn pijs_child_process_rejects_unsupported_shell_option() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = PiJsRuntime::with_clock(Arc::clone(&clock))
.await
.expect("create runtime");
runtime
.eval(
r"
globalThis.childOptionResult = {};
import('node:child_process').then(({ spawn }) => {
try {
spawn('pi', ['--version'], { shell: true });
globalThis.childOptionResult.threw = false;
} catch (err) {
globalThis.childOptionResult.threw = true;
globalThis.childOptionResult.message = String((err && err.message) || err || '');
}
globalThis.childOptionResult.done = true;
});
",
)
.await
.expect("eval child_process unsupported shell script");
drain_until_idle(&runtime, &clock).await;
let r = get_global_json(&runtime, "childOptionResult").await;
assert_eq!(r["done"], serde_json::json!(true));
assert_eq!(r["threw"], serde_json::json!(true));
assert_eq!(
r["message"],
serde_json::json!(
"node:child_process.spawn: only shell=false is supported in PiJS"
)
);
assert_eq!(runtime.drain_hostcall_requests().len(), 0);
});
}
// -----------------------------------------------------------------------
// bd-2b9y: Node core shim unit tests
// -----------------------------------------------------------------------
#[test]
fn pijs_node_os_module_exports() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = PiJsRuntime::with_clock(Arc::clone(&clock))
.await
.expect("create runtime");
runtime
.eval(
r"
globalThis.osResults = {};
import('node:os').then((os) => {
globalThis.osResults.homedir = os.homedir();
globalThis.osResults.tmpdir = os.tmpdir();
globalThis.osResults.hostname = os.hostname();
globalThis.osResults.platform = os.platform();
globalThis.osResults.arch = os.arch();
globalThis.osResults.type = os.type();
globalThis.osResults.release = os.release();
globalThis.osResults.done = true;
});
",
)
.await
.expect("eval node:os");
let r = get_global_json(&runtime, "osResults").await;
assert_eq!(r["done"], serde_json::json!(true));
// homedir returns HOME env or fallback
assert!(r["homedir"].is_string());
// tmpdir matches std::env::temp_dir()
let expected_tmpdir = std::env::temp_dir().display().to_string();
assert_eq!(r["tmpdir"].as_str().unwrap(), expected_tmpdir);
// hostname is a non-empty string (real system hostname)
assert!(
r["hostname"].as_str().is_some_and(|s| !s.is_empty()),
"hostname should be non-empty string"
);
// platform/arch/type match current system
let expected_platform = match std::env::consts::OS {
"macos" => "darwin",
"windows" => "win32",
other => other,
};
assert_eq!(r["platform"].as_str().unwrap(), expected_platform);
let expected_arch = match std::env::consts::ARCH {
"x86_64" => "x64",
"aarch64" => "arm64",
other => other,
};
assert_eq!(r["arch"].as_str().unwrap(), expected_arch);
let expected_type = match std::env::consts::OS {
"linux" => "Linux",
"macos" => "Darwin",
"windows" => "Windows_NT",
other => other,
};
assert_eq!(r["type"].as_str().unwrap(), expected_type);
assert_eq!(r["release"], serde_json::json!("6.0.0"));
});
}
#[test]
fn build_node_os_module_produces_valid_js() {
let source = super::build_node_os_module();
// Verify basic structure - has expected exports
assert!(
source.contains("export function platform()"),
"missing platform"
);
assert!(source.contains("export function cpus()"), "missing cpus");
assert!(source.contains("_numCpus"), "missing _numCpus");
// Print first few lines for debugging
for (i, line) in source.lines().enumerate().take(20) {
eprintln!(" {i}: {line}");
}
let num_cpus = std::thread::available_parallelism().map_or(1, std::num::NonZero::get);
assert!(
source.contains(&format!("const _numCpus = {num_cpus}")),
"expected _numCpus = {num_cpus} in module"
);
}
#[test]
fn pijs_node_os_native_values_cpus_and_userinfo() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = PiJsRuntime::with_clock(Arc::clone(&clock))
.await
.expect("create runtime");
runtime
.eval(
r"
globalThis.nativeOsResults = {};
import('node:os').then((os) => {
globalThis.nativeOsResults.cpuCount = os.cpus().length;
globalThis.nativeOsResults.totalmem = os.totalmem();
globalThis.nativeOsResults.freemem = os.freemem();
globalThis.nativeOsResults.eol = os.EOL;
globalThis.nativeOsResults.endianness = os.endianness();
globalThis.nativeOsResults.devNull = os.devNull;
const ui = os.userInfo();
globalThis.nativeOsResults.uid = ui.uid;
globalThis.nativeOsResults.username = ui.username;
globalThis.nativeOsResults.hasShell = typeof ui.shell === 'string';
globalThis.nativeOsResults.hasHomedir = typeof ui.homedir === 'string';
globalThis.nativeOsResults.done = true;
});
",
)
.await
.expect("eval node:os native");
let r = get_global_json(&runtime, "nativeOsResults").await;
assert_eq!(r["done"], serde_json::json!(true));
// cpus() returns array with count matching available parallelism
let expected_cpus =
std::thread::available_parallelism().map_or(1, std::num::NonZero::get);
assert_eq!(r["cpuCount"], serde_json::json!(expected_cpus));
// totalmem/freemem are positive numbers
assert!(r["totalmem"].as_f64().unwrap() > 0.0);
assert!(r["freemem"].as_f64().unwrap() > 0.0);
// EOL is correct for platform
let expected_eol = if cfg!(windows) { "\r\n" } else { "\n" };
assert_eq!(r["eol"], serde_json::json!(expected_eol));
assert_eq!(r["endianness"], serde_json::json!("LE"));
let expected_dev_null = if cfg!(windows) {
"\\\\.\\NUL"
} else {
"/dev/null"
};
assert_eq!(r["devNull"], serde_json::json!(expected_dev_null));
// userInfo has real uid and non-empty username
assert!(r["uid"].is_number());
assert!(r["username"].as_str().is_some_and(|s| !s.is_empty()));
assert_eq!(r["hasShell"], serde_json::json!(true));
assert_eq!(r["hasHomedir"], serde_json::json!(true));
});
}
#[test]
fn pijs_node_os_bare_import_alias() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = PiJsRuntime::with_clock(Arc::clone(&clock))
.await
.expect("create runtime");
runtime
.eval(
r"
globalThis.bare_os_ok = false;
import('os').then((os) => {
globalThis.bare_os_ok = typeof os.homedir === 'function'
&& typeof os.platform === 'function';
});
",
)
.await
.expect("eval bare os import");
assert_eq!(
get_global_json(&runtime, "bare_os_ok").await,
serde_json::json!(true)
);
});
}
#[test]
fn pijs_node_url_module_exports() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = PiJsRuntime::with_clock(Arc::clone(&clock))
.await
.expect("create runtime");
runtime
.eval(
r"
globalThis.urlResults = {};
import('node:url').then((url) => {
globalThis.urlResults.fileToPath = url.fileURLToPath('file:///home/user/test.txt');
globalThis.urlResults.pathToFile = url.pathToFileURL('/home/user/test.txt').href;
const u = new url.URL('https://example.com/path?key=val#frag');
globalThis.urlResults.href = u.href;
globalThis.urlResults.protocol = u.protocol;
globalThis.urlResults.hostname = u.hostname;
globalThis.urlResults.pathname = u.pathname;
globalThis.urlResults.toString = u.toString();
globalThis.urlResults.done = true;
});
",
)
.await
.expect("eval node:url");
let r = get_global_json(&runtime, "urlResults").await;
assert_eq!(r["done"], serde_json::json!(true));
assert_eq!(r["fileToPath"], serde_json::json!("/home/user/test.txt"));
assert_eq!(
r["pathToFile"],
serde_json::json!("file:///home/user/test.txt")
);
// URL parsing
assert!(r["href"].as_str().unwrap().starts_with("https://"));
assert_eq!(r["protocol"], serde_json::json!("https:"));
assert_eq!(r["hostname"], serde_json::json!("example.com"));
// Shim URL.pathname includes query+fragment (lightweight parser)
assert!(r["pathname"].as_str().unwrap().starts_with("/path"));
});
}
#[test]
fn pijs_node_crypto_create_hash_and_uuid() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = PiJsRuntime::with_clock(Arc::clone(&clock))
.await
.expect("create runtime");
runtime
.eval(
r"
globalThis.cryptoResults = {};
import('node:crypto').then((crypto) => {
// createHash
const hash = crypto.createHash('sha256');
hash.update('hello');
globalThis.cryptoResults.hexDigest = hash.digest('hex');
// createHash chained
globalThis.cryptoResults.chainedHex = crypto
.createHash('sha256')
.update('world')
.digest('hex');
// randomUUID
const uuid = crypto.randomUUID();
globalThis.cryptoResults.uuidLength = uuid.length;
// UUID v4 format: 8-4-4-4-12
globalThis.cryptoResults.uuidHasDashes = uuid.split('-').length === 5;
globalThis.cryptoResults.done = true;
});
",
)
.await
.expect("eval node:crypto");
let r = get_global_json(&runtime, "cryptoResults").await;
assert_eq!(r["done"], serde_json::json!(true));
// createHash returns a hex string
assert!(r["hexDigest"].is_string());
let hex = r["hexDigest"].as_str().unwrap();
// djb2-simulated hash, not real SHA-256 — verify it's a non-empty hex string
assert!(!hex.is_empty());
assert!(hex.chars().all(|c| c.is_ascii_hexdigit()));
// chained usage also works
assert!(r["chainedHex"].is_string());
let chained = r["chainedHex"].as_str().unwrap();
assert!(!chained.is_empty());
assert!(chained.chars().all(|c| c.is_ascii_hexdigit()));
// Two different inputs produce different hashes
assert_ne!(r["hexDigest"], r["chainedHex"]);
// randomUUID format
assert_eq!(r["uuidLength"], serde_json::json!(36));
assert_eq!(r["uuidHasDashes"], serde_json::json!(true));
});
}
#[test]
fn pijs_web_crypto_get_random_values_smoke() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = PiJsRuntime::with_clock(Arc::clone(&clock))
.await
.expect("create runtime");
runtime
.eval(
r"
const bytes = new Uint8Array(32);
crypto.getRandomValues(bytes);
globalThis.cryptoRng = {
len: bytes.length,
inRange: Array.from(bytes).every((n) => Number.isInteger(n) && n >= 0 && n <= 255),
};
",
)
.await
.expect("eval web crypto getRandomValues");
let r = get_global_json(&runtime, "cryptoRng").await;
assert_eq!(r["len"], serde_json::json!(32));
assert_eq!(r["inRange"], serde_json::json!(true));
});
}
#[test]
fn pijs_buffer_global_operations() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = PiJsRuntime::with_clock(Arc::clone(&clock))
.await
.expect("create runtime");
runtime
.eval(
r"
globalThis.bufResults = {};
// Test the global Buffer polyfill (set up during runtime init)
const B = globalThis.Buffer;
globalThis.bufResults.hasBuffer = typeof B === 'function';
globalThis.bufResults.hasFrom = typeof B.from === 'function';
// Buffer.from with array input
const arr = B.from([65, 66, 67]);
globalThis.bufResults.fromArrayLength = arr.length;
// Uint8Array allocation
const zeroed = new Uint8Array(16);
globalThis.bufResults.allocLength = zeroed.length;
globalThis.bufResults.done = true;
",
)
.await
.expect("eval Buffer");
let r = get_global_json(&runtime, "bufResults").await;
assert_eq!(r["done"], serde_json::json!(true));
assert_eq!(r["hasBuffer"], serde_json::json!(true));
assert_eq!(r["hasFrom"], serde_json::json!(true));
assert_eq!(r["fromArrayLength"], serde_json::json!(3));
assert_eq!(r["allocLength"], serde_json::json!(16));
});
}
#[test]
fn pijs_node_fs_promises_async_roundtrip() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = PiJsRuntime::with_clock(Arc::clone(&clock))
.await
.expect("create runtime");
runtime
.eval(
r"
globalThis.fspResults = {};
import('node:fs/promises').then(async (fsp) => {
// Write then read back
await fsp.writeFile('/test/hello.txt', 'async content');
const data = await fsp.readFile('/test/hello.txt', 'utf8');
globalThis.fspResults.readBack = data;
// stat
const st = await fsp.stat('/test/hello.txt');
globalThis.fspResults.statIsFile = st.isFile();
globalThis.fspResults.statSize = st.size;
// mkdir + readdir
await fsp.mkdir('/test/subdir');
await fsp.writeFile('/test/subdir/a.txt', 'aaa');
const entries = await fsp.readdir('/test/subdir');
globalThis.fspResults.dirEntries = entries;
// unlink
await fsp.unlink('/test/subdir/a.txt');
const exists = await fsp.access('/test/subdir/a.txt').then(() => true).catch(() => false);
globalThis.fspResults.deletedFileExists = exists;
globalThis.fspResults.done = true;
});
",
)
.await
.expect("eval fs/promises");
drain_until_idle(&runtime, &clock).await;
let r = get_global_json(&runtime, "fspResults").await;
assert_eq!(r["done"], serde_json::json!(true));
assert_eq!(r["readBack"], serde_json::json!("async content"));
assert_eq!(r["statIsFile"], serde_json::json!(true));
assert!(r["statSize"].as_u64().unwrap() > 0);
assert_eq!(r["dirEntries"], serde_json::json!(["a.txt"]));
assert_eq!(r["deletedFileExists"], serde_json::json!(false));
});
}
#[test]
fn pijs_node_process_module_exports() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let config = PiJsRuntimeConfig {
cwd: "/test/project".to_string(),
args: vec!["arg1".to_string(), "arg2".to_string()],
env: HashMap::new(),
limits: PiJsRuntimeLimits::default(),
repair_mode: RepairMode::default(),
allow_unsafe_sync_exec: false,
deny_env: false,
disk_cache_dir: None,
};
let runtime =
PiJsRuntime::with_clock_and_config_with_policy(Arc::clone(&clock), config, None)
.await
.expect("create runtime");
runtime
.eval(
r"
globalThis.procResults = {};
import('node:process').then((proc) => {
globalThis.procResults.platform = proc.platform;
globalThis.procResults.arch = proc.arch;
globalThis.procResults.version = proc.version;
globalThis.procResults.pid = proc.pid;
globalThis.procResults.cwdType = typeof proc.cwd;
globalThis.procResults.cwdValue = typeof proc.cwd === 'function'
? proc.cwd() : proc.cwd;
globalThis.procResults.hasEnv = typeof proc.env === 'object';
globalThis.procResults.hasStdout = typeof proc.stdout === 'object';
globalThis.procResults.hasStderr = typeof proc.stderr === 'object';
globalThis.procResults.hasNextTick = typeof proc.nextTick === 'function';
// nextTick should schedule microtask
globalThis.procResults.nextTickRan = false;
proc.nextTick(() => { globalThis.procResults.nextTickRan = true; });
// hrtime should return array
const hr = proc.hrtime();
globalThis.procResults.hrtimeIsArray = Array.isArray(hr);
globalThis.procResults.hrtimeLength = hr.length;
globalThis.procResults.done = true;
});
",
)
.await
.expect("eval node:process");
drain_until_idle(&runtime, &clock).await;
let r = get_global_json(&runtime, "procResults").await;
assert_eq!(r["done"], serde_json::json!(true));
// platform/arch are determined at runtime from env/cfg
assert!(r["platform"].is_string(), "platform should be a string");
let expected_arch = if cfg!(target_arch = "aarch64") {
"arm64"
} else {
"x64"
};
assert_eq!(r["arch"], serde_json::json!(expected_arch));
assert!(r["version"].is_string());
assert_eq!(r["pid"], serde_json::json!(1));
assert!(r["hasEnv"] == serde_json::json!(true));
assert!(r["hasStdout"] == serde_json::json!(true));
assert!(r["hasStderr"] == serde_json::json!(true));
assert!(r["hasNextTick"] == serde_json::json!(true));
// nextTick is scheduled as microtask — should have run
assert_eq!(r["nextTickRan"], serde_json::json!(true));
assert_eq!(r["hrtimeIsArray"], serde_json::json!(true));
assert_eq!(r["hrtimeLength"], serde_json::json!(2));
});
}
#[test]
fn pijs_pi_path_join_behavior() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = PiJsRuntime::with_clock(Arc::clone(&clock))
.await
.expect("create runtime");
runtime
.eval(
r"
globalThis.joinResults = {};
globalThis.joinResults.concatAbs = pi.path.join('/a', '/b');
globalThis.joinResults.normal = pi.path.join('a', 'b');
globalThis.joinResults.root = pi.path.join('/', 'a');
globalThis.joinResults.dots = pi.path.join('/a', '..', 'b');
globalThis.joinResults.done = true;
",
)
.await
.expect("eval pi.path.join");
let r = get_global_json(&runtime, "joinResults").await;
assert_eq!(r["done"], serde_json::json!(true));
// Should be /a/b, NOT /b (bug fix)
assert_eq!(r["concatAbs"], serde_json::json!("/a/b"));
assert_eq!(r["normal"], serde_json::json!("a/b"));
assert_eq!(r["root"], serde_json::json!("/a"));
assert_eq!(r["dots"], serde_json::json!("/b"));
});
}
#[test]
fn pijs_node_path_relative_resolve_format() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let config = PiJsRuntimeConfig {
cwd: "/home/user/project".to_string(),
args: Vec::new(),
env: HashMap::new(),
limits: PiJsRuntimeLimits::default(),
repair_mode: RepairMode::default(),
allow_unsafe_sync_exec: false,
deny_env: false,
disk_cache_dir: None,
};
let runtime =
PiJsRuntime::with_clock_and_config_with_policy(Arc::clone(&clock), config, None)
.await
.expect("create runtime");
runtime
.eval(
r"
globalThis.pathResults2 = {};
import('node:path').then((path) => {
// relative
globalThis.pathResults2.relSameDir = path.relative('/a/b/c', '/a/b/c/d');
globalThis.pathResults2.relUp = path.relative('/a/b/c', '/a/b');
globalThis.pathResults2.relSame = path.relative('/a/b', '/a/b');
// resolve uses cwd as base
globalThis.pathResults2.resolveAbs = path.resolve('/absolute/path');
globalThis.pathResults2.resolveRel = path.resolve('relative');
// format
globalThis.pathResults2.formatFull = path.format({
dir: '/home/user',
base: 'file.txt'
});
// sep and delimiter constants
globalThis.pathResults2.sep = path.sep;
globalThis.pathResults2.delimiter = path.delimiter;
// dirname edge cases
globalThis.pathResults2.dirnameRoot = path.dirname('/');
globalThis.pathResults2.dirnameNested = path.dirname('/a/b/c');
// join edge cases
globalThis.pathResults2.joinEmpty = path.join();
globalThis.pathResults2.joinDots = path.join('a', '..', 'b');
globalThis.pathResults2.done = true;
});
",
)
.await
.expect("eval path extended 2");
let r = get_global_json(&runtime, "pathResults2").await;
assert_eq!(r["done"], serde_json::json!(true));
assert_eq!(r["relSameDir"], serde_json::json!("d"));
assert_eq!(r["relUp"], serde_json::json!(".."));
assert_eq!(r["relSame"], serde_json::json!("."));
assert_eq!(r["resolveAbs"], serde_json::json!("/absolute/path"));
// resolve('relative') should resolve against cwd
assert!(r["resolveRel"].as_str().unwrap().ends_with("/relative"));
assert_eq!(r["formatFull"], serde_json::json!("/home/user/file.txt"));
assert_eq!(r["sep"], serde_json::json!("/"));
assert_eq!(r["delimiter"], serde_json::json!(":"));
assert_eq!(r["dirnameRoot"], serde_json::json!("/"));
assert_eq!(r["dirnameNested"], serde_json::json!("/a/b"));
// join doesn't normalize; normalize is separate
let join_dots = r["joinDots"].as_str().unwrap();
assert!(join_dots == "b" || join_dots == "a/../b");
});
}
#[test]
fn pijs_node_util_module_exports() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = PiJsRuntime::with_clock(Arc::clone(&clock))
.await
.expect("create runtime");
runtime
.eval(
r"
globalThis.utilResults = {};
import('node:util').then((util) => {
globalThis.utilResults.hasInspect = typeof util.inspect === 'function';
globalThis.utilResults.hasPromisify = typeof util.promisify === 'function';
globalThis.utilResults.inspectResult = util.inspect({ a: 1, b: [2, 3] });
globalThis.utilResults.done = true;
});
",
)
.await
.expect("eval node:util");
let r = get_global_json(&runtime, "utilResults").await;
assert_eq!(r["done"], serde_json::json!(true));
assert_eq!(r["hasInspect"], serde_json::json!(true));
assert_eq!(r["hasPromisify"], serde_json::json!(true));
// inspect should return some string representation
assert!(r["inspectResult"].is_string());
});
}
#[test]
fn pijs_node_assert_module_pass_and_fail() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = PiJsRuntime::with_clock(Arc::clone(&clock))
.await
.expect("create runtime");
runtime
.eval(
r"
globalThis.assertResults = {};
import('node:assert').then((mod) => {
const assert = mod.default;
// Passing assertions should not throw
assert.ok(true);
assert.strictEqual(1, 1);
assert.deepStrictEqual({ a: 1 }, { a: 1 });
assert.notStrictEqual(1, 2);
// Failing assertion should throw
try {
assert.strictEqual(1, 2);
globalThis.assertResults.failDidNotThrow = true;
} catch (e) {
globalThis.assertResults.failThrew = true;
globalThis.assertResults.failMessage = e.message || String(e);
}
globalThis.assertResults.done = true;
});
",
)
.await
.expect("eval node:assert");
let r = get_global_json(&runtime, "assertResults").await;
assert_eq!(r["done"], serde_json::json!(true));
assert_eq!(r["failThrew"], serde_json::json!(true));
assert!(r["failMessage"].is_string());
});
}
#[test]
fn pijs_node_fs_sync_edge_cases() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = PiJsRuntime::with_clock(Arc::clone(&clock))
.await
.expect("create runtime");
runtime
.eval(
r"
globalThis.fsEdge = {};
import('node:fs').then((fs) => {
// Write, overwrite, read back
fs.writeFileSync('/edge/file.txt', 'first');
fs.writeFileSync('/edge/file.txt', 'second');
globalThis.fsEdge.overwrite = fs.readFileSync('/edge/file.txt', 'utf8');
// existsSync for existing vs non-existing
globalThis.fsEdge.existsTrue = fs.existsSync('/edge/file.txt');
globalThis.fsEdge.existsFalse = fs.existsSync('/nonexistent/file.txt');
// mkdirSync + readdirSync with withFileTypes
fs.mkdirSync('/edge/dir');
fs.writeFileSync('/edge/dir/a.txt', 'aaa');
fs.mkdirSync('/edge/dir/sub');
const dirents = fs.readdirSync('/edge/dir', { withFileTypes: true });
globalThis.fsEdge.direntCount = dirents.length;
const fileDirent = dirents.find(d => d.name === 'a.txt');
const dirDirent = dirents.find(d => d.name === 'sub');
globalThis.fsEdge.fileIsFile = fileDirent ? fileDirent.isFile() : null;
globalThis.fsEdge.dirIsDir = dirDirent ? dirDirent.isDirectory() : null;
// rmSync recursive
fs.writeFileSync('/edge/dir/sub/deep.txt', 'deep');
fs.rmSync('/edge/dir', { recursive: true });
globalThis.fsEdge.rmRecursiveGone = !fs.existsSync('/edge/dir');
// accessSync on non-existing file should throw
try {
fs.accessSync('/nope');
globalThis.fsEdge.accessThrew = false;
} catch (e) {
globalThis.fsEdge.accessThrew = true;
}
// statSync on directory
fs.mkdirSync('/edge/statdir');
const dStat = fs.statSync('/edge/statdir');
globalThis.fsEdge.dirStatIsDir = dStat.isDirectory();
globalThis.fsEdge.dirStatIsFile = dStat.isFile();
globalThis.fsEdge.done = true;
});
",
)
.await
.expect("eval fs edge cases");
let r = get_global_json(&runtime, "fsEdge").await;
assert_eq!(r["done"], serde_json::json!(true));
assert_eq!(r["overwrite"], serde_json::json!("second"));
assert_eq!(r["existsTrue"], serde_json::json!(true));
assert_eq!(r["existsFalse"], serde_json::json!(false));
assert_eq!(r["direntCount"], serde_json::json!(2));
assert_eq!(r["fileIsFile"], serde_json::json!(true));
assert_eq!(r["dirIsDir"], serde_json::json!(true));
assert_eq!(r["rmRecursiveGone"], serde_json::json!(true));
assert_eq!(r["accessThrew"], serde_json::json!(true));
assert_eq!(r["dirStatIsDir"], serde_json::json!(true));
assert_eq!(r["dirStatIsFile"], serde_json::json!(false));
});
}
#[test]
fn pijs_node_net_and_http_stubs_throw() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = PiJsRuntime::with_clock(Arc::clone(&clock))
.await
.expect("create runtime");
runtime
.eval(
r"
globalThis.stubResults = {};
(async () => {
// node:net createServer should throw
const net = await import('node:net');
try {
net.createServer();
globalThis.stubResults.netThrew = false;
} catch (e) {
globalThis.stubResults.netThrew = true;
}
// node:http createServer should throw
const http = await import('node:http');
try {
http.createServer();
globalThis.stubResults.httpThrew = false;
} catch (e) {
globalThis.stubResults.httpThrew = true;
}
// node:https createServer should throw
const https = await import('node:https');
try {
https.createServer();
globalThis.stubResults.httpsThrew = false;
} catch (e) {
globalThis.stubResults.httpsThrew = true;
}
globalThis.stubResults.done = true;
})();
",
)
.await
.expect("eval stub throws");
drain_until_idle(&runtime, &clock).await;
let r = get_global_json(&runtime, "stubResults").await;
assert_eq!(r["done"], serde_json::json!(true));
assert_eq!(r["netThrew"], serde_json::json!(true));
assert_eq!(r["httpThrew"], serde_json::json!(true));
assert_eq!(r["httpsThrew"], serde_json::json!(true));
});
}
#[test]
fn pijs_node_readline_stub_exports() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = PiJsRuntime::with_clock(Arc::clone(&clock))
.await
.expect("create runtime");
runtime
.eval(
r"
globalThis.rlResult = {};
import('node:readline').then((rl) => {
globalThis.rlResult.hasCreateInterface = typeof rl.createInterface === 'function';
globalThis.rlResult.done = true;
});
",
)
.await
.expect("eval readline");
let r = get_global_json(&runtime, "rlResult").await;
assert_eq!(r["done"], serde_json::json!(true));
assert_eq!(r["hasCreateInterface"], serde_json::json!(true));
});
}
#[test]
fn pijs_node_stream_promises_pipeline_pass_through() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = PiJsRuntime::with_clock(Arc::clone(&clock))
.await
.expect("create runtime");
runtime
.eval(
r#"
globalThis.streamInterop = { done: false };
(async () => {
const { Readable, PassThrough, Writable } = await import("node:stream");
const { pipeline } = await import("node:stream/promises");
const collected = [];
const source = Readable.from(["alpha", "-", "omega"]);
const through = new PassThrough();
const sink = new Writable({
write(chunk, _encoding, callback) {
collected.push(String(chunk));
callback(null);
}
});
await pipeline(source, through, sink);
globalThis.streamInterop.value = collected.join("");
globalThis.streamInterop.done = true;
})().catch((e) => {
globalThis.streamInterop.error = String(e && e.message ? e.message : e);
globalThis.streamInterop.done = false;
});
"#,
)
.await
.expect("eval node:stream pipeline");
drain_until_idle(&runtime, &clock).await;
let result = get_global_json(&runtime, "streamInterop").await;
assert_eq!(result["done"], serde_json::json!(true));
assert_eq!(result["value"], serde_json::json!("alpha-omega"));
});
}
#[test]
fn pijs_fs_create_stream_pipeline_copies_content() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = PiJsRuntime::with_clock(Arc::clone(&clock))
.await
.expect("create runtime");
runtime
.eval(
r#"
globalThis.fsStreamCopy = { done: false };
(async () => {
const fs = await import("node:fs");
const { pipeline } = await import("node:stream/promises");
fs.writeFileSync("/tmp/source.txt", "stream-data-123");
const src = fs.createReadStream("/tmp/source.txt");
const dst = fs.createWriteStream("/tmp/dest.txt");
await pipeline(src, dst);
globalThis.fsStreamCopy.value = fs.readFileSync("/tmp/dest.txt", "utf8");
globalThis.fsStreamCopy.done = true;
})().catch((e) => {
globalThis.fsStreamCopy.error = String(e && e.message ? e.message : e);
globalThis.fsStreamCopy.done = false;
});
"#,
)
.await
.expect("eval fs stream copy");
drain_until_idle(&runtime, &clock).await;
let result = get_global_json(&runtime, "fsStreamCopy").await;
assert_eq!(result["done"], serde_json::json!(true));
assert_eq!(result["value"], serde_json::json!("stream-data-123"));
});
}
#[test]
fn pijs_node_stream_web_stream_bridge_roundtrip() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = PiJsRuntime::with_clock(Arc::clone(&clock))
.await
.expect("create runtime");
runtime
.eval(
r#"
globalThis.webBridge = { done: false, skipped: false };
(async () => {
if (typeof ReadableStream !== "function" || typeof WritableStream !== "function") {
globalThis.webBridge.skipped = true;
globalThis.webBridge.done = true;
return;
}
const { Readable, Writable } = await import("node:stream");
const { pipeline } = await import("node:stream/promises");
const webReadable = new ReadableStream({
start(controller) {
controller.enqueue("ab");
controller.enqueue("cd");
controller.close();
}
});
const nodeReadable = Readable.fromWeb(webReadable);
const fromWebChunks = [];
const webWritable = new WritableStream({
write(chunk) {
fromWebChunks.push(String(chunk));
}
});
const nodeWritable = Writable.fromWeb(webWritable);
await pipeline(nodeReadable, nodeWritable);
const nodeReadableRoundtrip = Readable.from(["x", "y"]);
const webReadableRoundtrip = Readable.toWeb(nodeReadableRoundtrip);
const reader = webReadableRoundtrip.getReader();
const toWebChunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
toWebChunks.push(String(value));
}
globalThis.webBridge.fromWeb = fromWebChunks.join("");
globalThis.webBridge.toWeb = toWebChunks.join("");
globalThis.webBridge.done = true;
})().catch((e) => {
globalThis.webBridge.error = String(e && e.message ? e.message : e);
globalThis.webBridge.done = false;
});
"#,
)
.await
.expect("eval web stream bridge");
drain_until_idle(&runtime, &clock).await;
let result = get_global_json(&runtime, "webBridge").await;
assert_eq!(result["done"], serde_json::json!(true));
if result["skipped"] == serde_json::json!(true) {
return;
}
assert_eq!(result["fromWeb"], serde_json::json!("abcd"));
assert_eq!(result["toWeb"], serde_json::json!("xy"));
});
}
// ── Streaming hostcall tests ────────────────────────────────────────
#[test]
fn pijs_stream_chunks_delivered_via_async_iterator() {
futures::executor::block_on(async {
let runtime = PiJsRuntime::with_clock(DeterministicClock::new(0))
.await
.expect("create runtime");
// Start a streaming exec call
runtime
.eval(
r#"
globalThis.chunks = [];
globalThis.done = false;
(async () => {
const stream = pi.exec("cat", ["big.txt"], { stream: true });
for await (const chunk of stream) {
globalThis.chunks.push(chunk);
}
globalThis.done = true;
})();
"#,
)
.await
.expect("eval");
let requests = runtime.drain_hostcall_requests();
assert_eq!(requests.len(), 1);
let call_id = requests[0].call_id.clone();
// Send three non-final chunks then a final one
for seq in 0..3 {
runtime.complete_hostcall(
call_id.clone(),
HostcallOutcome::StreamChunk {
sequence: seq,
chunk: serde_json::json!({ "line": seq }),
is_final: false,
},
);
let stats = runtime.tick().await.expect("tick chunk");
assert!(stats.ran_macrotask);
}
// Hostcall should still be pending (tracker not yet completed)
assert!(
runtime.hostcall_tracker.borrow().is_pending(&call_id),
"hostcall should still be pending after non-final chunks"
);
// Send final chunk
runtime.complete_hostcall(
call_id.clone(),
HostcallOutcome::StreamChunk {
sequence: 3,
chunk: serde_json::json!({ "line": 3 }),
is_final: true,
},
);
let stats = runtime.tick().await.expect("tick final");
assert!(stats.ran_macrotask);
// Hostcall is now completed
assert!(
!runtime.hostcall_tracker.borrow().is_pending(&call_id),
"hostcall should be completed after final chunk"
);
// Run microtasks to let the async iterator resolve
runtime.tick().await.expect("tick settle");
let chunks = get_global_json(&runtime, "chunks").await;
let arr = chunks.as_array().expect("chunks is array");
assert_eq!(arr.len(), 4, "expected 4 chunks, got {arr:?}");
for (i, c) in arr.iter().enumerate() {
assert_eq!(c["line"], serde_json::json!(i), "chunk {i}");
}
let done = get_global_json(&runtime, "done").await;
assert_eq!(
done,
serde_json::json!(true),
"async loop should have completed"
);
});
}
#[test]
fn pijs_stream_error_rejects_async_iterator() {
futures::executor::block_on(async {
let runtime = PiJsRuntime::with_clock(DeterministicClock::new(0))
.await
.expect("create runtime");
runtime
.eval(
r#"
globalThis.chunks = [];
globalThis.errMsg = null;
(async () => {
try {
const stream = pi.exec("fail", [], { stream: true });
for await (const chunk of stream) {
globalThis.chunks.push(chunk);
}
} catch (e) {
globalThis.errMsg = e.message;
}
})();
"#,
)
.await
.expect("eval");
let requests = runtime.drain_hostcall_requests();
let call_id = requests[0].call_id.clone();
// Send one good chunk
runtime.complete_hostcall(
call_id.clone(),
HostcallOutcome::StreamChunk {
sequence: 0,
chunk: serde_json::json!("first"),
is_final: false,
},
);
runtime.tick().await.expect("tick chunk 0");
// Now error the hostcall
runtime.complete_hostcall(
call_id,
HostcallOutcome::Error {
code: "STREAM_ERR".into(),
message: "broken pipe".into(),
},
);
runtime.tick().await.expect("tick error");
runtime.tick().await.expect("tick settle");
let chunks = get_global_json(&runtime, "chunks").await;
assert_eq!(
chunks.as_array().expect("array").len(),
1,
"should have received 1 chunk before error"
);
let err = get_global_json(&runtime, "errMsg").await;
assert_eq!(err, serde_json::json!("broken pipe"));
});
}
#[test]
fn pijs_stream_http_returns_async_iterator() {
futures::executor::block_on(async {
let runtime = PiJsRuntime::with_clock(DeterministicClock::new(0))
.await
.expect("create runtime");
runtime
.eval(
r#"
globalThis.chunks = [];
globalThis.done = false;
(async () => {
const stream = pi.http({ url: "http://example.com", stream: true });
for await (const chunk of stream) {
globalThis.chunks.push(chunk);
}
globalThis.done = true;
})();
"#,
)
.await
.expect("eval");
let requests = runtime.drain_hostcall_requests();
assert_eq!(requests.len(), 1);
let call_id = requests[0].call_id.clone();
// Two chunks: non-final then final
runtime.complete_hostcall(
call_id.clone(),
HostcallOutcome::StreamChunk {
sequence: 0,
chunk: serde_json::json!("chunk-a"),
is_final: false,
},
);
runtime.tick().await.expect("tick a");
runtime.complete_hostcall(
call_id,
HostcallOutcome::StreamChunk {
sequence: 1,
chunk: serde_json::json!("chunk-b"),
is_final: true,
},
);
runtime.tick().await.expect("tick b");
runtime.tick().await.expect("tick settle");
let chunks = get_global_json(&runtime, "chunks").await;
let arr = chunks.as_array().expect("array");
assert_eq!(arr.len(), 2);
assert_eq!(arr[0], serde_json::json!("chunk-a"));
assert_eq!(arr[1], serde_json::json!("chunk-b"));
assert_eq!(
get_global_json(&runtime, "done").await,
serde_json::json!(true)
);
});
}
#[test]
#[allow(clippy::too_many_lines)]
fn pijs_stream_concurrent_exec_calls_have_independent_lifecycle() {
futures::executor::block_on(async {
let runtime = PiJsRuntime::with_clock(DeterministicClock::new(0))
.await
.expect("create runtime");
runtime
.eval(
r#"
globalThis.streamA = [];
globalThis.streamB = [];
globalThis.doneA = false;
globalThis.doneB = false;
(async () => {
const stream = pi.exec("cmd-a", [], { stream: true });
for await (const chunk of stream) {
globalThis.streamA.push(chunk);
}
globalThis.doneA = true;
})();
(async () => {
const stream = pi.exec("cmd-b", [], { stream: true });
for await (const chunk of stream) {
globalThis.streamB.push(chunk);
}
globalThis.doneB = true;
})();
"#,
)
.await
.expect("eval");
let requests = runtime.drain_hostcall_requests();
assert_eq!(requests.len(), 2, "expected two streaming exec requests");
let mut call_a: Option<String> = None;
let mut call_b: Option<String> = None;
for request in &requests {
match &request.kind {
HostcallKind::Exec { cmd } if cmd == "cmd-a" => {
call_a = Some(request.call_id.clone());
}
HostcallKind::Exec { cmd } if cmd == "cmd-b" => {
call_b = Some(request.call_id.clone());
}
_ => {}
}
}
let call_a = call_a.expect("call_id for cmd-a");
let call_b = call_b.expect("call_id for cmd-b");
assert_ne!(call_a, call_b, "concurrent calls must have distinct ids");
assert_eq!(runtime.pending_hostcall_count(), 2);
runtime.complete_hostcall(
call_a.clone(),
HostcallOutcome::StreamChunk {
sequence: 0,
chunk: serde_json::json!("a0"),
is_final: false,
},
);
runtime.tick().await.expect("tick a0");
runtime.complete_hostcall(
call_b.clone(),
HostcallOutcome::StreamChunk {
sequence: 0,
chunk: serde_json::json!("b0"),
is_final: false,
},
);
runtime.tick().await.expect("tick b0");
assert_eq!(runtime.pending_hostcall_count(), 2);
runtime.complete_hostcall(
call_b.clone(),
HostcallOutcome::StreamChunk {
sequence: 1,
chunk: serde_json::json!("b1"),
is_final: true,
},
);
runtime.tick().await.expect("tick b1");
assert_eq!(runtime.pending_hostcall_count(), 1);
assert!(runtime.is_hostcall_pending(&call_a));
assert!(!runtime.is_hostcall_pending(&call_b));
runtime.complete_hostcall(
call_a.clone(),
HostcallOutcome::StreamChunk {
sequence: 1,
chunk: serde_json::json!("a1"),
is_final: true,
},
);
runtime.tick().await.expect("tick a1");
assert_eq!(runtime.pending_hostcall_count(), 0);
assert!(!runtime.is_hostcall_pending(&call_a));
runtime.tick().await.expect("tick settle 1");
runtime.tick().await.expect("tick settle 2");
let stream_a = get_global_json(&runtime, "streamA").await;
let stream_b = get_global_json(&runtime, "streamB").await;
assert_eq!(
stream_a.as_array().expect("streamA array"),
&vec![serde_json::json!("a0"), serde_json::json!("a1")]
);
assert_eq!(
stream_b.as_array().expect("streamB array"),
&vec![serde_json::json!("b0"), serde_json::json!("b1")]
);
assert_eq!(
get_global_json(&runtime, "doneA").await,
serde_json::json!(true)
);
assert_eq!(
get_global_json(&runtime, "doneB").await,
serde_json::json!(true)
);
});
}
#[test]
fn pijs_stream_chunk_ignored_after_hostcall_completed() {
futures::executor::block_on(async {
let runtime = PiJsRuntime::with_clock(DeterministicClock::new(0))
.await
.expect("create runtime");
runtime
.eval(
r#"
globalThis.result = null;
pi.tool("read", { path: "test.txt" }).then(r => {
globalThis.result = r;
});
"#,
)
.await
.expect("eval");
let requests = runtime.drain_hostcall_requests();
let call_id = requests[0].call_id.clone();
// Complete normally first
runtime.complete_hostcall(
call_id.clone(),
HostcallOutcome::Success(serde_json::json!({ "content": "done" })),
);
runtime.tick().await.expect("tick success");
// Now try to deliver a stream chunk to the same call_id — should be ignored
runtime.complete_hostcall(
call_id,
HostcallOutcome::StreamChunk {
sequence: 0,
chunk: serde_json::json!("stale"),
is_final: false,
},
);
// This should not panic
let stats = runtime.tick().await.expect("tick stale chunk");
assert!(stats.ran_macrotask, "macrotask should run (and be ignored)");
let result = get_global_json(&runtime, "result").await;
assert_eq!(result["content"], serde_json::json!("done"));
});
}
// ── node:child_process sync tests ──────────────────────────────────
#[test]
fn pijs_exec_sync_denied_by_default_security_policy() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = PiJsRuntime::with_clock(Arc::clone(&clock))
.await
.expect("create runtime");
runtime
.eval(
r"
globalThis.syncDenied = {};
import('node:child_process').then(({ execSync }) => {
try {
execSync('echo should-not-run');
globalThis.syncDenied.threw = false;
} catch (e) {
globalThis.syncDenied.threw = true;
globalThis.syncDenied.msg = String((e && e.message) || e || '');
}
globalThis.syncDenied.done = true;
});
",
)
.await
.expect("eval execSync deny");
let r = get_global_json(&runtime, "syncDenied").await;
assert_eq!(r["done"], serde_json::json!(true));
assert_eq!(r["threw"], serde_json::json!(true));
assert!(
r["msg"]
.as_str()
.unwrap_or("")
.contains("disabled by default"),
"unexpected denial message: {}",
r["msg"]
);
});
}
#[test]
fn pijs_exec_sync_enforces_exec_mediation_for_critical_commands() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let config = PiJsRuntimeConfig {
allow_unsafe_sync_exec: true,
..PiJsRuntimeConfig::default()
};
let policy = crate::extensions::PolicyProfile::Permissive.to_policy();
let runtime = PiJsRuntime::with_clock_and_config_with_policy(
Arc::clone(&clock),
config,
Some(policy),
)
.await
.expect("create runtime");
runtime
.eval(
r"
globalThis.syncMediation = {};
import('node:child_process').then(({ execSync }) => {
try {
execSync('dd if=/dev/zero of=/dev/null count=1');
globalThis.syncMediation.threw = false;
} catch (e) {
globalThis.syncMediation.threw = true;
globalThis.syncMediation.msg = String((e && e.message) || e || '');
}
globalThis.syncMediation.done = true;
});
",
)
.await
.expect("eval execSync mediation");
let r = get_global_json(&runtime, "syncMediation").await;
assert_eq!(r["done"], serde_json::json!(true));
assert_eq!(r["threw"], serde_json::json!(true));
assert!(
r["msg"].as_str().unwrap_or("").contains("exec mediation"),
"unexpected mediation denial message: {}",
r["msg"]
);
});
}
#[test]
fn pijs_exec_sync_runs_command_and_returns_stdout() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = runtime_with_sync_exec_enabled(Arc::clone(&clock)).await;
runtime
.eval(
r"
globalThis.syncResult = {};
import('node:child_process').then(({ execSync }) => {
try {
const output = execSync('echo hello-sync');
globalThis.syncResult.stdout = output.trim();
globalThis.syncResult.done = true;
} catch (e) {
globalThis.syncResult.error = String(e);
globalThis.syncResult.stack = e.stack || '';
globalThis.syncResult.done = false;
}
}).catch(e => {
globalThis.syncResult.promiseError = String(e);
});
",
)
.await
.expect("eval execSync test");
let r = get_global_json(&runtime, "syncResult").await;
assert!(
r["done"] == serde_json::json!(true),
"execSync test failed: error={}, stack={}, promiseError={}",
r["error"],
r["stack"],
r["promiseError"]
);
assert_eq!(r["stdout"], serde_json::json!("hello-sync"));
});
}
#[test]
fn pijs_exec_sync_throws_on_nonzero_exit() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = runtime_with_sync_exec_enabled(Arc::clone(&clock)).await;
runtime
.eval(
r"
globalThis.syncErr = {};
import('node:child_process').then(({ execSync }) => {
try {
execSync('exit 42');
globalThis.syncErr.threw = false;
} catch (e) {
globalThis.syncErr.threw = true;
globalThis.syncErr.status = e.status;
globalThis.syncErr.hasStderr = typeof e.stderr === 'string';
}
globalThis.syncErr.done = true;
});
",
)
.await
.expect("eval execSync nonzero");
let r = get_global_json(&runtime, "syncErr").await;
assert_eq!(r["done"], serde_json::json!(true));
assert_eq!(r["threw"], serde_json::json!(true));
// Status is a JS number (always f64 in QuickJS), so compare as f64
assert_eq!(r["status"].as_f64(), Some(42.0));
assert_eq!(r["hasStderr"], serde_json::json!(true));
});
}
#[test]
fn pijs_exec_sync_empty_command_throws() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = runtime_with_sync_exec_enabled(Arc::clone(&clock)).await;
runtime
.eval(
r"
globalThis.emptyResult = {};
import('node:child_process').then(({ execSync }) => {
try {
execSync('');
globalThis.emptyResult.threw = false;
} catch (e) {
globalThis.emptyResult.threw = true;
globalThis.emptyResult.msg = e.message;
}
globalThis.emptyResult.done = true;
});
",
)
.await
.expect("eval execSync empty");
let r = get_global_json(&runtime, "emptyResult").await;
assert_eq!(r["done"], serde_json::json!(true));
assert_eq!(r["threw"], serde_json::json!(true));
assert!(
r["msg"]
.as_str()
.unwrap_or("")
.contains("command is required")
);
});
}
#[test]
fn pijs_spawn_sync_returns_result_object() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = runtime_with_sync_exec_enabled(Arc::clone(&clock)).await;
runtime
.eval(
r"
globalThis.spawnSyncResult = {};
import('node:child_process').then(({ spawnSync }) => {
const r = spawnSync('echo', ['spawn-test']);
globalThis.spawnSyncResult.stdout = r.stdout.trim();
globalThis.spawnSyncResult.status = r.status;
globalThis.spawnSyncResult.hasOutput = Array.isArray(r.output);
globalThis.spawnSyncResult.noError = r.error === undefined;
globalThis.spawnSyncResult.done = true;
});
",
)
.await
.expect("eval spawnSync test");
let r = get_global_json(&runtime, "spawnSyncResult").await;
assert_eq!(r["done"], serde_json::json!(true));
assert_eq!(r["stdout"], serde_json::json!("spawn-test"));
assert_eq!(r["status"].as_f64(), Some(0.0));
assert_eq!(r["hasOutput"], serde_json::json!(true));
assert_eq!(r["noError"], serde_json::json!(true));
});
}
#[test]
fn pijs_spawn_sync_captures_nonzero_exit() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = runtime_with_sync_exec_enabled(Arc::clone(&clock)).await;
runtime
.eval(
r"
globalThis.spawnSyncFail = {};
import('node:child_process').then(({ spawnSync }) => {
const r = spawnSync('sh', ['-c', 'exit 7']);
globalThis.spawnSyncFail.status = r.status;
globalThis.spawnSyncFail.signal = r.signal;
globalThis.spawnSyncFail.done = true;
});
",
)
.await
.expect("eval spawnSync fail");
let r = get_global_json(&runtime, "spawnSyncFail").await;
assert_eq!(r["done"], serde_json::json!(true));
assert_eq!(r["status"].as_f64(), Some(7.0));
assert_eq!(r["signal"], serde_json::json!(null));
});
}
#[test]
fn pijs_spawn_sync_bad_command_returns_error() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = runtime_with_sync_exec_enabled(Arc::clone(&clock)).await;
runtime
.eval(
r"
globalThis.badCmd = {};
import('node:child_process').then(({ spawnSync }) => {
const r = spawnSync('__nonexistent_binary_xyzzy__');
globalThis.badCmd.hasError = r.error !== undefined;
globalThis.badCmd.statusNull = r.status === null;
globalThis.badCmd.done = true;
});
",
)
.await
.expect("eval spawnSync bad cmd");
let r = get_global_json(&runtime, "badCmd").await;
assert_eq!(r["done"], serde_json::json!(true));
assert_eq!(r["hasError"], serde_json::json!(true));
assert_eq!(r["statusNull"], serde_json::json!(true));
});
}
#[test]
fn pijs_exec_file_sync_runs_binary_directly() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = runtime_with_sync_exec_enabled(Arc::clone(&clock)).await;
runtime
.eval(
r"
globalThis.execFileResult = {};
import('node:child_process').then(({ execFileSync }) => {
const output = execFileSync('echo', ['file-sync-test']);
globalThis.execFileResult.stdout = output.trim();
globalThis.execFileResult.done = true;
});
",
)
.await
.expect("eval execFileSync test");
let r = get_global_json(&runtime, "execFileResult").await;
assert_eq!(r["done"], serde_json::json!(true));
assert_eq!(r["stdout"], serde_json::json!("file-sync-test"));
});
}
#[test]
fn pijs_exec_sync_captures_stderr() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = runtime_with_sync_exec_enabled(Arc::clone(&clock)).await;
runtime
.eval(
r"
globalThis.stderrResult = {};
import('node:child_process').then(({ execSync }) => {
try {
execSync('echo err-msg >&2 && exit 1');
globalThis.stderrResult.threw = false;
} catch (e) {
globalThis.stderrResult.threw = true;
globalThis.stderrResult.stderr = e.stderr.trim();
}
globalThis.stderrResult.done = true;
});
",
)
.await
.expect("eval execSync stderr");
let r = get_global_json(&runtime, "stderrResult").await;
assert_eq!(r["done"], serde_json::json!(true));
assert_eq!(r["threw"], serde_json::json!(true));
assert_eq!(r["stderr"], serde_json::json!("err-msg"));
});
}
#[test]
#[cfg(unix)]
fn pijs_exec_sync_with_cwd_option() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = runtime_with_sync_exec_enabled(Arc::clone(&clock)).await;
runtime
.eval(
r"
globalThis.cwdResult = {};
import('node:child_process').then(({ execSync }) => {
const output = execSync('pwd', { cwd: '/tmp' });
globalThis.cwdResult.dir = output.trim();
globalThis.cwdResult.done = true;
});
",
)
.await
.expect("eval execSync cwd");
let r = get_global_json(&runtime, "cwdResult").await;
assert_eq!(r["done"], serde_json::json!(true));
// /tmp may resolve to /private/tmp on macOS
let dir = r["dir"].as_str().unwrap_or("");
assert!(
dir == "/tmp" || dir.ends_with("/tmp"),
"expected /tmp, got: {dir}"
);
});
}
#[test]
fn pijs_spawn_sync_empty_command_throws() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = runtime_with_sync_exec_enabled(Arc::clone(&clock)).await;
runtime
.eval(
r"
globalThis.emptySpawn = {};
import('node:child_process').then(({ spawnSync }) => {
try {
spawnSync('');
globalThis.emptySpawn.threw = false;
} catch (e) {
globalThis.emptySpawn.threw = true;
globalThis.emptySpawn.msg = e.message;
}
globalThis.emptySpawn.done = true;
});
",
)
.await
.expect("eval spawnSync empty");
let r = get_global_json(&runtime, "emptySpawn").await;
assert_eq!(r["done"], serde_json::json!(true));
assert_eq!(r["threw"], serde_json::json!(true));
assert!(
r["msg"]
.as_str()
.unwrap_or("")
.contains("command is required")
);
});
}
#[test]
#[cfg(unix)]
fn pijs_spawn_sync_options_as_second_arg() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = runtime_with_sync_exec_enabled(Arc::clone(&clock)).await;
// spawnSync(cmd, options) with no args array — options is 2nd param
runtime
.eval(
r"
globalThis.optsResult = {};
import('node:child_process').then(({ spawnSync }) => {
const r = spawnSync('pwd', { cwd: '/tmp' });
globalThis.optsResult.stdout = r.stdout.trim();
globalThis.optsResult.done = true;
});
",
)
.await
.expect("eval spawnSync opts as 2nd arg");
let r = get_global_json(&runtime, "optsResult").await;
assert_eq!(r["done"], serde_json::json!(true));
let stdout = r["stdout"].as_str().unwrap_or("");
assert!(
stdout == "/tmp" || stdout.ends_with("/tmp"),
"expected /tmp, got: {stdout}"
);
});
}
// ── node:os expanded API tests ─────────────────────────────────────
#[test]
fn pijs_os_expanded_apis() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = PiJsRuntime::with_clock(Arc::clone(&clock))
.await
.expect("create runtime");
runtime
.eval(
r"
globalThis.osEx = {};
import('node:os').then((os) => {
const cpuArr = os.cpus();
globalThis.osEx.cpusIsArray = Array.isArray(cpuArr);
globalThis.osEx.cpusLen = cpuArr.length;
globalThis.osEx.cpuHasModel = typeof cpuArr[0].model === 'string';
globalThis.osEx.cpuHasSpeed = typeof cpuArr[0].speed === 'number';
globalThis.osEx.cpuHasTimes = typeof cpuArr[0].times === 'object';
globalThis.osEx.totalmem = os.totalmem();
globalThis.osEx.totalMemPositive = os.totalmem() > 0;
globalThis.osEx.freeMemPositive = os.freemem() > 0;
globalThis.osEx.freeMemLessTotal = os.freemem() <= os.totalmem();
globalThis.osEx.uptimePositive = os.uptime() > 0;
const la = os.loadavg();
globalThis.osEx.loadavgIsArray = Array.isArray(la);
globalThis.osEx.loadavgLen = la.length;
globalThis.osEx.networkInterfacesIsObj = typeof os.networkInterfaces() === 'object';
const ui = os.userInfo();
globalThis.osEx.userInfoHasUid = typeof ui.uid === 'number';
globalThis.osEx.userInfoHasUsername = typeof ui.username === 'string';
globalThis.osEx.userInfoHasHomedir = typeof ui.homedir === 'string';
globalThis.osEx.userInfoHasShell = typeof ui.shell === 'string';
globalThis.osEx.endianness = os.endianness();
globalThis.osEx.eol = os.EOL;
globalThis.osEx.devNull = os.devNull;
globalThis.osEx.hasConstants = typeof os.constants === 'object';
globalThis.osEx.done = true;
});
",
)
.await
.expect("eval node:os expanded");
let r = get_global_json(&runtime, "osEx").await;
assert_eq!(r["done"], serde_json::json!(true));
// cpus()
assert_eq!(r["cpusIsArray"], serde_json::json!(true));
assert!(r["cpusLen"].as_f64().unwrap_or(0.0) >= 1.0);
assert_eq!(r["cpuHasModel"], serde_json::json!(true));
assert_eq!(r["cpuHasSpeed"], serde_json::json!(true));
assert_eq!(r["cpuHasTimes"], serde_json::json!(true));
// totalmem/freemem
assert_eq!(r["totalMemPositive"], serde_json::json!(true));
assert_eq!(r["freeMemPositive"], serde_json::json!(true));
assert_eq!(r["freeMemLessTotal"], serde_json::json!(true));
// uptime
assert_eq!(r["uptimePositive"], serde_json::json!(true));
// loadavg
assert_eq!(r["loadavgIsArray"], serde_json::json!(true));
assert_eq!(r["loadavgLen"].as_f64(), Some(3.0));
// networkInterfaces
assert_eq!(r["networkInterfacesIsObj"], serde_json::json!(true));
// userInfo
assert_eq!(r["userInfoHasUid"], serde_json::json!(true));
assert_eq!(r["userInfoHasUsername"], serde_json::json!(true));
assert_eq!(r["userInfoHasHomedir"], serde_json::json!(true));
assert_eq!(r["userInfoHasShell"], serde_json::json!(true));
// endianness / EOL / devNull / constants
assert_eq!(r["endianness"], serde_json::json!("LE"));
let expected_eol = if cfg!(windows) { "\r\n" } else { "\n" };
assert_eq!(r["eol"], serde_json::json!(expected_eol));
let expected_dev_null = if cfg!(windows) {
"\\\\.\\NUL"
} else {
"/dev/null"
};
assert_eq!(r["devNull"], serde_json::json!(expected_dev_null));
assert_eq!(r["hasConstants"], serde_json::json!(true));
});
}
// ── Buffer expanded API tests ──────────────────────────────────────
#[test]
fn pijs_buffer_expanded_apis() {
futures::executor::block_on(async {
let clock = Arc::new(DeterministicClock::new(0));
let runtime = PiJsRuntime::with_clock(Arc::clone(&clock))
.await
.expect("create runtime");
runtime
.eval(
r"
globalThis.bufResult = {};
(() => {
const B = globalThis.Buffer;
// alloc
const a = B.alloc(4, 0xAB);
globalThis.bufResult.allocFill = Array.from(a);
// from string + hex encoding
const hex = B.from('48656c6c6f', 'hex');
globalThis.bufResult.hexDecode = hex.toString('utf8');
// concat
const c = B.concat([B.from('Hello'), B.from(' World')]);
globalThis.bufResult.concat = c.toString();
// byteLength
globalThis.bufResult.byteLength = B.byteLength('Hello');
// compare
globalThis.bufResult.compareEqual = B.compare(B.from('abc'), B.from('abc'));
globalThis.bufResult.compareLess = B.compare(B.from('abc'), B.from('abd'));
globalThis.bufResult.compareGreater = B.compare(B.from('abd'), B.from('abc'));
// isEncoding
globalThis.bufResult.isEncodingUtf8 = B.isEncoding('utf8');
globalThis.bufResult.isEncodingFake = B.isEncoding('fake');
// isBuffer
globalThis.bufResult.isBufferTrue = B.isBuffer(B.from('x'));
globalThis.bufResult.isBufferFalse = B.isBuffer('x');
// instance methods
const b = B.from('Hello World');
globalThis.bufResult.indexOf = b.indexOf('World');
globalThis.bufResult.includes = b.includes('World');
globalThis.bufResult.notIncludes = b.includes('xyz');
const sliced = b.slice(0, 5);
globalThis.bufResult.slice = sliced.toString();
globalThis.bufResult.toJSON = b.toJSON().type;
const eq1 = B.from('abc');
const eq2 = B.from('abc');
const eq3 = B.from('xyz');
globalThis.bufResult.equalsTrue = eq1.equals(eq2);
globalThis.bufResult.equalsFalse = eq1.equals(eq3);
// copy
const src = B.from('Hello');
const dst = B.alloc(5);
src.copy(dst);
globalThis.bufResult.copy = dst.toString();
// write
const wb = B.alloc(10);
wb.write('Hi');
globalThis.bufResult.write = wb.toString('utf8', 0, 2);
// readUInt / writeUInt
const nb = B.alloc(4);
nb.writeUInt16BE(0x1234, 0);
globalThis.bufResult.readUInt16BE = nb.readUInt16BE(0);
nb.writeUInt32LE(0xDEADBEEF, 0);
globalThis.bufResult.readUInt32LE = nb.readUInt32LE(0);
// hex encoding
const hb = B.from([0xDE, 0xAD]);
globalThis.bufResult.toHex = hb.toString('hex');
// base64 round-trip
const b64 = B.from('Hello').toString('base64');
const roundTrip = B.from(b64, 'base64').toString();
globalThis.bufResult.base64Round = roundTrip;
globalThis.bufResult.done = true;
})();
",
)
.await
.expect("eval Buffer expanded");
let r = get_global_json(&runtime, "bufResult").await;
assert_eq!(r["done"], serde_json::json!(true));
// alloc with fill
assert_eq!(r["allocFill"], serde_json::json!([0xAB, 0xAB, 0xAB, 0xAB]));
// hex decode
assert_eq!(r["hexDecode"], serde_json::json!("Hello"));
// concat
assert_eq!(r["concat"], serde_json::json!("Hello World"));
// byteLength
assert_eq!(r["byteLength"].as_f64(), Some(5.0));
// compare
assert_eq!(r["compareEqual"].as_f64(), Some(0.0));
assert!(r["compareLess"].as_f64().unwrap_or(0.0) < 0.0);
assert!(r["compareGreater"].as_f64().unwrap_or(0.0) > 0.0);
// isEncoding
assert_eq!(r["isEncodingUtf8"], serde_json::json!(true));
assert_eq!(r["isEncodingFake"], serde_json::json!(false));
// isBuffer
assert_eq!(r["isBufferTrue"], serde_json::json!(true));
assert_eq!(r["isBufferFalse"], serde_json::json!(false));
// indexOf / includes
assert_eq!(r["indexOf"].as_f64(), Some(6.0));
assert_eq!(r["includes"], serde_json::json!(true));
assert_eq!(r["notIncludes"], serde_json::json!(false));
// slice
assert_eq!(r["slice"], serde_json::json!("Hello"));
// toJSON
assert_eq!(r["toJSON"], serde_json::json!("Buffer"));
// equals
assert_eq!(r["equalsTrue"], serde_json::json!(true));
assert_eq!(r["equalsFalse"], serde_json::json!(false));
// copy
assert_eq!(r["copy"], serde_json::json!("Hello"));
// write
assert_eq!(r["write"], serde_json::json!("Hi"));
// readUInt16BE
assert_eq!(r["readUInt16BE"].as_f64(), Some(f64::from(0x1234)));
// readUInt32LE
assert_eq!(r["readUInt32LE"].as_f64(), Some(f64::from(0xDEAD_BEEF_u32)));
// hex
assert_eq!(r["toHex"], serde_json::json!("dead"));
// base64 round-trip
assert_eq!(r["base64Round"], serde_json::json!("Hello"));
});
}
}