Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b87c616
feat: configure dynamic plugins through CLI
willkill07 Jun 29, 2026
a81d953
fix: harden dynamic plugin schema editing
willkill07 Jun 29, 2026
a202a79
chore: remove dynamic plugin documentation updates
willkill07 Jun 29, 2026
2208d4a
chore: restore plugin documentation references
willkill07 Jun 29, 2026
e83cbc7
fix: restore secrets from raw field edits
willkill07 Jun 29, 2026
ac2e888
Merge branch 'main' into wkk_dynamic-plugin-config-editor
bbednarski9 Jun 29, 2026
dcc63ff
fix: address dynamic plugin editor review feedback
willkill07 Jun 30, 2026
2ac7a53
fix: redact Draft 7 tuple secrets
willkill07 Jun 30, 2026
1cd2ef1
refactor: centralize plugin menu navigation
willkill07 Jun 30, 2026
115c6bf
fix: bound dynamic plugin schema reads
willkill07 Jun 30, 2026
405a2c1
fix: distinguish plugin clear from reset
willkill07 Jun 30, 2026
b0c7ff8
fix: tighten dynamic plugin secret discovery
willkill07 Jun 30, 2026
6e61fd5
fix: reject unresolved secret schema references
willkill07 Jun 30, 2026
99baa92
test: move plugin schema tests out of src
willkill07 Jun 30, 2026
bbee2b4
docs: remove plugin configuration guide updates
willkill07 Jun 30, 2026
589319d
Merge branch 'main' into wkk_dynamic-plugin-config-editor
willkill07 Jun 30, 2026
e0a78f8
Merge upstream/main into wkk_dynamic-plugin-config-editor
willkill07 Jul 1, 2026
20e2bb5
Merge upstream/main into wkk_dynamic-plugin-config-editor
willkill07 Jul 2, 2026
bf28b7d
fix: harden dynamic plugin config handling
willkill07 Jul 2, 2026
89c239b
fix: redact ref sibling secrets
willkill07 Jul 2, 2026
6a96025
Merge branch 'main' into wkk_dynamic-plugin-config-editor
willkill07 Jul 2, 2026
cbec051
test: create explicit config for plugin e2e
willkill07 Jul 2, 2026
fc0f04c
Merge remote-tracking branch 'upstream/main' into wkk_dynamic-plugin-…
willkill07 Jul 2, 2026
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
15,030 changes: 9,082 additions & 5,948 deletions ATTRIBUTIONS-Rust.md

Large diffs are not rendered by default.

266 changes: 265 additions & 1 deletion Cargo.lock

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,10 @@ nemo-relay plugins edit --project
```

The editor creates or updates the nearest project plugin file at
`.nemo-relay/plugins.toml`. In the menu:
`.nemo-relay/plugins.toml`. In the top-level menu, select **Observability**,
then configure these sections:

1. Enable the `Observability` component.
1. Toggle the Observability component on.
2. Open `ATOF`, toggle the section `[on]`

Optionally set:
Expand All @@ -84,7 +85,7 @@ The editor creates or updates the nearest project plugin file at
Optionally set:
- `output_directory` to `.nemo-relay/atif`
- `filename_template` to `trajectory-{session_id}.json`
4. Press `p` to preview the generated TOML.
4. Return to the top-level menu and press `p` to preview the generated TOML.
5. Press `s` to save.

> [!NOTE]
Expand Down
1 change: 1 addition & 0 deletions about.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ accepted = [
"BSL-1.0",
"ISC",
"MIT",
"MIT-0",
"MPL-2.0",
"Unicode-3.0",
"Zlib",
Expand Down
5 changes: 4 additions & 1 deletion crates/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,11 @@ console = "0.16"
futures-util = "0.3"
http = "1"
http-body-util = "0.1"
dialoguer = { version = "0.11", default-features = false }
dialoguer = { version = "0.11", default-features = false, features = ["password"] }
jsonschema = { version = "0.46.6", default-features = false }
percent-encoding = "2"
reqwest = { version = "0.12", default-features = false, features = ["charset", "http2", "json", "rustls-tls-native-roots", "stream"] }
regex = "1"
ring = "0.17"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Expand Down
5 changes: 5 additions & 0 deletions crates/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ plugin config with:
nemo-relay plugins edit
```

