Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/ci-path-filters.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ shared:
- 'crates/adaptive/src/**'
- 'crates/core/Cargo.toml'
- 'crates/core/src/**'
- 'crates/plugin-host-config/Cargo.toml'
- 'crates/plugin-host-config/src/**'
- 'crates/types/Cargo.toml'
- 'crates/types/src/**'
- 'crates/worker-proto/Cargo.toml'
Expand All @@ -45,6 +47,8 @@ rust_package:
- 'crates/core/src/**'
- 'crates/plugin/Cargo.toml'
- 'crates/plugin/src/**'
- 'crates/plugin-host-config/Cargo.toml'
- 'crates/plugin-host-config/src/**'
- 'crates/types/Cargo.toml'
- 'crates/types/src/**'
- 'crates/worker-proto/Cargo.toml'
Expand Down Expand Up @@ -96,6 +100,8 @@ python_package:
- 'crates/adaptive/src/**'
- 'crates/core/Cargo.toml'
- 'crates/core/src/**'
- 'crates/plugin-host-config/Cargo.toml'
- 'crates/plugin-host-config/src/**'
- 'crates/types/Cargo.toml'
- 'crates/types/src/**'
- 'crates/python/Cargo.toml'
Expand Down Expand Up @@ -152,6 +158,7 @@ docs:
- 'crates/node/*.d.ts'
- 'crates/node/*.js'
- 'crates/node/package.json'
- 'crates/plugin-host-config/src/**'
- 'crates/*/Cargo.toml'
- 'crates/**/*.md'
- 'fern/**'
Expand Down Expand Up @@ -195,6 +202,8 @@ openclaw:
- 'integrations/openclaw/**'

python:
- 'crates/plugin-host-config/Cargo.toml'
- 'crates/plugin-host-config/src/**'
- 'crates/python/Cargo.toml'
- 'crates/python/src/**'
- 'pyproject.toml'
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,7 @@ jobs:
nemo-relay-worker-proto
nemo-relay-worker
nemo-relay
nemo-relay-plugin-host-config
nemo-relay-adaptive
nemo-relay-pii-redaction
nemo-relay-switchyard
Expand Down
2 changes: 1 addition & 1 deletion .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ publish:artifactory:cargo:
artifactory = { index = "sparse+${NEMO_RELAY_CI_ARTIFACTORY_CARGO_URL}" }
EOF
export CARGO_REGISTRIES_ARTIFACTORY_TOKEN="Bearer ${NEMO_RELAY_CI_ARTIFACTORY_KEY}"
export NEMO_RELAY_ARTIFACTORY_CRATE_DIRS="types plugin worker-proto worker core adaptive pii-redaction switchyard ffi cli"
export NEMO_RELAY_ARTIFACTORY_CRATE_DIRS="types plugin worker-proto worker core plugin-host-config adaptive pii-redaction switchyard ffi cli"

