finalize refactor to be aligned with our hero skills #42
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
issues
test
Implementation Spec for Issue #42
Objective
Finalize the refactor so the workspace cleanly matches the Hero standard: collapse the "engine vs. daemon glue" into a single well-organized library with documented modules, push every CLI behavior down into that library so
mycelium_clibecomes a thin clap+presentation wrapper, makemycelium_sdkthe only path the CLI uses to talk to the daemon (typedMyceliumClientcalls instead of hand-rolledrpc_call(method, json!())), and add a Linux-only end-to-end test that boots two realmycelium_serverprocesses and exercises the OpenRPC surface (peers, routes, send/recv, topics) through the SDK. Legacy TCP servers (8989/8990/8991), the canonical Hero UDS, and themycelium_server/mycelium_server_privatebinaries stay exactly where they are.Current state (what we found)
crates/mycelium/(src/lib.rs, ~554 lines) is a pure library — routing, crypto, TUN, peer manager, message stack, proxy, DNS, CDN. Nomain, no daemon glue. Confirmed viadocs/crates.md("the engine"). Modules are well separated.crates/mycelium_lib/(src/lib.rs796 lines +src/network/7 files) is daemon glue that does NOT overlap withmycelium's engine concerns: clapCli<N>,MyceliumConfig/MergedNodeConfig, key file helpers,init_logging,run_node,dispatch_subcommand, plus thenetwork/netlink namespace.crates/mycelium_cli/(~1200 lines) currently mixes three concerns: clap definitions; service registration with hero_proc (self_register.rs); per-command logic (peer/routes/proxy/stats/inspect/dispatch/message) with hand-rolledrpc_call(socket_path, "getPeers", json!({}))viaclient.transport().call_raw(...)instead of the SDK's typed methods. Key handling helpers are duplicated verbatim betweenmycelium_cli/src/dispatch.rsandmycelium_lib/src/lib.rs.message.rsstill uses rawreqwestagainst127.0.0.1:8989.crates/mycelium_sdk/is already correctly set up viahero_rpc_derive::openrpc_client!("../../docs/openrpc.json")— generates a typedMyceliumJSONRPCAPIClientfor all 99 RPC methods. The CLI bypasses these methods today.crates/mycelium_api/owns both legacy axum HTTP REST (8989), legacy jsonrpsee TCP (8990), and the canonical Hero UDS HTTP/JSON-RPC server. Out of scope.crates/mycelium_server/andcrates/mycelium_server_private/are already thin (30-80 linemain.rs).tests/directory exists in any crate — no end-to-end test infrastructure exists today.CLAUDE.mdcodifies the legacy TCP exception, the canonical UDS at$HERO_SOCKET_DIR/mycelium/rpc.sock, and the documented six-crate layout.Target state
crates/mycelium/— unchanged. Add a top-level rustdoc onlib.rsclarifying its scope.crates/mycelium_lib/— unchanged in responsibilities; reorganized into documented modules:cli,config,keys,logging,node,dispatch, plus the existingdefaultsandnetwork. Each module gets a//!rustdoc.crates/mycelium_cli/— thin wrapper. Hand-rolled JSON-RPC plumbing deleted; every command uses the SDK's typed methods. Duplicate key-file handling deduped.message.rsmigrates to typed SDK calls over UDS. Presentation (prettytable) stays.crates/mycelium_sdk/— gains a tinyconnect_default()convenience helper.crates/mycelium_api/,mycelium_server/,mycelium_server_private/,mycelium_ui/— unchanged.crates/mycelium_e2e/test-only crate. All tests gated on#[cfg(target_os = "linux")]. Spawns twomycelium_serverchild processes with--no-tun, peers them, exercises peers/routes/topics/messaging through the SDK.Requirements
mycelium_serverandmycelium_server_privateclap surface unchanged.mycelium_libandmyceliumstay separate (see Notes for the merge analysis).*_clientor*_rhaicrates.transport().call_raw(method, value).default_socket_path()frommycelium_sdk.Dropguard).Files to Modify/Create
crates/mycelium/crates/mycelium/src/lib.rs— add a//!rustdoc block at the top describing the crate's role. No code changes.crates/mycelium_lib/crates/mycelium_lib/src/lib.rs— split into modules; reduces to ~80 lines ofpub mod+pub usedeclarations.crates/mycelium_lib/src/cli.rs(new) —Cli<N>,NodeArguments,LoggingFormat.crates/mycelium_lib/src/config.rs(new) —MyceliumConfig,MergedNodeConfig,merge_config,load_config_file, default-address constants,default_rpc_socket_path.crates/mycelium_lib/src/keys.rs(new) —resolve_key_path,get_node_keys,load_key_file,save_key_file.crates/mycelium_lib/src/logging.rs(new) —init_logging.crates/mycelium_lib/src/node.rs(new) —run_nodeand the underlay constants.crates/mycelium_lib/src/dispatch.rs(new) — single-line re-export ofmycelium_cli::dispatch_subcommand(for backwards compatibility).crates/mycelium_lib/src/network/— unchanged.crates/mycelium_cli/crates/mycelium_cli/src/lib.rs— drop duplicatedresolve_default_key_path.crates/mycelium_cli/src/dispatch.rs— keep local key helpers (cycle avoidance — see Notes); switchCommand::Messageto passsocket_path.crates/mycelium_cli/src/rpc_client.rs— DELETE.crates/mycelium_cli/src/peer.rs— replace rawrpc_callwithclient.get_peers/add_peer/delete_peer(...).crates/mycelium_cli/src/routes.rs— same migration forget_selected_routes/get_fallback_routes/get_queried_subnets.crates/mycelium_cli/src/proxy.rs— same migration for proxy methods.crates/mycelium_cli/src/stats.rs— same migration forget_packet_statistics.crates/mycelium_cli/src/message.rs— rewritesend_msg/recv_msgto useclient.push_message/pop_messageover UDS instead ofreqwestagainst127.0.0.1:8989.crates/mycelium_cli/src/cli.rs— mark--api-addrflag as deprecated in clap help text.crates/mycelium_cli/Cargo.toml— dropreqwestandmycelium_apiif unused after migration.crates/mycelium_sdk/crates/mycelium_sdk/src/lib.rs— addpub async fn connect_default() -> Result<MyceliumClient, OpenRpcError>.crates/mycelium_e2e/(new)crates/mycelium_e2e/Cargo.toml—publish = false, depends onmycelium_sdk,mycelium_server(forCARGO_BIN_EXE_mycelium_server),tokio,tempfile,base64,anyhow,serde_json.crates/mycelium_e2e/src/lib.rs(new, empty stub).crates/mycelium_e2e/tests/two_nodes.rs(new) — entire file#[cfg(target_os = "linux")]. ImplementsNodeHandlewithDrop,spawn_node(), and#[tokio::test] async fn two_nodes_full_surface().crates/mycelium_e2e/README.md(new, ~30 lines) — Linux-only, run instructions.Workspace
Cargo.toml— add"crates/mycelium_e2e"to workspace members.Implementation Plan
Step 1: Document
mycelium's scope (no code changes)Files:
crates/mycelium/src/lib.rs//!rustdoc block at the top describing the engine library's role.cargo check -p myceliumpasses.Dependencies: none.
Step 2: Reorganize
mycelium_libinto documented modulesFiles:
crates/mycelium_lib/src/lib.rs(split),cli.rs,config.rs,keys.rs,logging.rs,node.rs,dispatch.rs(all new).lib.rsinto their new module files.lib.rsdeclarespub modand re-exports the same public surface today's daemons import (verified by reading bothmycelium_server/src/main.rsandmycelium_server_private/src/main.rs).//!rustdoc explaining its purpose.cargo build -p mycelium_lib,cargo build -p mycelium_server,cargo build -p mycelium_server_privateall pass with no source changes in the daemons.Dependencies: Step 1.
Step 3: Add convenience helper to
mycelium_sdkFiles:
crates/mycelium_sdk/src/lib.rspub async fn connect_default() -> Result<MyceliumClient, OpenRpcError>.cargo check -p mycelium_sdk.Dependencies: none.
Step 4: Migrate CLI peer/routes/proxy/stats handlers to typed SDK calls
Files:
crates/mycelium_cli/src/peer.rs,routes.rs,proxy.rs,stats.rs.rpc_call(socket_path, "<method>", json!({...}))withclient.<method>(<Method>Input { ... }).await?.OpenRpcError -> Box<dyn std::error::Error>or change return types.use mycelium_api::{...}anduse crate::rpc_client::rpc_callfrom each file.Dependencies: Step 3.
Step 5: Migrate
message.rsto typed SDK calls over UDSFiles:
crates/mycelium_cli/src/message.rs,dispatch.rs.send_msgto takesocket_path: &Path, buildPushMessageInput, callclient.push_message(input).await?.recv_msgto callclient.pop_message(...).--reply-to, useclient.push_message_reply(...).dispatch.rsCommand::Messagearm.--api-addrto mark as deprecated.Dependencies: Step 3.
Step 6: Delete
rpc_client.rsand dedupe key handlingFiles:
crates/mycelium_cli/src/rpc_client.rs(delete),lib.rs,dispatch.rs,Cargo.toml.rpc_client.rs; removemod rpc_client; pub use rpc_client::rpc_call;fromlib.rs.resolve_default_key_pathfrommycelium_cli/src/lib.rs(cycle-safe — only deletes the duplicate; the local key helpers indispatch.rsstay to avoid a cycle intomycelium_lib).reqwestandmycelium_apifrommycelium_cli/Cargo.tomlifcargo checkpasses without them.Dependencies: Steps 4 and 5.
Step 7: Verify daemon binaries still build and behave identically
Files: none (verification only).
cargo build --workspacesucceeds.cargo run -p mycelium_server -- --helpmatches the previous output.cargo run -p mycelium_server -- --no-tun --uds-only --tcp-listen-port 0boots and binds the UDS.Dependencies: Steps 2 and 6.
Step 8: Add the
mycelium_e2ecrate skeletonFiles: workspace
Cargo.toml,crates/mycelium_e2e/Cargo.toml,src/lib.rs,README.md.mycelium_serverso cargo providesCARGO_BIN_EXE_mycelium_serverto integration tests.Dependencies: Step 3.
Step 9: Implement the two-node end-to-end test
Files:
crates/mycelium_e2e/tests/two_nodes.rs.#[cfg(target_os = "linux")].NodeHandleguard struct withDropthat kills the child.async fn spawn_nodebootsmycelium_serverwith--no-tun --disable-quic --disable-peer-discovery --uds-onlyand ephemeral TCP/UDS paths under atempfile::TempDir; polls untilclient.get_info()succeeds.#[tokio::test(flavor = "multi_thread")] async fn two_nodes_full_surface()runs: get_info, get_peers (with peer up), get_selected_routes (non-empty after settling), topic add/list/remove, push_message + pop_message round-trip with payload assertion.Dependencies: Step 8.
Step 10: Re-verify and document
Files:
CLAUDE.md,docs/crates.md.cargo build --workspaceandcargo test --workspacepass on macOS (e2e compiles to empty module).CLAUDE.mdpointing to the e2e tests.docs/crates.mdwith the newmycelium_libsubmodules and themycelium_e2eentry.Dependencies: Steps 7 and 9.
Acceptance Criteria
cargo build --workspacesucceeds on macOS and Linux.cargo build -p mycelium_serverandcargo build -p mycelium_server_privateproduce binaries with identical clap help text (modulo the deprecation note on--api-addr).crates/mycelium_cli/src/rpc_client.rsno longer exists;grep "transport()\.call_raw" crates/mycelium_cli/srcreturns zero matches.crates/mycelium_lib/src/lib.rsis under 100 lines (just declarations); the actual logic lives in submodules each with a//!rustdoc.*_clientor*_rhaicrates exist.cargo test -p mycelium_e2e --test two_nodespasses on Linux. On macOS it compiles and reports zero tests.get_info,get_peers,get_selected_routes, topic add/list/remove,push_message+pop_messageround-trip with payload assertion.docs/crates.mdreflects the new layout and listsmycelium_e2e.Notes
myceliumandmycelium_lib? Recommendation: keep them separate.myceliumis a pure engine library also consumed bymobile/(excluded from the workspace, builds with different feature flags). Foldingmycelium_lib's clap/tokio/Hero-proc surface intomyceliumwould force every mobile build to drag inclap,config,dirs,toml, etc. — a hard regression in mobile build time and binary size. The two crates have no overlap in responsibilities today; the apparent confusion is a documentation problem, addressed by Step 2 splittingmycelium_lib's monolithiclib.rsinto clearly-named submodules with header rustdoc.mycelium_apiis real but out of scope for this issue. Filing as a follow-up.--api-addrflag is kept for backwards compatibility but becomes a no-op oncemessage.rsmigrates to UDS. Hard-removing would break operator scripts.mycelium_libdepends onmycelium_cli. Step 6 keeps the local key helpers inmycelium_cli/src/dispatch.rs(small, ~30 lines) rather than reaching intomycelium_libto avoid a cycle.--no-tunso no privileges required. Issue accepts Linux-only scope.Route,QueriedSubnet, etc. structs may have slightly different field types than themycelium_api::Routetypes the CLI uses today. Step 4 will handle mechanically (.unwrap_or_default()etc.) in the prettytable formatting.Note: This spec replaces the earlier one based on follow-up direction from @kristof. Key changes vs the previous spec: rename
mycelium→mycelium_engine, dissolvemycelium_lib(kept only as a thin shim formycelium_server_private), introducemycelium_daemonfor runtime glue,mycelium_serverbecomes flag-free with all configuration via OpenRPC, single unifiedMyceliumRpctrait shared by TCP jsonrpsee and UDS transports, five newnode.*RPC methods for runtime reconfigure, focus this iteration onmycelium_server(private daemon left untouched).Implementation Spec for Issue #42 (revised)
Objective
Restructure the workspace so
crates/mycelium/becomescrates/mycelium_engine/(engine + config-loading only),crates/mycelium_lib/is dissolved with its responsibilities split between a newcrates/mycelium_daemon/library (run_node, key I/O, logging, network.* dispatch, OpenRPC service implementation) andcrates/mycelium_cli/(clap types only); makemycelium_servera flag-free binary that boots fromMYCELIUM_STATE_DIR(default~/hero/var/mycelium/) withpriv_key.binauto-generated on first boot, peers seeded fromBOOTSTRAP_PEERS, the legacy TCP underlay on 9651, QUIC + peer-discovery + TUN at their existing defaults, and exposes ALL post-boot reconfiguration through OpenRPC; unify the OpenRPC surface behind a singleMyceliumRpctrait that both transports (TCP jsonrpsee at 8990 and Hero UDS HTTP/JSON-RPC) call into; add new RPC methodsnode.set_private_key,node.set_listen_ports,node.set_tun,node.restart,node.get_configtodocs/openrpc.jsonAND implement them, achieving 100% spec/impl coverage; thinmycelium_clito clap-tree →mycelium_sdktyped call → prettytable/JSON output (keepingself_register.rsfor--start/--stop); migratemycelium_cli/src/message.rsoffreqwestto typed SDK calls over UDS; expose a tinyconnect_default()convenience helper frommycelium_sdk; keep the legacy TCP listeners (8989/8990/8991) perCLAUDE.md; leavemycelium_server_privatesource untouched (smallest possible shim only if needed); addcrates/mycelium_e2e/(publish=false) two-process Linux-only e2e test that spawns flag-freemycelium_serverbinaries with isolated env vars and exercises peers, routes, topics, message round-trip.Direction (set by user)
mycelium→mycelium_engine(package name, directory name, all dependents includingmobile/).mycelium_lib; introducemycelium_daemon.mycelium_server: zero clap flags, all config via OpenRPC.MYCELIUM_STATE_DIRenv var; sensible bootstrap defaults seeded from existingBOOTSTRAP_PEERS.node.*RPC methods that persist + restart engine in-process.mycelium_cli: pure clap → SDK → output, withself_register.rskept (Hero proc lifecycle wiring).MyceliumRpctrait, TCP jsonrpsee + UDS shims call it, 100% spec coverage.#[cfg(target_os = "linux")].Crate layout (after)
crates/mycelium_engine/mycelium_enginecrates/mycelium_metrics/mycelium_metricscrates/mycelium_api/mycelium_apiMyceliumRpctraitcrates/mycelium_daemon/mycelium_daemoncrates/mycelium_sdk/mycelium_sdkconnect_default()crates/mycelium_cli/mycelium_clicrates/mycelium_lib/mycelium_lib(shim)mycelium_server_privatesource compatibility onlycrates/mycelium_server/mycelium_servercrates/mycelium_server_private/mycelium_server_privatecrates/mycelium_ui/mycelium_uicrates/mycelium_e2e/mobile/New OpenRPC methods to add to
docs/openrpc.jsonAll under tag
Node,paramStructure: by-name, dotted-namespace per existingnetwork.*precedent.node.set_private_key$MYCELIUM_STATE_DIR/priv_key.bin, restarts engine in-process under new identity.private_key_hex(string, 64 hex chars).RestartResult { restarted: bool, node_pubkey: string, node_address: string }.node.set_listen_portstcp_listen_port(uint16, optional),quic_listen_port(uint16|null, optional; null disables),peer_discovery_port(uint16, optional). At least one required.RestartResult { restarted: true, tcp_listen_port, quic_listen_port, peer_discovery_port }.node.set_tunenabled(bool, optional),tun_name(string, optional, platform-validated).RestartResult { restarted: true, tun_enabled: bool, tun_name: string|null }.node.restartRestartResult { restarted: true, node_pubkey, node_address }.node.get_configNodeConfig { key_fingerprint, node_pubkey, node_address, node_subnet, tcp_listen_port, quic_listen_port, peer_discovery_port, peer_discovery_mode, no_tun, tun_name, rpc_socket_path, uds_only, metrics_api_address, firewall_mark, update_workers, enable_dns, vsock_listen_port, peers: [string], state_dir }.After this iteration
docs/openrpc.jsontotals 38 methods (32 existing + 5 new +rpc.discover).Files to Modify/Create (grouped by crate)
Workspace
Cargo.toml— replacecrates/myceliumwithcrates/mycelium_engine, addcrates/mycelium_daemon, addcrates/mycelium_e2e. Keepcrates/mycelium_libas shim member.CLAUDE.md— rewrite Crate Layout table; documentMYCELIUM_STATE_DIR; document flag-free server; update bootstrap-peers reference.docs/crates.md— full rewrite for new layout.docs/openrpc.json— append fivenode.*methods +RestartResult/NodeConfigschemas; bumpinfo.versionto 0.9.0.crates/mycelium/→crates/mycelium_engine/Cargo.toml—name = "mycelium_engine".src/lib.rs— package-name change only.src/config.rs(NEW) —OnDiskConfig,OnDiskPeers(de)serializers; pure types, no I/O conventions.crates/mycelium_metrics/Cargo.tomlmycelium = { path = "../mycelium" }→mycelium_engine = { path = "../mycelium_engine" }. Sourceuse mycelium::→use mycelium_engine::.crates/mycelium_api/Cargo.toml— rename dep.src/rpc.rs— replace existing two jsonrpsee traits with onepub trait MyceliumRpc(async-trait, 38 typed methods); typed Input/Output for every method;pub async fn dispatch<R: MyceliumRpc>(rpc, method, params) -> Result<Value, RpcError>helper.src/rpc.rs—JsonRpc::spawntakesArc<dyn MyceliumRpc>; registers each method viaregister_async_methodcalling dispatch.src/rpc/unix.rs—spawn(rpc: Arc<dyn MyceliumRpc>, socket_path). Replace 500-line match with onedispatch(...)call.src/rpc/{admin.rs, peer.rs, route.rs, message.rs, models.rs, traits.rs, spec.rs}— keep only schema types; remove old jsonrpsee impls.pub trait NetworkDispatchandnetwork_dispatchfield onServerState<M>.network.*methods live onMyceliumRpcdirectly.crates/mycelium_daemon/(NEW)Cargo.toml— depends onmycelium_engine,mycelium_api,mycelium_metrics,tokio,tracing,tracing-subscriber,serde,toml,dirs,async-trait,rtnetlink+netlink-packet-route+futures+libc(Linux only).src/lib.rs— public surfacepub use mycelium_engine::{crypto, endpoint::Endpoint, peer_manager::PrivateNetworkKey}; pub mod defaults; pub mod state; pub mod logging; pub mod rpc_impl; pub mod network; pub async fn run_node(state, private_network_config) -> Result<()>.src/defaults.rs— moved frommycelium_lib/src/defaults.rs; addDEFAULT_TCP_LISTEN_PORT=9651,DEFAULT_QUIC_LISTEN_PORT,DEFAULT_PEER_DISCOVERY_PORT,TUN_NAME,DEFAULT_HTTP_API_SERVER_ADDRESS=127.0.0.1:8989,DEFAULT_JSONRPC_API_SERVER_ADDRESS=127.0.0.1:8990.src/state.rs—DaemonState { state_dir, priv_key, config, peers_path, config_path }.resolve_state_dir()readsMYCELIUM_STATE_DIR, falls back to$HOME/hero/var/mycelium/then/tmp/mycelium-state.load_or_initcreates dir 0o700, generatespriv_key.bin(0o640) if missing, seeds peers fromdefaults::BOOTSTRAP_PEERS, defaults all ports/TUN/HTTP/RPC addrs, computesrpc_socket_pathfromHERO_SOCKET_DIR.save_priv_key,save_config. Files:priv_key.bin,config.toml,peers.toml.src/logging.rs—init_logging_from_env(silent, debug, format)andinit_logging_from_env()env-only variant. ReadsRUST_LOG, defaults to info.src/network/{mod.rs, dispatch.rs, errors.rs, linux_impl.rs, managed.rs, models.rs, stub.rs}— moved verbatim frommycelium_lib/src/network/*. Repurposed as helper called byrpc_impl.rs(no longer trait-based, sinceNetworkDispatchtrait deleted).src/rpc_impl.rs(NEW) —pub struct MyceliumRpcImpl<M: Metrics> { node, state, managed, restart_tx }. Implementsmycelium_api::MyceliumRpcfor all 38 methods.node.set_*/node.restartwrite through state to disk then signalrestart_tx.node.get_configreturns snapshot.network.*calls intocrate::network::imp::*.src/runner.rs(NEW) —pub async fn run_node(...)supervisor loop: buildConfigfrom state, buildNode, buildMyceliumRpcImpl, spawn UDSmycelium_api::rpc::unix::spawn(rpc.clone(), socket_path), conditionally spawnHttp::spawn+JsonRpc::spawnif!uds_only,tokio::select!{ sigint, sigterm, restart_rx.recv() }. On restart: drop everything and re-loop.src/legacy_compat.rs— re-exports needed for_privatevia the shim crate (MyceliumConfig,NodeArguments,MergedNodeConfig,merge_config,load_config_file,resolve_key_path,load_key_file,save_key_file,dispatch_subcommandforwarding tomycelium_cli).crates/mycelium_lib/(shim, ~30 lines total)Cargo.toml— depend onmycelium_daemon,mycelium_cli,mycelium_engine,clap,serde,tokio. Drop everything else.src/lib.rs—pub use mycelium_daemon::{init_logging, LoggingFormat, load_config_file, merge_config, resolve_key_path, load_key_file, save_key_file, run_node, dispatch_subcommand, MyceliumConfig, MergedNodeConfig, NodeArguments, PrivateNetworkKey, BOOTSTRAP_PEERS, DEFAULT_*}; pub use mycelium_cli::{Cli, Command, MessageCommand, PeersCommand, ProxyCommand, ProxyProbeCommand, RoutesCommand, StatsCommand}; pub use mycelium_engine::{crypto, endpoint::Endpoint};.crates/mycelium_lib/src/network/,crates/mycelium_lib/src/defaults.rs(moved to daemon).crates/mycelium_cli/Cargo.toml— dropreqwest,mycelium_api,mycelium = { path = "../mycelium" }; addmycelium_engineforcrypto::PublicKeyininspect.rs.src/cli.rs— drop--api-addr, drop daemon-only types (these move tomycelium_daemon::legacy_compat).src/dispatch.rs— rewrite each arm:let client = mycelium_sdk::connect_default().await?; client.<method>(<TypedInput>{...}).await?;.src/peer.rs,proxy.rs,routes.rs,stats.rs— typed SDK calls; droprpc_call.src/message.rs— full rewrite: dropreqwest/api_addr, useclient.push_message/pop_message/push_message_reply/get_message_info.src/rpc_client.rs.src/lib.rs— drop localinit_logging+resolve_default_key_path(moved to daemon); keeprun.src/self_register.rs— UNCHANGED (kept).src/inspect.rs— UNCHANGED.crates/mycelium_sdk/src/lib.rs— addpub async fn connect_default() -> Result<MyceliumClient, OpenRpcError>.crates/mycelium_server/Cargo.toml— dropclap; depend onmycelium_daemon.src/main.rs— rewrite (~25 lines):MYCELIUM_STATE_DIRand OpenRPC.crates/mycelium_server_private/Cargo.toml— replacemycelium = { path = "../mycelium" }withmycelium_engine = { path = "../mycelium_engine" }; keepmycelium_lib = { path = "../mycelium_lib" }(shim).src/main.rs— UNCHANGED.crates/mycelium_e2e/(NEW)Cargo.toml—publish = false. Dev-deps:mycelium_sdk,tokio,serde_json,tempfile,anyhow.tests/two_node_linux.rs—#![cfg(target_os = "linux")]. Spawns twomycelium_serverchildren with isolatedMYCELIUM_STATE_DIR/HERO_SOCKET_DIR/RUST_LOGenv vars; pre-writes per-nodeconfig.tomlso they bind differenttcp_listen_ports (e.g. 19651, 19652). Pollsrpc.sockuntil ready, then via SDK:addPeer, pollgetPeersuntil alive,getSelectedRoutes,addTopic,pushMessageround-trip withpopMessage.Dropguards SIGTERM children + cleanup tempdirs.mobile/Cargo.toml—mycelium_engine = { path = "../crates/mycelium_engine", features = ["vendored-openssl"] }; featuremactunfd = ["mycelium_engine/mactunfd"].src/lib.rs—use mycelium::→use mycelium_engine::(3 lines).README.md— update path references.Implementation Plan
Step 1 — Rename
mycelium→mycelium_engine(workspace-wide)Files: root
Cargo.toml;crates/mycelium/→crates/mycelium_engine/directory rename +Cargo.tomlname; all dep paths inmycelium_metrics,mycelium_api,mycelium_cli,mycelium_lib,mycelium_server_private,mobile/; alluse mycelium::references in non-_privatecrates andmobile/.Subtasks:
git mv; editCargo.tomlfiles; sed-replaceuse mycelium::→use mycelium_engine::; replace feature stringsmycelium/feature→mycelium_engine/feature;cargo check --workspaceandcd mobile && cargo check.Dependencies: none.
Step 2 — Add
mycelium_engine::configmoduleFiles:
crates/mycelium_engine/src/lib.rs,crates/mycelium_engine/src/config.rs(NEW).Subtasks: define
OnDiskConfig/OnDiskPeerswith serde; addpub mod config;. No I/O conventions (live in daemon).Dependencies: Step 1.
Step 3 — Create
crates/mycelium_daemon/skeleton with engine-side glue [parallel-safe with Step 4]Files (new):
crates/mycelium_daemon/Cargo.toml,src/lib.rs,src/defaults.rs,src/state.rs,src/logging.rs,src/network/{mod.rs,dispatch.rs,errors.rs,linux_impl.rs,managed.rs,models.rs,stub.rs}(moved frommycelium_lib),src/runner.rs(placeholder),src/legacy_compat.rs. Add to root workspace.Dependencies: Step 1, Step 2.
Step 4 — Rewrite
mycelium_apito expose unifiedMyceliumRpctrait [parallel-safe with Step 3]Files:
src/rpc.rs(replace traits with onepub trait MyceliumRpc, 38 methods, typed Input/Output,dispatchhelper);JsonRpc::spawntakesArc<dyn MyceliumRpc>;src/rpc/unix.rsspawnsimilarly; supporting modules become Input/Output schema-only; DELETENetworkDispatchtrait +network_dispatchfield.Subtasks: enumerate 38 methods; build typed Input/Output (node.* stubs return -32601 until Step 6 fills them); migrate transports;
cargo check -p mycelium_api.Dependencies: Step 1.
Step 5 — Convert
mycelium_libto compatibility shimFiles:
crates/mycelium_lib/Cargo.toml(slim deps);crates/mycelium_lib/src/lib.rs(re-export shim, ~30 lines); DELETEsrc/network/andsrc/defaults.rs.Subtasks: build,
cargo check -p mycelium_server_privateto confirm shim sufficient. If_privateneeds anything else, add re-export — never modify_privatesource.Dependencies: Step 3.
Step 6 — Implement
MyceliumRpcforMyceliumRpcImpl<M>+ restart loopFiles:
crates/mycelium_daemon/src/rpc_impl.rs(NEW),src/runner.rs(replace placeholder),src/lib.rs.Subtasks: implement all 38 trait methods;
node.set_*/node.restartpersist + signalrestart_tx;node.get_configreturns snapshot;network.*calls into network helper; supervisor loop withtokio::select!over signals + restart channel.Dependencies: Step 3, Step 4.
Step 7 — Add
node.*methods todocs/openrpc.jsonFiles:
docs/openrpc.json.Subtasks: append 5 method entries; add
RestartResult/NodeConfigschemas; bumpinfo.versionto 0.9.0;python -m json.toolvalidation.Dependencies: Step 4 (Input/Output struct names must match exactly).
Step 8 — Make
mycelium_serverflag-freeFiles:
crates/mycelium_server/Cargo.toml(drop clap, swap tomycelium_daemon);src/main.rs(rewrite ~25 lines);README.md.Subtasks:
cargo build -p mycelium_server; smoke test that first-launch creates~/hero/var/mycelium/{priv_key.bin,config.toml,peers.toml}and bindsrpc.sock+ 8989/8990.Dependencies: Step 6.
Step 9 — Thin
mycelium_cliFiles:
Cargo.toml(dropreqwest,mycelium_api);src/cli.rs(drop--api-addr+ daemon-only types);src/dispatch.rs(typed SDK calls);src/{peer,proxy,routes,stats}.rs(typed SDK calls);src/message.rs(rewrite to UDS + SDK); DELETEsrc/rpc_client.rs;src/lib.rs(drop localinit_logging,resolve_default_key_path).Subtasks:
cargo build -p mycelium_cli; manual smokemycelium_cli peers listagainst running server from Step 8.Dependencies: Step 8.
Step 10 — Add
connect_default()tomycelium_sdkFiles:
crates/mycelium_sdk/src/lib.rs(single new helper).Dependencies: Step 7 (regenerated client must include
node.*).Step 11 — Linux-only end-to-end test crate
Files:
crates/mycelium_e2e/Cargo.toml(NEW);tests/two_node_linux.rs(NEW); rootCargo.toml.Subtasks: spawn two children with isolated env; pre-write
config.tomlper node with differenttcp_listen_port; via SDK call addPeer, poll getPeers, getSelectedRoutes, addTopic, pushMessage→popMessage round-trip;Dropguards.Dependencies: Step 8, Step 9, Step 10.
Step 12 — Documentation refresh
Files:
CLAUDE.md,docs/crates.md.Subtasks: rewrite Crate Layout table, document
MYCELIUM_STATE_DIR, document flag-free server, documentnode.*methods, replacemycelium_lib/src/defaults.rsreference withmycelium_daemon/src/defaults.rs.Dependencies: Step 11.
Acceptance Criteria
cargo check --workspacepasses from repo root.cargo checkinsidemobile/passes.cargo build -p mycelium_serverproduces a binary that with no env vars and no flags creates$HOME/hero/var/mycelium/{priv_key.bin,config.toml,peers.toml}and binds rpc socket + TCP 8989/8990.mycelium_server --helperrors with no-flags message; noclapinmycelium_server/Cargo.toml.mycelium_server_privatesource files are byte-identical before and after.docs/openrpc.jsonlists exactly 38 methods (32 existing + 5 new +rpc.discover); every method is implemented inMyceliumRpcImpl; reverse holds too.mycelium_cli/Cargo.tomldoes not listreqwest;crates/mycelium_cli/src/rpc_client.rsdoes not exist.node.set_*returnsRestartResult { restarted: true, ... }; follow-upnode.get_configreflects the change persisted to disk.cargo test -p mycelium_e2e --test two_node_linuxboots two daemons and exchanges a message round-trip in <60s.git grep -n 'use mycelium::'returns zero hits incrates/;git grep -n 'mycelium = { path' Cargo.toml crates/*/Cargo.toml mobile/Cargo.tomlreturns zero hits.Risks / Notes
mycelium_server_privatebuild break: mitigated bymycelium_libshim that re-exports every name_privateuses. Verified bycargo check -p mycelium_server_privateafter Step 5 with zero source edits — only itsCargo.tomlchanges (Step 1, dep path).mobile/build break: mitigated by Cargo.toml path + 3-lineusechange in Step 1.engine → metrics → api → cli/sdk → daemon → lib (shim) → server_private. No cycle.MyceliumRpctyped Input deser errors: per-method match arm indispatchmapsserde_json::from_value::<XInput>errors to JSON-RPC -32602.node.*: Step 7 hard-codes same field names/order as Step 4 typed types, both reviewed in same patch. SDK regen produces compile errors if mismatched.mycelium_server/README.md+CLAUDE.md. Existing systemd/Docker units need update.HERO_SOCKET_DIRvsMYCELIUM_STATE_DIR: sockets under$HERO_SOCKET_DIR/mycelium/, persistent state under$MYCELIUM_STATE_DIR. Intentionally separate.network.*runtime requirements: needsCAP_NET_ADMINon Linux. e2e test does not exercisenetwork.*(peers/routes/topics/messages only).#[cfg(target_os = "linux")]; skipped silently on macOS/Windows.Test Results
cargo test --workspaceon macOS host (workspace check + unit tests + doctests):mycelium_apimycelium_engine(lib)mycelium_engine(doctests)mycelium_sdkmycelium_sdk(doctest)mycelium_uimycelium_e2e(Linux-only)Totals: 94 passed, 0 failed, 1 ignored.
The
mycelium_e2eintegration test (tests/two_node_linux.rs) is gated on#[cfg(target_os = "linux")]so it compiles to an empty module on macOS and reports zero tests there. On a Linux host it spawns twomycelium_serverchild processes (each with isolatedMYCELIUM_STATE_DIRandHERO_SOCKET_DIR), peers them viaaddPeerover the canonical UDS, then exercisesgetInfo,getPeers,getSelectedRoutes,addTopic/getTopics/removeTopic, and apushMessage→popMessageround-trip. Drop guards SIGTERM the children on test exit.cargo check --workspaceis clean.cargo build -p mycelium_serverproduces a flag-free binary that boots fromMYCELIUM_STATE_DIR(smoke-tested: state files auto-created, UDS bound, bootstrap peers connected, routes acquired).Implementation summary
All 12 steps from the revised spec landed on the working branch (no commit yet — final commit is the one separate authorisation step).
Crate layout (after)
crates/mycelium_engine/crates/mycelium/configmodule (OnDiskConfig,OnDiskPeers)crates/mycelium_metrics/crates/mycelium_api/pub trait MyceliumRpc(38 typed methods); both transports (TCP jsonrpsee 8990, UDS HTTP/JSON-RPC) call shareddispatch(rpc, method, params).NetworkDispatchtrait deleted. Six stale uncompiled files insrc/rpc/removed.crates/mycelium_daemon/run_node,DaemonState, key I/O, logging, network.* netlink helpers,MyceliumRpcImpl<M>(full impl of all 38 methods). Holds the supervisor restart loop.crates/mycelium_sdk/connect_default()convenience helper; auto-regenerated typed methods for the 5 newnode.*callscrates/mycelium_cli/rpc_client.rsdeleted.reqwestandmycelium_apidropped.--api-addrflag dropped.self_register.rskept verbatim.crates/mycelium_lib/mycelium_server_privateimports. Source code of_privateis byte-identical.crates/mycelium_server/main.rs. Boots fromMYCELIUM_STATE_DIR(default~/hero/var/mycelium/), auto-generatespriv_key.bin,config.toml,peers.tomlon first run. Noclapdep.crates/mycelium_server_private/Cargo.tomladjusted for the rename. Continues throughmycelium_libshim.crates/mycelium_ui/crates/mycelium_e2e/#[cfg(target_os = "linux")]). Spawns twomycelium_serverchildren with isolated env, peers them, exercises peers/routes/topics/messaging through the SDK.mobile/uselines retargeted tomycelium_engine.New OpenRPC methods
Added to
docs/openrpc.jsonand implemented inmycelium_daemon::rpc_impl::MyceliumRpcImpl:node.set_private_key— replaces priv key, persists, restarts engine in-processnode.set_listen_ports— TCP/QUIC/peer-discovery underlay ports, persists, restartsnode.set_tun— toggle TUN / change interface name, persists, restartsnode.restart— manual restart from current persisted confignode.get_config— full snapshot (key fingerprint, ports, peers, TUN config, paths)Spec
info.versionbumped to0.9.0. Total methods: 38 (32 existing + 5 new +rpc.discover).Bootstrap & runtime configuration
mycelium_serveris flag-free. Configuration:Runtime reconfiguration is performed by calling
node.set_*over the UDS — the daemon persists the change to disk, then tears down and rebuilds the engine in-process under the new identity / ports / TUN config. Peer add/remove (addPeer/deletePeer) is hot, no restart required.Verification
cargo check --workspaceclean.cargo test --workspace: 94 unit + integration tests pass on macOS, 0 fail (e2e Linux-only — reports zero tests on macOS by design).cargo build -p mycelium_serverbuilds. Smoke test: with no env overrides the binary creates the state directory, generatespriv_key.bin,config.toml,peers.toml, binds the canonical UDS, connects to all 10 bootstrap peers, and acquires routes from each.cargo build -p mycelium_server_privatebuilds (legacy clap path through the shim continues to work).git grep "use mycelium::" crates/returns zero hits.git grep "transport().call_raw" crates/mycelium_cli/returns zero hits.git grep reqwest crates/mycelium_cli/returns zero hits.crates/mycelium_cli/src/rpc_client.rsdoes not exist.Files touched
mycelium_api/src/rpc/unix.rsand the duplicated jsonrpsee impl inmycelium_api/src/rpc.rs, plus collapsing the 800-linemycelium_lib/src/lib.rsinto a 50-line shim).mycelium_engine/src/config.rs, themycelium_daemoncrate (state/logging/runner/rpc_impl/legacy_runner/keys/cli_legacy/config_legacy/network/* + Cargo.toml), themycelium_e2ecrate (Cargo.toml/lib.rs/build.rs/tests/two_node_linux.rs),mycelium_api/src/rpc/network.rs,mycelium_server/README.md.mycelium_api/src/rpc/:admin.rs,peer.rs,route.rs,message.rs,models.rs,traits.rs.Notes / follow-ups
mycelium_server_privatestill routes throughlegacy_runnerwhich uses the temporaryUnimplementedMyceliumRpcplaceholder — its RPC endpoints currently return-32603 not yet implemented (Step 6). Per the iteration scope this was deliberate; promoting_privateto use the realMyceliumRpcImplis a follow-up issue.--api-addrCLI flag has been removed. Operator scripts that referenced it must update to use the canonical UDS (or TCP 8990 JSON-RPC).$HOME/hero/var/mycelium/state directory was created on this host during the Step 8 smoke test (since the binary now ignores--helpand boots). Removable withrm -rf ~/hero/var/mycelium.