lsp_plugin: add dev-eneabled flag for client
What changed, and why it matters
This commit adds an opt-in 'dev-lsps-client-enabled' flag so the experimental LSPS client plugin only runs when explicitly enabled. It also renames the existing service flag for consistency and moves the service setup to happen only after the flag is checked. There is no direct security fix here; it is a hardening/guardrail change to prevent an unfinished experimental feature from being active by default.
No urgent action required. Treat as routine hardening for an experimental plugin. If deploying LSPS functionality, ensure both client and service flags are documented and intentionally enabled.
Security signals we found
Experimental feature now gated behind explicit opt-in flag
Plugin disables itself cleanly when the required dev option is absent
Service state and RPC server construction moved after enablement check
No input validation, cryptographic, or memory-safety changes present
Evidence from the diff
The patch changes the LSPS plugin’s client and service binaries to require explicit dev flags (‘dev-lsps-client-enabled’ and ‘dev-lsps-service-enabled’) before starting. Previously the service had a default-false boolean option, but the client had no enable gate. The commit also defers construction of the JSON-RPC server/state until after the option is verified, and updates the Python test to pass both flags. This reduces accidental exposure of experimental code but does not patch a known vulnerability.
Changed components
plugins/lsps-plugin/src/client.rsplugins/lsps-plugin/src/service.rstests/test_cln_lsps.pyInspect captured patch +27 / −18
diff --git a/plugins/lsps-plugin/src/client.rs b/plugins/lsps-plugin/src/client.rs
index 92b5a9e1..ce39d655 100644
--- a/plugins/lsps-plugin/src/client.rs
+++ b/plugins/lsps-plugin/src/client.rs
@@ -4,10 +4,17 @@ use cln_lsps::lsps0::{
self,
transport::{Bolt8Transport, CustomMessageHookManager, WithCustomMessageHookManager},
};
+use cln_plugin::options;
use log::debug;
use serde::Deserialize;
use std::path::Path;
+/// An option to enable this service.
+const OPTION_ENABLED: options::FlagConfigOption = options::ConfigOption::new_flag(
+ "dev-lsps-client-enabled",
+ "Enables an LSPS client on the node.",
+);
+
#[derive(Clone)]
struct State {
hook_manager: CustomMessageHookManager,
@@ -26,14 +33,22 @@ async fn main() -> Result<(), anyhow::Error> {
if let Some(plugin) = cln_plugin::Builder::new(tokio::io::stdin(), tokio::io::stdout())
.hook("custommsg", CustomMessageHookManager::on_custommsg::<State>)
+ .option(OPTION_ENABLED)
.rpcmethod(
"lsps-listprotocols",
"list protocols supported by lsp",
on_lsps_listprotocols,
)
- .start(state)
+ .configure()
.await?
{
+ if !plugin.option(&OPTION_ENABLED)? {
+ return plugin
+ .disable(&format!("`{}` not enabled", OPTION_ENABLED.name))
+ .await;
+ }
+
+ let plugin = plugin.start(state).await?;
plugin.join().await
} else {
Ok(())
diff --git a/plugins/lsps-plugin/src/service.rs b/plugins/lsps-plugin/src/service.rs
index c8907ae9..a350a3d9 100644
--- a/plugins/lsps-plugin/src/service.rs
+++ b/plugins/lsps-plugin/src/service.rs
@@ -16,14 +16,9 @@ use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::Arc;
-/// An option to enable this service. It defaults to `false` as we don't want a
-/// node to be an LSP per default.
-/// If a user want's to run an LSP service on their node this has to explicitly
-/// set to true. We keep this as a dev option for now until it actually does
-/// something.
-const OPTION_ENABLED: options::DefaultBooleanConfigOption = ConfigOption::new_bool_with_default(
- "dev-lsps-service",
- false,
+/// An option to enable this service.
+const OPTION_ENABLED: options::FlagConfigOption = ConfigOption::new_flag(
+ "dev-lsps-service-enabled",
"Enables an LSPS service on the node.",
);
@@ -34,14 +29,6 @@ struct State {
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
- let lsps_service = JsonRpcServer::builder()
- .with_handler(
- Lsps0listProtocolsRequest::METHOD.to_string(),
- Arc::new(Lsps0ListProtocolsHandler),
- )
- .build();
- let state = State { lsps_service };
-
if let Some(plugin) = cln_plugin::Builder::new(tokio::io::stdin(), tokio::io::stdout())
.option(OPTION_ENABLED)
.hook("custommsg", on_custommsg)
@@ -54,6 +41,13 @@ async fn main() -> Result<(), anyhow::Error> {
.await;
}
+ let lsps_builder = JsonRpcServer::builder().with_handler(
+ Lsps0listProtocolsRequest::METHOD.to_string(),
+ Arc::new(Lsps0ListProtocolsHandler {}),
+ );
+ let lsps_service = lsps_builder.build();
+
+ let state = State { lsps_service };
let plugin = plugin.start(state).await?;
plugin.join().await
} else {
diff --git a/tests/test_cln_lsps.py b/tests/test_cln_lsps.py
index 28f09d64..5803d934 100644
--- a/tests/test_cln_lsps.py
+++ b/tests/test_cln_lsps.py
@@ -21,7 +21,7 @@ def test_lsps_service_disabled(node_factory):
@unittest.skipUnless(RUST, 'RUST is not enabled')
def test_lsps0_listprotocols(node_factory):
l1, l2 = node_factory.get_nodes(2, opts=[
- {}, {"dev-lsps-service": True}
+ {"dev-lsps-client-enabled": None}, {"dev-lsps-service-enabled": None}
])
# We don't need a channel to query for lsps services
Why this scored 18/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.