fix(runner): restore verified JS provider startup

This commit is contained in:
Dotta 2026-09-02 18:32:59 -05:00
parent 96fcadeb51
commit 62b79deb20
13 changed files with 164 additions and 121 deletions

View File

@ -23,10 +23,7 @@ use crate::durable::{
AcpxLaunchProfile, Command, CommandExecution, CommandExecutor, DurableRunnerConfig,
DurableRunnerError, EventPriority, PolledEvent,
};
use crate::process_supervisor::{
is_node_interpreter, verified_node_esm_loader_arguments, VerifiedProcessArgument,
VerifiedProcessLaunch, VERIFIED_NODE_ESM_LOADER, VERIFIED_NODE_EVAL_ARG,
};
use crate::process_supervisor::{VerifiedProcessArgument, VerifiedProcessLaunch};
use crate::provider_bridge::{
authorized_tool_catalog_digest, AuthorizedToolSet, ToolResult, TOOL_SET_SCHEMA,
};
@ -213,7 +210,7 @@ impl AcpxProviderDescriptor {
"ACPX runner launch profile does not authenticate its command",
)
})?;
let mut verified_args = launch_profile
let verified_args = launch_profile
.args
.iter()
.map(|argument| {
@ -232,13 +229,6 @@ 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. 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.splice(0..0, verified_node_esm_loader_arguments());
}
Ok(AcpxSidecarTransportConfig {
command: launch_profile.command.clone(),
args: launch_profile.args.clone(),
@ -1409,7 +1399,7 @@ mod tests {
fn binds_sidecar_paths_arguments_and_contents_to_the_runner_profile() {
let directory = temporary_directory("launch-binding");
let command = directory.join("node");
let sidecar = directory.join("sidecar.js");
let sidecar = directory.join("sidecar.cjs");
write_artifact(&command, b"qualified node", true);
write_artifact(&sidecar, b"qualified sidecar", false);
let args = vec![sidecar.to_string_lossy().into_owned()];
@ -1429,16 +1419,6 @@ mod tests {
let verified_launch = transport.verified_launch.as_ref().unwrap();
assert!(matches!(
verified_launch.arguments().first(),
Some(VerifiedProcessArgument::Literal(argument))
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

@ -514,6 +514,9 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
.iter()
.any(|value| value == "--finish-turn-with-pending-tool");
let require_dynamic_tool = args.iter().any(|value| value == "--require-dynamic-tool");
let require_completion_contract = args
.iter()
.any(|value| value == "--require-completion-contract");
let expected_canonical_task_context = argument(&args, "--expected-canonical-task-context")
.map(|value| serde_json::from_str::<Value>(&value))
.transpose()?;
@ -766,6 +769,15 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
if require_dynamic_tool && !has_task_context_tool(&message) {
return Err("thread/start omitted the authorized dynamic tool".into());
}
if require_completion_contract
&& message.pointer("/params/completionContract")
!= Some(&json!({
"revision": "revision-1",
"criterionIds": ["criterion-1"],
}))
{
return Err("thread/start omitted the durable completion contract".into());
}
state.thread_id = "codex-thread-1".to_owned();
state.active_turn_id = None;
save_state(&state_path, &state)?;
@ -784,6 +796,15 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
if require_dynamic_tool && !has_task_context_tool(&message) {
return Err("thread/resume omitted the authorized dynamic tool".into());
}
if require_completion_contract
&& message.pointer("/params/completionContract")
!= Some(&json!({
"revision": "revision-1",
"criterionIds": ["criterion-1"],
}))
{
return Err("thread/resume omitted the durable completion contract".into());
}
let unowned_turn_marker = state_path.with_file_name("resume-unowned-turn");
if resume_unowned_turn_when_marked && unowned_turn_marker.exists() {
state.active_turn_id = Some("provider-turn-unowned".to_owned());

View File

@ -15,9 +15,8 @@ use crate::durable::QualifiedLaunchArtifact;
use crate::durable::{redact_text, OpenCodeLaunchProfile};
use crate::local_runner::LocalRunnerError;
use crate::process_supervisor::{
is_node_interpreter, verified_node_esm_loader_arguments, BoundedLogBuffer, ProcessOutput,
SupervisedProcess, VerifiedProcessArgument, VerifiedProcessLaunch, VERIFIED_NODE_ESM_LOADER,
VERIFIED_NODE_EVAL_ARG,
BoundedLogBuffer, ProcessOutput, SupervisedProcess, VerifiedProcessArgument,
VerifiedProcessLaunch,
};
use crate::provider_bridge::{AuthorizedTool, DurableReplayFilter, ToolResult};
use crate::provider_events::normalized_codex_terminal_event_type;
@ -55,6 +54,12 @@ pub(crate) const MAX_SETTLED_PROVIDER_TURN_IDS: usize = 4_096;
type QuestionOptionLabels = BTreeMap<String, BTreeMap<String, String>>;
type QuestionSetMapping = (String, Value, QuestionOptionLabels);
#[derive(Clone)]
struct ProviderCompletionContract {
revision: String,
criterion_ids: Vec<String>,
}
fn base64_encode(input: &[u8]) -> String {
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut encoded = String::with_capacity(input.len().div_ceil(3) * 4);
@ -525,6 +530,7 @@ pub struct CodexProvider {
trace: Option<ProviderTraceSink>,
last_trace_frame_id: Option<u64>,
opencode_launch_profile: Option<OpenCodeLaunchProfile>,
completion_contract: Option<ProviderCompletionContract>,
}
impl CodexProvider {
@ -532,7 +538,14 @@ impl CodexProvider {
config: &CodexProviderConfig,
resume_thread_id: Option<&str>,
) -> Result<Self, LocalRunnerError> {
Self::start_with_tools_for_generation(config, std::iter::empty(), resume_thread_id, 1, None)
Self::start_with_tools_for_generation(
config,
std::iter::empty(),
resume_thread_id,
1,
None,
None,
)
}
pub fn start_with_tools(
@ -540,7 +553,14 @@ impl CodexProvider {
authorized_tools: impl IntoIterator<Item = AuthorizedTool>,
resume_thread_id: Option<&str>,
) -> Result<Self, LocalRunnerError> {
Self::start_with_tools_for_generation(config, authorized_tools, resume_thread_id, 1, None)
Self::start_with_tools_for_generation(
config,
authorized_tools,
resume_thread_id,
1,
None,
None,
)
}
pub(crate) fn start_with_tools_for_generation(
@ -549,6 +569,7 @@ impl CodexProvider {
resume_thread_id: Option<&str>,
process_generation: u64,
opencode_launch_profile: Option<&OpenCodeLaunchProfile>,
completion_contract: Option<(&str, &[String])>,
) -> Result<Self, LocalRunnerError> {
config.validate()?;
if process_generation == 0 {
@ -649,6 +670,12 @@ impl CodexProvider {
trace: ProviderTraceSink::from_environment(),
last_trace_frame_id: None,
opencode_launch_profile: opencode_launch_profile.cloned(),
completion_contract: completion_contract.map(|(revision, criterion_ids)| {
ProviderCompletionContract {
revision: revision.to_owned(),
criterion_ids: criterion_ids.to_vec(),
}
}),
};
let initialized = provider.request(
"initialize",
@ -678,6 +705,17 @@ impl CodexProvider {
let params_object = params
.as_object_mut()
.expect("Codex thread parameters are an object");
if config.provider == "opencode" {
if let Some(contract) = provider.completion_contract.as_ref() {
params_object.insert(
"completionContract".to_owned(),
json!({
"revision": contract.revision,
"criterionIds": contract.criterion_ids,
}),
);
}
}
let method = if let Some(thread_id) = resume_thread_id {
params_object.insert("threadId".to_owned(), json!(thread_id));
"thread/resume"
@ -837,6 +875,7 @@ impl CodexProvider {
let completed_turn_authority = self.completed_turn_authority.clone();
let completion_reconciliation_pending = self.completion_reconciliation_pending;
let durable_tool_call_replays = self.durable_tool_call_replays;
let completion_contract = self.completion_contract.clone();
// Exact turn identities may be forgotten only after the provider
// process that could emit them is gone. Resume the same thread in a
@ -849,6 +888,12 @@ impl CodexProvider {
Some(&thread_id),
next_generation,
self.opencode_launch_profile.as_ref(),
completion_contract.as_ref().map(|contract| {
(
contract.revision.as_str(),
contract.criterion_ids.as_slice(),
)
}),
)?;
replacement.durable_tool_call_replays = durable_tool_call_replays;
if replacement.active_provider_turn_id.is_some() {
@ -1949,18 +1994,11 @@ fn verified_opencode_launch(
.map_err(|error| LocalRunnerError::invalid(error.to_string()))?;
let executable = verify_launch_artifact(&profile.executable, "OpenCode provider executable")
.map_err(|error| LocalRunnerError::invalid(error.to_string()))?;
let mut args = vec![
let args = vec![
VerifiedProcessArgument::Artifact(proxy),
VerifiedProcessArgument::Literal(TRUSTED_OPENCODE_EXECUTABLE_ARG.to_owned()),
VerifiedProcessArgument::ExecutableArtifact(executable),
];
if is_node_interpreter(&profile.command.path) {
// 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))
}
@ -2683,7 +2721,7 @@ mod tests {
}
#[test]
fn verified_opencode_proxy_retains_esm_semantics_for_extensionless_snapshot() {
fn verified_opencode_proxy_executes_the_descriptor_safe_commonjs_bundle() {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
@ -2694,10 +2732,10 @@ mod tests {
));
fs::create_dir_all(&directory).unwrap();
let command = directory.join("node");
let proxy = directory.join("proxy.js");
let proxy = directory.join("proxy.cjs");
let executable = directory.join("opencode");
fs::write(&command, b"qualified node").unwrap();
fs::write(&proxy, b"export {};\n").unwrap();
fs::write(&proxy, b"module.exports = {};\n").unwrap();
fs::write(&executable, b"qualified opencode").unwrap();
let profile = OpenCodeLaunchProfile {
command: qualified_artifact(&command),
@ -2708,25 +2746,15 @@ mod tests {
let launch = verified_opencode_launch(&profile).unwrap();
assert!(matches!(
launch.arguments().first(),
Some(VerifiedProcessArgument::Literal(argument))
if argument == VERIFIED_NODE_EVAL_ARG
Some(VerifiedProcessArgument::Artifact(_))
));
assert!(matches!(
launch.arguments().get(1),
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(4),
launch.arguments().get(2),
Some(VerifiedProcessArgument::ExecutableArtifact(_))
));
fs::remove_dir_all(directory).unwrap();

View File

@ -26,14 +26,6 @@ use sha2::{Digest, Sha256};
use crate::local_runner::LocalRunnerError;
const PROCESS_OUTPUT_QUEUE_CAPACITY: usize = 256;
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()
.and_then(|name| name.to_str())
.is_some_and(|name| matches!(name, "node" | "nodejs" | "node.exe"))
}
#[derive(Clone, Debug)]
pub struct VerifiedProcessArtifact {
@ -196,13 +188,6 @@ 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,

View File

@ -1001,6 +1001,12 @@ impl CodexCommandExecutor {
Some(&thread_id),
process_generation,
self.opencode_launch_profile.as_ref(),
state.completion_contract.as_ref().map(|contract| {
(
contract.revision.as_str(),
contract.criterion_ids.as_slice(),
)
}),
)
.map_err(|error| {
DurableRunnerError::invalid(format!(
@ -1395,6 +1401,12 @@ impl CodexCommandExecutor {
state.thread_id.as_deref(),
process_generation,
self.opencode_launch_profile.as_ref(),
state.completion_contract.as_ref().map(|contract| {
(
contract.revision.as_str(),
contract.criterion_ids.as_slice(),
)
}),
)
.map_err(|error| {
DurableRunnerError::invalid(format!("failed to start Codex provider: {error}"))

View File

@ -95,7 +95,7 @@ fn opencode_config(state_dir: &Path) -> DurableRunnerConfig {
fs::write(
&proxy_script,
format!(
"#!/bin/sh\nexec '{}' --state-file '{}' --call-log '{}'\n",
"#!/bin/sh\nexec '{}' --state-file '{}' --call-log '{}' --require-completion-contract\n",
env!("CARGO_BIN_EXE_fake-codex-app-server"),
state_dir.join("fake-opencode-state.json").display(),
state_dir.join("fake-opencode-calls.log").display(),

View File

@ -187,8 +187,8 @@ try {
);
}
const opencodeProxyPath = "dist/cli/opencode-app-server-proxy.js";
const acpxSidecarPath = "dist/cli/acpx-runtime-sidecar.js";
const opencodeProxyPath = "dist/cli/opencode-app-server-proxy.cjs";
const acpxSidecarPath = "dist/cli/acpx-runtime-sidecar.cjs";
const opencodeCommand = "node_modules/.bin/opencode";
const opencodeExecutable = "node_modules/opencode-ai/bin/opencode.exe";
const nodeCommand = "node_modules/node/bin/node";

View File

@ -12,11 +12,16 @@ export const verifiedProviderEntrypoints = Object.freeze([
name: "acpx-runtime-sidecar",
source: resolve(packageRoot, "src/cli/acpx-runtime-sidecar.ts"),
output: resolve(packageRoot, "dist/cli/acpx-runtime-sidecar.js"),
verifiedOutput: resolve(packageRoot, "dist/cli/acpx-runtime-sidecar.cjs"),
}),
Object.freeze({
name: "opencode-app-server-proxy",
source: resolve(packageRoot, "src/cli/opencode-app-server-proxy.ts"),
output: resolve(packageRoot, "dist/cli/opencode-app-server-proxy.js"),
verifiedOutput: resolve(
packageRoot,
"dist/cli/opencode-app-server-proxy.cjs",
),
}),
]);
@ -47,27 +52,35 @@ function assertSelfContainedBundle(entrypoint, result) {
export async function bundleVerifiedProviderEntrypoints({ write = true } = {}) {
const results = [];
for (const entrypoint of verifiedProviderEntrypoints) {
const result = await build({
entryPoints: [entrypoint.source],
outfile: entrypoint.output,
bundle: true,
platform: "node",
format: "esm",
target: "node24",
packages: "bundle",
splitting: false,
sourcemap: false,
legalComments: "none",
metafile: true,
treeShaking: true,
write,
logLevel: "silent",
});
assertSelfContainedBundle(entrypoint, result);
const buildBundle = async (outfile, format) => {
const result = await build({
entryPoints: [entrypoint.source],
outfile,
bundle: true,
platform: "node",
format,
target: "node24",
packages: "bundle",
splitting: false,
sourcemap: false,
legalComments: "none",
metafile: true,
treeShaking: true,
write,
logLevel: "silent",
});
assertSelfContainedBundle(entrypoint, result);
return result;
};
const result = await buildBundle(entrypoint.output, "esm");
const verifiedResult = await buildBundle(entrypoint.verifiedOutput, "cjs");
if (write && process.platform !== "win32") {
await chmod(entrypoint.output, 0o755);
await Promise.all([
chmod(entrypoint.output, 0o755),
chmod(entrypoint.verifiedOutput, 0o755),
]);
}
results.push({ entrypoint, result });
results.push({ entrypoint, result, verifiedResult });
}
return results;
}

View File

@ -7,13 +7,15 @@ import {
verifiedProviderEntrypoints,
} from "./build-verified-provider-entrypoints.mjs";
test("verified JS provider entrypoints bundle into one descriptor-safe file", async () => {
test("provider entrypoints include self-contained ESM and descriptor-safe CommonJS bundles", async () => {
const bundles = await bundleVerifiedProviderEntrypoints({ write: false });
assert.equal(bundles.length, verifiedProviderEntrypoints.length);
for (const { entrypoint, result } of bundles) {
assert.equal(result.outputFiles?.length, 1, entrypoint.name);
const source = result.outputFiles[0].text;
assert.match(source, /^#!\/usr\/bin\/env node\n/);
for (const { entrypoint, result, verifiedResult } of bundles) {
for (const bundle of [result, verifiedResult]) {
assert.equal(bundle.outputFiles?.length, 1, entrypoint.name);
const source = bundle.outputFiles[0].text;
assert.match(source, /^#!\/usr\/bin\/env node\n/);
}
}
});
@ -24,8 +26,10 @@ test("written provider entrypoints satisfy qualified launch permissions", async
}
await bundleVerifiedProviderEntrypoints();
for (const entrypoint of verifiedProviderEntrypoints) {
const mode = (await stat(entrypoint.output)).mode;
assert.equal(mode & 0o022, 0, entrypoint.name);
assert.notEqual(mode & 0o100, 0, entrypoint.name);
for (const output of [entrypoint.output, entrypoint.verifiedOutput]) {
const mode = (await stat(output)).mode;
assert.equal(mode & 0o022, 0, entrypoint.name);
assert.notEqual(mode & 0o100, 0, entrypoint.name);
}
}
});

View File

@ -108,7 +108,7 @@ it("rejects caller-selected local ACPX artifacts even when they are self-hashed"
}
});
it.each(["acpx-runtime-sidecar.js", "opencode-app-server-proxy.js"] as const)(
it.each(["acpx-runtime-sidecar.cjs", "opencode-app-server-proxy.cjs"] as const)(
"resolves the %s local provider artifact from verified build-owned output",
async (artifact) => {
const directory = await mkdtemp(
@ -143,12 +143,12 @@ it.each(["acpx-runtime-sidecar.js", "opencode-app-server-proxy.js"] as const)(
it("derives the ACPX package root only from the verified dist/cli layout", () => {
expect(
runnerdLaunchProfileInternals.acpxProviderPackageRoot(
"/provider-pack/dist/cli/acpx-runtime-sidecar.js",
"/provider-pack/dist/cli/acpx-runtime-sidecar.cjs",
),
).toBe("/provider-pack");
expect(() =>
runnerdLaunchProfileInternals.acpxProviderPackageRoot(
"/unverified/acpx-runtime-sidecar.js",
"/unverified/acpx-runtime-sidecar.cjs",
),
).toThrow("ACPX sidecar must use the provider package dist/cli layout");
});
@ -462,7 +462,7 @@ it.each([
runtimeContextPath: "/isolated/runtime-context.json",
hasRuntimeContext: true,
acpxSidecarPath:
"/verified/provider-pack/dist/cli/acpx-runtime-sidecar.js",
"/verified/provider-pack/dist/cli/acpx-runtime-sidecar.cjs",
});
expect(environment).toMatchObject({

View File

@ -1089,7 +1089,7 @@ function approvedRunnerArtifact(runnerBinaryPath: string): {
}
type BuildOwnedCliArtifact =
"acpx-runtime-sidecar.js" | "opencode-app-server-proxy.js";
"acpx-runtime-sidecar.cjs" | "opencode-app-server-proxy.cjs";
function buildOwnedCliArtifactCandidates(
artifact: BuildOwnedCliArtifact,
@ -1114,7 +1114,7 @@ function resolveBuildOwnedCliArtifact(
function acpxProviderPackageRoot(sidecarScript: string): string {
const cliDirectory = dirname(sidecarScript);
if (
basename(sidecarScript) !== "acpx-runtime-sidecar.js" ||
basename(sidecarScript) !== "acpx-runtime-sidecar.cjs" ||
basename(cliDirectory) !== "cli" ||
basename(dirname(cliDirectory)) !== "dist"
) {
@ -1141,7 +1141,7 @@ function acpxRunnerLaunchProfile(
if (!options.runnerFilesystemRoot) {
const buildCommand = process.execPath;
const buildSidecarCandidates = buildOwnedCliArtifactCandidates(
"acpx-runtime-sidecar.js",
"acpx-runtime-sidecar.cjs",
);
if (
options.providerNodeCommand !== undefined ||
@ -1157,7 +1157,7 @@ function acpxRunnerLaunchProfile(
);
}
const buildSidecar = resolveBuildOwnedCliArtifact(
"acpx-runtime-sidecar.js",
"acpx-runtime-sidecar.cjs",
buildSidecarCandidates,
);
if (sidecarScript !== buildSidecar) {
@ -1336,7 +1336,7 @@ export function createCapabilityRunnerdProviderEnvironment(input: {
const sidecarPath =
input.acpxSidecarPath ??
input.options.acpxSidecarPath ??
resolve(packageRoot, "dist", "cli", "acpx-runtime-sidecar.js");
resolve(packageRoot, "dist", "cli", "acpx-runtime-sidecar.cjs");
return {
...createSanitizedAcpxSpawnInput(
input.options.environment,
@ -2099,16 +2099,16 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
const opencodeProxyPath =
this.options.opencodeProxyPath ??
(provider === "opencode" && !this.options.runnerFilesystemRoot
? resolveBuildOwnedCliArtifact("opencode-app-server-proxy.js")
? resolveBuildOwnedCliArtifact("opencode-app-server-proxy.cjs")
: fileURLToPath(
new URL("../cli/opencode-app-server-proxy.js", import.meta.url),
new URL("../cli/opencode-app-server-proxy.cjs", import.meta.url),
));
const acpxSidecarPath =
this.options.acpxSidecarPath ??
(provider === "acpx" && !this.options.runnerFilesystemRoot
? resolveBuildOwnedCliArtifact("acpx-runtime-sidecar.js")
? resolveBuildOwnedCliArtifact("acpx-runtime-sidecar.cjs")
: fileURLToPath(
new URL("../cli/acpx-runtime-sidecar.js", import.meta.url),
new URL("../cli/acpx-runtime-sidecar.cjs", import.meta.url),
));
const providerNodeCommand =
this.options.providerNodeCommand ?? process.execPath;
@ -2589,16 +2589,16 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
const opencodeProxyPath =
this.options.opencodeProxyPath ??
(provider === "opencode" && !this.options.runnerFilesystemRoot
? resolveBuildOwnedCliArtifact("opencode-app-server-proxy.js")
? resolveBuildOwnedCliArtifact("opencode-app-server-proxy.cjs")
: fileURLToPath(
new URL("../cli/opencode-app-server-proxy.js", import.meta.url),
new URL("../cli/opencode-app-server-proxy.cjs", import.meta.url),
));
const acpxSidecarPath =
this.options.acpxSidecarPath ??
(provider === "acpx" && !this.options.runnerFilesystemRoot
? resolveBuildOwnedCliArtifact("acpx-runtime-sidecar.js")
? resolveBuildOwnedCliArtifact("acpx-runtime-sidecar.cjs")
: fileURLToPath(
new URL("../cli/acpx-runtime-sidecar.js", import.meta.url),
new URL("../cli/acpx-runtime-sidecar.cjs", import.meta.url),
));
const providerNodeCommand =
this.options.providerNodeCommand ?? process.execPath;

View File

@ -250,11 +250,11 @@ describe("remote provider pack manifest", () => {
const opencodeCommand = "#!/bin/sh\n";
const opencodeExecutable = "opencode-binary\n";
await writeFile(
join(root, "dist", "cli", "opencode-app-server-proxy.js"),
join(root, "dist", "cli", "opencode-app-server-proxy.cjs"),
proxy,
);
await writeFile(
join(root, "dist", "cli", "acpx-runtime-sidecar.js"),
join(root, "dist", "cli", "acpx-runtime-sidecar.cjs"),
sidecar,
);
await writeFile(join(root, "node_modules", "node", "bin", "node"), node);
@ -305,11 +305,11 @@ describe("remote provider pack manifest", () => {
sha256: digest(opencodeExecutable),
},
opencodeProxy: {
path: "dist/cli/opencode-app-server-proxy.js",
path: "dist/cli/opencode-app-server-proxy.cjs",
sha256: proxySha,
},
acpxSidecar: {
path: "dist/cli/acpx-runtime-sidecar.js",
path: "dist/cli/acpx-runtime-sidecar.cjs",
sha256: sidecarSha,
},
},
@ -352,14 +352,14 @@ describe("remote provider pack manifest", () => {
}
await writeManifest();
await writeFile(
join(root, "dist", "cli", "opencode-app-server-proxy.js"),
join(root, "dist", "cli", "opencode-app-server-proxy.cjs"),
"tampered\n",
);
expect(() => readRemoteProviderPackManifest(root)).toThrow(
"OpenCode proxy digest mismatch",
);
await writeFile(
join(root, "dist", "cli", "opencode-app-server-proxy.js"),
join(root, "dist", "cli", "opencode-app-server-proxy.cjs"),
proxy,
);
await writeFile(

View File

@ -4448,8 +4448,8 @@ const REMOTE_PROVIDER_PACK_ARTIFACT_PATHS = {
productionLock: "pnpm-lock.yaml",
opencodeCommand: "node_modules/.bin/opencode",
opencodeExecutable: "node_modules/opencode-ai/bin/opencode.exe",
opencodeProxy: "dist/cli/opencode-app-server-proxy.js",
acpxSidecar: "dist/cli/acpx-runtime-sidecar.js",
opencodeProxy: "dist/cli/opencode-app-server-proxy.cjs",
acpxSidecar: "dist/cli/acpx-runtime-sidecar.cjs",
} as const;
type RemoteProviderPackManifest = {