57 Commits
Author SHA1 Message Date
james.maes 4732d6e786 fix(cli): let the CLI take its gRPC token from the environment
nunciod mints the gRPC bearer token into the OS keyring, so the keychain
item's ACL trusts nunciod. nuncio-cli is a different binary, so reading
that item raises an interactive consent prompt. Nothing can answer that
prompt from CI, ssh, or a background shell, and the blocking keyring read
never returns -- the CLI hung with no output and no error.

Add an opt-in NUNCIO_GRPC_TOKEN override: when set and non-empty the CLI
uses it verbatim and never touches the keyring. The keyring remains the
default and is otherwise unchanged.

The keyring read itself can no longer stall forever either. It now runs on
a detached OS thread behind a bounded timeout that reports what is blocking
and how to bypass it. A detached thread rather than spawn_blocking, because
Tokio joins its blocking pool during runtime shutdown, which would just move
the hang to process exit.

Collapse the eight copies of the token-resolution block onto that one
resolver so no call site can drift.

Refs #419
2026-08-14 19:46:23 -05:00
james.maes d05659e05b Merge pull request #416 from KofTwentyTwo/story/imap-tier1-identity
feat(imap): key messages by their server object id
2026-08-14 18:48:43 -05:00
james.maes 93f00cdd0b feat(imap): key messages by their server object id
IMAP hardcoded no object id into the message key, so it could only ever
reach the Message-ID+content tier or the folder-scoped surrogate, while
JMAP entered at the top tier from the server's Email id. One account
synced through both protocols therefore minted disjoint key spaces.

RFC 8474 EMAILID is the same identifier JMAP exposes, so requesting it
makes the two keys equal. Negotiate per capability: EMAILID only where
the server advertises OBJECTID, X-GM-MSGID only where it advertises
X-GM-EXT-1. A server offering neither behaves exactly as before.

EMAILID cannot go through the typed client: imap-proto's msg_att parser
is a closed alt with no catch-all, and async-imap turns the resulting
nom error into a fatal io::Error that closes the session. The raw layer
reads it off the transport beneath ImapStream instead, so an attribute
the crate cannot parse can never reach its decoder.

Lifting stored rows to a stronger tier changes every message_key, so
IDENTITY_SCHEMA_VERSION moves to 3 and the existing reconciliation
rebuilds the cached store rather than repointing rows one at a time.
2026-08-14 18:41:10 -05:00
james.maes b6987746ab docs: chart the shortest route to a hand-drivable daemon
The engine is ahead of its contract: it executes verified mutations but the
Mail service has no RPC to ask for one, so a human cannot move, delete or
flag anything. By this repo's definition of done the placement work has an
engine and nothing else.

Records the three steps that buy manual usability, what works and what does
not if you boot it today, the 81-issue backlog behind them, and the two test
traps (tracing callsite Interest, tokio::time::pause over real I/O) that are
invisible at default parallelism.
2026-08-14 18:08:52 -05:00
james.maes c40f6f05cd Merge pull request #385 from KofTwentyTwo/docs/multi-engine-sync-model
docs(adr): record the convergent multi-engine sync model
2026-08-14 15:21:58 -05:00
james.maes 4f6a024d3b docs(roadmap): reconcile the prose WS total with the table 2026-08-12 10:46:08 -05:00
james.maes 79f357864a fix(store): chunk IN-list lookups below SQLite's bind cap
existing_placements binds four parameters per key into one row-value
IN (...) list, and its caller hands it an entire folder: FolderChanges
accumulates every batch into one Vec, so the Full rung -- an ordinary
first sync, and the unrecognised-checkpoint fallback -- passes the whole
mailbox. SQLITE_MAX_VARIABLE_NUMBER is 32766, so past 8191 placements
the statement fails, the folder sync aborts before writing a checkpoint,
and every retry fails identically. A first sync of any large mailbox was
permanently broken.

Split the batch internally at 4000 keys and union the results, so the
limit is unreachable from outside rather than one forgetful caller away.
existing_message_ids had the same shape at a 32766 ceiling and is
chunked the same way; it stays because it is public API drawing the
identity-vs-occupancy distinction, not because it has callers.

Also correct three comments that documented traps as free:

- The IMAP batching doc claimed the working set stays bounded regardless
  of mailbox size. Batching bounds each request, not memory; the parsed
  messages accumulate across batches.
- Adding the EMAILID/X-GM-MSGID FETCH items is not free: it repoints
  every stored message's key, orphaning the old messages row and
  stranding its decrypted body in messages_fts. It needs the repoint
  path to reap first, or it leaks plaintext.
- IMAP never requests the tier-1/2 identifiers, so an IMAP-derived key
  and a JMAP-derived key for the same message differ. Recorded on
  derive_message_key and on the IMAP place() helper.

De-link ADR 0002 in the roadmap: the file exists on no merge target, so
the markdown now matches the rendered page, which already used plain
bold.
2026-08-12 10:45:28 -05:00
james.maes 7fadc04662 docs(roadmap): refresh WS counts and record the ADR 0002 split
WS-B, WS-C and WS-E grew when ADR 0002 broke the multi-engine sync model into
#386-#397, and the 2026-08-07 snapshot never caught up. Counts now match the
live milestones. Nothing is marked closed: the identity split and its chain are
green on a branch, and the numbers move when it merges.

Refs #394
2026-08-12 10:27:43 -05:00
james.maes adef7fc1b6 fix(store): harden placement queries, paging, and write locking
Five defects found in review:

- existing_placements never pinned that a never-saved key is absent, and
  its negative case was vacuous (no Archive row existed to over-report).
  Over-reporting makes genuinely new mail look already-seen and be
  skipped by the sync classifier. Both are now real assertions, backed
  by a count so a dropped IN filter fails.
- save_email_at and delete_placements both read then write in one
  deferred transaction. In WAL mode that aborts with SQLITE_BUSY, which
  busy_timeout does not retry; twenty racing saves reproducibly failed.
  Both now use BEGIN IMMEDIATE, matching the WORM and ledger appenders,
  with a concurrency regression test.
- list_messages_page claimed the folder join was one-to-one. It is not:
  the placements key permits one message in one folder twice, and the
  message-only cursor made the second row unreachable across a page
  boundary. The keyset now carries the full placement address.
- list_messages_for_export matched a folder id without its account,
  reintroducing the cross-account collision this model removes.
2026-08-11 15:31:06 -05:00
james.maes 1aacf74509 docs(plan): record pre-flight rulings on gate, ledger access, scoping 2026-08-11 13:56:45 -05:00
james.maes d138dbf65d docs(plan): plan the message/placement identity split
Refs #394
2026-08-11 13:46:56 -05:00
james.maes c6eca910c4 docs(adr): decide the pre-release posture and drop the migration compromise
The first draft reasoned like a shipped product: preserve every stored row,
never change behaviour without a path, keep old and new daemons interoperable.
Nothing is released, so none of that is owed -- and paying for it distorted the
model in one place that matters.

Message identity cannot be derived for rows already stored, because Message-ID
was never persisted and the octets are discarded after parse. Honouring those
rows meant keeping them under their old surrogate forever: permanently
un-deduped, invisible to every cross-folder guarantee the design exists to
provide, and leaving the store two-tier for as long as the install survives.
Resetting the message store and re-syncing from the server -- which is
authoritative regardless -- costs a developer one sync and buys a single
coherent model.

The audit ledger follows the same reasoning rather than gaining a permanent
namespace boundary: a hash chain whose entries reference ids that no longer
resolve has no audit value, so it is reinitialised with the store it describes.

This also removes the dependency that shaped the build order. Placements waited
on captured identity accumulating only because history had to be preserved;
now capture need only land one merge ahead of the model consuming it. The order
is recorded explicitly, along with the note that change enumeration still ships
alone and still fixes ghost messages for a single engine.

The posture section is written to be deleted at the first real release, when
every one of these questions becomes a genuine obligation.
2026-08-10 15:29:47 -05:00
james.maes 85c5d46ba0 docs(adr): record the convergent multi-engine sync model
The engine syncs as though it is the only client of an account: a forward-only
UID high-water mark that cannot observe deletion, and fire-and-forget mutations
that report success when a UID command silently did nothing. A second client --
another machine, a phone, or webmail -- makes both assumptions false, so the
local view diverges and nothing detects it.

