use geth_auth::{AuthExplanation, AuthOp}; use geth_cas::{FileConflict, FileRoot, FileRootScan}; use geth_db::{CrSqliteChangeBatch, DbResource}; use geth_discovery::{DiscoveredPeer, PeerCard}; use geth_document::{DocumentResource, DocumentState}; use geth_keychain::KeychainOp; use geth_kv::{KvEntry, KvResource}; use geth_pipe::{PipeConnection, PipeListener}; use geth_pubsub::PubsubMessage; use geth_resource::ResourceDescriptor; use geth_secrets::{BearerAccess, ResourceMasterSecret}; use geth_ssh_identity::{ SshCertApproval, SshCertRequest, SshCertificateRecord, SshRevocationEntry, }; use geth_types::BlobHash; use serde::{Deserialize, Serialize}; use std::path::PathBuf; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "kebab-case")] pub enum ControlRequest { Status, NodeId, PeerCardExport { out: Option, }, PeerCardImport { path: PathBuf, }, PeerCardList, ResourceList, ResourceCreate { kind: String, name: String, }, CasAdd { path: PathBuf, }, CasGet { hash: BlobHash, out: PathBuf, }, CasHash { path: PathBuf, }, CasHas { hash: BlobHash, }, CasPin { hash: BlobHash, }, CasUnpin { hash: BlobHash, }, CasCleanup { dry_run: bool, }, CasList, CasRootAdd { name: String, path: PathBuf, }, CasRootList, CasRootScan { name: String, }, CasConflictRecord { root: String, path: String, kind: String, detail: String, base_tree: Option, local_tree: Option, remote_tree: Option, }, CasConflictList { root: Option, }, CasConflictResolve { conflict_id: String, resolution: String, note: Option, }, KeychainInit { admin_key_path: Option, }, KeychainStatus, SecretStatus, SecretCreate { resource: String, }, SecretRotate { resource: String, }, SecretBearerCreate { resource: String, capabilities: Vec, expires_at_ms: Option, }, SecretBearerList, SecretBearerRevoke { resource: String, secret: String, }, AuthExplain { subject: String, resource: String, capability: String, }, AuthGrant { subject: String, resource: String, capability: String, grant_id: Option, }, AuthRevoke { resource: String, grant_id: String, }, SshCertRequest { public_key_path: PathBuf, cert_kind: String, principals: Vec, requested_validity: Option, renewal_of: Option, reason: Option, }, SshCertRequests, SshCertApprove { request_id: String, ca_key_path: PathBuf, valid_for: Option, serial: Option, out: Option, }, SshCertImport { request_id: String, cert_path: PathBuf, }, SshCertList, SshRevocationAdd { kind: String, target: String, reason: Option, }, SshRevocationList, SshRevocationExport { out: PathBuf, format: String, }, DbAdd { name: String, path: PathBuf, }, DbStatus { name: String, }, DbChanges { name: String, after_db_version: Option, limit: u32, }, KvCreate { name: String, }, KvSet { name: String, key: String, value: String, }, KvGet { name: String, key: String, }, DocumentCreate { name: String, }, DocumentStatus { name: String, }, DocumentSet { name: String, state_json: String, }, DocumentGet { name: String, }, PubsubPub { topic: String, message: String, }, PubsubSub { topic: String, }, PipeListen { name: String, }, PipeConnect { target: String, }, ModuleStub { module: String, command: String, }, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "kebab-case")] pub enum ControlResponse { Status(StatusResponse), NodeId(NodeIdResponse), PeerCardExported { card: PeerCard, out: Option, note: String, }, PeerCardImported { peer: DiscoveredPeer, note: String, }, PeerCardList { peers: Vec, note: String, }, ResourceList { resources: Vec, }, ResourceCreated { resource: ResourceDescriptor, }, CasAdded { hash: BlobHash, size_bytes: u64, }, CasGot { hash: BlobHash, out: PathBuf, size_bytes: u64, }, CasHash { hash: BlobHash, }, CasHas { hash: BlobHash, present: bool, }, CasPinned { hash: BlobHash, pinned: bool, }, CasCleanup { removed: Vec, retained_pinned: Vec, dry_run: bool, }, CasList { blobs: Vec, }, CasRootAdded { root: FileRoot, }, CasRootList { roots: Vec, }, CasRootScanned { scan: FileRootScan, }, CasConflictRecorded { conflict: FileConflict, }, CasConflictList { conflicts: Vec, }, CasConflictResolved { conflict: FileConflict, }, KeychainStatus(KeychainStatusResponse), KeychainInitialized { ops: Vec, }, SecretStatus { secrets: Vec, }, SecretCreated { secret: ResourceMasterSecret, }, SecretBearerCreated { access: BearerAccess, }, SecretBearerList { access: Vec, }, SecretBearerRevoked { resource: String, secret: String, }, AuthExplain(AuthExplanation), AuthOpRecorded { op: AuthOp, }, SshCertRequested { request: SshCertRequest, }, SshCertRequests { requests: Vec, }, SshCertApproved { approval: SshCertApproval, }, SshCertImported { certificate: SshCertificateRecord, }, SshCertList { requests: Vec, certificates: Vec, }, SshRevocationAdded { revocation: SshRevocationEntry, }, SshRevocationList { revocations: Vec, }, SshRevocationExported { out: PathBuf, format: String, count: usize, note: String, }, DbAdded { db: DbResource, }, DbStatus { db: DbResource, }, DbChanges { db: DbResource, batch: CrSqliteChangeBatch, }, KvCreated { kv: KvResource, }, KvSet { entry: KvEntry, }, KvGet { entry: Option, }, DocumentCreated { document: DocumentResource, }, DocumentStatus { document: DocumentResource, }, DocumentSet { state: DocumentState, }, DocumentGet { state: DocumentState, }, PubsubPublished { message: PubsubMessage, }, PubsubMessages { topic: String, messages: Vec, note: String, }, PipeListening { listener: PipeListener, }, PipeConnected { connection: PipeConnection, }, NotImplemented { module: String, command: String, }, Error { message: String, }, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct StatusResponse { pub home: PathBuf, pub socket: PathBuf, pub agent_id: String, pub node_id: String, pub iroh_enabled: bool, pub endpoint_id: Option, pub iroh_relay_mode: String, pub iroh_local_discovery: bool, pub iroh: String, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct NodeIdResponse { pub agent_id: String, pub node_id: String, pub endpoint_id: Option, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct KeychainStatusResponse { pub initialized: bool, pub admin_keys: usize, pub users: usize, pub devices: usize, pub nodes: usize, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CasBlob { pub hash: BlobHash, pub size_bytes: u64, pub pinned: bool, } #[derive(Debug, thiserror::Error)] pub enum ControlError { #[error("json error: {0}")] Json(#[from] serde_json::Error), } pub fn encode_request(request: &ControlRequest) -> Result { let mut line = serde_json::to_string(request)?; line.push('\n'); Ok(line) } pub fn decode_request(line: &str) -> Result { serde_json::from_str(line).map_err(ControlError::from) } pub fn encode_response(response: &ControlResponse) -> Result { let mut line = serde_json::to_string(response)?; line.push('\n'); Ok(line) } pub fn decode_response(line: &str) -> Result { serde_json::from_str(line).map_err(ControlError::from) } #[cfg(test)] mod tests { use super::*; #[test] fn control_request_response_serialization_roundtrip() { let request = ControlRequest::CasHas { hash: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".into(), }; assert_eq!( decode_request(&encode_request(&request).expect("encode")).expect("decode"), request ); let response = ControlResponse::CasHas { hash: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".into(), present: true, }; assert_eq!( decode_response(&encode_response(&response).expect("encode")).expect("decode"), response ); let request = ControlRequest::SshRevocationExport { out: PathBuf::from("revocations.krl-spec"), format: "openssh-krl-spec".to_owned(), }; assert_eq!( decode_request(&encode_request(&request).expect("encode")).expect("decode"), request ); let response = ControlResponse::SshRevocationExported { out: PathBuf::from("revocations.krl-spec"), format: "openssh-krl-spec".to_owned(), count: 2, note: "OpenSSH KRL specification".to_owned(), }; assert_eq!( decode_response(&encode_response(&response).expect("encode")).expect("decode"), response ); let request = ControlRequest::DocumentSet { name: "notes".to_owned(), state_json: r#"{"title":"notes"}"#.to_owned(), }; assert_eq!( decode_request(&encode_request(&request).expect("encode")).expect("decode"), request ); let request = ControlRequest::PipeListen { name: "inbox".to_owned(), }; assert_eq!( decode_request(&encode_request(&request).expect("encode")).expect("decode"), request ); let request = ControlRequest::DbChanges { name: "notes".to_owned(), after_db_version: Some(7), limit: 10, }; assert_eq!( decode_request(&encode_request(&request).expect("encode")).expect("decode"), request ); let request = ControlRequest::CasRootScan { name: "notes".to_owned(), }; assert_eq!( decode_request(&encode_request(&request).expect("encode")).expect("decode"), request ); let request = ControlRequest::CasConflictResolve { conflict_id: "file-conflict:notes:1".to_owned(), resolution: "keep-local".to_owned(), note: Some("local file is authoritative".to_owned()), }; assert_eq!( decode_request(&encode_request(&request).expect("encode")).expect("decode"), request ); let request = ControlRequest::PeerCardExport { out: None }; assert_eq!( decode_request(&encode_request(&request).expect("encode")).expect("decode"), request ); } }