auths_sdk/ports/agent.rs
1//! Agent-based signing port for delegating cryptographic operations to a running agent process.
2//!
3//! The agent signing port abstracts the IPC-based signing protocol so that
4//! the SDK workflow layer remains platform-independent. On Unix, the CLI
5//! wires a concrete adapter that speaks the auths-agent wire protocol over
6//! a Unix domain socket. On Windows, WASM, and other targets the
7//! `NoopAgentProvider` is used instead.
8
9/// Errors from agent signing operations.
10#[derive(Debug, thiserror::Error)]
11#[non_exhaustive]
12pub enum AgentSigningError {
13 /// The agent is not available on this platform or is not installed.
14 #[error("agent unavailable: {0}")]
15 Unavailable(String),
16
17 /// The agent socket exists but the connection failed.
18 #[error("agent connection failed: {0}")]
19 ConnectionFailed(String),
20
21 /// The agent accepted the request but signing failed.
22 #[error("agent signing failed: {0}")]
23 SigningFailed(String),
24
25 /// The agent could not be started.
26 #[error("agent startup failed: {0}")]
27 StartupFailed(String),
28}
29
30/// Port for delegating signing operations to a running agent process.
31///
32/// Implementations must convert the agent's native response format into an
33/// SSHSIG PEM string (`-----BEGIN SSH SIGNATURE----- … -----END SSH SIGNATURE-----`)
34/// so that callers receive the same format as [`crate::signing::sign_with_seed`].
35///
36/// Args:
37/// * Trait methods accept namespace identifiers, public key bytes, and raw data to sign.
38///
39/// Usage:
40/// ```ignore
41/// let pem = agent.try_sign("git", &pubkey_bytes, &commit_data)?;
42/// agent.ensure_running()?;
43/// agent.add_identity("git", &pkcs8_der_bytes)?;
44/// ```
45pub trait AgentSigningPort: Send + Sync + 'static {
46 /// Attempt to sign `data` via the running agent.
47 ///
48 /// Returns an SSHSIG PEM string on success. The adapter is responsible for
49 /// converting raw signature bytes from the agent wire protocol into PEM.
50 ///
51 /// Args:
52 /// * `namespace`: The SSH namespace for the signature (e.g. `"git"`).
53 /// * `pubkey`: The public key identifying the signing key (carries curve type).
54 /// * `data`: The raw bytes to sign.
55 fn try_sign(
56 &self,
57 namespace: &str,
58 pubkey: &auths_verifier::DevicePublicKey,
59 data: &[u8],
60 ) -> Result<String, AgentSigningError>;
61
62 /// Start the agent daemon if it is not already running.
63 ///
64 /// Implementations should suppress all stdout/stderr output (quiet mode).
65 fn ensure_running(&self) -> Result<(), AgentSigningError>;
66
67 /// Load a decrypted PKCS#8 DER-encoded keypair into the running agent.
68 ///
69 /// Args:
70 /// * `namespace`: The namespace to associate with the loaded key.
71 /// * `pkcs8_der`: Decrypted PKCS#8 DER bytes — the output of keychain
72 /// decryption (i.e. `decrypt_keypair()` result), not a raw seed or
73 /// encrypted blob.
74 fn add_identity(&self, namespace: &str, pkcs8_der: &[u8]) -> Result<(), AgentSigningError>;
75}
76
77/// Transport abstraction for the agent listener.
78///
79/// Separates the connection-acceptance mechanism (Unix socket, TCP, in-process
80/// channel) from the signing state machine (`AgentCore` / `AgentHandle`).
81/// The CLI provides a Unix socket implementation; tests can use a direct
82/// in-process adapter that bypasses IPC entirely.
83///
84/// Usage:
85/// ```ignore
86/// struct InProcessTransport { handle: Arc<AgentHandle> }
87///
88/// impl AgentTransport for InProcessTransport {
89/// fn serve(&self) -> Result<(), AgentTransportError> {
90/// // Drive the handle directly without a socket
91/// Ok(())
92/// }
93/// fn is_available(&self) -> bool { true }
94/// fn socket_path(&self) -> Option<&Path> { None }
95/// }
96/// ```
97pub trait AgentTransport: Send + Sync + 'static {
98 /// Start accepting connections and serving agent requests.
99 ///
100 /// This method blocks until the agent is shut down. Implementations
101 /// should delegate to the `AgentHandle` for key operations.
102 ///
103 /// Args: none (connection details are configured at construction time).
104 fn serve(&self) -> Result<(), AgentTransportError>;
105
106 /// Check whether the transport backend is available on this platform.
107 ///
108 /// Returns `false` on platforms that lack Unix domain socket support
109 /// or when the required runtime dependencies are missing.
110 fn is_available(&self) -> bool;
111
112 /// Return the filesystem path of the listener socket, if applicable.
113 ///
114 /// In-process transports return `None`.
115 fn socket_path(&self) -> Option<&std::path::Path>;
116}
117
118/// Errors from agent transport operations.
119#[derive(Debug, thiserror::Error)]
120#[non_exhaustive]
121pub enum AgentTransportError {
122 /// The transport binding failed (e.g. socket already in use).
123 #[error("bind failed: {0}")]
124 BindFailed(String),
125
126 /// An I/O error during connection handling.
127 #[error("transport I/O error: {0}")]
128 Io(#[from] std::io::Error),
129
130 /// The agent handle reported an error.
131 #[error("agent error: {0}")]
132 AgentError(String),
133}
134
135/// No-op agent provider for platforms without agent support.
136///
137/// All methods return [`AgentSigningError::Unavailable`].
138pub struct NoopAgentProvider;
139
140impl AgentSigningPort for NoopAgentProvider {
141 fn try_sign(
142 &self,
143 _namespace: &str,
144 _pubkey: &auths_verifier::DevicePublicKey,
145 _data: &[u8],
146 ) -> Result<String, AgentSigningError> {
147 Err(AgentSigningError::Unavailable(
148 "agent not supported on this platform".into(),
149 ))
150 }
151
152 fn ensure_running(&self) -> Result<(), AgentSigningError> {
153 Err(AgentSigningError::Unavailable(
154 "agent not supported on this platform".into(),
155 ))
156 }
157
158 fn add_identity(&self, _namespace: &str, _pkcs8_der: &[u8]) -> Result<(), AgentSigningError> {
159 Err(AgentSigningError::Unavailable(
160 "agent not supported on this platform".into(),
161 ))
162 }
163}