/ packages / auths-python / src / commit_sign.rs
commit_sign.rs
 1  use std::path::PathBuf;
 2  use std::sync::Arc;
 3  
 4  use auths_core::signing::PrefilledPassphraseProvider;
 5  use auths_core::storage::keychain::get_platform_keychain_with_config;
 6  use auths_sdk::workflows::signing::{
 7      CommitSigningContext, CommitSigningParams, CommitSigningWorkflow,
 8  };
 9  use pyo3::exceptions::PyRuntimeError;
10  use pyo3::prelude::*;
11  
12  use crate::identity::{make_keychain_config, resolve_passphrase};
13  
14  #[pyclass]
15  #[derive(Clone)]
16  pub struct PyCommitSignResult {
17      #[pyo3(get)]
18      pub signature_pem: String,
19      #[pyo3(get)]
20      pub method: String,
21      #[pyo3(get)]
22      pub namespace: String,
23  }
24  
25  #[pymethods]
26  impl PyCommitSignResult {
27      fn __repr__(&self) -> String {
28          let pem_preview = if self.signature_pem.len() > 40 {
29              format!("{}...", &self.signature_pem[..40])
30          } else {
31              self.signature_pem.clone()
32          };
33          format!(
34              "CommitSignResult(method='{}', pem='{}')",
35              self.method, pem_preview,
36          )
37      }
38  }
39  
40  /// Sign git commit/tag data, producing an SSHSIG PEM signature.
41  ///
42  /// Uses a 3-tier fallback: ssh-agent -> auto-start -> direct signing.
43  /// In headless environments (Python SDK), falls through to direct signing.
44  ///
45  /// Args:
46  /// * `data`: The raw commit or tag bytes to sign.
47  /// * `identity_key_alias`: Keychain alias for the identity key.
48  /// * `repo_path`: Path to the auths repository.
49  /// * `passphrase`: Optional passphrase for the keychain.
50  ///
51  /// Usage:
52  /// ```ignore
53  /// let result = sign_commit(py, b"commit data", "main", "~/.auths", None)?;
54  /// ```
55  #[pyfunction]
56  #[pyo3(signature = (data, identity_key_alias, repo_path, passphrase=None))]
57  pub fn sign_commit(
58      py: Python<'_>,
59      data: &[u8],
60      identity_key_alias: &str,
61      repo_path: &str,
62      passphrase: Option<String>,
63  ) -> PyResult<PyCommitSignResult> {
64      let passphrase_str = resolve_passphrase(passphrase);
65      let env_config = make_keychain_config(&passphrase_str);
66      let provider = Arc::new(PrefilledPassphraseProvider::new(&passphrase_str));
67  
68      let keychain = get_platform_keychain_with_config(&env_config)
69          .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_KEYCHAIN_ERROR] Keychain error: {e}")))?;
70      let keychain = Arc::from(keychain);
71  
72      let repo = PathBuf::from(shellexpand::tilde(repo_path).as_ref());
73  
74      let params =
75          CommitSigningParams::new(identity_key_alias, "git", data.to_vec()).with_repo_path(repo);
76  
77      let signing_ctx = CommitSigningContext {
78          key_storage: keychain,
79          passphrase_provider: provider,
80          agent_signing: Arc::new(auths_sdk::ports::agent::NoopAgentProvider),
81      };
82  
83      #[allow(clippy::disallowed_methods)] // Presentation boundary
84      let now = chrono::Utc::now();
85  
86      py.allow_threads(move || {
87          let pem = CommitSigningWorkflow::execute(&signing_ctx, params, now)
88              .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_SIGNING_FAILED] Commit signing failed: {e}")))?;
89  
90          Ok(PyCommitSignResult {
91              signature_pem: pem,
92              method: "direct".to_string(),
93              namespace: "git".to_string(),
94          })
95      })
96  }