chronogrid/docs/ARCHITECTURE/ADR/0012-daemon-scaffold.md

144 lines
5.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ADR-0012: chronogrid_daemon — Headless Peer Node Binary Scaffold
**Date:** 2026-08-23
**Status:** Accepted
**Task:** 2.7
**Author:** Automated Agent (Cline)
---
## Context
Phase 2 completed the P2P networking stack (`chronogrid_net`) and the
core-to-net sync bridge (`chronogrid_core` with `p2p` feature). The missing
piece was a runnable binary that composes these components into a headless
background process suitable for deployment on Linux servers, NAS devices,
or always-on Windows machines.
Requirements (from `docs/TASKS/phase_2_networking_and_schema.md`, Task 2.7):
- Binary crate `chronogrid-daemon` in `apps/chronogrid_daemon/`.
- CLI args via `clap`: `--config`, `--db`, `--log-level`.
- TOML config loading with platform-default fallback.
- `chronogrid_crypto` key derivation + OS keychain integration.
- `chronogrid_core` database initialization.
- `chronogrid_net` SwarmManager startup.
- Structured logging via `tracing`.
- Graceful shutdown on SIGTERM/SIGINT.
- `config.example.toml` documentation.
- `docs/DEPLOYMENT/01-headless-daemon-setup.md` setup guide.
---
## Decision
### 1. Crate Structure
Added `apps/chronogrid_daemon` to the workspace `Cargo.toml` members list.
The crate is a pure `[[bin]]` crate (no `[lib]` target) named `chronogrid-daemon`.
Dependencies are intentionally minimal — all heavy lifting is delegated to the
three internal crates (`chronogrid_core`, `chronogrid_crypto`, `chronogrid_net`).
### 2. CLI Argument Design
`clap` with `derive` is used for zero-boilerplate CLI argument parsing.
Three arguments:
| Flag | Type | Purpose |
|---|---|---|
| `-c`/`--config` | `Option<PathBuf>` | Config file path (optional) |
| `--db` | `Option<PathBuf>` | DB path override (optional) |
| `--log-level` | `String` | Tracing filter (default: `info`) |
### 3. TOML Configuration
Four config sections with `#[serde(default)]` on every field, allowing partial
or empty config files. The `Config::default()` implementation provides sensible
platform-aware defaults:
- **Linux defaults**: `/var/lib/chronogrid/` for all data files, `/etc/chronogrid/config.toml`.
- **Windows defaults**: `%APPDATA%\ChronoGrid\` for all data files.
### 4. Headless Key Management
The daemon does not require a human-entered passphrase. Instead:
1. The "passphrase" for Argon2id is synthesized from the node's `NodeId` hex +
the cluster `SwarmKey` fingerprint: `"chronogrid-daemon:<node_id>:<fingerprint>"`.
2. A fresh 16-byte random salt is generated on first boot and stored together
with the 32-byte derived key as a 48-byte blob in the OS keychain.
3. On subsequent boots, the 48-byte blob is retrieved, the salt extracted, and
the key re-derived — providing cryptographic verification that the retrieved
credential is correct for this node.
This approach provides per-node database encryption uniqueness without requiring
interactive password input, while still leveraging the OS keychain for
credential protection (DPAPI on Windows, SecretService on Linux).
### 5. Startup Sequence (8 Steps)
```
Step 1: Init tracing (log_level from CLI or RUST_LOG env)
Step 2: Load TOML config (graceful fallback to defaults)
Step 3: load_or_create_identity() + SwarmKey::load_or_create()
Step 4: resolve_master_key() — Argon2id KDF + OS keychain
Step 5: Database::open() — SQLCipher + schema validation
Step 6: SwarmManager::start() — libp2p swarm with mDNS/Kad/Gossipsub
Step 7: SyncEngine::new().run() — bridges swarm events to CRDT DB
Step 8: shutdown_signal().await — blocks until SIGINT/SIGTERM
```
### 6. Graceful Shutdown
`tokio::signal` is used for cross-platform signal handling:
- `ctrl_c()` for SIGINT on all platforms.
- `signal(SignalKind::terminate())` for SIGTERM on Unix (`#[cfg(unix)]`).
- Windows only handles SIGINT (SIGTERM is not a native Win32 concept).
The shutdown sequence: `SwarmHandle::shutdown()` → join `SyncEngine` task.
### 7. `libp2p::Multiaddr` Re-export
Rather than adding a direct `libp2p` dependency to `chronogrid_daemon`,
`chronogrid_net::lib.rs` was extended to re-export `libp2p::Multiaddr`
alongside the existing `libp2p::PeerId` re-export. This keeps `chronogrid_net`
as the single dependency for all libp2p types used by downstream crates.
---
## Consequences
### Positive
- Single binary with zero runtime dependencies beyond the OS keychain service.
- First-boot experience is fully automated (key generation, schema init).
- `config.example.toml` provides comprehensive documentation in the file itself.
- Systemd unit uses security hardening directives (`NoNewPrivileges`,
`ProtectSystem=strict`, `ReadWritePaths=/var/lib/chronogrid`).
- `Multiaddr` re-export simplifies downstream crate dependency graphs.
### Neutral
- Argon2id KDF runs on every boot (even when retrieving from keychain) to verify
the credential. This takes ~1–2 s at 64 MiB / 3 iterations — acceptable for
a daemon that starts infrequently.
### Negative / Risks
- **Android**: The keychain module is excluded on Android (`cfg(not(target_os = "android"))`).
The daemon binary is not intended for Android — the Flutter KeyStore bridge
(Task 1.5) handles that platform.
- **Passphrase-based key mode not yet exposed**: The current daemon uses only
the automated node+swarm-fingerprint passphrase. A future enhancement could
add `--passphrase` for user-supplied secrets on shared-hardware deployments.
---
## Related
- ADR-0002: `chronogrid_crypto` Argon2id KDF & OS Keychain
- ADR-0003: `chronogrid_core` SQLCipher Database Initializer
- ADR-0011: P2P Swarm Manager (`SwarmManager`, `SwarmHandle`)
- Task 2.8: Expand Verification Pipeline (next task)