The top-level editor menu contains one entry per supported built-in, followed by
the dynamic plugin references in the selected physical `plugins.toml`. Dynamic
plugins with a manifest-declared JSON Schema provide structured field controls.
Other dynamic plugins use a raw JSON object editor.

The canonical plugin file is `plugins.toml`; user config lives at
`~/.config/nemo-relay/plugins.toml` or
`$XDG_CONFIG_HOME/nemo-relay/plugins.toml`. Project config lives at
Expand Down
73 changes: 48 additions & 25 deletions crates/cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ pub(crate) struct PluginJsonContext<'a> {
/// Plugin configuration subcommands.
#[derive(Debug, Clone, Subcommand)]
pub(crate) enum PluginsSubcommand {
/// Interactively create or edit built-in plugin configuration in `plugins.toml`.
/// Interactively create or edit built-in and dynamic plugin configuration.
Edit(PluginsEditCommand),
/// Register a manifest-backed dynamic plugin in `plugins.toml`.
Add(PluginsAddCommand),
Expand Down Expand Up @@ -811,28 +811,28 @@ fn load_shared_config(explicit: Option<&PathBuf>) -> Result<ResolvedConfig, CliE
let mut merged = toml::Value::Table(toml::map::Map::new());
let mut config_toml_plugin_sources = Vec::new();
for path in config_paths(explicit) {
if path.exists() {
let raw = std::fs::read_to_string(&path)?;
let parsed = raw
.parse::<toml::Table>()
.map(toml::Value::Table)
.map_err(|error| {
CliError::Config(format!("invalid TOML in {}: {error}", path.display()))
})?;
let legacy_observability = legacy_observability_sections(&parsed);
if !legacy_observability.is_empty() {
return Err(CliError::Config(format!(
"legacy observability config in {} is no longer supported: {}; configure \
observability in plugins.toml with `nemo-relay plugins edit`",
path.display(),
legacy_observability.join(", ")
)));
}
if has_config_toml_plugin_config(&parsed) {
config_toml_plugin_sources.push(path.clone());
}
merge_toml(&mut merged, parsed);
let Some(raw) = read_config_file(&path, explicit.is_some(), "configuration")? else {
continue;
};
let parsed = raw
.parse::<toml::Table>()
.map(toml::Value::Table)
.map_err(|error| {
CliError::Config(format!("invalid TOML in {}: {error}", path.display()))
})?;
let legacy_observability = legacy_observability_sections(&parsed);
if !legacy_observability.is_empty() {
return Err(CliError::Config(format!(
"legacy observability config in {} is no longer supported: {}; configure \
observability in plugins.toml with `nemo-relay plugins edit`",
path.display(),
legacy_observability.join(", ")
)));
}
if has_config_toml_plugin_config(&parsed) {
config_toml_plugin_sources.push(path.clone());
}
merge_toml(&mut merged, parsed);
}
if config_toml_plugin_sources.len() > 1 {
return Err(CliError::Config(format!(
Expand All @@ -856,6 +856,30 @@ fn load_shared_config(explicit: Option<&PathBuf>) -> Result<ResolvedConfig, CliE
Ok(resolved)
}

fn read_config_file(
path: &Path,
required: bool,
description: &str,
) -> Result<Option<String>, CliError> {
match path.try_exists() {
Ok(false) if !required => Ok(None),
Ok(false) => Err(CliError::Config(format!(
"explicit {description} file {} does not exist",
path.display()
))),
Err(error) => Err(CliError::Config(format!(
"failed to inspect {description} file {}: {error}",
path.display()
))),
Ok(true) => std::fs::read_to_string(path).map(Some).map_err(|error| {
CliError::Config(format!(
"failed to read {description} file {}: {error}",
path.display()
))
}),
}
}

/// Returns true if any of the implicit config file locations exists on disk. Used by the
/// easy-path dispatcher to decide whether to launch setup (no config found) or proceed
/// with config-driven settings. Mirrors `config_paths(None)` but only checks existence.
Expand Down Expand Up @@ -1045,10 +1069,9 @@ where
let mut runtime_documents = Vec::new();

for path in &paths {
if !path.exists() {
let Some(raw) = read_config_file(path, false, "plugin configuration")? else {
continue;
}
let raw = std::fs::read_to_string(path)?;
};
let mut parsed = raw
.parse::<toml::Table>()
.map(toml::Value::Table)
Expand Down
55 changes: 31 additions & 24 deletions crates/cli/src/model_pricing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@ use crate::config::{
PricingValidateCommand, ServerArgs, resolve_server_config,
};
use crate::error::CliError;
use crate::plugins::config_io::{
TargetScope, read_plugin_config, target_path, validate_config, write_plugin_config,
};
use crate::plugins::config_io::{PluginConfigDocument, TargetScope, target_path, validate_config};

const PRICING_PLUGIN_KIND: &str = "pricing";

Expand All @@ -37,13 +35,13 @@ pub(crate) fn validate(command: PricingValidateCommand) -> Result<(), CliError>
pub(crate) fn init(command: PricingInitCommand) -> Result<(), CliError> {
let scope = target_pricing_scope(&command.scope)?;
let path = target_path(scope)?;
let mut plugin_config = read_plugin_config(&path)?;
let index = ensure_pricing_component(&mut plugin_config)?;
let pricing_config = pricing_config_from_component(&plugin_config.components[index])?;
store_pricing_config(&mut plugin_config.components[index], &pricing_config)?;
plugin_config.components[index].enabled = true;
validate_config(&plugin_config)?;
write_plugin_config(&path, &plugin_config)?;
update_plugin_config_document(&path, |plugin_config| {
let index = ensure_pricing_component(plugin_config)?;
let pricing_config = pricing_config_from_component(&plugin_config.components[index])?;
store_pricing_config(&mut plugin_config.components[index], &pricing_config)?;
plugin_config.components[index].enabled = true;
Ok(())
})?;
println!("Initialized model pricing config: {}", path.display());
Ok(())
}
Expand All @@ -58,23 +56,22 @@ pub(crate) fn add_source(command: PricingAddSourceCommand) -> Result<(), CliErro
read_pricing_catalog(&source_path)?;
let scope = target_pricing_scope(&command.scope)?;
let path = target_path(scope)?;
let mut plugin_config = read_plugin_config(&path)?;
let index = ensure_pricing_component(&mut plugin_config)?;
let mut pricing_config = pricing_config_from_component(&plugin_config.components[index])?;
let source = PricingSourceConfig::File { path: source_path };

if !pricing_config.sources.contains(&source) {
if command.append {
pricing_config.sources.push(source);
} else {
pricing_config.sources.insert(0, source);
update_plugin_config_document(&path, |plugin_config| {
let index = ensure_pricing_component(plugin_config)?;
let mut pricing_config = pricing_config_from_component(&plugin_config.components[index])?;
if !pricing_config.sources.contains(&source) {
if command.append {
pricing_config.sources.push(source);
} else {
pricing_config.sources.insert(0, source);
}
}
}

store_pricing_config(&mut plugin_config.components[index], &pricing_config)?;
plugin_config.components[index].enabled = true;
validate_config(&plugin_config)?;
write_plugin_config(&path, &plugin_config)?;
store_pricing_config(&mut plugin_config.components[index], &pricing_config)?;
plugin_config.components[index].enabled = true;
Ok(())
})?;
println!(
"Added model pricing source: {} -> {}",
command.path.display(),
Expand All @@ -83,6 +80,16 @@ pub(crate) fn add_source(command: PricingAddSourceCommand) -> Result<(), CliErro
Ok(())
}

fn update_plugin_config_document(
path: &Path,
update: impl FnOnce(&mut PluginConfig) -> Result<(), CliError>,
) -> Result<(), CliError> {
let mut document = PluginConfigDocument::read(path)?;
update(document.config_mut())?;
validate_config(document.config())?;
document.write()
}

pub(crate) fn resolve(command: PricingResolveCommand) -> Result<(), CliError> {
let sources = pricing_catalog_sources_from_current_config()?;
if sources.is_empty() {
Expand Down
Loading
Loading