SendMessage previously always sent from the first configured account
with no way to choose, so it silently used the wrong identity the
moment a second account existed. Add an optional account_id field to
SendMessageRequest; when set, resolve that specific account and fail
with a new AccountNotFound error if it does not exist, rather than
falling back to a different account. Omitting account_id preserves
the prior first-account behavior. Thread the selector through
ComposeRequest, resolve_sending_account, the CLI's mail send --account
flag, and regenerate the nuncio.v1 descriptor golden.
Refs #196, #170
Co-authored-by: James Maes <jmaes@dmdbrands.com>
* feat(store): add per-folder sync-state checkpoint table + CRUD
Adds folder_sync_state(account_id, folder_id, state) with
get/save methods and a round-trip test, giving inbound sync a place
to persist a per-mailbox resumption token between runs.
Refs #195#184
* feat(mail): honor since_state for incremental IMAP fetch + bound FETCH stalls
ImapEngine now derives a real UIDNEXT-based checkpoint after selecting a
folder and narrows the UID FETCH to {uidnext}:* when a checkpoint is
supplied, instead of always re-fetching 1:* and returning a state
fabricated from emails.len(). An unparseable checkpoint falls back to a
full fetch but is surfaced distinctly. Each per-item FETCH read is bounded
by a named timeout, surfacing MailError::FetchStalled instead of hanging.
MockMailBackend captures the since_state arg and simulates an incremental
(empty) second fetch. Session-level duplex tests assert the narrowed FETCH
range and the bounded stall.
Refs #195#184#183
* feat(sync): persist and thread the folder checkpoint, publish sync progress
fetch_and_persist now reads the folder's stored checkpoint, passes it to
the backend for a narrowed fetch, and persists the returned checkpoint only
after that folder's messages land -- so the second sync of an account/folder
is genuinely incremental. Adds CoreEvent::SyncProgress (per folder) as an
internal signal; map_core_event drops it from the gRPC stream since it has
no wire-contract case (proto unchanged).
Refs #195#184
* fix(mail): track UIDVALIDITY in sync checkpoint to prevent mail loss
A checkpoint stored only UIDNEXT, so a mailbox renumber (UIDVALIDITY
change, UIDs restart low) would drive {old_uidnext}:* against the new low
UIDs -- per RFC 3501 n:* returns only the single highest message and
silently skips the rest, permanently dropping mail. The checkpoint now
encodes {uidvalidity}:{uidnext}; the incremental narrowing applies only
when the stored UIDVALIDITY still matches the mailbox's current one. A
mismatch (renumber), a missing current UIDVALIDITY, or a legacy
bare-number/unparseable checkpoint falls back to a safe full 1:* re-fetch
with a debug-visible warn, then rewrites the checkpoint fresh.
Refs #195#184
---------
Co-authored-by: James Maes <jmaes@dmdbrands.com>
* feat(store): persist per-account IMAP/SMTP TLS modes; add account CRUD
Adds a backfill-safe imap_tls_mode/smtp_tls_mode column migration,
binds+reads both modes in save_account/list_accounts (previously
validated then silently discarded and hardcoded back to ImplicitTls),
and adds get_account/delete_account. Regression test proves the modes
survive a save/list/get round-trip.
Refs #194#182
* feat(mail): branch IMAP/SMTP transport on TlsMode; add connection probes
Threads TlsMode through ImapEngine/ImapDualSocketManager and
SmtpTransportEngine so implicit TLS, STARTTLS, and plain are genuine
transport paths (a unified MailStream plus a real IMAP STARTTLS
negotiation), not validate-and-discard. Adds bounded connect timeout
and per-engine probe_connection backing TestAccountConnection, with
mock/loopback-only setup tests for each mode.
Refs #194#182
* feat(accounts): edit/delete/test account lifecycle over gRPC
Implements Accounts UpdateAccount/RemoveAccount/TestAccountConnection in
the daemon (update mirrors add_account's keyring rollback discipline;
test probes real IMAP+SMTP via an injectable tester, honoring persisted
TLS modes), wires the configured TLS modes into build_mail_backend/
build_smtp_sender, and replaces the fabricated CLI account
show/edit/delete/test handlers with real thin gRPC clients. Adds an
offline full-daemon lifecycle E2E and unauth-rejection coverage for all
three new RPCs.
Refs #194#182
* fix(accounts): restore prior credential on update rollback
A password rotation that fails to persist previously deleted the
just-written secret, destroying the working credential the key already
held and orphaning the still-present account row. Snapshot the prior
secret first and restore it on save failure (falling back to delete
only when none existed). Regression tests force save_account to fail
and assert the original secret survives.
Refs #194#182
* docs(accounts): scope TestAccountConnection SMTP claim to reachability
The SMTP leg only dials and negotiates TLS/STARTTLS; it performs no AUTH
(lettre exposes no clean auth-only probe on AsyncSmtpTransport), while
IMAP genuinely logs in. Clarify the proto doc for smtp_ok/smtp_error and
the CLI output wording so nothing implies SMTP authentication was
verified. Doc-only proto change; golden unchanged.
Refs #194#182
---------
Co-authored-by: James Maes <jmaes@dmdbrands.com>
If set_secret succeeds but the subsequent save_account fails, the
credential was left in the keyring with no account row referencing it.
add_account now deletes the just-written secret before returning,
appending any rollback-delete failure to the original error message
without masking it.
Refs #197, #169
Co-authored-by: James Maes <jmaes@dmdbrands.com>
Adds a server-streaming Triage RPC that walks the whole message store
in bounded keyset chunks, evaluates the live FilterEngine against each
message via the shared apply_filter_actions helper, and streams real
cumulative progress -- so the active rule set can be applied to mail
synced before those rules existed. Wires the CLI's first streaming-RPC
consumer (filter triage) and regenerates the proto contract golden.
Refs #193
Co-authored-by: James Maes <jmaes@dmdbrands.com>
Adds UpdateRule/ExportRules/ImportRules/GetExecutionLogs to the
authenticated Filters gRPC service and rewires the CLI's
filter edit/export/import/logs subcommands onto it, replacing their
throwaway ephemeral local-db reads with the daemon's real, persistent
store and live FilterEngine. ImportRules now reports per-line parse
failures instead of silently dropping them.
Regenerates the nuncio-proto contract-stability golden for the
additive proto changes.
Refs #192
Co-authored-by: James Maes <jmaes@dmdbrands.com>
The account condition field lumped in with folder and compared against
email.folder_id, so WHERE account = '...' would false-positive on any
message sharing a folder with the target account. Split the arm so it
reads email.account_id instead.
header[...] conditions silently evaluated to false forever because Email
has no parsed-headers field to match against. Reject header conditions
at validation time with an honest FieldTypeMismatch instead of accepting
a rule that can never fire; the runtime fallback is now documented as an
unreachable defensive branch.
Refs #191
Co-authored-by: James Maes <jmaes@dmdbrands.com>
* feat(filters): wire FilterEngine into live inbound sync
Every synced message is now evaluated against the daemon's live
FilterEngine after it is persisted. MARK READ/UNREAD apply immediately
via set_message_read; MOVE/COPY/FLAG/UNFLAG/DELETE/FORWARD/CALL WEBHOOK
enqueue a real PendingRemoteMutation through the existing outbox for
the background worker to drain. Every match appends a
FilterExecutionLog entry, so a rule match now produces a genuine,
persisted, observable side effect instead of nothing.
The evaluate-and-route logic lives in a standalone apply_filter_actions
helper (not inlined into fetch_and_persist) so other sync paths can
reuse the exact same routing instead of re-deriving it.
* fix(filters): fire filter actions exactly once per new message
Sync is not incremental yet, so every re-sync re-fetches and
re-persists messages the backend still reports. Applying filter
actions unconditionally on every persisted message meant a rule kept
re-firing on every sync cycle for a message that arrived once,
enqueueing a fresh outbox mutation and execution log entry each time.
Guard fetch_and_persist so it only routes a message through
apply_filter_actions when the message did not already exist in the
store before this sync pass (DatabaseError::is_not_found, checked
before save_email's INSERT OR REPLACE erases the distinction). Also
makes the immediate-action match in apply_filter_actions exhaustive
over the remote action variants instead of a wildcard, so a future
immediate-style action can't silently fall through to remote routing.
---------
Co-authored-by: James Maes <jmaes@dmdbrands.com>
Replace JmapEngine's canned session/mailbox/message data with real
RFC 8620/8621 HTTP session discovery and Mailbox/get + Email/query +
Email/get JSON-RPC calls over reqwest, gated behind with_credentials
so uncredentialed callers keep the deterministic no-network fallback.
Fix the camelCase wire bug on JmapEmail (receivedAt/isUnread/
bodySnippet were silently defaulting) and add a regression test.
Wire nunciod::sync::build_mail_backend to construct the JMAP engine
with the account's real host/address/keyring password, mirroring the
IMAP arm, and rewrite the sync.rs tests that previously relied on
JmapEngine's canned data to run against wiremock servers instead.
Co-authored-by: James Maes <jmaes@dmdbrands.com>
Publish the M2-S1 contacts engine over the daemon's authenticated gRPC
API, mirroring the Calendar vertical: add nuncio.v1.Contacts (Sync,
ListContacts, GetContact, CreateContact) plus Contact/ContactEmail/
ContactPhone messages to the proto contract and regenerate the golden
descriptor.
Wire ContactsEngineOverrides + ContactsGrpcService into nunciod,
mounted behind its own BearerAuthInterceptor exactly like every other
service. ListContacts/GetContact always read genuinely persisted data
from DatabaseEngine; Sync fetches from an injected ContactsBackend
(MockContactsBackend in tests) and persists via
contacts_sync::sync_with_backend, mirroring calendar_sync.rs; with no
backend injected, production Sync returns an honest error since
AccountConfig carries no per-account CardDAV configuration yet.
Add connect_contacts/AuthenticatedContactsClient to nuncio-proto's
shared client helpers. Replace the CLI's Commands::Contact handlers
(previously a disposable in-memory ContactsDatabase.in_memory() per
invocation) with real gRPC calls against the daemon, so a contact
added via `contact add` persists and is visible to a later, separate
CLI invocation.
Deviations from the issue's literal proto sketch, flagged for review:
- Added CreateContact (not in the issue's message list): the
acceptance criteria requires `contact add` to persist a locally-
authored contact across CLI invocations, but the daemon has no
per-account CardDAV credential to write back to a real server. This
RPC only ever writes to the daemon's own local store -- never a
fabricated CardDAV write-back -- which is the same honest-surface
spirit as Calendar's "no create/delete" note (that note is about not
pretending to write to the remote server, not about never
persisting locally-authored data at all).
- ListContacts/CreateContact take a required account_id (the issue
sketched a bare ListContactsRequest{}), because the real
DatabaseEngine::list_contacts/save_contact are account-scoped;
ContactSubcommand::List/Search/Add/Sync now all take --account to
match.
- Contact search has no server-side RPC; `contact search` fetches the
full ListContacts result and filters client-side by display name,
organization, and email.
- Contact's created_at/updated_at/last_interacted_at are omitted from
the proto message -- no client use case identified for them yet.
Full-daemon offline E2E (contacts_e2e_test.rs) proves Sync -> List/Get
over the authenticated API, CreateContact's persistence surviving a
second, independent client connection, and an unauthenticated call
rejected with Code::Unauthenticated.
Co-authored-by: James Maes <jmaes@dmdbrands.com>
Replace CardDavClient::fetch_remote_vcards's hardcoded "Google CardDAV
Sync Contact" return with a real reqwest REPORT/addressbook-query
client that parses a genuine multistatus response. Add a real vCard
(RFC 6350) parser -- the crate previously only had a generator -- and
prove round-trip fidelity through Contact::to_vcard(). Add the
ContactsBackend trait and MockContactsBackend, mirroring
nuncio_cal::CalendarBackend/MockCalendarBackend. Distinguish transport
failures from parse failures via CardDavError::TransportFailed/
ParseFailed rather than ever silently succeeding with fabricated data.
Give nuncio-store real, tested save_contact/list_contacts/get_contact
CRUD backed by a genuine SQLite table + trigram FTS mirror, so contacts
persist through the daemon's own store instead of nuncio-contacts's
disconnected ContactsDatabase pool. This takes nuncio-store as a
narrow, deliberate dependency on nuncio-contacts::Contact.
Opt nuncio-contacts into the workspace lint gate ([lints] workspace,
#![forbid(unsafe_code)]) for parity with every other crate.
Wiremock-only, offline: no live network calls in tests.
Co-authored-by: James Maes <jmaes@dmdbrands.com>
* feat(calendar): expand RRULE recurrence in ListEvents and filter by calendar_id
Wire the previously-dead nuncio_cal::RecurrenceEngine into
CalendarGrpcService::list_events: non-recurring events pass through
unchanged, while recurring masters are expanded into real occurrences
for the requested window via a new store method
(list_recurring_calendar_events, no time filter since a master's own
timestamps can precede the window while its occurrences fall inside
it) and a pure expand_events_for_window merge helper that keeps
recurring rows sourced only from expansion, never double-counting a
master that also overlaps the window in the raw query.
Fix expand_occurrences to stop trusting an empty match as "return the
raw master": that fabricated an out-of-window result whenever a
window fell entirely outside a bounded (UNTIL/COUNT) series, and could
hang scanning an unbounded series for a match that would never come.
skip_while/take_while over the RRuleSet's monotonic iterator bounds
the walk and returns a genuinely empty result instead.
Also apply the calendar_id filter list_events accepted but ignored,
closing the gap between it and the mock backend's own filtering.
Refs #186
* docs(nunciod): reword stale comment referencing removed rrule fallback
---------
Co-authored-by: James Maes <jmaes@dmdbrands.com>
Wire the orphaned nuncio-cal engine into nunciod as a real, authenticated
gRPC vertical mirroring Mail: add service Calendar (Sync/ListEvents/GetEvent)
to nuncio.proto with a regenerated descriptor golden, connect_calendar in
nuncio-proto's client, calendar_sync::sync_with_backend plus
CalendarEngineOverrides/CalendarGrpcService mounted behind its own
BearerAuthInterceptor in nunciod::grpc, and real `cal sync`/`cal list` gRPC
clients in nuncio-cli replacing the fabricated CLI output. ListEvents/GetEvent
always read genuinely persisted data; production Sync returns an honest error
since there is no persisted per-account CalDAV configuration yet, and only an
injected MockCalendarBackend exercises a working Sync. Adds unit, daemon
integration, and offline full-daemon E2E tests, including an
unauthenticated-call-rejected assertion for the new service.
Refs #185
Co-authored-by: James Maes <jmaes@dmdbrands.com>
Several gRPC test helpers created an ephemeral sqlite-backed
DatabaseEngine via connect_ephemeral() and then discarded the
returned TempDir guard before returning the still-live db/server to
the caller. Dropping the guard unlinks the directory backing the
sqlite file; on Windows the still-open file handle keeps the data
reachable (masking the bug locally), but on Linux the directory entry
disappears immediately, so any pool connection opened afterward fails
with "unable to open database file" -- exactly the flake CI hit in
nunciod::grpc::tests::preview_rule_dry_runs_against_a_stored_message_without_persisting.
Fixes the same pattern in nunciod::grpc's spawn_test_server and
spawn_filters_test_server helpers, nuncio-cli's HeadlessRunner
(ephemeral_with), and grpc_auth_test's start_server: each now returns
(or stores) the TempDir so it stays alive for as long as its caller
keeps using the database.
Sweep every README/doc for accuracy and remove fabricated or stale content:
- README/CLAUDE: add the live roadmap-page link and a rule to keep the rendered
page in sync with docs/ROADMAP.md; fix the stale cov alias and the
already-moved-shells note.
- .github/CONTRIBUTING+SECURITY: drop the fabricated nuncio.mx site link and the
aspirational release/security claims; state honest pre-alpha status.
- docs/RUNNING, DEVELOPING, ARCHITECTURE, BACKLOG, Home, NSQL spec, corruption
notes, proto/README: correct crate count/list, service list, CLI flags, phase
status, and remove claims of unshipped/removed things (IPC-as-current,
hardcoded-keys, unwired engines); mark superseded api-surface-draft historical.
- Remove the fabricated benchmark baked into nsql_pipeline.svg and delete the
orphaned ipc_sequence.svg (depicted the removed IPC with a fake timing).
Fix the TZID bug in the iCalendar parser: DTSTART/DTEND values carrying a TZID
parameter hit a catch-all that zeroed the timestamp. Resolve the zone via
chrono_tz and return an error on an unknown or DST-ambiguous zone rather than
fabricating a 1970 timestamp.
Add a CalendarBackend trait mirroring MailBackend, a real reqwest CalDAV REPORT
transport (CalDavClient + CalDavAccountConfig) that surfaces network/HTTP
failures as honest errors, and make MockCalendarBackend implement the trait.
wiremock integration tests cover the REPORT path and a transport failure.
Add save/list/get calendar_events CRUD to the store. Enable recursive_triggers
so INSERT OR REPLACE fires the FTS delete trigger, keeping events_fts in sync on
upsert; verified this only strengthens the WORM-audit guards and never
double-indexes the encrypted message FTS.
Refs #178
- Enforce a rule's account scope in evaluate, evaluate_with_timeout, and
preview via matches_account; previously a rule created ON ACCOUNT one account
would fire on any account whose message matched the WHERE clause.
- Return true byte offsets from find_keyword_outside_quotes (char_indices)
instead of a char count, so the WHERE/ACTION and ON ACCOUNT splits no longer
corrupt or panic on non-ASCII text preceding a keyword.
- Add FilterOperator::NotIn evaluated as the complement of IN. Negated IN lists
previously mapped to NotEquals over a List value, which no eval arm handled,
so NOT IN was silently always false.
Refs #177
Emit a FileDescriptorSet golden (proto/nuncio/v1/descriptor.bin) at build time
and assert the freshly-compiled descriptor matches it byte-for-byte, so any
breaking change to the nuncio.v1 wire contract fails the build until the golden
is deliberately regenerated. Source info is stripped so pure doc-comment edits
do not perturb the golden.
Add proto/README.md documenting Swift/C#/TypeScript codegen from the proto,
the loopback bearer-token auth model, the System.Subscribe push stream, and the
v1 versioning policy (additive-only within v1; breaking changes go to nuncio.v2).
Refs #174
Delete nuncio-core::ipc (IpcDaemonServer, IpcClient, framing, protocol -- the
demux-race transport), the daemon's :9422 boot + JSON-RPC handler, the stubbed
'nuncio-cli daemon' subcommand, and the IPC-transport test (gRPC coverage
already exists). The daemon now awaits nunciod::grpc::serve as its foreground
task; DB/keyring/sync/outbox/update remain background tasks.
Refs #173
Reword ~190 source comments and user-facing strings across crates/ to explain
the code's intent/constraints instead of citing story numbers, GH issues, or
roadmap phases (that traceability lives in git history + docs). Kept RFC and
technical rationale; changed no logic. Enforces the CLAUDE.md comment convention.
The plain-text (non --json) listing printed only 'N account(s) registered'; it
now lists each account's id, name, email, IMAP/SMTP endpoints, and TLS. The
--json output already carried the fields. Test asserts the details appear in
plain output.
Add authenticated Export (ExportMailbox -> mbox/eml-zip/json/jsonl written to a
local file, backed by the real ExportEngine + a scoped message query) and Audit
(ListRecords, VerifyChain over the WORM ledger with a structured report) gRPC
services, each mounted with the bearer interceptor like the others. Wire CLI
'mail export' and 'system audit list/verify'. Audit records expose the record
MAC, never the signing key. Offline tests cover export content/counts, chain
verify (intact + tampered), and auth rejection.
Refs #172
nunciod opens ~/.nuncio/<db>, but SQLite create_if_missing creates only the
file, not its parent directory -- so on a fresh machine (no ~/.nuncio) the
daemon failed to start with SQLITE_CANTOPEN (code 14). connect_file now
create_dir_all's the parent first. Found via dogfooding (first real daemon run);
adds a regression test.
Add an authenticated Filters service backed by the NSQL engine + store:
CreateRule/DeleteRule persist and hot-reload the live FilterEngine (ArcSwap);
ValidateRule returns an honest valid/error verdict without failing the RPC;
PreviewRule dry-runs a candidate (unsaved) rule against stored mail. Mounted
with the bearer interceptor like the other services (auth-rejection tested).
Wire CLI filter create/list/delete/validate/test to it; the shared filter_engine
instance is reused by both the gRPC path and the legacy filter.* handler during
the transition. filter edit/export/import/logs stay local for now (tracked).
Refs #171
Add MailEngineOverrides so tests inject MockMailBackend/MockMessageSender into a
live daemon without production depending on mocks (default = real engines). Add
an authenticated Mail.Sync RPC (awaits completion) and wire CLI 'mail sync' to
it, off the ephemeral runner. New spine_e2e_test drives the whole spine over
gRPC offline: AddAccount -> Sync -> ListMessages/GetMessage -> MarkRead (with
MessageFlagsChanged on Subscribe) -> SendMessage (captured by the mock sender).
Refs #161
Add the outbound path: authenticated Mail.SendMessage builds an SMTP transport
from the account's SMTP endpoint + keyring password and sends via
SmtpTransportEngine -- honest success/failure, never fabricated. New
MessageSender trait (production SmtpTransportEngine; MockMessageSender for tests)
mirrors the sync MailBackend seam. #168: AccountConfig gains smtp_host/smtp_port
via a backfill-safe accounts migration (old rows fall back to the IMAP
host/port); proto + CLI now persist the SMTP endpoint instead of dropping it.
CLI 'mail send' is a real gRPC client; the fabricated 'Message sent' output is
gone. Tests prove the exact message reaches a mock transport and failures
surface honestly (no live network).
Refs #160#168
Add an authenticated Mail service (ListFolders, ListMessages, GetMessage,
MarkRead, SearchMessages), mounted with the bearer interceptor like
System/Accounts. Backed by DatabaseEngine read methods + a new set_message_read;
GetMessage returns the decrypted body; MarkRead persists then emits
MessageFlagsChanged (so it streams via Subscribe). Wire nuncio-cli
mail list/read/mark/search + folder list off the ephemeral runner onto the
daemon over gRPC, with honest errors when the daemon is unreachable. Offline
E2E round-trips all RPCs and proves mark-read persistence + streaming.
Refs #159
Add nunciod::sync: sync_with_backend(&dyn MailBackend) fetches folders +
messages and persists each via save_email, wrapping the status lifecycle so a
sync ALWAYS completes (Idle + SyncCompleted) and can never stick at Syncing;
failures surface as CoreEvent::Error. Production builds a real ImapEngine from
account config + the keyring password. The daemon now claims the EventBus
command receiver and runs real syncs on SyncAll/SyncAccount (previously those
commands -- including post-recovery resync -- were silently inert). Remove the
outbox worker's 'Simulate remote IMAP/JMAP mutation execution' fake; it no
longer fabricates 'completed' (outbound send is 1.C.5). Tests inject
MockMailBackend and assert persistence + the SyncStarted/SyncCompleted/Error
sequence, offline.
Refs #158
Add an authenticated Accounts gRPC service (AddAccount/ListAccounts), mounted
with the same bearer interceptor as System (every service authenticated). The
daemon now uses a persistent DB (~/.nuncio/nuncio.db; was ephemeral temp_dir)
and stores account passwords ONLY in the OS keyring -- never in SQLite, logs,
Debug/Serialize output, or the ListAccounts response (CLI PasswordArg redacts;
rpassword prompt, no echo). Wire nuncio-cli account add/list to the daemon over
gRPC. Tests prove persistence across restart and credential secrecy.
Security-reviewed, APPROVED.
Refs #156#157
Data-loss fix (1.B.4): open_with_backup_dir no longer coerces transient errors
(SQLITE_BUSY/LOCKED/IOERR/CANTOPEN, pool timeouts) into 'corrupt' -- it now
propagates them without salvaging or deleting the live DB. is_sqlite_corruption_error
is a strict allowlist of SQLITE_CORRUPT/NOTADB (primary code & 0xff) plus a
deterministic raw-header-magic pre-flight check. Salvage SQL now matches the real
filter_rules/conditions/actions schema, so rules are actually restored (1.B.3).
De-flake #155: root cause was a deferred WAL checkpoint healing the test's header
corruption ~10-50ms after close; close() now runs wal_checkpoint(TRUNCATE) before
pool close. Test re-enabled and stable (130 runs, 0 failures).
Refs #154#155
The FTS index was built over the encrypted body column, so body search never
matched real terms; it was also created lazily with no backfill. Now migrate()
creates the FTS tables eagerly and save_email writes plaintext subject/sender/
body into a standalone messages_fts table in the same transaction (stored
columns stay AES-256-GCM ciphertext). migrate() backfills rows missing from the
index by decrypting with the engine key. Tests exercise the real save_email
path (no raw-SQL bypass) and backfill. Documented tradeoff: the FTS index holds
plaintext-derived trigrams, so body text is recoverable from the index while
search is enabled -- encrypted index / whole-DB encryption is future work.
Refs #152#153
Add System.Subscribe server-streaming RPC on the authenticated System service,
mapping every CoreEvent variant to a nuncio.v1.Event oneof. The daemon streams
live EventBus broadcasts (skip-on-lag with a warning; clean close when the bus
drops). Add a client subscribe helper in nuncio-proto. Regression test proves
20 concurrent unary GetStatus calls and 20 streamed events arrive uncorrupted
and in order -- the property the old hand-rolled JSON-RPC IPC demux lacked.
Refs #151
Move the gRPC address resolver and add an authenticated client helper
(connect_system + BearerTokenInterceptor) into the shared nuncio-proto crate so
client shells never depend on the nunciod binary. Wire nuncio-cli's system
status to query the live daemon over gRPC (token from an injectable OS keyring),
printing real status or an honest error when unreachable. Refactor nunciod to
reuse the shared addr items. Reconcile tests to assert the real gRPC round-trip
instead of local ephemeral state.
Refs #150
nunciod now serves the nuncio.v1.System gRPC service on loopback (default
127.0.0.1:9420, env NUNCIO_GRPC_ADDR), authenticated by a bearer token minted
into the OS keyring (SecretManager, CSPRNG, fail-closed). A tonic interceptor
requires 'authorization: Bearer <token>' and compares in constant time
(subtle::ConstantTimeEq); every missing/malformed/wrong-token path returns
Unauthenticated. GetStatus returns live engine status + version. Existing
JSON-RPC IPC (9422) unchanged. Security-reviewed, APPROVED.
Refs #149
New crate crates/nuncio-proto houses the versioned protobuf contract
(proto/nuncio/v1) and tonic-generated client+server stubs, starting with a
minimal System.GetStatus RPC. protoc is vendored via protoc-bin-vendored
(prebuilt binaries; no system protoc, cmake, or C++ toolchain) for reproducible
CI on Linux/macOS/Windows. Uses the tonic 0.14 split (tonic + tonic-prost
runtime, tonic-prost-build codegen). Generated code exempted from workspace
lints; hand-written code keeps them.
Refs #148
Move nuncio-tui, nuncio-gui (+src-tauri, ui), and nuncio-mcp out of the Cargo
workspace into _reference/ (git renames; history preserved), retained for the
Phase 5 native client-repo extraction. Remove the unused nuncio-mcp dev-dep
from nunciod and rework daemon_e2e_test to drive the real nuncio-cli
HeadlessRunner (more coverage, no archived-shell dependency). Workspace is now
engine + daemon + reference CLI (8 members); build/clippy/tests green.
Refs #145
Gate the nunciod 24h auto-update-check loop behind NUNCIO_AUTO_UPDATE_ENABLED
(defaults off), so the daemon never autonomously checks/installs updates. Make
update.apply RPC return an explicit 'disabled pending Phase 4' error instead of
performing the fail-open install; update.check (read-only) is preserved.
UpdateEngine (update.rs) untouched for the Phase 4 checksum fix.
Refs #140
Remove the hardcoded DEFAULT_STORAGE_KEY, DEFAULT_WORM_KEY, and ledger secret
literals. Add an OsKeyring-backed SecretManager::production() with fail-closed,
CSPRNG-generated, per-key vault provisioning (get_or_create_key_bytes). Thread
storage/WORM/ledger keys through DatabaseEngine, recovery salvage, and the
daemon orchestrator; remove the per-call secret_key parameters on WORM/ledger
signing and verification (closing that bug class). Tests use MockKeyring only;
added cross-key-rejection tests. Security-reviewed and approved.
Refs #146#147
Adopt the previously-uncommitted working-tree edits (audited state) as the
baseline. Fix real clippy lints properly (manual_strip -> strip_prefix,
let-else for infallible hardcoded regex/HMAC constructors, type aliases for
complex tuples); convert user-reachable CLI/daemon unwrap/expect to graceful
errors; allow unwrap/expect/panic in test code per project convention.
Quarantine one flaky recovery test (corruption-detection/close race) with an
explicit #[ignore] + tracking (GH #155, Phase 1.B). fmt, clippy (-D warnings,
all targets), and the full suite pass.