Skip to main content

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}