Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2a6c938
feat: add dynamic plugin lifecycle CLI management
afourniernv Jun 23, 2026
2940171
feat: complete dynamic plugin lifecycle CLI diagnostics
afourniernv Jun 23, 2026
6b9be08
test: harden dynamic plugin lifecycle CLI coverage
afourniernv Jun 23, 2026
90d89c7
refactor: clarify dynamic plugin lifecycle responses
afourniernv Jun 23, 2026
086832f
refactor: harden dynamic plugin target parsing
afourniernv Jun 23, 2026
5e94508
fix: address dynamic plugin lifecycle CLI review nits
afourniernv Jun 24, 2026
ec081cd
Merge remote-tracking branch 'github/main' into afournier/relay-340-a…
afourniernv Jun 24, 2026
a55a87a
refactor: tighten dynamic plugin lifecycle text rendering
afourniernv Jun 24, 2026
cd8c682
refactor: reuse shared plugins toml constant
afourniernv Jun 24, 2026
cf6ddf7
refactor: hide dynamic plugin lifecycle state file
afourniernv Jun 24, 2026
eaf621d
refactor: trim lifecycle mutation command output
afourniernv Jun 24, 2026
b73b13e
fix: resolve lifecycle target manifest refs symmetrically
afourniernv Jun 24, 2026
3757b24
fix: simplify lifecycle mutation output handling
afourniernv Jun 24, 2026
eebfff8
test: raise lifecycle CLI patch coverage
afourniernv Jun 24, 2026
f35f8d1
refactor: derive lifecycle labels with strum
afourniernv Jun 24, 2026
ba501e6
refactor: derive lifecycle scope labels with strum
afourniernv Jun 24, 2026
c993948
refactor: derive lifecycle labels from enum display
afourniernv Jun 24, 2026
eb98a40
Merge remote-tracking branch 'github/main' into afournier/relay-340-a…
afourniernv Jun 24, 2026
518648e
refactor: reduce lifecycle renderer string churn
afourniernv Jun 24, 2026
5d736c3
Merge branch 'main' into afournier/relay-340-add-dynamic-plugin-lifec…
afourniernv Jun 24, 2026
63f460d
refactor: simplify lifecycle output rendering
willkill07 Jun 24, 2026
44411df
test: fix stale launcher config initializers
afourniernv Jun 24, 2026
c595e36
Merge branch 'afournier/relay-340-add-dynamic-plugin-lifecycle-cli-co…
afourniernv Jun 24, 2026
0cd15e9
fix: restore lifecycle inspect and host config semantics
afourniernv Jun 24, 2026
106bdcb
Merge remote-tracking branch 'github/main' into afournier/relay-340-a…
afourniernv Jun 24, 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
58 changes: 58 additions & 0 deletions ATTRIBUTIONS-Rust.md
Original file line number Diff line number Diff line change
Expand Up @@ -33812,6 +33812,64 @@ SOFTWARE.

```

## strum - 0.27.2
**Repository URL**: https://github.com/Peternator7/strum
**License Type(s)**: MIT
### License: https://spdx.org/licenses/MIT.html
```
MIT License

Copyright (c) 2019 Peter Glotfelty

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

```

## strum_macros - 0.27.2
**Repository URL**: https://github.com/Peternator7/strum
**License Type(s)**: MIT
### License: https://spdx.org/licenses/MIT.html
```
MIT License

Copyright (c) 2019 Peter Glotfelty

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