crates="$(
uv run --no-project python - <<'PY'
Expand Down
24 changes: 24 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
[workspace]
members = [
"crates/core",
"crates/plugin-host-config",
"crates/types",
"crates/plugin",
"crates/worker-proto",
Expand All @@ -28,6 +29,7 @@ repository = "https://github.com/NVIDIA/NeMo-Relay"

[workspace.dependencies]
nemo-relay = { version = "0.7.0", path = "crates/core", default-features = false }
nemo-relay-plugin-host-config = { version = "0.7.0", path = "crates/plugin-host-config" }
nemo-relay-types = { version = "0.7.0", path = "crates/types" }
nemo-relay-plugin = { version = "0.7.0", path = "crates/plugin" }
nemo-relay-worker-proto = { version = "0.7.0", path = "crates/worker-proto" }
Expand Down
17 changes: 11 additions & 6 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ The release pipeline publishes these package surfaces from a tag push:

| Ecosystem | Published Surface |
|---|---|
| crates.io | `nemo-relay-types`, `nemo-relay-plugin`, `nemo-relay-worker-proto`, `nemo-relay-worker`, `nemo-relay`, `nemo-relay-adaptive`, `nemo-relay-pii-redaction`, `nemo-relay-switchyard`, `nemo-relay-ffi`, `nemo-relay-cli` |
| crates.io | `nemo-relay-types`, `nemo-relay-plugin`, `nemo-relay-worker-proto`, `nemo-relay-worker`, `nemo-relay`, `nemo-relay-plugin-host-config`, `nemo-relay-adaptive`, `nemo-relay-pii-redaction`, `nemo-relay-switchyard`, `nemo-relay-ffi`, `nemo-relay-cli` |
| PyPI | `nemo-relay` wheels and source distribution, `nemo-relay-plugin` and `nemo-relay-cli-bin` wheels |
| npm | `nemo-relay-node` and its seven platform packages, `nemo-relay-openclaw`, `nemo-relay-cli-bin`, and its seven platform packages |
| GitHub Releases | CLI binaries, `nemo-relay` and `nemo-relay-cli-bin` wheels, CLI and Node npm tarballs, and checksums |
Expand All @@ -52,7 +52,8 @@ NeMo Relay versions are anchored on the workspace SemVer in the repository root
version for the Rust workspace.
- The root `Cargo.toml` `workspace.dependencies` entries for
`nemo-relay-types`, `nemo-relay-plugin`, `nemo-relay-worker-proto`,
`nemo-relay-worker`, `nemo-relay`, `nemo-relay-adaptive`,
`nemo-relay-worker`, `nemo-relay`, `nemo-relay-plugin-host-config`,
`nemo-relay-adaptive`,
`nemo-relay-pii-redaction`, `nemo-relay-switchyard`, `nemo-relay-ffi`, and
`nemo-relay-cli` must
stay aligned with that same version.
Expand Down Expand Up @@ -138,7 +139,8 @@ Before you create a release tag, confirm the following:
- GitHub Actions `id-token: write` access for the top-level crates.io publish job
- crates.io trusted publishers for `nemo-relay-types`,
`nemo-relay-plugin`, `nemo-relay-worker-proto`, `nemo-relay-worker`,
`nemo-relay`, `nemo-relay-adaptive`, `nemo-relay-pii-redaction`,
`nemo-relay`, `nemo-relay-plugin-host-config`, `nemo-relay-adaptive`,
`nemo-relay-pii-redaction`,
`nemo-relay-switchyard`, `nemo-relay-ffi`, and `nemo-relay-cli` are
configured for the top-level
[`.github/workflows/ci.yaml`](.github/workflows/ci.yaml) workflow
Expand Down Expand Up @@ -173,7 +175,8 @@ The helper updates:
1. The root [`Cargo.toml`](Cargo.toml) workspace version.
2. The root [`Cargo.toml`](Cargo.toml) `workspace.dependencies` versions for
`nemo-relay-types`, `nemo-relay-plugin`, `nemo-relay-worker-proto`,
`nemo-relay-worker`, `nemo-relay`, `nemo-relay-adaptive`,
`nemo-relay-worker`, `nemo-relay`, `nemo-relay-plugin-host-config`,
`nemo-relay-adaptive`,
`nemo-relay-pii-redaction`, `nemo-relay-switchyard`, `nemo-relay-ffi`, and
`nemo-relay-cli`.
3. [`crates/node/package.json`](crates/node/package.json) and the `crates/node`
Expand Down Expand Up @@ -285,7 +288,8 @@ The release pipeline then:
- `publish-rust` stamps Cargo workspace versions from the release tag, then
runs `cargo publish --package` for `nemo-relay-types`,
`nemo-relay-plugin`, `nemo-relay-worker-proto`, `nemo-relay-worker`,
`nemo-relay`, `nemo-relay-adaptive`, `nemo-relay-pii-redaction`,
`nemo-relay`, `nemo-relay-plugin-host-config`, `nemo-relay-adaptive`,
`nemo-relay-pii-redaction`,
`nemo-relay-switchyard`, `nemo-relay-ffi`, and `nemo-relay-cli` through
trusted publishing from
the top-level workflow
Expand Down Expand Up @@ -357,7 +361,8 @@ for that tag.
After the release is live, verify:

1. The `nemo-relay-types`, `nemo-relay-plugin`, `nemo-relay-worker-proto`,
`nemo-relay-worker`, `nemo-relay`, `nemo-relay-adaptive`,
`nemo-relay-worker`, `nemo-relay`, `nemo-relay-plugin-host-config`,
`nemo-relay-adaptive`,
`nemo-relay-pii-redaction`, `nemo-relay-switchyard`, `nemo-relay-ffi`, and
`nemo-relay-cli` crates
are visible on crates.io.
Expand Down
1 change: 1 addition & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ component_management:
- "crates/plugin/src"
- "crates/worker-proto/src"
- "crates/worker/src"
- "crates/plugin-host-config/src"
- "python/plugin/src/nemo_relay_plugin"
statuses:
- type: project
Expand Down
1 change: 1 addition & 0 deletions crates/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ switchyard = ["dep:nemo-relay-switchyard"]

[dependencies]
nemo-relay = { workspace = true, features = ["guardrails-remote", "object-store", "worker-grpc"] }
nemo-relay-plugin-host-config.workspace = true
nemo-relay-adaptive = { workspace = true, features = ["redis-backend"] }
nemo-relay-pii-redaction.workspace = true
nemo-relay-switchyard = { workspace = true, optional = true, features = ["schema"] }
Expand Down
107 changes: 41 additions & 66 deletions crates/cli/src/configuration/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod types;

pub(crate) use types::*;

#[cfg(test)]
use std::collections::HashSet;
use std::env;
use std::fs::{self, OpenOptions};
Expand All @@ -19,13 +20,12 @@ use nemo_relay::logging::LoggingConfig;
use nemo_relay::plugin::dynamic::{
DYNAMIC_PLUGIN_MANIFEST_FILENAME, DynamicPluginManifest, DynamicPluginManifestLoad,
};
use nemo_relay::plugin::{
PluginError, deduplicate_plugin_config_paths, merge_plugin_config_documents,
};
use ring::rand::{SecureRandom, SystemRandom};
use ring::{digest, hmac};
use serde::Deserialize;
use serde_json::{Map, Value};
#[cfg(test)]
use serde_json::Map;
use serde_json::Value;

use crate::error::CliError;
use crate::filesystem::{LockAttempt, try_lock_exclusive, try_lock_shared};
Expand Down Expand Up @@ -604,6 +604,7 @@ pub(crate) fn sign_python_environment_attestation(
))
}

