178a38357e
Two changes developed together in one session; they share the same module
structure (db/mod.rs, the core lib root) and only compile together, so they
land as one commit.
## UserManager + per-user encryption (§9/§11)
New `users::UserManager`: owns the system.db pool plus a map
`userid -> SqlitePool` of unlocked databases. The pool *is* the unlock token —
its connect options carry the DEK as SQLCipher's raw key, so an open pool means
the key is in RAM until restart and dropping it re-locks (§9). Knows nothing
about cookies.
New `crypto` module: envelope encryption. A random 256-bit DEK encrypts
`{userid}.db`; `users.database_password` holds it sealed with AES-256-GCM under
`Argon2id(password, salt)`. The AEAD tag is the password verifier — one
derivation both authenticates and yields the key, so encrypted users store no
second hash. Cleartext users store the Argon2id output directly, compared in
constant time. Argon2 runs in spawn_blocking behind a 2-permit semaphore
(256 MiB per derivation).
- SQLCipher via `libsqlite3-sys` `bundled-sqlcipher-vendored-openssl`, pinned
<0.38 so it unifies with the one sqlx-sqlite links (a newer copy would apply
the feature to a SQLite sqlx never uses). OpenSSL is vendored and static, so
the binary stays self-contained.
- Schema split into `create_registry_tables` (instance-wide, no user key) and
`create_owner_tables` (one owner's content, identical in every file). No FK in
the owner bucket may reach the registry — enforced by a standalone test.
Dropped `chat_history.model_db_id` (write-only, and the only registry-crossing
key); moved `projects`/`project_tickets` into the owner bucket.
- Provisioning invariant: the file is written before the row, deleted after it,
so a crash leaves an orphan file, never a user without a database. `open_db`
never creates: a missing file is an error, not a silent empty database.
Not consumed yet: no login, call sites still use the shared system.db pool.
## Extract crates/skald-core
The headless core moves out of `src/` into its own crate; `skald` (server) and
the coming `skald-setup` are shells around it. Two dependencies on the shell
were inverted rather than dragged along, so the core names neither Tauri nor any
concrete plugin:
- `Plugin::tools(self: Arc<Self>)` — plugins contribute tools through this hook
(sibling of `http_router`), so the core no longer downcasts to
`MobileConnectorPlugin`.
- `tools::restart::set_restart_handler` — the desktop shell installs its
teardown-and-respawn; the core defaults to the supervisor exit code. The core
loses its `desktop` feature.
- `boot`'s stdout formatter moves to the binary (`src/boot_format.rs`); the core
only emits tracing events.
All 79 core tests pass; the binary boots and serves in a clean directory, and
the mobile-connector tools still register through the new hook.
100 lines
4.7 KiB
TOML
100 lines
4.7 KiB
TOML
[workspace]
|
|
members = [
|
|
".",
|
|
"crates/skald-core",
|
|
"crates/honcho-client",
|
|
"crates/llm-client",
|
|
"crates/core-api",
|
|
"crates/mcp-client",
|
|
"crates/plugin-tailscale-remote",
|
|
"crates/plugin-honcho",
|
|
"crates/plugin-transcribe-whisper-local",
|
|
"crates/plugin-telegram-bot",
|
|
"crates/plugin-comfyui",
|
|
"crates/plugin-tts-orpheus-3b",
|
|
"crates/plugin-tts-kokoro",
|
|
"crates/plugin-elevenlabs",
|
|
"crates/skald-relay-common",
|
|
"crates/skald-relay-server",
|
|
"crates/skald-relay-client",
|
|
"crates/plugin-mobile-connector",
|
|
]
|
|
resolver = "2"
|
|
|
|
[package]
|
|
name = "skald"
|
|
version = "0.1.0"
|
|
edition = "2024"
|
|
|
|
[features]
|
|
default = ["whisper-local"]
|
|
whisper-local = ["dep:plugin-transcribe-whisper-local"]
|
|
# Desktop bundle mode: wraps the headless server in a Tauri webview with a
|
|
# system-tray icon (menu-bar on macOS, notification area on Windows, AppIndicator
|
|
# on Linux). When enabled, `main.rs` enters the Tauri event loop instead of the
|
|
# plain tokio blocking path; the backend runs as a task on Tauri's shared runtime.
|
|
# Build a distributable bundle with: cargo tauri build --features desktop
|
|
desktop = ["dep:tauri", "dep:dirs", "dep:tauri-build"]
|
|
# Embedded (pure-Rust) Tailscale provider. Off by default: the `tailscale` crate
|
|
# forces the `aws-lc-rs` crypto backend (a cmake/NASM C build) back into the
|
|
# tree, defeating the ring-only crypto path. The recommended `tailscale_sys`
|
|
# provider (system tailscaled) stays available without it. Enable only for a
|
|
# self-contained embedded mesh (re-introduces the aws-lc-rs C build).
|
|
embedded-tailscale = ["plugin-tailscale-remote/remote-tailscale"]
|
|
|
|
[build-dependencies]
|
|
tauri-build = { version = "2", optional = true , features = [] }
|
|
|
|
[dependencies]
|
|
skald-core = { path = "crates/skald-core" }
|
|
|
|
axum = { version = "0.8", features = ["ws", "multipart"] }
|
|
tokio = { version = "1.52.3", features = ["full"] }
|
|
tokio-util = { version = "0.7", features = ["rt"] }
|
|
futures = "0.3"
|
|
tower-http = { version = "0.7.0", features = ["fs", "compression-gzip", "compression-br", "set-header"] }
|
|
tower = "0.5"
|
|
serde = { version = "1", features = ["derive"] }
|
|
serde_yaml = "0.9"
|
|
anyhow = "1"
|
|
sqlx = { version = "0.9.0", features = ["runtime-tokio", "sqlite"] }
|
|
reqwest = { version = "0.13.4", default-features = false, features = ["rustls-no-provider", "charset", "http2", "system-proxy", "json", "multipart"] }
|
|
# rustls is pinned as a direct dependency solely to select the crypto provider:
|
|
# `ring` instead of the default `aws-lc-rs`, avoiding aws-lc's cmake/NASM build.
|
|
# Every reqwest client uses `rustls-no-provider`, so exactly one process-wide
|
|
# provider is installed in main() before any TLS handshake.
|
|
#
|
|
# TLS therefore never touches OpenSSL. libcrypto is nonetheless in the tree now,
|
|
# vendored and statically linked for SQLCipher (see `libsqlite3-sys` in
|
|
# crates/skald-core) — so the binary stays self-contained, but "no OpenSSL
|
|
# anywhere" is no longer true.
|
|
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12", "logging"] }
|
|
async-trait = "0.1"
|
|
serde_json = "1"
|
|
indexmap = { version = "2", features = ["serde"] }
|
|
tracing = "0.1"
|
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
|
tracing-appender = "0.2"
|
|
chrono = { version = "0.4", default-features = false, features = ["clock", "std"] }
|
|
libc = "0.2"
|
|
notify = "8"
|
|
honcho-client = { path = "crates/honcho-client" }
|
|
llm-client = { path = "crates/llm-client" }
|
|
core-api = { path = "crates/core-api" }
|
|
mcp-client = { path = "crates/mcp-client" }
|
|
plugin-tailscale-remote = { path = "crates/plugin-tailscale-remote" }
|
|
plugin-honcho = { path = "crates/plugin-honcho" }
|
|
plugin-transcribe-whisper-local = { path = "crates/plugin-transcribe-whisper-local", optional = true }
|
|
plugin-telegram-bot = { path = "crates/plugin-telegram-bot" }
|
|
plugin-comfyui = { path = "crates/plugin-comfyui" }
|
|
plugin-tts-orpheus-3b = { path = "crates/plugin-tts-orpheus-3b" }
|
|
plugin-tts-kokoro = { path = "crates/plugin-tts-kokoro" }
|
|
plugin-elevenlabs = { path = "crates/plugin-elevenlabs" }
|
|
plugin-mobile-connector = { path = "crates/plugin-mobile-connector" }
|
|
|
|
# ── Desktop bundle (Tauri) ───────────────────────────────────────────────────
|
|
# Optional, activated by the `desktop` feature. Wraps the headless server in a
|
|
# Tauri webview with a system-tray icon. See src/desktop/ and docs/desktop.md.
|
|
tauri = { version = "2", optional = true, features = ["tray-icon"] }
|
|
dirs = { version = "5", optional = true }
|