```

## subtle - 2.6.1
**Repository URL**: https://github.com/dalek-cryptography/subtle
**License Type(s)**: BSD-3-Clause
Expand Down
23 changes: 23 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions crates/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ rustls = { version = "0.23", default-features = false, features = ["ring", "std"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_yaml = "0.9"
strum = { version = "0.27", features = ["derive"] }
thiserror = "2"
tokio = { version = "1", features = ["macros", "net", "process", "rt-multi-thread", "signal", "sync", "time"] }
tokio-tungstenite = { version = "0.27", default-features = false, features = ["connect", "rustls-tls-native-roots"] }
Expand Down
135 changes: 127 additions & 8 deletions crates/cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use nemo_relay::plugin::dynamic::DynamicPluginManifest;
use nemo_relay::plugin::{PluginError, merge_plugin_config_documents};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use strum::Display;

use crate::error::CliError;
use crate::plugin_shim::PluginShimCommand;
Expand Down Expand Up @@ -197,11 +198,51 @@ pub(crate) struct PluginsCommand {
pub(crate) command: PluginsSubcommand,
}

#[derive(Debug, Clone, Copy)]
pub(crate) struct PluginJsonContext<'a> {
pub(crate) command: &'static str,
pub(crate) target: Option<&'a str>,
}

/// Plugin configuration subcommands.
#[derive(Debug, Clone, Subcommand)]
pub(crate) enum PluginsSubcommand {
/// Interactively create or edit built-in plugin configuration in `plugins.toml`.
Edit(PluginsEditCommand),
/// Register a manifest-backed dynamic plugin in `plugins.toml`.
Add(PluginsAddCommand),
/// Validate a manifest-backed dynamic plugin by path or installed ID.
Validate(PluginsValidateCommand),
/// List discovered dynamic plugins from the resolved host config.
List(PluginsListCommand),
/// Inspect one discovered dynamic plugin by canonical ID.
Inspect(PluginsInspectCommand),
/// Mark a registered dynamic plugin enabled in desired state.
Enable(PluginsEnableCommand),
/// Mark a registered dynamic plugin disabled in desired state.
Disable(PluginsDisableCommand),
/// Tombstone a registered dynamic plugin and remove its host discovery reference.
Remove(PluginsRemoveCommand),
}

impl PluginsSubcommand {
pub(crate) fn json_context(&self) -> Option<PluginJsonContext<'_>> {
match self {
Self::Validate(command) if command.json => Some(PluginJsonContext {
command: "plugins validate",
target: Some(command.target.as_str()),
}),
Self::List(command) if command.json => Some(PluginJsonContext {
command: "plugins list",
target: None,
}),
Self::Inspect(command) if command.json => Some(PluginJsonContext {
command: "plugins inspect",
target: Some(command.id.as_str()),
}),
_ => None,
}
}
}

/// Args for `nemo-relay pricing`.
Expand Down Expand Up @@ -298,7 +339,7 @@ pub(crate) struct PricingResolveCommand {
.args(["user", "project", "global"])
.multiple(false)
))]
pub(crate) struct PluginsEditCommand {
pub(crate) struct PluginsScopeArgs {
/// Edit the user config at `$XDG_CONFIG_HOME/nemo-relay/plugins.toml`.
#[arg(long)]
pub(crate) user: bool,
Expand All @@ -310,6 +351,74 @@ pub(crate) struct PluginsEditCommand {
pub(crate) global: bool,
}

/// Args for `nemo-relay plugins edit`.
#[derive(Debug, Clone, Default, Args)]
pub(crate) struct PluginsEditCommand {
#[command(flatten)]
pub(crate) scope: PluginsScopeArgs,
}

/// Args for `nemo-relay plugins add`.
#[derive(Debug, Clone, Default, Args)]
pub(crate) struct PluginsAddCommand {
#[command(flatten)]
pub(crate) scope: PluginsScopeArgs,
/// Path to a plugin directory or explicit `relay-plugin.toml`.
pub(crate) path: PathBuf,
}

/// Args for `nemo-relay plugins validate`.
#[derive(Debug, Clone, Args)]
pub(crate) struct PluginsValidateCommand {
/// Canonical plugin ID or a local plugin directory / `relay-plugin.toml` path.
pub(crate) target: String,
/// Emit machine-readable JSON output.
#[arg(long)]
pub(crate) json: bool,
}

/// Args for `nemo-relay plugins list`.
#[derive(Debug, Clone, Default, Args)]
pub(crate) struct PluginsListCommand {
/// Include tombstoned dynamic plugin records in the output.
#[arg(long)]
pub(crate) all: bool,
/// Emit machine-readable JSON output.
#[arg(long)]
pub(crate) json: bool,
}

/// Args for `nemo-relay plugins inspect`.
#[derive(Debug, Clone, Args)]
pub(crate) struct PluginsInspectCommand {
/// Canonical plugin ID.
pub(crate) id: String,
/// Emit machine-readable JSON output.
#[arg(long)]
pub(crate) json: bool,
}

/// Args for `nemo-relay plugins enable`.
#[derive(Debug, Clone, Args)]
pub(crate) struct PluginsEnableCommand {
/// Canonical plugin ID.
pub(crate) id: String,
}

/// Args for `nemo-relay plugins disable`.
#[derive(Debug, Clone, Args)]
pub(crate) struct PluginsDisableCommand {
/// Canonical plugin ID.
pub(crate) id: String,
}

/// Args for `nemo-relay plugins remove`.
#[derive(Debug, Clone, Args)]
pub(crate) struct PluginsRemoveCommand {
/// Canonical plugin ID.
pub(crate) id: String,
}

#[derive(Debug, Clone, Default, Args)]
pub(crate) struct ServerArgs {
/// Path to an explicit config file (disables auto-discovery of workspace/global/system)
Expand Down Expand Up @@ -488,22 +597,24 @@ pub(crate) struct ResolvedDynamicPluginConfig {
pub(crate) plugin_id: String,
pub(crate) manifest_ref: String,
pub(crate) config: Map<String, Value>,
pub(crate) has_explicit_config: bool,
pub(crate) source: PathBuf,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Display)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub(crate) enum DynamicPluginHostConfigStatus {
Absent,
Present,
}

impl ResolvedDynamicPluginConfig {
pub(crate) fn host_config_status(&self) -> DynamicPluginHostConfigStatus {
if self.config.is_empty() {
DynamicPluginHostConfigStatus::Absent
} else {
if self.has_explicit_config {
DynamicPluginHostConfigStatus::Present
} else {
DynamicPluginHostConfigStatus::Absent
}
}
}
Expand Down Expand Up @@ -623,6 +734,13 @@ pub(crate) fn resolve_server_config(args: &ServerArgs) -> Result<ResolvedConfig,
Ok(resolved)
}

/// Resolves shared config for plugin-facing CLI commands without mutating gateway runtime fields.
pub(crate) fn resolve_plugins_config(
explicit: Option<&PathBuf>,
) -> Result<ResolvedConfig, CliError> {
load_shared_config(explicit)
}

/// Resolves transparent `run` configuration and switches the gateway to an ephemeral bind address.
///
/// Explicit run arguments override inherited top-level server flags, which override shared config.
Expand Down Expand Up @@ -715,7 +833,7 @@ fn apply_server_overrides(config: &mut GatewayConfig, args: &ServerArgs) -> Resu
Ok(())
}

const PLUGINS_TOML: &str = "plugins.toml";
pub(crate) const PLUGINS_TOML: &str = "plugins.toml";

// Loads config from the ordered shared locations, deep-merges TOML tables, maps the typed file
// shape onto runtime structs, applies a sibling/discovered plugins.toml when present, then lets
Expand Down Expand Up @@ -937,7 +1055,7 @@ struct PluginTomlPluginsSection {
struct FileDynamicPluginConfig {
manifest: String,
#[serde(default)]
config: Map<String, Value>,
config: Option<Map<String, Value>>,
}

fn load_plugin_toml_config(
Expand Down Expand Up @@ -1068,7 +1186,8 @@ fn resolve_dynamic_plugin_refs(
resolved.push(ResolvedDynamicPluginConfig {
plugin_id,
manifest_ref,
config: dynamic.config,
has_explicit_config: dynamic.config.is_some(),
config: dynamic.config.unwrap_or_default(),
source: source.to_path_buf(),
});
}
Expand Down
32 changes: 32 additions & 0 deletions crates/cli/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,18 @@ use axum::Json;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use nemo_relay::error::FlowError;
use serde::Serialize;
use serde_json::{Map, Value, json};
use strum::Display;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Display)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub(crate) enum PluginLifecycleFailureKind {
Failed,
NotFound,
Refused,
}

#[derive(Debug, thiserror::Error)]
pub(crate) enum CliError {
Expand All @@ -27,6 +38,13 @@ pub(crate) enum CliError {
Config(String),
#[error("launcher error: {0}")]
Launch(String),
#[error("{message}")]
PluginLifecycle {
command: &'static str,
target: Option<String>,
kind: PluginLifecycleFailureKind,
message: String,
},
#[error("NeMo Relay runtime error: {0}")]
Flow(#[from] nemo_relay::error::FlowError),
#[error("openinference error: {0}")]
Expand All @@ -41,6 +59,20 @@ impl CliError {
_ => None,
}
}

pub(crate) fn plugin_lifecycle(
&self,
) -> Option<(&'static str, Option<&str>, PluginLifecycleFailureKind, &str)> {
match self {
Self::PluginLifecycle {
command,
target,
kind,
message,
} => Some((command, target.as_deref(), *kind, message.as_str())),
_ => None,
}
}
}

impl IntoResponse for CliError {
Expand Down
Loading
Loading