1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
//! Python bindings for murk via PyO3.
//!
//! ```python
//! import murk
//!
//! vault = murk.load() # reads MURK_KEY from env, .murk from cwd
//! vault.get("DATABASE_URL") # decrypt a single value
//! vault.export() # dict of all key/values
//! murk.get("DATABASE_URL") # one-liner convenience
//! ```
use std::collections::HashMap;
use pyo3::exceptions::PyRuntimeError;
use pyo3::prelude::*;
use crate::{env, export, policy, types};
/// A loaded and decrypted murk vault.
#[pyclass]
struct Vault {
inner: types::Vault,
decrypted: types::Murk,
pubkey: String,
}
#[pymethods]
impl Vault {
/// Get a single decrypted secret value. Resolution order: a personal scoped
/// override, then a named-group value we can read, then the shared value.
///
/// The returned `String` is a plain Python-owned copy — once it crosses
/// the FFI boundary the plaintext is outside murk's zeroization.
///
/// When the loaded identity is a granted agent, the vault's agent policy is
/// enforced before the value is returned — the same gate the CLI applies at
/// `agent exec`. Raises `RuntimeError` if policy forbids the key. For an
/// operator identity this is a no-op.
fn get(&self, key: &str) -> PyResult<Option<String>> {
let value = crate::get_secret(&self.decrypted, key, &self.pubkey).map(str::to_string);
// Only enforce when there is a value to hand back: a key the agent
// cannot decrypt is already inaccessible, so policy is moot.
if value.is_some() {
policy::enforce_agent_policy(
&self.inner,
&self.decrypted,
&self.pubkey,
&[key.to_string()],
)
.map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
}
Ok(value)
}
/// Export all secrets as a dict. Scoped values override shared values.
///
/// For a granted agent, the vault's agent policy is enforced over the full
/// key set first (mirroring `murk agent exec`): if any resolvable key is
/// outside the policy, the whole export raises `RuntimeError` rather than
/// returning a partial dict. For an operator identity this is a no-op.
fn export(&self) -> PyResult<HashMap<String, String>> {
let resolved = export::resolve_secrets(&self.inner, &self.decrypted, &self.pubkey, &[]);
let keys: Vec<String> = resolved.keys().cloned().collect();
policy::enforce_agent_policy(&self.inner, &self.decrypted, &self.pubkey, &keys)
.map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
// Python dicts own plain Strings — zeroization ends at the FFI boundary.
Ok(resolved
.into_iter()
.map(|(k, v)| (k, v.to_string()))
.collect())
}
/// List all key names.
fn keys(&self) -> Vec<String> {
self.inner.schema.keys().cloned().collect()
}
/// Number of secrets in the vault.
fn __len__(&self) -> usize {
self.inner.schema.len()
}
/// Get a value by key (dict-style access).
fn __getitem__(&self, key: &str) -> PyResult<String> {
self.get(key)?
.ok_or_else(|| PyRuntimeError::new_err(format!("key not found: {key}")))
}
/// Check if a key exists.
fn __contains__(&self, key: &str) -> bool {
self.inner.schema.contains_key(key)
}
fn __repr__(&self) -> String {
format!(
"Vault({} secrets, {} recipients)",
self.inner.schema.len(),
self.inner.recipients.len()
)
}
}
/// Load a murk vault. Reads MURK_KEY from the environment.
#[pyfunction]
#[pyo3(signature = (vault_path=".murk"))]
fn load(vault_path: &str) -> PyResult<Vault> {
let (vault, murk, identity) =
crate::load_vault(vault_path).map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
let pubkey = identity
.pubkey_string()
.map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
Ok(Vault {
inner: vault,
decrypted: murk,
pubkey,
})
}
/// One-liner: load the vault and get a single key.
#[pyfunction]
#[pyo3(signature = (key, vault_path=".murk"))]
fn get(key: &str, vault_path: &str) -> PyResult<Option<String>> {
let v = load(vault_path)?;
v.get(key)
}
/// One-liner: load the vault and export all secrets as a dict.
#[pyfunction]
#[pyo3(signature = (vault_path=".murk"))]
fn export_all(vault_path: &str) -> PyResult<HashMap<String, String>> {
let v = load(vault_path)?;
v.export()
}
/// Whether a decryption identity (MURK_KEY / MURK_KEY_FILE) is available in the
/// environment — i.e. whether load() can decrypt. This does not check whether a
/// secret exists; use `key in vault` / Vault.keys for that.
#[pyfunction]
fn has_identity() -> bool {
env::resolve_key().is_ok()
}
/// Python module definition.
#[pymodule]
fn murk(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<Vault>()?;
m.add_function(wrap_pyfunction!(load, m)?)?;
m.add_function(wrap_pyfunction!(get, m)?)?;
m.add_function(wrap_pyfunction!(export_all, m)?)?;
m.add_function(wrap_pyfunction!(has_identity, m)?)?;
Ok(())
}