fix(runner): restore verified JS provider startup
This commit is contained in:
parent
96fcadeb51
commit
62b79deb20
|
|
@ -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(_))
|
||||
));
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}"))
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue