Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,39 @@ range may break in any release.

### Added

- **M5 (slice 3) — TUI search, generator overlay, and `:` command line.** The
three previewed keys go live: `/` filters the item list as you type, `g`
opens a password-generator overlay, and `:` opens a small vim-style command
line.
- **Search (`/`).** Live, case-insensitive substring match on item name and
username, composed on top of the active folder filter. Enter accepts the
query (filter stays, shown in the `Items (n) /query` pane title), Esc in
search mode drops it, and Esc in normal mode peels an active filter back
before quitting. Every query edit re-anchors the selection and re-masks any
revealed secret. Arrow keys still move the selection mid-search.
- **Generator (`g`).** A centered overlay over the browser showing a freshly
generated password (`vault-core`'s `generate_password`, same engine as
`vault generate`): `g`/`r` regenerate, `+`/`-` adjust length (clamped
8–128, Bitwarden's ceiling), `s` toggles symbols, `c` copies, `Esc` closes.
The password lives in a `GeneratorState` (zeroised on drop, redacted in
`Debug`).
- **Copying a generated password** uses a new `Request::CopyText { text,
clear_after_secs }`: the value rides the local UDS once (exactly like
`Unlock`'s password already does), and the agent writes it to its own
clipboard with the same 30-second auto-clear machinery as `Request::Copy`.
Requires an unlocked agent; headless (`--no-default-features`) builds
decline it cleanly.
- **Command line (`:`).** Deliberately tiny vocabulary: `q`/`quit`,
`r`/`refresh`, `sync` (agent re-pulls `/sync`, list reloads), `lock` (agent
drops keys, screen flips to the Locked banner). Unknown commands toast the
vocabulary. The status bar echoes the line being edited (`/query▌` /
`:cmd▌`) ahead of toasts and hints.
- Tests: `vault-tui` adds search/compose/re-anchor, command-buffer, and
generator (defaults, regenerate, clamp, symbols, `Debug`-redaction) units
plus `TestBackend` smokes for the query title/status echo, command echo,
and generator overlay; `vault-agent`'s locked-session test now covers
`CopyText`-while-locked. No new dependencies.

- **M5 (slice 2) — TUI reveal + clipboard copy.** The detail pane is no longer
secret-free: `Space` reveals the selected login's password on demand and
`c` / `u` / `o` copy the password / username / URI to the clipboard, with a
Expand Down
51 changes: 51 additions & 0 deletions crates/vault-agent/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,43 @@ async fn dispatch(req: Request, state: &Arc<Mutex<AgentState>>) -> Response {
Request::Copy { .. } => Response::Error(vault_ipc::proto::Error::Internal(
"clipboard support not compiled in".to_owned(),
)),
#[cfg(feature = "clipboard")]
Request::CopyText {
text,
clear_after_secs,
} => {
// The wrapper zeroises the inbound bytes no matter which way the
// arm exits; `value` is the copy the clear task carries.
let text = zeroize::Zeroizing::new(text);
let mut s = state.lock().await;
let outcome = if s.is_unlocked() {
std::str::from_utf8(&text)
.map_err(|e| {
vault_ipc::proto::Error::Internal(format!(
"copy text is not valid UTF-8: {e}"
))
})
.and_then(|v| {
let value = zeroize::Zeroizing::new(v.to_owned());
s.clipboard_set(&value).map(|()| value)
})
} else {
Err(vault_ipc::proto::Error::Locked)
};
s.touch();
drop(s);
match outcome {
Ok(value) => {
schedule_clipboard_clear(state.clone(), value, clear_after_secs);
Response::Ok
}
Err(e) => Response::Error(e),
}
}
#[cfg(not(feature = "clipboard"))]
Request::CopyText { .. } => Response::Error(vault_ipc::proto::Error::Internal(
"clipboard support not compiled in".to_owned(),
)),
Request::Remove { selector } => {
// Hold the agent mutex across the network call. Vault is
// single-user / single-agent, so request concurrency is low and
Expand Down Expand Up @@ -364,6 +401,20 @@ mod tests {
let resp: Response = read_frame(&mut rd).await.unwrap();
assert!(matches!(resp, Response::Error(_)));

// CopyText-while-locked must likewise decline before touching the
// clipboard (Locked with the feature, "not compiled in" without).
write_frame(
&mut wr,
&Request::CopyText {
text: b"generated-password".to_vec(),
clear_after_secs: Some(0),
},
)
.await
.unwrap();
let resp: Response = read_frame(&mut rd).await.unwrap();
assert!(matches!(resp, Response::Error(_)));

write_frame(&mut wr, &Request::Quit).await.unwrap();
let resp: Response = read_frame(&mut rd).await.unwrap();
assert!(matches!(resp, Response::Ok));
Expand Down
16 changes: 16 additions & 0 deletions crates/vault-ipc/src/proto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,22 @@ pub enum Request {
clear_after_secs: Option<u64>,
},

/// Place caller-supplied text on the agent's clipboard with the same
/// timed auto-clear as [`Request::Copy`].
///
/// This is the copy path for values that don't live in the vault — e.g. a
/// freshly generated password the user hasn't saved yet. The plaintext
/// rides the local UDS exactly like `Unlock`'s password does, and the
/// agent zeroises it after the clipboard write. Requires an unlocked
/// agent, mirroring every other data verb.
CopyText {
/// The value to copy; secret, wiped by the agent after use.
text: Vec<u8>,
/// Seconds before the agent clears the clipboard; `None` uses the
/// agent default, `Some(0)` disables auto-clear.
clear_after_secs: Option<u64>,
},

/// Soft-delete a cipher by id or decrypted name.
///
/// `selector` is matched against `Cipher.id` first (exact); if no id
Expand Down
Loading
Loading