2026-05-15 15:08:20 +02:00
use anyhow ::{ Context , Result , bail } ;
2026-05-20 13:57:14 +02:00
use base64 ::Engine ;
2026-07-18 16:08:19 +02:00
use clap ::{ Args , CommandFactory , FromArgMatches , Parser , Subcommand , ValueEnum } ;
2026-05-23 02:40:03 +02:00
use clap_complete ::{ Shell , generate } ;
2026-07-18 16:15:13 +02:00
use geth_config ::{ ConfigKey , GethConfig , GethPaths } ;
2026-07-05 22:24:08 +02:00
use geth_control ::{ ControlRequest , ControlResponse , SyncStreamStatus } ;
2026-07-18 16:31:59 +02:00
use geth_node ::service ::{
ServiceInstallOptions , ServiceLogState , ServiceLogsReport , ServiceManager , ServiceReport ,
} ;
2026-07-18 15:23:20 +02:00
use std ::io ::{ Read , Write , stdout } ;
2026-05-15 15:08:20 +02:00
use std ::path ::PathBuf ;
2026-07-05 22:24:08 +02:00
use std ::time ::{ Duration , Instant } ;
2026-05-15 15:08:20 +02:00
2026-05-23 01:17:30 +02:00
const TOP_LEVEL_AFTER_HELP : & str = r #" Common starts:
2026-07-18 15:23:20 +02:00
geth daemon install # initialize , install , and start in background
geth daemon run - - ephemeral # disposable foreground daemon
geth guide quickstart
2026-05-23 01:17:30 +02:00
geth guide owner - setup
geth init
geth init - - admin - key ~ / . ssh / id_ed25519_sk . pub - - signing - key ~ / . ssh / id_ed25519_sk - - node - name laptop
geth daemon run
geth status
2026-07-18 15:23:20 +02:00
Use ` geth < command > - - help ` for details and ` geth guide ` for complete workflows . " #;
const DAEMON_AFTER_HELP : & str = r #" Examples:
geth daemon install # persistent user service ; starts immediately
geth daemon status
2026-07-18 16:31:59 +02:00
geth daemon logs
2026-07-18 15:23:20 +02:00
geth daemon stop
geth daemon start
geth daemon uninstall
geth daemon run # foreground , persistent state
geth daemon run - - ephemeral # foreground , temporary state
The service commands only use the current user ' s service manager . They never
install a privileged system service . " #;
const SERVICE_MANAGER_HELP : & str = " Backend: auto, systemd-user, launchd-user, or windows-task " ;
2026-05-23 01:17:30 +02:00
const INIT_LONG_ABOUT : & str = r #" Initialize local geth state.
With no owner options , ` geth init ` creates local directories , config , metadata
store , and the daemon agent identity . This is enough for local CAS / KV / document
testing and for later enrollment into an owner ' s mesh .
Owner setup is SSH - admin - rooted . ` - - admin - key ` is the OpenSSH public key that is
recorded as an admin trust anchor . ` - - signing - key ` is the matching private SSH
key path used immediately through ` ssh - keygen - Y sign ` to sign the initial
keychain / auth statements . For security - key / YubiKey keys , use the private key
stub path such as ` ~ / . ssh / id_ed25519_sk ` ; ssh - keygen / ssh - agent will trigger the
hardware - backed signing flow .
If any owner setup option is used ( ` - - admin - key ` , ` - - signing - key ` , ` - - owner ` ,
` - - node - name ` , or ` - - capability ` ) , both ` - - admin - key ` and ` - - signing - key ` are
required so geth never creates unsigned owner / device / node statements by
accident . " #;
const INIT_AFTER_HELP : & str = r #" Examples:
# Local - only node for development or later enrollment :
geth init
# Owner / admin node using an SSH or YubiKey - backed admin key :
geth init \
- - admin - key ~ / . ssh / id_ed25519_sk . pub \
- - signing - key ~ / . ssh / id_ed25519_sk \
- - owner eric \
- - node - name laptop
# Owner node with initial resource grants :
geth init \
- - admin - key ~ / . ssh / id_ed25519 . pub \
- - signing - key ~ / . ssh / id_ed25519 \
- - node - name laptop \
- - capability resource :cas :local = cas . fetch \
- - capability resource :ssh - proxy :local = ssh_proxy . connect
Key paths :
- - admin - key OpenSSH public key , usually * . pub . Stored as the trust anchor .
- - signing - key Matching private key or security - key stub . Used to sign init ops .
Related :
geth guide owner - setup
geth keychain status
geth node list " #;
const GUIDE_INDEX : & str = r #" Usage: geth guide <topic>
Topics :
2026-07-18 15:23:20 +02:00
quickstart Choose disposable , foreground , or background startup .
2026-05-23 01:17:30 +02:00
init Local - only init versus owner / admin init .
owner - setup First owner node with SSH / YubiKey admin trust .
enrollment Add another node / device to the owner mesh .
keys Meaning of - - admin - key and - - signing - key .
overlay Optional Iroh overlay network planning .
service Install and manage geth as a user service .
2026-05-23 02:40:03 +02:00
completions Shell completion installation examples .
2026-05-23 01:17:30 +02:00
smoke - test Minimal commands to verify a node and daemon . " #;
2026-07-18 15:23:20 +02:00
const GUIDE_QUICKSTART : & str = r #" Choose the startup that matches what you are doing.
Try geth without keeping state :
geth daemon run - - ephemeral
The command prints its temporary home . In another terminal , use that path :
geth - - home < printed - path > status
geth - - home < printed - path > node id
The state is removed after a normal daemon shutdown ( Ctrl - C ) .
Install and start a persistent background daemon :
geth daemon install
geth status
` daemon install ` creates the local home if necessary , installs a user - level
service , enables it for future logins , and starts it immediately . Manage it with :
geth daemon status
geth daemon stop
geth daemon start
geth daemon uninstall
Run a persistent daemon in the foreground instead :
geth init
geth daemon run
Use ` - - home < dir > ` on any command to select an isolated home without exporting
GETH_HOME . See ` geth guide owner - setup ` before adding other machines .
" #;
2026-05-23 01:17:30 +02:00
const GUIDE_INIT : & str = r #" geth init has two modes.
Local - only :
geth init
Creates GETH_HOME , config . toml , geth . sqlite , CAS directories , and a local agent
identity . Use this for local testing or for a node that will later request
enrollment into an owner ' s mesh .
Owner / admin :
geth init - - admin - key ~ / . ssh / id_ed25519_sk . pub - - signing - key ~ / . ssh / id_ed25519_sk - - node - name laptop
This records signed owner , device , node , and agent bindings . Use it on the
machine where you control the admin SSH / YubiKey key . Once initialized , inspect :
geth keychain status
geth node list
" #;
const GUIDE_OWNER_SETUP : & str = r #" Owner setup flow:
1. Pick or create an SSH admin key . Security - key / YubiKey - backed OpenSSH keys are
supported through ssh - keygen :
ssh - keygen - t ed25519 - sk - f ~ / . ssh / id_ed25519_sk
2. Initialize the owner node :
geth init \
- - admin - key ~ / . ssh / id_ed25519_sk . pub \
- - signing - key ~ / . ssh / id_ed25519_sk \
- - owner eric \
- - node - name owner - laptop
3. Start the daemon and export a peer card :
geth daemon run
geth peer export - - out / tmp / owner . peer . json
` - - admin - key ` is public and replicated as the admin trust anchor .
` - - signing - key ` is private and only used locally to sign canonical init ops .
" #;
const GUIDE_ENROLLMENT : & str = r #" Add another node/device:
On the new node :
geth init
2026-07-18 16:48:35 +02:00
geth daemon install
geth node enroll join / tmp / owner . peer . json \
- - admin - key / tmp / owner - admin . pub \
- - node - name workstation
2026-05-23 01:17:30 +02:00
On the owner / YubiKey machine :
geth node enroll list
geth node enroll approve < request - id > - - signing - key ~ / . ssh / id_ed25519_sk
Back on the new node :
2026-07-18 16:48:35 +02:00
geth node enroll sync < owner - node - id >
geth keychain status
2026-05-23 01:17:30 +02:00
Enrollment approval records signed keychain / auth operations . Discovery and peer
2026-07-18 16:48:35 +02:00
cards alone never grant trust or capabilities . The join command combines peer
import , explicit admin public - key trust bootstrap , signed request creation , and
Iroh submission ; it does not approve the request . Verify the admin public key
through a separate trusted channel before using it . For offline transfer or
recovery , the lower - level ` request ` , ` submit ` , and ` import ` commands remain
available .
2026-05-23 01:17:30 +02:00
" #;
const GUIDE_KEYS : & str = r #" Key terminology:
- - admin - key
OpenSSH public key path , usually ending in . pub . This key is recorded in the
geth keychain as an admin trust anchor . It is safe to distribute .
- - signing - key
Matching private key path , or the OpenSSH security - key / YubiKey stub path . geth
shells out to ssh - keygen - Y sign with explicit namespaces to sign canonical
geth keychain / auth operations . The private key is not copied into geth state .
2026-05-27 00:59:52 +02:00
Signing sources :
Local key file :
- - signing - key ~ / . ssh / id_ed25519 - - admin - key ~ / . ssh / id_ed25519 . pub
Encrypted key file :
Load it into ssh - agent with ` ssh - add ~ / . ssh / id_ed25519 ` , then sign through
the agent by passing the public key path :
- - signing - key ~ / . ssh / id_ed25519 . pub
FIDO / YubiKey OpenSSH key :
Use the security - key stub or load it into ssh - agent :
- - signing - key ~ / . ssh / id_ed25519_sk - - admin - key ~ / . ssh / id_ed25519_sk . pub
PKCS #11 :
Direct ssh - keygen - Y signing does not expose a portable - D provider option .
Load the token key into ssh - agent with ` ssh - add - s < provider > ` , then pass
the public key path with - - signing - key .
2026-05-23 01:17:30 +02:00
Examples :
Software key :
- - admin - key ~ / . ssh / id_ed25519 . pub - - signing - key ~ / . ssh / id_ed25519
YubiKey / FIDO OpenSSH key :
- - admin - key ~ / . ssh / id_ed25519_sk . pub - - signing - key ~ / . ssh / id_ed25519_sk
2026-05-27 00:59:52 +02:00
Generate the active OpenSSH allowed_signers projection :
geth keychain allowed - signers - - out ~ / . config / geth / allowed_signers
Sign an arbitrary authorized_keys snapshot with an active admin key :
geth keychain sign - file - - in ~ / . ssh / authorized_keys - - out ~ / . ssh / authorized_keys . sig - - signing - key ~ / . ssh / id_ed25519_sk
Verify the snapshot signature against the current keychain trust root :
geth keychain verify - file - - in ~ / . ssh / authorized_keys - - signature ~ / . ssh / authorized_keys . sig
2026-05-23 01:17:30 +02:00
If you use - - owner , - - node - name , or - - capability during init , geth requires both
key options because those fields create signed owner / device / node statements .
" #;
const GUIDE_SERVICE : & str = r #" Install geth as a user service:
2026-07-18 15:23:20 +02:00
geth daemon install
geth daemon status
2026-07-18 16:31:59 +02:00
geth daemon logs
geth daemon logs - - follow
2026-07-18 15:23:20 +02:00
geth daemon stop
geth daemon start
geth daemon uninstall
2026-05-23 01:17:30 +02:00
Service installation targets user service managers , not system services :
Linux : systemd - - user
macOS : launchd user agent
Windows : current - user scheduled task
2026-07-18 16:31:59 +02:00
` daemon install ` waits for the local control endpoint to become ready . If the
current executable is under a Cargo target or temporary directory , geth copies
it into ` < geth - home > / bin ` before creating the service definition so ` cargo
clean ` or temporary - file cleanup cannot break the service . Pass
` - - allow - transient - binary ` only when that direct reference is intentional .
2026-05-23 01:17:30 +02:00
Preview definitions without installing :
geth daemon service print
2026-07-18 15:23:20 +02:00
The longer ` geth daemon service .. . ` command family remains available for
automation compatibility and for installing without immediately starting via
` geth daemon service install ` .
2026-05-23 01:17:30 +02:00
" #;
2026-05-23 02:40:03 +02:00
const GUIDE_COMPLETIONS : & str = r #" Shell completions:
geth can print completions for bash , zsh , fish , PowerShell , and elvish . The
generated scripts are produced from the same Clap command tree as ` geth - - help ` ,
so subcommands and flags stay in sync with the executable .
Bash :
mkdir - p ~ / . local / share / bash - completion / completions
geth completions bash > ~ / . local / share / bash - completion / completions / geth
Zsh :
mkdir - p ~ / . zfunc
geth completions zsh > ~ / . zfunc / _geth
# Ensure ~ / . zfunc is in fpath , then run : compinit
Fish :
mkdir - p ~ / . config / fish / completions
geth completions fish > ~ / . config / fish / completions / geth . fish
PowerShell :
geth completions powershell > geth . ps1
# Source geth . ps1 from your PowerShell profile .
Elvish :
mkdir - p ~ / . elvish / lib
geth completions elvish > ~ / . elvish / lib / geth . elv
" #;
2026-05-23 01:17:30 +02:00
const GUIDE_OVERLAY : & str = r #" Optional overlay network:
geth has an experimental overlay - network design inspired by iroh - lan . The
intended future runtime is a private L3 - style packet overlay carried over geth ' s
daemon - owned Iroh endpoint .
Current prototype commands :
geth overlay status
geth overlay plan home
geth overlay plan home - - cidr 172.2 2. 0.0 / 24
geth overlay join home - - secret < resource - secret >
2026-05-23 02:08:51 +02:00
geth overlay interface - plan home - - platform linux
geth overlay up home
geth overlay down home
geth overlay send home < node > - - packet - base64 < ipv4 - packet >
geth overlay recv home
2026-05-23 01:17:30 +02:00
geth overlay leave home
Current limits :
2026-05-23 02:08:51 +02:00
- overlay up creates a real TUN / Wintun - style L3 device and usually needs
privileges or host network entitlements
- host network changes are explicit opt - in only
2026-05-23 01:17:30 +02:00
- discovery can suggest peers , but never grants overlay access
- overlay access must be resource - authorized with overlay . join / overlay . route
- all overlay packets must be carried over Iroh , not SSH or another transport
2026-05-30 12:24:40 +02:00
- release / platform notes live in docs / overlay - platforms . md
2026-05-23 02:08:51 +02:00
Bearer invite flow :
geth resource create overlay home
geth secret bearer create resource :overlay :home - - capability overlay . join
geth overlay join home - - secret < bearer - token >
2026-05-23 01:17:30 +02:00
" #;
const GUIDE_SMOKE_TEST : & str = r #" Minimal smoke test:
2026-07-18 15:23:20 +02:00
Terminal 1 ( prints a temporary home ) :
geth daemon run - - ephemeral
2026-05-23 01:17:30 +02:00
Terminal 2 :
2026-07-18 15:23:20 +02:00
geth - - home < printed - path > status
geth - - home < printed - path > node id
2026-05-23 01:17:30 +02:00
echo " hello geth " > / tmp / hello - geth . txt
2026-07-18 15:23:20 +02:00
geth - - home < printed - path > cas add / tmp / hello - geth . txt
geth - - home < printed - path > cas list
geth - - home < printed - path > keychain status
2026-05-23 01:17:30 +02:00
For two - node owner / enrollment testing , use :
geth guide owner - setup
geth guide enrollment
" #;
2026-05-15 15:08:20 +02:00
#[ derive(Debug, Parser) ]
2026-05-23 01:17:30 +02:00
#[ command(
name = " geth " ,
2026-07-18 15:23:20 +02:00
version ,
2026-05-23 01:17:30 +02:00
about = " Personal local-first Iroh mesh runtime " ,
2026-07-18 15:23:20 +02:00
long_about = " Personal local-first Iroh mesh runtime for nodes, resources, and secure peer workflows. \n \n This is the local-first geth project, not the Ethereum client. One executable provides both daemon and control commands. " ,
after_long_help = TOP_LEVEL_AFTER_HELP ,
arg_required_else_help = true
2026-05-23 01:17:30 +02:00
) ]
2026-05-15 15:08:20 +02:00
pub struct Cli {
2026-07-18 15:23:20 +02:00
#[ arg(
long ,
global = true ,
value_name = " DIR " ,
help = " Use DIR as geth home instead of GETH_HOME or the OS data directory "
) ]
pub home : Option < PathBuf > ,
2026-07-18 17:15:36 +02:00
#[ arg(
long ,
global = true ,
conflicts_with = " jsonl " ,
help = " Print one pretty machine-readable JSON document "
) ]
2026-05-15 15:08:20 +02:00
pub json : bool ,
2026-05-23 01:17:30 +02:00
#[ arg(
long ,
global = true ,
2026-07-18 17:15:36 +02:00
conflicts_with = " json " ,
help = " Print each result as one compact newline-delimited JSON object "
2026-05-23 01:17:30 +02:00
) ]
2026-05-15 15:08:20 +02:00
pub jsonl : bool ,
#[ command(subcommand) ]
pub command : Command ,
}
#[ derive(Debug, Subcommand) ]
pub enum Command {
2026-07-18 15:23:20 +02:00
/// Show task-oriented setup and workflow guides
2026-05-23 01:17:30 +02:00
Guide {
#[ arg(value_enum) ]
topic : Option < GuideTopic > ,
} ,
2026-07-18 15:23:20 +02:00
/// Generate shell completion scripts
2026-05-23 02:40:03 +02:00
Completions {
#[ arg(value_enum) ]
shell : Shell ,
} ,
2026-07-18 15:23:20 +02:00
#[ command(
about = " Initialize local state and identity " ,
long_about = INIT_LONG_ABOUT ,
after_long_help = INIT_AFTER_HELP
) ]
2026-05-21 11:29:29 +02:00
Init {
2026-05-23 01:17:30 +02:00
#[ arg(
long ,
value_name = " OPENSSH_PUBLIC_KEY " ,
help = " OpenSSH public key recorded as the owner/admin trust anchor " ,
long_help = " Path to the OpenSSH public key recorded as the owner/admin trust anchor, usually ~/.ssh/<key>.pub. This key is public and replicated in the geth keychain. "
) ]
2026-05-21 11:29:29 +02:00
admin_key : Option < PathBuf > ,
2026-05-23 01:17:30 +02:00
#[ arg(
long ,
value_name = " OPENSSH_PRIVATE_KEY " ,
help = " Matching private key or YubiKey/FIDO stub used to sign init statements " ,
long_help = " Path to the matching private OpenSSH key, or security-key/YubiKey stub such as ~/.ssh/id_ed25519_sk. geth uses ssh-keygen -Y sign with explicit geth namespaces; it does not copy the private key into geth state. "
) ]
2026-05-21 11:29:29 +02:00
signing_key : Option < PathBuf > ,
2026-05-23 01:17:30 +02:00
#[ arg(
long ,
default_value = " owner " ,
help = " Owner/user display name recorded during owner init "
) ]
2026-05-21 11:29:29 +02:00
owner : String ,
2026-05-23 01:17:30 +02:00
#[ arg(
long ,
default_value = " local " ,
help = " Friendly node name recorded during owner init "
) ]
2026-05-21 11:29:29 +02:00
node_name : String ,
2026-05-23 01:17:30 +02:00
#[ arg(
long = " capability " ,
value_name = " RESOURCE=CAPABILITY " ,
help = " Initial capability grant for this node; repeatable "
) ]
2026-05-21 11:29:29 +02:00
capabilities : Vec < String > ,
} ,
2026-07-18 15:23:20 +02:00
/// Run, install, and manage the daemon
2026-05-15 15:08:20 +02:00
Daemon {
#[ command(subcommand) ]
command : DaemonCommand ,
} ,
2026-07-18 16:15:13 +02:00
/// Inspect, validate, and safely edit daemon configuration
Config {
#[ command(subcommand) ]
command : ConfigCommand ,
} ,
2026-07-18 15:23:20 +02:00
/// Show daemon, storage, Iroh, and backend health
2026-05-15 15:08:20 +02:00
Status ,
2026-07-18 15:23:20 +02:00
/// Inspect or trigger peer synchronization
2026-05-21 19:31:05 +02:00
Sync {
#[ command(subcommand) ]
command : SyncCommand ,
} ,
2026-07-18 15:23:20 +02:00
/// Wait for daemon, peer, or sync readiness
2026-07-05 22:24:08 +02:00
Wait {
#[ command(subcommand) ]
command : WaitCommand ,
} ,
2026-07-18 15:23:20 +02:00
/// Create or restore an offline local-state backup
2026-07-05 22:35:18 +02:00
Backup {
#[ command(subcommand) ]
command : BackupCommand ,
} ,
2026-07-18 15:23:20 +02:00
/// Diagnose local configuration, daemon, trust, and dependency problems
2026-07-05 22:39:15 +02:00
Doctor ,
2026-07-18 15:23:20 +02:00
/// Inspect and manage trusted nodes and enrollment
2026-05-15 15:08:20 +02:00
Node {
#[ command(subcommand) ]
command : NodeCommand ,
} ,
2026-07-18 15:23:20 +02:00
/// Exchange peer cards and test peer connectivity
2026-05-18 04:03:52 +02:00
Peer {
#[ command(subcommand) ]
command : PeerCommand ,
} ,
2026-07-18 15:23:20 +02:00
/// Plan and operate the experimental Iroh overlay network
2026-05-23 01:17:30 +02:00
Overlay {
#[ command(subcommand) ]
command : OverlayCommand ,
} ,
2026-07-18 17:21:12 +02:00
/// List/create resources and discover authorization capabilities
2026-05-15 15:08:20 +02:00
Resource {
#[ command(subcommand) ]
command : ResourceCommand ,
} ,
2026-07-18 15:23:20 +02:00
/// Manage the SSH-admin-rooted identity keychain
2026-05-15 15:08:20 +02:00
Keychain {
#[ command(subcommand) ]
command : KeychainCommand ,
} ,
2026-07-18 15:23:20 +02:00
/// Explain and manage resource-scoped authorization
2026-05-15 15:08:20 +02:00
Auth {
#[ command(subcommand) ]
command : AuthCommand ,
} ,
2026-07-18 15:23:20 +02:00
/// Manage resource secret epochs and bearer access
2026-05-15 15:08:20 +02:00
Secret {
#[ command(subcommand) ]
command : SecretCommand ,
} ,
2026-07-18 15:23:20 +02:00
/// Store, fetch, pin, and synchronize content-addressed data
2026-05-15 15:08:20 +02:00
Cas {
#[ command(subcommand) ]
command : CasCommand ,
} ,
2026-07-18 15:23:20 +02:00
/// Operate synchronized key-value stores
2026-05-15 15:08:20 +02:00
Kv {
#[ command(subcommand) ]
command : KvCommand ,
} ,
2026-07-18 15:23:20 +02:00
/// Publish or read daemon-lifetime topic messages
2026-05-15 15:08:20 +02:00
Pubsub {
#[ command(subcommand) ]
command : PubsubCommand ,
} ,
2026-07-18 15:23:20 +02:00
/// Open authorized message and byte-stream pipes
2026-05-15 15:08:20 +02:00
Pipe {
#[ command(subcommand) ]
command : PipeCommand ,
} ,
2026-07-18 15:23:20 +02:00
/// Register and synchronize SQLite/cr-sqlite databases
2026-05-15 15:08:20 +02:00
Db {
#[ command(subcommand) ]
command : DbCommand ,
} ,
2026-07-18 15:23:20 +02:00
/// Create and synchronize document resources
2026-05-15 15:08:20 +02:00
Document {
#[ command(subcommand) ]
command : DocumentCommand ,
} ,
2026-07-18 15:23:20 +02:00
/// Use geth-managed SSH proxy, certificate, and revocation workflows
2026-05-15 15:08:20 +02:00
Ssh {
#[ command(subcommand) ]
command : SshCommand ,
} ,
}
2026-05-23 01:17:30 +02:00
#[ derive(Clone, Debug, ValueEnum) ]
pub enum GuideTopic {
2026-07-18 15:23:20 +02:00
Quickstart ,
2026-05-23 01:17:30 +02:00
Init ,
OwnerSetup ,
Enrollment ,
Keys ,
Overlay ,
Service ,
2026-05-23 02:40:03 +02:00
Completions ,
2026-05-23 01:17:30 +02:00
SmokeTest ,
}
2026-07-18 16:15:13 +02:00
#[ derive(Debug, Subcommand) ]
pub enum ConfigCommand {
/// Print the selected config.toml path
Path ,
/// Print the selected config and its effective values
Show ,
/// Parse and validate the complete selected config
Validate ,
/// Safely update one supported setting and validate the result
Set {
#[ arg(value_enum) ]
key : ConfigKeyArg ,
value : String ,
} ,
}
#[ derive(Clone, Copy, Debug, ValueEnum) ]
pub enum ConfigKeyArg {
#[ value(name = " iroh.relay_mode " ) ]
IrohRelayMode ,
#[ value(name = " iroh.relay_map " ) ]
IrohRelayMap ,
#[ value(name = " iroh.local_discovery " ) ]
IrohLocalDiscovery ,
#[ value(name = " sync.live_sync_enabled " ) ]
SyncLiveSyncEnabled ,
#[ value(name = " sync.live_sync_interval_ms " ) ]
SyncLiveSyncIntervalMs ,
}
impl From < ConfigKeyArg > for ConfigKey {
fn from ( value : ConfigKeyArg ) -> Self {
match value {
ConfigKeyArg ::IrohRelayMode = > Self ::IrohRelayMode ,
ConfigKeyArg ::IrohRelayMap = > Self ::IrohRelayMap ,
ConfigKeyArg ::IrohLocalDiscovery = > Self ::IrohLocalDiscovery ,
ConfigKeyArg ::SyncLiveSyncEnabled = > Self ::SyncLiveSyncEnabled ,
ConfigKeyArg ::SyncLiveSyncIntervalMs = > Self ::SyncLiveSyncIntervalMs ,
}
}
}
2026-05-15 15:08:20 +02:00
#[ derive(Debug, Subcommand) ]
2026-07-18 15:23:20 +02:00
#[ command(after_long_help = DAEMON_AFTER_HELP) ]
2026-05-15 15:08:20 +02:00
pub enum DaemonCommand {
2026-07-18 15:23:20 +02:00
/// Run the daemon in the foreground
Run {
#[ arg(
long ,
help = " Use a temporary home that is removed after normal shutdown "
) ]
ephemeral : bool ,
} ,
/// Initialize, install, enable, and start a persistent user service
Install {
#[ arg(long, default_value = " auto " , help = SERVICE_MANAGER_HELP) ]
manager : String ,
#[ arg(long, help = " Executable path stored in the service definition " ) ]
bin : Option < PathBuf > ,
2026-07-18 16:31:59 +02:00
#[ arg(
long ,
help = " Reference a temporary/Cargo-target binary directly instead of copying it into the geth home "
) ]
allow_transient_binary : bool ,
#[ arg(
long ,
help = " Return after starting without waiting for daemon readiness "
) ]
no_wait : bool ,
#[ arg(
long ,
default_value_t = 30_000 ,
help = " Maximum readiness wait in milliseconds "
) ]
timeout_ms : u64 ,
2026-07-18 15:23:20 +02:00
} ,
/// Start the installed user service
Start {
#[ arg(long, default_value = " auto " , help = SERVICE_MANAGER_HELP) ]
manager : String ,
} ,
/// Stop the installed user service
Stop {
#[ arg(long, default_value = " auto " , help = SERVICE_MANAGER_HELP) ]
manager : String ,
} ,
/// Show the installed user service status
Status {
#[ arg(long, default_value = " auto " , help = SERVICE_MANAGER_HELP) ]
manager : String ,
} ,
2026-07-18 16:31:59 +02:00
/// Show recent user-service logs or follow them
Logs {
#[ arg(long, default_value = " auto " , help = SERVICE_MANAGER_HELP) ]
manager : String ,
#[ arg(
long ,
default_value_t = 100 ,
value_parser = parse_log_line_count ,
help = " Recent lines to show per log source "
) ]
lines : usize ,
#[ arg(long, help = " Continue following new log entries " ) ]
follow : bool ,
} ,
2026-07-18 15:23:20 +02:00
/// Stop and remove the installed user service
Uninstall {
#[ arg(long, default_value = " auto " , help = SERVICE_MANAGER_HELP) ]
manager : String ,
} ,
/// Advanced and compatibility-preserving service controls
2026-05-16 00:17:08 +02:00
Service {
#[ command(subcommand) ]
command : ServiceCommand ,
} ,
}
#[ derive(Debug, Subcommand) ]
pub enum ServiceCommand {
2026-07-18 15:23:20 +02:00
/// Install and enable the user service
2026-05-16 00:17:08 +02:00
Install {
2026-07-18 15:23:20 +02:00
#[ arg(long, default_value = " auto " , help = SERVICE_MANAGER_HELP) ]
2026-05-16 00:17:08 +02:00
manager : String ,
2026-07-18 15:23:20 +02:00
#[ arg(long, help = " Executable path stored in the service definition " ) ]
2026-05-16 00:17:08 +02:00
bin : Option < PathBuf > ,
2026-07-18 15:23:20 +02:00
#[ arg(long, help = " Start the service immediately after installation " ) ]
2026-05-16 00:17:08 +02:00
start : bool ,
2026-07-18 16:31:59 +02:00
#[ arg(
long ,
help = " Reference a temporary/Cargo-target binary directly instead of copying it into the geth home "
) ]
allow_transient_binary : bool ,
2026-05-16 00:17:08 +02:00
} ,
2026-07-18 15:23:20 +02:00
/// Stop and remove the user service
2026-05-16 00:17:08 +02:00
Uninstall {
2026-07-18 15:23:20 +02:00
#[ arg(long, default_value = " auto " , help = SERVICE_MANAGER_HELP) ]
2026-05-16 00:17:08 +02:00
manager : String ,
} ,
2026-07-18 15:23:20 +02:00
/// Start the installed user service
2026-05-16 00:17:08 +02:00
Start {
2026-07-18 15:23:20 +02:00
#[ arg(long, default_value = " auto " , help = SERVICE_MANAGER_HELP) ]
2026-05-16 00:17:08 +02:00
manager : String ,
} ,
2026-07-18 15:23:20 +02:00
/// Stop the installed user service
2026-05-16 00:17:08 +02:00
Stop {
2026-07-18 15:23:20 +02:00
#[ arg(long, default_value = " auto " , help = SERVICE_MANAGER_HELP) ]
2026-05-16 00:17:08 +02:00
manager : String ,
} ,
2026-07-18 15:23:20 +02:00
/// Query the user service manager
2026-05-16 00:17:08 +02:00
Status {
2026-07-18 15:23:20 +02:00
#[ arg(long, default_value = " auto " , help = SERVICE_MANAGER_HELP) ]
2026-05-16 00:17:08 +02:00
manager : String ,
} ,
2026-07-18 15:23:20 +02:00
/// Preview the service definition without installing it
2026-05-16 00:17:08 +02:00
Print {
2026-07-18 15:23:20 +02:00
#[ arg(long, default_value = " auto " , help = SERVICE_MANAGER_HELP) ]
2026-05-16 00:17:08 +02:00
manager : String ,
2026-07-18 15:23:20 +02:00
#[ arg(long, help = " Executable path used in the preview " ) ]
2026-05-16 00:17:08 +02:00
bin : Option < PathBuf > ,
} ,
2026-05-15 15:08:20 +02:00
}
2026-05-21 19:31:05 +02:00
#[ derive(Debug, Subcommand) ]
pub enum SyncCommand {
2026-07-18 15:23:20 +02:00
/// Show per-peer stream health, cursors, and retry state
2026-05-21 19:31:05 +02:00
Status ,
2026-07-18 15:23:20 +02:00
/// Synchronize all known peers or one selected node immediately
2026-05-21 19:31:05 +02:00
Now { node : Option < String > } ,
}
2026-07-05 22:24:08 +02:00
#[ derive(Debug, Subcommand) ]
pub enum WaitCommand {
2026-07-18 15:23:20 +02:00
/// Wait until the local control socket answers
2026-07-05 22:24:08 +02:00
Daemon {
#[ arg(long, default_value_t = 30_000) ]
timeout_ms : u64 ,
#[ arg(long, default_value_t = 250) ]
interval_ms : u64 ,
} ,
2026-07-18 15:23:20 +02:00
/// Wait until an imported peer answers over Iroh
2026-07-05 22:24:08 +02:00
Peer {
node : String ,
#[ arg(long, default_value_t = 30_000) ]
timeout_ms : u64 ,
#[ arg(long, default_value_t = 500) ]
interval_ms : u64 ,
} ,
2026-07-18 15:23:20 +02:00
/// Wait until a peer's sync streams are healthy or marked stale
2026-07-05 22:24:08 +02:00
Sync {
node : String ,
#[ arg(long, default_value_t = 30_000) ]
timeout_ms : u64 ,
#[ arg(long, default_value_t = 500) ]
interval_ms : u64 ,
} ,
}
2026-07-05 22:35:18 +02:00
#[ derive(Debug, Subcommand) ]
pub enum BackupCommand {
2026-07-18 15:23:20 +02:00
/// Create an offline backup directory without private identity keys
2026-07-05 22:35:18 +02:00
Create {
#[ arg(long, value_name = " DIR " ) ]
out : PathBuf ,
} ,
2026-07-18 15:23:20 +02:00
/// Restore a backup into a new, empty geth home
2026-07-05 22:35:18 +02:00
Restore {
backup_dir : PathBuf ,
#[ arg(long, value_name = " DIR " ) ]
target_home : PathBuf ,
} ,
}
2026-05-15 15:08:20 +02:00
#[ derive(Debug, Subcommand) ]
pub enum NodeCommand {
2026-07-18 15:23:20 +02:00
/// Print the local stable node, agent, and Iroh endpoint identifiers
2026-05-15 15:08:20 +02:00
Id ,
2026-07-18 15:23:20 +02:00
/// Show the same local runtime health as `geth status`
2026-05-15 15:08:20 +02:00
Status ,
2026-07-18 15:23:20 +02:00
/// List the active trusted-node view
2026-05-21 11:29:29 +02:00
List ,
2026-07-18 15:23:20 +02:00
/// Request, review, approve, or synchronize node enrollment
2026-05-21 18:01:38 +02:00
Enroll {
#[ command(subcommand) ]
command : NodeEnrollCommand ,
} ,
2026-07-18 15:23:20 +02:00
/// Rename a trusted node with an admin-signed keychain operation
2026-05-21 11:29:29 +02:00
Rename {
node : String ,
name : String ,
#[ arg(long) ]
signing_key : Option < PathBuf > ,
} ,
2026-07-18 15:23:20 +02:00
/// Revoke a trusted node with an admin-signed keychain operation
2026-05-21 11:29:29 +02:00
Revoke {
node : String ,
#[ arg(long) ]
signing_key : Option < PathBuf > ,
} ,
2026-07-18 15:23:20 +02:00
/// Grant a trusted node one resource capability
2026-05-21 11:29:29 +02:00
Grant {
node : String ,
resource : String ,
capability : String ,
#[ arg(long) ]
grant_id : Option < String > ,
2026-05-21 18:15:10 +02:00
#[ arg(long) ]
signing_key : PathBuf ,
#[ arg(long) ]
admin_key : Option < PathBuf > ,
2026-05-21 11:29:29 +02:00
} ,
2026-07-18 15:23:20 +02:00
/// Revoke a previously issued node grant
2026-05-21 11:29:29 +02:00
RevokeGrant {
resource : String ,
grant_id : String ,
2026-05-21 18:15:10 +02:00
#[ arg(long) ]
signing_key : PathBuf ,
#[ arg(long) ]
admin_key : Option < PathBuf > ,
} ,
2026-07-18 15:23:20 +02:00
/// Bind an Iroh endpoint to a trusted node
2026-05-21 18:15:10 +02:00
EndpointAdd {
node : String ,
endpoint : String ,
#[ arg(long) ]
signing_key : PathBuf ,
} ,
2026-07-18 15:23:20 +02:00
/// Revoke an Iroh endpoint binding
2026-05-21 18:15:10 +02:00
EndpointRevoke {
node : String ,
endpoint : String ,
#[ arg(long) ]
signing_key : PathBuf ,
2026-05-21 11:29:29 +02:00
} ,
2026-05-15 15:08:20 +02:00
}
2026-05-21 18:01:38 +02:00
#[ derive(Debug, Subcommand) ]
pub enum NodeEnrollCommand {
2026-07-18 16:48:35 +02:00
/// Import an owner peer card, create a request, and submit it in one step
#[ command(
after_help = " Example: \n geth node enroll join owner.peer.json --admin-key owner-admin.pub --node-name workstation --capability resource:kv:preferences=kv.read \n \n The explicitly supplied admin public key bootstraps trust for later approval sync. Peer-card import supplies candidate routing metadata only. Owner approval remains a separate, admin-signed action. "
) ]
Join {
peer_card_path : PathBuf ,
#[ arg(long) ]
admin_key : PathBuf ,
#[ arg(long) ]
node_name : String ,
#[ arg(long = " capability " ) ]
capabilities : Vec < String > ,
#[ arg(long) ]
reason : Option < String > ,
#[ arg(long) ]
out : Option < PathBuf > ,
} ,
2026-07-18 15:23:20 +02:00
/// Create a signed enrollment request on the new node
2026-07-18 16:08:19 +02:00
#[ command(
after_help = " Examples: \n geth node enroll request --node-name workstation --out workstation.enroll.json \n geth node enroll request --node-name ci-runner --capability resource:kv:builds=kv.read "
) ]
2026-05-21 18:01:38 +02:00
Request {
#[ arg(long) ]
node_name : String ,
#[ arg(long = " capability " ) ]
capabilities : Vec < String > ,
#[ arg(long) ]
reason : Option < String > ,
#[ arg(long) ]
out : Option < PathBuf > ,
} ,
2026-07-18 15:23:20 +02:00
/// Send an enrollment request to an imported owner peer
2026-05-21 18:01:38 +02:00
Submit {
owner_node : String ,
#[ arg(long) ]
request_id : Option < String > ,
#[ arg(long) ]
path : Option < PathBuf > ,
} ,
2026-07-18 15:23:20 +02:00
/// Import an enrollment request from a file
Import { path : PathBuf } ,
/// List received enrollment requests
2026-05-21 18:01:38 +02:00
List {
#[ arg(long) ]
status : Option < String > ,
} ,
2026-07-18 15:23:20 +02:00
/// Approve a request with the owner's SSH admin key
2026-05-21 18:01:38 +02:00
Approve {
request_id : String ,
#[ arg(long) ]
signing_key : PathBuf ,
#[ arg(long) ]
admin_key : Option < PathBuf > ,
#[ arg(long) ]
node_name : Option < String > ,
#[ arg(long = " capability " ) ]
capabilities : Vec < String > ,
} ,
2026-07-18 15:23:20 +02:00
/// Pull approved enrollment state from the owner node
Sync { owner_node : String } ,
2026-05-21 18:01:38 +02:00
}
2026-05-18 04:03:52 +02:00
#[ derive(Debug, Subcommand) ]
pub enum PeerCommand {
2026-07-18 15:23:20 +02:00
/// Export this daemon's signed peer card
2026-05-18 04:03:52 +02:00
Export {
#[ arg(long) ]
out : Option < PathBuf > ,
} ,
2026-07-18 15:23:20 +02:00
/// Import and verify a signed peer card
Import { path : PathBuf } ,
/// List imported and discovered peer candidates
2026-05-18 04:03:52 +02:00
List ,
2026-07-18 15:23:20 +02:00
/// Test protected Iroh connectivity to a peer
Ping { node : String } ,
/// Ask a peer whether this node has a capability
2026-05-18 17:01:50 +02:00
AuthCheck {
node : String ,
resource : String ,
capability : String ,
} ,
2026-05-18 04:03:52 +02:00
}
2026-05-23 01:17:30 +02:00
#[ derive(Debug, Subcommand) ]
pub enum OverlayCommand {
2026-07-18 15:23:20 +02:00
/// Show local overlay memberships and active interfaces
2026-05-23 01:17:30 +02:00
Status ,
2026-07-18 15:23:20 +02:00
/// Preview deterministic overlay addressing without changing state
2026-05-23 01:17:30 +02:00
Plan {
name : String ,
#[ arg(long) ]
cidr : Option < String > ,
} ,
2026-07-18 15:23:20 +02:00
/// Join an overlay using a resource or bearer secret
2026-05-23 01:17:30 +02:00
Join {
name : String ,
#[ arg(long) ]
secret : String ,
#[ arg(long) ]
cidr : Option < String > ,
} ,
2026-07-18 15:23:20 +02:00
/// Remove local overlay membership
Leave { name : String } ,
/// Preview platform-specific interface setup
2026-05-23 02:08:51 +02:00
InterfacePlan {
name : String ,
#[ arg(long) ]
platform : Option < String > ,
} ,
2026-07-18 15:23:20 +02:00
/// Create the local TUN/Wintun-style overlay interface
2026-05-23 02:08:51 +02:00
Up {
name : String ,
#[ arg(long) ]
bearer_secret : Option < String > ,
#[ arg(long) ]
mtu : Option < u16 > ,
} ,
2026-07-18 15:23:20 +02:00
/// Stop the local overlay interface
Down { name : String } ,
/// List peers visible to an overlay
Peers { name : String } ,
/// Send one validated IPv4 packet over Iroh
2026-05-23 02:08:51 +02:00
Send {
name : String ,
node : String ,
#[ arg(long) ]
packet_base64 : String ,
#[ arg(long) ]
bearer_secret : Option < String > ,
} ,
2026-07-18 15:23:20 +02:00
/// Read locally received overlay packets
2026-05-23 02:08:51 +02:00
Recv {
name : String ,
#[ arg(long) ]
peek : bool ,
} ,
2026-05-23 01:17:30 +02:00
}
2026-05-15 15:08:20 +02:00
#[ derive(Debug, Subcommand) ]
pub enum ResourceCommand {
2026-07-18 15:23:20 +02:00
/// List registered resources
2026-05-15 15:08:20 +02:00
List ,
2026-07-18 15:23:20 +02:00
/// Register a named resource
2026-05-15 15:08:20 +02:00
Create { kind : String , name : String } ,
2026-07-18 17:21:12 +02:00
/// List resource ID patterns and the capabilities they accept
#[ command(
alias = " catalog " ,
after_help = " Examples: \n geth resource capabilities \n geth resource capabilities kv \n geth resource capabilities --json "
) ]
Capabilities {
#[ arg(help = " Optional resource family to describe, such as kv, ssh, or pipe " ) ]
family : Option < String > ,
} ,
2026-05-15 15:08:20 +02:00
}
#[ derive(Debug, Subcommand) ]
pub enum KeychainCommand {
2026-07-18 15:23:20 +02:00
/// Initialize local keychain metadata and optional admin trust
2026-05-16 16:34:20 +02:00
Init {
#[ arg(long) ]
admin_key : Option < PathBuf > ,
2026-05-19 16:04:20 +02:00
#[ arg(long) ]
signing_key : Option < PathBuf > ,
2026-05-16 16:34:20 +02:00
} ,
2026-07-18 15:23:20 +02:00
/// Show the reduced identity view and signature verification state
2026-05-15 15:08:20 +02:00
Status ,
2026-07-18 15:23:20 +02:00
/// Add an SSH public key as an admin trust anchor
2026-05-26 18:53:20 +02:00
AdminAdd {
#[ arg(long) ]
admin_key : PathBuf ,
#[ arg(long) ]
signing_key : PathBuf ,
#[ arg(long) ]
principal : Option < String > ,
#[ arg(long) ]
valid_after_ms : Option < i64 > ,
#[ arg(long) ]
valid_before_ms : Option < i64 > ,
} ,
2026-07-18 15:23:20 +02:00
/// Revoke an admin trust anchor
2026-05-26 18:53:20 +02:00
AdminRevoke {
key : String ,
#[ arg(long) ]
signing_key : PathBuf ,
#[ arg(long) ]
admin_key : Option < PathBuf > ,
} ,
2026-07-18 15:23:20 +02:00
/// Write the active OpenSSH allowed_signers projection
2026-05-27 00:59:52 +02:00
AllowedSigners {
#[ arg(long) ]
out : Option < PathBuf > ,
} ,
2026-07-18 15:23:20 +02:00
/// Sign an arbitrary file through an active SSH admin key
2026-05-27 00:59:52 +02:00
SignFile {
#[ arg(long = " in " ) ]
input : PathBuf ,
#[ arg(long) ]
out : Option < PathBuf > ,
#[ arg(long) ]
namespace : Option < String > ,
#[ arg(long) ]
signing_key : PathBuf ,
#[ arg(long) ]
admin_key : Option < PathBuf > ,
} ,
2026-07-18 15:23:20 +02:00
/// Verify a file signature against current keychain trust
2026-05-27 00:59:52 +02:00
VerifyFile {
#[ arg(long = " in " ) ]
input : PathBuf ,
#[ arg(long) ]
signature : PathBuf ,
#[ arg(long) ]
namespace : Option < String > ,
#[ arg(long) ]
allowed_signers : Option < PathBuf > ,
#[ arg(long) ]
principal : Option < String > ,
} ,
2026-07-18 21:00:06 +02:00
/// Verify a linked SSHSIGCHAIN JSONL file against an explicit root key
2026-05-27 00:59:52 +02:00
VerifySigchain {
#[ arg(long = " in " ) ]
input : PathBuf ,
#[ arg(long) ]
2026-07-18 21:00:06 +02:00
chain_id : String ,
2026-05-27 00:59:52 +02:00
#[ arg(long) ]
2026-07-18 21:00:06 +02:00
root_key : PathBuf ,
#[ arg(long, default_value = geth_keychain::SSH_SIGCHAIN_NAMESPACE) ]
namespace : String ,
2026-05-27 00:59:52 +02:00
} ,
2026-07-18 15:23:20 +02:00
/// Explain why one keychain operation was accepted or rejected
Explain { op_id : String } ,
/// Explain the current trust state of one signer
ExplainSigner { key : String } ,
/// Verify all locally stored keychain operations
2026-05-26 18:53:20 +02:00
Verify ,
2026-07-18 15:23:20 +02:00
/// Pull verified keychain operations from a trusted peer
Sync { node : String } ,
2026-05-15 15:08:20 +02:00
}
#[ derive(Debug, Subcommand) ]
pub enum AuthCommand {
2026-07-18 15:23:20 +02:00
/// Explain an allow or deny decision for one subject and capability
2026-05-15 15:08:20 +02:00
Explain {
subject : String ,
resource : String ,
capability : String ,
} ,
2026-07-18 15:23:20 +02:00
/// Pull verified authorization operations from a trusted peer
Sync { node : String } ,
/// Add an admin-signed resource capability grant
2026-07-18 16:08:19 +02:00
#[ command(
after_help = " Example: \n geth auth grant <node-id> resource:kv:preferences kv.read --signing-key ~/.ssh/id_ed25519 "
) ]
2026-05-16 16:32:03 +02:00
Grant {
subject : String ,
resource : String ,
capability : String ,
#[ arg(long) ]
grant_id : Option < String > ,
2026-05-21 18:15:10 +02:00
#[ arg(long) ]
signing_key : PathBuf ,
#[ arg(long) ]
admin_key : Option < PathBuf > ,
2026-05-16 16:32:03 +02:00
} ,
2026-07-18 15:23:20 +02:00
/// Revoke an admin-signed grant by id
2026-05-16 16:32:03 +02:00
Revoke {
resource : String ,
grant_id : String ,
2026-05-21 18:15:10 +02:00
#[ arg(long) ]
signing_key : PathBuf ,
#[ arg(long) ]
admin_key : Option < PathBuf > ,
2026-05-16 16:32:03 +02:00
} ,
2026-05-15 15:08:20 +02:00
}
#[ derive(Debug, Subcommand) ]
pub enum SecretCommand {
2026-07-18 15:23:20 +02:00
/// List resource secret epochs
2026-05-15 15:08:20 +02:00
Status ,
2026-07-18 15:23:20 +02:00
/// Create the first local secret epoch for a resource
Create { resource : String } ,
/// Rotate a resource to a new local secret epoch
Rotate { resource : String } ,
/// Create, prove, verify, list, or revoke bearer access
2026-05-17 02:58:58 +02:00
Bearer {
#[ command(subcommand) ]
command : SecretBearerCommand ,
} ,
}
#[ derive(Debug, Subcommand) ]
pub enum SecretBearerCommand {
2026-07-18 15:23:20 +02:00
/// Create resource-scoped bearer access and print its token once
2026-05-17 02:58:58 +02:00
Create {
resource : String ,
#[ arg(long = " capability " , required = true) ]
capabilities : Vec < String > ,
#[ arg(long) ]
expires_at_ms : Option < i64 > ,
} ,
2026-07-18 15:23:20 +02:00
/// List public bearer metadata without private tokens
2026-05-17 02:58:58 +02:00
List ,
2026-07-18 15:23:20 +02:00
/// Issue a possession challenge for requested capabilities
2026-05-19 19:08:08 +02:00
Challenge {
resource : String ,
#[ arg(long = " capability " , required = true) ]
capabilities : Vec < String > ,
} ,
2026-07-18 15:23:20 +02:00
/// Produce a challenge response from a bearer token
2026-05-19 19:08:08 +02:00
Prove {
2026-05-20 13:10:34 +02:00
token : String ,
2026-05-19 19:08:08 +02:00
resource : String ,
#[ arg(long) ]
nonce : String ,
#[ arg(long = " capability " , required = true) ]
capabilities : Vec < String > ,
} ,
2026-07-18 15:23:20 +02:00
/// Verify a bearer challenge response locally
2026-05-19 19:08:08 +02:00
Verify {
2026-05-20 13:10:34 +02:00
token : String ,
2026-05-19 19:08:08 +02:00
resource : String ,
#[ arg(long) ]
nonce : String ,
#[ arg(long) ]
response : String ,
#[ arg(long = " capability " , required = true) ]
capabilities : Vec < String > ,
} ,
2026-07-18 15:23:20 +02:00
/// Revoke bearer access by its public id
Revoke { resource : String , bearer_id : String } ,
2026-05-15 15:08:20 +02:00
}
#[ derive(Debug, Subcommand) ]
pub enum CasCommand {
2026-07-18 15:23:20 +02:00
/// Add a file to the local content-addressed store
Add { path : PathBuf } ,
/// Add a prototype resource-encrypted file envelope
AddPrivate { resource : String , path : PathBuf } ,
/// Copy a local blob to a path
2026-05-15 15:08:20 +02:00
Get {
hash : String ,
#[ arg(long) ]
out : PathBuf ,
} ,
2026-07-18 15:23:20 +02:00
/// Decrypt a prototype private blob to a path
2026-05-21 01:35:00 +02:00
GetPrivate {
resource : String ,
hash : String ,
#[ arg(long) ]
out : PathBuf ,
} ,
2026-07-18 15:23:20 +02:00
/// Fetch an authorized blob from an imported peer over Iroh
2026-05-18 17:18:25 +02:00
Fetch {
node : String ,
hash : String ,
2026-05-19 19:16:30 +02:00
#[ arg(long) ]
bearer_secret : Option < String > ,
2026-05-18 17:18:25 +02:00
} ,
2026-07-18 15:23:20 +02:00
/// Hash a file without adding it
Hash { path : PathBuf } ,
/// Check whether a blob exists locally
Has { hash : String } ,
/// Protect a blob from cleanup
Pin { hash : String } ,
/// Allow a blob to be removed by cleanup
Unpin { hash : String } ,
/// Remove unpinned local blobs
2026-05-16 21:10:25 +02:00
Cleanup {
#[ arg(long) ]
dry_run : bool ,
} ,
2026-07-18 15:23:20 +02:00
/// List known local and peer providers for a blob
Providers { hash : String } ,
/// List local blobs and pin state
2026-05-15 15:08:20 +02:00
List ,
2026-07-18 15:23:20 +02:00
/// Register, scan, synchronize, and safely apply file roots
2026-05-18 03:50:09 +02:00
Root {
#[ command(subcommand) ]
command : CasRootCommand ,
} ,
2026-07-18 15:23:20 +02:00
/// Inspect and resolve durable file-root conflicts
2026-05-18 03:57:26 +02:00
Conflict {
#[ command(subcommand) ]
command : CasConflictCommand ,
} ,
2026-05-18 03:50:09 +02:00
}
#[ derive(Debug, Subcommand) ]
pub enum CasRootCommand {
2026-07-18 15:23:20 +02:00
/// Register a local directory as a named file root
Add { name : String , path : PathBuf } ,
/// List local and peer-qualified file roots
2026-05-18 03:50:09 +02:00
List ,
2026-07-18 15:23:20 +02:00
/// Scan a local root and store its deterministic CAS tree
Scan { name : String } ,
/// Pull authorized tree metadata and bytes from a peer
2026-05-20 13:22:40 +02:00
Sync {
node : String ,
name : String ,
#[ arg(long) ]
bearer_secret : Option < String > ,
} ,
2026-07-18 15:23:20 +02:00
/// Materialize a tree without overwriting local edits
2026-07-18 16:08:19 +02:00
#[ command(
after_help = " Examples: \n geth cas root apply photos --to ./restored-photos --dry-run \n geth cas root apply remote-<peer-id>-photos --to ./restored-photos "
) ]
2026-05-20 13:30:57 +02:00
Apply {
source : String ,
#[ arg(long) ]
to : PathBuf ,
#[ arg(long) ]
dry_run : bool ,
} ,
2026-05-15 15:08:20 +02:00
}
2026-05-18 03:57:26 +02:00
#[ derive(Debug, Subcommand) ]
pub enum CasConflictCommand {
2026-07-18 15:23:20 +02:00
/// Record a file-root conflict explicitly
2026-05-18 03:57:26 +02:00
Record {
root : String ,
path : String ,
kind : String ,
#[ arg(long) ]
detail : String ,
#[ arg(long) ]
base_tree : Option < String > ,
#[ arg(long) ]
local_tree : Option < String > ,
#[ arg(long) ]
remote_tree : Option < String > ,
} ,
2026-07-18 15:23:20 +02:00
/// List unresolved and resolved conflicts
2026-05-18 03:57:26 +02:00
List {
#[ arg(long) ]
root : Option < String > ,
} ,
2026-07-18 15:23:20 +02:00
/// Record an operator-selected conflict resolution
2026-05-18 03:57:26 +02:00
Resolve {
conflict_id : String ,
resolution : String ,
#[ arg(long) ]
note : Option < String > ,
} ,
}
2026-05-15 15:08:20 +02:00
#[ derive(Debug, Subcommand) ]
pub enum KvCommand {
2026-07-18 15:23:20 +02:00
/// Create a named local key-value store
Create { name : String } ,
/// Set a local key, optionally checking a non-owner subject
2026-07-18 16:08:19 +02:00
#[ command(
after_help = " Examples: \n geth kv set preferences theme dark \n geth kv set preferences theme dark --subject <node-id> "
) ]
2026-05-15 15:08:20 +02:00
Set {
name : String ,
key : String ,
value : String ,
2026-05-18 04:06:41 +02:00
#[ arg(long) ]
subject : Option < String > ,
2026-05-15 15:08:20 +02:00
} ,
2026-07-18 15:23:20 +02:00
/// Read a local value
Get { name : String , key : String } ,
/// Pull authorized values from a peer over Iroh
2026-05-18 18:36:03 +02:00
Sync {
node : String ,
name : String ,
2026-05-19 19:16:30 +02:00
#[ arg(long) ]
bearer_secret : Option < String > ,
2026-05-18 18:36:03 +02:00
} ,
2026-05-15 15:08:20 +02:00
}
#[ derive(Debug, Subcommand) ]
pub enum PubsubCommand {
2026-07-18 15:23:20 +02:00
/// Publish a daemon-lifetime message locally or to a peer
2026-05-18 18:41:04 +02:00
Pub {
topic : String ,
message : String ,
#[ arg(long) ]
node : Option < String > ,
2026-05-19 19:16:30 +02:00
#[ arg(long) ]
bearer_secret : Option < String > ,
2026-05-18 18:41:04 +02:00
} ,
2026-07-18 15:23:20 +02:00
/// Read the current daemon-lifetime topic snapshot
2026-05-18 18:41:04 +02:00
Sub {
topic : String ,
2026-05-19 15:28:48 +02:00
#[ arg(long) ]
node : Option < String > ,
2026-05-19 19:16:30 +02:00
#[ arg(long) ]
bearer_secret : Option < String > ,
2026-05-18 18:41:04 +02:00
} ,
2026-05-15 15:08:20 +02:00
}
#[ derive(Debug, Subcommand) ]
pub enum PipeCommand {
2026-07-18 15:23:20 +02:00
/// Register a daemon-lifetime listener locally or on a peer
2026-05-18 18:45:10 +02:00
Listen {
name : String ,
2026-05-19 19:21:42 +02:00
#[ arg(long) ]
node : Option < String > ,
#[ arg(long) ]
bearer_secret : Option < String > ,
2026-05-18 18:45:10 +02:00
} ,
2026-07-18 15:23:20 +02:00
/// Request an authorized pipe connection
2026-05-18 18:45:10 +02:00
Connect {
target : String ,
#[ arg(long) ]
node : Option < String > ,
2026-05-19 19:16:30 +02:00
#[ arg(long) ]
bearer_secret : Option < String > ,
2026-05-18 18:45:10 +02:00
} ,
2026-07-18 15:23:20 +02:00
/// Forward a local loopback TCP listener to a peer's loopback target
2026-05-21 01:12:01 +02:00
ForwardTcp {
#[ arg(long) ]
listen : String ,
#[ arg(long) ]
node : String ,
#[ arg(long) ]
target : String ,
#[ arg(long) ]
bearer_secret : Option < String > ,
} ,
2026-07-18 15:23:20 +02:00
/// Forward a local Unix socket to an absolute socket path on a peer
2026-05-21 01:15:51 +02:00
ForwardUnix {
#[ arg(long) ]
listen : PathBuf ,
#[ arg(long) ]
node : String ,
#[ arg(long) ]
target : PathBuf ,
#[ arg(long) ]
bearer_secret : Option < String > ,
} ,
2026-07-18 15:23:20 +02:00
/// Send text, a file, or stdin through a dedicated Iroh pipe
2026-05-20 13:57:14 +02:00
Send {
target : String ,
2026-05-20 13:59:41 +02:00
message : Option < String > ,
#[ arg(long = " in " , value_name = " PATH " ) ]
input : Option < PathBuf > ,
2026-05-20 13:57:14 +02:00
#[ arg(long) ]
node : Option < String > ,
#[ arg(long) ]
bearer_secret : Option < String > ,
} ,
2026-07-18 15:23:20 +02:00
/// Drain messages from a local daemon-lifetime listener
2026-05-20 13:57:14 +02:00
Recv {
name : String ,
#[ arg(long) ]
peek : bool ,
} ,
2026-05-15 15:08:20 +02:00
}
#[ derive(Debug, Subcommand) ]
pub enum DbCommand {
2026-07-18 15:23:20 +02:00
/// Register a local SQLite database without mutating its schema
Add { name : String , path : PathBuf } ,
/// Show schema compatibility and cr-sqlite metadata
Status { name : String } ,
/// Read a typed batch from crsql_changes
2026-05-17 20:32:36 +02:00
Changes {
name : String ,
#[ arg(long) ]
after_db_version : Option < i64 > ,
#[ arg(long, default_value_t = 100) ]
limit : u32 ,
} ,
2026-07-18 15:23:20 +02:00
/// Pull and apply an authorized compatible change batch
2026-05-18 22:11:57 +02:00
Sync {
node : String ,
name : String ,
#[ arg(long, default_value_t = 100) ]
limit : u32 ,
2026-05-19 19:16:30 +02:00
#[ arg(long) ]
bearer_secret : Option < String > ,
2026-05-18 22:11:57 +02:00
} ,
2026-05-15 15:08:20 +02:00
}
#[ derive(Debug, Subcommand) ]
pub enum DocumentCommand {
2026-07-18 15:23:20 +02:00
/// Create a named local document resource
Create { name : String } ,
/// Show local document metadata
Status { name : String } ,
/// Replace local document state from validated JSON
Set { name : String , state_json : String } ,
/// Read local document state
Get { name : String } ,
/// Pull authorized document changes from a peer
2026-05-19 19:16:30 +02:00
Sync {
node : String ,
name : String ,
#[ arg(long) ]
bearer_secret : Option < String > ,
} ,
2026-05-15 15:08:20 +02:00
}
#[ derive(Debug, Subcommand) ]
pub enum SshCommand {
2026-07-18 15:23:20 +02:00
/// Act as an OpenSSH ProxyCommand over an authorized Iroh stream
2026-05-16 00:17:08 +02:00
Proxy {
node : String ,
2026-05-19 19:16:30 +02:00
#[ arg(long) ]
bearer_secret : Option < String > ,
2026-05-16 00:17:08 +02:00
} ,
2026-07-18 15:23:20 +02:00
/// Run a restricted geth admin command on a peer
2026-05-21 01:49:48 +02:00
AdminShell {
node : String ,
command : String ,
#[ arg(long) ]
bearer_secret : Option < String > ,
} ,
2026-07-18 15:23:20 +02:00
/// Request, approve, import, list, or sync SSH certificates
2026-05-16 00:17:08 +02:00
Cert {
#[ command(subcommand) ]
command : SshCertCommand ,
} ,
2026-07-18 15:23:20 +02:00
/// Add, export, import, list, or sync SSH revocations
2026-05-16 00:17:08 +02:00
Revocation {
#[ command(subcommand) ]
command : SshRevocationCommand ,
} ,
}
#[ derive(Debug, Subcommand) ]
pub enum SshCertCommand {
2026-07-18 15:23:20 +02:00
/// Create a signed SSH certificate request
2026-05-16 00:17:08 +02:00
Request {
#[ arg(long) ]
public_key : PathBuf ,
#[ arg(long, default_value = " user " ) ]
kind : String ,
#[ arg(long = " principal " , required = true) ]
principals : Vec < String > ,
#[ arg(long) ]
valid_for : Option < String > ,
#[ arg(long) ]
renewal_of : Option < String > ,
#[ arg(long) ]
reason : Option < String > ,
2026-05-19 15:44:13 +02:00
#[ arg(long) ]
subject : Option < String > ,
} ,
2026-07-18 15:23:20 +02:00
/// List local certificate requests
2026-05-19 15:44:13 +02:00
Requests {
#[ arg(long) ]
subject : Option < String > ,
2026-05-16 00:17:08 +02:00
} ,
2026-07-18 15:23:20 +02:00
/// Build or execute the OpenSSH certificate signing command
2026-05-16 00:17:08 +02:00
Approve {
request_id : String ,
#[ arg(long) ]
ca_key : PathBuf ,
#[ arg(long) ]
valid_for : Option < String > ,
#[ arg(long) ]
serial : Option < u64 > ,
#[ arg(long) ]
out : Option < PathBuf > ,
2026-05-19 15:44:13 +02:00
#[ arg(long) ]
2026-05-19 15:56:47 +02:00
sign : bool ,
#[ arg(long) ]
2026-05-19 15:44:13 +02:00
subject : Option < String > ,
2026-05-16 00:17:08 +02:00
} ,
2026-07-18 15:23:20 +02:00
/// Attach a signed certificate to its request
2026-05-16 00:17:08 +02:00
Import {
request_id : String ,
#[ arg(long) ]
cert : PathBuf ,
2026-05-19 15:44:13 +02:00
#[ arg(long) ]
subject : Option < String > ,
} ,
2026-07-18 15:23:20 +02:00
/// List stored SSH certificates
2026-05-19 15:44:13 +02:00
List {
#[ arg(long) ]
subject : Option < String > ,
2026-05-16 00:17:08 +02:00
} ,
2026-07-18 15:23:20 +02:00
/// Pull authorized certificate metadata from a peer
2026-05-18 17:24:10 +02:00
Sync {
node : String ,
2026-05-19 19:16:30 +02:00
#[ arg(long) ]
bearer_secret : Option < String > ,
2026-05-18 17:24:10 +02:00
} ,
2026-05-16 00:17:08 +02:00
}
#[ derive(Debug, Subcommand) ]
pub enum SshRevocationCommand {
2026-07-18 15:23:20 +02:00
/// Record a key, certificate, serial, or key-id revocation
2026-05-16 00:17:08 +02:00
Add {
kind : String ,
target : String ,
#[ arg(long) ]
reason : Option < String > ,
2026-05-19 15:44:13 +02:00
#[ arg(long) ]
subject : Option < String > ,
} ,
2026-07-18 15:23:20 +02:00
/// List stored SSH revocations
2026-05-19 15:44:13 +02:00
List {
#[ arg(long) ]
subject : Option < String > ,
2026-05-16 00:17:08 +02:00
} ,
2026-07-18 15:23:20 +02:00
/// Export JSONL, OpenSSH KRL specification, or binary KRL data
2026-05-16 00:17:08 +02:00
Export {
#[ arg(long) ]
out : PathBuf ,
2026-05-17 18:29:47 +02:00
#[ arg(long, default_value = " jsonl " ) ]
format : String ,
2026-05-18 11:51:12 +02:00
#[ arg(long) ]
ca_public : Option < PathBuf > ,
2026-05-19 15:44:13 +02:00
#[ arg(long) ]
subject : Option < String > ,
2026-05-16 00:17:08 +02:00
} ,
2026-07-18 15:23:20 +02:00
/// Import JSONL or an enumerable OpenSSH KRL specification
2026-05-18 11:56:42 +02:00
Import {
path : PathBuf ,
#[ arg(long, default_value = " jsonl " ) ]
format : String ,
2026-05-19 15:44:13 +02:00
#[ arg(long) ]
subject : Option < String > ,
2026-05-18 11:56:42 +02:00
} ,
2026-07-18 15:23:20 +02:00
/// Pull authorized revocation metadata from a peer
2026-05-18 17:24:10 +02:00
Sync {
node : String ,
2026-05-19 19:16:30 +02:00
#[ arg(long) ]
bearer_secret : Option < String > ,
2026-05-18 17:24:10 +02:00
} ,
2026-05-15 15:08:20 +02:00
}
#[ derive(Debug, Args) ]
pub struct EmptyArgs { }
2026-07-18 17:15:36 +02:00
#[ derive(Clone, Copy, Debug, PartialEq, Eq) ]
enum OutputMode {
Human ,
Json ,
Jsonl ,
}
impl OutputMode {
fn from_flags ( json : bool , jsonl : bool ) -> Self {
if jsonl {
Self ::Jsonl
} else if json {
Self ::Json
} else {
Self ::Human
}
}
fn is_machine ( self ) -> bool {
self ! = Self ::Human
}
fn encode ( self , value : & serde_json ::Value ) -> Result < String > {
match self {
Self ::Json = > serde_json ::to_string_pretty ( value ) . map_err ( Into ::into ) ,
Self ::Jsonl = > serde_json ::to_string ( value ) . map_err ( Into ::into ) ,
Self ::Human = > bail! ( " internal error: attempted JSON encoding in human output mode " ) ,
}
}
fn print ( self , value : & serde_json ::Value ) -> Result < ( ) > {
println! ( " {} " , self . encode ( value ) ? ) ;
Ok ( ( ) )
}
}
2026-05-15 15:08:20 +02:00
pub async fn run ( ) -> Result < ( ) > {
2026-07-18 16:08:19 +02:00
let matches = documented_cli_command ( ) . get_matches ( ) ;
let cli = Cli ::from_arg_matches ( & matches ) . context ( " parse geth command " ) ? ;
2026-07-18 17:15:36 +02:00
let output = OutputMode ::from_flags ( cli . json , cli . jsonl ) ;
2026-07-05 18:30:44 +02:00
match run_inner ( cli ) . await {
Ok ( ( ) ) = > Ok ( ( ) ) ,
2026-07-18 17:15:36 +02:00
Err ( error ) if output . is_machine ( ) = > {
print_json_error ( & error , output ) ? ;
2026-07-05 18:30:44 +02:00
std ::process ::exit ( 1 ) ;
}
Err ( error ) = > Err ( error ) ,
}
}
2026-07-18 16:08:19 +02:00
fn documented_cli_command ( ) -> clap ::Command {
document_missing_arguments ( Cli ::command ( ) , " geth " )
}
fn document_missing_arguments ( mut command : clap ::Command , path : & str ) -> clap ::Command {
let command_path = path . to_owned ( ) ;
command = command . mut_args ( | argument | {
if argument . get_help ( ) . is_some ( ) {
argument
} else if let Some ( help ) = argument_help ( & command_path , argument . get_id ( ) . as_str ( ) ) {
argument . help ( help )
} else {
argument
}
} ) ;
command . mut_subcommands ( | subcommand | {
let child_path = format! ( " {path} {} " , subcommand . get_name ( ) ) ;
document_missing_arguments ( subcommand , & child_path )
} )
}
fn argument_help ( path : & str , id : & str ) -> Option < & 'static str > {
let contextual = match ( path , id ) {
( " geth resource create " , " kind " ) = > {
Some ( " Resource kind, such as cas, kv, document, db, pipe, or pubsub " )
}
( " geth overlay interface-plan " , " platform " ) = > {
Some ( " Target platform: linux, macos, or windows; defaults to this host " )
}
( " geth ssh cert request " , " kind " ) = > Some ( " Certificate kind: user or host " ) ,
( " geth ssh revocation add " , " kind " ) = > {
Some ( " Revocation kind: public-key, certificate, serial, or key-id " )
}
( " geth cas conflict record " , " kind " ) = > {
Some ( " Conflict kind, such as concurrent-edit, delete-edit, or divergent-rename " )
}
( " geth pipe connect " | " geth pipe send " , " target " ) = > Some ( " Registered pipe listener name " ) ,
( " geth pipe forward-tcp " , " target " ) = > {
Some ( " Remote loopback TCP target, for example 127.0.0.1:5432 " )
}
( " geth pipe forward-unix " , " target " ) = > Some ( " Absolute Unix socket path on the peer " ) ,
( " geth ssh revocation add " , " target " ) = > {
Some ( " Key, certificate, serial, or key-id selected by KIND " )
}
( " geth ssh admin-shell " , " command " ) = > Some ( " Restricted command: help, status, or node-id " ) ,
( " geth node enroll list " , " status " ) = > {
Some ( " Optional request status filter: pending, approved, or rejected " )
}
( " geth ssh revocation export " | " geth ssh revocation import " , " format " ) = > {
Some ( " Format: jsonl, openssh, or krl where supported " )
}
( " geth cas root apply " , " source " ) = > {
Some ( " Local or peer-qualified file-root name to materialize " )
}
( " geth cas conflict resolve " , " resolution " ) = > {
Some ( " Resolution: keep-local, accept-remote, keep-both, or manual " )
}
2026-07-18 16:15:13 +02:00
( " geth config set " , " key " ) = > Some ( " Supported dotted configuration key " ) ,
( " geth config set " , " value " ) = > Some ( " New value for the selected setting " ) ,
2026-07-18 16:08:19 +02:00
( " geth node enroll request " , " capabilities " ) = > {
Some ( " Requested RESOURCE=CAPABILITY pair; repeat to request more than one " )
}
2026-07-18 16:48:35 +02:00
( " geth node enroll join " , " peer_card_path " ) = > {
Some ( " Signed owner peer-card JSON file to import as an untrusted candidate " )
}
( " geth node enroll join " , " admin_key " ) = > {
Some ( " Owner OpenSSH admin public key used as the explicit trust anchor " )
}
( " geth node enroll join " , " capabilities " ) = > {
Some ( " Requested RESOURCE=CAPABILITY pair; repeat to request more than one " )
}
( " geth node enroll join " | " geth node enroll request " , " out " ) = > {
Some ( " Optional path for a portable enrollment-request JSON copy " )
}
2026-07-18 16:08:19 +02:00
( " geth kv create " | " geth kv set " | " geth kv get " | " geth kv sync " , " name " ) = > {
Some ( " Name of the KV store " )
}
( " geth kv set " | " geth kv get " , " key " ) = > Some ( " Key within the named KV store " ) ,
( " geth db add " | " geth db status " | " geth db changes " | " geth db sync " , " name " ) = > {
Some ( " Name of the database resource " )
}
(
" geth document create "
| " geth document set "
| " geth document get "
| " geth document sync " ,
" name " ,
) = > Some ( " Name of the document resource " ) ,
( " geth cas root add " | " geth cas root scan " | " geth cas root sync " , " name " ) = > {
Some ( " Name of the file root " )
}
_ = > None ,
} ;
contextual . or ( match id {
" topic " = > Some ( " Embedded guide topic; omit it to list available topics " ) ,
" shell " = > Some ( " Shell whose completion script should be generated " ) ,
" node " | " owner_node " = > Some ( " Trusted node id or friendly node name " ) ,
" timeout_ms " = > Some ( " Maximum time to wait, in milliseconds " ) ,
" interval_ms " = > Some ( " Delay between readiness checks, in milliseconds " ) ,
" out " = > Some ( " Output file or directory; command output is used when omitted if supported " ) ,
" backup_dir " = > Some ( " Backup directory containing manifest.json and the home payload " ) ,
" target_home " = > Some ( " New empty directory that will receive the restored home " ) ,
" node_name " = > Some ( " Human-friendly name for the node " ) ,
" capabilities " | " capability " = > {
Some ( " Resource capability name; repeat the flag where supported " )
}
" reason " = > Some ( " Optional operator-readable reason recorded with the operation " ) ,
" request_id " = > Some ( " Enrollment or certificate request identifier " ) ,
" path " = > Some ( " Local input file or directory path " ) ,
" signing_key " = > {
Some ( " Private OpenSSH key, public agent key, or FIDO/YubiKey key stub used to sign " )
}
" admin_key " = > Some ( " Matching OpenSSH admin public key; inferred where possible " ) ,
" name " = > Some ( " Name of the command-specific local object " ) ,
" resource " = > Some ( " Canonical resource id, for example resource:kv:preferences " ) ,
" grant_id " = > Some ( " Stable grant identifier; generated when omitted where supported " ) ,
" endpoint " = > Some ( " Iroh EndpointID to bind to or revoke from the node " ) ,
" kind " = > Some ( " Command-specific object kind " ) ,
" cidr " = > Some ( " Overlay IPv4 CIDR, for example 172.22.0.0/24 " ) ,
" secret " = > Some ( " Private resource or bearer token; it is never stored in command logs " ) ,
" platform " = > Some ( " Target operating-system platform " ) ,
" bearer_secret " = > Some ( " Optional resource-scoped bearer token for remote authorization " ) ,
" mtu " = > Some ( " Overlay interface MTU in bytes " ) ,
" packet_base64 " = > Some ( " Complete IPv4 packet encoded as base64 " ) ,
" peek " = > Some ( " Read current data without draining it " ) ,
" principal " | " principals " = > {
Some ( " OpenSSH signer or certificate principal; repeat where supported " )
}
" valid_after_ms " = > Some ( " Earliest validity time as Unix milliseconds " ) ,
" valid_before_ms " = > Some ( " Latest validity time as Unix milliseconds " ) ,
" input " = > Some ( " Input file path " ) ,
" namespace " = > Some ( " SSH signature namespace; defaults to the relevant geth namespace " ) ,
" signature " = > Some ( " OpenSSH signature file path " ) ,
" allowed_signers " = > Some ( " OpenSSH allowed_signers file used for verification " ) ,
" base_url " = > Some ( " Publication base URL recorded in signed discovery metadata " ) ,
2026-07-18 21:00:06 +02:00
" chain_id " = > Some ( " Pinned 32-byte SSHSIGCHAIN chain ID as 64 hexadecimal characters " ) ,
" root_key " = > Some ( " Pinned OpenSSH root public-key file for SSHSIGCHAIN verification " ) ,
2026-07-18 16:08:19 +02:00
" snapshots " = > Some ( " Named snapshot mapping NAME=PATH; repeatable " ) ,
" checkpoint " = > Some ( " Signed publication checkpoint file " ) ,
" sigchain " = > Some ( " Canonical keychain signature-chain JSONL file " ) ,
" url " = > Some ( " HTTPS URL to fetch " ) ,
" import " = > Some ( " Import the verified result into local state " ) ,
" op_id " = > Some ( " Canonical keychain operation identifier " ) ,
" subject " = > Some ( " Principal evaluated or authorized instead of the local owner " ) ,
" expires_at_ms " = > Some ( " Optional bearer expiration as Unix milliseconds " ) ,
" token " = > Some ( " Private bearer token returned when access was created " ) ,
" nonce " = > Some ( " Challenge nonce returned by the challenge command " ) ,
" response " = > Some ( " Bearer possession proof response " ) ,
" bearer_id " = > Some ( " Public bearer-access identifier, not the private token " ) ,
" hash " = > Some ( " BLAKE3 CAS blob hash " ) ,
" dry_run " = > Some ( " Preview changes without mutating local state or files " ) ,
" source " = > Some ( " Command-specific source object " ) ,
" to " = > Some ( " Destination directory " ) ,
" root " = > Some ( " File-root name; omit the filter to include all roots " ) ,
" detail " = > Some ( " Operator-readable conflict detail " ) ,
" base_tree " = > Some ( " Optional common-ancestor CAS tree hash " ) ,
" local_tree " = > Some ( " Optional local CAS tree hash " ) ,
" remote_tree " = > Some ( " Optional remote CAS tree hash " ) ,
" conflict_id " = > Some ( " Durable file-conflict identifier " ) ,
" resolution " = > Some ( " Operator-selected conflict resolution " ) ,
" note " = > Some ( " Optional operator note recorded with the action " ) ,
" value " = > Some ( " UTF-8 value to store " ) ,
" key " = > Some ( " Key name, signer id, or fingerprint selected by the command " ) ,
" message " = > Some ( " UTF-8 message payload " ) ,
" target " = > Some ( " Command-specific destination or target " ) ,
" listen " = > Some ( " Local loopback address or absolute Unix socket path to listen on " ) ,
" after_db_version " = > Some ( " Return changes strictly after this cr-sqlite db_version " ) ,
" limit " = > Some ( " Maximum number of change rows to read or synchronize " ) ,
" state_json " = > Some ( " Complete JSON object or value used as the new document state " ) ,
" command " = > Some ( " Restricted command name " ) ,
" public_key " = > Some ( " OpenSSH public key to certify " ) ,
" valid_for " = > Some ( " OpenSSH validity interval, for example 8h or 7d " ) ,
" renewal_of " = > Some ( " Certificate id this request renews " ) ,
" serial " = > Some ( " Explicit OpenSSH certificate serial number " ) ,
" sign " = > Some ( " Run ssh-keygen now and import the generated certificate " ) ,
" cert " = > Some ( " Signed OpenSSH certificate file " ) ,
" format " = > Some ( " Input or output format selected by the command " ) ,
" ca_public " = > Some ( " OpenSSH CA public key used for KRL generation " ) ,
" ca_key " = > Some ( " OpenSSH CA private key or hardware-key stub used to sign " ) ,
_ = > None ,
} )
}
2026-07-05 18:30:44 +02:00
async fn run_inner ( cli : Cli ) -> Result < ( ) > {
2026-07-18 17:15:36 +02:00
let output = OutputMode ::from_flags ( cli . json , cli . jsonl ) ;
2026-05-23 02:40:03 +02:00
if let Command ::Completions { shell } = & cli . command {
2026-07-18 17:15:36 +02:00
if output . is_machine ( ) {
bail! ( " completion scripts cannot be combined with --json or --jsonl " ) ;
}
2026-05-23 02:40:03 +02:00
print_completions ( * shell ) ;
return Ok ( ( ) ) ;
}
2026-07-18 17:21:12 +02:00
if let Command ::Resource {
command : ResourceCommand ::Capabilities { family } ,
} = & cli . command
{
print_resource_capabilities ( family . as_deref ( ) , output ) ? ;
return Ok ( ( ) ) ;
}
2026-07-18 15:23:20 +02:00
if cli . home . is_some ( )
& & matches! (
& cli . command ,
Command ::Daemon {
command : DaemonCommand ::Run { ephemeral : true }
}
)
{
bail! (
" --home cannot be combined with --ephemeral; omit --ephemeral for persistent state or omit --home for an automatically managed temporary home "
) ;
}
let paths = cli
. home
. clone ( )
. map ( GethPaths ::from_home )
. map_or_else ( GethPaths ::resolve , Ok )
. context ( " resolve geth paths " ) ? ;
2026-05-15 15:08:20 +02:00
match cli . command {
2026-05-23 01:17:30 +02:00
Command ::Guide { topic } = > {
2026-07-18 17:15:36 +02:00
print_guide ( topic , output ) ? ;
2026-05-23 01:17:30 +02:00
}
2026-07-18 16:15:13 +02:00
Command ::Config { command } = > {
2026-07-18 17:15:36 +02:00
run_config_command ( & paths , command , output ) ? ;
2026-07-18 16:15:13 +02:00
}
2026-05-21 11:29:29 +02:00
Command ::Init {
admin_key ,
signing_key ,
owner ,
node_name ,
capabilities ,
} = > {
let node = geth_node ::init_owned_node (
& paths ,
geth_node ::InitOwnerOptions {
admin_key_path : admin_key ,
signing_key_path : signing_key ,
owner_name : owner ,
node_name ,
capabilities ,
} ,
)
. context ( " initialize geth node " ) ? ;
2026-07-18 17:15:36 +02:00
if output . is_machine ( ) {
output . print ( & serde_json ::json! ( {
" type " : " initialized " ,
" home " : node . paths . home ( ) ,
" agent_id " : node . agent_id ,
" node_id " : node . node_id ,
} ) ) ? ;
} else {
println! ( " initialized geth home: {} " , node . paths . home ( ) . display ( ) ) ;
println! ( " agent: {} " , node . agent_id ) ;
println! ( " node: {} " , node . node_id ) ;
}
2026-05-15 15:08:20 +02:00
}
Command ::Daemon {
2026-07-18 15:23:20 +02:00
command : DaemonCommand ::Run { ephemeral : true } ,
} = > {
2026-07-18 17:15:36 +02:00
run_ephemeral_daemon ( output ) . await ? ;
2026-07-18 15:23:20 +02:00
}
Command ::Daemon {
command : DaemonCommand ::Run { ephemeral : false } ,
2026-05-15 15:08:20 +02:00
} = > {
2026-07-18 17:15:36 +02:00
if output . is_machine ( ) {
output . print ( & serde_json ::json! ( {
" type " : " daemon-starting " ,
" home " : paths . home ( ) ,
" control_endpoint " : paths . control_endpoint ( ) ,
} ) ) ? ;
stdout ( ) . flush ( ) . context ( " flush daemon startup details " ) ? ;
}
2026-05-15 15:08:20 +02:00
geth_node ::run_daemon ( paths )
. await
. context ( " run geth daemon " ) ? ;
}
2026-07-18 15:23:20 +02:00
Command ::Daemon {
2026-07-18 16:31:59 +02:00
command :
DaemonCommand ::Install {
manager ,
bin ,
allow_transient_binary ,
no_wait ,
timeout_ms ,
} ,
2026-07-18 15:23:20 +02:00
} = > {
let report = run_service_command (
& paths ,
ServiceCommand ::Install {
manager ,
bin ,
start : true ,
2026-07-18 16:31:59 +02:00
allow_transient_binary ,
2026-07-18 15:23:20 +02:00
} ,
)
. context ( " install and start geth user service " ) ? ;
2026-07-18 16:31:59 +02:00
let readiness = if no_wait {
None
} else {
Some (
run_wait_command (
& paths ,
WaitCommand ::Daemon {
timeout_ms ,
interval_ms : 250 ,
} ,
)
. await ? ,
)
} ;
2026-07-18 17:15:36 +02:00
print_daemon_install_report ( & report , readiness . as_ref ( ) , output ) ? ;
2026-07-18 16:31:59 +02:00
if readiness . as_ref ( ) . is_some_and ( | readiness | ! readiness . ready ) {
std ::process ::exit ( 1 ) ;
}
2026-07-18 15:23:20 +02:00
}
Command ::Daemon {
command : DaemonCommand ::Start { manager } ,
} = > {
let report = run_service_command ( & paths , ServiceCommand ::Start { manager } )
. context ( " start geth user service; install it first with `geth daemon install` " ) ? ;
2026-07-18 17:15:36 +02:00
print_service_report ( & report , output ) ? ;
2026-07-18 15:23:20 +02:00
}
Command ::Daemon {
command : DaemonCommand ::Stop { manager } ,
} = > {
let report = run_service_command ( & paths , ServiceCommand ::Stop { manager } )
. context ( " stop geth user service " ) ? ;
2026-07-18 17:15:36 +02:00
print_service_report ( & report , output ) ? ;
2026-07-18 15:23:20 +02:00
}
Command ::Daemon {
command : DaemonCommand ::Status { manager } ,
} = > {
let report = run_service_command ( & paths , ServiceCommand ::Status { manager } )
. context ( " query geth user service; install it with `geth daemon install` " ) ? ;
2026-07-18 17:15:36 +02:00
print_service_report ( & report , output ) ? ;
2026-07-18 16:31:59 +02:00
}
Command ::Daemon {
command :
DaemonCommand ::Logs {
manager ,
lines ,
follow ,
} ,
} = > {
2026-07-18 17:15:36 +02:00
if follow & & output . is_machine ( ) {
2026-07-18 16:31:59 +02:00
bail! ( " --follow cannot be combined with --json or --jsonl " ) ;
}
let report = geth_node ::service ::logs_user_service (
& paths ,
manager . parse ::< ServiceManager > ( ) ? ,
lines ,
follow ,
)
. context ( " read geth user-service logs " ) ? ;
2026-07-18 17:15:36 +02:00
print_service_logs_report ( & report , output ) ? ;
2026-07-18 16:31:59 +02:00
if matches! (
report . state ,
ServiceLogState ::NotInstalled | ServiceLogState ::Unavailable
) {
std ::process ::exit ( 1 ) ;
}
2026-07-18 15:23:20 +02:00
}
Command ::Daemon {
command : DaemonCommand ::Uninstall { manager } ,
} = > {
let report = run_service_command ( & paths , ServiceCommand ::Uninstall { manager } )
. context ( " uninstall geth user service " ) ? ;
2026-07-18 17:15:36 +02:00
print_service_report ( & report , output ) ? ;
2026-07-18 15:23:20 +02:00
}
2026-05-16 00:17:08 +02:00
Command ::Daemon {
command : DaemonCommand ::Service { command } ,
} = > {
let report =
run_service_command ( & paths , command ) . context ( " manage geth user service " ) ? ;
2026-07-18 17:15:36 +02:00
print_service_report ( & report , output ) ? ;
2026-05-16 00:17:08 +02:00
}
2026-07-05 22:24:08 +02:00
Command ::Wait { command } = > {
let report = run_wait_command ( & paths , command ) . await ? ;
2026-07-18 17:15:36 +02:00
print_wait_report ( & report , output ) ? ;
2026-07-05 22:24:08 +02:00
if ! report . ready {
std ::process ::exit ( 1 ) ;
}
}
2026-07-05 22:35:18 +02:00
Command ::Backup { command } = > {
2026-07-18 17:15:36 +02:00
run_backup_command ( & paths , command , output ) . context ( " run backup command " ) ? ;
2026-07-05 22:35:18 +02:00
}
2026-07-05 22:39:15 +02:00
Command ::Doctor = > {
let report = geth_node ::doctor ::run_doctor ( & paths )
. await
. context ( " run doctor " ) ? ;
2026-07-18 17:15:36 +02:00
print_doctor_report ( & report , output ) ? ;
2026-07-05 22:39:15 +02:00
if ! report . ok {
std ::process ::exit ( 1 ) ;
}
}
2026-05-21 01:03:38 +02:00
Command ::Ssh {
command :
SshCommand ::Proxy {
node ,
bearer_secret ,
} ,
2026-07-18 17:15:36 +02:00
} if output = = OutputMode ::Human = > {
2026-05-21 01:03:38 +02:00
geth_node ::stream_ssh_proxy ( & paths , node , bearer_secret )
. await
. context ( " stream SSH proxy through geth daemon " ) ? ;
}
2026-05-21 01:12:01 +02:00
Command ::Pipe {
command :
PipeCommand ::ForwardTcp {
listen ,
node ,
target ,
bearer_secret ,
} ,
2026-07-18 17:15:36 +02:00
} if output = = OutputMode ::Human = > {
2026-05-21 01:12:01 +02:00
println! ( " forwarding tcp {listen} -> {node} : {target} " ) ;
geth_node ::run_tcp_forward ( & paths , listen , node , target , bearer_secret )
. await
. context ( " run TCP forward through geth daemon " ) ? ;
}
2026-05-21 01:15:51 +02:00
Command ::Pipe {
command :
PipeCommand ::ForwardUnix {
listen ,
node ,
target ,
bearer_secret ,
} ,
2026-07-18 17:15:36 +02:00
} if output = = OutputMode ::Human = > {
2026-05-21 01:15:51 +02:00
println! (
" forwarding unix {} -> {node}:{} " ,
listen . display ( ) ,
target . display ( )
) ;
geth_node ::run_unix_forward ( & paths , listen , node , target , bearer_secret )
. await
. context ( " run Unix socket forward through geth daemon " ) ? ;
}
2026-05-15 15:08:20 +02:00
command = > {
let request = request_for_command ( command ) ? ;
let response = geth_node ::send_control ( & paths , request )
. await
. with_context ( | | {
2026-07-18 15:23:20 +02:00
format! (
" connect to daemon at {} \n next: start it with `geth daemon install` (background) or `geth daemon run` (foreground) " ,
paths . socket_path ( ) . display ( )
)
2026-05-15 15:08:20 +02:00
} ) ? ;
2026-07-18 17:15:36 +02:00
print_response ( response , output ) ? ;
2026-05-15 15:08:20 +02:00
}
}
Ok ( ( ) )
}
2026-07-18 17:15:36 +02:00
fn run_config_command ( paths : & GethPaths , command : ConfigCommand , output : OutputMode ) -> Result < ( ) > {
2026-07-18 16:15:13 +02:00
let path = paths . config_file ( ) ;
match command {
ConfigCommand ::Path = > {
2026-07-18 17:15:36 +02:00
if output . is_machine ( ) {
output . print ( & serde_json ::json! ( {
" type " : " config-path " ,
" path " : path ,
" exists " : path . exists ( ) ,
} ) ) ? ;
2026-07-18 16:15:13 +02:00
} else {
println! ( " {} " , path . display ( ) ) ;
}
}
ConfigCommand ::Show = > {
let exists = path . exists ( ) ;
let text = if exists {
std ::fs ::read_to_string ( & path )
. with_context ( | | format! ( " read config at {} " , path . display ( ) ) ) ?
} else {
GethConfig ::default_toml ( ) . to_owned ( )
} ;
let config = GethConfig ::parse ( & text )
. with_context ( | | format! ( " validate config at {} " , path . display ( ) ) ) ? ;
2026-07-18 17:15:36 +02:00
if output . is_machine ( ) {
output . print ( & serde_json ::json! ( {
" type " : " config " ,
" path " : path ,
" source " : if exists { " file " } else { " built-in-defaults " } ,
" raw " : text ,
" effective " : config_json ( & config ) ,
} ) ) ? ;
2026-07-18 16:15:13 +02:00
} else {
println! ( " # path: {} " , path . display ( ) ) ;
if ! exists {
println! ( " # source: built-in defaults (file does not exist yet) " ) ;
}
print! ( " {text} " ) ;
if ! text . ends_with ( '\n' ) {
println! ( ) ;
}
}
}
ConfigCommand ::Validate = > {
let exists = path . exists ( ) ;
let config = GethConfig ::load ( & path )
. with_context ( | | format! ( " validate config at {} " , path . display ( ) ) ) ? ;
2026-07-18 17:15:36 +02:00
if output . is_machine ( ) {
output . print ( & serde_json ::json! ( {
" type " : " config-validation " ,
" path " : path ,
" valid " : true ,
" source " : if exists { " file " } else { " built-in-defaults " } ,
" effective " : config_json ( & config ) ,
} ) ) ? ;
2026-07-18 16:15:13 +02:00
} else if exists {
println! ( " valid config: {} " , path . display ( ) ) ;
} else {
println! (
" valid built-in defaults; config file does not exist yet: {} " ,
path . display ( )
) ;
}
}
ConfigCommand ::Set { key , value } = > {
let key = ConfigKey ::from ( key ) ;
let config = GethConfig ::set ( & path , key , & value )
. with_context ( | | format! ( " set {} in {} " , key . as_str ( ) , path . display ( ) ) ) ? ;
2026-07-18 17:15:36 +02:00
if output . is_machine ( ) {
output . print ( & serde_json ::json! ( {
" type " : " config-updated " ,
" path " : path ,
" key " : key . as_str ( ) ,
" value " : value ,
" restart_required " : true ,
" effective " : config_json ( & config ) ,
} ) ) ? ;
2026-07-18 16:15:13 +02:00
} else {
println! ( " updated {} in {} " , key . as_str ( ) , path . display ( ) ) ;
println! ( " restart the daemon for the change to take effect " ) ;
}
}
}
Ok ( ( ) )
}
fn config_json ( config : & GethConfig ) -> serde_json ::Value {
let relay_maps = config
. iroh
. relay_maps
. iter ( )
. map ( | ( name , map ) | ( name . clone ( ) , map . urls . clone ( ) ) )
. collect ::< std ::collections ::BTreeMap < _ , _ > > ( ) ;
serde_json ::json! ( {
" iroh " : {
" relay_mode " : config . iroh . relay_mode . label ( ) ,
" relay_maps " : relay_maps ,
" local_discovery " : config . iroh . local_discovery ,
} ,
" sync " : {
" live_sync_enabled " : config . sync . live_sync_enabled ,
" live_sync_interval_ms " : config . sync . live_sync_interval_ms ,
} ,
} )
}
2026-07-18 17:15:36 +02:00
async fn run_ephemeral_daemon ( output : OutputMode ) -> Result < ( ) > {
2026-07-18 15:23:20 +02:00
let ( home , paths , node ) = create_ephemeral_node ( ) ? ;
2026-07-18 17:15:36 +02:00
if output . is_machine ( ) {
output . print ( & serde_json ::json! ( {
" type " : " ephemeral-daemon-starting " ,
" home " : paths . home ( ) ,
" node_id " : node . node_id ,
" control_command " : format ! ( " geth --home {} status " , paths . home ( ) . display ( ) ) ,
" cleanup " : " state is removed after normal daemon shutdown " ,
} ) ) ? ;
2026-07-18 15:23:20 +02:00
} else {
println! ( " starting ephemeral geth daemon " ) ;
println! ( " home: {} " , paths . home ( ) . display ( ) ) ;
println! ( " node: {} " , node . node_id ) ;
println! ( " control: geth --home {} status " , paths . home ( ) . display ( ) ) ;
println! ( " cleanup: state is removed after normal shutdown (Ctrl-C) " ) ;
}
stdout ( )
. flush ( )
. context ( " flush ephemeral startup details " ) ? ;
geth_node ::run_daemon ( paths )
. await
. context ( " run ephemeral geth daemon " ) ? ;
drop ( home ) ;
Ok ( ( ) )
}
fn create_ephemeral_node ( ) -> Result < ( tempfile ::TempDir , GethPaths , geth_node ::LocalNode ) > {
let home = tempfile ::Builder ::new ( )
. prefix ( " geth-ephemeral- " )
. tempdir ( )
. context ( " create ephemeral geth home " ) ? ;
let paths = GethPaths ::from_home ( home . path ( ) ) ;
let node = geth_node ::init_node ( & paths ) . context ( " initialize ephemeral geth node " ) ? ;
Ok ( ( home , paths , node ) )
}
2026-07-18 17:15:36 +02:00
fn print_json_error ( error : & anyhow ::Error , output : OutputMode ) -> Result < ( ) > {
2026-07-05 18:30:44 +02:00
let message = error . to_string ( ) ;
let detail = format! ( " {error:#} " ) ;
2026-07-18 17:15:36 +02:00
let hint = json_error_hint ( & message ) ;
output . print ( & serde_json ::json! ( {
" type " : " error " ,
" code " : json_error_code ( & detail ) ,
" message " : message ,
" detail " : detail ,
" hint " : hint ,
} ) )
2026-07-05 18:30:44 +02:00
}
fn json_error_code ( detail : & str ) -> & 'static str {
let lower = detail . to_ascii_lowercase ( ) ;
if lower . contains ( " connect to daemon " ) | | lower . contains ( " connection refused " ) {
" daemon_unavailable "
2026-07-18 15:56:45 +02:00
} else if lower . contains ( " daemon is already running " ) {
" daemon_already_running "
2026-07-05 18:30:44 +02:00
} else if lower . contains ( " unauthorized " ) | | lower . contains ( " missing grant " ) {
" unauthorized "
} else if lower . contains ( " peer candidate not found " ) {
" peer_not_found "
} else if lower . contains ( " resource not found " ) {
" resource_not_found "
} else if lower . contains ( " invalid " ) {
" invalid_input "
} else {
" command_failed "
}
}
fn json_error_hint ( detail : & str ) -> Option < String > {
detail
. lines ( )
. find_map ( | line | line . strip_prefix ( " next: " ) )
. map ( ToOwned ::to_owned )
}
2026-07-18 17:15:36 +02:00
fn print_guide ( topic : Option < GuideTopic > , output : OutputMode ) -> Result < ( ) > {
2026-05-23 01:17:30 +02:00
let ( name , body ) = match topic {
None = > ( " index " , GUIDE_INDEX ) ,
2026-07-18 15:23:20 +02:00
Some ( GuideTopic ::Quickstart ) = > ( " quickstart " , GUIDE_QUICKSTART ) ,
2026-05-23 01:17:30 +02:00
Some ( GuideTopic ::Init ) = > ( " init " , GUIDE_INIT ) ,
Some ( GuideTopic ::OwnerSetup ) = > ( " owner-setup " , GUIDE_OWNER_SETUP ) ,
Some ( GuideTopic ::Enrollment ) = > ( " enrollment " , GUIDE_ENROLLMENT ) ,
Some ( GuideTopic ::Keys ) = > ( " keys " , GUIDE_KEYS ) ,
Some ( GuideTopic ::Overlay ) = > ( " overlay " , GUIDE_OVERLAY ) ,
Some ( GuideTopic ::Service ) = > ( " service " , GUIDE_SERVICE ) ,
2026-05-23 02:40:03 +02:00
Some ( GuideTopic ::Completions ) = > ( " completions " , GUIDE_COMPLETIONS ) ,
2026-05-23 01:17:30 +02:00
Some ( GuideTopic ::SmokeTest ) = > ( " smoke-test " , GUIDE_SMOKE_TEST ) ,
} ;
2026-07-18 17:15:36 +02:00
if output . is_machine ( ) {
output . print ( & serde_json ::json! ( {
" type " : " guide " ,
" topic " : name ,
" body " : body ,
} ) ) ? ;
2026-05-23 01:17:30 +02:00
} else {
print! ( " {body} " ) ;
}
Ok ( ( ) )
}
2026-07-18 17:21:12 +02:00
fn resource_capability_contracts (
family : Option < & str > ,
) -> Result < Vec < & 'static geth_node ::resource_contracts ::ResourceModuleContract > > {
let contracts = geth_node ::resource_contracts ::contracts ( ) ;
let Some ( family ) = family else {
return Ok ( contracts . iter ( ) . collect ( ) ) ;
} ;
let canonical_family = match family {
" cas " | " file-root " = > " cas-file-root " ,
" ssh-proxy " = > " ssh " ,
other = > other ,
} ;
let selected = contracts
. iter ( )
. filter ( | contract | contract . family = = canonical_family )
. collect ::< Vec < _ > > ( ) ;
if selected . is_empty ( ) {
let available = contracts
. iter ( )
. map ( | contract | contract . family )
. collect ::< Vec < _ > > ( )
. join ( " , " ) ;
bail! (
" invalid resource family {family:?}; available families: {available} \n next: run `geth resource capabilities` to inspect the complete catalog "
) ;
}
Ok ( selected )
}
fn print_resource_capabilities ( family : Option < & str > , output : OutputMode ) -> Result < ( ) > {
let contracts = resource_capability_contracts ( family ) ? ;
if output . is_machine ( ) {
let families = contracts
. iter ( )
. map ( | contract | {
serde_json ::json! ( {
" family " : contract . family ,
" resource_id_patterns " : contract . resource_ids ,
" capabilities " : contract . capabilities ,
" operations_and_host_effects " : contract . mutation_points ,
} )
} )
. collect ::< Vec < _ > > ( ) ;
output . print ( & serde_json ::json! ( {
" type " : " resource-capabilities " ,
" families " : families ,
" grant_example " : " geth auth grant <subject> <resource-id> <capability> --signing-key <path> " ,
" note " : " replace angle-bracket placeholders; discovery does not grant access " ,
} ) ) ? ;
return Ok ( ( ) ) ;
}
for ( index , contract ) in contracts . iter ( ) . enumerate ( ) {
if index > 0 {
println! ( ) ;
}
println! ( " {} " , contract . family ) ;
println! ( " resource IDs: " ) ;
for resource_id in contract . resource_ids {
println! ( " {resource_id} " ) ;
}
println! ( " capabilities: " ) ;
for capability in contract . capabilities {
println! ( " {capability} " ) ;
}
println! ( " operations and host effects: " ) ;
for point in contract . mutation_points {
println! ( " - {point} " ) ;
}
}
println! ( ) ;
println! ( " grant: geth auth grant <subject> <resource-id> <capability> --signing-key <path> " ) ;
println! ( " note: replace angle-bracket placeholders; discovery does not grant access " ) ;
Ok ( ( ) )
}
2026-05-23 02:40:03 +02:00
fn print_completions ( shell : Shell ) {
2026-07-18 16:08:19 +02:00
let mut command = documented_cli_command ( ) ;
2026-05-23 02:40:03 +02:00
generate ( shell , & mut command , " geth " , & mut stdout ( ) ) ;
}
2026-05-15 15:08:20 +02:00
fn request_for_command ( command : Command ) -> Result < ControlRequest > {
Ok ( match command {
2026-05-23 02:40:03 +02:00
Command ::Completions { .. } = > bail! ( " completion generation does not use the daemon " ) ,
2026-05-15 15:08:20 +02:00
Command ::Status = > ControlRequest ::Status ,
2026-05-21 19:31:05 +02:00
Command ::Sync {
command : SyncCommand ::Status ,
} = > ControlRequest ::SyncStatus ,
Command ::Sync {
command : SyncCommand ::Now { node } ,
} = > ControlRequest ::SyncNow { node } ,
2026-05-15 15:08:20 +02:00
Command ::Node {
command : NodeCommand ::Id ,
} = > ControlRequest ::NodeId ,
Command ::Node {
command : NodeCommand ::Status ,
} = > ControlRequest ::Status ,
2026-05-21 11:29:29 +02:00
Command ::Node {
command : NodeCommand ::List ,
} = > ControlRequest ::NodeList ,
2026-05-21 18:01:38 +02:00
Command ::Node {
command : NodeCommand ::Enroll { command } ,
} = > match command {
2026-07-18 16:48:35 +02:00
NodeEnrollCommand ::Join {
peer_card_path ,
admin_key ,
node_name ,
capabilities ,
reason ,
out ,
} = > ControlRequest ::NodeEnrollJoin {
peer_card_path ,
admin_key_path : admin_key ,
node_name ,
capabilities ,
reason ,
out ,
} ,
2026-05-21 18:01:38 +02:00
NodeEnrollCommand ::Request {
node_name ,
capabilities ,
reason ,
out ,
} = > ControlRequest ::NodeEnrollRequest {
node_name ,
capabilities ,
reason ,
out ,
} ,
NodeEnrollCommand ::Submit {
owner_node ,
request_id ,
path ,
} = > ControlRequest ::NodeEnrollSubmit {
owner_node ,
request_id ,
path ,
} ,
NodeEnrollCommand ::Import { path } = > ControlRequest ::NodeEnrollImport { path } ,
NodeEnrollCommand ::List { status } = > ControlRequest ::NodeEnrollList { status } ,
NodeEnrollCommand ::Approve {
request_id ,
signing_key ,
admin_key ,
node_name ,
capabilities ,
} = > ControlRequest ::NodeEnrollApprove {
request_id ,
signing_key_path : signing_key ,
admin_key_path : admin_key ,
node_name ,
capabilities ,
} ,
NodeEnrollCommand ::Sync { owner_node } = > ControlRequest ::NodeEnrollSync { owner_node } ,
} ,
2026-05-21 11:29:29 +02:00
Command ::Node {
command :
NodeCommand ::Rename {
node ,
name ,
signing_key ,
} ,
} = > ControlRequest ::NodeRename {
node ,
name ,
signing_key_path : signing_key ,
} ,
Command ::Node {
command : NodeCommand ::Revoke { node , signing_key } ,
} = > ControlRequest ::NodeRevoke {
node ,
signing_key_path : signing_key ,
} ,
Command ::Node {
command :
NodeCommand ::Grant {
node ,
resource ,
capability ,
grant_id ,
2026-05-21 18:15:10 +02:00
signing_key ,
admin_key ,
2026-05-21 11:29:29 +02:00
} ,
} = > ControlRequest ::NodeGrant {
node ,
resource ,
capability ,
grant_id ,
2026-05-21 18:15:10 +02:00
signing_key_path : Some ( signing_key ) ,
admin_key_path : admin_key ,
2026-05-21 11:29:29 +02:00
} ,
Command ::Node {
2026-05-21 18:15:10 +02:00
command :
NodeCommand ::RevokeGrant {
resource ,
grant_id ,
signing_key ,
admin_key ,
} ,
} = > ControlRequest ::NodeRevokeGrant {
resource ,
grant_id ,
signing_key_path : Some ( signing_key ) ,
admin_key_path : admin_key ,
} ,
Command ::Node {
command :
NodeCommand ::EndpointAdd {
node ,
endpoint ,
signing_key ,
} ,
} = > ControlRequest ::NodeEndpointAdd {
node ,
endpoint ,
signing_key_path : Some ( signing_key ) ,
} ,
Command ::Node {
command :
NodeCommand ::EndpointRevoke {
node ,
endpoint ,
signing_key ,
} ,
} = > ControlRequest ::NodeEndpointRevoke {
node ,
endpoint ,
signing_key_path : Some ( signing_key ) ,
} ,
2026-05-18 04:03:52 +02:00
Command ::Peer { command } = > match command {
PeerCommand ::Export { out } = > ControlRequest ::PeerCardExport { out } ,
PeerCommand ::Import { path } = > ControlRequest ::PeerCardImport { path } ,
PeerCommand ::List = > ControlRequest ::PeerCardList ,
2026-05-18 12:09:50 +02:00
PeerCommand ::Ping { node } = > ControlRequest ::PeerPing { node } ,
2026-05-18 17:01:50 +02:00
PeerCommand ::AuthCheck {
node ,
resource ,
capability ,
} = > ControlRequest ::PeerAuthCheck {
node ,
resource ,
capability ,
} ,
2026-05-18 04:03:52 +02:00
} ,
2026-05-23 01:17:30 +02:00
Command ::Overlay { command } = > match command {
OverlayCommand ::Status = > ControlRequest ::OverlayStatus ,
OverlayCommand ::Plan { name , cidr } = > ControlRequest ::OverlayPlan { name , cidr } ,
OverlayCommand ::Join { name , secret , cidr } = > {
ControlRequest ::OverlayJoin { name , secret , cidr }
}
OverlayCommand ::Leave { name } = > ControlRequest ::OverlayLeave { name } ,
2026-05-23 02:08:51 +02:00
OverlayCommand ::InterfacePlan { name , platform } = > {
ControlRequest ::OverlayInterfacePlan { name , platform }
}
OverlayCommand ::Up {
name ,
bearer_secret ,
mtu ,
} = > ControlRequest ::OverlayUp {
name ,
bearer_secret ,
mtu ,
} ,
OverlayCommand ::Down { name } = > ControlRequest ::OverlayDown { name } ,
OverlayCommand ::Peers { name } = > ControlRequest ::OverlayPeers { name } ,
OverlayCommand ::Send {
name ,
node ,
packet_base64 ,
bearer_secret ,
} = > ControlRequest ::OverlaySend {
name ,
node ,
packet_base64 ,
bearer_secret ,
} ,
OverlayCommand ::Recv { name , peek } = > ControlRequest ::OverlayRecv { name , peek } ,
2026-05-23 01:17:30 +02:00
} ,
2026-05-15 15:08:20 +02:00
Command ::Resource {
command : ResourceCommand ::List ,
} = > ControlRequest ::ResourceList ,
Command ::Resource {
command : ResourceCommand ::Create { kind , name } ,
} = > ControlRequest ::ResourceCreate { kind , name } ,
2026-07-18 17:21:12 +02:00
Command ::Resource {
command : ResourceCommand ::Capabilities { .. } ,
} = > bail! ( " resource capability discovery is handled directly by the CLI " ) ,
2026-05-15 15:08:20 +02:00
Command ::Keychain {
2026-05-19 16:04:20 +02:00
command :
KeychainCommand ::Init {
admin_key ,
signing_key ,
} ,
2026-05-16 16:34:20 +02:00
} = > ControlRequest ::KeychainInit {
admin_key_path : admin_key ,
2026-05-19 16:04:20 +02:00
signing_key_path : signing_key ,
2026-05-15 15:08:20 +02:00
} ,
Command ::Keychain {
command : KeychainCommand ::Status ,
} = > ControlRequest ::KeychainStatus ,
2026-05-26 18:53:20 +02:00
Command ::Keychain {
command :
KeychainCommand ::AdminAdd {
admin_key ,
signing_key ,
principal ,
valid_after_ms ,
valid_before_ms ,
} ,
} = > ControlRequest ::KeychainAdminAdd {
admin_key_path : admin_key ,
signing_key_path : signing_key ,
principal ,
valid_after_ms ,
valid_before_ms ,
} ,
Command ::Keychain {
command :
KeychainCommand ::AdminRevoke {
key ,
signing_key ,
admin_key ,
} ,
} = > ControlRequest ::KeychainAdminRevoke {
key ,
signing_key_path : signing_key ,
admin_key_path : admin_key ,
} ,
Command ::Keychain {
2026-05-27 00:59:52 +02:00
command : KeychainCommand ::AllowedSigners { out } ,
} = > ControlRequest ::KeychainAllowedSigners { out } ,
Command ::Keychain {
command :
KeychainCommand ::SignFile {
input ,
out ,
namespace ,
signing_key ,
admin_key ,
} ,
} = > ControlRequest ::KeychainSignFile {
input ,
out ,
namespace ,
signing_key_path : Some ( signing_key ) ,
admin_key_path : admin_key ,
} ,
Command ::Keychain {
command :
KeychainCommand ::VerifyFile {
input ,
signature ,
namespace ,
allowed_signers ,
principal ,
} ,
} = > ControlRequest ::KeychainVerifyFile {
input ,
signature ,
namespace ,
allowed_signers_path : allowed_signers ,
principal ,
} ,
Command ::Keychain {
command :
2026-07-18 21:00:06 +02:00
KeychainCommand ::VerifySigchain {
input ,
chain_id ,
root_key ,
namespace ,
2026-05-27 00:59:52 +02:00
} ,
2026-07-18 21:00:06 +02:00
} = > ControlRequest ::KeychainVerifySigchain {
input ,
chain_id ,
root_key_path : root_key ,
namespace : Some ( namespace ) ,
2026-05-27 00:59:52 +02:00
} ,
Command ::Keychain {
command : KeychainCommand ::Explain { op_id } ,
} = > ControlRequest ::KeychainExplain { op_id } ,
Command ::Keychain {
command : KeychainCommand ::ExplainSigner { key } ,
} = > ControlRequest ::KeychainExplainSigner { key } ,
2026-05-26 18:53:20 +02:00
Command ::Keychain {
command : KeychainCommand ::Verify ,
} = > ControlRequest ::KeychainVerify ,
2026-05-21 11:29:29 +02:00
Command ::Keychain {
command : KeychainCommand ::Sync { node } ,
} = > ControlRequest ::KeychainSync { node } ,
2026-05-21 18:01:38 +02:00
Command ::Auth {
command : AuthCommand ::Sync { node } ,
} = > ControlRequest ::AuthSync { node } ,
2026-05-15 15:08:20 +02:00
Command ::Auth {
command :
AuthCommand ::Explain {
subject ,
resource ,
capability ,
} ,
} = > ControlRequest ::AuthExplain {
subject ,
resource ,
capability ,
} ,
2026-05-16 16:32:03 +02:00
Command ::Auth {
command :
AuthCommand ::Grant {
subject ,
resource ,
capability ,
grant_id ,
2026-05-21 18:15:10 +02:00
signing_key ,
admin_key ,
2026-05-16 16:32:03 +02:00
} ,
} = > ControlRequest ::AuthGrant {
subject ,
resource ,
capability ,
grant_id ,
2026-05-21 18:15:10 +02:00
signing_key_path : Some ( signing_key ) ,
admin_key_path : admin_key ,
2026-05-16 16:32:03 +02:00
} ,
Command ::Auth {
2026-05-21 18:15:10 +02:00
command :
AuthCommand ::Revoke {
resource ,
grant_id ,
signing_key ,
admin_key ,
} ,
} = > ControlRequest ::AuthRevoke {
resource ,
grant_id ,
signing_key_path : Some ( signing_key ) ,
admin_key_path : admin_key ,
} ,
2026-05-16 22:18:49 +02:00
Command ::Secret { command } = > match command {
SecretCommand ::Status = > ControlRequest ::SecretStatus ,
SecretCommand ::Create { resource } = > ControlRequest ::SecretCreate { resource } ,
SecretCommand ::Rotate { resource } = > ControlRequest ::SecretRotate { resource } ,
2026-05-17 02:58:58 +02:00
SecretCommand ::Bearer { command } = > match command {
SecretBearerCommand ::Create {
resource ,
capabilities ,
expires_at_ms ,
} = > ControlRequest ::SecretBearerCreate {
resource ,
capabilities ,
expires_at_ms ,
} ,
SecretBearerCommand ::List = > ControlRequest ::SecretBearerList ,
2026-05-19 19:08:08 +02:00
SecretBearerCommand ::Challenge {
resource ,
capabilities ,
} = > ControlRequest ::SecretBearerChallenge {
resource ,
capabilities ,
} ,
SecretBearerCommand ::Prove {
2026-05-20 13:10:34 +02:00
token ,
2026-05-19 19:08:08 +02:00
resource ,
capabilities ,
nonce ,
} = > ControlRequest ::SecretBearerProve {
2026-05-20 13:10:34 +02:00
secret : token ,
2026-05-19 19:08:08 +02:00
resource ,
capabilities ,
nonce ,
} ,
SecretBearerCommand ::Verify {
2026-05-20 13:10:34 +02:00
token ,
2026-05-19 19:08:08 +02:00
resource ,
capabilities ,
nonce ,
response ,
} = > ControlRequest ::SecretBearerVerify {
2026-05-20 13:10:34 +02:00
secret : token ,
2026-05-19 19:08:08 +02:00
resource ,
capabilities ,
nonce ,
response ,
} ,
2026-05-20 13:10:34 +02:00
SecretBearerCommand ::Revoke {
resource ,
bearer_id ,
} = > ControlRequest ::SecretBearerRevoke {
resource ,
secret : bearer_id ,
} ,
2026-05-17 02:58:58 +02:00
} ,
2026-05-15 15:08:20 +02:00
} ,
Command ::Cas { command } = > match command {
CasCommand ::Add { path } = > ControlRequest ::CasAdd { path } ,
2026-05-21 01:35:00 +02:00
CasCommand ::AddPrivate { resource , path } = > {
ControlRequest ::CasAddPrivate { resource , path }
}
2026-05-15 15:08:20 +02:00
CasCommand ::Get { hash , out } = > ControlRequest ::CasGet {
hash : hash . into ( ) ,
out ,
} ,
2026-05-21 01:35:00 +02:00
CasCommand ::GetPrivate {
resource ,
hash ,
out ,
} = > ControlRequest ::CasGetPrivate {
resource ,
hash : hash . into ( ) ,
out ,
} ,
2026-05-19 19:16:30 +02:00
CasCommand ::Fetch {
node ,
hash ,
bearer_secret ,
} = > ControlRequest ::CasFetch {
2026-05-18 17:18:25 +02:00
node ,
hash : hash . into ( ) ,
2026-05-19 19:16:30 +02:00
bearer_secret ,
2026-05-18 17:18:25 +02:00
} ,
2026-05-15 15:08:20 +02:00
CasCommand ::Hash { path } = > ControlRequest ::CasHash { path } ,
CasCommand ::Has { hash } = > ControlRequest ::CasHas { hash : hash . into ( ) } ,
2026-05-16 16:36:35 +02:00
CasCommand ::Pin { hash } = > ControlRequest ::CasPin { hash : hash . into ( ) } ,
CasCommand ::Unpin { hash } = > ControlRequest ::CasUnpin { hash : hash . into ( ) } ,
2026-05-16 21:10:25 +02:00
CasCommand ::Cleanup { dry_run } = > ControlRequest ::CasCleanup { dry_run } ,
2026-05-19 15:37:02 +02:00
CasCommand ::Providers { hash } = > ControlRequest ::CasProviders { hash : hash . into ( ) } ,
2026-05-15 15:08:20 +02:00
CasCommand ::List = > ControlRequest ::CasList ,
2026-05-18 03:50:09 +02:00
CasCommand ::Root { command } = > match command {
CasRootCommand ::Add { name , path } = > ControlRequest ::CasRootAdd { name , path } ,
CasRootCommand ::List = > ControlRequest ::CasRootList ,
CasRootCommand ::Scan { name } = > ControlRequest ::CasRootScan { name } ,
2026-05-20 13:22:40 +02:00
CasRootCommand ::Sync {
node ,
name ,
bearer_secret ,
} = > ControlRequest ::CasRootSync {
node ,
name ,
bearer_secret ,
} ,
2026-05-20 13:30:57 +02:00
CasRootCommand ::Apply {
source ,
to ,
dry_run ,
} = > ControlRequest ::CasRootApply {
source ,
target : to ,
dry_run ,
} ,
2026-05-18 03:50:09 +02:00
} ,
2026-05-18 03:57:26 +02:00
CasCommand ::Conflict { command } = > match command {
CasConflictCommand ::Record {
root ,
path ,
kind ,
detail ,
base_tree ,
local_tree ,
remote_tree ,
} = > ControlRequest ::CasConflictRecord {
root ,
path ,
kind ,
detail ,
base_tree : base_tree . map ( Into ::into ) ,
local_tree : local_tree . map ( Into ::into ) ,
remote_tree : remote_tree . map ( Into ::into ) ,
} ,
CasConflictCommand ::List { root } = > ControlRequest ::CasConflictList { root } ,
CasConflictCommand ::Resolve {
conflict_id ,
resolution ,
note ,
} = > ControlRequest ::CasConflictResolve {
conflict_id ,
resolution ,
note ,
} ,
} ,
2026-05-15 15:08:20 +02:00
} ,
2026-05-16 21:52:35 +02:00
Command ::Kv { command } = > match command {
KvCommand ::Create { name } = > ControlRequest ::KvCreate { name } ,
2026-05-18 04:06:41 +02:00
KvCommand ::Set {
name ,
key ,
value ,
subject ,
} = > ControlRequest ::KvSet {
name ,
key ,
value ,
subject ,
} ,
2026-05-16 21:52:35 +02:00
KvCommand ::Get { name , key } = > ControlRequest ::KvGet { name , key } ,
2026-05-19 19:16:30 +02:00
KvCommand ::Sync {
node ,
name ,
bearer_secret ,
} = > ControlRequest ::KvSync {
node ,
name ,
bearer_secret ,
} ,
2026-05-15 15:08:20 +02:00
} ,
2026-05-17 18:23:51 +02:00
Command ::Pubsub { command } = > match command {
2026-05-18 18:41:04 +02:00
PubsubCommand ::Pub {
topic ,
message ,
node ,
2026-05-19 19:16:30 +02:00
bearer_secret ,
2026-05-18 18:41:04 +02:00
} = > ControlRequest ::PubsubPub {
topic ,
message ,
node ,
2026-05-19 19:16:30 +02:00
bearer_secret ,
} ,
PubsubCommand ::Sub {
topic ,
node ,
bearer_secret ,
} = > ControlRequest ::PubsubSub {
topic ,
node ,
bearer_secret ,
2026-05-18 18:41:04 +02:00
} ,
2026-05-15 15:08:20 +02:00
} ,
2026-05-17 20:17:26 +02:00
Command ::Pipe { command } = > match command {
2026-05-19 19:21:42 +02:00
PipeCommand ::Listen {
name ,
node ,
bearer_secret ,
} = > ControlRequest ::PipeListen {
name ,
node ,
bearer_secret ,
} ,
2026-05-19 19:16:30 +02:00
PipeCommand ::Connect {
target ,
node ,
bearer_secret ,
} = > ControlRequest ::PipeConnect {
target ,
node ,
bearer_secret ,
} ,
2026-05-21 01:12:01 +02:00
PipeCommand ::ForwardTcp {
listen ,
node ,
target ,
bearer_secret ,
} = > ControlRequest ::PipeTcpForward {
listen_addr : listen ,
node ,
target_addr : target ,
bearer_secret ,
} ,
2026-05-21 01:15:51 +02:00
PipeCommand ::ForwardUnix {
listen ,
node ,
target ,
bearer_secret ,
} = > ControlRequest ::PipeUnixForward {
listen_path : listen ,
node ,
target_path : target ,
bearer_secret ,
} ,
2026-05-20 13:57:14 +02:00
PipeCommand ::Send {
target ,
message ,
2026-05-20 13:59:41 +02:00
input ,
2026-05-20 13:57:14 +02:00
node ,
bearer_secret ,
} = > ControlRequest ::PipeSend {
target ,
2026-05-20 13:59:41 +02:00
data_base64 : pipe_send_payload_base64 ( message , input ) ? ,
2026-05-20 13:57:14 +02:00
node ,
bearer_secret ,
} ,
PipeCommand ::Recv { name , peek } = > ControlRequest ::PipeRecv { name , peek } ,
2026-05-15 15:08:20 +02:00
} ,
2026-05-16 21:13:33 +02:00
Command ::Db { command } = > match command {
DbCommand ::Add { name , path } = > ControlRequest ::DbAdd { name , path } ,
DbCommand ::Status { name } = > ControlRequest ::DbStatus { name } ,
2026-05-17 20:32:36 +02:00
DbCommand ::Changes {
name ,
after_db_version ,
limit ,
} = > ControlRequest ::DbChanges {
name ,
after_db_version ,
limit ,
} ,
2026-05-19 19:16:30 +02:00
DbCommand ::Sync {
node ,
name ,
limit ,
bearer_secret ,
} = > ControlRequest ::DbSync {
node ,
name ,
limit ,
bearer_secret ,
} ,
2026-05-15 15:08:20 +02:00
} ,
2026-05-16 22:15:18 +02:00
Command ::Document { command } = > match command {
DocumentCommand ::Create { name } = > ControlRequest ::DocumentCreate { name } ,
DocumentCommand ::Status { name } = > ControlRequest ::DocumentStatus { name } ,
2026-05-17 19:59:03 +02:00
DocumentCommand ::Set { name , state_json } = > {
ControlRequest ::DocumentSet { name , state_json }
}
DocumentCommand ::Get { name } = > ControlRequest ::DocumentGet { name } ,
2026-05-19 19:16:30 +02:00
DocumentCommand ::Sync {
node ,
name ,
bearer_secret ,
} = > ControlRequest ::DocumentSync {
node ,
name ,
bearer_secret ,
} ,
2026-05-15 15:08:20 +02:00
} ,
2026-05-16 00:17:08 +02:00
Command ::Ssh { command } = > match command {
2026-05-19 19:16:30 +02:00
SshCommand ::Proxy {
node ,
bearer_secret ,
} = > ControlRequest ::SshProxyConnect {
node ,
bearer_secret ,
} ,
2026-05-21 01:49:48 +02:00
SshCommand ::AdminShell {
node ,
command ,
bearer_secret ,
} = > ControlRequest ::SshAdminShell {
node ,
command ,
bearer_secret ,
} ,
2026-05-16 00:17:08 +02:00
SshCommand ::Cert { command } = > match command {
SshCertCommand ::Request {
public_key ,
kind ,
principals ,
valid_for ,
renewal_of ,
reason ,
2026-05-19 15:44:13 +02:00
subject ,
2026-05-16 00:17:08 +02:00
} = > ControlRequest ::SshCertRequest {
public_key_path : public_key ,
cert_kind : kind ,
principals ,
requested_validity : valid_for ,
renewal_of ,
reason ,
2026-05-19 15:44:13 +02:00
subject ,
2026-05-16 00:17:08 +02:00
} ,
2026-05-19 15:44:13 +02:00
SshCertCommand ::Requests { subject } = > ControlRequest ::SshCertRequests { subject } ,
2026-05-16 00:17:08 +02:00
SshCertCommand ::Approve {
request_id ,
ca_key ,
valid_for ,
serial ,
out ,
2026-05-19 15:56:47 +02:00
sign ,
2026-05-19 15:44:13 +02:00
subject ,
2026-05-16 00:17:08 +02:00
} = > ControlRequest ::SshCertApprove {
request_id ,
ca_key_path : ca_key ,
valid_for ,
serial ,
out ,
2026-05-19 15:56:47 +02:00
sign ,
2026-05-19 15:44:13 +02:00
subject ,
2026-05-16 00:17:08 +02:00
} ,
2026-05-19 15:44:13 +02:00
SshCertCommand ::Import {
request_id ,
cert ,
subject ,
} = > ControlRequest ::SshCertImport {
2026-05-16 00:17:08 +02:00
request_id ,
cert_path : cert ,
2026-05-19 15:44:13 +02:00
subject ,
2026-05-16 00:17:08 +02:00
} ,
2026-05-19 15:44:13 +02:00
SshCertCommand ::List { subject } = > ControlRequest ::SshCertList { subject } ,
2026-05-19 19:16:30 +02:00
SshCertCommand ::Sync {
node ,
bearer_secret ,
} = > ControlRequest ::SshCertSync {
node ,
bearer_secret ,
} ,
2026-05-16 00:17:08 +02:00
} ,
SshCommand ::Revocation { command } = > match command {
SshRevocationCommand ::Add {
kind ,
target ,
reason ,
2026-05-19 15:44:13 +02:00
subject ,
2026-05-16 00:17:08 +02:00
} = > ControlRequest ::SshRevocationAdd {
kind ,
target ,
reason ,
2026-05-19 15:44:13 +02:00
subject ,
2026-05-16 00:17:08 +02:00
} ,
2026-05-19 15:44:13 +02:00
SshRevocationCommand ::List { subject } = > {
ControlRequest ::SshRevocationList { subject }
}
2026-05-18 11:51:12 +02:00
SshRevocationCommand ::Export {
out ,
format ,
ca_public ,
2026-05-19 15:44:13 +02:00
subject ,
2026-05-18 11:51:12 +02:00
} = > ControlRequest ::SshRevocationExport {
out ,
format ,
ca_public ,
2026-05-19 15:44:13 +02:00
subject ,
} ,
SshRevocationCommand ::Import {
path ,
format ,
subject ,
} = > ControlRequest ::SshRevocationImport {
path ,
format ,
subject ,
2026-05-18 11:51:12 +02:00
} ,
2026-05-19 19:16:30 +02:00
SshRevocationCommand ::Sync {
node ,
bearer_secret ,
} = > ControlRequest ::SshRevocationSync {
node ,
bearer_secret ,
} ,
2026-05-16 00:17:08 +02:00
} ,
2026-05-15 15:08:20 +02:00
} ,
2026-07-05 22:24:08 +02:00
Command ::Guide { .. }
2026-07-18 16:15:13 +02:00
| Command ::Config { .. }
2026-07-05 22:24:08 +02:00
| Command ::Init { .. }
| Command ::Daemon { .. }
2026-07-05 22:35:18 +02:00
| Command ::Backup { .. }
2026-07-05 22:39:15 +02:00
| Command ::Doctor
2026-07-05 22:24:08 +02:00
| Command ::Wait { .. } = > {
2026-05-23 01:17:30 +02:00
bail! ( " command is handled directly " )
}
2026-05-15 15:08:20 +02:00
} )
}
2026-07-05 22:24:08 +02:00
#[ derive(Debug) ]
struct WaitReport {
target : String ,
ready : bool ,
elapsed_ms : u128 ,
attempts : u64 ,
reason : String ,
}
2026-07-18 17:15:36 +02:00
fn run_backup_command ( paths : & GethPaths , command : BackupCommand , output : OutputMode ) -> Result < ( ) > {
2026-07-05 22:35:18 +02:00
match command {
BackupCommand ::Create { out } = > {
let report = geth_node ::backup ::create_backup ( paths , & out ) ? ;
2026-07-18 17:15:36 +02:00
if output . is_machine ( ) {
output . print ( & serde_json ::json! ( {
" type " : " backup-created " ,
" backup_dir " : report . backup_dir ,
" files_copied " : report . files_copied ,
" bytes_copied " : report . bytes_copied ,
" manifest " : report . manifest ,
} ) ) ? ;
2026-07-05 22:35:18 +02:00
} else {
println! ( " backup: {} " , report . backup_dir . display ( ) ) ;
println! ( " files_copied: {} " , report . files_copied ) ;
println! ( " bytes_copied: {} " , report . bytes_copied ) ;
println! (
" manifest: {} " ,
report . backup_dir . join ( " manifest.json " ) . display ( )
) ;
println! (
" note: private geth identity keys and private SSH admin keys are not copied "
) ;
}
}
BackupCommand ::Restore {
backup_dir ,
target_home ,
} = > {
let report = geth_node ::backup ::restore_backup ( & backup_dir , & target_home ) ? ;
2026-07-18 17:15:36 +02:00
if output . is_machine ( ) {
output . print ( & serde_json ::json! ( {
" type " : " backup-restored " ,
" backup_dir " : report . backup_dir ,
" target_home " : report . target_home ,
" files_restored " : report . files_restored ,
" bytes_restored " : report . bytes_restored ,
" manifest " : report . manifest ,
} ) ) ? ;
2026-07-05 22:35:18 +02:00
} else {
println! ( " restored: {} " , report . target_home . display ( ) ) ;
println! ( " backup: {} " , report . backup_dir . display ( ) ) ;
println! ( " files_restored: {} " , report . files_restored ) ;
println! ( " bytes_restored: {} " , report . bytes_restored ) ;
println! (
" note: validate with GETH_HOME={} geth status after starting a daemon for the restored home " ,
report . target_home . display ( )
) ;
}
}
}
Ok ( ( ) )
}
2026-07-18 17:15:36 +02:00
fn print_doctor_report ( report : & geth_node ::doctor ::DoctorReport , output : OutputMode ) -> Result < ( ) > {
if output . is_machine ( ) {
output . print ( & serde_json ::json! ( {
" type " : " doctor " ,
" ok " : report . ok ,
" checks " : report . checks ,
} ) ) ? ;
2026-07-05 22:39:15 +02:00
return Ok ( ( ) ) ;
}
println! ( " doctor: {} " , if report . ok { " ok " } else { " failed " } ) ;
for check in & report . checks {
println! ( " {:?} \t {} \t {} " , check . status , check . code , check . message ) ;
if let Some ( hint ) = & check . hint {
println! ( " hint \t {} \t {} " , check . code , hint ) ;
}
}
Ok ( ( ) )
}
2026-07-05 22:24:08 +02:00
async fn run_wait_command ( paths : & GethPaths , command : WaitCommand ) -> Result < WaitReport > {
match command {
WaitCommand ::Daemon {
timeout_ms ,
interval_ms ,
} = > wait_for_daemon ( paths , timeout_ms , interval_ms ) . await ,
WaitCommand ::Peer {
node ,
timeout_ms ,
interval_ms ,
} = > wait_for_peer ( paths , node , timeout_ms , interval_ms ) . await ,
WaitCommand ::Sync {
node ,
timeout_ms ,
interval_ms ,
} = > wait_for_sync ( paths , node , timeout_ms , interval_ms ) . await ,
}
}
async fn wait_for_daemon (
paths : & GethPaths ,
timeout_ms : u64 ,
interval_ms : u64 ,
) -> Result < WaitReport > {
wait_loop ( " daemon " . to_owned ( ) , timeout_ms , interval_ms , | | async {
match geth_node ::send_control ( paths , ControlRequest ::Status ) . await {
Ok ( ControlResponse ::Status ( _ ) ) = > Ok ( Some ( " daemon control is ready " . to_owned ( ) ) ) ,
Ok ( other ) = > Ok ( Some ( format! ( " unexpected response: {:?} " , other ) ) ) ,
Err ( error ) = > Err ( anyhow ::anyhow! ( error . to_string ( ) ) ) ,
}
} )
. await
}
async fn wait_for_peer (
paths : & GethPaths ,
node : String ,
timeout_ms : u64 ,
interval_ms : u64 ,
) -> Result < WaitReport > {
wait_loop ( format! ( " peer: {node} " ) , timeout_ms , interval_ms , | | {
let node = node . clone ( ) ;
async move {
match geth_node ::send_control ( paths , ControlRequest ::PeerPing { node } ) . await {
Ok ( ControlResponse ::PeerPinged { note , .. } ) = > Ok ( Some ( note ) ) ,
Ok ( ControlResponse ::Error { message } ) = > Err ( anyhow ::anyhow! ( message ) ) ,
Ok ( _ ) = > Ok ( None ) ,
Err ( error ) = > Err ( anyhow ::anyhow! ( error . to_string ( ) ) ) ,
}
}
} )
. await
}
async fn wait_for_sync (
paths : & GethPaths ,
node : String ,
timeout_ms : u64 ,
interval_ms : u64 ,
) -> Result < WaitReport > {
wait_loop ( format! ( " sync: {node} " ) , timeout_ms , interval_ms , | | {
let node = node . clone ( ) ;
async move {
match geth_node ::send_control ( paths , ControlRequest ::SyncStatus ) . await {
Ok ( ControlResponse ::SyncStatus { peers , .. } ) = > {
let Some ( peer ) = peers . into_iter ( ) . find ( | peer | peer . peer_node_id = = node )
else {
return Ok ( None ) ;
} ;
if peer . streams . is_empty ( ) {
return Ok ( None ) ;
}
if peer . streams . iter ( ) . all ( sync_stream_ready ) {
Ok ( Some ( " sync streams are healthy or stale " . to_owned ( ) ) )
} else {
Ok ( None )
}
}
Ok ( ControlResponse ::Error { message } ) = > Err ( anyhow ::anyhow! ( message ) ) ,
Ok ( _ ) = > Ok ( None ) ,
Err ( error ) = > Err ( anyhow ::anyhow! ( error . to_string ( ) ) ) ,
}
}
} )
. await
}
fn sync_stream_ready ( stream : & SyncStreamStatus ) -> bool {
stream . state = = " ok " | | stream . stale
}
async fn wait_loop < F , Fut > (
target : String ,
timeout_ms : u64 ,
interval_ms : u64 ,
mut check : F ,
) -> Result < WaitReport >
where
F : FnMut ( ) -> Fut ,
Fut : std ::future ::Future < Output = Result < Option < String > > > ,
{
let started = Instant ::now ( ) ;
let timeout = Duration ::from_millis ( timeout_ms ) ;
let interval = Duration ::from_millis ( interval_ms . max ( 1 ) ) ;
let mut attempts = 0 ;
let mut last_error : Option < String > = None ;
loop {
attempts + = 1 ;
match check ( ) . await {
Ok ( Some ( reason ) ) = > {
return Ok ( WaitReport {
target ,
ready : true ,
elapsed_ms : started . elapsed ( ) . as_millis ( ) ,
attempts ,
reason ,
} ) ;
}
Ok ( None ) = > { }
Err ( error ) = > {
last_error = Some ( error . to_string ( ) ) ;
}
}
if started . elapsed ( ) > = timeout {
return Ok ( WaitReport {
target ,
ready : false ,
elapsed_ms : started . elapsed ( ) . as_millis ( ) ,
attempts ,
reason : last_error . unwrap_or_else ( | | " timeout waiting for readiness " . to_owned ( ) ) ,
} ) ;
}
tokio ::time ::sleep ( interval ) . await ;
}
}
2026-07-18 17:15:36 +02:00
fn print_wait_report ( report : & WaitReport , output : OutputMode ) -> Result < ( ) > {
if output . is_machine ( ) {
output . print ( & serde_json ::json! ( {
" type " : " wait " ,
" target " : report . target ,
" ready " : report . ready ,
" elapsed_ms " : report . elapsed_ms ,
" attempts " : report . attempts ,
" reason " : report . reason ,
} ) ) ? ;
2026-07-05 22:24:08 +02:00
return Ok ( ( ) ) ;
}
println! ( " target: {} " , report . target ) ;
println! ( " ready: {} " , report . ready ) ;
println! ( " elapsed_ms: {} " , report . elapsed_ms ) ;
println! ( " attempts: {} " , report . attempts ) ;
println! ( " reason: {} " , report . reason ) ;
Ok ( ( ) )
}
2026-05-16 00:17:08 +02:00
fn run_service_command ( paths : & GethPaths , command : ServiceCommand ) -> Result < ServiceReport > {
Ok ( match command {
ServiceCommand ::Install {
manager ,
bin ,
start ,
2026-07-18 16:31:59 +02:00
allow_transient_binary ,
2026-05-16 00:17:08 +02:00
} = > {
let manager = manager . parse ::< ServiceManager > ( ) ? ;
2026-07-18 16:31:59 +02:00
geth_node ::init_node ( paths ) . context ( " initialize geth home before service install " ) ? ;
let ( executable , copied_from ) = service_executable ( paths , bin , allow_transient_binary ) ? ;
let mut report = geth_node ::service ::install_user_service (
2026-05-16 00:17:08 +02:00
paths ,
ServiceInstallOptions {
manager ,
executable ,
start ,
} ,
2026-07-18 16:31:59 +02:00
) ? ;
if let Some ( source ) = copied_from {
report . note = format! (
" {}; copied transient executable from {} into the selected geth home " ,
report . note ,
source . display ( )
) ;
}
report
2026-05-16 00:17:08 +02:00
}
ServiceCommand ::Uninstall { manager } = > {
geth_node ::service ::uninstall_user_service ( manager . parse ::< ServiceManager > ( ) ? ) ?
}
ServiceCommand ::Start { manager } = > {
geth_node ::service ::start_user_service ( manager . parse ::< ServiceManager > ( ) ? ) ?
}
ServiceCommand ::Stop { manager } = > {
geth_node ::service ::stop_user_service ( manager . parse ::< ServiceManager > ( ) ? ) ?
}
ServiceCommand ::Status { manager } = > {
geth_node ::service ::status_user_service ( manager . parse ::< ServiceManager > ( ) ? ) ?
}
ServiceCommand ::Print { manager , bin } = > {
2026-07-18 16:31:59 +02:00
let executable = bin
. map ( Ok )
. unwrap_or_else ( std ::env ::current_exe )
. context ( " resolve geth executable for service preview " ) ? ;
2026-05-16 00:17:08 +02:00
geth_node ::service ::print_user_service (
paths ,
manager . parse ::< ServiceManager > ( ) ? ,
& executable ,
) ?
}
} )
}
2026-07-18 16:31:59 +02:00
fn parse_log_line_count ( value : & str ) -> std ::result ::Result < usize , String > {
value
. parse ::< usize > ( )
. ok ( )
. filter ( | lines | * lines > 0 )
. ok_or_else ( | | " log line count must be a positive integer " . to_owned ( ) )
}
fn service_executable (
paths : & GethPaths ,
bin : Option < PathBuf > ,
allow_transient : bool ,
) -> Result < ( PathBuf , Option < PathBuf > ) > {
let executable = bin
. map ( Ok )
2026-05-16 00:17:08 +02:00
. unwrap_or_else ( std ::env ::current_exe )
2026-07-18 16:31:59 +02:00
. context ( " resolve current geth executable " ) ? ;
let executable = executable
. canonicalize ( )
. with_context ( | | format! ( " resolve service executable {} " , executable . display ( ) ) ) ? ;
if ! executable . is_file ( ) {
bail! (
" service executable is not a regular file: {} " ,
executable . display ( )
) ;
}
if ! allow_transient & & transient_executable_reason ( & executable ) . is_some ( ) {
let bin_dir = paths . home ( ) . join ( " bin " ) ;
std ::fs ::create_dir_all ( & bin_dir ) . context ( " create durable service binary directory " ) ? ;
let file_name = if cfg! ( windows ) { " geth.exe " } else { " geth " } ;
let destination = bin_dir . join ( file_name ) ;
let mut source = std ::fs ::File ::open ( & executable )
. with_context ( | | format! ( " open service executable {} " , executable . display ( ) ) ) ? ;
let permissions = source
. metadata ( )
. context ( " read service executable metadata " ) ?
. permissions ( ) ;
let mut temporary = tempfile ::NamedTempFile ::new_in ( & bin_dir )
. context ( " create temporary service executable " ) ? ;
std ::io ::copy ( & mut source , temporary . as_file_mut ( ) )
. context ( " copy service executable into geth home " ) ? ;
temporary
. as_file_mut ( )
. set_permissions ( permissions )
. context ( " preserve service executable permissions " ) ? ;
temporary
. as_file_mut ( )
. sync_all ( )
. context ( " flush copied service executable " ) ? ;
temporary
. persist ( & destination )
. map_err ( | error | error . error )
. with_context ( | | {
format! (
" install durable service executable at {} " ,
destination . display ( )
)
} ) ? ;
return Ok ( ( destination , Some ( executable ) ) ) ;
}
Ok ( ( executable , None ) )
}
fn transient_executable_reason ( executable : & std ::path ::Path ) -> Option < & 'static str > {
if executable . starts_with ( std ::env ::temp_dir ( ) ) {
return Some ( " temporary files can be removed while the service still references them " ) ;
}
let components = executable
. components ( )
. filter_map ( | component | component . as_os_str ( ) . to_str ( ) )
. map ( | component | component . to_ascii_lowercase ( ) )
. collect ::< Vec < _ > > ( ) ;
components . windows ( 2 ) . find_map ( | pair | {
( pair [ 0 ] = = " target " & & matches! ( pair [ 1 ] . as_str ( ) , " debug " | " release " | " deps " ) )
. then_some ( " Cargo target artifacts are not a durable service installation path " )
} )
2026-05-16 00:17:08 +02:00
}
2026-05-27 00:59:52 +02:00
fn print_keychain_sigchain_report ( report : & geth_keychain ::KeychainSigchainReport ) {
println! ( " ops: {} " , report . ops ) ;
println! ( " signatures: {} " , report . signatures ) ;
println! ( " accepted_ops: {} " , report . accepted_ops ) ;
println! ( " rejected_ops: {} " , report . rejected_ops ) ;
println! ( " active_admin_keys: {} " , report . active_admin_keys ) ;
println! (
" accepted_head: {} " ,
report
. accepted_head
. as_ref ( )
. map ( | head | head . as_str ( ) )
. unwrap_or ( " none " )
) ;
println! ( " note: {} " , report . note ) ;
}
2026-07-18 17:15:36 +02:00
fn print_response ( response : ControlResponse , output : OutputMode ) -> Result < ( ) > {
if let ControlResponse ::Error { message } = & response {
bail! ( message . clone ( ) ) ;
}
if output . is_machine ( ) {
output . print ( & serde_json ::to_value ( & response ) ? ) ? ;
2026-05-15 15:08:20 +02:00
return Ok ( ( ) ) ;
}
match response {
ControlResponse ::Status ( status ) = > {
println! ( " geth daemon: running " ) ;
println! ( " home: {} " , status . home . display ( ) ) ;
println! ( " socket: {} " , status . socket . display ( ) ) ;
println! ( " agent: {} " , status . agent_id ) ;
println! ( " node: {} " , status . node_id ) ;
2026-07-05 18:14:52 +02:00
println! ( " uptime seconds: {} " , status . daemon_uptime_seconds ) ;
2026-07-05 18:10:41 +02:00
println! (
" store schema: {}/{} " ,
status . store_schema_version , status . store_current_schema_version
) ;
println! ( " store journal: {} " , status . store_journal_mode ) ;
println! ( " store synchronous: {} " , status . store_synchronous ) ;
println! ( " store status: {} " , status . store_status ) ;
println! ( " store note: {} " , status . store_note ) ;
2026-05-16 01:54:00 +02:00
println! (
" endpoint: {} " ,
status . endpoint_id . as_deref ( ) . unwrap_or ( " not started " )
) ;
2026-05-16 03:17:45 +02:00
println! ( " iroh relay: {} " , status . iroh_relay_mode ) ;
2026-05-16 14:33:45 +02:00
println! (
" iroh discovery: {} " ,
if status . iroh_local_discovery {
" local-network enabled "
} else {
" local-network disabled "
}
) ;
2026-05-15 15:08:20 +02:00
println! ( " iroh: {} " , status . iroh ) ;
2026-05-22 14:33:46 +02:00
for backend in status . native_backends {
println! (
" native backend {}: {} target {} {} ({}) " ,
backend . module ,
backend . current_backend ,
backend . target_crate ,
backend . target_version ,
backend . status
) ;
2026-05-22 15:28:52 +02:00
if ! backend . blocker . is_empty ( ) {
println! (
" native backend {} note: {} " ,
backend . module , backend . blocker
) ;
}
2026-05-22 14:33:46 +02:00
}
2026-05-15 15:08:20 +02:00
}
ControlResponse ::NodeId ( node ) = > {
println! ( " agent: {} " , node . agent_id ) ;
println! ( " node: {} " , node . node_id ) ;
println! (
" endpoint: {} " ,
node . endpoint_id
. as_deref ( )
. unwrap_or ( " not started in bootstrap " )
) ;
}
2026-05-18 04:03:52 +02:00
ControlResponse ::PeerCardExported { card , out , note } = > {
println! ( " peer card: {} " , card . node_id ) ;
println! ( " agent: {} " , card . agent_id ) ;
println! ( " endpoints: {} " , card . endpoints . len ( ) ) ;
if let Some ( path ) = out {
println! ( " wrote: {} " , path . display ( ) ) ;
} else {
println! ( " {} " , serde_json ::to_string_pretty ( & card ) ? ) ;
}
println! ( " note: {note} " ) ;
}
ControlResponse ::PeerCardImported { peer , note } = > {
println! ( " imported peer: {} " , peer . card . node_id ) ;
println! ( " agent: {} " , peer . card . agent_id ) ;
println! ( " trust: candidate-only " ) ;
println! ( " note: {note} " ) ;
}
ControlResponse ::PeerCardList { peers , note } = > {
if peers . is_empty ( ) {
println! ( " no peer candidates " ) ;
} else {
for peer in peers {
println! (
" {} \t {} \t {} endpoints \t candidate-only " ,
peer . card . node_id ,
peer . card . agent_id ,
peer . card . endpoints . len ( )
) ;
}
}
println! ( " note: {note} " ) ;
}
2026-05-18 12:09:50 +02:00
ControlResponse ::PeerPinged {
peer_node_id ,
peer_agent_id ,
endpoint_id ,
alpn ,
note ,
} = > {
println! ( " peer pong: {peer_node_id} " ) ;
println! ( " agent: {peer_agent_id} " ) ;
println! ( " endpoint: {endpoint_id} " ) ;
println! ( " alpn: {alpn} " ) ;
println! ( " note: {note} " ) ;
}
2026-05-18 17:01:50 +02:00
ControlResponse ::PeerAuthChecked {
peer_node_id ,
peer_agent_id ,
endpoint_id ,
resource ,
capability ,
allowed ,
reason ,
evaluated_ops ,
note ,
} = > {
println! ( " peer auth: {peer_node_id} " ) ;
println! ( " agent: {peer_agent_id} " ) ;
println! ( " endpoint: {endpoint_id} " ) ;
println! ( " resource: {resource} " ) ;
println! ( " capability: {capability} " ) ;
println! ( " allowed: {allowed} " ) ;
println! ( " reason: {reason} " ) ;
println! ( " evaluated_ops: {evaluated_ops} " ) ;
println! ( " note: {note} " ) ;
}
2026-05-15 15:08:20 +02:00
ControlResponse ::ResourceList { resources } = > {
if resources . is_empty ( ) {
println! ( " no resources " ) ;
} else {
for resource in resources {
println! ( " {} \t {} \t {} " , resource . kind , resource . name , resource . id ) ;
}
}
}
ControlResponse ::ResourceCreated { resource } = > {
println! (
" created resource: {} {} ({}) " ,
resource . kind , resource . name , resource . id
) ;
}
2026-05-23 01:17:30 +02:00
ControlResponse ::OverlayStatus { networks , note } = > {
if networks . is_empty ( ) {
println! ( " no overlay networks active " ) ;
} else {
for network in networks {
println! (
" {} \t {} \t {} \t {:?} \t {} peers " ,
network . name ,
network . resource ,
network . cidr ,
network . state ,
network . peers . len ( )
) ;
}
}
println! ( " note: {note} " ) ;
}
ControlResponse ::OverlayPlanned { plan } = > {
println! ( " overlay: {} " , plan . name ) ;
println! ( " resource: {} " , plan . resource ) ;
println! ( " cidr: {} " , plan . cidr ) ;
println! ( " alpn: {} " , plan . alpn ) ;
println! ( " capabilities: {} " , plan . capabilities . join ( " , " ) ) ;
println! ( " discovery: {} " , plan . discovery ) ;
println! ( " runtime: {} " , plan . runtime ) ;
for note in plan . security {
println! ( " security: {note} " ) ;
}
for note in plan . implementation_notes {
println! ( " implementation: {note} " ) ;
}
}
ControlResponse ::OverlayJoined { join } = > {
println! ( " overlay: {} " , join . plan . name ) ;
println! ( " resource: {} " , join . plan . resource ) ;
2026-05-23 02:08:51 +02:00
println! ( " cidr: {} " , join . network . cidr ) ;
println! (
" virtual_ip: {} " ,
join . network . virtual_ip . as_deref ( ) . unwrap_or ( " unassigned " )
) ;
println! ( " state: {:?} " , join . network . state ) ;
2026-05-23 01:17:30 +02:00
println! ( " enabled: {} " , join . enabled ) ;
println! ( " note: {} " , join . note ) ;
}
ControlResponse ::OverlayLeft {
name ,
stopped ,
note ,
} = > {
println! ( " overlay: {name} " ) ;
println! ( " stopped: {stopped} " ) ;
println! ( " note: {note} " ) ;
}
2026-05-23 02:08:51 +02:00
ControlResponse ::OverlayInterfacePlanned { plan } = > {
println! ( " overlay: {} " , plan . name ) ;
println! ( " platform: {} " , plan . platform ) ;
println! ( " interface: {} " , plan . interface_name ) ;
println! ( " cidr: {} " , plan . cidr ) ;
println! (
" virtual_ip: {} " ,
plan . virtual_ip . as_deref ( ) . unwrap_or ( " unassigned " )
) ;
println! ( " requires_privileges: {} " , plan . requires_privileges ) ;
for command in plan . commands {
println! ( " command: {command} " ) ;
}
for note in plan . notes {
println! ( " note: {note} " ) ;
}
}
ControlResponse ::OverlayRuntimeStarted { status } = > {
println! ( " overlay: {} " , status . name ) ;
println! ( " interface: {} " , status . interface_name ) ;
println! ( " virtual_ip: {} " , status . virtual_ip ) ;
println! ( " cidr: {} " , status . cidr ) ;
println! ( " mtu: {} " , status . mtu ) ;
println! ( " packets_from_tun: {} " , status . packets_from_tun ) ;
println! ( " packets_to_tun: {} " , status . packets_to_tun ) ;
println! ( " packets_to_peers: {} " , status . packets_to_peers ) ;
if let Some ( error ) = status . last_error {
println! ( " last_error: {error} " ) ;
}
println! ( " note: {} " , status . note ) ;
}
ControlResponse ::OverlayRuntimeStopped {
name ,
stopped ,
note ,
} = > {
println! ( " overlay: {name} " ) ;
println! ( " stopped: {stopped} " ) ;
println! ( " note: {note} " ) ;
}
ControlResponse ::OverlayPeers { name , peers , note } = > {
println! ( " overlay: {name} " ) ;
if peers . is_empty ( ) {
println! ( " no overlay peer candidates " ) ;
} else {
for peer in peers {
println! (
" {} \t {} \t {} \t {} " ,
peer . node_id ,
peer . endpoint_id . as_deref ( ) . unwrap_or ( " no-endpoint " ) ,
peer . virtual_ip . as_deref ( ) . unwrap_or ( " no-virtual-ip " ) ,
peer . state
) ;
}
}
println! ( " note: {note} " ) ;
}
ControlResponse ::OverlayPacketSent {
peer_node_id ,
peer_agent_id ,
endpoint_id ,
packet ,
allowed ,
reason ,
note ,
} = > {
println! ( " peer: {peer_node_id} " ) ;
println! ( " agent: {peer_agent_id} " ) ;
println! ( " endpoint: {endpoint_id} " ) ;
println! ( " allowed: {allowed} " ) ;
println! ( " reason: {reason} " ) ;
if let Some ( packet ) = packet {
println! ( " packet: {} " , packet . id ) ;
println! ( " size_bytes: {} " , packet . size_bytes ) ;
}
println! ( " note: {note} " ) ;
}
ControlResponse ::OverlayPackets {
name ,
packets ,
drained ,
note ,
} = > {
println! ( " overlay: {name} " ) ;
println! ( " drained: {drained} " ) ;
for packet in packets {
println! (
" {} \t {} \t {} \t {} bytes " ,
packet . id , packet . source_node , packet . destination_node , packet . size_bytes
) ;
}
println! ( " note: {note} " ) ;
}
2026-05-15 15:08:20 +02:00
ControlResponse ::CasAdded { hash , size_bytes } = > {
println! ( " {hash} {size_bytes} bytes " ) ;
}
2026-05-21 01:35:00 +02:00
ControlResponse ::CasPrivateAdded {
resource ,
epoch ,
plaintext_hash ,
encrypted_hash ,
size_bytes ,
note ,
} = > {
println! ( " encrypted_hash: {encrypted_hash} " ) ;
println! ( " plaintext_hash: {plaintext_hash} " ) ;
println! ( " resource: {resource} " ) ;
println! ( " epoch: {epoch} " ) ;
println! ( " size_bytes: {size_bytes} " ) ;
println! ( " note: {note} " ) ;
}
2026-05-15 15:08:20 +02:00
ControlResponse ::CasGot {
hash ,
out ,
size_bytes ,
} = > {
println! ( " wrote {hash} to {} ( {size_bytes} bytes) " , out . display ( ) ) ;
}
2026-05-21 01:35:00 +02:00
ControlResponse ::CasPrivateGot {
resource ,
hash ,
plaintext_hash ,
out ,
size_bytes ,
note ,
} = > {
println! (
" decrypted {hash} for {resource} to {} ({size_bytes} bytes) " ,
out . display ( )
) ;
println! ( " plaintext_hash: {plaintext_hash} " ) ;
println! ( " note: {note} " ) ;
}
2026-05-18 17:18:25 +02:00
ControlResponse ::CasFetched {
peer_node_id ,
peer_agent_id ,
endpoint_id ,
hash ,
size_bytes ,
allowed ,
reason ,
note ,
} = > {
if allowed {
println! ( " fetched {hash} from {peer_node_id} ( {size_bytes} bytes) " ) ;
} else {
println! ( " fetch denied for {hash} from {peer_node_id} " ) ;
}
println! ( " agent: {peer_agent_id} " ) ;
println! ( " endpoint: {endpoint_id} " ) ;
println! ( " allowed: {allowed} " ) ;
println! ( " reason: {reason} " ) ;
println! ( " note: {note} " ) ;
}
2026-05-15 15:08:20 +02:00
ControlResponse ::CasHash { hash } = > println! ( " {hash} " ) ,
ControlResponse ::CasHas { hash , present } = > println! ( " {hash} : {present} " ) ,
2026-05-16 16:36:35 +02:00
ControlResponse ::CasPinned { hash , pinned } = > {
println! ( " {hash} : pinned= {pinned} " ) ;
}
2026-05-16 21:10:25 +02:00
ControlResponse ::CasCleanup {
removed ,
retained_pinned ,
dry_run ,
} = > {
let action = if dry_run { " would remove " } else { " removed " } ;
println! ( " {action} : {} " , removed . len ( ) ) ;
for hash in removed {
println! ( " {hash} " ) ;
}
println! ( " retained_pinned: {} " , retained_pinned . len ( ) ) ;
for hash in retained_pinned {
println! ( " {hash} " ) ;
}
}
2026-05-15 15:08:20 +02:00
ControlResponse ::CasList { blobs } = > {
for blob in blobs {
2026-05-16 16:36:35 +02:00
let pin = if blob . pinned { " pinned " } else { " unpinned " } ;
println! ( " {} \t {} bytes \t {} " , blob . hash , blob . size_bytes , pin ) ;
2026-05-15 15:08:20 +02:00
}
}
2026-05-19 15:37:02 +02:00
ControlResponse ::CasProviders { hash , providers } = > {
println! ( " hash: {hash} " ) ;
println! ( " providers: {} " , providers . len ( ) ) ;
for provider in providers {
println! (
" {} \t {} \t {} " ,
provider . peer_node_id , provider . endpoint_id , provider . last_seen_ms
) ;
}
}
2026-05-18 03:50:09 +02:00
ControlResponse ::CasRootAdded { root } = > {
println! ( " added file root: {} " , root . name ) ;
println! ( " id: {} " , root . id ) ;
println! ( " resource: {} " , root . resource ) ;
println! ( " path: {} " , root . path ) ;
}
ControlResponse ::CasRootList { roots } = > {
if roots . is_empty ( ) {
println! ( " no file roots " ) ;
} else {
for root in roots {
println! (
" {} \t {} \t {} " ,
root . name ,
root . path ,
root . latest_tree
. map ( | hash | hash . to_string ( ) )
. unwrap_or_else ( | | " unscanned " . to_owned ( ) )
) ;
}
}
}
ControlResponse ::CasRootScanned { scan } = > {
println! ( " file root: {} " , scan . root . name ) ;
println! ( " tree: {} " , scan . tree . hash ) ;
println! ( " tree_bytes: {} " , scan . tree . size_bytes ) ;
println! ( " changes: {} " , scan . changes . len ( ) ) ;
for change in scan . changes {
println! ( " {change:?} " ) ;
}
println! ( " note: {} " , scan . note ) ;
}
2026-05-20 13:22:40 +02:00
ControlResponse ::CasRootSynced {
peer_node_id ,
peer_agent_id ,
endpoint_id ,
name ,
root ,
tree_bytes_imported ,
2026-05-21 01:40:50 +02:00
sync_conflicts ,
2026-05-20 13:22:40 +02:00
allowed ,
reason ,
note ,
} = > {
if let Some ( root ) = root {
println! ( " synced file root: {name} " ) ;
println! ( " peer: {peer_node_id} " ) ;
println! ( " path: {} " , root . path ) ;
println! (
" tree: {} " ,
root . latest_tree
. map ( | hash | hash . to_string ( ) )
. unwrap_or_else ( | | " unscanned " . to_owned ( ) )
) ;
println! ( " tree_bytes_imported: {tree_bytes_imported} " ) ;
2026-05-21 01:40:50 +02:00
println! ( " sync_conflicts: {} " , sync_conflicts . len ( ) ) ;
for conflict in sync_conflicts {
println! (
" {} \t {} \t {} \t {} " ,
conflict . id ,
conflict . path ,
conflict . kind . as_str ( ) ,
conflict . status . as_str ( )
) ;
}
2026-05-20 13:22:40 +02:00
} else {
println! ( " file root sync denied by {peer_node_id} " ) ;
}
println! ( " agent: {peer_agent_id} " ) ;
println! ( " endpoint: {endpoint_id} " ) ;
println! ( " allowed: {allowed} " ) ;
println! ( " reason: {reason} " ) ;
println! ( " note: {note} " ) ;
}
2026-05-20 13:30:57 +02:00
ControlResponse ::CasRootApplied {
source ,
target ,
files_written ,
dirs_created ,
conflicts ,
dry_run ,
note ,
} = > {
println! ( " applied file root: {source} " ) ;
println! ( " target: {} " , target . display ( ) ) ;
println! ( " dry_run: {dry_run} " ) ;
println! ( " files_written: {files_written} " ) ;
println! ( " dirs_created: {dirs_created} " ) ;
println! ( " conflicts: {} " , conflicts . len ( ) ) ;
for conflict in conflicts {
println! (
" {} \t {} \t {} \t {} " ,
conflict . id ,
conflict . path ,
conflict . kind . as_str ( ) ,
conflict . status . as_str ( )
) ;
}
println! ( " note: {note} " ) ;
}
2026-05-18 03:57:26 +02:00
ControlResponse ::CasConflictRecorded { conflict } = > {
println! ( " recorded conflict: {} " , conflict . id ) ;
print_file_conflict ( & conflict ) ;
}
ControlResponse ::CasConflictList { conflicts } = > {
if conflicts . is_empty ( ) {
println! ( " no file conflicts " ) ;
} else {
for conflict in conflicts {
println! (
" {} \t {} \t {} \t {} \t {} " ,
conflict . id ,
conflict . root ,
conflict . path ,
conflict . kind . as_str ( ) ,
conflict . status . as_str ( )
) ;
}
}
}
ControlResponse ::CasConflictResolved { conflict } = > {
println! ( " resolved conflict: {} " , conflict . id ) ;
print_file_conflict ( & conflict ) ;
}
2026-05-15 15:08:20 +02:00
ControlResponse ::KeychainStatus ( status ) = > {
println! ( " initialized: {} " , status . initialized ) ;
println! ( " admin_keys: {} " , status . admin_keys ) ;
2026-05-19 16:05:57 +02:00
println! ( " signatures: {} " , status . signatures ) ;
2026-05-19 18:58:07 +02:00
println! ( " verified_signatures: {} " , status . verified_signatures ) ;
println! ( " failed_signatures: {} " , status . failed_signatures ) ;
2026-05-15 15:08:20 +02:00
println! ( " users: {} " , status . users ) ;
println! ( " devices: {} " , status . devices ) ;
println! ( " nodes: {} " , status . nodes ) ;
}
2026-05-19 16:04:20 +02:00
ControlResponse ::KeychainInitialized { ops , signatures } = > {
2026-05-16 16:34:20 +02:00
println! ( " initialized keychain " ) ;
for op in ops {
println! ( " recorded keychain op: {} " , op . id ) ;
}
2026-05-19 16:04:20 +02:00
for signature in signatures {
println! (
" signed keychain op: {} by {} ({}) " ,
signature . op_id , signature . signer , signature . namespace
) ;
}
2026-05-16 16:34:20 +02:00
}
2026-05-26 18:53:20 +02:00
ControlResponse ::KeychainAdminUpdated {
op ,
signatures ,
note ,
} = > {
println! ( " recorded keychain op: {} " , op . id ) ;
for signature in signatures {
println! (
" signed keychain op: {} by {} ({}) " ,
signature . op_id , signature . signer , signature . namespace
) ;
}
println! ( " note: {note} " ) ;
}
ControlResponse ::KeychainAllowedSigners {
allowed_signers ,
2026-05-27 00:59:52 +02:00
out ,
2026-05-26 18:53:20 +02:00
note ,
..
} = > {
2026-05-27 00:59:52 +02:00
if let Some ( out ) = out {
println! ( " wrote allowed_signers: {} " , out . display ( ) ) ;
if allowed_signers . is_empty ( ) {
println! ( " warning: generated file has no active admin public keys " ) ;
}
} else {
print! ( " {allowed_signers} " ) ;
if allowed_signers . is_empty ( ) {
println! ( " no active admin public keys available " ) ;
}
2026-05-26 18:53:20 +02:00
}
eprintln! ( " note: {note} " ) ;
}
2026-05-27 00:59:52 +02:00
ControlResponse ::KeychainFileSigned {
input ,
out ,
namespace ,
signer ,
note ,
} = > {
println! ( " signed file: {} " , input . display ( ) ) ;
println! (
" signature: {} " ,
out . map ( | path | path . display ( ) . to_string ( ) )
. unwrap_or_else ( | | " none " . to_owned ( ) )
) ;
println! ( " namespace: {namespace} " ) ;
println! ( " signer: {signer} " ) ;
eprintln! ( " note: {note} " ) ;
}
ControlResponse ::KeychainFileVerified {
input ,
signature ,
namespace ,
verified ,
principal ,
note ,
} = > {
println! ( " file: {} " , input . display ( ) ) ;
println! ( " signature: {} " , signature . display ( ) ) ;
println! ( " namespace: {namespace} " ) ;
println! ( " principal: {} " , principal . as_deref ( ) . unwrap_or ( " none " ) ) ;
println! ( " verified: {verified} " ) ;
eprintln! ( " note: {note} " ) ;
}
2026-07-18 21:00:06 +02:00
ControlResponse ::KeychainSigchainVerified {
2026-05-27 00:59:52 +02:00
input ,
2026-07-18 21:00:06 +02:00
chain_id ,
records ,
head ,
2026-07-18 21:48:27 +02:00
active_keys ,
2026-07-18 21:00:06 +02:00
devices ,
2026-07-18 21:48:27 +02:00
disclosed_profiles ,
incomplete_profiles ,
2026-07-18 22:05:57 +02:00
anchor_attester_threshold ,
anchor_backend_threshold ,
required_anchor_backends ,
required_anchor_classes ,
2026-05-27 00:59:52 +02:00
note ,
} = > {
println! ( " sigchain: {} " , input . display ( ) ) ;
2026-07-18 21:00:06 +02:00
println! ( " chain_id: {chain_id} " ) ;
println! ( " records: {records} " ) ;
println! ( " head: {head} " ) ;
2026-07-18 21:48:27 +02:00
println! ( " active_keys: {active_keys} " ) ;
2026-07-18 21:00:06 +02:00
println! ( " devices: {devices} " ) ;
2026-07-18 21:48:27 +02:00
println! ( " disclosed_profiles: {disclosed_profiles} " ) ;
println! ( " incomplete_profiles: {incomplete_profiles} " ) ;
2026-07-18 22:05:57 +02:00
println! ( " anchor_attester_threshold: {anchor_attester_threshold} " ) ;
println! ( " anchor_backend_threshold: {anchor_backend_threshold} " ) ;
println! ( " required_anchor_backends: {required_anchor_backends} " ) ;
println! ( " required_anchor_classes: {required_anchor_classes} " ) ;
2026-05-27 00:59:52 +02:00
eprintln! ( " note: {note} " ) ;
}
ControlResponse ::KeychainExplained { subject , lines } = > {
println! ( " subject: {subject} " ) ;
for line in lines {
println! ( " {line} " ) ;
}
}
ControlResponse ::KeychainVerified { report } = > {
print_keychain_sigchain_report ( & report ) ;
2026-05-26 18:53:20 +02:00
}
2026-05-21 11:29:29 +02:00
ControlResponse ::KeychainSynced {
peer_node_id ,
peer_agent_id ,
endpoint_id ,
ops_imported ,
signatures_imported ,
invalid_ops_rejected ,
2026-05-21 19:31:05 +02:00
high_water_ms ,
2026-05-21 11:29:29 +02:00
note ,
} = > {
println! ( " synced keychain from: {peer_node_id} " ) ;
println! ( " agent: {peer_agent_id} " ) ;
println! ( " endpoint: {endpoint_id} " ) ;
println! ( " ops_imported: {ops_imported} " ) ;
println! ( " signatures_imported: {signatures_imported} " ) ;
println! ( " invalid_ops_rejected: {invalid_ops_rejected} " ) ;
2026-05-21 19:31:05 +02:00
println! ( " high_water_ms: {high_water_ms} " ) ;
2026-05-21 11:29:29 +02:00
println! ( " note: {note} " ) ;
}
2026-05-21 18:01:38 +02:00
ControlResponse ::AuthSynced {
peer_node_id ,
peer_agent_id ,
endpoint_id ,
ops_imported ,
signatures_imported ,
invalid_ops_rejected ,
2026-05-21 19:31:05 +02:00
high_water_ms ,
2026-05-21 18:01:38 +02:00
note ,
} = > {
println! ( " synced auth from: {peer_node_id} " ) ;
println! ( " agent: {peer_agent_id} " ) ;
println! ( " endpoint: {endpoint_id} " ) ;
println! ( " ops_imported: {ops_imported} " ) ;
println! ( " signatures_imported: {signatures_imported} " ) ;
println! ( " invalid_ops_rejected: {invalid_ops_rejected} " ) ;
2026-05-21 19:31:05 +02:00
println! ( " high_water_ms: {high_water_ms} " ) ;
println! ( " note: {note} " ) ;
}
ControlResponse ::SyncStatus { peers , note } = > {
if peers . is_empty ( ) {
println! ( " no sync peers " ) ;
} else {
for peer in peers {
println! ( " peer: {} " , peer . peer_node_id ) ;
if peer . streams . is_empty ( ) {
println! ( " no sync attempts recorded " ) ;
}
for stream in peer . streams {
println! (
2026-07-05 22:57:52 +02:00
" {} \t state={} \t stale={} \t cursor={} \t last_attempt={} \t last_success={} \t imported={} \t rejected={} \t failures={} \t retry_in_ms={} \t error={} \t next={} " ,
2026-05-21 19:31:05 +02:00
stream . stream ,
2026-05-22 15:00:09 +02:00
stream . state ,
stream . stale ,
2026-05-21 19:31:05 +02:00
stream . cursor_ms ,
stream
. last_attempt_ms
. map ( | value | value . to_string ( ) )
. unwrap_or_else ( | | " never " . to_owned ( ) ) ,
stream
. last_success_ms
. map ( | value | value . to_string ( ) )
. unwrap_or_else ( | | " never " . to_owned ( ) ) ,
stream . last_imported ,
stream . last_rejected ,
2026-07-05 22:57:52 +02:00
stream . consecutive_failures ,
stream
. retry_in_ms
. map ( | value | value . to_string ( ) )
. unwrap_or_else ( | | " - " . to_owned ( ) ) ,
2026-05-22 15:00:09 +02:00
stream . last_error . unwrap_or_else ( | | " - " . to_owned ( ) ) ,
stream . next_action
2026-05-21 19:31:05 +02:00
) ;
}
}
}
println! ( " note: {note} " ) ;
}
ControlResponse ::SyncRan { peers , note } = > {
if peers . is_empty ( ) {
println! ( " no sync peers " ) ;
} else {
for peer in peers {
println! ( " peer: {} " , peer . peer_node_id ) ;
for stream in peer . streams {
let state = if ! stream . attempted {
" skipped "
} else if stream . success {
" ok "
} else {
" failed "
} ;
println! (
" {} \t {} \t cursor={} \t imported={} \t rejected={} \t error={} " ,
stream . stream ,
state ,
stream . cursor_ms ,
stream . imported ,
stream . rejected ,
stream . error . unwrap_or_else ( | | " - " . to_owned ( ) )
) ;
}
}
}
2026-05-21 18:01:38 +02:00
println! ( " note: {note} " ) ;
}
2026-05-16 22:18:49 +02:00
ControlResponse ::SecretStatus { secrets } = > {
if secrets . is_empty ( ) {
println! ( " no resource secrets " ) ;
} else {
for secret in secrets {
println! ( " {} \t {} \t epoch {} " , secret . id , secret . resource , secret . epoch ) ;
}
}
}
ControlResponse ::SecretCreated { secret } = > {
println! ( " resource secret: {} " , secret . id ) ;
println! ( " resource: {} " , secret . resource ) ;
println! ( " epoch: {} " , secret . epoch ) ;
}
2026-05-17 02:58:58 +02:00
ControlResponse ::SecretBearerCreated { access } = > {
2026-05-20 13:10:34 +02:00
println! ( " bearer id: {} " , access . secret ) ;
if let Some ( token ) = access . token {
println! ( " bearer token: {token} " ) ;
}
2026-05-17 02:58:58 +02:00
println! ( " resource: {} " , access . resource ) ;
println! (
" capabilities: {} " ,
access
. capabilities
. iter ( )
. map ( ToString ::to_string )
. collect ::< Vec < _ > > ( )
. join ( " , " )
) ;
if let Some ( expires_at ) = access . expires_at {
println! ( " expires_at_ms: {} " , expires_at . 0 ) ;
}
println! ( " may_delegate: {} " , access . may_delegate ) ;
}
ControlResponse ::SecretBearerList { access } = > {
if access . is_empty ( ) {
println! ( " no bearer access " ) ;
} else {
for item in access {
println! (
" {} \t {} \t {} \t may_delegate={} " ,
item . secret ,
item . resource ,
item . capabilities
. iter ( )
. map ( ToString ::to_string )
. collect ::< Vec < _ > > ( )
. join ( " , " ) ,
item . may_delegate
) ;
}
}
}
2026-05-19 19:08:08 +02:00
ControlResponse ::SecretBearerChallenge { challenge } = > {
println! ( " bearer challenge " ) ;
println! ( " resource: {} " , challenge . resource ) ;
println! ( " nonce: {} " , challenge . nonce ) ;
println! ( " issued_at_ms: {} " , challenge . issued_at . 0 ) ;
println! (
" capabilities: {} " ,
challenge
. capabilities
. iter ( )
. map ( ToString ::to_string )
. collect ::< Vec < _ > > ( )
. join ( " , " )
) ;
}
ControlResponse ::SecretBearerProof { proof } = > {
println! ( " bearer proof " ) ;
println! ( " secret: {} " , proof . secret ) ;
println! ( " resource: {} " , proof . resource ) ;
println! ( " nonce: {} " , proof . nonce ) ;
println! ( " response: {} " , proof . response ) ;
println! (
" capabilities: {} " ,
proof
. capabilities
. iter ( )
. map ( ToString ::to_string )
. collect ::< Vec < _ > > ( )
. join ( " , " )
) ;
}
ControlResponse ::SecretBearerVerified {
secret ,
resource ,
capabilities ,
verified ,
reason ,
} = > {
println! ( " bearer verified: {verified} " ) ;
println! ( " secret: {secret} " ) ;
println! ( " resource: {resource} " ) ;
println! ( " capabilities: {} " , capabilities . join ( " , " ) ) ;
println! ( " reason: {reason} " ) ;
}
2026-05-17 02:58:58 +02:00
ControlResponse ::SecretBearerRevoked { resource , secret } = > {
println! ( " revoked bearer secret: {secret} " ) ;
println! ( " resource: {resource} " ) ;
}
2026-05-15 15:08:20 +02:00
ControlResponse ::AuthExplain ( explain ) = > {
println! ( " allowed: {} " , explain . allowed ) ;
println! ( " subject: {} " , explain . subject ) ;
println! ( " resource: {} " , explain . resource ) ;
println! ( " capability: {} " , explain . capability ) ;
println! ( " reason: {} " , explain . reason ) ;
println! ( " evaluated_ops: {} " , explain . evaluated_ops ) ;
2026-05-22 14:28:44 +02:00
if ! explain . diagnostics . is_empty ( ) {
println! ( " diagnostics: {} " , explain . diagnostics . join ( " , " ) ) ;
}
2026-05-15 15:08:20 +02:00
}
2026-05-21 18:15:10 +02:00
ControlResponse ::AuthOpRecorded { op , signatures } = > {
2026-05-16 16:32:03 +02:00
println! ( " recorded auth op: {} " , op . id ) ;
println! ( " resource: {} " , op . resource ) ;
2026-05-21 18:15:10 +02:00
for signature in signatures {
println! (
" signed auth op: {} by {} ({}) " ,
signature . op_id , signature . signer , signature . namespace
) ;
}
2026-05-16 16:32:03 +02:00
}
2026-05-21 11:29:29 +02:00
ControlResponse ::NodeList { nodes , note } = > {
if nodes . is_empty ( ) {
println! ( " no enrolled nodes " ) ;
} else {
for node in nodes {
println! (
" {} \t {} \t device {} \t {} endpoints " ,
node . name ,
node . id ,
node . device ,
node . endpoints . len ( )
) ;
}
}
println! ( " note: {note} " ) ;
}
ControlResponse ::NodeKeychainUpdated {
ops ,
signatures ,
note ,
} = > {
for op in ops {
println! ( " recorded keychain op: {} " , op . id ) ;
}
for signature in signatures {
println! (
" signed keychain op: {} by {} ({}) " ,
signature . op_id , signature . signer , signature . namespace
) ;
}
println! ( " note: {note} " ) ;
}
2026-05-21 18:01:38 +02:00
ControlResponse ::NodeGrantUpdated {
op ,
signatures ,
note ,
} = > {
2026-05-21 11:29:29 +02:00
println! ( " recorded auth op: {} " , op . id ) ;
println! ( " resource: {} " , op . resource ) ;
2026-05-21 18:01:38 +02:00
for signature in signatures {
println! (
" signed auth op: {} by {} ({}) " ,
signature . op_id , signature . signer , signature . namespace
) ;
}
println! ( " note: {note} " ) ;
}
ControlResponse ::NodeEnrollmentRequested { request , out , note } = > {
println! ( " node enrollment request: {} " , request . id ) ;
println! ( " node: {} " , request . requester_node ) ;
println! ( " requested_name: {} " , request . requested_node_name ) ;
println! ( " status: {} " , request . status ) ;
if let Some ( out ) = out {
println! ( " written: {} " , out . display ( ) ) ;
}
println! ( " note: {note} " ) ;
}
2026-07-18 16:48:35 +02:00
ControlResponse ::NodeEnrollmentJoined {
peer ,
request ,
out ,
owner_node_id ,
admin_key_id ,
accepted ,
note ,
} = > {
println! ( " imported owner candidate: {} " , peer . card . node_id ) ;
println! ( " trust: candidate-only " ) ;
println! ( " admin trust anchor: {admin_key_id} " ) ;
println! ( " node enrollment request: {} " , request . id ) ;
println! ( " requested_name: {} " , request . requested_node_name ) ;
if let Some ( out ) = out {
println! ( " written: {} " , out . display ( ) ) ;
}
println! ( " submitted_to: {owner_node_id} " ) ;
println! ( " accepted: {accepted} " ) ;
println! ( " next: the owner reviews and approves this request with an admin key " ) ;
println! ( " note: {note} " ) ;
}
2026-05-21 18:01:38 +02:00
ControlResponse ::NodeEnrollmentSubmitted {
request_id ,
owner_node_id ,
accepted ,
note ,
} = > {
println! ( " submitted enrollment request: {request_id} " ) ;
println! ( " owner_node: {owner_node_id} " ) ;
println! ( " accepted: {accepted} " ) ;
println! ( " note: {note} " ) ;
}
ControlResponse ::NodeEnrollmentImported { request , note } = > {
println! ( " imported enrollment request: {} " , request . id ) ;
println! ( " node: {} " , request . requester_node ) ;
println! ( " requested_name: {} " , request . requested_node_name ) ;
println! ( " status: {} " , request . status ) ;
println! ( " note: {note} " ) ;
}
ControlResponse ::NodeEnrollmentList { requests , note } = > {
if requests . is_empty ( ) {
println! ( " no node enrollment requests " ) ;
} else {
for request in requests {
println! (
" {} \t {} \t {} \t {} capabilities " ,
request . id ,
request . status ,
request . requested_node_name ,
request . requested_capabilities . len ( )
) ;
}
}
println! ( " note: {note} " ) ;
}
ControlResponse ::NodeEnrollmentApproved {
request ,
keychain_ops ,
keychain_signatures ,
auth_ops ,
auth_signatures ,
note ,
} = > {
println! ( " approved enrollment request: {} " , request . id ) ;
println! ( " node: {} " , request . requester_node ) ;
println! ( " keychain_ops: {} " , keychain_ops . len ( ) ) ;
println! ( " keychain_signatures: {} " , keychain_signatures . len ( ) ) ;
println! ( " auth_ops: {} " , auth_ops . len ( ) ) ;
println! ( " auth_signatures: {} " , auth_signatures . len ( ) ) ;
println! ( " note: {note} " ) ;
}
ControlResponse ::NodeEnrollmentSynced {
owner_node ,
keychain_ops_imported ,
keychain_signatures_imported ,
auth_ops_imported ,
auth_signatures_imported ,
invalid_ops_rejected ,
note ,
} = > {
println! ( " synced enrollment from: {owner_node} " ) ;
println! ( " keychain_ops_imported: {keychain_ops_imported} " ) ;
println! ( " keychain_signatures_imported: {keychain_signatures_imported} " ) ;
println! ( " auth_ops_imported: {auth_ops_imported} " ) ;
println! ( " auth_signatures_imported: {auth_signatures_imported} " ) ;
println! ( " invalid_ops_rejected: {invalid_ops_rejected} " ) ;
2026-05-21 11:29:29 +02:00
println! ( " note: {note} " ) ;
}
2026-05-16 00:17:08 +02:00
ControlResponse ::SshCertRequested { request } = > {
println! ( " ssh cert request: {} " , request . id ) ;
println! ( " status: {} " , request . status ) ;
println! ( " kind: {} " , request . cert_kind ) ;
println! ( " principals: {} " , request . principals . join ( " , " ) ) ;
println! ( " public_key_fingerprint: {} " , request . public_key_fingerprint ) ;
}
ControlResponse ::SshCertRequests { requests } = > {
if requests . is_empty ( ) {
println! ( " no ssh certificate requests " ) ;
} else {
for request in requests {
println! (
" {} \t {} \t {} \t {} \t {} " ,
request . id ,
request . status ,
request . cert_kind ,
request . principals . join ( " , " ) ,
request . public_key_fingerprint
) ;
}
}
}
ControlResponse ::SshCertApproved { approval } = > {
println! ( " approved ssh cert request: {} " , approval . request_id ) ;
println! ( " valid_for: {} " , approval . valid_for ) ;
if let Some ( serial ) = approval . serial {
println! ( " serial: {serial} " ) ;
}
if let Some ( output_path ) = approval . output_path {
println! ( " expected_certificate: {output_path} " ) ;
}
println! ( " signing_command: " ) ;
println! ( " {} " , shell_quote_command ( & approval . signing_command ) ) ;
2026-05-19 15:56:47 +02:00
println! ( " signed: {} " , approval . signed ) ;
if let Some ( certificate_id ) = approval . certificate_id {
println! ( " certificate_id: {certificate_id} " ) ;
}
2026-05-16 00:17:08 +02:00
println! ( " note: {} " , approval . note ) ;
}
ControlResponse ::SshCertImported { certificate } = > {
println! ( " imported ssh certificate: {} " , certificate . id ) ;
println! ( " request: {} " , certificate . request_id ) ;
println! ( " fingerprint: {} " , certificate . certificate_fingerprint ) ;
}
ControlResponse ::SshCertList {
requests ,
certificates ,
} = > {
println! ( " requests: " ) ;
if requests . is_empty ( ) {
println! ( " none " ) ;
} else {
for request in requests {
println! (
" {} \t {} \t {} \t {} " ,
request . id ,
request . status ,
request . cert_kind ,
request . principals . join ( " , " )
) ;
}
}
println! ( " certificates: " ) ;
if certificates . is_empty ( ) {
println! ( " none " ) ;
} else {
for certificate in certificates {
println! (
" {} \t {} \t {} " ,
certificate . id , certificate . request_id , certificate . certificate_fingerprint
) ;
}
}
}
2026-05-18 17:24:10 +02:00
ControlResponse ::SshCertSynced {
peer_node_id ,
peer_agent_id ,
endpoint_id ,
requests_imported ,
certificates_imported ,
allowed ,
reason ,
note ,
} = > {
if allowed {
println! (
" synced ssh cert metadata from {peer_node_id}: {requests_imported} requests, {certificates_imported} certificates "
) ;
} else {
println! ( " ssh cert metadata sync denied by {peer_node_id} " ) ;
}
println! ( " agent: {peer_agent_id} " ) ;
println! ( " endpoint: {endpoint_id} " ) ;
println! ( " allowed: {allowed} " ) ;
println! ( " reason: {reason} " ) ;
println! ( " note: {note} " ) ;
}
2026-05-16 00:17:08 +02:00
ControlResponse ::SshRevocationAdded { revocation } = > {
println! ( " added ssh revocation: {} " , revocation . id ) ;
println! ( " kind: {} " , revocation . kind ) ;
println! ( " target: {} " , revocation . target ) ;
if let Some ( reason ) = revocation . reason {
println! ( " reason: {reason} " ) ;
}
}
ControlResponse ::SshRevocationList { revocations } = > {
if revocations . is_empty ( ) {
println! ( " no ssh revocations " ) ;
} else {
for revocation in revocations {
println! (
" {} \t {} \t {} \t {} " ,
revocation . id ,
revocation . kind ,
revocation . target ,
revocation . reason . unwrap_or_default ( )
) ;
}
}
}
2026-05-17 18:29:47 +02:00
ControlResponse ::SshRevocationExported {
out ,
format ,
count ,
note ,
} = > {
2026-05-16 00:17:08 +02:00
println! ( " exported {count} ssh revocations to {} " , out . display ( ) ) ;
2026-05-17 18:29:47 +02:00
println! ( " format: {format} " ) ;
println! ( " note: {note} " ) ;
2026-05-16 00:17:08 +02:00
}
2026-05-18 11:56:42 +02:00
ControlResponse ::SshRevocationImported {
revocations ,
format ,
count ,
note ,
} = > {
println! ( " imported {count} ssh revocations " ) ;
println! ( " format: {format} " ) ;
for revocation in revocations {
println! (
" {} \t {} \t {} " ,
revocation . id , revocation . kind , revocation . target
) ;
}
println! ( " note: {note} " ) ;
}
2026-05-18 17:24:10 +02:00
ControlResponse ::SshRevocationSynced {
peer_node_id ,
peer_agent_id ,
endpoint_id ,
revocations_imported ,
allowed ,
reason ,
note ,
} = > {
if allowed {
println! ( " synced {revocations_imported} ssh revocations from {peer_node_id} " ) ;
} else {
println! ( " ssh revocation sync denied by {peer_node_id} " ) ;
}
println! ( " agent: {peer_agent_id} " ) ;
println! ( " endpoint: {endpoint_id} " ) ;
println! ( " allowed: {allowed} " ) ;
println! ( " reason: {reason} " ) ;
println! ( " note: {note} " ) ;
}
2026-05-16 21:13:33 +02:00
ControlResponse ::DbAdded { db } = > {
println! ( " registered db: {} " , db . name ) ;
println! ( " id: {} " , db . id ) ;
println! ( " resource: {} " , db . resource ) ;
println! ( " path: {} " , db . path ) ;
println! ( " sync_status: {} " , db . sync_status ) ;
}
ControlResponse ::DbStatus { db } = > {
println! ( " db: {} " , db . name ) ;
println! ( " id: {} " , db . id ) ;
println! ( " resource: {} " , db . resource ) ;
println! ( " path: {} " , db . path ) ;
println! ( " path_exists: {} " , db . path_exists ) ;
println! (
" size_bytes: {} " ,
db . size_bytes
. map ( | size | size . to_string ( ) )
. unwrap_or_else ( | | " unknown " . to_owned ( ) )
) ;
println! ( " schema_metadata: {} " , db . schema_metadata ) ;
2026-05-17 20:01:36 +02:00
println! (
" crsqlite_changes_available: {} " ,
db . crsqlite_changes . available
) ;
if let Some ( count ) = db . crsqlite_changes . change_count {
println! ( " crsqlite_change_count: {count} " ) ;
}
if let Some ( version ) = db . crsqlite_changes . max_db_version {
println! ( " crsqlite_max_db_version: {version} " ) ;
}
if let Some ( error ) = db . crsqlite_changes . error {
println! ( " crsqlite_changes_error: {error} " ) ;
}
2026-05-16 21:13:33 +02:00
println! ( " sync_status: {} " , db . sync_status ) ;
}
2026-05-17 20:32:36 +02:00
ControlResponse ::DbChanges { db , batch } = > {
println! ( " db: {} " , db . name ) ;
println! ( " changes: {} " , batch . changes . len ( ) ) ;
println! (
" max_db_version: {} " ,
batch
. max_db_version
. map ( | version | version . to_string ( ) )
. unwrap_or_else ( | | " none " . to_owned ( ) )
) ;
println! ( " schema_metadata: {} " , batch . schema_metadata ) ;
for change in batch . changes {
println! (
" {} \t {} \t {} " ,
change . db_version , change . table_name , change . column_id
) ;
}
}
2026-05-18 22:11:57 +02:00
ControlResponse ::DbSynced {
peer_node_id ,
peer_agent_id ,
endpoint_id ,
name ,
changes_received ,
2026-05-19 19:02:39 +02:00
changes_applied ,
2026-05-18 22:11:57 +02:00
max_db_version ,
schema_match ,
allowed ,
reason ,
note ,
} = > {
if allowed {
println! ( " synced db changes for {name} from {peer_node_id} " ) ;
} else {
println! ( " db sync denied by {peer_node_id} " ) ;
}
println! ( " agent: {peer_agent_id} " ) ;
println! ( " endpoint: {endpoint_id} " ) ;
println! ( " changes_received: {changes_received} " ) ;
2026-05-19 19:02:39 +02:00
println! ( " changes_applied: {changes_applied} " ) ;
2026-05-18 22:11:57 +02:00
println! (
" max_db_version: {} " ,
max_db_version
. map ( | version | version . to_string ( ) )
. unwrap_or_else ( | | " none " . to_owned ( ) )
) ;
println! ( " schema_match: {schema_match} " ) ;
println! ( " allowed: {allowed} " ) ;
println! ( " reason: {reason} " ) ;
println! ( " note: {note} " ) ;
}
2026-05-16 21:52:35 +02:00
ControlResponse ::KvCreated { kv } = > {
println! ( " created kv: {} " , kv . name ) ;
println! ( " id: {} " , kv . id ) ;
println! ( " resource: {} " , kv . resource ) ;
println! ( " sync_status: {} " , kv . sync_status ) ;
}
ControlResponse ::KvSet { entry } = > {
println! ( " set {} {} " , entry . store , entry . key ) ;
}
ControlResponse ::KvGet { entry } = > {
if let Some ( entry ) = entry {
println! ( " {} " , entry . value ) ;
} else {
println! ( " not found " ) ;
}
}
2026-05-18 18:36:03 +02:00
ControlResponse ::KvSynced {
peer_node_id ,
peer_agent_id ,
endpoint_id ,
name ,
entries_imported ,
allowed ,
reason ,
note ,
} = > {
if allowed {
println! ( " synced kv {name} from {peer_node_id} : {entries_imported} entries " ) ;
} else {
println! ( " kv sync denied for {name} by {peer_node_id} " ) ;
}
println! ( " agent: {peer_agent_id} " ) ;
println! ( " endpoint: {endpoint_id} " ) ;
println! ( " allowed: {allowed} " ) ;
println! ( " reason: {reason} " ) ;
println! ( " note: {note} " ) ;
}
2026-05-16 22:15:18 +02:00
ControlResponse ::DocumentCreated { document } = > {
println! ( " created document: {} " , document . name ) ;
println! ( " id: {} " , document . id ) ;
println! ( " resource: {} " , document . resource ) ;
println! ( " sync_status: {} " , document . sync_status ) ;
println! ( " state_bytes: {} " , document . state_bytes ) ;
}
ControlResponse ::DocumentStatus { document } = > {
println! ( " document: {} " , document . name ) ;
println! ( " id: {} " , document . id ) ;
println! ( " resource: {} " , document . resource ) ;
println! ( " sync_status: {} " , document . sync_status ) ;
println! ( " state_bytes: {} " , document . state_bytes ) ;
}
2026-05-17 19:59:03 +02:00
ControlResponse ::DocumentSet { state } = > {
println! ( " updated document: {} " , state . document . name ) ;
println! ( " state_bytes: {} " , state . document . state_bytes ) ;
println! ( " updated_at_ms: {} " , state . updated_at . 0 ) ;
}
ControlResponse ::DocumentGet { state } = > {
println! ( " {} " , state . state_json ) ;
}
2026-05-18 18:49:34 +02:00
ControlResponse ::DocumentSynced {
peer_node_id ,
peer_agent_id ,
endpoint_id ,
name ,
updated ,
allowed ,
reason ,
note ,
} = > {
if allowed {
println! ( " synced document {name} from {peer_node_id} : updated= {updated} " ) ;
} else {
println! ( " document sync denied for {name} by {peer_node_id} " ) ;
}
println! ( " agent: {peer_agent_id} " ) ;
println! ( " endpoint: {endpoint_id} " ) ;
println! ( " allowed: {allowed} " ) ;
println! ( " reason: {reason} " ) ;
println! ( " note: {note} " ) ;
}
2026-05-17 18:23:51 +02:00
ControlResponse ::PubsubPublished { message } = > {
println! ( " published: {} " , message . topic ) ;
println! ( " published_at_ms: {} " , message . published_at . 0 ) ;
}
2026-05-18 18:41:04 +02:00
ControlResponse ::PubsubRemotePublished {
peer_node_id ,
peer_agent_id ,
endpoint_id ,
message ,
allowed ,
reason ,
note ,
} = > {
if allowed {
println! ( " published: {} " , message . topic ) ;
println! ( " peer: {peer_node_id} " ) ;
println! ( " published_at_ms: {} " , message . published_at . 0 ) ;
} else {
println! ( " pubsub publish denied by {peer_node_id} " ) ;
}
println! ( " agent: {peer_agent_id} " ) ;
println! ( " endpoint: {endpoint_id} " ) ;
println! ( " allowed: {allowed} " ) ;
println! ( " reason: {reason} " ) ;
println! ( " note: {note} " ) ;
}
2026-05-17 18:23:51 +02:00
ControlResponse ::PubsubMessages {
topic ,
messages ,
note ,
} = > {
println! ( " topic: {topic} " ) ;
println! ( " messages: {} " , messages . len ( ) ) ;
for message in messages {
println! ( " {} \t {} " , message . published_at . 0 , message . message ) ;
}
println! ( " note: {note} " ) ;
}
2026-05-19 15:28:48 +02:00
ControlResponse ::PubsubRemoteMessages {
peer_node_id ,
peer_agent_id ,
endpoint_id ,
topic ,
messages ,
allowed ,
reason ,
note ,
} = > {
if allowed {
println! ( " topic: {topic} " ) ;
println! ( " peer: {peer_node_id} " ) ;
println! ( " messages: {} " , messages . len ( ) ) ;
for message in messages {
println! ( " {} \t {} " , message . published_at . 0 , message . message ) ;
}
} else {
println! ( " pubsub subscribe denied by {peer_node_id} " ) ;
}
println! ( " agent: {peer_agent_id} " ) ;
println! ( " endpoint: {endpoint_id} " ) ;
println! ( " allowed: {allowed} " ) ;
println! ( " reason: {reason} " ) ;
println! ( " note: {note} " ) ;
}
2026-05-17 20:17:26 +02:00
ControlResponse ::PipeListening { listener } = > {
println! ( " listening pipe: {} " , listener . name ) ;
println! ( " id: {} " , listener . id ) ;
println! ( " listened_at_ms: {} " , listener . listened_at . 0 ) ;
println! ( " note: {} " , listener . note ) ;
}
ControlResponse ::PipeConnected { connection } = > {
println! ( " pipe target: {} " , connection . target ) ;
println! ( " local_listener_found: {} " , connection . local_listener_found ) ;
println! ( " connected_at_ms: {} " , connection . connected_at . 0 ) ;
println! ( " note: {} " , connection . note ) ;
}
2026-05-19 19:21:42 +02:00
ControlResponse ::PipeRemoteListening {
peer_node_id ,
peer_agent_id ,
endpoint_id ,
listener ,
allowed ,
reason ,
note ,
} = > {
if let Some ( listener ) = listener {
println! ( " listening pipe: {} " , listener . name ) ;
println! ( " peer: {peer_node_id} " ) ;
println! ( " id: {} " , listener . id ) ;
println! ( " listened_at_ms: {} " , listener . listened_at . 0 ) ;
} else {
println! ( " pipe listen denied by {peer_node_id} " ) ;
}
println! ( " agent: {peer_agent_id} " ) ;
println! ( " endpoint: {endpoint_id} " ) ;
println! ( " allowed: {allowed} " ) ;
println! ( " reason: {reason} " ) ;
println! ( " note: {note} " ) ;
}
2026-05-18 18:45:10 +02:00
ControlResponse ::PipeRemoteConnected {
peer_node_id ,
peer_agent_id ,
endpoint_id ,
connection ,
allowed ,
reason ,
note ,
} = > {
if allowed {
println! ( " pipe target: {} " , connection . target ) ;
println! ( " peer: {peer_node_id} " ) ;
println! ( " remote_listener_found: {} " , connection . local_listener_found ) ;
println! ( " connected_at_ms: {} " , connection . connected_at . 0 ) ;
} else {
println! ( " pipe connect denied by {peer_node_id} " ) ;
}
println! ( " agent: {peer_agent_id} " ) ;
println! ( " endpoint: {endpoint_id} " ) ;
println! ( " allowed: {allowed} " ) ;
println! ( " reason: {reason} " ) ;
println! ( " note: {note} " ) ;
}
2026-05-20 13:57:14 +02:00
ControlResponse ::PipeSent {
message ,
listener_found ,
note ,
} = > {
println! ( " listener_found: {listener_found} " ) ;
if let Some ( message ) = message {
println! ( " pipe: {} " , message . pipe ) ;
println! ( " received_at_ms: {} " , message . received_at . 0 ) ;
print_pipe_message_data ( & message ) ? ;
}
println! ( " note: {note} " ) ;
}
ControlResponse ::PipeRemoteSent {
peer_node_id ,
peer_agent_id ,
endpoint_id ,
message ,
listener_found ,
allowed ,
reason ,
note ,
} = > {
println! ( " peer: {peer_node_id} " ) ;
println! ( " agent: {peer_agent_id} " ) ;
println! ( " endpoint: {endpoint_id} " ) ;
println! ( " allowed: {allowed} " ) ;
println! ( " listener_found: {listener_found} " ) ;
if let Some ( message ) = message {
println! ( " pipe: {} " , message . pipe ) ;
println! ( " received_at_ms: {} " , message . received_at . 0 ) ;
}
println! ( " reason: {reason} " ) ;
println! ( " note: {note} " ) ;
}
ControlResponse ::PipeMessages {
name ,
messages ,
drained ,
note ,
} = > {
println! ( " pipe: {name} " ) ;
println! ( " messages: {} " , messages . len ( ) ) ;
println! ( " drained: {drained} " ) ;
for message in messages {
print_pipe_message_data ( & message ) ? ;
}
println! ( " note: {note} " ) ;
}
2026-05-19 15:51:11 +02:00
ControlResponse ::SshProxyConnected {
peer_node_id ,
peer_agent_id ,
endpoint_id ,
connection ,
allowed ,
reason ,
note ,
} = > {
if allowed {
println! ( " ssh proxy target: {peer_node_id} " ) ;
if let Some ( connection ) = connection {
println! ( " connected_at_ms: {} " , connection . connected_at . 0 ) ;
if let Some ( local_sshd_target ) = connection . local_sshd_target {
println! ( " remote_sshd_target: {local_sshd_target} " ) ;
}
println! (
" admin_shell_available: {} " ,
connection . admin_shell_available
) ;
println! ( " connection_note: {} " , connection . note ) ;
}
} else {
println! ( " ssh proxy denied by {peer_node_id} " ) ;
}
println! ( " agent: {peer_agent_id} " ) ;
println! ( " endpoint: {endpoint_id} " ) ;
println! ( " allowed: {allowed} " ) ;
println! ( " reason: {reason} " ) ;
println! ( " note: {note} " ) ;
2026-05-21 01:49:48 +02:00
}
ControlResponse ::SshAdminShellOutput {
peer_node_id ,
peer_agent_id ,
endpoint_id ,
command ,
output ,
allowed ,
reason ,
note ,
} = > {
if allowed {
println! ( " {output} " ) ;
} else {
println! ( " ssh admin shell denied by {peer_node_id} " ) ;
}
println! ( " command: {command} " ) ;
println! ( " peer: {peer_node_id} " ) ;
println! ( " agent: {peer_agent_id} " ) ;
println! ( " endpoint: {endpoint_id} " ) ;
println! ( " allowed: {allowed} " ) ;
println! ( " reason: {reason} " ) ;
println! ( " note: {note} " ) ;
2026-05-19 15:51:11 +02:00
}
2026-05-15 15:08:20 +02:00
ControlResponse ::NotImplemented { module , command } = > {
println! ( " {module} {command} : not implemented yet " ) ;
}
ControlResponse ::Error { message } = > bail! ( message ) ,
}
Ok ( ( ) )
}
2026-05-16 00:17:08 +02:00
2026-07-18 16:31:59 +02:00
fn print_daemon_install_report (
report : & ServiceReport ,
readiness : Option < & WaitReport > ,
2026-07-18 17:15:36 +02:00
output : OutputMode ,
2026-07-18 16:31:59 +02:00
) -> Result < ( ) > {
2026-07-18 17:15:36 +02:00
if output . is_machine ( ) {
2026-07-18 16:31:59 +02:00
let readiness = readiness . map ( wait_report_json ) ;
2026-07-18 17:15:36 +02:00
output . print ( & serde_json ::json! ( {
" type " : " daemon-install " ,
" service " : service_report_json ( report ) ,
" readiness " : readiness ,
} ) ) ? ;
2026-07-18 16:31:59 +02:00
return Ok ( ( ) ) ;
}
2026-07-18 17:15:36 +02:00
print_service_report ( report , OutputMode ::Human ) ? ;
2026-07-18 16:31:59 +02:00
if let Some ( readiness ) = readiness {
println! ( " readiness: " ) ;
println! ( " ready: {} " , readiness . ready ) ;
println! ( " elapsed_ms: {} " , readiness . elapsed_ms ) ;
println! ( " reason: {} " , readiness . reason ) ;
if ! readiness . ready {
println! ( " next: inspect logs with `geth daemon logs` " ) ;
}
}
Ok ( ( ) )
}
fn wait_report_json ( report : & WaitReport ) -> serde_json ::Value {
serde_json ::json! ( {
" target " : report . target ,
" ready " : report . ready ,
" elapsed_ms " : report . elapsed_ms ,
" attempts " : report . attempts ,
" reason " : report . reason ,
} )
}
fn service_report_json ( report : & ServiceReport ) -> serde_json ::Value {
serde_json ::json! ( {
2026-07-18 17:15:36 +02:00
" type " : " service " ,
2026-07-18 16:31:59 +02:00
" manager " : report . manager . to_string ( ) ,
" action " : report . action . as_str ( ) ,
" service_name " : report . service_name ,
" state " : report . state ,
" definition_path " : report . definition_path ,
" definition " : report . definition ,
" commands " : report . commands ,
" note " : report . note ,
} )
}
2026-07-18 17:15:36 +02:00
fn print_service_report ( report : & ServiceReport , output : OutputMode ) -> Result < ( ) > {
if output . is_machine ( ) {
output . print ( & service_report_json ( report ) ) ? ;
2026-05-16 00:17:08 +02:00
return Ok ( ( ) ) ;
}
println! ( " service: {} " , report . service_name ) ;
println! ( " manager: {} " , report . manager ) ;
2026-07-18 16:31:59 +02:00
println! ( " action: {} " , report . action . as_str ( ) ) ;
2026-07-18 15:23:20 +02:00
if let Some ( state ) = & report . state {
println! ( " state: {state} " ) ;
}
2026-07-18 16:31:59 +02:00
if let Some ( path ) = & report . definition_path {
2026-05-16 00:17:08 +02:00
println! ( " definition: {} " , path . display ( ) ) ;
}
if ! report . commands . is_empty ( ) {
println! ( " commands: " ) ;
2026-07-18 16:31:59 +02:00
for command in & report . commands {
println! ( " {} " , shell_quote_command ( command ) ) ;
2026-05-16 00:17:08 +02:00
}
}
2026-07-18 16:31:59 +02:00
if let Some ( definition ) = & report . definition {
2026-05-16 00:17:08 +02:00
println! ( " definition_body: " ) ;
print! ( " {definition} " ) ;
}
println! ( " note: {} " , report . note ) ;
Ok ( ( ) )
}
2026-07-18 17:15:36 +02:00
fn print_service_logs_report ( report : & ServiceLogsReport , output : OutputMode ) -> Result < ( ) > {
if output . is_machine ( ) {
output . print ( & serde_json ::json! ( {
" type " : " service-logs " ,
" manager " : report . manager . to_string ( ) ,
" service_name " : report . service_name ,
" state " : report . state . as_str ( ) ,
" sources " : report . sources ,
" command " : report . command ,
" lines " : report . lines ,
" note " : report . note ,
} ) ) ? ;
2026-07-18 16:31:59 +02:00
return Ok ( ( ) ) ;
}
println! ( " service: {} " , report . service_name ) ;
println! ( " manager: {} " , report . manager ) ;
println! ( " state: {} " , report . state . as_str ( ) ) ;
for source in & report . sources {
println! ( " source: {} " , source . display ( ) ) ;
}
if let Some ( command ) = & report . command {
println! ( " command: {} " , shell_quote_command ( command ) ) ;
}
for line in & report . lines {
println! ( " {line} " ) ;
}
println! ( " note: {} " , report . note ) ;
Ok ( ( ) )
}
2026-05-18 03:57:26 +02:00
fn print_file_conflict ( conflict : & geth_cas ::FileConflict ) {
println! ( " root: {} " , conflict . root ) ;
println! ( " resource: {} " , conflict . resource ) ;
println! ( " path: {} " , conflict . path ) ;
println! ( " kind: {} " , conflict . kind . as_str ( ) ) ;
println! ( " status: {} " , conflict . status . as_str ( ) ) ;
if let Some ( hash ) = & conflict . base_tree {
println! ( " base_tree: {hash} " ) ;
}
if let Some ( hash ) = & conflict . local_tree {
println! ( " local_tree: {hash} " ) ;
}
if let Some ( hash ) = & conflict . remote_tree {
println! ( " remote_tree: {hash} " ) ;
}
println! ( " detail: {} " , conflict . detail ) ;
if let Some ( resolution ) = & conflict . resolution {
println! ( " resolution: {} " , resolution . as_str ( ) ) ;
}
if let Some ( note ) = & conflict . resolution_note {
println! ( " resolution_note: {note} " ) ;
}
}
2026-05-20 13:57:14 +02:00
fn print_pipe_message_data ( message : & geth_pipe ::PipeMessage ) -> Result < ( ) > {
let bytes = base64 ::engine ::general_purpose ::STANDARD
. decode ( & message . data_base64 )
. context ( " decode pipe message " ) ? ;
match String ::from_utf8 ( bytes ) {
Ok ( text ) = > println! ( " {text} " ) ,
Err ( error ) = > println! (
" base64:{} " ,
base64 ::engine ::general_purpose ::STANDARD . encode ( error . into_bytes ( ) )
) ,
}
Ok ( ( ) )
}
2026-05-20 13:59:41 +02:00
fn pipe_send_payload_base64 ( message : Option < String > , input : Option < PathBuf > ) -> Result < String > {
match ( message , input ) {
( Some ( message ) , None ) = > Ok ( base64 ::engine ::general_purpose ::STANDARD . encode ( message ) ) ,
( None , Some ( path ) ) if path . as_os_str ( ) = = " - " = > {
let mut bytes = Vec ::new ( ) ;
std ::io ::stdin ( )
. read_to_end ( & mut bytes )
. context ( " read pipe payload from stdin " ) ? ;
Ok ( base64 ::engine ::general_purpose ::STANDARD . encode ( bytes ) )
}
( None , Some ( path ) ) = > {
let bytes = std ::fs ::read ( & path ) . with_context ( | | format! ( " read {} " , path . display ( ) ) ) ? ;
Ok ( base64 ::engine ::general_purpose ::STANDARD . encode ( bytes ) )
}
( Some ( _ ) , Some ( _ ) ) = > bail! ( " pipe send accepts either MESSAGE or --in, not both " ) ,
( None , None ) = > bail! ( " pipe send requires MESSAGE or --in <path>; use --in - for stdin " ) ,
}
}
2026-05-16 00:17:08 +02:00
fn shell_quote_command ( command : & [ String ] ) -> String {
command
. iter ( )
. map ( | arg | {
if arg
. bytes ( )
. all ( | byte | byte . is_ascii_alphanumeric ( ) | | b " -_./:=+@, " . contains ( & byte ) )
{
arg . clone ( )
} else {
format! ( " ' {} ' " , arg . replace ( '\'' , " ' \\ '' " ) )
}
} )
. collect ::< Vec < _ > > ( )
. join ( " " )
}
2026-07-05 18:30:44 +02:00
#[ cfg(test) ]
mod tests {
use super ::* ;
2026-07-18 15:23:20 +02:00
#[ test ]
fn help_describes_common_workflows_and_command_families ( ) {
2026-07-18 16:08:19 +02:00
let help = documented_cli_command ( ) . render_long_help ( ) . to_string ( ) ;
2026-07-18 15:23:20 +02:00
assert! ( help . contains ( " geth daemon install " ) ) ;
assert! ( help . contains ( " geth daemon run --ephemeral " ) ) ;
assert! ( help . contains ( " Show daemon, storage, Iroh, and backend health " ) ) ;
assert! ( help . contains ( " Run, install, and manage the daemon " ) ) ;
assert! ( help . contains ( " --home <DIR> " ) ) ;
}
2026-07-18 16:08:19 +02:00
#[ test ]
fn every_cli_argument_has_operator_facing_help ( ) {
fn check ( command : clap ::Command , path : String , missing : & mut Vec < String > ) {
for argument in command . get_arguments ( ) {
let id = argument . get_id ( ) . as_str ( ) ;
if matches! ( id , " help " | " version " ) {
continue ;
}
if argument . get_help ( ) . is_none ( ) {
missing . push ( format! ( " {path} : {id} " ) ) ;
}
}
for subcommand in command . get_subcommands ( ) {
check (
subcommand . clone ( ) ,
format! ( " {path} {} " , subcommand . get_name ( ) ) ,
missing ,
) ;
}
}
let mut missing = Vec ::new ( ) ;
check ( documented_cli_command ( ) , " geth " . to_owned ( ) , & mut missing ) ;
assert! (
missing . is_empty ( ) ,
" missing argument help: \n {} " ,
missing . join ( " \n " )
) ;
}
2026-07-18 15:23:20 +02:00
#[ test ]
fn common_daemon_lifecycle_commands_parse_directly ( ) {
2026-07-18 16:31:59 +02:00
for command in [ " install " , " start " , " stop " , " status " , " logs " , " uninstall " ] {
2026-07-18 15:23:20 +02:00
let parsed = Cli ::try_parse_from ( [ " geth " , " daemon " , command ] ) ;
assert! ( parsed . is_ok ( ) , " daemon {command} should parse: {parsed:?} " ) ;
}
let parsed = Cli ::try_parse_from ( [ " geth " , " daemon " , " run " , " --ephemeral " ] )
. expect ( " parse ephemeral daemon " ) ;
assert! ( matches! (
parsed . command ,
Command ::Daemon {
command : DaemonCommand ::Run { ephemeral : true }
}
) ) ;
2026-07-18 16:31:59 +02:00
assert! ( Cli ::try_parse_from ( [ " geth " , " daemon " , " logs " , " --lines " , " 0 " ] ) . is_err ( ) ) ;
}
2026-07-18 21:00:06 +02:00
#[ test ]
fn sshsigchain_exposes_one_pinned_verifier_not_static_compatibility_commands ( ) {
let parsed = Cli ::try_parse_from ( [
" geth " ,
" keychain " ,
" verify-sigchain " ,
" --in " ,
" chain.jsonl " ,
" --chain-id " ,
" 00 " ,
" --root-key " ,
" root.pub " ,
] )
. expect ( " parse SSHSIGCHAIN verifier " ) ;
assert! ( matches! (
parsed . command ,
Command ::Keychain {
command : KeychainCommand ::VerifySigchain { .. }
}
) ) ;
for removed in [
" sigchain " ,
" publish-bundle " ,
" import-sigchain " ,
" verify-checkpoint " ,
" fetch " ,
" verify-sigchain-v1 " ,
] {
assert! (
Cli ::try_parse_from ( [ " geth " , " keychain " , removed ] ) . is_err ( ) ,
" removed static command {removed} must not parse "
) ;
}
}
2026-07-18 16:31:59 +02:00
#[ test ]
fn service_install_copies_transient_binaries_unless_explicitly_overridden ( ) {
assert! (
transient_executable_reason ( std ::path ::Path ::new ( " /work/geth/target/debug/geth " ) )
. is_some ( )
) ;
assert! ( transient_executable_reason ( std ::path ::Path ::new ( " /usr/local/bin/geth " ) ) . is_none ( ) ) ;
let binary = tempfile ::NamedTempFile ::new ( ) . expect ( " temporary binary " ) ;
std ::fs ::write ( binary . path ( ) , b " geth-test-binary " ) . expect ( " write binary " ) ;
let home = tempfile ::tempdir ( ) . expect ( " temporary geth home " ) ;
let paths = GethPaths ::from_home ( home . path ( ) ) ;
let ( copied , source ) = service_executable ( & paths , Some ( binary . path ( ) . to_path_buf ( ) ) , false )
. expect ( " copy transient binary " ) ;
assert_eq! (
copied ,
paths
. home ( )
. join ( " bin " )
. join ( if cfg! ( windows ) { " geth.exe " } else { " geth " } )
) ;
assert_eq! ( source , Some ( binary . path ( ) . canonicalize ( ) . expect ( " source " ) ) ) ;
assert_eq! (
std ::fs ::read ( copied ) . expect ( " read copy " ) ,
b " geth-test-binary "
) ;
std ::fs ::write ( binary . path ( ) , b " geth-updated-binary " ) . expect ( " update source binary " ) ;
let ( copied , _ ) = service_executable ( & paths , Some ( binary . path ( ) . to_path_buf ( ) ) , false )
. expect ( " replace copied binary " ) ;
assert_eq! (
std ::fs ::read ( copied ) . expect ( " read replacement " ) ,
b " geth-updated-binary "
) ;
let ( direct , source ) = service_executable ( & paths , Some ( binary . path ( ) . to_path_buf ( ) ) , true )
. expect ( " allow transient binary " ) ;
assert_eq! ( direct , binary . path ( ) . canonicalize ( ) . expect ( " source " ) ) ;
assert! ( source . is_none ( ) ) ;
2026-07-18 15:23:20 +02:00
}
2026-07-18 16:15:13 +02:00
#[ test ]
fn configuration_commands_parse_and_update_an_isolated_home ( ) {
for command in [ " path " , " show " , " validate " ] {
let parsed = Cli ::try_parse_from ( [ " geth " , " config " , command ] ) ;
assert! ( parsed . is_ok ( ) , " config {command} should parse: {parsed:?} " ) ;
}
let parsed =
Cli ::try_parse_from ( [ " geth " , " config " , " set " , " sync.live_sync_interval_ms " , " 500 " ] ) ;
assert! ( parsed . is_ok ( ) , " config set should parse: {parsed:?} " ) ;
let home = tempfile ::tempdir ( ) . expect ( " temporary geth home " ) ;
let paths = GethPaths ::from_home ( home . path ( ) ) ;
run_config_command (
& paths ,
ConfigCommand ::Set {
key : ConfigKeyArg ::SyncLiveSyncIntervalMs ,
value : " 500 " . to_owned ( ) ,
} ,
2026-07-18 17:15:36 +02:00
OutputMode ::Human ,
2026-07-18 16:15:13 +02:00
)
. expect ( " set config " ) ;
let config = GethConfig ::load ( & paths . config_file ( ) ) . expect ( " load config " ) ;
assert_eq! ( config . sync . live_sync_interval_ms , 500 ) ;
}
2026-07-18 16:48:35 +02:00
#[ test ]
fn guided_enrollment_join_parses_the_common_new_node_workflow ( ) {
let parsed = Cli ::try_parse_from ( [
" geth " ,
" node " ,
" enroll " ,
" join " ,
" owner.peer.json " ,
" --admin-key " ,
" owner-admin.pub " ,
" --node-name " ,
" workstation " ,
" --capability " ,
" resource:kv:preferences=kv.read " ,
] )
. expect ( " parse guided enrollment " ) ;
assert! ( matches! (
parsed . command ,
Command ::Node {
command : NodeCommand ::Enroll {
command : NodeEnrollCommand ::Join { .. }
}
}
) ) ;
}
2026-07-18 17:15:36 +02:00
#[ test ]
fn json_and_jsonl_have_distinct_single_document_contracts ( ) {
let value = serde_json ::json! ( {
" type " : " contract-test " ,
" nested " : { " ok " : true } ,
} ) ;
let pretty = OutputMode ::Json . encode ( & value ) . expect ( " pretty json " ) ;
let jsonl = OutputMode ::Jsonl . encode ( & value ) . expect ( " jsonl " ) ;
assert! ( pretty . contains ( '\n' ) ) ;
assert! ( ! jsonl . contains ( '\n' ) ) ;
assert_eq! (
serde_json ::from_str ::< serde_json ::Value > ( & jsonl ) . expect ( " decode jsonl " ) ,
value
) ;
assert! ( Cli ::try_parse_from ( [ " geth " , " status " , " --json " , " --jsonl " ] ) . is_err ( ) ) ;
}
2026-07-18 17:21:12 +02:00
#[ test ]
fn resource_capabilities_are_available_without_daemon_state ( ) {
let all = resource_capability_contracts ( None ) . expect ( " complete catalog " ) ;
assert! ( all . len ( ) > = 8 ) ;
let kv = resource_capability_contracts ( Some ( " kv " ) ) . expect ( " kv catalog " ) ;
assert_eq! ( kv . len ( ) , 1 ) ;
assert_eq! ( kv [ 0 ] . resource_ids , & [ " resource:kv:<name> " ] ) ;
assert! ( kv [ 0 ] . capabilities . contains ( & " kv.write_prefix:<prefix> " ) ) ;
assert_eq! (
resource_capability_contracts ( Some ( " cas " ) ) . expect ( " cas alias " ) [ 0 ] . family ,
" cas-file-root "
) ;
assert! ( resource_capability_contracts ( Some ( " unknown " ) ) . is_err ( ) ) ;
let parsed = Cli ::try_parse_from ( [ " geth " , " resource " , " capabilities " , " ssh " ] )
. expect ( " parse capability discovery " ) ;
assert! ( matches! (
parsed . command ,
Command ::Resource {
command : ResourceCommand ::Capabilities { family : Some ( family ) }
} if family = = " ssh "
) ) ;
}
2026-07-18 15:23:20 +02:00
#[ tokio::test ]
async fn ephemeral_daemon_rejects_an_explicit_persistent_home ( ) {
let parsed = Cli ::try_parse_from ( [
" geth " ,
" --home " ,
" /tmp/persistent-geth " ,
" daemon " ,
" run " ,
" --ephemeral " ,
] )
. expect ( " parse command before semantic validation " ) ;
let error = run_inner ( parsed )
. await
. expect_err ( " --home and --ephemeral must conflict " ) ;
let message = error . to_string ( ) ;
assert! ( message . contains ( " --home " ) ) ;
assert! ( message . contains ( " --ephemeral " ) ) ;
}
#[ test ]
fn ephemeral_node_initializes_and_cleans_up_its_temporary_home ( ) {
let ( home , paths , node ) = create_ephemeral_node ( ) . expect ( " create ephemeral node " ) ;
let path = paths . home ( ) . to_path_buf ( ) ;
assert! ( paths . metadata_db ( ) . exists ( ) ) ;
assert! ( paths . config_file ( ) . exists ( ) ) ;
assert! ( node . node_id . starts_with ( " node: " ) ) ;
drop ( home ) ;
assert! ( ! path . exists ( ) ) ;
}
2026-07-05 18:30:44 +02:00
#[ test ]
fn json_error_classification_is_stable_for_common_failures ( ) {
assert_eq! (
json_error_code ( " connect to daemon at /tmp/geth.sock: No such file or directory " ) ,
" daemon_unavailable "
) ;
assert_eq! (
json_error_code ( " unauthorized: missing grant " ) ,
" unauthorized "
) ;
assert_eq! (
json_error_code ( " peer candidate not found: node:missing " ) ,
" peer_not_found "
) ;
assert_eq! (
json_error_hint (
" peer candidate not found: node:missing \n next: import a peer card with `geth peer import <path>` " ,
) ,
Some ( " import a peer card with `geth peer import <path>` " . to_owned ( ) )
) ;
}
}