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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
//! Thin client that routes CLI commands through a running daemon when one is
//! up, falling back to the file session otherwise. The functions return an
//! `Option`: `Some(..)` means the daemon handled it, `None` means "no daemon —
//! fall back to the file-session path." On non-Unix everything returns `None`.
/// Result of asking the daemon for a secret (human path).
pub enum GetOutcome {
/// The daemon returned the value.
Value(String),
/// The daemon is up but the vault isn't unlocked — caller should fall back.
NotUnlocked,
/// The daemon is up and the vault unlocked, but the secret doesn't exist.
NotFound,
}
/// Result of a gated (agent-path) request to the daemon.
pub enum GatedOutcome {
/// Policy + judge allowed it; carries the value and tier.
Granted(String, crate::policy::Tier),
/// Policy or the AI judge denied it.
Denied(String),
/// Daemon up but the vault isn't unlocked — caller should fall back.
NotUnlocked,
/// Vault unlocked but the secret doesn't exist.
NotFound,
}
#[cfg(unix)]
mod imp {
use super::{GatedOutcome, GetOutcome};
use crate::daemon::{self, Request, Response};
use anyhow::{anyhow, Result};
use std::path::PathBuf;
fn base() -> PathBuf {
daemon::base_dir()
}
/// True when a daemon is running for this project.
pub fn available() -> bool {
daemon::is_running(&base())
}
/// Cache a vault's key in the daemon. `None` = no daemon (fall back).
///
/// The key is derived (and the passphrase validated) **client-side** by
/// opening the vault here; only the 32-byte derived key crosses the socket,
/// never the passphrase (finding #3). A wrong passphrase fails locally and
/// never reaches the daemon.
pub fn unlock(vault: &str, passphrase: &str) -> Option<Result<()>> {
if !available() {
return None;
}
let dir = base().join(vault);
let key_hex = match crate::vault::Vault::open(&dir, passphrase) {
Ok(v) => hex::encode(v.key().bytes()),
Err(e) => return Some(Err(e)),
};
let req = Request::Unlock {
vault: vault.to_string(),
key: key_hex,
};
Some(match daemon::send(&base(), &req) {
Ok(Response::Unlocked) => Ok(()),
Ok(Response::Error { message }) => Err(anyhow!(message)),
Ok(other) => Err(anyhow!("unexpected daemon response: {other:?}")),
Err(e) => Err(e),
})
}
/// Drop one vault's key. `None` = no daemon.
pub fn lock(vault: &str) -> Option<usize> {
if !available() {
return None;
}
match daemon::send(
&base(),
&Request::Lock {
vault: vault.to_string(),
},
) {
Ok(Response::Locked { count }) => Some(count),
_ => Some(0),
}
}
/// Drop every cached key. `None` = no daemon.
pub fn lock_all() -> Option<usize> {
if !available() {
return None;
}
match daemon::send(&base(), &Request::LockAll) {
Ok(Response::Locked { count }) => Some(count),
_ => Some(0),
}
}
/// Read a secret via the daemon. `None` = no daemon, or a protocol error
/// the caller should treat as "fall back".
pub fn get(vault: &str, secret: &str) -> Option<GetOutcome> {
if !available() {
return None;
}
let req = Request::Get {
vault: vault.to_string(),
secret: secret.to_string(),
};
match daemon::send(&base(), &req) {
Ok(Response::Secret { value }) => Some(GetOutcome::Value(value)),
Ok(Response::NotUnlocked) => Some(GetOutcome::NotUnlocked),
Ok(Response::NotFound) => Some(GetOutcome::NotFound),
_ => None,
}
}
/// Agent path: a gated request the daemon evaluates (policy + judge + audit).
/// `None` = no daemon, so the caller runs the same gate locally instead.
pub fn get_gated(
vault: &str,
secret: &str,
caller: &str,
scope: &str,
reason: &str,
) -> Option<GatedOutcome> {
if !available() {
return None;
}
let req = Request::GetGated {
vault: vault.to_string(),
secret: secret.to_string(),
caller: caller.to_string(),
scope: scope.to_string(),
reason: reason.to_string(),
};
match daemon::send(&base(), &req) {
Ok(Response::Granted { value, tier }) => Some(GatedOutcome::Granted(value, tier)),
Ok(Response::Denied { reason }) => Some(GatedOutcome::Denied(reason)),
Ok(Response::NotUnlocked) => Some(GatedOutcome::NotUnlocked),
Ok(Response::NotFound) => Some(GatedOutcome::NotFound),
_ => None,
}
}
/// Names of vaults currently unlocked in the daemon (empty if none / down).
pub fn unlocked_vaults() -> Vec<String> {
if !available() {
return Vec::new();
}
match daemon::send(&base(), &Request::Status) {
Ok(Response::Status { vaults }) => vaults.into_iter().map(|v| v.name).collect(),
_ => Vec::new(),
}
}
}
#[cfg(not(unix))]
mod imp {
use super::{GatedOutcome, GetOutcome};
use anyhow::Result;
pub fn unlock(_vault: &str, _passphrase: &str) -> Option<Result<()>> {
None
}
pub fn lock(_vault: &str) -> Option<usize> {
None
}
pub fn lock_all() -> Option<usize> {
None
}
pub fn get(_vault: &str, _secret: &str) -> Option<GetOutcome> {
None
}
pub fn get_gated(
_vault: &str,
_secret: &str,
_caller: &str,
_scope: &str,
_reason: &str,
) -> Option<GatedOutcome> {
None
}
pub fn unlocked_vaults() -> Vec<String> {
Vec::new()
}
}
pub use imp::{get, get_gated, lock, lock_all, unlock, unlocked_vaults};