#[cfg(test)]
pub(crate) fn verify_python_environment_attestation(
source_artifact_sha256: &str,
environment_sha256: &str,
Expand Down Expand Up @@ -1298,8 +1299,10 @@ struct PluginTomlConfig {
dynamic_plugins: Vec<ResolvedDynamicPluginConfig>,
dynamic_plugin_policy: DynamicPluginHostPolicy,
contributing_sources: Vec<PathBuf>,
selected_sources: Vec<PathBuf>,
}

#[cfg(test)]
#[derive(Debug, Clone, Default, Deserialize)]
struct PluginTomlPluginsSection {
#[serde(default)]
Expand All @@ -1308,6 +1311,7 @@ struct PluginTomlPluginsSection {
policy: Option<crate::plugins::policy::FileDynamicPluginHostPolicy>,
}

#[cfg(test)]
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
struct FileDynamicPluginConfig {
Expand Down Expand Up @@ -1365,69 +1369,34 @@ fn load_plugin_toml_config_from_paths<I>(paths: I) -> Result<Option<PluginTomlCo
where
I: IntoIterator<Item = PathBuf>,
{
let paths = deduplicate_plugin_config_paths(paths);
let mut dynamic_plugins = Vec::new();
let mut dynamic_plugin_policy = DynamicPluginHostPolicy::default();
let mut seen_plugin_ids = HashSet::new();
let mut contributing_sources = Vec::new();
let mut runtime_documents = Vec::new();

for path in &paths {
let Some(raw) = read_config_file(path, false, "plugin configuration")? else {
continue;
};
let mut parsed = raw
.parse::<toml::Table>()
.map(toml::Value::Table)
.map_err(|error| {
CliError::Config(format!(
"invalid plugin TOML in {}: {error}",
path.display()
))
})?;
let resolved_plugins =
resolve_dynamic_plugin_refs(path, &mut parsed, &mut seen_plugin_ids)?;
if !resolved_plugins.dynamic_plugins.is_empty()
|| resolved_plugins.dynamic_plugin_policy != DynamicPluginHostPolicy::default()
{
contributing_sources.push(path.clone());
}
dynamic_plugins.extend(resolved_plugins.dynamic_plugins);
dynamic_plugin_policy.merge_from(resolved_plugins.dynamic_plugin_policy);
runtime_documents.push((
path.clone(),
serde_json::to_value(remove_dynamic_plugin_sections(parsed))
.expect("toml value serializes to JSON"),
));
}

// Delegate merged runtime plugin config to the shared core primitive after dynamic refs have
// been validated independently. Documents remain ordered from lowest to highest precedence.
let resolved = merge_plugin_config_documents(runtime_documents).map_err(|err| match err {
PluginError::InvalidConfig(message) => CliError::Config(message),
other => CliError::Config(other.to_string()),
})?;
match resolved {
Some((value, sources)) => {
contributing_sources.extend(sources.iter().cloned());
contributing_sources.sort();
contributing_sources.dedup();
Ok(Some(PluginTomlConfig {
value: plugin_toml_runtime_value(value),
dynamic_plugins,
dynamic_plugin_policy,
contributing_sources,
}))
}
None => Ok((!dynamic_plugins.is_empty()
|| dynamic_plugin_policy != DynamicPluginHostPolicy::default())
.then_some(PluginTomlConfig {
value: None,
dynamic_plugins,
dynamic_plugin_policy,
contributing_sources,
})),
let resolved = nemo_relay_plugin_host_config::resolve_plugin_files_from_paths(paths, None)
.map_err(|error| CliError::Config(error.to_string()))?;
if !resolved.had_input {
return Ok(None);
}
// The shared resolver pins sources to their physical paths so lifecycle state and snapshots
// cannot be split across aliases. Keep the CLI's established presentation contract, however:
// contributing sources use the selected spelling and are sorted independently of precedence.
let mut contributing_sources = resolved.contributing_selected_sources;
contributing_sources.sort();
contributing_sources.dedup();
Ok(Some(PluginTomlConfig {
value: resolved.runtime_value,
dynamic_plugins: resolved
.dynamic_plugins
.into_iter()
.map(|plugin| ResolvedDynamicPluginConfig {
plugin_id: plugin.plugin_id,
manifest_ref: plugin.manifest_ref,
config: plugin.config,
has_explicit_config: plugin.has_explicit_config,
source: plugin.source,
})
.collect(),
dynamic_plugin_policy: resolved.dynamic_plugin_policy,
contributing_sources,
selected_sources: resolved.selected_sources,
}))
}

fn apply_plugin_toml_config(resolved: &mut ResolvedConfig, plugin_toml: Option<PluginTomlConfig>) {
Expand All @@ -1439,13 +1408,16 @@ fn apply_plugin_toml_config(resolved: &mut ResolvedConfig, plugin_toml: Option<P
}
resolved.dynamic_plugins = plugin_toml.dynamic_plugins;
resolved.dynamic_plugin_policy = plugin_toml.dynamic_plugin_policy;
resolved.plugin_selected_sources = plugin_toml.selected_sources;
}

#[cfg(test)]
struct ResolvedDynamicPluginRefs {
dynamic_plugins: Vec<ResolvedDynamicPluginConfig>,
dynamic_plugin_policy: DynamicPluginHostPolicy,
}

#[cfg(test)]
fn resolve_dynamic_plugin_refs(
source: &Path,
value: &mut toml::Value,
Expand Down Expand Up @@ -1513,6 +1485,7 @@ fn resolve_dynamic_plugin_refs(
})
}

#[cfg(test)]
fn resolve_dynamic_manifest_path(source: &Path, manifest: &str) -> PathBuf {
let manifest = PathBuf::from(manifest);
if manifest.is_absolute() {
Expand All @@ -1525,13 +1498,15 @@ fn resolve_dynamic_manifest_path(source: &Path, manifest: &str) -> PathBuf {
}
}

#[cfg(test)]
fn plugin_toml_runtime_value(value: Value) -> Option<Value> {
match value {
Value::Object(ref object) if object.is_empty() => None,
other => Some(other),
}
}

#[cfg(test)]
fn remove_dynamic_plugin_sections(mut value: toml::Value) -> toml::Value {
if let Some(root) = value.as_table_mut()
&& let Some(toml::Value::Table(plugins)) = root.get_mut("plugins")
Expand Down
1 change: 1 addition & 0 deletions crates/cli/src/configuration/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ pub(crate) struct ResolvedConfig {
pub(crate) logging: LoggingConfig,
pub(crate) dynamic_plugins: Vec<ResolvedDynamicPluginConfig>,
pub(crate) dynamic_plugin_policy: DynamicPluginHostPolicy,
pub(crate) plugin_selected_sources: Vec<PathBuf>,
pub(crate) bootstrap_fingerprint: Option<String>,
}

Expand Down
Loading
Loading