From 55cb455de3a02de2174eac06491c353aff9efe1b Mon Sep 17 00:00:00 2001 From: Eric Wendland Date: Wed, 20 May 2026 13:59:41 +0200 Subject: [PATCH] Accept file payloads for pipe send --- AGENTS.md | 4 ++-- README.md | 8 ++++---- crates/geth-cli/src/lib.rs | 27 +++++++++++++++++++++++++-- docs/architecture.md | 2 +- docs/roadmap.md | 4 ++-- 5 files changed, 34 insertions(+), 11 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 08d95bc..eea155b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -177,8 +177,8 @@ Roadmap items should be actionable and checkable: ALPN and requires `pipe.connect` on `resource:pipe:`. Remote `geth pipe listen --node ` requires `pipe.listen` on the same resource before creating a daemon-lifetime listener on the peer. `geth pipe - send --node ` carries a byte message over the - dedicated `/geth/pipe/1` ALPN when `pipe.connect` is authorized, and + send [message|--in |--in -] --node ` carries a byte + message over the dedicated `/geth/pipe/1` ALPN when `pipe.connect` is authorized, and `geth pipe recv ` drains local daemon-lifetime messages. Long-lived bidirectional streams and TCP/Unix forwarding are still roadmap work. - `geth ssh proxy ` performs an authorized control-plane handshake over diff --git a/README.md b/README.md index 6e518ec..0cd779d 100644 --- a/README.md +++ b/README.md @@ -145,7 +145,7 @@ The bootstrap implementation provides: - pipe registry/message commands: `geth pipe listen [--node ] [--bearer-secret ]`, `geth pipe connect [--node ] [--bearer-secret ]`, - `geth pipe send [--node ] [--bearer-secret ]`, + `geth pipe send [message|--in |--in -] [--node ] [--bearer-secret ]`, and `geth pipe recv [--peek]` `geth peer export/import/list` is for untrusted peer-card exchange. Peer cards @@ -201,9 +201,9 @@ peer's current daemon-lifetime snapshot for that topic. Remote pipe connect uses the same protected Iroh control path and requires `pipe.connect` on `resource:pipe:`. The current prototype records a remote connection attempt and whether a listener exists. `geth pipe send - --node ` uses the dedicated `/geth/pipe/1` Iroh ALPN to -write a byte message to an authorized peer listener, and `geth pipe recv ` -drains local daemon-lifetime messages. +[message|--in |--in -] --node ` uses the dedicated `/geth/pipe/1` +Iroh ALPN to write a byte message to an authorized peer listener, and `geth pipe +recv ` drains local daemon-lifetime messages. Remote pipe listen uses the same protected path: `geth pipe listen --node ` requires `pipe.listen` on `resource:pipe:` before registering a daemon-lifetime listener on the diff --git a/crates/geth-cli/src/lib.rs b/crates/geth-cli/src/lib.rs index ba41f70..20fdeb1 100644 --- a/crates/geth-cli/src/lib.rs +++ b/crates/geth-cli/src/lib.rs @@ -4,6 +4,7 @@ use clap::{Args, Parser, Subcommand}; use geth_config::GethPaths; use geth_control::{ControlRequest, ControlResponse}; use geth_node::service::{ServiceInstallOptions, ServiceManager, ServiceReport}; +use std::io::Read; use std::path::PathBuf; #[derive(Debug, Parser)] @@ -398,7 +399,9 @@ pub enum PipeCommand { }, Send { target: String, - message: String, + message: Option, + #[arg(long = "in", value_name = "PATH")] + input: Option, #[arg(long)] node: Option, #[arg(long)] @@ -880,11 +883,12 @@ fn request_for_command(command: Command) -> Result { PipeCommand::Send { target, message, + input, node, bearer_secret, } => ControlRequest::PipeSend { target, - data_base64: base64::engine::general_purpose::STANDARD.encode(message.as_bytes()), + data_base64: pipe_send_payload_base64(message, input)?, node, bearer_secret, }, @@ -2140,6 +2144,25 @@ fn print_pipe_message_data(message: &geth_pipe::PipeMessage) -> Result<()> { Ok(()) } +fn pipe_send_payload_base64(message: Option, input: Option) -> Result { + 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 ; use --in - for stdin"), + } +} + fn shell_quote_command(command: &[String]) -> String { command .iter() diff --git a/docs/architecture.md b/docs/architecture.md index 09ce78d..3b7a7e1 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -208,7 +208,7 @@ daemon-lifetime runtime. `geth pipe connect --node ` sends an authorized remote connect request over the protected Iroh control ALPN. The remote daemon validates endpoint/card binding and requires `pipe.connect` on `resource:pipe:` before recording the connection attempt and reporting -whether a listener exists. `geth pipe send --node ` +whether a listener exists. `geth pipe send [message|--in |--in -] --node ` uses the dedicated `/geth/pipe/1` ALPN to write a byte message to a peer listener after the same endpoint/card and capability checks. `geth pipe recv ` drains local daemon-lifetime messages. `geth pipe listen --node diff --git a/docs/roadmap.md b/docs/roadmap.md index ee51abc..46d0469 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -364,8 +364,8 @@ Goal: add authorized stream-oriented management workflows over Iroh. - `[x]` Remote pipe listen requires `pipe.listen` on `resource:pipe:`. - `[x]` Tests cover denied and allowed remote listener registration. - - `[x]` `geth pipe send --node ` carries a byte - message over the dedicated `/geth/pipe/1` Iroh ALPN. + - `[x]` `geth pipe send [message|--in |--in -] --node ` + carries a byte message over the dedicated `/geth/pipe/1` Iroh ALPN. - `[x]` `geth pipe recv ` drains daemon-lifetime pipe messages. - `[x]` Remote pipe send requires `pipe.connect` on `resource:pipe:`.