acp: Allow running NPM agents from registry (#47291)
Release Notes: - N/A
This commit is contained in:
1 parent
e5af0f1e46
commit
5d1e65bdfb
4 files changed
+447
-156
No files matched your search
@@ -8,6 +8,7 @@ use semver::Version;
|
||||
use serde::Deserialize;
|
||||
use smol::io::BufReader;
|
||||
use smol::{fs, lock::Mutex};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Display;
|
||||
use std::{
|
||||
env::{self, consts},
|
||||
@@ -30,6 +31,15 @@ pub struct NodeBinaryOptions {
|
||||
pub use_paths: Option<(PathBuf, PathBuf)>,
|
||||
}
|
||||
|
||||
/// Use this when you need to launch npm as a long-lived process (for example, an agent server),
|
||||
/// so the invocation and environment stay consistent with the Node runtime's proxy and CA setup.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct NpmCommand {
|
||||
pub path: PathBuf,
|
||||
pub args: Vec<String>,
|
||||
pub env: HashMap<String, String>,
|
||||
}
|
||||
|
||||
pub enum VersionStrategy<'a> {
|
||||
/// Install if current version doesn't match pinned version
|
||||
Pin(&'a Version),
|
||||
@@ -86,7 +96,7 @@ impl NodeRuntime {
|
||||
Err(err) => {
|
||||
return Box::new(UnavailableNodeRuntime {
|
||||
error_message: err.to_string().into(),
|
||||
});
|
||||
}) as Box<dyn NodeRuntimeTrait>;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -128,7 +138,7 @@ impl NodeRuntime {
|
||||
log::info!("using Node.js found on PATH: {:?}", instance);
|
||||
state.instance = Some(instance.boxed_clone());
|
||||
state.last_options = Some(options);
|
||||
return Box::new(instance);
|
||||
return Box::new(instance) as Box<dyn NodeRuntimeTrait>;
|
||||
}
|
||||
Err(err) => Some(err),
|
||||
}
|
||||
@@ -228,6 +238,14 @@ impl NodeRuntime {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn npm_command(&self, subcommand: &str, args: &[&str]) -> Result<NpmCommand> {
|
||||
let http = self.0.lock().await.http.clone();
|
||||
self.instance()
|
||||
.await
|
||||
.npm_command(http.proxy(), subcommand, args)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn npm_package_latest_version(&self, name: &str) -> Result<Version> {
|
||||
let http = self.0.lock().await.http.clone();
|
||||
let output = self
|
||||
@@ -352,6 +370,13 @@ trait NodeRuntimeTrait: Send + Sync {
|
||||
args: &[&str],
|
||||
) -> Result<Output>;
|
||||
|
||||
async fn npm_command(
|
||||
&self,
|
||||
proxy: Option<&Url>,
|
||||
subcommand: &str,
|
||||
args: &[&str],
|
||||
) -> Result<NpmCommand>;
|
||||
|
||||
async fn npm_package_installed_version(
|
||||
&self,
|
||||
local_package_directory: &Path,
|
||||
@@ -528,40 +553,12 @@ impl NodeRuntimeTrait for ManagedNodeRuntime {
|
||||
subcommand: &str,
|
||||
args: &[&str],
|
||||
) -> Result<Output> {
|
||||
let attempt = || async move {
|
||||
let node_binary = self.installation_path.join(Self::NODE_PATH);
|
||||
let npm_file = self.installation_path.join(Self::NPM_PATH);
|
||||
let env_path = path_with_node_binary_prepended(&node_binary).unwrap_or_default();
|
||||
|
||||
anyhow::ensure!(
|
||||
smol::fs::metadata(&node_binary).await.is_ok(),
|
||||
"missing node binary file"
|
||||
);
|
||||
anyhow::ensure!(
|
||||
smol::fs::metadata(&npm_file).await.is_ok(),
|
||||
"missing npm file"
|
||||
);
|
||||
|
||||
let node_ca_certs = env::var(NODE_CA_CERTS_ENV_VAR).unwrap_or_else(|_| String::new());
|
||||
|
||||
let mut command = util::command::new_smol_command(node_binary);
|
||||
command.env("PATH", env_path);
|
||||
command.env(NODE_CA_CERTS_ENV_VAR, node_ca_certs);
|
||||
command.arg(npm_file).arg(subcommand);
|
||||
command.arg(format!(
|
||||
"--cache={}",
|
||||
self.installation_path.join("cache").display()
|
||||
));
|
||||
command.args([
|
||||
"--userconfig".into(),
|
||||
self.installation_path.join("blank_user_npmrc"),
|
||||
]);
|
||||
command.args([
|
||||
"--globalconfig".into(),
|
||||
self.installation_path.join("blank_global_npmrc"),
|
||||
]);
|
||||
command.args(args);
|
||||
configure_npm_command(&mut command, directory, proxy);
|
||||
let attempt = || async {
|
||||
let npm_command = self.npm_command(proxy, subcommand, args).await?;
|
||||
let mut command = util::command::new_smol_command(npm_command.path);
|
||||
command.args(npm_command.args);
|
||||
command.envs(npm_command.env);
|
||||
configure_npm_command(&mut command, directory);
|
||||
command.output().await.map_err(|e| anyhow!("{e}"))
|
||||
};
|
||||
|
||||
@@ -586,6 +583,43 @@ impl NodeRuntimeTrait for ManagedNodeRuntime {
|
||||
|
||||
output.map_err(|e| anyhow!("{e}"))
|
||||
}
|
||||
|
||||
async fn npm_command(
|
||||
&self,
|
||||
proxy: Option<&Url>,
|
||||
subcommand: &str,
|
||||
args: &[&str],
|
||||
) -> Result<NpmCommand> {
|
||||
let node_binary = self.installation_path.join(Self::NODE_PATH);
|
||||
let npm_file = self.installation_path.join(Self::NPM_PATH);
|
||||
|
||||
anyhow::ensure!(
|
||||
smol::fs::metadata(&node_binary).await.is_ok(),
|
||||
"missing node binary file"
|
||||
);
|
||||
anyhow::ensure!(
|
||||
smol::fs::metadata(&npm_file).await.is_ok(),
|
||||
"missing npm file"
|
||||
);
|
||||
|
||||
let command_args = build_npm_command_args(
|
||||
Some(&npm_file),
|
||||
&self.installation_path.join("cache"),
|
||||
Some(&self.installation_path.join("blank_user_npmrc")),
|
||||
Some(&self.installation_path.join("blank_global_npmrc")),
|
||||
proxy,
|
||||
subcommand,
|
||||
args,
|
||||
);
|
||||
let command_env = npm_command_env(Some(&node_binary));
|
||||
|
||||
Ok(NpmCommand {
|
||||
path: node_binary,
|
||||
args: command_args,
|
||||
env: command_env,
|
||||
})
|
||||
}
|
||||
|
||||
async fn npm_package_installed_version(
|
||||
&self,
|
||||
local_package_directory: &Path,
|
||||
@@ -688,19 +722,11 @@ impl NodeRuntimeTrait for SystemNodeRuntime {
|
||||
subcommand: &str,
|
||||
args: &[&str],
|
||||
) -> anyhow::Result<Output> {
|
||||
let node_ca_certs = env::var(NODE_CA_CERTS_ENV_VAR).unwrap_or_else(|_| String::new());
|
||||
let mut command = util::command::new_smol_command(self.npm.clone());
|
||||
let path = path_with_node_binary_prepended(&self.node).unwrap_or_default();
|
||||
command
|
||||
.env("PATH", path)
|
||||
.env(NODE_CA_CERTS_ENV_VAR, node_ca_certs)
|
||||
.arg(subcommand)
|
||||
.arg(format!(
|
||||
"--cache={}",
|
||||
self.scratch_dir.join("cache").display()
|
||||
))
|
||||
.args(args);
|
||||
configure_npm_command(&mut command, directory, proxy);
|
||||
let npm_command = self.npm_command(proxy, subcommand, args).await?;
|
||||
let mut command = util::command::new_smol_command(npm_command.path);
|
||||
command.args(npm_command.args);
|
||||
command.envs(npm_command.env);
|
||||
configure_npm_command(&mut command, directory);
|
||||
let output = command.output().await?;
|
||||
anyhow::ensure!(
|
||||
output.status.success(),
|
||||
@@ -711,6 +737,30 @@ impl NodeRuntimeTrait for SystemNodeRuntime {
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
async fn npm_command(
|
||||
&self,
|
||||
proxy: Option<&Url>,
|
||||
subcommand: &str,
|
||||
args: &[&str],
|
||||
) -> Result<NpmCommand> {
|
||||
let command_args = build_npm_command_args(
|
||||
None,
|
||||
&self.scratch_dir.join("cache"),
|
||||
None,
|
||||
None,
|
||||
proxy,
|
||||
subcommand,
|
||||
args,
|
||||
);
|
||||
let command_env = npm_command_env(Some(&self.node));
|
||||
|
||||
Ok(NpmCommand {
|
||||
path: self.npm.clone(),
|
||||
args: command_args,
|
||||
env: command_env,
|
||||
})
|
||||
}
|
||||
|
||||
async fn npm_package_installed_version(
|
||||
&self,
|
||||
local_package_directory: &Path,
|
||||
@@ -773,6 +823,15 @@ impl NodeRuntimeTrait for UnavailableNodeRuntime {
|
||||
bail!("{}", self.error_message)
|
||||
}
|
||||
|
||||
async fn npm_command(
|
||||
&self,
|
||||
_proxy: Option<&Url>,
|
||||
_subcommand: &str,
|
||||
_args: &[&str],
|
||||
) -> Result<NpmCommand> {
|
||||
bail!("{}", self.error_message)
|
||||
}
|
||||
|
||||
async fn npm_package_installed_version(
|
||||
&self,
|
||||
_local_package_directory: &Path,
|
||||
@@ -782,59 +841,101 @@ impl NodeRuntimeTrait for UnavailableNodeRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
fn configure_npm_command(
|
||||
command: &mut smol::process::Command,
|
||||
directory: Option<&Path>,
|
||||
proxy: Option<&Url>,
|
||||
) {
|
||||
fn configure_npm_command(command: &mut smol::process::Command, directory: Option<&Path>) {
|
||||
if let Some(directory) = directory {
|
||||
command.current_dir(directory);
|
||||
command.args(["--prefix".into(), directory.to_path_buf()]);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(mut proxy) = proxy.cloned() {
|
||||
// Map proxy settings from `http://localhost:10809` to `http://127.0.0.1:10809`
|
||||
// NodeRuntime without environment information can not parse `localhost`
|
||||
// correctly.
|
||||
// TODO: map to `[::1]` if we are using ipv6
|
||||
if matches!(proxy.host(), Some(Host::Domain(domain)) if domain.eq_ignore_ascii_case("localhost"))
|
||||
{
|
||||
// When localhost is a valid Host, so is `127.0.0.1`
|
||||
let _ = proxy.set_ip_host(IpAddr::V4(Ipv4Addr::LOCALHOST));
|
||||
fn proxy_argument(proxy: Option<&Url>) -> Option<String> {
|
||||
let mut proxy = proxy.cloned()?;
|
||||
// Map proxy settings from `http://localhost:10809` to `http://127.0.0.1:10809`
|
||||
// NodeRuntime without environment information can not parse `localhost`
|
||||
// correctly.
|
||||
// TODO: map to `[::1]` if we are using ipv6
|
||||
if matches!(proxy.host(), Some(Host::Domain(domain)) if domain.eq_ignore_ascii_case("localhost"))
|
||||
{
|
||||
// When localhost is a valid Host, so is `127.0.0.1`
|
||||
let _ = proxy.set_ip_host(IpAddr::V4(Ipv4Addr::LOCALHOST));
|
||||
}
|
||||
|
||||
Some(proxy.as_str().to_string())
|
||||
}
|
||||
|
||||
fn build_npm_command_args(
|
||||
entrypoint: Option<&Path>,
|
||||
cache_dir: &Path,
|
||||
user_config: Option<&Path>,
|
||||
global_config: Option<&Path>,
|
||||
proxy: Option<&Url>,
|
||||
subcommand: &str,
|
||||
args: &[&str],
|
||||
) -> Vec<String> {
|
||||
let mut command_args = Vec::new();
|
||||
if let Some(entrypoint) = entrypoint {
|
||||
command_args.push(entrypoint.to_string_lossy().into_owned());
|
||||
}
|
||||
command_args.push(subcommand.to_string());
|
||||
command_args.push(format!("--cache={}", cache_dir.display()));
|
||||
if let Some(user_config) = user_config {
|
||||
command_args.push("--userconfig".into());
|
||||
command_args.push(user_config.to_string_lossy().into_owned());
|
||||
}
|
||||
if let Some(global_config) = global_config {
|
||||
command_args.push("--globalconfig".into());
|
||||
command_args.push(global_config.to_string_lossy().into_owned());
|
||||
}
|
||||
if let Some(proxy_arg) = proxy_argument(proxy) {
|
||||
command_args.push("--proxy".into());
|
||||
command_args.push(proxy_arg);
|
||||
}
|
||||
command_args.extend(args.into_iter().map(|a| a.to_string()));
|
||||
command_args
|
||||
}
|
||||
|
||||
fn npm_command_env(node_binary: Option<&Path>) -> HashMap<String, String> {
|
||||
let mut command_env = HashMap::new();
|
||||
if let Some(node_binary) = node_binary {
|
||||
let env_path = path_with_node_binary_prepended(node_binary).unwrap_or_default();
|
||||
command_env.insert("PATH".into(), env_path.to_string_lossy().into_owned());
|
||||
}
|
||||
|
||||
if let Ok(node_ca_certs) = env::var(NODE_CA_CERTS_ENV_VAR) {
|
||||
if !node_ca_certs.is_empty() {
|
||||
command_env.insert(NODE_CA_CERTS_ENV_VAR.to_string(), node_ca_certs);
|
||||
}
|
||||
|
||||
command.args(["--proxy", proxy.as_str()]);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// SYSTEMROOT is a critical environment variables for Windows.
|
||||
if let Some(val) = env::var("SYSTEMROOT")
|
||||
.context("Missing environment variable: SYSTEMROOT!")
|
||||
.log_err()
|
||||
{
|
||||
command.env("SYSTEMROOT", val);
|
||||
command_env.insert("SYSTEMROOT".into(), val);
|
||||
}
|
||||
// Without ComSpec, the post-install will always fail.
|
||||
if let Some(val) = env::var("ComSpec")
|
||||
.context("Missing environment variable: ComSpec!")
|
||||
.log_err()
|
||||
{
|
||||
command.env("ComSpec", val);
|
||||
command_env.insert("ComSpec".into(), val);
|
||||
}
|
||||
}
|
||||
|
||||
command_env
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use http_client::Url;
|
||||
|
||||
use super::configure_npm_command;
|
||||
use super::proxy_argument;
|
||||
|
||||
// Map localhost to 127.0.0.1
|
||||
// NodeRuntime without environment information can not parse `localhost` correctly.
|
||||
#[test]
|
||||
fn test_configure_npm_command_map_localhost_proxy() {
|
||||
fn test_proxy_argument_map_localhost_proxy() {
|
||||
const CASES: [(&str, &str); 4] = [
|
||||
// Map localhost to 127.0.0.1
|
||||
("http://localhost:9090/", "http://127.0.0.1:9090/"),
|
||||
@@ -851,15 +952,8 @@ mod tests {
|
||||
];
|
||||
|
||||
for (proxy, mapped_proxy) in CASES {
|
||||
let mut dummy = smol::process::Command::new("");
|
||||
let proxy = Url::parse(proxy).unwrap();
|
||||
configure_npm_command(&mut dummy, None, Some(&proxy));
|
||||
let proxy = dummy
|
||||
.get_args()
|
||||
.skip_while(|&arg| arg != "--proxy")
|
||||
.skip(1)
|
||||
.next();
|
||||
let proxy = proxy.expect("Proxy was not passed to Command correctly");
|
||||
let proxy = proxy_argument(Some(&proxy)).expect("Proxy was not passed correctly");
|
||||
assert_eq!(
|
||||
proxy, mapped_proxy,
|
||||
"Incorrectly mapped localhost to 127.0.0.1"
|
||||
|
||||
Reference in new issue
Block a user