use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
use net_sdk::deck::{DeckClient, OperatorIdentity};
use net_sdk::meshos::{EntityKeypair, LoggingDispatcher, MeshOsConfig, MeshOsDaemonSdk};
use crate::config::Profile;
use crate::error::{connection_failure, generic, invalid_args, sdk, CliError};
use crate::parsers::{hex_decode_32, parse_u64_flexible};
use crate::secret::{zeroize_string, ScrubbedBytes, ScrubbedString};
#[derive(Debug, Clone)]
pub struct RemoteAttach {
pub addr: SocketAddr,
pub public_key: [u8; 32],
pub node_id: u64,
pub psk: [u8; 32],
}
pub struct CliContext {
_sdk: MeshOsDaemonSdk,
deck: Arc<DeckClient>,
identity: OperatorIdentity,
mesh: Option<net_sdk::Mesh>,
}
impl CliContext {
pub fn deck(&self) -> Arc<DeckClient> {
Arc::clone(&self.deck)
}
pub fn identity(&self) -> &OperatorIdentity {
&self.identity
}
#[allow(dead_code)]
pub fn mesh_node(&self) -> Option<Arc<net_sdk::MeshNode>> {
self.mesh.as_ref().map(|m| m.node_arc())
}
pub fn mesh(&self) -> Option<&net_sdk::Mesh> {
self.mesh.as_ref()
}
pub fn require_mesh(&self) -> Result<&net_sdk::Mesh, CliError> {
self.mesh().ok_or_else(|| {
crate::error::sdk(
"internal: remote-attach context returned no mesh — \
build_with_remote always populates it, this should be unreachable",
)
})
}
#[allow(dead_code)]
pub fn require_mesh_node(&self) -> Result<Arc<net_sdk::MeshNode>, CliError> {
self.mesh_node().ok_or_else(|| {
crate::error::sdk(
"internal: remote-attach context returned no mesh — \
build_with_remote always populates it, this should be unreachable",
)
})
}
pub async fn build(
profile: &Profile,
identity_override: Option<&Path>,
node_id: u64,
require_identity: bool,
) -> Result<Self, CliError> {
Self::build_inner(profile, identity_override, node_id, require_identity, None).await
}
#[allow(dead_code)]
pub async fn build_with_remote(
profile: &Profile,
identity_override: Option<&Path>,
node_id: u64,
require_identity: bool,
remote: RemoteAttach,
) -> Result<Self, CliError> {
Self::build_inner(
profile,
identity_override,
node_id,
require_identity,
Some(remote),
)
.await
}
async fn build_inner(
profile: &Profile,
identity_override: Option<&Path>,
node_id: u64,
require_identity: bool,
remote: Option<RemoteAttach>,
) -> Result<Self, CliError> {
if let Some(endpoint) = profile.endpoint.as_deref() {
if endpoint != "in-process" {
return Err(invalid_args(format!(
"endpoint `{endpoint}` is not supported in this build; \
only `in-process` is available until the substrate \
remote-attach surface lands"
)));
}
}
let keypair = match identity_override.or(profile.identity.as_deref()) {
Some(path) => load_identity_keypair(path).await?,
None => {
if require_identity {
return Err(invalid_args(
"no operator identity configured; pass --identity <PATH> \
or set `identity = \"...\"` under your profile in the \
config file. Admin / ICE commits refuse to sign with \
an ephemeral keypair.",
));
}
tracing::warn!(
"no operator identity configured; using an ephemeral \
keypair. Run `net-mesh identity generate --out <PATH>` and \
point your profile at the result for stable operator id."
);
EntityKeypair::generate()
}
};
let mut cfg = MeshOsConfig::default();
cfg.this_node = node_id;
cfg.tick_interval = Duration::from_millis(250);
let dispatcher = Arc::new(LoggingDispatcher::new());
let sdk = MeshOsDaemonSdk::start(cfg, dispatcher);
let identity = OperatorIdentity::from_keypair(keypair);
let deck = Arc::new(DeckClient::from_runtime(sdk.runtime(), identity.clone()));
let mesh = match remote {
Some(remote) => Some(build_remote_mesh(remote).await?),
None => None,
};
Ok(Self {
_sdk: sdk,
deck,
identity,
mesh,
})
}
}
async fn build_remote_mesh(remote: RemoteAttach) -> Result<net_sdk::Mesh, CliError> {
build_attached_mesh("127.0.0.1:0", None, &remote).await
}
pub(crate) async fn build_attached_mesh(
bind: &str,
identity: Option<net_sdk::identity::Identity>,
remote: &RemoteAttach,
) -> Result<net_sdk::Mesh, CliError> {
let mut builder = net_sdk::MeshBuilder::new(bind, &remote.psk)
.map_err(|e| connection_failure(format!("mesh builder rejected bind address: {e}")))?;
if let Some(id) = identity {
builder = builder.identity(id);
}
let mesh = builder
.build()
.await
.map_err(|e| connection_failure(format!("mesh build failed: {e}")))?;
mesh.start();
mesh.connect_via(&remote.addr.to_string(), &remote.public_key, remote.node_id)
.await
.map_err(|e| {
connection_failure(format!(
"routed handshake with {} (node_id={}) failed: {e}",
remote.addr, remote.node_id
))
})?;
Ok(mesh)
}
#[allow(dead_code)]
pub fn resolve_remote_attach(
profile: &Profile,
addr: Option<&str>,
pubkey: Option<&str>,
node_id: Option<&str>,
psk_hex: Option<&str>,
) -> Result<Option<RemoteAttach>, CliError> {
let addr_str = addr.or(profile.node_addr.as_deref());
let pubkey_str = pubkey.or(profile.node_pubkey.as_deref());
let node_id_str = node_id.or(profile.node_id.as_deref());
let psk_str = psk_hex.or(profile.psk_hex.as_deref());
let any_set =
addr_str.is_some() || pubkey_str.is_some() || node_id_str.is_some() || psk_str.is_some();
if !any_set {
return Ok(None);
}
let addr_str = addr_str.ok_or_else(|| {
invalid_args("remote-attach requires --node-addr (or `node_addr` in the profile)")
})?;
let pubkey_str = pubkey_str.ok_or_else(|| {
invalid_args("remote-attach requires --node-pubkey (or `node_pubkey` in the profile)")
})?;
let node_id_str = node_id_str.ok_or_else(|| {
invalid_args("remote-attach requires --node-id (or `node_id` in the profile)")
})?;
let psk_str = psk_str.ok_or_else(|| {
invalid_args("remote-attach requires --psk-hex (or `psk_hex` in the profile)")
})?;
let addr: SocketAddr = addr_str.parse().map_err(|e| {
invalid_args(format!(
"--node-addr `{addr_str}` is not a valid IP:port: {e}"
))
})?;
let public_key =
hex_decode_32(pubkey_str).map_err(|e| invalid_args(format!("--node-pubkey: {e}")))?;
let node_id =
parse_u64_flexible(node_id_str).map_err(|e| invalid_args(format!("--node-id: {e}")))?;
let psk = hex_decode_32(psk_str).map_err(|e| invalid_args(format!("--psk-hex: {e}")))?;
Ok(Some(RemoteAttach {
addr,
public_key,
node_id,
psk,
}))
}
pub fn require_remote_attach(
profile: &Profile,
args: &crate::commands::aggregator::RemoteAttachArgs,
missing: impl FnOnce() -> CliError,
) -> Result<RemoteAttach, CliError> {
resolve_remote_attach(
profile,
args.node_addr.as_deref(),
args.node_pubkey.as_deref(),
args.remote_node_id.as_deref(),
args.psk_hex.as_deref(),
)?
.ok_or_else(missing)
}
pub(crate) async fn load_identity_keypair(path: &Path) -> Result<EntityKeypair, CliError> {
Ok(EntityKeypair::from_bytes(load_identity_seed(path).await?))
}
pub(crate) async fn load_identity_seed(path: &Path) -> Result<[u8; 32], CliError> {
let text = ScrubbedString::new(tokio::fs::read_to_string(path).await.map_err(|e| {
generic(format!(
"failed to read identity file {}: {e}",
path.display()
))
})?);
let mut parsed: PartialIdentityFile = toml::from_str(text.as_str()).map_err(|_| {
invalid_args(format!(
"identity file {} failed to parse as identity TOML",
path.display()
))
})?;
let decoded = hex::decode(&parsed.seed_hex).map_err(|e| {
invalid_args(format!(
"identity file {} `seed_hex` is not valid hex: {e}",
path.display()
))
});
zeroize_string(&mut parsed.seed_hex);
let seed_bytes = ScrubbedBytes::new(decoded?);
if seed_bytes.as_slice().len() != 32 {
return Err(invalid_args(format!(
"identity file {} `seed_hex` decodes to {} bytes; expected 32",
path.display(),
seed_bytes.as_slice().len()
)));
}
let mut arr = [0u8; 32];
arr.copy_from_slice(seed_bytes.as_slice());
Ok(arr)
}
pub(crate) async fn load_operator_identity(
path: &Path,
) -> Result<net_sdk::identity::Identity, CliError> {
let seed = load_identity_seed(path).await?;
net_sdk::identity::Identity::from_bytes(&seed).map_err(|e| {
invalid_args(format!(
"identity file {} `seed_hex` is not a valid 32-byte seed: {e:?}",
path.display()
))
})
}
#[derive(serde::Deserialize)]
struct PartialIdentityFile {
seed_hex: String,
}
pub async fn resolve_profile(
config_path: Option<&Path>,
profile_name: &str,
) -> Result<Profile, CliError> {
let file = crate::config::ConfigFile::load(config_path)
.await
.map_err(|e| sdk(format!("config load: {e}")))?;
Ok(file.profile(profile_name))
}
#[allow(dead_code)]
pub fn resolved_identity_path(profile: &Profile, overide_: Option<&Path>) -> Option<PathBuf> {
overide_
.map(Path::to_path_buf)
.or_else(|| profile.identity.clone())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::ExitCodeKind;
fn valid_pubkey() -> &'static str {
"0101010101010101010101010101010101010101010101010101010101010101"
}
fn valid_psk() -> &'static str {
"0x4242424242424242424242424242424242424242424242424242424242424242"
}
#[test]
fn resolve_returns_none_when_nothing_set() {
let profile = Profile::default();
let out = resolve_remote_attach(&profile, None, None, None, None).unwrap();
assert!(out.is_none());
}
#[test]
fn resolve_pulls_full_target_from_flags() {
let profile = Profile::default();
let out = resolve_remote_attach(
&profile,
Some("127.0.0.1:51820"),
Some(valid_pubkey()),
Some("0x42"),
Some(valid_psk()),
)
.unwrap()
.expect("remote-attach resolved");
assert_eq!(out.addr.port(), 51820);
assert_eq!(out.node_id, 0x42);
assert_eq!(out.public_key[0], 0x01);
assert_eq!(out.psk[0], 0x42);
}
#[test]
fn flags_override_profile_defaults() {
let profile = Profile {
node_addr: Some("10.0.0.1:1".into()),
node_pubkey: Some(valid_pubkey().into()),
node_id: Some("1".into()),
psk_hex: Some(valid_psk().into()),
..Profile::default()
};
let out = resolve_remote_attach(&profile, Some("127.0.0.1:9999"), None, None, None)
.unwrap()
.expect("resolved");
assert_eq!(out.addr.port(), 9999);
}
#[test]
fn missing_pubkey_is_typed_invalid_args() {
let profile = Profile::default();
let err = resolve_remote_attach(
&profile,
Some("127.0.0.1:1"),
None,
Some("1"),
Some(valid_psk()),
)
.expect_err("should reject partial remote spec");
assert_eq!(err.kind(), ExitCodeKind::InvalidArgs);
}
#[test]
fn bad_pubkey_length_is_typed_invalid_args() {
let profile = Profile::default();
let err = resolve_remote_attach(
&profile,
Some("127.0.0.1:1"),
Some("0xaa"),
Some("1"),
Some(valid_psk()),
)
.expect_err("should reject short pubkey");
assert_eq!(err.kind(), ExitCodeKind::InvalidArgs);
}
#[test]
fn bad_addr_is_typed_invalid_args() {
let profile = Profile::default();
let err = resolve_remote_attach(
&profile,
Some("not-an-addr"),
Some(valid_pubkey()),
Some("1"),
Some(valid_psk()),
)
.expect_err("should reject garbage addr");
assert_eq!(err.kind(), ExitCodeKind::InvalidArgs);
}
#[tokio::test]
async fn a_malformed_identity_file_never_echoes_the_seed() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("operator.toml");
let seed = "aa".repeat(32);
std::fs::write(
&path,
format!("operator_id_hex = \"0x1\"\nseed_hex = [\"{seed}\"]\n"),
)
.expect("write");
let err = load_identity_seed(&path)
.await
.expect_err("a malformed identity file must be refused");
let rendered = format!("{err}");
assert!(
!rendered.contains(&seed),
"the parse failure echoed the seed: {rendered}",
);
assert!(
!rendered.contains("seed_hex"),
"the parse failure quoted the seed_hex line, which carries the key \
in a well-formed file: {rendered}",
);
}
}