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
60 changes: 60 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,52 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Changed

- **One confirmation rule** ([#579](https://github.com/OpenVTC/openvtc/issues/579)). A destructive or outward-facing action
is armed by its key and committed by `y` alone; Enter — the key that opened the view —
now cancels like any other key. Every prompt ends "y: confirm · any other key:
cancel". Affected: community delete/leave/cancel-join, device-access revoke, Inbox
dismiss/clear-all, relationship and credential remove, invitation credential
delete/purge, agent-name remove, persona deletions, capability toggle, vetting
abandon/delete-ticket/withdraw/decline, local-face delete, make-reusable and
forget-contact. Typed-word confirmation stays for WIPE, RESET and context deletion.
- **Now confirmed** ([#579](https://github.com/OpenVTC/openvtc/issues/579)): Inbox `r` (the rejection is sent to the peer),
Settings "Remove the unlock code", and Communities `m` (issue VMC) and `R` (renew).
- **A key table per view, a `?` overlay, feedback for unused keys**
([#580](https://github.com/OpenVTC/openvtc/issues/580)). Every main-page footer is drawn from the view's key table in one
format (`key: does · key: does`, one key vocabulary); `?` lists exactly the focused
view's keys and the global ones; a key a view does not take says "x does nothing here
— ? lists this panel's keys" in the footer row. Help / Status's shortcut list comes
from the same table.
- **Consistent keys** ([#580](https://github.com/OpenVTC/openvtc/issues/580)): archive is `x` on the VTA invitation list (was
`r`), show archived is `v` there (was `i`), refresh is `r` (was Tab); Communities
joins with `n` (was `j`), and `j`/`k` move there and in the menu, as in rooms and
elections. TSP Relationships takes ↑/↓ and Esc.
- **Text fields have a cursor** ([#578](https://github.com/OpenVTC/openvtc/issues/578)). Every main-page form field is edited
through `tui_input`: ←/→, Ctrl+←/→ and Home/End move within the field (they no longer
switch panels), Backspace/Del delete either side, Ctrl+A/E/U/W/K edit, and no Ctrl or
Alt chord types its letter. A paste is inserted at the cursor on the main page and in
setup (it was appended on one and replaced the whole field on the other); the typed
WIPE/RESET/DELETE words still take no paste.
- **The header names the class of a messaging failure** ([#585](https://github.com/OpenVTC/openvtc/issues/585)): "✗ mediator
unreachable", "mediator refused us", "mediator incompatible", "mediator not answering"
… with "see Help". Help / Status shows the class, the full reason and what to do,
and `r` there reconnects; so does Enter on the Settings mediator row. "Messaging
unavailable" messages name the class and the remedy. The identity pane offers the
holder-grant command only for a refusal, never for an unreachable agent.
- **Notifications** ([#584](https://github.com/OpenVTC/openvtc/issues/584)): the Inbox badge counts what was sent to you, not
your own pings and requests; Communities and Vetting carry badges of their own; the
activity log draws errors and warnings in colour with a glyph.
- **Setup: Esc goes back** ([#588](https://github.com/OpenVTC/openvtc/issues/588)) on every wizard page that has a page before
it, keeping what was typed (it used to clear the field); choice pages say so. The
setup DID is kept for the whole run, so going back and forward does not invalidate a
PNM grant. The boxes no longer carry their own "Step x/y" counters beside the
header's sections.
- **Wording** ([#554](https://github.com/OpenVTC/openvtc/issues/554)): British spelling in the UI strings that were American;
the profile's secret is the "unlock code" in Settings and the CLI as in setup; README
glossary and CONTRIBUTING wording convention.
- **Status lines are typed** ([#577](https://github.com/OpenVTC/openvtc/issues/577)): every panel's status carries a severity
set by the code that produced it; the colour no longer comes from the wording.

- **Inbound dispatch and the join protocol live in `openvtc-core`**
([#530](https://github.com/OpenVTC/openvtc/issues/530)). `process_inbound_message` and
what it reports (`InboundEffects`, the off-loop `VerifyJob`s) moved from the CLI to
Expand Down Expand Up @@ -104,6 +150,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Fixed

- **Nothing essential is clipped at 80x24** ([#576](https://github.com/OpenVTC/openvtc/issues/576)): all twelve menu items fit
(the activity log shrinks to 4 rows below 32 rows; the menu scrolls to the selection
on anything shorter, and is wide enough for its longest item); the footer row leads
with `F10: quit` and `?: keys` and keeps only whole hints; setup step 1 scrolls to
every PNM command and shows the copy result in its footer row.
- **A ping reports its result** ([#584](https://github.com/OpenVTC/openvtc/issues/584)) in the Relationships panel — the pong
and its latency, or "No reply to the ping after 30 s" — and an unanswered ping's
outbound task is expired instead of sitting in the Inbox.
- **Keys that did nothing silently now say so, and keys a footer did not list are gone
or listed** ([#580](https://github.com/OpenVTC/openvtc/issues/580)): e.g. Inbox `a` on a task with nothing to accept, `d` on
a membership credential, VTA `u` on an active invitation, persona `d` on a persona in
use, letters in the identity pane's read-only views and pickers.
- **The masked passphrase length counts characters**, not bytes ([#578](https://github.com/OpenVTC/openvtc/issues/578)).

- **Setup and backup import refuse an unlock code shorter than 8 characters**
([#574](https://github.com/OpenVTC/openvtc/issues/574)). Every unlock path already
required 8, so a profile set up with a shorter code could never be opened again. A
Expand Down
13 changes: 13 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,19 @@ cargo fmt --all --check
cargo clippy --workspace --all-targets -- -D warnings
```

### Wording in the UI

- **British spelling** in user-visible strings (`colour`, `recognised`,
`authorise`, `initialising`), as the newer code already uses. Protocol,
SDK and wire text keeps its own spelling (`"kind": "organization"`, a VTA
error matched verbatim).
- **One name per concept**, as in the README's glossary: *persona*,
*community*, *unlock code*, *Inbox*, *Help / Status*.
- **Keys** are named with one vocabulary — `Enter`, `Esc`, `Tab`,
`Shift+Tab`, `↑/↓`, `←/→`, `Space`, `Del`, `Ctrl+X` — and a view's keys
are declared as a `keymap::Binding` list, never typed into a footer
string (`openvtc/src/ui/pages/main/keymap.rs`).

### Commit Messages

- Use [Conventional Commits](https://www.conventionalcommits.org/) style: `feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `chore:`
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ The `did-git-sign` commit signer lives in

- [Quickstart](#quickstart)
- [Core Concepts](#core-concepts)
- [Glossary](#glossary)
- [Decentralised Identity](#decentralised-identity)
- [Decentralised Communication](#decentralised-communication)
- [Profiles and Configurations](#profiles-and-configurations)
Expand Down Expand Up @@ -79,6 +80,23 @@ openvtc --help

- **DIDComm Messaging Protocol** - An open standard for decentralised communication. Built on the foundation of Decentralised Identifiers (DIDs), it enables parties to exchange verifiable data such as credentials and establishes secure communication channels between parties without relying on centralised servers.

## Glossary

The TUI uses one name for each of these:

- **Persona** — an identity you hold: a DID your VTA keeps the keys for. The
*My Identity* panel manages your personas (and their attributes, faces and
worlds).
- **Community** — a Verifiable Trust Community (VTC) you belong to or are
joining. The UI says *community*; *VTC* names the service behind it.
- **Unlock code** — the secret that protects your profile on this machine
(`--unlock-code`; *Settings → Protection*). An export file's *passphrase*
is a different secret, chosen per export.
- **Inbox** — requests and credentials sent to you, waiting for an answer
(the docs' *tasks*).
- **Help / Status** — connection status, your DIDs and the keyboard
shortcuts; `?` on any panel lists that panel's keys.

## Decentralised Identity

OpenVTC uses `did:webvh` for your **persona DID**. It enhances the existing `did:web` method, providing:
Expand Down
19 changes: 16 additions & 3 deletions openvtc-core/src/didcomm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1078,7 +1078,12 @@ pub const DIDCOMM_EVENT_CHANNEL_CAPACITY: usize = 256;
/// without this helper having to know about the outer state shape.
pub enum ReconnectOutcome {
Connected,
Failed(String),
/// The reconnect failed: why, and which class of failure (R6.4) — so the
/// caller can tell an unreachable mediator from a refused one.
Failed {
reason: String,
class: crate::errors::ErrorClass,
},
}

/// Replace the persona listener and wait for it to come up. Used by the
Expand Down Expand Up @@ -1113,14 +1118,22 @@ pub async fn reconnect_persona_listener_io(
// failure this codebase has already been bitten by (#132).
service.remove_listener(&listener_id).await;
if let Err(e) = add_listener(service, &new_config).await {
return ReconnectOutcome::Failed(format!("{e:#}"));
return ReconnectOutcome::Failed {
reason: format!("{e:#}"),
class: crate::errors::class_of(&e),
};
}
match service
.wait_connected(&listener_id, std::time::Duration::from_secs(30))
.await
{
Ok(()) => ReconnectOutcome::Connected,
Err(e) => ReconnectOutcome::Failed(e),
// The listener never came up: from here that is a mediator that did
// not answer in time, whatever its last state.
Err(e) => ReconnectOutcome::Failed {
reason: e,
class: crate::errors::ErrorClass::Timeout,
},
}
}

Expand Down
8 changes: 4 additions & 4 deletions openvtc/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,18 +154,18 @@ pub fn read_unlock_code_file(path: &str) -> anyhow::Result<zeroize::Zeroizing<St
std::io::stdin()
.lock()
.read_line(&mut first_line)
.context("failed to read the unlock passphrase from standard input")?;
.context("failed to read the unlock code from standard input")?;
} else {
let file = std::fs::File::open(path)
.with_context(|| format!("failed to open unlock passphrase file `{path}`"))?;
.with_context(|| format!("failed to open unlock code file `{path}`"))?;
std::io::BufReader::new(file)
.read_line(&mut first_line)
.with_context(|| format!("failed to read unlock passphrase file `{path}`"))?;
.with_context(|| format!("failed to read unlock code file `{path}`"))?;
}

let passphrase = zeroize::Zeroizing::new(first_line.trim().to_string());
if passphrase.is_empty() {
anyhow::bail!("no unlock passphrase on the first line of `{path}`");
anyhow::bail!("no unlock code on the first line of `{path}`");
}
Ok(passphrase)
}
Expand Down
6 changes: 3 additions & 3 deletions openvtc/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -977,12 +977,12 @@ fn prompt_unlock_code(profile: &str) -> Result<UnlockCode, OpenVTCError> {
// Wiped on drop. (dialoguer's own line buffer is beyond reach.)
let input = zeroize::Zeroizing::new(
Password::with_theme(&ColorfulTheme::default())
.with_prompt("Please enter unlock passphrase")
.with_prompt("Please enter your unlock code")
.allow_empty_password(false)
.interact()
.map_err(|e| {
eprintln!("Failed to read passphrase input: {e}");
OpenVTCError::Config(format!("Passphrase input failed: {e}"))
eprintln!("Failed to read the unlock code: {e}");
OpenVTCError::Config(format!("Unlock code input failed: {e}"))
})?,
);
let code = UnlockCode::for_existing(&input)?;
Expand Down
27 changes: 22 additions & 5 deletions openvtc/src/state_handler/actions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ pub enum InboxAction {
},
/// Arm the confirmation for clearing all tasks (R25).
ConfirmClearAll,
/// Arm the confirmation for rejecting an inbound request (#579): a
/// rejection is sent to the peer, so it confirms like a removal does.
/// `vrc` is a VRC request; otherwise a relationship request.
ConfirmReject {
task_id: String,
vrc: bool,
},
/// Cancel a pending dismiss/clear-all confirmation (R25).
CancelConfirm,
Back,
Expand Down Expand Up @@ -207,9 +214,8 @@ pub enum SettingsAction {
passphrase: SecretString,
},
RemovePassphrase,
// Manual mediator-reconnect path: handler is live, but the Settings UI does
// not yet expose a control that emits it.
#[allow(dead_code)]
/// Reconnect messaging: Enter on the Settings mediator row, or `r` on
/// Help / Status.
ReconnectMediator,
/// Open the wipe-profile confirmation dialog from the Settings menu.
WipeProfileStart,
Expand All @@ -233,7 +239,7 @@ pub enum SettingsAction {
#[cfg(feature = "openpgp-card")]
TokenBack,
/// Clipboard copy result message for display on the status panel.
ClipboardCopied(String),
ClipboardCopied(crate::state_handler::main_page::status::Status),
/// Open the theme picker.
ThemeOpen,
/// Highlight — and preview — the theme at this row.
Expand Down Expand Up @@ -410,7 +416,7 @@ pub enum VettingAction {
/// Commit the open form.
Submit,
/// Show a status line (e.g. a clipboard result).
Status(String),
Status(crate::state_handler::main_page::status::Status),
/// Pasted text that is a `vetting-ticket:` link, for the request form.
PasteTicket(String),
// ── Applicant ────────────────────────────────────────────────────────
Expand Down Expand Up @@ -559,6 +565,8 @@ pub enum ElectionsAction {
BallotAdjust(i64),
/// Ballot: a character typed into a text answer.
BallotChar(char),
/// Ballot: the focused text answer, as edited (cursor editing, paste).
BallotText(String),
/// Ballot: delete the last character of a text answer.
BallotBackspace,
/// Ballot: leave the focused (optional) question blank, or answer it again.
Expand Down Expand Up @@ -935,6 +943,9 @@ pub enum Action {
/// Move the Communities-list selection to this index.
CommunitySelect(usize),

/// Move the TSP Relationships cursor to this row.
TspSelect(usize),

/// Make the Active community at this index the working context (D10 / R-C-6):
/// the community-scoped main page (relationships/inbox/VRCs/identity) switches
/// to its persona. No-op for a non-Active (read-only) community.
Expand Down Expand Up @@ -979,6 +990,12 @@ pub enum Action {
/// Dismiss a pending cancel confirmation without withdrawing.
CommunityCancelWithdraw,

/// Arm the confirmation for a message to a community (issue VMC, renew).
CommunityConfirmSend(crate::state_handler::main_page::content::CommunitySend),

/// Dismiss a pending send confirmation without sending.
CommunityCancelSend,

/// Cancel the Pending join at this index: best-effort notify the VTC, then
/// set it `Withdrawn` and deregister its session so it can be deleted or
/// re-joined. Only sent after the user confirms.
Expand Down
26 changes: 20 additions & 6 deletions openvtc/src/state_handler/background_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
//! R13 migrates the mediator change/reconnect path as the single proving case;
//! R14 migrates the remaining network dispatches onto the same mechanism.

use crate::state_handler::main_page::status::Status;
use openvtc_core::config::Config;

use crate::state_handler::didcomm::ReconnectOutcome;
Expand Down Expand Up @@ -523,7 +524,8 @@ pub(crate) fn apply_outcome(
}
}
ProgressUpdate::Witness(step) => {
state.main_page.content_panel.relationships.status_message = Some(step);
state.main_page.content_panel.relationships.status_message =
Some(Status::progress(step));
}
}
return AfterApply::Nothing;
Expand All @@ -536,9 +538,15 @@ pub(crate) fn apply_outcome(
state.connection.messaging_active = true;
state.main_page.log("Reconnected to mediator");
}
ReconnectOutcome::Failed(reason) => {
state.connection.status = state::MediatorStatus::Failed(reason.clone());
state.main_page.log(format!("Reconnect failed: {reason}"));
ReconnectOutcome::Failed { reason, class } => {
let failure = state::ConnectionFailure::new(reason, class);
state.main_page.log_warning(format!(
"Reconnect failed ({}): {} {}",
failure.short(),
failure.reason,
class.remedy()
));
state.connection.status = state::MediatorStatus::Failed(failure);
}
},
DispatchOutcome::Relationship(outcome) => outcome.apply(state, config, save),
Expand Down Expand Up @@ -795,11 +803,17 @@ mod tests {
&mut config,
&mut save,
&mut in_flight,
DispatchOutcome::MediatorReconnect(ReconnectOutcome::Failed("dead mediator".into())),
DispatchOutcome::MediatorReconnect(ReconnectOutcome::Failed {
reason: "dead mediator".into(),
class: openvtc_core::errors::ErrorClass::Unreachable,
}),
);

match &state.connection.status {
state::MediatorStatus::Failed(reason) => assert_eq!(reason, "dead mediator"),
state::MediatorStatus::Failed(f) => {
assert_eq!(f.reason, "dead mediator");
assert_eq!(f.short(), "mediator unreachable");
}
other => panic!("expected Failed status, got {other:?}"),
}
assert!(!state.connection.messaging_active);
Expand Down
17 changes: 11 additions & 6 deletions openvtc/src/state_handler/capability_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
//! pending thread, was dropped, and the view sat out the 30 s reply window to
//! report a host that had in fact answered.

use crate::state_handler::main_page::status::Status;
use std::sync::Arc;
use std::time::Instant;

Expand Down Expand Up @@ -195,10 +196,10 @@ fn arm(
view.pending_thid = Some(thid.to_string());
view.sent_at = Some(Instant::now());
if let Some((slug, enable)) = toggle {
view.status_message = Some(format!(
view.status_message = Some(Status::progress(format!(
"{} {slug}… awaiting the community's reply",
if enable { "enabling" } else { "disabling" }
));
)));
}
}

Expand Down Expand Up @@ -245,7 +246,7 @@ impl CapabilityOutcome {
// A failed *write* keeps the list on screen — it is still valid,
// the change simply did not go out.
Some(_) => {
view.status_message = Some(format!("couldn't send the change: {e}"));
view.status_message = Some(Status::error(format!("couldn't send the change: {e}")));
tracing::error!("capability toggle failed: {e}");
}
// A failed *query* has nothing to show, so the view says so.
Expand Down Expand Up @@ -422,7 +423,10 @@ mod tests {

let v = view(&state);
assert!(v.pending_thid.is_none());
assert_eq!(v.status_message.as_deref(), Some("chat is now enabled"));
assert_eq!(
v.status_message.as_ref().map(|s| s.text.as_str()),
Some("chat is now enabled")
);
}

/// A query that never left has nothing to show, so the view fails outright.
Expand Down Expand Up @@ -477,7 +481,8 @@ mod tests {
);
assert!(
v.status_message
.as_deref()
.as_ref()
.map(|s| s.text.as_str())
.is_some_and(|m| m.contains("couldn't send")),
"{:?}",
v.status_message
Expand Down Expand Up @@ -531,7 +536,7 @@ mod tests {

let v = view(&state);
assert_eq!(
v.status_message.as_deref(),
v.status_message.as_ref().map(|s| s.text.as_str()),
Some("only the community's administrators can enable or disable capabilities")
);
assert!(
Expand Down
Loading
Loading