ADR 0002 decides how a daemon behaves as one of several clients. The core
choice is to build no coordination layer: IMAP and WebDAV already coordinate
concurrent clients, so the work is finishing the sync model rather than adding
multi-engine support, and correctness at ten engines follows from correctness
at one. The exception is side-effecting filter actions, which land on third
parties with no shared resource to compare against; those get a single owner.

A draft was reviewed independently for protocol correctness, codebase fit, and
adversarially. All three falsified load-bearing claims, and the corrections are
material enough to record rather than summarise: Gmail has no QRESYNC and
Exchange has neither QRESYNC nor CONDSTORE, so the ladder needs four rungs
selected per mailbox; an absent COPYUID is conforming on success and cannot mean
conflict; keying identity on a Message-ID hash would have let a forged message
overwrite a trusted body; and the proposed convergence test could not have
detected the design's largest hole. The review evidence lands alongside the ADR
because several findings cost real effort to establish and would otherwise be
rediscovered expensively.

Documentation only. No code or contract changes.
2026-08-10 14:26:59 -05:00
James Maes a997220a98 docs: add Nuncio Monitor implementation plan
Twelve tasks across four phases: outbox account attribution, System.Shutdown,
System.GetHealth, then the monitor crate.
2026-08-07 13:46:05 -05:00
James Maes 1e5f19c6ae docs: add Nuncio Monitor design spec
In-repo dev instrument (crates/nuncio-monitor) plus the three engine
changes it needs: System.Shutdown, pending_remote_mutations.account_id,
and System.GetHealth (implements #296).
2026-08-07 13:36:46 -05:00
James Maes 418aa7052e docs: reconcile roadmap and handoff with M6.5, WS and OBS
Roadmap and its rendered page carried no mention of M6.5, the WS-A..WS-F
workstreams, or the completed OBS epic. AGENTS.md becomes a pointer to
CLAUDE.md instead of a near-verbatim copy.
2026-08-07 12:51:12 -05:00
james.maesandJames Maes 99351310ec feat(core): redaction policy, Redacted<T>, and no-secrets-in-logs canary (#378)
Add a Redacted<T> newtype in nuncio-core whose Debug, Display, and serde
render a fixed placeholder, so a wrapped credential cannot leak through a
derived Debug or serde_json; the value is reachable only via an explicit
expose_secret(). Migrate the CalDAV/CardDAV account auth tokens to it and
drop their hand-written redacting Debug impls.

Document the logging redaction policy (what must never be logged vs what
is allowed) in docs/LOGGING.md and the redact module doc.

Add a cross-crate no-secrets canary that installs a capturing subscriber,
drives auth reject, account add, mock and JMAP-wiremock sync, mark-read,
send, a filter rule-fire, a webhook dispatch, and CalDAV/CardDAV sync with
sentinel secrets/PII, and asserts none appear in captured telemetry.

Refs #363

Co-authored-by: James Maes <jmaes@dmdbrands.com>
2026-07-31 20:07:03 -05:00
james.maesandJames Maes ee63a62f17 feat(cli): wire verbosity flags and render typed daemon errors (#377)
Wire -v/-vv/-vvv to a real stderr tracing subscriber (previously parsed
but never used), with NUNCIO_LOG/RUST_LOG override precedence matching
the daemon. Decode the WS-A1 ErrorInfo from a failed RPC's status
details and render the gRPC code plus the typed reason alongside the
message, in both text and --json output, instead of dropping the code
or dumping a raw Status. Document daemon log levels, the rotating log
file location, JSON log output, and request_id correlation in
docs/RUNNING.md.

Refs #365

Co-authored-by: James Maes <jmaes@dmdbrands.com>
2026-07-31 19:41:49 -05:00
james.maesandJames Maes c8446a5800 docs: mark M5 complete, set M6 as next (#234)
* docs: mark M5 complete (feature surface done), set M6 as next

* docs: correct HANDOFF service count to eight

---------

Co-authored-by: James Maes <jmaes@dmdbrands.com>
2026-07-29 21:51:43 -05:00
james.maesandJames Maes ed71f51b22 docs: mark M1-M4 complete, set M5 as next milestone (#227)
* docs: mark M1-M4 complete, set M5 as next milestone

Phase 3 feature completeness (Calendar, Contacts, JMAP, live Filters)
is done and merged to dev; update ROADMAP.md, the rendered roadmap
page, and HANDOFF.md to reflect it and point at M5 next.

* docs: correct service count to eight (Calendar+Contacts added)

---------

Co-authored-by: James Maes <jmaes@dmdbrands.com>
2026-07-29 20:02:59 -05:00
James Maes c6b69bb1ec fix(build): remove unfixable cargo verify alias, reword docs
Cargo aliases are a single command argv and cannot chain subcommands, so
`cargo verify = ["fmt --all -- --check", "check-all", "test-all"]` tried to
run a literal subcommand named "fmt --all -- --check" and always errored.
The pre-commit hook already runs the three checks as separate commands, so
it was unaffected. Drop the broken alias, drop the matching RustRover run
configuration, and reword every doc that told readers to run `cargo verify`
to instead run fmt/check-all/test-all as three commands.
2026-07-29 11:34:06 -05:00
James Maes 514f8eb15b docs: audit pass — align all docs and repo meta with reality
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).
2026-07-28 10:49:48 -05:00
James Maes 4b93be2ffb docs: publish rendered roadmap page for GitHub Pages 2026-07-28 09:49:13 -05:00
James Maes 97cd2d627c docs: add story-execution guide and multi-machine handoff
Add docs/STORY-WORKFLOW.md (the canonical per-story execution/gate/review
protocol every roadmap issue links to) and docs/HANDOFF.md (how a new machine or
LLM picks up the work: current state, branches, the M1-M7 story index, and
coordination rules). Include the incomplete calendar API-vertical WIP as a
reference patch under docs/handoff/ (prior art for the M1 calendar story;
does not compile, apply-to-inspect only).
2026-07-28 09:09:37 -05:00
James Maes 4a45141364 docs(roadmap): chart path from current state to a client-ready backend
Rewrite ROADMAP.md as the authoritative plan from today's state (Phases 0-2
complete; Phase 3 in progress) to a frozen, versioned gRPC API that front-end
repos can consume. Records true status, folds in gaps found via dogfooding and
audit, and defines seven milestones (M1 calendar, M2 contacts, M3 JMAP, M4 live
filters, M5 mail lifecycle + incremental sync, M6 security hardening, M7 API
freeze + client-readiness gate) each with concrete exit criteria.
2026-07-28 08:18:46 -05:00
James Maes 1af9ce310a docs: reflect JSON-RPC IPC removal (daemon is gRPC-only)
Update CLAUDE.md, ARCHITECTURE.md, and the RustRover guide: the daemon serves
only the gRPC API on 127.0.0.1:9420; the old JSON-RPC IPC (:9422) is removed. Also
corrected the stale 'shells still in the workspace' note (they moved to _reference).
2026-07-27 07:48:39 -05:00
James Maes ffe6af655a docs(ide): add RustRover dev guide + one-click CLI run configs
Add docs/DEVELOPING-IN-RUSTROVER.md (setup, running the daemon + CLI, running and
debugging tests, quality gates, the dev loop, gotchas) and link it from CLAUDE.md.
Add one-click run configs: Run nuncio-cli (mail sync / account list / mail list).
2026-07-27 06:35:24 -05:00
James Maes d9d446b123 docs: add dogfooding run guide + cargo build-release alias
docs/RUNNING.md: verified build/run/use steps for the daemon + CLI (real gRPC +
OS keyring confirmed working). Add a 'cargo build-release' alias for the two
runnable binaries.
2026-07-27 05:49:22 -05:00
James Maes 690c32c406 docs: add gRPC API-surface draft mined from existing shells
Catalogue of 40 candidate RPCs across 8 services (25 real / 10 partial / 5 stub)
with reality tags, parity gaps, and open Phase 2 decisions. Input to the
proto/nuncio/v1 contract; sync RPCs to be drafted from Phase 1's real spine,
not today's status-flag stubs.

Refs #144
2026-07-26 20:47:32 -05:00
James Maes 69a4f432fd docs: reset project docs to verified state + engine-first roadmap
Remove fabricated PLAN-*/EXECUTIVE-*/TODO/TICKET/spec docs; add authoritative
ROADMAP, ADR-0001 (engine-first gRPC), and Phase 0/1 BACKLOG; rewrite README,
CLAUDE.md, ARCHITECTURE, and wiki Home to honest reality; add status banners to
the NSQL and self-healing specs; make wiki-sync mirror-with-delete.
2026-07-26 19:52:06 -05:00
James Maes d30c164184 docs: replace Mermaid text diagram with PlantUML PNG image 2026-07-23 17:04:53 -05:00
James Maes 504f977afe docs(sync): establish docs/ as Single Source of Truth with automated GitHub Wiki sync workflow 2026-07-23 13:05:02 -05:00
James Maes 2119f2c6c6 feat(telemetry): add DaemonTelemetry entity, cross-platform auto-update engine, and webhook HMAC integration tests 2026-07-23 12:49:10 -05:00
James Maes 646e4faa84 feat(filter,store,nunciod): fully implement Epic 8 (NSQL Filter Engine) and Epic 9 (Database Self-Healing Recovery) 2026-07-23 12:00:57 -05:00
James Maes 70587277af docs(ticket): publish EPIC-DATABASE-CORRUPTION-RECOVERY ticket (#286-#300) ready for implementation 2026-07-23 11:46:13 -05:00
James Maes fec4e4ea3c docs(ticket): publish EPIC-NSQL-FILTER-ENGINE ticket (#261-#285) ready for implementation 2026-07-23 11:42:30 -05:00
James Maes 891df8df6e docs(wiki): publish 4-interface multi-shell feature parity matrix and update task tracking in TODO.md 2026-07-23 11:20:45 -05:00
James Maes 093ad7748e docs: complete full documentation and GitHub governance alignment audit fixes 2026-07-23 10:44:53 -05:00
James Maes 32039fb6f7 docs: codify Unified V3 Public Release Mandate and Exhaustive E2E Testing Matrix 2026-07-23 10:43:35 -05:00
James Maes 6c6c5ef7aa docs: codify 100% Multi-Shell Feature Parity standing rule and publish V1-V3 Strategic Execution Plan 2026-07-23 10:41:23 -05:00
James Maes 21f2e743cd docs: publish Executive Architecture, Security & Code Audit Review 2026-07-23 08:56:33 -05:00
James Maes 192752f411 docs: publish master Hybrid Daemon Architecture Blueprint and shell refactoring specification 2026-07-23 08:16:06 -05:00
James Maes d7b0b4d96c feat(mcp): implement 4th presentation shell (Native MCP LLM Agent Interface) in crates/nuncio-mcp 2026-07-23 08:09:26 -05:00
James Maes c541a9dd8e docs: publish master enhanced production roadmap for V1, V2, and V3 based on PM competitive analysis 2026-07-23 08:05:44 -05:00
James Maes f9aa709b97 docs: update active zero-mock engineering milestones in docs/TODO.md 2026-07-22 21:33:07 -05:00
James Maes 1fcb9c2cb8 docs: publish 160-issue zero-mock production roadmap and update CI clippy step 2026-07-22 21:15:07 -05:00
James Maes a13fdbb7be feat(cli): standardize Noun + Verb command tree structure across CLI, core IPC, and documentation 2026-07-22 20:42:10 -05:00
James Maes 6d7f7e5c60 docs: mark all 28 micro-milestones and 8 stages 100% completed 2026-07-22 19:55:02 -05:00
James Maes 73a0aab17e docs: update TODO.md with complete list of GitHub issues #5 through #33 2026-07-22 19:23:57 -05:00
James Maes 4316a18259 docs: establish GitHub Milestones, Issues, and Project #5 as single source of truth 2026-07-22 19:18:37 -05:00