commit 70de1195c050823a05ec79b3526adcbed728a6f7
parent af8f36aa91b2e08fc2b4e8b7928d8f6304ff1288
Author: triesap <tyson@radroots.org>
Date: Tue, 25 Aug 2026 08:16:04 +0000
api: seal the daemon entrypoint boundary
- hide implementation modules behind a root-only facade
- expose a stable source-free daemon run error
- remove unused state and registry capabilities
- freeze and verify the exact standalone API surface
Diffstat:
20 files changed, 157 insertions(+), 72 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
@@ -79,8 +79,9 @@
--check`, `cargo check --workspace --all-targets --locked`, `cargo test
--workspace --all-targets --locked`, `cargo clippy --workspace --all-targets
--locked -- -D warnings`, warnings-denied Rustdoc, and
- `scripts/verify-supply-chain.sh`. Route them through extbuild when it is
- enabled. Nix and OCI remain deferred and unclaimed.
+ `scripts/verify-boundaries.sh` plus `scripts/verify-supply-chain.sh`. Route
+ them through extbuild when it is enabled. Nix and OCI remain deferred and
+ unclaimed.
- The supply-chain gate uses exact cargo-deny 0.19.8 and cargo-vet 0.10.2. Its
checked-in exemptions are visible accepted review debt, not claims of
independent source audits; only the justified Nostr 0.44 advisory exceptions
diff --git a/README b/README
@@ -21,12 +21,15 @@ cargo check --workspace --all-targets --locked
cargo test --workspace --all-targets --locked
cargo clippy --workspace --all-targets --locked -- -D warnings
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps --locked
+scripts/verify-boundaries.sh
scripts/verify-supply-chain.sh
```
-The supply-chain command checks the locked dependency, license, advisory, and
-immutable public-source policy. Nix and OCI are deferred and unclaimed through
-RCLD-RSHR-170.
+The boundary command byte-compares the root-only public API against its
+reviewed baseline and rejects forbidden repository roots or credential
+material. The supply-chain command checks the locked dependency, license,
+advisory, and immutable public-source policy. Nix and OCI are deferred and
+unclaimed through RCLD-RSHR-170.
## Copyright
diff --git a/contracts/api_baselines/radrootsd.txt b/contracts/api_baselines/radrootsd.txt
@@ -0,0 +1,9 @@
+pub mod radrootsd
+pub struct radrootsd::RadrootsdRunError
+impl core::error::Error for radrootsd::RadrootsdRunError
+impl core::fmt::Debug for radrootsd::RadrootsdRunError
+pub fn radrootsd::RadrootsdRunError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+impl core::fmt::Display for radrootsd::RadrootsdRunError
+pub fn radrootsd::RadrootsdRunError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub const fn radrootsd::crate_name() -> &'static str
+pub async fn radrootsd::run_daemon() -> core::result::Result<(), radrootsd::RadrootsdRunError>
diff --git a/scripts/verify-boundaries.sh b/scripts/verify-boundaries.sh
@@ -0,0 +1,26 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+repo_root="$(git rev-parse --show-toplevel)"
+cd "$repo_root"
+
+for forbidden_root in docs .github .act; do
+ test ! -e "$forbidden_root"
+ test ! -L "$forbidden_root"
+done
+test "$(cargo public-api --version)" = "cargo-public-api 0.52.0"
+temporary_api="$(mktemp)"
+trap 'rm -f "$temporary_api"' EXIT
+cargo +nightly-2026-07-16 public-api --all-features -sss -p radrootsd >"$temporary_api"
+cmp "$temporary_api" contracts/api_baselines/radrootsd.txt
+
+if git ls-files | grep -E -i '(^|/)(\.env|id_rsa|id_ed25519|credentials|[^/]+\.(pem|key|p12|pfx|jks|keystore))$' >/dev/null; then
+ echo "boundary_invalid: sensitive credential path is tracked" >&2
+ exit 1
+fi
+if git grep -I -n -E -e '-----BEGIN ([A-Z0-9 ]+ )?PRIVATE KEY-----|AKIA[0-9A-Z]{16}|gh[pousr]_[A-Za-z0-9_]{36,}|nsec1[023456789acdefghjklmnpqrstuvwxyz]{40,}' -- src >/dev/null; then
+ echo "boundary_invalid: production source contains credential material" >&2
+ exit 1
+fi
+
+echo "boundary ok: root-only API, fresh baseline, no forbidden or credential surface"
diff --git a/src/app/config.rs b/src/app/config.rs
@@ -4,9 +4,11 @@ use radroots_event::profile::{AuthoredProfile, Nip05Identifier};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
+#[cfg(not(test))]
+use super::paths::process_path_selection;
use super::paths::{
PathProfile, PathResolver, RadrootsdRuntimePaths, default_transport_publish_database_path,
- process_path_selection, resolve_runtime_paths_with_resolver,
+ resolve_runtime_paths_with_resolver,
};
#[derive(Debug, Serialize, Deserialize, Clone)]
@@ -224,6 +226,7 @@ fn load_settings_from_path_with_resolver(
Ok(settings)
}
+#[cfg(not(test))]
pub fn load_settings_from_path(path: impl AsRef<Path>) -> Result<Settings> {
let path = path.as_ref();
let (profile, repo_local_root) = process_path_selection()?;
diff --git a/src/app/mod.rs b/src/app/mod.rs
@@ -4,6 +4,4 @@ pub(crate) mod identity_storage;
mod paths;
mod runtime;
-pub use cli::Args;
-pub use config::Settings;
-pub use runtime::run;
+pub(crate) use runtime::run;
diff --git a/src/app/runtime.rs b/src/app/runtime.rs
@@ -565,7 +565,6 @@ pub async fn run() -> Result<()> {
)?;
let radrootsd = Radrootsd::new(
identity.clone(),
- settings.metadata.clone(),
settings.config.transport_publish.clone(),
settings.config.nip46.clone(),
);
@@ -736,7 +735,6 @@ mod tests {
let identity = DaemonIdentity::generate();
let state = Radrootsd::new(
identity,
- settings.metadata.clone(),
settings.config.transport_publish.clone(),
settings.config.nip46.clone(),
)
diff --git a/src/core/nip46/mod.rs b/src/core/nip46/mod.rs
@@ -1,3 +1,3 @@
-pub mod session;
+pub(crate) mod session;
-pub use session::{Nip46Session, Nip46SessionStore};
+pub(crate) use session::Nip46Session;
diff --git a/src/core/state.rs b/src/core/state.rs
@@ -1,5 +1,5 @@
use crate::app::identity_storage::DaemonIdentity;
-use crate::host_nostr::{DaemonNostrClient, Keys, Metadata, PublicKey};
+use crate::host_nostr::{DaemonNostrClient, Keys, PublicKey};
use anyhow::Result;
use crate::app::config::{Nip46Config, TransportPublishConfig};
@@ -10,8 +10,6 @@ pub struct Radrootsd {
pub(crate) client: DaemonNostrClient,
pub keys: Keys,
pub pubkey: PublicKey,
- pub metadata: Metadata,
- pub info: serde_json::Value,
pub transport_publish: TransportPublish,
pub(crate) nip46_sessions: crate::core::nip46::session::Nip46SessionStore,
pub nip46_config: Nip46Config,
@@ -20,17 +18,12 @@ pub struct Radrootsd {
impl Radrootsd {
pub(crate) fn new(
identity: DaemonIdentity,
- metadata: Metadata,
transport_publish_config: TransportPublishConfig,
nip46_config: Nip46Config,
) -> Result<Self> {
let keys: Keys = identity.keys().clone();
let pubkey = keys.public_key();
let client = DaemonNostrClient::with_keys(keys.clone());
- let info = serde_json::json!({
- "version": env!("CARGO_PKG_VERSION"),
- "build": option_env!("GIT_HASH").unwrap_or("unknown"),
- });
#[cfg(test)]
let transport_publish = TransportPublish::memory(transport_publish_config)?;
#[cfg(not(test))]
@@ -41,8 +34,6 @@ impl Radrootsd {
client,
keys,
pubkey,
- metadata,
- info,
transport_publish,
nip46_sessions,
nip46_config,
@@ -55,32 +46,21 @@ mod tests {
use super::Radrootsd;
use crate::app::config::{Nip46Config, TransportPublishConfig};
use crate::app::identity_storage::DaemonIdentity;
- use crate::host_nostr::Metadata;
#[test]
fn new_sets_core_fields() {
let identity = DaemonIdentity::generate();
- let metadata: Metadata =
- serde_json::from_str(r#"{"name":"radrootsd-test"}"#).expect("metadata");
let transport_publish_cfg = TransportPublishConfig::default();
let cfg = Nip46Config::default();
- let state = Radrootsd::new(
- identity.clone(),
- metadata.clone(),
- transport_publish_cfg.clone(),
- cfg.clone(),
- )
- .expect("state");
+ let state = Radrootsd::new(identity.clone(), transport_publish_cfg.clone(), cfg.clone())
+ .expect("state");
assert_eq!(state.pubkey, identity.public_key());
- assert_eq!(state.metadata, metadata);
assert_eq!(
state.transport_publish.config.enabled,
transport_publish_cfg.enabled
);
assert_eq!(state.nip46_config.session_ttl_secs, cfg.session_ttl_secs);
assert_eq!(state.nip46_config.perms, cfg.perms);
- assert_eq!(state.info["version"], env!("CARGO_PKG_VERSION"));
- assert_eq!(state.info["build"], "unknown");
}
}
diff --git a/src/core/transport_publish.rs b/src/core/transport_publish.rs
@@ -170,6 +170,7 @@ pub struct TransportPublish {
}
impl TransportPublish {
+ #[cfg(not(test))]
pub fn open(config: TransportPublishConfig) -> Result<Self, TransportPublishError> {
let store = TransportPublishStore::open(config.database_path.clone())?;
let publish_jobs = Arc::new(Semaphore::new(config.max_concurrent_publish_jobs));
@@ -183,6 +184,7 @@ impl TransportPublish {
})
}
+ #[cfg(test)]
pub fn memory(config: TransportPublishConfig) -> Result<Self, TransportPublishError> {
let store = TransportPublishStore::memory()?;
let publish_jobs = Arc::new(Semaphore::new(config.max_concurrent_publish_jobs));
@@ -992,6 +994,7 @@ impl TransportPublishStore {
Self::from_connection(connection)
}
+ #[cfg(test)]
pub fn memory() -> Result<Self, TransportPublishError> {
Self::from_connection(connect_sqlite(SqliteConnectOptions::new().in_memory(true))?)
}
diff --git a/src/lib.rs b/src/lib.rs
@@ -1,11 +1,39 @@
#![forbid(unsafe_code)]
#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
-pub mod app;
-pub mod core;
+use std::{error::Error, fmt};
+
+mod app;
+mod core;
mod host_nostr;
#[cfg_attr(coverage_nightly, coverage(off))]
-pub mod transport;
+mod transport;
+
+/// Stable, redacted failure returned by the daemon entry point.
+pub struct RadrootsdRunError {
+ _private: (),
+}
+
+impl fmt::Debug for RadrootsdRunError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str("RadrootsdRunError")
+ }
+}
+
+impl fmt::Display for RadrootsdRunError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str("daemon execution failed")
+ }
+}
+
+impl Error for RadrootsdRunError {}
+
+/// Runs the daemon without exposing implementation-owned modules or errors.
+pub async fn run_daemon() -> Result<(), RadrootsdRunError> {
+ app::run()
+ .await
+ .map_err(|_source| RadrootsdRunError { _private: () })
+}
pub const fn crate_name() -> &'static str {
env!("CARGO_PKG_NAME")
@@ -14,8 +42,18 @@ pub const fn crate_name() -> &'static str {
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
+ use std::error::Error;
+
#[test]
fn crate_name_matches_package() {
assert_eq!(super::crate_name(), "radrootsd");
}
+
+ #[test]
+ fn run_error_is_source_free_and_redacted() {
+ let error = super::RadrootsdRunError { _private: () };
+ assert_eq!(format!("{error}"), "daemon execution failed");
+ assert_eq!(format!("{error:?}"), "RadrootsdRunError");
+ assert!(error.source().is_none());
+ }
}
diff --git a/src/main.rs b/src/main.rs
@@ -56,7 +56,7 @@ async fn run() -> Result<()> {
#[cfg(not(test))]
#[cfg_attr(coverage_nightly, coverage(off))]
async fn run() -> Result<()> {
- radrootsd::app::run().await
+ radrootsd::run_daemon().await.map_err(anyhow::Error::new)
}
#[cfg(test)]
diff --git a/src/transport/jsonrpc/context.rs b/src/transport/jsonrpc/context.rs
@@ -2,16 +2,13 @@
use crate::core::Radrootsd;
-use super::registry::MethodRegistry;
-
#[derive(Clone)]
pub struct RpcContext {
pub state: Radrootsd,
- pub methods: MethodRegistry,
}
impl RpcContext {
- pub fn new(state: Radrootsd, methods: MethodRegistry) -> Self {
- Self { state, methods }
+ pub fn new(state: Radrootsd) -> Self {
+ Self { state }
}
}
diff --git a/src/transport/jsonrpc/methods/mod.rs b/src/transport/jsonrpc/methods/mod.rs
@@ -25,7 +25,6 @@ pub fn register_all(
#[cfg(test)]
mod tests {
use crate::app::identity_storage::DaemonIdentity;
- use crate::host_nostr::Metadata;
use jsonrpsee::server::RpcModule;
use radroots_protocol::radrootsd::transport_publish::v5::RETICULUM_UNAVAILABLE_MESSAGE as RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE;
@@ -41,8 +40,6 @@ mod tests {
fn state(transport_publish_enabled: bool, nip46_public_jsonrpc_enabled: bool) -> Radrootsd {
let identity = DaemonIdentity::generate();
- let metadata: Metadata =
- serde_json::from_str(r#"{"name":"radrootsd-test"}"#).expect("metadata");
let transport_publish = TransportPublishConfig {
enabled: transport_publish_enabled,
..TransportPublishConfig::default()
@@ -51,13 +48,13 @@ mod tests {
public_jsonrpc_enabled: nip46_public_jsonrpc_enabled,
..Nip46Config::default()
};
- Radrootsd::new(identity, metadata, transport_publish, nip46).expect("state")
+ Radrootsd::new(identity, transport_publish, nip46).expect("state")
}
#[test]
fn register_all_exposes_transport_publish_methods_by_default() {
let registry = MethodRegistry::default();
- let ctx = RpcContext::new(state(true, false), registry.clone());
+ let ctx = RpcContext::new(state(true, false));
let mut root = RpcModule::new(ctx.clone());
register_all(&mut root, ctx, registry).expect("register");
@@ -75,7 +72,7 @@ mod tests {
#[test]
fn register_all_exposes_nip46_when_public_jsonrpc_is_enabled() {
let registry = MethodRegistry::default();
- let ctx = RpcContext::new(state(true, true), registry.clone());
+ let ctx = RpcContext::new(state(true, true));
let mut root = RpcModule::new(ctx.clone());
register_all(&mut root, ctx, registry).expect("register");
@@ -86,7 +83,7 @@ mod tests {
#[tokio::test]
async fn publish_capabilities_rejects_unauthenticated_requests() {
let registry = MethodRegistry::default();
- let ctx = RpcContext::new(state(true, false), registry.clone());
+ let ctx = RpcContext::new(state(true, false));
let mut root = RpcModule::new(ctx.clone());
register_all(&mut root, ctx, registry).expect("register");
@@ -103,7 +100,7 @@ mod tests {
#[tokio::test]
async fn publish_capabilities_accepts_authenticated_requests() {
let registry = MethodRegistry::default();
- let ctx = RpcContext::new(state(true, false), registry.clone());
+ let ctx = RpcContext::new(state(true, false));
let principal = ctx
.state
.transport_publish
diff --git a/src/transport/jsonrpc/methods/transport_publish.rs b/src/transport/jsonrpc/methods/transport_publish.rs
@@ -134,7 +134,7 @@ mod tests {
use crate::core::transport_publish::{
PublishJobVisibility, PublishPrincipalInit, generate_bearer_token, hash_bearer_token,
};
- use crate::host_nostr::{Metadata, Timestamp};
+ use crate::host_nostr::Timestamp;
use crate::transport::jsonrpc::auth::{
TransportPublishAuthorization, authorize_transport_publish_request,
};
@@ -164,11 +164,8 @@ mod tests {
) -> (RpcModule<RpcContext>, RpcContext, String, String) {
let identity = DaemonIdentity::generate();
let signed_event = signed_event(&identity);
- let metadata: Metadata =
- serde_json::from_str(r#"{"name":"radrootsd-test"}"#).expect("metadata");
let state = Radrootsd::new(
identity.clone(),
- metadata,
transport_publish_config,
Nip46Config::default(),
)
@@ -200,7 +197,7 @@ mod tests {
})
.expect("principal");
let registry = MethodRegistry::default();
- let ctx = RpcContext::new(state, registry.clone());
+ let ctx = RpcContext::new(state);
let mut module = module(ctx.clone(), registry).expect("module");
module
.extensions_mut()
diff --git a/src/transport/jsonrpc/mod.rs b/src/transport/jsonrpc/mod.rs
@@ -29,7 +29,7 @@ pub async fn start_rpc(
) -> Result<ServerHandle> {
state.transport_publish.config.validate()?;
let registry = MethodRegistry::default();
- let ctx = RpcContext::new(state, registry.clone());
+ let ctx = RpcContext::new(state);
let transport_publish_store = ctx.state.transport_publish.store.clone();
let mut root = RpcModule::new(ctx.clone());
diff --git a/src/transport/jsonrpc/registry.rs b/src/transport/jsonrpc/registry.rs
@@ -16,8 +16,4 @@ impl MethodRegistry {
methods.push(name.to_string());
methods.sort();
}
-
- pub fn list(&self) -> Vec<String> {
- self.inner.read().unwrap_or_else(|e| e.into_inner()).clone()
- }
}
diff --git a/src/transport/jsonrpc/server.rs b/src/transport/jsonrpc/server.rs
@@ -131,7 +131,7 @@ mod tests {
PublishJobVisibility, PublishPrincipalInit, PublishRelayResolveFuture,
PublishRelayResolver, generate_bearer_token, hash_bearer_token,
};
- use crate::host_nostr::{Metadata, Timestamp};
+ use crate::host_nostr::Timestamp;
use crate::transport::jsonrpc::methods;
use crate::transport::jsonrpc::{MethodRegistry, RpcContext};
use crate::transport::relay_publish::MockRelayPublishAdapter as RadrootsMockRelayPublishAdapter;
@@ -192,11 +192,8 @@ mod tests {
RadrootsMockRelayPublishAdapter,
) {
let identity = DaemonIdentity::generate();
- let metadata: Metadata =
- serde_json::from_str(r#"{"name":"radrootsd-test"}"#).expect("metadata");
let mut state = Radrootsd::new(
identity.clone(),
- metadata,
transport_publish_config,
Nip46Config::default(),
)
@@ -274,7 +271,7 @@ mod tests {
let addr = unused_addr();
let store = state.transport_publish.store.clone();
let registry = MethodRegistry::default();
- let ctx = RpcContext::new(state, registry.clone());
+ let ctx = RpcContext::new(state);
let mut root = RpcModule::new(ctx.clone());
methods::register_all(&mut root, ctx, registry).expect("register methods");
let handle = start_server(addr, &rpc_cfg, store, root)
diff --git a/src/transport/nostr/listener.rs b/src/transport/nostr/listener.rs
@@ -1,24 +1,34 @@
+#[cfg(not(test))]
use std::time::Duration;
-use anyhow::{Result, anyhow};
+use anyhow::Result;
+#[cfg(not(test))]
+use anyhow::anyhow;
+#[cfg(not(test))]
use nostr::JsonUtil;
use nostr::nips::nip04;
use nostr::nips::nip44;
-use nostr::nips::nip46::{
- NostrConnectMessage, NostrConnectRequest, NostrConnectResponse, ResponseResult,
-};
+#[cfg(not(test))]
+use nostr::nips::nip46::NostrConnectMessage;
+use nostr::nips::nip46::{NostrConnectRequest, NostrConnectResponse, ResponseResult};
+#[cfg(not(test))]
use tokio::sync::broadcast;
+#[cfg(not(test))]
use tracing::{info, warn};
use crate::core::nip46::session::{
Nip46Session, PendingNostrRequest, session_expires_at, sign_event_allowed,
};
use crate::core::state::Radrootsd;
+#[cfg(not(test))]
use crate::host_nostr::{Filter, Kind, RelayPoolNotification, Timestamp, with_filter_tag};
+#[cfg(not(test))]
use crate::transport::nostr::protocol::sign_nip46_message;
+#[cfg(not(test))]
const DEFAULT_TIMEOUT_SECS: u64 = 10;
+#[cfg(not(test))]
pub fn spawn_nip46_listener(radrootsd: Radrootsd) {
tokio::spawn(async move {
if let Err(error) = run_nip46_listener(radrootsd).await {
@@ -27,6 +37,7 @@ pub fn spawn_nip46_listener(radrootsd: Radrootsd) {
});
}
+#[cfg(not(test))]
async fn run_nip46_listener(radrootsd: Radrootsd) -> Result<()> {
radrootsd.client.connect().await;
radrootsd
diff --git a/tests/package_boundary.rs b/tests/package_boundary.rs
@@ -0,0 +1,31 @@
+#![forbid(unsafe_code)]
+
+const ROOT: &str = include_str!("../src/lib.rs");
+const PUBLIC_API: &str = include_str!("../contracts/api_baselines/radrootsd.txt");
+
+#[test]
+fn implementation_modules_are_private_and_api_is_owned() {
+ for module in ["app", "core", "host_nostr", "transport"] {
+ assert!(ROOT.contains(&format!("mod {module};")));
+ assert!(!ROOT.contains(&format!("pub mod {module};")));
+ assert!(!PUBLIC_API.contains(&format!("radrootsd::{module}::")));
+ }
+ assert!(PUBLIC_API.contains("pub struct radrootsd::RadrootsdRunError"));
+ assert!(PUBLIC_API.contains("pub async fn radrootsd::run_daemon()"));
+ for dependency in ["anyhow::", "jsonrpsee::", "nostr::", "sqlx::"] {
+ assert!(!PUBLIC_API.contains(dependency), "leaked {dependency}");
+ }
+}
+
+#[test]
+fn public_error_is_redacted_and_source_free() {
+ let source = std::fs::read_to_string("src/lib.rs").expect("root source");
+ for required in [
+ "formatter.write_str(\"RadrootsdRunError\")",
+ "formatter.write_str(\"daemon execution failed\")",
+ "impl Error for RadrootsdRunError {}",
+ ] {
+ assert!(source.contains(required), "missing {required}");
+ }
+ assert!(!PUBLIC_API.contains("std::io::Error"));
+}