From 96fcadeb5178ac38b39767ed7a45e2f77230993a Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 2 Sep 2026 18:13:54 -0500 Subject: [PATCH] fix(runner): load sealed Node entrypoints as ESM --- .../runner-core/src/acpx_provider_backend.rs | 21 +++++++------- .../crates/runner-core/src/codex_provider.rs | 29 ++++++++++--------- .../runner-core/src/process_supervisor.rs | 10 ++++++- 3 files changed, 36 insertions(+), 24 deletions(-) diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_backend.rs b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_backend.rs index 6d21be5b56..e4ecef1f6c 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_backend.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_backend.rs @@ -24,8 +24,8 @@ use crate::durable::{ DurableRunnerError, EventPriority, PolledEvent, }; use crate::process_supervisor::{ - is_node_interpreter, VerifiedProcessArgument, VerifiedProcessLaunch, - VERIFIED_NODE_ESM_DEFAULT_TYPE_ARG, + is_node_interpreter, verified_node_esm_loader_arguments, VerifiedProcessArgument, + VerifiedProcessLaunch, VERIFIED_NODE_ESM_LOADER, VERIFIED_NODE_EVAL_ARG, }; use crate::provider_bridge::{ authorized_tool_catalog_digest, AuthorizedToolSet, ToolResult, TOOL_SET_SCHEMA, @@ -234,14 +234,10 @@ impl AcpxProviderDescriptor { .collect::, _>>()?; // Verified scripts are exposed to Node through an immutable descriptor // path (for example /proc/self/fd/12), which intentionally has no .js - // suffix. Pin ESM interpretation so Node does not misclassify the - // bundled sidecar as CommonJS merely because its verified path is - // extensionless. + // suffix. A runner-owned eval loader reads that exact sealed descriptor + // and imports it as ESM without mutating the qualified launch artifact. if is_node_interpreter(&launch_profile.command) { - verified_args.insert( - 0, - VerifiedProcessArgument::Literal(VERIFIED_NODE_ESM_DEFAULT_TYPE_ARG.to_owned()), - ); + verified_args.splice(0..0, verified_node_esm_loader_arguments()); } Ok(AcpxSidecarTransportConfig { command: launch_profile.command.clone(), @@ -1434,10 +1430,15 @@ mod tests { assert!(matches!( verified_launch.arguments().first(), Some(VerifiedProcessArgument::Literal(argument)) - if argument == VERIFIED_NODE_ESM_DEFAULT_TYPE_ARG + if argument == VERIFIED_NODE_EVAL_ARG )); assert!(matches!( verified_launch.arguments().get(1), + Some(VerifiedProcessArgument::Literal(argument)) + if argument == VERIFIED_NODE_ESM_LOADER + )); + assert!(matches!( + verified_launch.arguments().get(2), Some(VerifiedProcessArgument::Artifact(_)) )); diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/codex_provider.rs b/packages/paperclip-runner/runner/crates/runner-core/src/codex_provider.rs index aa37354814..cded391568 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/codex_provider.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/codex_provider.rs @@ -15,8 +15,9 @@ use crate::durable::QualifiedLaunchArtifact; use crate::durable::{redact_text, OpenCodeLaunchProfile}; use crate::local_runner::LocalRunnerError; use crate::process_supervisor::{ - is_node_interpreter, BoundedLogBuffer, ProcessOutput, SupervisedProcess, - VerifiedProcessArgument, VerifiedProcessLaunch, VERIFIED_NODE_ESM_DEFAULT_TYPE_ARG, + is_node_interpreter, verified_node_esm_loader_arguments, BoundedLogBuffer, ProcessOutput, + SupervisedProcess, VerifiedProcessArgument, VerifiedProcessLaunch, VERIFIED_NODE_ESM_LOADER, + VERIFIED_NODE_EVAL_ARG, }; use crate::provider_bridge::{AuthorizedTool, DurableReplayFilter, ToolResult}; use crate::provider_events::normalized_codex_terminal_event_type; @@ -1954,14 +1955,11 @@ fn verified_opencode_launch( VerifiedProcessArgument::ExecutableArtifact(executable), ]; if is_node_interpreter(&profile.command.path) { - // The immutable verified proxy path is extensionless, so explicitly - // retain the ESM semantics of the bundled .js artifact instead of - // letting Node infer CommonJS. Qualified non-Node test/provider - // commands retain their original argument contract. - args.insert( - 0, - VerifiedProcessArgument::Literal(VERIFIED_NODE_ESM_DEFAULT_TYPE_ARG.to_owned()), - ); + // The immutable verified proxy path is extensionless. Load its exact + // sealed bytes as an ESM data URL while leaving process.argv[1] bound + // to that descriptor, so the proxy retains its ordinary CLI contract. + // Qualified non-Node test/provider commands retain their arguments. + args.splice(0..0, verified_node_esm_loader_arguments()); } Ok(VerifiedProcessLaunch::new(command, args)) } @@ -2711,19 +2709,24 @@ mod tests { assert!(matches!( launch.arguments().first(), Some(VerifiedProcessArgument::Literal(argument)) - if argument == VERIFIED_NODE_ESM_DEFAULT_TYPE_ARG + if argument == VERIFIED_NODE_EVAL_ARG )); assert!(matches!( launch.arguments().get(1), - Some(VerifiedProcessArgument::Artifact(_)) + Some(VerifiedProcessArgument::Literal(argument)) + if argument == VERIFIED_NODE_ESM_LOADER )); assert!(matches!( launch.arguments().get(2), + Some(VerifiedProcessArgument::Artifact(_)) + )); + assert!(matches!( + launch.arguments().get(3), Some(VerifiedProcessArgument::Literal(argument)) if argument == TRUSTED_OPENCODE_EXECUTABLE_ARG )); assert!(matches!( - launch.arguments().get(3), + launch.arguments().get(4), Some(VerifiedProcessArgument::ExecutableArtifact(_)) )); fs::remove_dir_all(directory).unwrap(); diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/process_supervisor.rs b/packages/paperclip-runner/runner/crates/runner-core/src/process_supervisor.rs index 9e73e8e42c..415c9473ec 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/process_supervisor.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/process_supervisor.rs @@ -26,7 +26,8 @@ use sha2::{Digest, Sha256}; use crate::local_runner::LocalRunnerError; const PROCESS_OUTPUT_QUEUE_CAPACITY: usize = 256; -pub(crate) const VERIFIED_NODE_ESM_DEFAULT_TYPE_ARG: &str = "--experimental-default-type=module"; +pub(crate) const VERIFIED_NODE_EVAL_ARG: &str = "--eval"; +pub(crate) const VERIFIED_NODE_ESM_LOADER: &str = r#"const fs=require("node:fs");const source=fs.readFileSync(process.argv[1]);import("data:text/javascript;base64,"+source.toString("base64")).catch((error)=>{console.error(error instanceof Error?error.message:String(error));process.exitCode=1;});"#; pub(crate) fn is_node_interpreter(path: &Path) -> bool { path.file_name() @@ -195,6 +196,13 @@ pub enum VerifiedProcessArgument { ExecutableArtifact(VerifiedProcessArtifact), } +pub(crate) fn verified_node_esm_loader_arguments() -> [VerifiedProcessArgument; 2] { + [ + VerifiedProcessArgument::Literal(VERIFIED_NODE_EVAL_ARG.to_owned()), + VerifiedProcessArgument::Literal(VERIFIED_NODE_ESM_LOADER.to_owned()), + ] +} + #[derive(Clone, Debug)] pub struct VerifiedProcessLaunch { program: VerifiedProcessArtifact,