fix(runner): load sealed Node entrypoints as ESM

This commit is contained in:
Dotta 2026-09-02 18:13:54 -05:00
parent 8d36deaa72
commit 96fcadeb51
3 changed files with 36 additions and 24 deletions

View File

@ -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::<Result<Vec<_>, _>>()?;
// 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(_))
));

View File

@ -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();

View File

@ -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,