From 5c67730e131871c7d5837cf7123acc6cd40bd088 Mon Sep 17 00:00:00 2001 From: stefanskoricdev Date: Mon, 25 May 2026 13:27:27 +0200 Subject: [PATCH 1/7] db: Add encrypted Turso adapter with shared migration flow - Add EncryptedTursoDb alongside TursoDb in the shared DB module - Require SCE_DB_ENCRYPTION_KEY, reject empty keys, and configure Turso encryption via EncryptionOpts with strict aegis256 - Reuse extracted shared helpers for parent directory setup, runtime creation, and migration execution to keep encrypted/non-encrypted paths consistent - Expose sync execute()/query() on encrypted adapter and keep __sce_migrations metadata behavior aligned - Update context memory files to reflect the current shared Turso DB architecture and glossary terms Co-authored-by: SCE --- cli/src/services/db/mod.rs | 258 +++++++++++++++++++++++++-------- context/context-map.md | 2 +- context/glossary.md | 1 + context/sce/shared-turso-db.md | 2 + 4 files changed, 202 insertions(+), 61 deletions(-) diff --git a/cli/src/services/db/mod.rs b/cli/src/services/db/mod.rs index ffa37c12..16d10b87 100644 --- a/cli/src/services/db/mod.rs +++ b/cli/src/services/db/mod.rs @@ -22,6 +22,8 @@ const MIGRATIONS_TABLE_SQL: &str = "CREATE TABLE IF NOT EXISTS __sce_migrations )"; const SELECT_MIGRATION_SQL: &str = "SELECT id FROM __sce_migrations WHERE id = ?1 LIMIT 1"; const INSERT_MIGRATION_SQL: &str = "INSERT INTO __sce_migrations (id) VALUES (?1)"; +const DB_ENCRYPTION_KEY_ENV: &str = "SCE_DB_ENCRYPTION_KEY"; +const ENCRYPTION_CIPHER_AEGIS256: &str = "aegis256"; /// Service-specific Turso database configuration. #[allow(dead_code)] @@ -134,6 +136,100 @@ fn sentence_case(value: &str) -> String { first.to_uppercase().collect::() + chars.as_str() } +fn ensure_db_parent_dir(db_name: &str, db_path: &Path) -> Result<()> { + if let Some(parent) = db_path.parent() { + std::fs::create_dir_all(parent).with_context(|| { + format!( + "failed to create {db_name} parent directory: {}", + parent.display() + ) + })?; + } + + Ok(()) +} + +fn build_current_thread_runtime(db_name: &str) -> Result { + tokio::runtime::Builder::new_current_thread() + .enable_io() + .enable_time() + .build() + .with_context(|| { + format!("failed to create {db_name} tokio runtime. Try: rerun the command; if the issue persists, verify the local Tokio runtime environment.") + }) +} + +fn run_embedded_migrations( + conn: &turso::Connection, + runtime: &tokio::runtime::Runtime, + db_name: &str, + migrations: &[(&str, &str)], +) -> Result<()> { + ensure_migrations_table(conn, runtime, db_name)?; + + for (id, sql) in migrations { + if is_migration_applied(conn, runtime, db_name, id)? { + continue; + } + + apply_migration(conn, runtime, db_name, id, sql)?; + } + + Ok(()) +} + +fn ensure_migrations_table( + conn: &turso::Connection, + runtime: &tokio::runtime::Runtime, + db_name: &str, +) -> Result<()> { + runtime.block_on(async { + conn.execute(MIGRATIONS_TABLE_SQL, ()) + .await + .map_err(|e| anyhow::anyhow!("{db_name} migration metadata setup failed: {e}")) + })?; + + Ok(()) +} + +fn is_migration_applied( + conn: &turso::Connection, + runtime: &tokio::runtime::Runtime, + db_name: &str, + id: &str, +) -> Result { + runtime.block_on(async { + let mut rows = conn.query(SELECT_MIGRATION_SQL, (id,)).await.map_err(|e| { + anyhow::anyhow!("{db_name} migration metadata query failed for {id}: {e}") + })?; + + rows.next().await.map(|row| row.is_some()).map_err(|e| { + anyhow::anyhow!("{db_name} migration metadata row fetch failed for {id}: {e}") + }) + }) +} + +fn apply_migration( + conn: &turso::Connection, + runtime: &tokio::runtime::Runtime, + db_name: &str, + id: &str, + sql: &str, +) -> Result<()> { + runtime.block_on(async { + conn.execute(sql, ()) + .await + .map_err(|e| anyhow::anyhow!("{db_name} migration {id} failed: {e}"))?; + conn.execute(INSERT_MIGRATION_SQL, (id,)) + .await + .map_err(|e| { + anyhow::anyhow!("{db_name} migration metadata record failed for {id}: {e}") + })?; + + Ok(()) + }) +} + /// Generic Turso database adapter. /// /// Wraps a Turso connection with a tokio current-thread runtime so callers can @@ -146,6 +242,17 @@ pub struct TursoDb { spec: PhantomData M>, } +/// Generic encrypted Turso database adapter. +/// +/// Mirrors the structural seams of [`TursoDb`] while reserving encrypted local +/// database initialization for services that require at-rest encryption. +#[allow(dead_code)] +pub struct EncryptedTursoDb { + conn: turso::Connection, + runtime: tokio::runtime::Runtime, + spec: PhantomData M>, +} + #[allow(dead_code)] impl TursoDb { /// Open or create the database at the spec-provided canonical path. @@ -156,22 +263,9 @@ impl TursoDb { let db_name = M::db_name(); let db_path = M::db_path().with_context(|| format!("failed to resolve {db_name} path"))?; - if let Some(parent) = db_path.parent() { - std::fs::create_dir_all(parent).with_context(|| { - format!( - "failed to create {db_name} parent directory: {}", - parent.display() - ) - })?; - } + ensure_db_parent_dir(db_name, &db_path)?; - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_io() - .enable_time() - .build() - .with_context(|| { - format!("failed to create {db_name} tokio runtime. Try: rerun the command; if the issue persists, verify the local Tokio runtime environment.") - })?; + let runtime = build_current_thread_runtime(db_name)?; let conn = runtime.block_on(async { let path_str = db_path.to_str().ok_or_else(|| { @@ -276,71 +370,115 @@ impl TursoDb { /// Existing databases without migration metadata are brought forward by /// re-applying the current idempotent migration set and recording each ID. pub fn run_migrations(&self) -> Result<()> { - self.ensure_migrations_table()?; - - for (id, sql) in M::migrations() { - if self.is_migration_applied(id)? { - continue; - } + run_embedded_migrations(&self.conn, &self.runtime, M::db_name(), M::migrations()) + } +} - self.apply_migration(id, sql)?; +#[allow(dead_code)] +impl EncryptedTursoDb { + /// Open or create the encrypted database at the spec-provided canonical + /// path. + /// + /// This constructor is the encrypted counterpart to [`TursoDb::new`] and + /// uses a strict encrypted local-builder path. + pub fn new() -> Result { + let db_name = M::db_name(); + let db_path = M::db_path().with_context(|| format!("failed to resolve {db_name} path"))?; + let encryption_key = std::env::var(DB_ENCRYPTION_KEY_ENV).with_context(|| { + format!( + "missing or invalid {DB_ENCRYPTION_KEY_ENV} for {db_name}. Try: export {DB_ENCRYPTION_KEY_ENV} with a valid encryption key and rerun the command." + ) + })?; + if encryption_key.trim().is_empty() { + anyhow::bail!( + "missing or invalid {DB_ENCRYPTION_KEY_ENV} for {db_name}. Try: export {DB_ENCRYPTION_KEY_ENV} with a non-empty 64-character hex key and rerun the command." + ); } - Ok(()) - } + ensure_db_parent_dir(db_name, &db_path)?; - fn ensure_migrations_table(&self) -> Result<()> { - self.runtime.block_on(async { - self.conn - .execute(MIGRATIONS_TABLE_SQL, ()) - .await - .map_err(|e| { - anyhow::anyhow!("{} migration metadata setup failed: {e}", M::db_name()) - }) - })?; + let runtime = build_current_thread_runtime(db_name)?; - Ok(()) - } + let conn = runtime.block_on(async { + let path_str = db_path.to_str().ok_or_else(|| { + anyhow::anyhow!("invalid UTF-8 in database path: {}", db_path.display()) + })?; - fn is_migration_applied(&self, id: &str) -> Result { - self.runtime.block_on(async { - let mut rows = self - .conn - .query(SELECT_MIGRATION_SQL, (id,)) + let encryption_opts = turso::EncryptionOpts { + hexkey: encryption_key, + cipher: ENCRYPTION_CIPHER_AEGIS256.to_string(), + }; + + let db = turso::Builder::new_local(path_str) + .experimental_encryption(true) + .with_encryption(encryption_opts) + .build() .await .map_err(|e| { anyhow::anyhow!( - "{} migration metadata query failed for {id}: {e}", - M::db_name() + "failed to open encrypted {db_name} database at {} with cipher {ENCRYPTION_CIPHER_AEGIS256}. Try: verify {DB_ENCRYPTION_KEY_ENV} is a valid key and that local Turso encryption support is available: {e}", + db_path.display() ) })?; - rows.next().await.map(|row| row.is_some()).map_err(|e| { - anyhow::anyhow!( - "{} migration metadata row fetch failed for {id}: {e}", - M::db_name() - ) + db.connect().map_err(|e| { + anyhow::anyhow!("failed to connect to encrypted {db_name} database: {e}") }) - }) + })?; + + let db = Self { + conn, + runtime, + spec: PhantomData, + }; + + db.run_migrations() + .with_context(|| format!("failed to run {db_name} migrations"))?; + + Ok(db) } - fn apply_migration(&self, id: &str, sql: &str) -> Result<()> { + /// Execute a SQL statement that does not return rows. + /// + /// # Arguments + /// * `sql` - SQL statement, which may contain `?` placeholders. + /// * `params` - Parameter values implementing `IntoParams`. + /// + /// # Returns + /// Number of rows affected. + pub fn execute(&self, sql: &str, params: impl turso::params::IntoParams) -> Result { self.runtime.block_on(async { self.conn - .execute(sql, ()) + .execute(sql, params) .await - .map_err(|e| anyhow::anyhow!("{} migration {id} failed: {e}", M::db_name()))?; + .map_err(|e| anyhow::anyhow!("{} execute failed: {sql}: {e}", M::db_name())) + }) + } + + /// Execute a SQL query that returns rows. + /// + /// # Arguments + /// * `sql` - SQL query, which may contain `?` placeholders. + /// * `params` - Parameter values implementing `IntoParams`. + /// + /// # Returns + /// A `turso::Rows` iterator over the result set. + pub fn query(&self, sql: &str, params: impl turso::params::IntoParams) -> Result { + self.runtime.block_on(async { self.conn - .execute(INSERT_MIGRATION_SQL, (id,)) + .query(sql, params) .await - .map_err(|e| { - anyhow::anyhow!( - "{} migration metadata record failed for {id}: {e}", - M::db_name() - ) - })?; - - Ok(()) + .map_err(|e| anyhow::anyhow!("{} query failed: {sql}: {e}", M::db_name())) }) } + + /// Run all embedded migrations in order. + /// + /// Applied migration IDs are recorded in `__sce_migrations` so later + /// initializations apply only migrations that were not already recorded. + /// Existing databases without migration metadata are brought forward by + /// re-applying the current idempotent migration set and recording each ID. + pub fn run_migrations(&self) -> Result<()> { + run_embedded_migrations(&self.conn, &self.runtime, M::db_name(), M::migrations()) + } } diff --git a/context/context-map.md b/context/context-map.md index f79ea11a..9303d2fd 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -41,7 +41,7 @@ Feature/domain context: - `context/sce/agent-trace-post-rewrite-local-remap-ingestion.md` (current post-rewrite no-op baseline plus historical remap-ingestion reference) - `context/sce/agent-trace-rewrite-trace-transformation.md` (current post-rewrite no-op baseline plus historical rewrite-transformation reference) - `context/sce/local-db.md` (implemented `cli/src/services/local_db/mod.rs` local database spec with `LocalDb = TursoDb`, canonical local DB path resolution, zero local migrations, and inherited blocking `execute`/`query` methods using the shared Turso adapter) -- `context/sce/shared-turso-db.md` (current shared `cli/src/services/db/mod.rs` Turso database infrastructure seam, including `DbSpec`, generic `TursoDb`, sync `execute`/`query`/`query_map` wrappers, per-database `__sce_migrations` tracking, generic embedded migration execution, and current concrete wrappers for `LocalDb` plus `AgentTraceDb`) +- `context/sce/shared-turso-db.md` (current shared `cli/src/services/db/mod.rs` Turso database infrastructure seam, including `DbSpec`, generic `TursoDb`, `EncryptedTursoDb` encrypted constructor path with env key `SCE_DB_ENCRYPTION_KEY` + strict `aegis256` selection via Turso `EncryptionOpts`, encrypted-adapter sync `execute`/`query` wrappers plus migration execution parity, sync `query_map` on `TursoDb`, per-database `__sce_migrations` tracking, generic embedded migration execution, and current concrete wrappers for `LocalDb` plus `AgentTraceDb`) - `context/sce/agent-trace-db.md` (implemented `cli/src/services/agent_trace_db/mod.rs` Agent Trace database wrapper with canonical `/sce/agent-trace.db` path, ordered `diff_traces`, `post_commit_patch_intersections`, `diff_traces(time_ms, id)` index, `agent_traces`, nullable `diff_traces.model_id`, nullable `diff_traces.tool_name`, nullable `diff_traces.tool_version`, and nullable `agent_traces.agent_trace_id` migrations applied through shared migration metadata, typed parameterized insert helpers for diff traces including `model_id` + tool metadata, post-commit intersection rows, and built `agent_traces` rows with `agent_trace_id` plus schema-validated trace JSON containing range `content_hash`, inclusive bounded chronological recent `diff_traces` query/parse support with malformed-row skip accounting, registered setup/doctor lifecycle provider, and active hook writers for `diff_traces` intake plus post-commit intersection/agent-trace persistence) - `context/sce/agent-trace-core-schema-migrations.md` (historical reference for removed local DB schema bootstrap behavior; T03 now implements the actual local DB with migrations) - `context/sce/agent-trace-retry-queue-observability.md` (inactive local-hook retry path plus historical retry/metrics reference) diff --git a/context/glossary.md b/context/glossary.md index 8c917a54..cba8ca01 100644 --- a/context/glossary.md +++ b/context/glossary.md @@ -30,6 +30,7 @@ - `RuntimeCommand seam`: Internal command-execution abstraction where clap-parsed commands are converted into boxed command objects with `name()` and `execute(&AppContext)` methods, allowing app lifecycle orchestration to log and run commands without a single central dispatch `match` covering every command; the `RuntimeCommand` trait and `RuntimeCommandHandle` type alias are defined in `cli/src/services/command_registry.rs`, and the `CommandRegistry` struct maps command names to zero-arg constructor functions for dispatch. Migrated commands (`HelpCommand`, `HelpTextCommand`, `VersionCommand`, `CompletionCommand`, `AuthCommand`, `ConfigCommand`, `SetupCommand`, `DoctorCommand`, `HooksCommand`) live in service-owned `command.rs` files; parsed request construction lives in `cli/src/services/parse/command_runtime.rs` when user-provided options or subcommands are required. - `sce dependency baseline`: Current crate dependency set declared in `cli/Cargo.toml` (`anyhow`, `clap`, `clap_complete`, `dirs`, `hmac`, `inquire`, `reqwest`, `serde`, `serde_json`, `sha2`, `tokio`, `tracing`, `tracing-subscriber`, `turso`) and validated through normal compile/test coverage. - `local Turso adapter`: Module in `cli/src/services/local_db/mod.rs` that defines `LocalDbSpec` and exposes `LocalDb` as a `TursoDb` alias. It resolves the canonical local DB path with `local_db_path()`, currently declares zero migrations, and inherits `new()`, `execute()`, and `query()` from the shared generic adapter. +- `encrypted Turso adapter`: Generic adapter seam in `cli/src/services/db/mod.rs` exposed as `EncryptedTursoDb`, structurally parallel to `TursoDb` (connection, tokio runtime bridge, spec typing). Its constructor resolves `SCE_DB_ENCRYPTION_KEY`, rejects empty keys, enables Turso local encryption with strict `aegis256` cipher selection through `turso::EncryptionOpts`, and runs embedded migrations after connect; the adapter also exposes synchronous `execute`, `query`, and `run_migrations` helpers with `__sce_migrations` tracking parity. - `agent trace DB adapter`: Module in `cli/src/services/agent_trace_db/mod.rs` that defines `AgentTraceDbSpec`, exposes `AgentTraceDb` as a `TursoDb` alias, resolves `/sce/agent-trace.db` through `agent_trace_db_path()`, embeds an ordered split fresh-start migration set (`001..005`) that creates `diff_traces`, `post_commit_patch_intersections`, and `agent_traces` plus `idx_diff_traces_time_ms_id` and `idx_agent_traces_agent_trace_id`, with `agent_traces.agent_trace_id` enforced as `NOT NULL UNIQUE`; provides typed parameterized insert helpers for diff traces including `model_id` + tool metadata, post-commit intersection rows, and built agent-trace rows (including `agent_trace_id`); exposes chronological recent `diff_traces` query/parse support with malformed-row skip accounting; has `AgentTraceDbLifecycle` for setup/doctor integration; and is written by `sce hooks diff-trace` (`diff_traces`) plus `sce hooks post-commit` (`post_commit_patch_intersections` and built `agent_traces`). - `Agent Trace SCE metadata`: Implementation-owned top-level metadata emitted by `build_agent_trace(...)` as `metadata.sce.version`; the value is sourced from the compiled `sce` CLI package version via `env!("CARGO_PKG_VERSION")`, is schema-validated with the rest of the payload, and is persisted in AgentTraceDb `agent_traces.trace_json` without changing the top-level Agent Trace payload/schema `version`. - `Agent Trace range content_hash`: Per-range `content_hash` emitted by `build_agent_trace(...)` inside every `ranges[]` entry as `murmur3:`, computed from the touched-line kind/content of the `post_commit_patch` or embedded-patch hunk used to emit that range while excluding positions, paths, metadata, and database IDs. diff --git a/context/sce/shared-turso-db.md b/context/sce/shared-turso-db.md index 630441c6..a80871a6 100644 --- a/context/sce/shared-turso-db.md +++ b/context/sce/shared-turso-db.md @@ -14,6 +14,8 @@ - parent-directory creation - synchronous `execute()`, `query()`, and row-mapping `query_map()` wrappers - generic embedded migration execution through `run_migrations()` with per-database `__sce_migrations` metadata +- `EncryptedTursoDb`: encrypted-adapter seam parallel to `TursoDb` with the same structural shape (connection, runtime bridge, and spec marker). `EncryptedTursoDb::new()` now resolves `SCE_DB_ENCRYPTION_KEY`, enforces non-empty key input, enables Turso experimental local encryption, applies strict `aegis256` cipher selection through `turso::EncryptionOpts` during local DB open/connect, and runs embedded migrations after connect. +- `EncryptedTursoDb` now also exposes synchronous `execute()` and `query()` wrappers plus generic `run_migrations()` with the same `__sce_migrations` metadata flow used by `TursoDb`. - Shared lifecycle helpers: - `collect_db_path_health()` emits common parent/path health problems for DB-backed services. - `bootstrap_db_parent()` creates the resolved DB parent directory for repair/setup flows. From fbc4721602d9b0d80858ed9d483989412f52bd59 Mon Sep 17 00:00:00 2001 From: Ivan Ivic Date: Mon, 25 May 2026 14:50:56 +0200 Subject: [PATCH 2/7] auth_db: Implement encrypted auth DB module with lifecycle integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add encrypted auth DB foundation: `AuthDb = EncryptedTursoDb` wrapper, ordered auth token migrations (table + email index), and `AuthDbLifecycle` provider registered in the shared lifecycle catalog. Wire the canonical `/sce/auth.db` path resolver, module export, and lifecycle ordering (config → local_db → auth_db → agent_trace_db → hooks). Sync context files to reflect the new current-state DB surface. Plan: encrypted-auth-db Tasks: T01 (auth DB path + migrations), T02 (auth_db mod.rs), T03 (lifecycle integration) Co-authored-by: SCE --- .../auth/001_create_auth_tokens.sql | 11 ++ .../002_create_auth_tokens_email_index.sql | 2 + cli/src/services/auth_db/lifecycle.rs | 83 ++++++++++++++ cli/src/services/auth_db/mod.rs | 45 ++++++++ cli/src/services/default_paths.rs | 14 +++ cli/src/services/lifecycle.rs | 4 +- cli/src/services/mod.rs | 1 + context/architecture.md | 7 +- context/cli/default-path-catalog.md | 1 + context/context-map.md | 5 +- context/glossary.md | 8 +- context/overview.md | 8 +- context/patterns.md | 4 +- context/plans/encrypted-auth-db.md | 103 ++++++++++++++++++ context/sce/auth-db.md | 46 ++++++++ context/sce/shared-turso-db.md | 5 +- 16 files changed, 330 insertions(+), 17 deletions(-) create mode 100644 cli/migrations/auth/001_create_auth_tokens.sql create mode 100644 cli/migrations/auth/002_create_auth_tokens_email_index.sql create mode 100644 cli/src/services/auth_db/lifecycle.rs create mode 100644 cli/src/services/auth_db/mod.rs create mode 100644 context/plans/encrypted-auth-db.md create mode 100644 context/sce/auth-db.md diff --git a/cli/migrations/auth/001_create_auth_tokens.sql b/cli/migrations/auth/001_create_auth_tokens.sql new file mode 100644 index 00000000..1160f28f --- /dev/null +++ b/cli/migrations/auth/001_create_auth_tokens.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS auth_tokens ( + id TEXT PRIMARY KEY NOT NULL, + access_token TEXT NOT NULL, + token_type TEXT NOT NULL, + expires_in INTEGER NOT NULL, + refresh_token TEXT NOT NULL, + scope TEXT, + stored_at_unix_seconds INTEGER NOT NULL, + email TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) +); diff --git a/cli/migrations/auth/002_create_auth_tokens_email_index.sql b/cli/migrations/auth/002_create_auth_tokens_email_index.sql new file mode 100644 index 00000000..f988907d --- /dev/null +++ b/cli/migrations/auth/002_create_auth_tokens_email_index.sql @@ -0,0 +1,2 @@ +CREATE INDEX IF NOT EXISTS idx_auth_tokens_email +ON auth_tokens (email); diff --git a/cli/src/services/auth_db/lifecycle.rs b/cli/src/services/auth_db/lifecycle.rs new file mode 100644 index 00000000..0fbb04d6 --- /dev/null +++ b/cli/src/services/auth_db/lifecycle.rs @@ -0,0 +1,83 @@ +use anyhow::{Context, Result}; + +use crate::app::AppContext; +use crate::services::db::{bootstrap_db_parent, collect_db_path_health, DbSpec}; +use crate::services::default_paths::auth_db_path; +use crate::services::lifecycle::{ + FixOutcome, FixResultRecord, HealthCategory, HealthFixability, HealthProblem, + HealthProblemKind, HealthSeverity, LifecycleProviderId, ServiceLifecycle, SetupOutcome, +}; + +use super::{AuthDb, AuthDbSpec}; + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct AuthDbLifecycle; + +impl ServiceLifecycle for AuthDbLifecycle { + fn id(&self) -> LifecycleProviderId { + LifecycleProviderId::AuthDb + } + + fn diagnose(&self, _ctx: &AppContext) -> Vec { + diagnose_auth_db_health() + } + + fn fix(&self, _ctx: &AppContext, problems: &[HealthProblem]) -> Vec { + let should_bootstrap_parent = problems.iter().any(|problem| { + problem.category == HealthCategory::GlobalState + && problem.fixability == HealthFixability::AutoFixable + }); + if !should_bootstrap_parent { + return Vec::new(); + } + + match bootstrap_auth_db_parent() { + Ok(parent) => vec![FixResultRecord { + category: HealthCategory::GlobalState, + outcome: FixOutcome::Fixed, + detail: format!( + "Auth DB parent directory bootstrapped at '{}'.", + parent.display() + ), + }], + Err(error) => vec![FixResultRecord { + category: HealthCategory::GlobalState, + outcome: FixOutcome::Failed, + detail: format!("Automatic auth DB parent directory bootstrap failed: {error}"), + }], + } + } + + fn setup(&self, _ctx: &AppContext) -> Result { + AuthDb::new().context("Auth DB lifecycle setup failed while initializing auth DB")?; + Ok(SetupOutcome::default()) + } +} + +fn diagnose_auth_db_health() -> Vec { + let mut problems = Vec::new(); + + let db_path = match auth_db_path() { + Ok(path) => path, + Err(error) => { + problems.push(HealthProblem { + kind: HealthProblemKind::UnableToResolveStateRoot, + category: HealthCategory::GlobalState, + severity: HealthSeverity::Error, + fixability: HealthFixability::ManualOnly, + summary: format!("Unable to resolve expected auth DB path: {error}"), + remediation: String::from("Verify that the current platform exposes a writable SCE state directory before rerunning 'sce doctor'."), + next_action: "manual_steps", + }); + return problems; + } + }; + + collect_db_path_health(::db_name(), &db_path, &mut problems); + problems +} + +fn bootstrap_auth_db_parent() -> Result { + let db_path = auth_db_path().context("failed to resolve auth DB path")?; + bootstrap_db_parent(::db_name(), &db_path) +} diff --git a/cli/src/services/auth_db/mod.rs b/cli/src/services/auth_db/mod.rs new file mode 100644 index 00000000..593c3670 --- /dev/null +++ b/cli/src/services/auth_db/mod.rs @@ -0,0 +1,45 @@ +//! Encrypted auth Turso database adapter. + +use std::path::PathBuf; + +use anyhow::Result; + +use crate::services::{ + db::{DbSpec, EncryptedTursoDb}, + default_paths::auth_db_path, +}; + +const CREATE_AUTH_TOKENS_MIGRATION: &str = + include_str!("../../../migrations/auth/001_create_auth_tokens.sql"); +const CREATE_AUTH_TOKENS_EMAIL_INDEX_MIGRATION: &str = + include_str!("../../../migrations/auth/002_create_auth_tokens_email_index.sql"); + +const AUTH_MIGRATIONS: &[(&str, &str)] = &[ + ("001_create_auth_tokens", CREATE_AUTH_TOKENS_MIGRATION), + ( + "002_create_auth_tokens_email_index", + CREATE_AUTH_TOKENS_EMAIL_INDEX_MIGRATION, + ), +]; + +/// Encrypted auth database configuration. +pub struct AuthDbSpec; + +impl DbSpec for AuthDbSpec { + fn db_name() -> &'static str { + "auth DB" + } + + fn db_path() -> Result { + auth_db_path() + } + + fn migrations() -> &'static [(&'static str, &'static str)] { + AUTH_MIGRATIONS + } +} + +/// Encrypted auth Turso database adapter. +pub type AuthDb = EncryptedTursoDb; + +pub mod lifecycle; diff --git a/cli/src/services/default_paths.rs b/cli/src/services/default_paths.rs index 557e4da9..db312052 100644 --- a/cli/src/services/default_paths.rs +++ b/cli/src/services/default_paths.rs @@ -280,6 +280,20 @@ pub fn local_db_path() -> anyhow::Result { .join("local.db")) } +/// Returns the canonical path to the encrypted auth Turso database file. +/// +/// The path is `/sce/auth.db`, where `state_root` comes +/// from the shared default-path catalog (`XDG_STATE_HOME` or platform +/// equivalent). +#[allow(dead_code)] +pub fn auth_db_path() -> anyhow::Result { + Ok(resolve_sce_default_locations()? + .roots() + .state_root() + .join("sce") + .join("auth.db")) +} + /// Returns the canonical path to the agent trace Turso database file. /// /// The path is `/sce/agent-trace.db`, where `state_root` comes diff --git a/cli/src/services/lifecycle.rs b/cli/src/services/lifecycle.rs index 848a137a..7b80d7f1 100644 --- a/cli/src/services/lifecycle.rs +++ b/cli/src/services/lifecycle.rs @@ -10,6 +10,7 @@ pub type LifecycleProvider = Box; pub enum LifecycleProviderId { Config, LocalDb, + AuthDb, AgentTraceDb, Hooks, } @@ -128,11 +129,12 @@ pub trait ServiceLifecycle: Send + Sync { /// Returns lifecycle providers in deterministic orchestration order. /// -/// Provider order is config → `local_db` → `agent_trace_db` → hooks when hook lifecycle behavior is requested. +/// Provider order is config → `local_db` → `auth_db` → `agent_trace_db` → hooks when hook lifecycle behavior is requested. pub fn lifecycle_providers(include_hooks: bool) -> Vec { let mut providers: Vec = vec![ Box::new(crate::services::config::lifecycle::ConfigLifecycle), Box::new(crate::services::local_db::lifecycle::LocalDbLifecycle), + Box::new(crate::services::auth_db::lifecycle::AuthDbLifecycle), Box::new(crate::services::agent_trace_db::lifecycle::AgentTraceDbLifecycle), ]; diff --git a/cli/src/services/mod.rs b/cli/src/services/mod.rs index 23267779..829dda64 100644 --- a/cli/src/services/mod.rs +++ b/cli/src/services/mod.rs @@ -3,6 +3,7 @@ pub mod agent_trace_db; pub mod app_support; pub mod auth; pub mod auth_command; +pub mod auth_db; pub mod capabilities; pub mod command_registry; pub mod completion; diff --git a/context/architecture.md b/context/architecture.md index c25663f6..8ba5c69a 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -97,15 +97,16 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - `cli/src/services/observability.rs` provides deterministic runtime observability controls and rendering for app lifecycle logs, including shared config-resolved threshold/format and file-sink inputs with precedence `env > config file > defaults` for non-flag observability keys, optional file sink controls (`SCE_LOG_FILE`, `SCE_LOG_FILE_MODE` with deterministic truncate-or-append policy), stable event identifiers, severity filtering, the forced-emission warning path used for invalid discovered config startup diagnostics, stderr-only primary emission with optional mirrored file writes, and redaction-safe emission through the shared security helper. Its `observability::traits` submodule exposes the current `Logger` and object-safe `Telemetry` trait boundaries plus `NoopLogger`; the concrete observability logger and telemetry runtime still own behavior and implement those traits without changing runtime behavior. - `cli/src/services/observability.rs` no longer owns duplicate log enums or parsing helpers; it consumes the canonical primitive seam from `cli/src/services/config/mod.rs` and stays focused on logger and telemetry runtime behavior. - `cli/src/cli_schema.rs` is now the canonical owner for top-level command metadata for the real clap-backed command set (`auth`, `config`, `setup`, `doctor`, `hooks`, `version`, `completion`), including the slim top-level help purpose text and per-command visibility on `sce`, `sce help`, and `sce --help`; `cli/src/command_surface.rs` remains the custom top-level help renderer and known-command classifier, adding the synthetic `help` row plus the ASCII banner while consuming that shared metadata instead of maintaining a parallel command catalog. -- `cli/src/services/default_paths.rs` is the canonical production path catalog for the CLI: it resolves config/state/cache roots with platform-aware XDG or `dirs` fallbacks through an internal `roots` seam, exposes named default paths for current persisted artifacts (global config, auth tokens, local DB, agent trace DB), and owns canonical repo-relative, embedded-asset, install/runtime, hook, and context-path accessors so non-test production path definitions have one shared owner. Current production consumers such as config discovery, doctor reporting, setup/install flows, database adapters, and local hook runtime path resolution consume this shared catalog rather than defining owned path literals in their own modules. +- `cli/src/services/default_paths.rs` is the canonical production path catalog for the CLI: it resolves config/state/cache roots with platform-aware XDG or `dirs` fallbacks through an internal `roots` seam, exposes named default paths for current persisted artifacts and database files (global config, auth tokens, auth DB, local DB, agent trace DB), and owns canonical repo-relative, embedded-asset, install/runtime, hook, and context-path accessors so non-test production path definitions have one shared owner. Current production consumers such as config discovery, doctor reporting, setup/install flows, database adapters, and local hook runtime path resolution consume this shared catalog rather than defining owned path literals in their own modules. - `cli/src/services/config/mod.rs` defines `sce config` parser/runtime contracts (`show`, `validate`, `--help`), with bare `sce config` routed by `cli/src/app.rs` to the same help payload as `sce config --help`, deterministic config-file selection, explicit value precedence (`flags > env > config file > defaults`), strict config-file validation (`$schema`, `log_level`, `log_format`, `log_file`, `log_file_mode`, `timeout_ms`, `workos_client_id`, `policies.bash`, and `policies.attribution_hooks.enabled`), compile-time embedding of the canonical generated schema artifact and bash-policy preset catalog from the ephemeral crate-local `cli/assets/generated/config/**` mirror prepared from canonical `config/` outputs before Cargo packaging/builds, runtime parity between that schema and the Rust-side top-level allowed-key gate so startup config discovery accepts the canonical `"$schema": "https://sce.crocoder.dev/config.json"` declaration, graceful fallback for parse/schema/top-level-object failures in default-discovered config files (collected as `validation_errors` while defaults continue to resolve), explicit-path hard failures for `--config` and `SCE_CONFIG_FILE`, shared auth-key resolution with optional baked defaults starting at `workos_client_id`, repo-configured bash-policy preset/custom validation and merged reporting from discovered config files, a shared attribution-hooks runtime gate resolved from `SCE_ATTRIBUTION_HOOKS_ENABLED` or `policies.attribution_hooks.enabled` with disabled-by-default semantics, shared observability-runtime resolution for logging config keys (`log_level`, `log_format`, `log_file`, `log_file_mode`) with deterministic source-aware env-over-config precedence reused by `cli/src/app.rs`, and deterministic text/JSON output rendering where `show` includes resolved observability/auth/policy values with provenance while `validate` now returns only validation status plus issues/warnings. - `cli/src/services/output_format.rs` defines the canonical shared CLI output-format contract (`OutputFormat`) for supporting commands, with deterministic `text|json` parsing and command-scoped actionable invalid-value guidance. - `cli/src/services/config/mod.rs` is also the canonical owner for the shared runtime/config primitive seam used by the CLI: `LogLevel`, `LogFormat`, `LogFileMode`, the observability env-key constants, and the shared bool parsing helpers used by both config resolution and observability bootstrap. - `cli/src/services/capabilities.rs` defines the current broad CLI dependency-injection capability traits consumed by `AppContext`: `FsOps` with `StdFsOps` for filesystem operations and `GitOps` with `ProcessGitOps` for git command execution plus repository-root/hooks-directory resolution. Existing services do not consume these traits internally yet; doctor/setup/hooks/config migration is deferred to later lifecycle/AppContext tasks. -- `cli/src/services/lifecycle.rs` defines the current compile-safe `ServiceLifecycle` trait seam. It has default no-op `diagnose(&AppContext)`, `fix(&AppContext, &[HealthProblem])`, and `setup(&AppContext)` methods, with lifecycle-owned health, fix, and setup result types so the trait contract is not publicly anchored to doctor/setup module types. The same module owns the shared lifecycle provider catalog/factory, returning providers in deterministic order (config → local_db → agent_trace_db → hooks when requested). Hooks exposes a `HooksLifecycle` provider in `cli/src/services/hooks/lifecycle.rs` for hook rollout diagnosis/fix/setup using lifecycle-owned health records plus the canonical required-hook installer. Config exposes a `ConfigLifecycle` provider in `cli/src/services/config/lifecycle.rs` for global/repo-local config validation and repo-local `.sce/config.json` bootstrap. local_db exposes a `LocalDbLifecycle` provider in `cli/src/services/local_db/lifecycle.rs` for canonical local DB path health, parent-directory readiness/bootstrap, and `LocalDb::new()` setup. agent_trace_db exposes an `AgentTraceDbLifecycle` provider in `cli/src/services/agent_trace_db/lifecycle.rs` for canonical Agent Trace DB path health, parent-directory readiness/bootstrap, and `AgentTraceDb::new()` setup. Doctor runtime aggregates the full provider catalog for `diagnose` and `fix` and adapts lifecycle records into doctor report/fix records at the orchestration boundary; setup command aggregates the shared catalog for `setup` with hooks included only when requested and adapts hook setup outcomes before rendering setup-owned messages. +- `cli/src/services/lifecycle.rs` defines the current compile-safe `ServiceLifecycle` trait seam. It has default no-op `diagnose(&AppContext)`, `fix(&AppContext, &[HealthProblem])`, and `setup(&AppContext)` methods, with lifecycle-owned health, fix, and setup result types so the trait contract is not publicly anchored to doctor/setup module types. The same module owns the shared lifecycle provider catalog/factory, returning providers in deterministic order (config → local_db → auth_db → agent_trace_db → hooks when requested). Hooks exposes a `HooksLifecycle` provider in `cli/src/services/hooks/lifecycle.rs` for hook rollout diagnosis/fix/setup using lifecycle-owned health records plus the canonical required-hook installer. Config exposes a `ConfigLifecycle` provider in `cli/src/services/config/lifecycle.rs` for global/repo-local config validation and repo-local `.sce/config.json` bootstrap. local_db exposes a `LocalDbLifecycle` provider in `cli/src/services/local_db/lifecycle.rs` for canonical local DB path health, parent-directory readiness/bootstrap, and `LocalDb::new()` setup. auth_db exposes an `AuthDbLifecycle` provider in `cli/src/services/auth_db/lifecycle.rs` for canonical auth DB path health, parent-directory readiness/bootstrap, and `AuthDb::new()` setup. agent_trace_db exposes an `AgentTraceDbLifecycle` provider in `cli/src/services/agent_trace_db/lifecycle.rs` for canonical Agent Trace DB path health, parent-directory readiness/bootstrap, and `AgentTraceDb::new()` setup. Doctor runtime aggregates the full provider catalog for `diagnose` and `fix` and adapts lifecycle records into doctor report/fix records at the orchestration boundary; setup command aggregates the shared catalog for `setup` with hooks included only when requested and adapts hook setup outcomes before rendering setup-owned messages. - `cli/src/services/auth_command/mod.rs` defines the implemented auth command surface for `sce auth login|renew|logout|status`, including device-flow login, stored-token renewal (`--force` supported for renew), logout, and status rendering in text/JSON formats; `cli/src/services/auth_command/command.rs` owns the `AuthCommand` struct and its `RuntimeCommand` impl. - `cli/src/services/db/mod.rs` provides the shared generic Turso infrastructure seam: `DbSpec` supplies a service-specific name, path, and ordered embedded migrations, while `TursoDb` owns parent-directory creation, `Builder::new_local(...)` initialization, Turso connection setup, tokio current-thread runtime bridging, blocking `execute`/`query`/`query_map` wrappers, and generic migration execution with per-database `__sce_migrations` metadata. Existing DB files without migration metadata are upgraded by re-applying the current idempotent migration set and recording each migration ID, so setup/lifecycle initialization applies later migrations to already-created databases. The same module owns shared DB lifecycle helpers for path-health problem collection and DB parent-directory bootstrap. - `cli/src/services/local_db/mod.rs` provides the concrete local DB spec and `LocalDb` type alias over the shared generic `TursoDb` adapter. `LocalDbSpec` resolves the deterministic persistent runtime DB target through the shared default-path seam and declares no local migrations; `TursoDb` supplies blocking `execute`/`query`, parent-directory creation, Turso connection setup, tokio current-thread runtime bridging, and generic migration execution. +- `cli/src/services/auth_db/mod.rs` provides the encrypted auth DB spec and `AuthDb` type alias over `EncryptedTursoDb`. `AuthDbSpec` resolves `/sce/auth.db` through the shared default-path seam and embeds ordered auth migrations for the `auth_tokens` table plus `idx_auth_tokens_email`. Auth DB lifecycle setup/doctor integration is wired through `AuthDbLifecycle`; auth command/token-storage reads/writes are not redirected to this DB yet. - `cli/src/services/agent_trace_db/mod.rs` provides the Agent Trace DB spec and `AgentTraceDb` type alias over `TursoDb`. `AgentTraceDbSpec` resolves `/sce/agent-trace.db` through the shared default-path seam and embeds an ordered split fresh-start baseline migration set (`001_create_diff_traces`, `002_create_post_commit_patch_intersections`, `003_create_agent_traces`, `004_create_diff_traces_time_ms_id_index`, `005_create_agent_traces_agent_trace_id_index`) without `AUTOINCREMENT`; `agent_traces.agent_trace_id` is `NOT NULL UNIQUE` and indexed by `idx_agent_traces_agent_trace_id`. The module adds `DiffTraceInsert<'_>`/`insert_diff_trace()` (including `model_id`, `tool_name`, and nullable `tool_version` writes), `PostCommitPatchIntersectionInsert<'_>`/`insert_post_commit_patch_intersection()`, and `AgentTraceInsert<'_>`/`insert_agent_trace()` for parameterized writes plus `recent_diff_trace_patches(cutoff_time_ms, end_time_ms)` for inclusive chronological `diff_traces` reads that parse valid raw patch text and return skipped malformed-row reports. `cli/src/services/agent_trace_db/lifecycle.rs` registers Agent Trace DB setup/doctor lifecycle behavior; runtime writes come from `sce hooks diff-trace` (`diff_traces`) and `sce hooks post-commit` (`post_commit_patch_intersections` + built `agent_traces`). - `cli/src/test_support.rs` provides a shared test-only temp-directory helper (`TestTempDir`) used by service tests that need filesystem fixtures. - `cli/src/services/setup/mod.rs` defines the setup command contract (`SetupMode`, `SetupTarget`, `SetupRequest`, CLI flag parser/validator), an `inquire`-backed interactive target prompter (`InquireSetupTargetPrompter`), setup dispatch outcomes (proceed/cancelled), compile-time embedded asset access (`EmbeddedAsset`, target-scoped iterators, required-hook asset iterators/lookups) generated by `cli/build.rs` from the ephemeral crate-local `cli/assets/generated/config/{opencode,claude}/**` mirror plus `cli/assets/hooks/**`, and focused internal support seams for install-flow vs prompt-flow logic; `cli/src/services/setup/command.rs` owns `SetupCommand` and its `RuntimeCommand` impl. Its install engine/orchestrator stages embedded files and uses a unified remove-and-replace policy (removing existing targets before swapping staged content, with deterministic recovery guidance on swap failure and no backup artifact creation), and formats deterministic completion messaging; required-hook install orchestration (`install_required_git_hooks`) follows the same remove-and-replace policy (removing existing hooks before swapping staged content, with deterministic recovery guidance on swap failure). The setup command derives a repo-root-scoped context from the runtime `AppContext` before aggregating `ServiceLifecycle::setup` calls across lifecycle providers (config → local_db → agent_trace_db → hooks when requested), so setup providers receive the runtime logger, telemetry, and capability objects instead of a setup-local replacement context. @@ -118,7 +119,7 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - `cli/src/services/resilience.rs` defines bounded retry/timeout/backoff execution policy (`RetryPolicy`, `run_with_retry`) for transient operation hardening with deterministic failure messaging and retry observability. - No user-invocable `sce sync` command is wired in the current runtime; local DB and Agent Trace DB bootstrap flows through lifecycle providers aggregated by setup, and DB health/repair flows through the doctor surface. - `cli/src/services/patch.rs` defines the standalone patch domain model (`ParsedPatch`, `PatchFileChange`, `FileChangeKind`, `PatchHunk`, `TouchedLine`, `TouchedLineKind`) for in-memory parsed unified-diff representation, capturing only touched lines (added/removed) plus minimal per-file/per-hunk metadata while excluding non-hunk headers and unchanged context lines. All types are `serde`-serializable/deserializable with `snake_case` JSON field naming. The module also provides `parse_patch`, a public parser function that converts raw unified-diff text (both `Index:` SVN-style and `diff --git` git-style formats) into `ParsedPatch` structs, with `ParseError` for actionable malformed-input diagnostics. Storage-agnostic JSON load helpers (`load_patch_from_json` for string input, `load_patch_from_json_bytes` for byte input) reconstruct `ParsedPatch` from serialized JSON content with `PatchLoadError` for actionable deserialization diagnostics. Its patch-set operations now include deterministic ordered combination plus target-shaped intersection that prefers exact touched-line matches and falls back to historical `kind`+`content` matching when incremental diffs and canonical post-commit diffs have drifted line numbers; `parse_patch`, `combine_patches`, and `intersect_patches` are consumed by the active post-commit hook runtime. -- `cli/src/services/` contains module boundaries for command_registry, lifecycle, auth_command, config, setup, doctor, hooks, version, completion, help, patch, shared database infrastructure, local DB adapters, and Agent Trace DB adapters with explicit trait seams for future implementations. `cli/src/services/command_registry.rs` defines the `RuntimeCommand` trait, `RuntimeCommandHandle` type alias, `CommandRegistry` struct, and `build_default_registry()` function for the command dispatch registry. Service-owned command modules now own the migrated runtime command structs and `RuntimeCommand` impls for help/help-text, version, completion, auth, config, setup, doctor, and hooks. +- `cli/src/services/` contains module boundaries for command_registry, lifecycle, auth_command, config, setup, doctor, hooks, version, completion, help, patch, shared database infrastructure, local DB adapters, encrypted auth DB adapters, and Agent Trace DB adapters with explicit trait seams for future implementations. `cli/src/services/command_registry.rs` defines the `RuntimeCommand` trait, `RuntimeCommandHandle` type alias, `CommandRegistry` struct, and `build_default_registry()` function for the command dispatch registry. Service-owned command modules now own the migrated runtime command structs and `RuntimeCommand` impls for help/help-text, version, completion, auth, config, setup, doctor, and hooks. - `cli/README.md` is the crate-local onboarding and usage source of truth for placeholder behavior, safety limitations, and roadmap mapping back to service contracts. - `flake.nix` applies `rust-overlay` (`oxalica/rust-overlay`) to nixpkgs, pins `rust-bin.stable.1.93.1.default` with `rustfmt` + `clippy`, reads the package/check version from repo-root `.version`, builds `packages.sce` through Crane (`buildDepsOnly` -> `buildPackage`) with a filtered repo-root source that preserves the Cargo tree plus `cli/assets/hooks`, then injects generated OpenCode/Claude config payloads and schema inputs into a temporary `cli/assets/generated/` mirror during derivation unpack so `cli/build.rs` can package the crate without requiring committed generated crate assets, runs `cli-tests`, `cli-clippy`, and `cli-fmt` plus the dedicated `integrations-install-tests`, `integrations-install-clippy`, and `integrations-install-fmt` derivations through Crane-backed paths so both Rust crates have first-class default-flake verification, exposes directory-scoped JS validation derivations for both `npm/` and the shared `config/lib/` plugin package root, and also exposes the non-default `apps.install-channel-integration-tests` flake app for install-channel integration coverage outside the default check set. The shared config-lib source set is rooted at `config/lib/` and includes the shared `package.json`, `bun.lock`, and `tsconfig.json` plus `agent-trace-plugin/` and `bash-policy-plugin/`; `config-lib-bun-tests` runs the bash-policy runtime test from that shared root, while `config-lib-biome-check` and `config-lib-biome-format` run Biome over the copied shared package source. `.github/workflows/publish-crates.yml` follows the same asset-preparation rule but runs Cargo packaging from a temporary clean repository copy so crates.io publish no longer needs `--allow-dirty`. - `flake.nix` exposes release install/run surfaces as `packages.sce` (`packages.default = packages.sce`) plus `apps.sce` and `apps.default`, all targeting `${packages.sce}/bin/sce`; this keeps repo-local and remote flake run/install flows (`nix run .`, `nix run github:crocoder-dev/shared-context-engineering`, `nix profile install github:crocoder-dev/shared-context-engineering`) aligned to the same packaged CLI output. diff --git a/context/cli/default-path-catalog.md b/context/cli/default-path-catalog.md index 4a172194..dac3e491 100644 --- a/context/cli/default-path-catalog.md +++ b/context/cli/default-path-catalog.md @@ -16,6 +16,7 @@ - global config: `/sce/config.json` - auth tokens: `/sce/auth/tokens.json` +- auth DB: `/sce/auth.db` - local DB: `/sce/local.db` - agent trace DB: `/sce/agent-trace.db` diff --git a/context/context-map.md b/context/context-map.md index 9303d2fd..74a7b51a 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -10,7 +10,7 @@ Primary context files: Feature/domain context: - `context/cli/cli-command-surface.md` (CLI command surface including top-level help with ASCII art banner and gradient rendering, setup install flow, WorkOS device authorization flow + token storage behavior, attribution-only hook routing with DB-backed `diff-trace` dual persistence and post-commit Agent Trace payload persistence including range `content_hash`, setup-owned local DB + Agent Trace DB bootstrap plus doctor DB health coverage, nested flake release package/app installability, and Cargo local install + crates.io readiness policy; `sce sync` command wiring is deferred to `0.4.0`; migrated runtime command structs for help/version/completion/auth/config/setup/doctor/hooks are owned by their respective `services/{name}/command.rs` files, while clap-to-runtime conversion lives in `services/parse/command_runtime.rs`) -- `context/cli/default-path-catalog.md` (canonical production CLI path-ownership contract centered on `cli/src/services/default_paths.rs`, including persisted, repo-relative, embedded-asset, install/runtime, hook, and context-path families plus the regression guard that keeps production path ownership centralized) +- `context/cli/default-path-catalog.md` (canonical production CLI path-ownership contract centered on `cli/src/services/default_paths.rs`, including persisted auth/config files, named DB paths for auth/local/Agent Trace databases, repo-relative, embedded-asset, install/runtime, hook, and context-path families plus the regression guard that keeps production path ownership centralized) - `context/cli/patch-service.md` (standalone patch domain model, parser, JSON load helpers, and set operations in `cli/src/services/patch.rs` for in-memory parsed unified-diff representation, capturing only touched lines plus minimal per-file/per-hunk metadata, supporting both `Index:` SVN-style and `diff --git` git-style formats, with `ParseError` for actionable malformed-input diagnostics, `PatchLoadError`/`load_patch_from_json`/`load_patch_from_json_bytes` for storage-agnostic JSON reconstruction, `intersect_patches` for target-shaped overlap with exact-match-first and historical `kind`+`content` fallback semantics plus matched-constructed-hunk `model_id` provenance inheritance, and `combine_patches` for ordered patch combination with later-wins conflict resolution plus winning-hunk `model_id` provenance inheritance; `parse_patch`, `intersect_patches`, and `combine_patches` are consumed by the active post-commit hook runtime) - `context/cli/styling-service.md` (CLI text-mode output styling with `owo-colors` and `comfy-table`, TTY/`NO_COLOR` policy, shared helper API for human-facing surfaces, and per-column right-to-left RGB gradient banner rendering) - `context/cli/config-precedence-contract.md` (implemented `sce config` show/validate command contract, deterministic `flags > env > config file > defaults` resolution order, canonical `$schema` acceptance for startup-loaded `sce/config.json` files, shared auth-key env/config/optional baked-default support starting with `workos_client_id`, shared runtime resolution for flat logging observability keys, canonical Pkl-generated `sce/config.json` schema ownership plus CLI embedding/reuse contract, config-file selection order, `show` provenance output, trimmed `validate` output contract, and opt-in compiled-binary config-precedence E2E coverage contract) @@ -41,8 +41,9 @@ Feature/domain context: - `context/sce/agent-trace-post-rewrite-local-remap-ingestion.md` (current post-rewrite no-op baseline plus historical remap-ingestion reference) - `context/sce/agent-trace-rewrite-trace-transformation.md` (current post-rewrite no-op baseline plus historical rewrite-transformation reference) - `context/sce/local-db.md` (implemented `cli/src/services/local_db/mod.rs` local database spec with `LocalDb = TursoDb`, canonical local DB path resolution, zero local migrations, and inherited blocking `execute`/`query` methods using the shared Turso adapter) -- `context/sce/shared-turso-db.md` (current shared `cli/src/services/db/mod.rs` Turso database infrastructure seam, including `DbSpec`, generic `TursoDb`, `EncryptedTursoDb` encrypted constructor path with env key `SCE_DB_ENCRYPTION_KEY` + strict `aegis256` selection via Turso `EncryptionOpts`, encrypted-adapter sync `execute`/`query` wrappers plus migration execution parity, sync `query_map` on `TursoDb`, per-database `__sce_migrations` tracking, generic embedded migration execution, and current concrete wrappers for `LocalDb` plus `AgentTraceDb`) +- `context/sce/shared-turso-db.md` (current shared `cli/src/services/db/mod.rs` Turso database infrastructure seam, including `DbSpec`, generic `TursoDb`, `EncryptedTursoDb` encrypted constructor path with env key `SCE_DB_ENCRYPTION_KEY` + strict `aegis256` selection via Turso `EncryptionOpts`, encrypted-adapter sync `execute`/`query` wrappers plus migration execution parity, sync `query_map` on `TursoDb`, per-database `__sce_migrations` tracking, generic embedded migration execution, and current concrete wrappers for `LocalDb`, `AgentTraceDb`, and encrypted `AuthDb`) - `context/sce/agent-trace-db.md` (implemented `cli/src/services/agent_trace_db/mod.rs` Agent Trace database wrapper with canonical `/sce/agent-trace.db` path, ordered `diff_traces`, `post_commit_patch_intersections`, `diff_traces(time_ms, id)` index, `agent_traces`, nullable `diff_traces.model_id`, nullable `diff_traces.tool_name`, nullable `diff_traces.tool_version`, and nullable `agent_traces.agent_trace_id` migrations applied through shared migration metadata, typed parameterized insert helpers for diff traces including `model_id` + tool metadata, post-commit intersection rows, and built `agent_traces` rows with `agent_trace_id` plus schema-validated trace JSON containing range `content_hash`, inclusive bounded chronological recent `diff_traces` query/parse support with malformed-row skip accounting, registered setup/doctor lifecycle provider, and active hook writers for `diff_traces` intake plus post-commit intersection/agent-trace persistence) +- `context/sce/auth-db.md` (current encrypted auth DB foundation: canonical `/sce/auth.db` path resolver, `AuthDb = EncryptedTursoDb` wrapper, ordered `auth_tokens` table/index migrations, and `AuthDbLifecycle` provider registered in the shared lifecycle catalog) - `context/sce/agent-trace-core-schema-migrations.md` (historical reference for removed local DB schema bootstrap behavior; T03 now implements the actual local DB with migrations) - `context/sce/agent-trace-retry-queue-observability.md` (inactive local-hook retry path plus historical retry/metrics reference) - `context/sce/agent-trace-local-hooks-mvp-contract-gap-matrix.md` (T01 Local Hooks MVP production contract freeze and deterministic gap matrix for `agent-trace-local-hooks-production-mvp`) diff --git a/context/glossary.md b/context/glossary.md index cba8ca01..2cbae071 100644 --- a/context/glossary.md +++ b/context/glossary.md @@ -31,6 +31,8 @@ - `sce dependency baseline`: Current crate dependency set declared in `cli/Cargo.toml` (`anyhow`, `clap`, `clap_complete`, `dirs`, `hmac`, `inquire`, `reqwest`, `serde`, `serde_json`, `sha2`, `tokio`, `tracing`, `tracing-subscriber`, `turso`) and validated through normal compile/test coverage. - `local Turso adapter`: Module in `cli/src/services/local_db/mod.rs` that defines `LocalDbSpec` and exposes `LocalDb` as a `TursoDb` alias. It resolves the canonical local DB path with `local_db_path()`, currently declares zero migrations, and inherits `new()`, `execute()`, and `query()` from the shared generic adapter. - `encrypted Turso adapter`: Generic adapter seam in `cli/src/services/db/mod.rs` exposed as `EncryptedTursoDb`, structurally parallel to `TursoDb` (connection, tokio runtime bridge, spec typing). Its constructor resolves `SCE_DB_ENCRYPTION_KEY`, rejects empty keys, enables Turso local encryption with strict `aegis256` cipher selection through `turso::EncryptionOpts`, and runs embedded migrations after connect; the adapter also exposes synchronous `execute`, `query`, and `run_migrations` helpers with `__sce_migrations` tracking parity. +- `auth DB adapter`: Module in `cli/src/services/auth_db/mod.rs` that defines `AuthDbSpec` and exposes `AuthDb` as an `EncryptedTursoDb` alias. It resolves the canonical `/sce/auth.db` path with `auth_db_path()` and embeds ordered migrations for the `auth_tokens` table plus `idx_auth_tokens_email`. Auth runtime token-storage replacement is not wired yet. +- `AuthDbLifecycle`: Lifecycle provider in `cli/src/services/auth_db/lifecycle.rs` that implements `ServiceLifecycle` for encrypted auth DB setup/doctor integration. `diagnose` collects auth DB path health problems, `fix` bootstraps missing auth DB parent directory, and `setup` calls `AuthDb::new()`. Registered as `LifecycleProviderId::AuthDb` in the shared lifecycle catalog. - `agent trace DB adapter`: Module in `cli/src/services/agent_trace_db/mod.rs` that defines `AgentTraceDbSpec`, exposes `AgentTraceDb` as a `TursoDb` alias, resolves `/sce/agent-trace.db` through `agent_trace_db_path()`, embeds an ordered split fresh-start migration set (`001..005`) that creates `diff_traces`, `post_commit_patch_intersections`, and `agent_traces` plus `idx_diff_traces_time_ms_id` and `idx_agent_traces_agent_trace_id`, with `agent_traces.agent_trace_id` enforced as `NOT NULL UNIQUE`; provides typed parameterized insert helpers for diff traces including `model_id` + tool metadata, post-commit intersection rows, and built agent-trace rows (including `agent_trace_id`); exposes chronological recent `diff_traces` query/parse support with malformed-row skip accounting; has `AgentTraceDbLifecycle` for setup/doctor integration; and is written by `sce hooks diff-trace` (`diff_traces`) plus `sce hooks post-commit` (`post_commit_patch_intersections` and built `agent_traces`). - `Agent Trace SCE metadata`: Implementation-owned top-level metadata emitted by `build_agent_trace(...)` as `metadata.sce.version`; the value is sourced from the compiled `sce` CLI package version via `env!("CARGO_PKG_VERSION")`, is schema-validated with the rest of the payload, and is persisted in AgentTraceDb `agent_traces.trace_json` without changing the top-level Agent Trace payload/schema `version`. - `Agent Trace range content_hash`: Per-range `content_hash` emitted by `build_agent_trace(...)` inside every `ranges[]` entry as `murmur3:`, computed from the touched-line kind/content of the `post_commit_patch` or embedded-patch hunk used to emit that range while excluding positions, paths, metadata, and database IDs. @@ -69,8 +71,8 @@ - `app startup phases`: Current `cli/src/app.rs` execution model that separates dependency checking, startup-context construction, runtime initialization, command parse/execute, and output rendering into named helpers while preserving the CLI's existing exit-code, stderr-diagnostic, and degraded-startup behavior; output rendering and execution-phase logging helpers live in `cli/src/services/app_support.rs`. - `AppContext`: Minimal dependency-injection container in `cli/src/app.rs` constructed once during runtime initialization and passed to `RuntimeCommand::execute` (trait defined in `cli/src/services/command_registry.rs`); it holds `Arc`, `Arc`, `Arc`, `Arc`, and an optional `repo_root: Option`. The `repo_root` field is `None` at startup and command paths can derive a repo-root-scoped context with `AppContext::with_repo_root(...)`, preserving the runtime logger, telemetry, and capability objects while attaching the resolved root. `AppContext::repo_root()` returns `Option<&Path>` for lifecycle providers to access the resolved repository root without re-resolving it independently. - `CommandRegistry`: Statically populated command registry in `cli/src/services/command_registry.rs` that maps `&'static str` command names to zero-arg constructor functions (`fn() -> RuntimeCommandHandle`); populated at compile time via `build_default_registry()` and carried by `AppRuntime` during command dispatch. The `RuntimeCommand` trait and `RuntimeCommandHandle` type alias are co-located in the same module. Current registered constructors cover the full top-level command catalog: `help`, `auth`, `config`, `setup`, `doctor`, `hooks`, `version`, and `completion`; stateful parsed requests are constructed by `cli/src/services/parse/command_runtime.rs` when invocation options require per-command data. -- `ServiceLifecycle`: Compile-safe lifecycle trait seam in `cli/src/services/lifecycle.rs` with default no-op `diagnose`, `fix`, and `setup` methods that accept `&AppContext`; it exposes lifecycle-owned health, fix, and setup result types, while doctor/setup adapt those records at orchestration boundaries before rendering command-owned output. The hooks service has `HooksLifecycle` for hook rollout diagnosis/fix/setup, the config service has `ConfigLifecycle` for global/repo-local config validation plus repo-local config bootstrap, local_db has `LocalDbLifecycle` for canonical local DB path health/bootstrap/setup, and agent_trace_db has `AgentTraceDbLifecycle` for canonical Agent Trace DB path health/bootstrap/setup. Doctor runtime aggregates those providers for `diagnose` and `fix`; setup command now aggregates providers for `setup` in order (config → local_db → agent_trace_db → hooks when requested). -- `lifecycle provider catalog`: Shared factory in `cli/src/services/lifecycle.rs` (`lifecycle_providers(include_hooks)`) that returns boxed `ServiceLifecycle` providers in deterministic config → local_db → agent_trace_db → hooks order, used by doctor with hooks included and by setup with hooks included only when requested. +- `ServiceLifecycle`: Compile-safe lifecycle trait seam in `cli/src/services/lifecycle.rs` with default no-op `diagnose`, `fix`, and `setup` methods that accept `&AppContext`; it exposes lifecycle-owned health, fix, and setup result types, while doctor/setup adapt those records at orchestration boundaries before rendering command-owned output. The hooks service has `HooksLifecycle` for hook rollout diagnosis/fix/setup, the config service has `ConfigLifecycle` for global/repo-local config validation plus repo-local config bootstrap, local_db has `LocalDbLifecycle` for canonical local DB path health/bootstrap/setup, auth_db has `AuthDbLifecycle` for canonical auth DB path health/bootstrap/setup, and agent_trace_db has `AgentTraceDbLifecycle` for canonical Agent Trace DB path health/bootstrap/setup. Doctor runtime aggregates those providers for `diagnose` and `fix`; setup command now aggregates providers for `setup` in order (config → local_db → auth_db → agent_trace_db → hooks when requested). +- `lifecycle provider catalog`: Shared factory in `cli/src/services/lifecycle.rs` (`lifecycle_providers(include_hooks)`) that returns boxed `ServiceLifecycle` providers in deterministic config → local_db → auth_db → agent_trace_db → hooks order, used by doctor with hooks included and by setup with hooks included only when requested. - `sce config command surface`: Implemented top-level CLI command routed by `cli/src/app.rs` to `cli/src/services/config/mod.rs`, exposing `show`, `validate`, and `--help` for deterministic runtime config inspection and validation; `show` reports resolved flat logging observability values with provenance, while `validate` reports pass/fail plus validation issues and warnings only. - `sce version command surface`: Implemented top-level CLI command routed by `cli/src/app.rs` to `cli/src/services/version/mod.rs` plus `cli/src/services/version/command.rs`, exposing deterministic runtime identification output in text form by default and JSON form via `--format json`. - `sce version output contract`: `cli/src/services/version/mod.rs` rendering contract where text output is a deterministic single-line ` ()` payload and JSON output includes stable fields `status`, `command`, `binary`, `version`, and `build_profile`. @@ -81,7 +83,7 @@ - `CLI capability traits`: Broad dependency-injection seam in `cli/src/services/capabilities.rs` consumed by `AppContext`. `FsOps`/`StdFsOps` wrap filesystem operations and `GitOps`/`ProcessGitOps` wrap git process execution plus repository-root and hooks-directory resolution; current services do not consume them internally yet. - `FsOps`: Filesystem capability trait in `cli/src/services/capabilities.rs` with `read_file`, `write_file`, `metadata`, and `exists`, implemented in production by `StdFsOps`. - `GitOps`: Git capability trait in `cli/src/services/capabilities.rs` with `run_command`, `resolve_repository_root`, `resolve_hooks_directory`, and `is_available`, implemented in production by `ProcessGitOps`. -- `SCE default path policy seam`: Canonical path resolver in `cli/src/services/default_paths.rs` that owns config/state/cache root resolution through an internal `roots` helper seam, named default paths, and an explicit inventory for the current default persisted artifacts (`global config`, `auth tokens`, `local DB`); on Linux those defaults resolve to `$XDG_CONFIG_HOME/sce/config.json`, `$XDG_STATE_HOME/sce/auth/tokens.json`, and `$XDG_STATE_HOME/sce/local.db` with platform-equivalent `dirs` fallbacks elsewhere. The same module is also the canonical owner for broader production CLI path definitions and is protected by a regression test that fails when new non-test production path literals are introduced outside `default_paths.rs`. +- `SCE default path policy seam`: Canonical path resolver in `cli/src/services/default_paths.rs` that owns config/state/cache root resolution through an internal `roots` helper seam, named default paths, and an explicit inventory for the current default persisted artifacts (`global config`, `auth tokens`); named DB paths include `auth DB`, `local DB`, and `Agent Trace DB`. On Linux those defaults resolve to `$XDG_CONFIG_HOME/sce/config.json`, `$XDG_STATE_HOME/sce/auth/tokens.json`, `$XDG_STATE_HOME/sce/auth.db`, `$XDG_STATE_HOME/sce/local.db`, and `$XDG_STATE_HOME/sce/agent-trace.db` with platform-equivalent `dirs` fallbacks elsewhere. The same module is also the canonical owner for broader production CLI path definitions and is protected by a regression test that fails when new non-test production path literals are introduced outside `default_paths.rs`. - `cli config precedence contract`: Deterministic runtime value resolution in `cli/src/services/config/mod.rs` with precedence `flags > env > config file > defaults` for flag-backed keys (`log_level`, `timeout_ms`) plus shared app-runtime observability keys (`log_format`, `log_file`, `log_file_mode`) consumed by `cli/src/app.rs`; config discovery order is `--config`, `SCE_CONFIG_FILE`, then default discovered global+local paths (`${config_root}/sce/config.json` merged before `.sce/config.json`, with local overriding per key, where `config_root` comes from the shared default path policy seam and resolves to `$XDG_CONFIG_HOME` / `dirs::config_dir()` semantics with platform fallback behavior rather than the old state/data-root default). Runtime startup config loading now also permits the canonical top-level `"$schema": "https://sce.crocoder.dev/config.json"` declaration anywhere those config files are parsed. - `shared runtime/config primitives seam`: Canonical ownership in `cli/src/services/config/mod.rs` for the CLI's shared observability/config enums (`LogLevel`, `LogFormat`, `LogFileMode`), observability env-key constants, and shared bool parsing helpers reused by `cli/src/services/observability.rs`. - `sce config schema artifact`: Canonical JSON Schema for global and repo-local `sce/config.json` files, authored in `config/pkl/base/sce-config-schema.pkl`, generated to `config/schema/sce-config.schema.json`, and embedded by `cli/src/services/config/mod.rs` for shared `sce config validate` and doctor config validation. The current schema accepts the canonical `$schema` declaration, flat logging keys (`log_level`, `log_format`, `log_file`, `log_file_mode`), existing auth/config keys, and enforces the schema-level dependency that `log_file_mode` requires `log_file`. diff --git a/context/overview.md b/context/overview.md index 39bf0760..6d009b5f 100644 --- a/context/overview.md +++ b/context/overview.md @@ -12,16 +12,16 @@ The command loop now enforces a stable exit-code contract in `cli/src/app.rs`: ` The same runtime also emits stable user-facing stderr error classes (`SCE-ERR-PARSE`, `SCE-ERR-VALIDATION`, `SCE-ERR-RUNTIME`, `SCE-ERR-DEPENDENCY`) using deterministic `Error []: ...` diagnostics with class-default `Try:` remediation appended when missing. The app runtime now also includes a structured observability baseline in `cli/src/services/observability.rs`: deterministic env-controlled log threshold/format (`SCE_LOG_LEVEL` defaults to `error`; `SCE_LOG_FORMAT` defaults to `text`), optional file sink controls (`SCE_LOG_FILE`, `SCE_LOG_FILE_MODE` with deterministic `truncate` default), stable lifecycle event IDs, stderr-only primary emission so stdout command payloads remain pipe-safe, and `observability::traits` boundaries for logger and telemetry runtime dependency injection. The app command dispatcher now enforces a centralized stdout/stderr stream contract in `cli/src/app.rs`: command success payloads are emitted on stdout only, while redacted user-facing diagnostics are emitted on stderr. `cli/src/app.rs` also now runs through explicit startup phases — dependency check, observability config resolution, runtime initialization, command parse/execute, and output rendering — with the app runtime carrying logger/telemetry plus static command-registry state across those phases while preserving the existing exit-code and degraded-startup contracts. Within that lifecycle, `parse_command_phase` delegates clap-to-runtime conversion to `cli/src/services/parse/command_runtime.rs`, and `services::app_support::execute_command_phase` delegates to the command's own execution method instead of a central app-level `dispatch` function. Command structs for `help`, `version`, `completion`, `auth`, `config`, `setup`, `doctor`, and `hooks` live in service-owned `command.rs` files; `build_default_registry()` registers the migrated command constructors while parse-time conversion constructs stateful commands with user-provided options. -The CLI now also enforces a shared output-format parser contract in `cli/src/services/output_format.rs`, with canonical `--format ` parsing and command-specific actionable invalid-value guidance reused by `config` and `version` services. A compile-safe service lifecycle seam also exists in `cli/src/services/lifecycle.rs`: `ServiceLifecycle` exposes default no-op `diagnose`, `fix`, and `setup` methods against `AppContext`, uses lifecycle-owned health/fix/setup result types, and owns the shared lifecycle provider catalog/factory with deterministic config → local_db → agent_trace_db → hooks ordering. Hooks has a `services/hooks/lifecycle.rs` provider for hook rollout diagnosis/fix/setup, config has a `services/config/lifecycle.rs` provider for global/repo-local config validation plus repo-local config bootstrap, local_db has a `services/local_db/lifecycle.rs` provider for canonical local DB path health, parent-directory readiness/bootstrap, and `LocalDb::new()` setup, and agent_trace_db has a `services/agent_trace_db/lifecycle.rs` provider for canonical Agent Trace DB path health, parent-directory readiness/bootstrap, and `AgentTraceDb::new()` setup. Doctor runtime aggregates the full shared provider catalog for `diagnose` and `fix` and adapts lifecycle records into doctor-owned output records; setup command aggregates the shared provider catalog for `setup` with hooks included only when requested and adapts lifecycle setup outcomes before rendering setup-owned messages. +The CLI now also enforces a shared output-format parser contract in `cli/src/services/output_format.rs`, with canonical `--format ` parsing and command-specific actionable invalid-value guidance reused by `config` and `version` services. A compile-safe service lifecycle seam also exists in `cli/src/services/lifecycle.rs`: `ServiceLifecycle` exposes default no-op `diagnose`, `fix`, and `setup` methods against `AppContext`, uses lifecycle-owned health/fix/setup result types, and owns the shared lifecycle provider catalog/factory with deterministic config → local_db → auth_db → agent_trace_db → hooks ordering. Hooks has a `services/hooks/lifecycle.rs` provider for hook rollout diagnosis/fix/setup, config has a `services/config/lifecycle.rs` provider for global/repo-local config validation plus repo-local config bootstrap, local_db has a `services/local_db/lifecycle.rs` provider for canonical local DB path health, parent-directory readiness/bootstrap, and `LocalDb::new()` setup, auth_db has a `services/auth_db/lifecycle.rs` provider for canonical auth DB path health, parent-directory readiness/bootstrap, and `AuthDb::new()` setup, and agent_trace_db has a `services/agent_trace_db/lifecycle.rs` provider for canonical Agent Trace DB path health, parent-directory readiness/bootstrap, and `AgentTraceDb::new()` setup. Doctor runtime aggregates the full shared provider catalog for `diagnose` and `fix` and adapts lifecycle records into doctor-owned output records; setup command aggregates the shared provider catalog for `setup` with hooks included only when requested and adapts lifecycle setup outcomes before rendering setup-owned messages. The CLI now also includes a shared text styling service in `cli/src/services/style.rs` that provides deterministic color enablement via `owo-colors` and table rendering via `comfy-table`, with automatic TTY detection and `NO_COLOR` compliance for human-facing text output; stdout help/text surfaces, stderr diagnostics, and interactive prompt-adjacent text now reuse that shared styling policy while JSON, completion, and other non-interactive/machine-readable flows remain unstyled. The service exports `supports_color()`, `supports_color_stderr()`, `table()`, `style_if_enabled()`, and `banner_with_gradient()` helpers for use across command surfaces while preserving pipe-safe output for non-interactive environments. The `setup` command includes an `inquire`-backed target-selection flow: default interactive selection for OpenCode/Claude/both with required-hook installation in the same run, explicit non-interactive target flags (`--opencode`, `--claude`, `--both`), deterministic mutually-exclusive validation, and non-destructive cancellation exits. The CLI now compiles an embedded setup asset manifest from `config/.opencode/**`, `config/.claude/**`, and `cli/assets/hooks/**` via `cli/build.rs`; `cli/src/services/setup/mod.rs` exposes deterministic normalized relative paths plus file bytes and target-scoped iteration without runtime reads from `config/`. -The setup service also provides repository-root install orchestration: it resolves the repository root, derives a repo-root-scoped `AppContext` from the runtime command context, aggregates `ServiceLifecycle::setup` calls across lifecycle providers (config → local_db → agent_trace_db → hooks when requested), handles interactive or flag-based target selection for config asset installation, and reports deterministic completion details (selected target(s) and installed file counts). Setup uses a unified remove-and-replace policy for all write flows — it removes existing targets before swapping staged content and returns deterministic recovery guidance (recover from version control) on swap failure, without creating backup artifacts. The setup command gates all modes on an existing git repository before any writes. Internally, `cli/src/services/setup/mod.rs` now separates install-flow logic from interactive prompt logic through focused support seams. +The setup service also provides repository-root install orchestration: it resolves the repository root, derives a repo-root-scoped `AppContext` from the runtime command context, aggregates `ServiceLifecycle::setup` calls across lifecycle providers (config → local_db → auth_db → agent_trace_db → hooks when requested), handles interactive or flag-based target selection for config asset installation, and reports deterministic completion details (selected target(s) and installed file counts). Setup uses a unified remove-and-replace policy for all write flows — it removes existing targets before swapping staged content and returns deterministic recovery guidance (recover from version control) on swap failure, without creating backup artifacts. The setup command gates all modes on an existing git repository before any writes. Internally, `cli/src/services/setup/mod.rs` now separates install-flow logic from interactive prompt logic through focused support seams. The CLI now also applies baseline security hardening for reliability-driven automation: diagnostics/logging paths use deterministic secret redaction, `sce setup --hooks --repo ` canonicalizes and validates repository paths before execution, and setup write flows run explicit directory write-permission probes before staging/swap operations. The config service now provides deterministic runtime config resolution with explicit precedence (`flags > env > config file > defaults`), strict config-file validation (`$schema`, `log_level`, `log_format`, `log_file`, `log_file_mode`, `timeout_ms`, `workos_client_id`, and nested `policies.bash`), deterministic default discovery/merge of global+local config files (`${config_root}/sce/config.json` then `.sce/config.json` with local override, where `config_root` comes from the shared default-path seam with XDG/`dirs::config_dir()` config-root resolution), defaults for the resolved observability value set (`log_level=error`, `log_format=text`, `log_file_mode=truncate`), shared auth-key resolution with optional baked defaults starting at `workos_client_id`, first-class bash-policy preset/custom parsing with deterministic conflict and duplicate-prefix validation, and a canonical Pkl-authored `sce/config.json` JSON Schema generated to `config/schema/sce-config.schema.json` and embedded by `cli/src/services/config/mod.rs` for both `sce config validate` and doctor-time config checks. Runtime startup config loading now keeps parity with that schema by accepting the canonical `"$schema": "https://sce.crocoder.dev/config.json"` declaration in repo-local and global config files, so startup commands such as `sce version` no longer fail before dispatch on that field. App-runtime observability now consumes flat logging keys through the shared resolver, so env values still override config-file values while config files provide deterministic fallback for file logging; `sce config show` reports resolved observability/auth/policy values with provenance, while `sce config validate` is now a trimmed validation surface that reports only pass/fail plus validation errors or warnings in text and JSON modes. The canonical preset catalog and matching contract live in `config/pkl/data/bash-policy-presets.json` and `context/sce/bash-tool-policy-enforcement-contract.md`. Invalid default-discovered config files now also degrade gracefully at startup: `sce` keeps running with degraded observability defaults, logs `sce.config.invalid_config` warnings, and reserves hard failures for explicit `--config` / `SCE_CONFIG_FILE` targets or other truly invalid runtime observability inputs. `cli/src/services/config/mod.rs` is now also the canonical owner for the CLI's shared observability/config primitive seam: `LogLevel`, `LogFormat`, `LogFileMode`, the observability env-key constants, and the shared bool parsing helpers consumed by `cli/src/services/observability.rs`. The CLI now has a minimal `AppContext` dependency-injection container in `cli/src/app.rs` holding `Arc`, `Arc`, `Arc`, `Arc`, and an optional `repo_root: Option`; it can derive repo-root-scoped contexts with `with_repo_root(...)` while preserving runtime dependencies. The broad capability seam lives in `cli/src/services/capabilities.rs`, where `FsOps`/`StdFsOps` wrap filesystem operations and `GitOps`/`ProcessGitOps` wrap git process execution plus repository-root/hooks-directory resolution. Current services have not migrated to consume the filesystem/git traits internally yet. -The shared default path service in `cli/src/services/default_paths.rs` is now the canonical owner for production CLI path definitions. It resolves per-user config/state/cache roots through a dedicated internal `roots` seam, exposes the current persisted-artifact inventory (global config, auth tokens, local DB), and also defines the repo-relative, embedded-asset, install/runtime, hook, and context-path accessors consumed across current CLI production code. Non-test production modules should consume this shared catalog instead of hardcoding owned path literals. No default cache-backed persisted artifact currently exists, so cache-root resolution remains available without speculative cache-path features and no legacy default-path fallback is supported. +The shared default path service in `cli/src/services/default_paths.rs` is now the canonical owner for production CLI path definitions. It resolves per-user config/state/cache roots through a dedicated internal `roots` seam, exposes the current persisted-artifact inventory (global config and auth tokens), and also defines named DB paths (auth DB, local DB, Agent Trace DB) plus the repo-relative, embedded-asset, install/runtime, hook, and context-path accessors consumed across current CLI production code. Non-test production modules should consume this shared catalog instead of hardcoding owned path literals. No default cache-backed persisted artifact currently exists, so cache-root resolution remains available without speculative cache-path features and no legacy default-path fallback is supported. The same config resolver now also owns the attribution-hooks gate used by local hook runtime: `SCE_ATTRIBUTION_HOOKS_ENABLED` overrides `policies.attribution_hooks.enabled`, and the gate defaults to disabled. Generated config now includes repo-local OpenCode plugin assets for both profiles: `sce-bash-policy.ts` plus `sce-agent-trace.ts` are emitted under `config/.opencode/plugins/` and `config/automated/.opencode/plugins/`; the agent-trace plugin extracts `{ sessionID, diff, time, model_id }` from user `message.updated` events with diffs, tracks per-session OpenCode client version from `session.created`/`session.updated`, and sends payloads to `sce hooks diff-trace` with `tool_name="opencode"` plus optional `tool_version`; the Rust hook continues to validate required fields and persists `model_id`, `tool_name`, and nullable `tool_version` into `diff_traces` through AgentTraceDb. Bash-policy also emits shared runtime logic and preset data under `config/.opencode/lib/` (also emitted for `config/automated/.opencode/**`). Claude bash-policy enforcement has been removed from generated outputs. The `doctor` command now exposes explicit inspection mode (`sce doctor`) and repair-intent mode (`sce doctor --fix`) at the CLI/help/schema level while keeping diagnosis mode read-only. It now validates both current global operator health and the current repo/hook-integrity slice: state-root resolution, global config path resolution, global and repo-local `sce/config.json` readability/schema validity, local DB and Agent Trace DB path + health, DB parent-directory readiness, git availability, non-repo vs bare-repo targeting failures, effective git hook-path source (default, per-repo `core.hooksPath`, or global `core.hooksPath`), hooks-directory health, required hook presence/executable permissions/content drift against canonical embedded SCE-managed hook assets, and repo-root OpenCode integration presence across the installed `plugins`, `agents`, `commands`, and `skills` inventories with embedded SHA-256 content verification for OpenCode assets. Text mode now renders the approved human-only layout with ordered `Environment` / `Configuration` / `Repository` / `Git Hooks` / `Integrations` sections, `SCE doctor diagnose` / `SCE doctor fix` headers, bracketed `[PASS]`/`[FAIL]`/`[MISS]` status tokens, shared-style green pass plus red fail/miss coloring when color output is enabled, simplified `label (path)` row formatting, top-level-only hook rows, and integration parent/child rows that reflect missing vs content-mismatch states; JSON output now reports Agent Trace DB health under `agent_trace_db` (as a row within the Configuration section in text mode). Repo-scoped database reporting is empty by default because no repo-owned SCE database currently exists. Fix mode reuses the canonical setup hook install flow to repair missing/stale/non-executable required hooks and can also bootstrap missing canonical DB parent directories while preserving manual-only guidance for unsupported issues. @@ -46,7 +46,7 @@ The targeted support commands (`handover`, `commit`, `validate`) keep their thin The prior no-git-wrapper Agent Trace design artifacts under `context/sce/agent-trace-*.md` are retained only as historical reference; the current CLI runtime no longer wires the removed Agent Trace schema adaptation, payload building, retry replay, or rewrite handling paths into local hook execution. The hooks service now uses a minimal attribution-only runtime: `commit-msg` is the only hook that mutates behavior, conditionally injecting exactly one canonical SCE trailer when the attribution-hooks gate is enabled and `SCE_DISABLED` is false; `pre-commit` and `post-rewrite` remain deterministic no-op entrypoints; `post-commit` is an active intersection entrypoint that captures current commit patch, queries recent `diff_traces` from past 7 days, combines/intersects patches, persists intersection metadata to `post_commit_patch_intersections`, and persists the schema-validated built Agent Trace payload, including optional top-level `tool` metadata from recent diff-trace rows, top-level `metadata.sce.version` from the compiled `sce` CLI package version, and range-level `content_hash` values, to AgentTraceDb `agent_traces` (DB-only, no post-commit Agent Trace file artifact); and `diff-trace` currently validates/persists required non-empty `sessionID`/`diff`/`model_id`/`tool_name`, required `tool_version` (must be present and either `null` or a non-empty string), plus required `u64` millisecond `time`, with non-lossy AgentTraceDb `time_ms` conversion and collision-safe timestamp+attempt artifact filenames. The CLI now also includes an approved operator-environment doctor contract documented in `context/sce/agent-trace-hook-doctor.md`; the runtime now matches the implemented T06 slice for `sce doctor --fix` parsing/help, stable problem/fix-result reporting, canonical hook-repair reuse, and bounded doctor-owned local-DB directory bootstrap for the missing SCE-owned DB parent path. -The local DB service now provides `LocalDb` as a thin `TursoDb` alias in `cli/src/services/local_db/mod.rs`; `LocalDbSpec` resolves the canonical local DB path from the shared default-path catalog and currently declares zero migrations. Shared Turso infrastructure lives in `cli/src/services/db/mod.rs`, where `DbSpec` and generic `TursoDb` support dual-mode operation — local mode via `turso::Builder::new_local()` when `SCE_SYNC_URL`+`SCE_SYNC_TOKEN` are absent, or sync (Turso Cloud) mode via `turso::sync::Builder::new_remote()` when both are set. It owns parent-directory creation, connection setup, tokio current-thread runtime bridging, synchronous `execute`/`query`/`query_map`, generic migration execution, sync operations (`push`/`pull`/`checkpoint`/`stats`) that are no-ops in local mode (sync is never triggered automatically from `execute()`), and shared DB lifecycle helpers for service-specific database wrappers. Agent Trace persistence now has its own `cli/src/services/agent_trace_db/mod.rs` wrapper, canonical `/sce/agent-trace.db` path, a split fresh-start baseline migration set (`001..005`) covering `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, and indexes (`idx_diff_traces_time_ms_id`, `idx_agent_traces_agent_trace_id`) without `AUTOINCREMENT`, plus `agent_traces.agent_trace_id` as `NOT NULL UNIQUE`; it also provides typed parameterized insert helpers for diff traces, post-commit intersection rows, and built agent-trace rows, chronological recent `diff_traces` query/parse support with malformed-row skip accounting, registered setup/doctor lifecycle provider, active `sce hooks diff-trace` writes for `diff_traces`, and active `sce hooks post-commit` writes for built `agent_traces` payloads. +The local DB service now provides `LocalDb` as a thin `TursoDb` alias in `cli/src/services/local_db/mod.rs`; `LocalDbSpec` resolves the canonical local DB path from the shared default-path catalog and currently declares zero migrations. Shared Turso infrastructure lives in `cli/src/services/db/mod.rs`, where `DbSpec` and generic `TursoDb` support dual-mode operation — local mode via `turso::Builder::new_local()` when `SCE_SYNC_URL`+`SCE_SYNC_TOKEN` are absent, or sync (Turso Cloud) mode via `turso::sync::Builder::new_remote()` when both are set. It owns parent-directory creation, connection setup, tokio current-thread runtime bridging, synchronous `execute`/`query`/`query_map`, generic migration execution, sync operations (`push`/`pull`/`checkpoint`/`stats`) that are no-ops in local mode (sync is never triggered automatically from `execute()`), and shared DB lifecycle helpers for service-specific database wrappers. Auth DB persistence now has a thin encrypted wrapper in `cli/src/services/auth_db/mod.rs`: `AuthDb = EncryptedTursoDb` resolves `/sce/auth.db` and embeds ordered `auth_tokens` table/index migrations, with lifecycle registration wired through `AuthDbLifecycle` in `cli/src/services/auth_db/lifecycle.rs`; auth runtime token-storage replacement remains deferred. Agent Trace persistence now has its own `cli/src/services/agent_trace_db/mod.rs` wrapper, canonical `/sce/agent-trace.db` path, a split fresh-start baseline migration set (`001..005`) covering `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, and indexes (`idx_diff_traces_time_ms_id`, `idx_agent_traces_agent_trace_id`) without `AUTOINCREMENT`, plus `agent_traces.agent_trace_id` as `NOT NULL UNIQUE`; it also provides typed parameterized insert helpers for diff traces, post-commit intersection rows, and built agent-trace rows, chronological recent `diff_traces` query/parse support with malformed-row skip accounting, registered setup/doctor lifecycle provider, active `sce hooks diff-trace` writes for `diff_traces`, and active `sce hooks post-commit` writes for built `agent_traces` payloads. The hooks command surface now also supports concrete runtime subcommand routing (`pre-commit`, `commit-msg`, `post-commit`, `post-rewrite`, `diff-trace`) with deterministic argument/STDIN validation. Current runtime behavior keeps attribution disabled by default: the attribution gate enables canonical trailer insertion in `commit-msg`, `pre-commit`/`post-rewrite` remain deterministic no-ops, `post-commit` is the active bounded recent-diff-trace intersection path, and `diff-trace` is the active intake path for parsed STDIN `{ sessionID, diff, time, model_id, tool_name, tool_version }` payload persistence with required non-empty `tool_name`, required nullable/non-empty `tool_version`, required `u64` millisecond `time`, non-lossy AgentTraceDb `time_ms` conversion, and collision-safe timestamp+attempt artifact filenames. This behavior is documented in `context/sce/agent-trace-hooks-command-routing.md`. The setup service now also exposes deterministic required-hook embedded asset accessors (`iter_required_hook_assets`, `get_required_hook_asset`) backed by canonical templates in `cli/assets/hooks/` for `pre-commit`, `commit-msg`, and `post-commit`; this behavior is documented in `context/sce/setup-githooks-hook-asset-packaging.md`. The setup service now also includes required-hook install orchestration (`install_required_git_hooks`) that resolves repository root and effective hooks path from git truth, enforces deterministic per-hook outcomes (`Installed`/`Updated`/`Skipped`), and uses a unified remove-and-replace policy that removes existing hooks before swapping staged content with deterministic recovery guidance on swap failures; this behavior is documented in `context/sce/setup-githooks-install-flow.md`. diff --git a/context/patterns.md b/context/patterns.md index ba2390e8..48979b93 100644 --- a/context/patterns.md +++ b/context/patterns.md @@ -122,7 +122,7 @@ - For setup command messaging, emit deterministic completion output that includes selected target(s) and per-target install counts. - Keep module seams for future domains present and compile-safe even when behavior is deferred. - Keep dependency additions explicit and minimal in `cli/Cargo.toml`, and anchor dependency intent in domain-owned service types/tests rather than a separate compile-time dependency snapshot module. -- Route local Turso access through service adapters so command handlers do not expose low-level `turso` API details. New Turso-backed services should build on `cli/src/services/db/mod.rs` (`DbSpec` + `TursoDb`) for runtime, connection, per-database `__sce_migrations` tracking, and migration infrastructure, then expose domain-specific methods from their own service modules. +- Route local Turso access through service adapters so command handlers do not expose low-level `turso` API details. New Turso-backed services should build on `cli/src/services/db/mod.rs` (`DbSpec` + `TursoDb`, or `EncryptedTursoDb` when at-rest encryption is required) for runtime, connection, per-database `__sce_migrations` tracking, and migration infrastructure, then expose domain-specific methods from their own service modules. - For current local DB flows, route initialization through the dedicated adapter (`cli/src/services/local_db/mod.rs`) and invoke it from approved orchestration surfaces such as setup or doctor rather than exposing a partial user command before its contract is approved. - For Turso-backed services with setup/doctor ownership, add service-owned lifecycle providers that reuse shared DB path-health and parent-bootstrap helpers, then register them through `lifecycle_providers()` instead of adding command-local database checks. - For transient local IO/database hotspots, apply bounded resilience wrappers with explicit retry count, timeout, and capped backoff (`cli/src/services/resilience.rs`) and surface terminal failures with deterministic `Try:` remediation guidance. @@ -138,7 +138,7 @@ - For commit-msg co-author policy seams, gate canonical trailer insertion on runtime controls (`SCE_DISABLED` plus the shared attribution-hooks enablement gate), and enforce idempotent dedupe so allowed cases end with exactly one `Co-authored-by: SCE ` trailer. - For local hook attribution flows, resolve the top-level enablement gate through the shared config precedence model (`SCE_ATTRIBUTION_HOOKS_ENABLED` over `policies.attribution_hooks.enabled`, default `false`) so commit-msg attribution stays disabled by default without adding hook-specific config parsing. - Do not assume post-commit persistence, retry replay, remap ingestion, or rewrite trace transformation are active in the current local-hook runtime; those paths are removed from the current baseline. -- For the current local DB baseline, resolve one deterministic per-user persistent DB target (Linux: `${XDG_STATE_HOME:-~/.local/state}/sce/local.db`; platform-equivalent state roots elsewhere), keep the path neutral rather than Agent Trace-branded, create parent directories before first use, and route initialization through `LocalDb::new()`. As database services split, keep path/migration ownership in each `DbSpec`: `LocalDbSpec` owns the neutral local DB path with zero migrations, `AgentTraceDbSpec` owns `/sce/agent-trace.db` plus ordered Agent Trace migrations, and shared Turso mechanics plus migration metadata stay in `TursoDb`. +- For the current local DB baseline, resolve one deterministic per-user persistent DB target (Linux: `${XDG_STATE_HOME:-~/.local/state}/sce/local.db`; platform-equivalent state roots elsewhere), keep the path neutral rather than Agent Trace-branded, create parent directories before first use, and route initialization through `LocalDb::new()`. As database services split, keep path/migration ownership in each `DbSpec`: `LocalDbSpec` owns the neutral local DB path with zero migrations, `AuthDbSpec` owns encrypted `/sce/auth.db` plus ordered auth migrations, `AgentTraceDbSpec` owns `/sce/agent-trace.db` plus ordered Agent Trace migrations, and shared Turso mechanics plus migration metadata stay in `TursoDb` / `EncryptedTursoDb`. - For hosted event intake seams, verify provider signatures before payload parsing (GitHub `sha256=` HMAC over body, GitLab token-equality secret check), resolve old/new heads from provider payload fields, and derive deterministic reconciliation run idempotency keys from provider+event+repo+head tuple material. - For hosted rewrite mapping seams, resolve candidates deterministically in strict precedence order (patch-id exact, then range-diff score, then fuzzy score), classify top-score ties as `ambiguous`, enforce low-confidence unresolved behavior below `0.60`, and preserve stable outcome ordering via canonical candidate SHA sorting. - For hosted reconciliation observability, publish run-level mapped/unmapped counts, confidence histogram buckets, runtime timing, and normalized error-class labels so retry/quality drift can be monitored without requiring a full dashboard surface. diff --git a/context/plans/encrypted-auth-db.md b/context/plans/encrypted-auth-db.md new file mode 100644 index 00000000..44baa148 --- /dev/null +++ b/context/plans/encrypted-auth-db.md @@ -0,0 +1,103 @@ +# Plan: Encrypted auth DB module + +## Change summary + +Create a new `auth_db` service for encrypted local persistence of auth tokens and related user information. The service will mirror the structure and conventions of `local_db` and `agent_trace_db`, use the shared `EncryptedTursoDb` adapter, embed ordered SQL migrations from a new auth migration directory, and expose lifecycle setup/doctor integration through `lifecycle.rs`. + +## Success criteria + +- `cli/src/services/auth_db/mod.rs` exists and follows the thin database-wrapper pattern used by `local_db` and `agent_trace_db`. +- `AuthDb` is a type alias for `EncryptedTursoDb`. +- `AuthDbSpec` implements `DbSpec` with a canonical auth DB path, diagnostic name, and ordered embedded migrations. +- A new auth migration directory exists under `cli/migrations/auth/`. +- The baseline migration creates a single `auth_tokens` table with: + - `id` required primary key, without `AUTOINCREMENT` + - `access_token` required + - `token_type` required + - `expires_in` required + - `refresh_token` required + - `scope` optional + - `stored_at_unix_seconds` required + - `email` required + - `created_at` required +- All `auth_tokens` columns are `NOT NULL` except `scope`. +- An index exists on `auth_tokens(email)`. +- `cli/src/services/auth_db/lifecycle.rs` follows the existing `ServiceLifecycle` pattern for path diagnosis, parent bootstrap, and setup initialization through `AuthDb::new()`. +- Required module/path/lifecycle wiring compiles without changing runtime auth-token read/write behavior yet. +- `nix flake check` passes. + +## Constraints and non-goals + +- **In scope**: new `auth_db` module files, new auth migration SQL files, canonical path resolver, service export, lifecycle provider registration, and context sync for the new current-state DB surface. +- **Out of scope**: replacing existing auth token storage, adding auth runtime reads/writes, token refresh behavior, token encryption/key management beyond using the existing `EncryptedTursoDb` adapter and `SCE_DB_ENCRYPTION_KEY`, cloud sync behavior, and any schema beyond the requested single `auth_tokens` table plus email index. +- Reuse existing dependencies and database infrastructure; do not add a new database library. +- Follow existing naming, migration embedding, lifecycle, and error-context conventions from `local_db` and `agent_trace_db`. +- Use forward-only embedded migrations consistent with current database modules. + +## Assumptions + +- The module path is `cli/src/services/auth_db/{mod.rs,lifecycle.rs}`. +- The table name is `auth_tokens`. +- The canonical database path should be added to the shared default-path catalog as `/sce/auth.db`, unless implementation review identifies an already-approved auth DB path in the PR. +- “Wiring” means the minimal non-runtime integration needed for the new module to compile and participate in setup/doctor lifecycle flows: `services/mod.rs`, `default_paths.rs`, and `services/lifecycle.rs` updates. It does not include changing auth command/token-storage behavior. + +## Task stack + +- [x] T01: `Add auth DB path and migration files` (status:done) + - Task ID: T01 + - Goal: Add the canonical auth DB path resolver and auth migration SQL files that define the requested encrypted database schema. + - Boundaries (in/out of scope): In — add `auth_db_path()` to `cli/src/services/default_paths.rs`; create `cli/migrations/auth/001_create_auth_tokens.sql`; create `cli/migrations/auth/002_create_auth_tokens_email_index.sql` or equivalent ordered split migrations. Out — no Rust `auth_db` module implementation yet, no auth runtime writes, no lifecycle provider registration. + - Done when: The path resolver returns `/sce/auth.db`; the baseline table migration creates `auth_tokens` with the requested columns, `id` primary key without `AUTOINCREMENT`, and all columns `NOT NULL` except `scope`; the email index migration creates an index on `email`; migration SQL is idempotent in the same style as Agent Trace DB migrations. + - Verification notes (commands or checks): Inspect SQL for schema compliance; run targeted Rust compile/format checks during implementation if path changes require compile validation. + - Completed: 2026-05-25 + - Files changed: `cli/src/services/default_paths.rs`, `cli/migrations/auth/001_create_auth_tokens.sql`, `cli/migrations/auth/002_create_auth_tokens_email_index.sql` + - Evidence: `nix develop -c sh -c 'cd cli && cargo check'` passed; `nix develop -c sh -c 'cd cli && cargo fmt -- --check'` passed; SQL inspection confirmed the required `auth_tokens` table columns/constraints and `idx_auth_tokens_email` idempotent index. + - Context sync classification: localized implementation change with default-path/current-state drift repaired in root and domain context during `sce-context-sync`. + +- [x] T02: `Create auth_db mod.rs using EncryptedTursoDb` (status:done) + - Task ID: T02 + - Goal: Create `cli/src/services/auth_db/mod.rs` as the encrypted database wrapper for auth token persistence. + - Boundaries (in/out of scope): In — define `AuthDbSpec`, `pub type AuthDb = EncryptedTursoDb`, embed ordered auth migrations with `include_str!`, implement `DbSpec` using `auth_db_path()`, and expose `pub mod lifecycle;`. Out — no domain-specific insert/query helpers unless needed for compile-only tests, no auth command integration, no token-storage replacement. + - Done when: `AuthDb::new()` would open the encrypted auth DB through `EncryptedTursoDb`, require `SCE_DB_ENCRYPTION_KEY` via the shared adapter, and run the auth migrations through `__sce_migrations`. + - Verification notes (commands or checks): `nix develop -c sh -c 'cd cli && cargo check'`; inspect that the module mirrors `local_db`/`agent_trace_db` naming and migration style. + - Completed: 2026-05-25 + - Files changed: `cli/src/services/auth_db/mod.rs`, `cli/src/services/mod.rs` + - Evidence: `nix develop -c sh -c 'cd cli && cargo check'` passed; `nix develop -c sh -c 'cd cli && cargo fmt -- --check'` passed; inspection confirmed `AuthDbSpec` uses `auth_db_path()`, migration IDs remain ordered, and `AuthDb` aliases `EncryptedTursoDb`. + - Notes: `pub mod lifecycle;` and `cli/src/services/auth_db/lifecycle.rs` remain deferred to T03 per readiness decision; `AuthDb` is marked `#[allow(dead_code)]` until lifecycle/runtime wiring consumes it. + - Context sync classification: important localized DB-service state change; updated auth DB and shared Turso/domain context plus root discoverability/current-state references. + +- [x] T03: `Add auth DB lifecycle integration` (status:done) + - Task ID: T03 + - Goal: Create `cli/src/services/auth_db/lifecycle.rs` and wire the provider into the shared lifecycle catalog. + - Boundaries (in/out of scope): In — implement `AuthDbLifecycle` with `diagnose`, `fix`, and `setup` following `LocalDbLifecycle` and `AgentTraceDbLifecycle`; use shared `collect_db_path_health()` and `bootstrap_db_parent()` helpers; add a `LifecycleProviderId::AuthDb`; register `AuthDbLifecycle` in deterministic provider order; export `pub mod auth_db;` from `services/mod.rs`. Out — no doctor renderer redesign, no setup output shape changes beyond existing lifecycle aggregation behavior. + - Done when: Setup initializes the encrypted auth DB through lifecycle aggregation, doctor/fix can diagnose/bootstrap the auth DB parent path, and provider order remains deterministic with auth DB placed alongside the other DB providers. + - Verification notes (commands or checks): `nix develop -c sh -c 'cd cli && cargo check'`; inspect `lifecycle_providers(include_hooks)` order and provider ID coverage. + - Completed: 2026-05-25 + - Files changed: `cli/src/services/auth_db/lifecycle.rs`, `cli/src/services/auth_db/mod.rs`, `cli/src/services/lifecycle.rs` + - Evidence: `cargo check` passed; `cargo fmt -- --check` passed; provider order confirmed: config → `local_db` → `auth_db` → `agent_trace_db` → hooks; `LifecycleProviderId::AuthDb` added to enum. + - Context sync classification: important change — new lifecycle provider and enum variant affect cross-cutting service lifecycle behavior. + +- [ ] T04: `Add focused tests for auth DB schema and lifecycle wiring` (status:todo) + - Task ID: T04 + - Goal: Add narrow tests that prove the new auth DB migration list and lifecycle wiring stay deterministic. + - Boundaries (in/out of scope): In — module-level tests or existing lifecycle tests verifying migration IDs/order, `auth_tokens` schema/index SQL presence, path resolver behavior if covered by existing default-path tests, and lifecycle provider inclusion/order. Out — integration tests that require real auth login, WorkOS calls, or production token data. + - Done when: Tests fail if the auth DB provider is not registered, migration ordering changes unexpectedly, or the required table/index SQL is missing required constraints. + - Verification notes (commands or checks): Prefer targeted test commands during implementation, then rely on `nix flake check` for final coverage. + +- [ ] T05: `Sync context for encrypted auth DB current state` (status:todo) + - Task ID: T05 + - Goal: Update durable context so future sessions know the auth DB module, encrypted adapter usage, migration schema, path, and lifecycle registration exist. + - Boundaries (in/out of scope): In — update focused context files such as `context/sce/shared-turso-db.md`, a new or existing auth DB context file, `context/context-map.md`, and glossary/overview entries only if the change is important at those scopes. Out — completed-work narration in durable context, unrelated Agent Trace or local DB rewrites. + - Done when: Context describes the resulting current state rather than task history, and no stale statements conflict with the new auth DB surface. + - Verification notes (commands or checks): Review context files against code truth after implementation; ensure `context/plans/encrypted-auth-db.md` remains the active execution artifact. + +- [ ] T06: `Final validation and cleanup` (status:todo) + - Task ID: T06 + - Goal: Run the full repo validation pass, remove temporary scaffolding, and confirm all plan success criteria are met. + - Boundaries (in/out of scope): In — full test/lint/format validation, generated-output parity, temporary-file cleanup, and success-criteria evidence capture in this plan. Out — new auth DB runtime features or schema expansion. + - Done when: `nix flake check` passes, `nix run .#pkl-check-generated` passes, no task-owned temporary scaffolding remains, and this plan records validation evidence for every success criterion. + - Verification notes (commands or checks): `nix run .#pkl-check-generated`; `nix flake check`. + +## Open questions + +None for planning. The implementation should call out before coding if the current PR already introduced a canonical auth DB path that differs from the `/sce/auth.db` assumption. diff --git a/context/sce/auth-db.md b/context/sce/auth-db.md new file mode 100644 index 00000000..9cf7803c --- /dev/null +++ b/context/sce/auth-db.md @@ -0,0 +1,46 @@ +# Auth DB + +The encrypted auth DB foundation currently consists of a thin Rust wrapper plus path and migration assets. Runtime auth-token reads/writes still use the existing token-storage path. + +## Implemented surface + +- Canonical path resolver: `cli/src/services/default_paths.rs::auth_db_path()`. +- Database file path: `/sce/auth.db`. +- Service wrapper: `cli/src/services/auth_db/mod.rs`. +- `AuthDbSpec` implements `DbSpec` with diagnostic name `auth DB`, `auth_db_path()`, and ordered embedded auth migrations. +- `AuthDb` is a type alias for `EncryptedTursoDb`, consumed by the lifecycle provider at `cli/src/services/auth_db/lifecycle.rs`. +- Migration directory: `cli/migrations/auth/`. +- Ordered migrations: + - `001_create_auth_tokens.sql` + - `002_create_auth_tokens_email_index.sql` + +## Schema baseline + +`auth_tokens` is created idempotently with: + +- `id TEXT PRIMARY KEY NOT NULL` +- `access_token TEXT NOT NULL` +- `token_type TEXT NOT NULL` +- `expires_in INTEGER NOT NULL` +- `refresh_token TEXT NOT NULL` +- `scope TEXT` (nullable) +- `stored_at_unix_seconds INTEGER NOT NULL` +- `email TEXT NOT NULL` +- `created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))` + +The email lookup index is `idx_auth_tokens_email` on `auth_tokens(email)`. + +## Lifecycle integration (T03) + +`AuthDbLifecycle` is registered in `cli/src/services/auth_db/lifecycle.rs` following the existing DB lifecycle pattern: +- `diagnose` collects auth DB path health problems. +- `fix` bootstraps missing auth DB parent directory. +- `setup` calls `AuthDb::new()` to initialize the encrypted database. +- `LifecycleProviderId::AuthDb` is the provider identifier. +- The lifecycle provider is registered in deterministic order: config → local_db → auth_db → agent_trace_db → hooks. + +## Not yet wired + +- Existing auth command token storage still uses the current runtime path; auth reads/writes are not redirected to this DB. + +See also: [shared-turso-db.md](shared-turso-db.md), [../cli/default-path-catalog.md](../cli/default-path-catalog.md), [../context-map.md](../context-map.md) diff --git a/context/sce/shared-turso-db.md b/context/sce/shared-turso-db.md index a80871a6..de1479b5 100644 --- a/context/sce/shared-turso-db.md +++ b/context/sce/shared-turso-db.md @@ -26,8 +26,9 @@ The shared module is exported from `cli/src/services/mod.rs` and compile-checked - `cli/src/services/local_db/mod.rs`: `LocalDb = TursoDb`, with `LocalDbSpec` resolving `local_db_path()` and declaring zero migrations. - `cli/src/services/agent_trace_db/mod.rs`: `AgentTraceDb = TursoDb`, with `AgentTraceDbSpec` resolving `agent_trace_db_path()` and loading ordered Agent Trace migrations for `diff_traces` and `post_commit_patch_intersections`. +- `cli/src/services/auth_db/mod.rs`: `AuthDb = EncryptedTursoDb`, with `AuthDbSpec` resolving `auth_db_path()` and loading ordered auth migrations for `auth_tokens` plus the email index. -Both database wrappers now have lifecycle providers. `lifecycle_providers(include_hooks)` registers database providers in order `LocalDbLifecycle` → `AgentTraceDbLifecycle` before optional hooks, so setup initializes both databases and doctor diagnoses/fixes both canonical DB paths. +All three database wrappers (local DB, auth DB, Agent Trace DB) have lifecycle providers. `lifecycle_providers(include_hooks)` registers database providers in order `LocalDbLifecycle` → `AuthDbLifecycle` → `AgentTraceDbLifecycle` before optional hooks, so setup initializes all three databases and doctor diagnoses/fixes all three canonical DB paths. ## Migration metadata @@ -35,4 +36,4 @@ Both database wrappers now have lifecycle providers. `lifecycle_providers(includ Existing databases created before migration metadata are upgraded by re-applying the current idempotent migration list and recording each migration ID. This lets later `sce setup` / lifecycle initialization runs apply migrations added after the database file already existed, including Agent Trace DB schema/index additions. -See also: [local-db.md](local-db.md), [agent-trace-db.md](agent-trace-db.md), [overview.md](../overview.md), [architecture.md](../architecture.md), [glossary.md](../glossary.md) +See also: [local-db.md](local-db.md), [agent-trace-db.md](agent-trace-db.md), [auth-db.md](auth-db.md), [overview.md](../overview.md), [architecture.md](../architecture.md), [glossary.md](../glossary.md) From 186bfce0110b07f03e9ef47e1914c955037882c4 Mon Sep 17 00:00:00 2001 From: stefanskoricdev Date: Mon, 25 May 2026 16:31:14 +0200 Subject: [PATCH 3/7] auth_db: Update auth token schema and rename auth_tokens table to auth_credentials Replace baseline auth schema/table naming from auth_tokens to auth_credentials. Add updated_at column and an auto-update trigger to track credential modification timestamp Co-authored-by: SCE --- .../auth/001_create_auth_tokens.sql | 6 +- ...te_auth_credentials_updated_at_trigger.sql | 9 + .../002_create_auth_tokens_email_index.sql | 2 - cli/src/services/auth_db/mod.rs | 183 +++++++++++++++++- context/architecture.md | 4 +- context/cli/cli-command-surface.md | 14 +- context/cli/service-lifecycle.md | 2 +- context/context-map.md | 4 +- context/glossary.md | 4 +- context/sce/agent-trace-hook-doctor.md | 2 +- context/sce/auth-db.md | 13 +- .../sce/setup-repo-local-config-bootstrap.md | 2 +- context/sce/shared-turso-db.md | 2 +- 13 files changed, 214 insertions(+), 33 deletions(-) create mode 100644 cli/migrations/auth/002_create_auth_credentials_updated_at_trigger.sql delete mode 100644 cli/migrations/auth/002_create_auth_tokens_email_index.sql diff --git a/cli/migrations/auth/001_create_auth_tokens.sql b/cli/migrations/auth/001_create_auth_tokens.sql index 1160f28f..974a00c6 100644 --- a/cli/migrations/auth/001_create_auth_tokens.sql +++ b/cli/migrations/auth/001_create_auth_tokens.sql @@ -1,4 +1,4 @@ -CREATE TABLE IF NOT EXISTS auth_tokens ( +CREATE TABLE IF NOT EXISTS auth_credentials ( id TEXT PRIMARY KEY NOT NULL, access_token TEXT NOT NULL, token_type TEXT NOT NULL, @@ -6,6 +6,6 @@ CREATE TABLE IF NOT EXISTS auth_tokens ( refresh_token TEXT NOT NULL, scope TEXT, stored_at_unix_seconds INTEGER NOT NULL, - email TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) ); diff --git a/cli/migrations/auth/002_create_auth_credentials_updated_at_trigger.sql b/cli/migrations/auth/002_create_auth_credentials_updated_at_trigger.sql new file mode 100644 index 00000000..d4e2354e --- /dev/null +++ b/cli/migrations/auth/002_create_auth_credentials_updated_at_trigger.sql @@ -0,0 +1,9 @@ +CREATE TRIGGER IF NOT EXISTS auth_credentials_set_updated_at +AFTER UPDATE ON auth_credentials +FOR EACH ROW +WHEN NEW.updated_at = OLD.updated_at +BEGIN + UPDATE auth_credentials + SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') + WHERE id = NEW.id; +END; diff --git a/cli/migrations/auth/002_create_auth_tokens_email_index.sql b/cli/migrations/auth/002_create_auth_tokens_email_index.sql deleted file mode 100644 index f988907d..00000000 --- a/cli/migrations/auth/002_create_auth_tokens_email_index.sql +++ /dev/null @@ -1,2 +0,0 @@ -CREATE INDEX IF NOT EXISTS idx_auth_tokens_email -ON auth_tokens (email); diff --git a/cli/src/services/auth_db/mod.rs b/cli/src/services/auth_db/mod.rs index 593c3670..47f341e8 100644 --- a/cli/src/services/auth_db/mod.rs +++ b/cli/src/services/auth_db/mod.rs @@ -9,16 +9,19 @@ use crate::services::{ default_paths::auth_db_path, }; -const CREATE_AUTH_TOKENS_MIGRATION: &str = +const CREATE_AUTH_CREDENTIALS_MIGRATION: &str = include_str!("../../../migrations/auth/001_create_auth_tokens.sql"); -const CREATE_AUTH_TOKENS_EMAIL_INDEX_MIGRATION: &str = - include_str!("../../../migrations/auth/002_create_auth_tokens_email_index.sql"); +const CREATE_AUTH_CREDENTIALS_UPDATED_AT_TRIGGER_MIGRATION: &str = + include_str!("../../../migrations/auth/002_create_auth_credentials_updated_at_trigger.sql"); const AUTH_MIGRATIONS: &[(&str, &str)] = &[ - ("001_create_auth_tokens", CREATE_AUTH_TOKENS_MIGRATION), ( - "002_create_auth_tokens_email_index", - CREATE_AUTH_TOKENS_EMAIL_INDEX_MIGRATION, + "001_create_auth_credentials", + CREATE_AUTH_CREDENTIALS_MIGRATION, + ), + ( + "002_create_auth_credentials_updated_at_trigger", + CREATE_AUTH_CREDENTIALS_UPDATED_AT_TRIGGER_MIGRATION, ), ]; @@ -43,3 +46,171 @@ impl DbSpec for AuthDbSpec { pub type AuthDb = EncryptedTursoDb; pub mod lifecycle; + +#[cfg(test)] +mod tests { + use std::{ + collections::HashMap, + fs, + path::PathBuf, + sync::OnceLock, + time::{SystemTime, UNIX_EPOCH}, + }; + + use super::*; + use crate::services::{ + db::{DbSpec, TursoDb}, + lifecycle::{lifecycle_providers, LifecycleProviderId}, + }; + use anyhow::Context; + + static TEST_DB_PATH: OnceLock = OnceLock::new(); + + struct TestAuthDbSpec; + + impl DbSpec for TestAuthDbSpec { + fn db_name() -> &'static str { + "test auth DB" + } + + fn db_path() -> Result { + TEST_DB_PATH + .get() + .cloned() + .context("test DB path should be initialized") + } + + fn migrations() -> &'static [(&'static str, &'static str)] { + AUTH_MIGRATIONS + } + } + + fn unique_test_db_path() -> PathBuf { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system time should be after Unix epoch") + .as_nanos(); + std::env::temp_dir() + .join(format!("sce-auth-db-test-{}-{nonce}", std::process::id())) + .join("auth.db") + } + + fn sqlite_object_exists(db: &TursoDb, object_type: &str, name: &str) -> bool { + let rows: Vec = db + .query_map( + "SELECT name FROM sqlite_master WHERE type = ?1 AND name = ?2", + (object_type, name), + |row| row.get::(0).map_err(anyhow::Error::from), + ) + .expect("sqlite_master query should succeed"); + !rows.is_empty() + } + + fn applied_migration_ids(db: &TursoDb) -> Vec { + db.query_map( + "SELECT id FROM __sce_migrations ORDER BY id ASC", + (), + |row| row.get::(0).map_err(anyhow::Error::from), + ) + .expect("migration metadata query should succeed") + } + + #[test] + fn auth_db_baseline_migration_creates_table_index_and_constraints() { + let db_path = unique_test_db_path(); + TEST_DB_PATH + .set(db_path.clone()) + .expect("test DB path should only be initialized once"); + + let db = TursoDb::::new().expect("test auth DB should open"); + + // Verify table, trigger, and migration IDs + assert!(sqlite_object_exists(&db, "table", "auth_credentials")); + assert!(sqlite_object_exists( + &db, + "trigger", + "auth_credentials_set_updated_at" + )); + + // Verify migration IDs are ordered + assert_eq!( + applied_migration_ids(&db), + vec![ + "001_create_auth_credentials", + "002_create_auth_credentials_updated_at_trigger", + ] + ); + + // Verify column NOT NULL constraints via PRAGMA table_info + // Returns: cid, name, type, notnull, dflt_value, pk + let columns: Vec<(String, i32)> = db + .query_map( + "SELECT name, \"notnull\" FROM pragma_table_info('auth_credentials') ORDER BY cid", + (), + |row| { + let name: String = row.get::(0)?; + let notnull: i32 = row.get::(1)?; + Ok((name, notnull)) + }, + ) + .expect("pragma table_info should succeed"); + + let col_map: HashMap = columns.into_iter().collect(); + + // Required columns must be NOT NULL + for col in &[ + "id", + "access_token", + "token_type", + "expires_in", + "refresh_token", + "stored_at_unix_seconds", + "created_at", + "updated_at", + ] { + assert_eq!( + col_map.get(*col), + Some(&1), + "column '{col}' should be NOT NULL" + ); + } + + // scope must allow NULL + assert_eq!( + col_map.get("scope"), + Some(&0), + "column 'scope' should allow NULL" + ); + + if let Some(parent) = db_path.parent() { + fs::remove_dir_all(parent).expect("test DB directory should be removed"); + } + } + + #[test] + fn auth_db_lifecycle_provider_included() { + let providers = lifecycle_providers(false); + + let auth_count = providers + .iter() + .filter(|p| p.id() == LifecycleProviderId::AuthDb) + .count(); + assert_eq!( + auth_count, 1, + "AuthDb lifecycle provider should be registered exactly once" + ); + + // Verify deterministic order: Config -> LocalDb -> AuthDb -> AgentTraceDb + let provider_ids: Vec = providers.iter().map(|p| p.id()).collect(); + assert_eq!( + provider_ids, + vec![ + LifecycleProviderId::Config, + LifecycleProviderId::LocalDb, + LifecycleProviderId::AuthDb, + LifecycleProviderId::AgentTraceDb, + ], + "lifecycle provider order should be deterministic: Config -> LocalDb -> AuthDb -> AgentTraceDb" + ); + } +} diff --git a/context/architecture.md b/context/architecture.md index 8ba5c69a..8c3d5daa 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -106,10 +106,10 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - `cli/src/services/auth_command/mod.rs` defines the implemented auth command surface for `sce auth login|renew|logout|status`, including device-flow login, stored-token renewal (`--force` supported for renew), logout, and status rendering in text/JSON formats; `cli/src/services/auth_command/command.rs` owns the `AuthCommand` struct and its `RuntimeCommand` impl. - `cli/src/services/db/mod.rs` provides the shared generic Turso infrastructure seam: `DbSpec` supplies a service-specific name, path, and ordered embedded migrations, while `TursoDb` owns parent-directory creation, `Builder::new_local(...)` initialization, Turso connection setup, tokio current-thread runtime bridging, blocking `execute`/`query`/`query_map` wrappers, and generic migration execution with per-database `__sce_migrations` metadata. Existing DB files without migration metadata are upgraded by re-applying the current idempotent migration set and recording each migration ID, so setup/lifecycle initialization applies later migrations to already-created databases. The same module owns shared DB lifecycle helpers for path-health problem collection and DB parent-directory bootstrap. - `cli/src/services/local_db/mod.rs` provides the concrete local DB spec and `LocalDb` type alias over the shared generic `TursoDb` adapter. `LocalDbSpec` resolves the deterministic persistent runtime DB target through the shared default-path seam and declares no local migrations; `TursoDb` supplies blocking `execute`/`query`, parent-directory creation, Turso connection setup, tokio current-thread runtime bridging, and generic migration execution. -- `cli/src/services/auth_db/mod.rs` provides the encrypted auth DB spec and `AuthDb` type alias over `EncryptedTursoDb`. `AuthDbSpec` resolves `/sce/auth.db` through the shared default-path seam and embeds ordered auth migrations for the `auth_tokens` table plus `idx_auth_tokens_email`. Auth DB lifecycle setup/doctor integration is wired through `AuthDbLifecycle`; auth command/token-storage reads/writes are not redirected to this DB yet. +- `cli/src/services/auth_db/mod.rs` provides the encrypted auth DB spec and `AuthDb` type alias over `EncryptedTursoDb`. `AuthDbSpec` resolves `/sce/auth.db` through the shared default-path seam and embeds ordered auth migrations where baseline SQL creates `auth_credentials` without `user_id`, with `updated_at`, and a trigger that auto-refreshes `updated_at` on row updates. Auth DB lifecycle setup/doctor integration is wired through `AuthDbLifecycle`; auth command/token-storage reads/writes are not redirected to this DB yet. - `cli/src/services/agent_trace_db/mod.rs` provides the Agent Trace DB spec and `AgentTraceDb` type alias over `TursoDb`. `AgentTraceDbSpec` resolves `/sce/agent-trace.db` through the shared default-path seam and embeds an ordered split fresh-start baseline migration set (`001_create_diff_traces`, `002_create_post_commit_patch_intersections`, `003_create_agent_traces`, `004_create_diff_traces_time_ms_id_index`, `005_create_agent_traces_agent_trace_id_index`) without `AUTOINCREMENT`; `agent_traces.agent_trace_id` is `NOT NULL UNIQUE` and indexed by `idx_agent_traces_agent_trace_id`. The module adds `DiffTraceInsert<'_>`/`insert_diff_trace()` (including `model_id`, `tool_name`, and nullable `tool_version` writes), `PostCommitPatchIntersectionInsert<'_>`/`insert_post_commit_patch_intersection()`, and `AgentTraceInsert<'_>`/`insert_agent_trace()` for parameterized writes plus `recent_diff_trace_patches(cutoff_time_ms, end_time_ms)` for inclusive chronological `diff_traces` reads that parse valid raw patch text and return skipped malformed-row reports. `cli/src/services/agent_trace_db/lifecycle.rs` registers Agent Trace DB setup/doctor lifecycle behavior; runtime writes come from `sce hooks diff-trace` (`diff_traces`) and `sce hooks post-commit` (`post_commit_patch_intersections` + built `agent_traces`). - `cli/src/test_support.rs` provides a shared test-only temp-directory helper (`TestTempDir`) used by service tests that need filesystem fixtures. -- `cli/src/services/setup/mod.rs` defines the setup command contract (`SetupMode`, `SetupTarget`, `SetupRequest`, CLI flag parser/validator), an `inquire`-backed interactive target prompter (`InquireSetupTargetPrompter`), setup dispatch outcomes (proceed/cancelled), compile-time embedded asset access (`EmbeddedAsset`, target-scoped iterators, required-hook asset iterators/lookups) generated by `cli/build.rs` from the ephemeral crate-local `cli/assets/generated/config/{opencode,claude}/**` mirror plus `cli/assets/hooks/**`, and focused internal support seams for install-flow vs prompt-flow logic; `cli/src/services/setup/command.rs` owns `SetupCommand` and its `RuntimeCommand` impl. Its install engine/orchestrator stages embedded files and uses a unified remove-and-replace policy (removing existing targets before swapping staged content, with deterministic recovery guidance on swap failure and no backup artifact creation), and formats deterministic completion messaging; required-hook install orchestration (`install_required_git_hooks`) follows the same remove-and-replace policy (removing existing hooks before swapping staged content, with deterministic recovery guidance on swap failure). The setup command derives a repo-root-scoped context from the runtime `AppContext` before aggregating `ServiceLifecycle::setup` calls across lifecycle providers (config → local_db → agent_trace_db → hooks when requested), so setup providers receive the runtime logger, telemetry, and capability objects instead of a setup-local replacement context. +- `cli/src/services/setup/mod.rs` defines the setup command contract (`SetupMode`, `SetupTarget`, `SetupRequest`, CLI flag parser/validator), an `inquire`-backed interactive target prompter (`InquireSetupTargetPrompter`), setup dispatch outcomes (proceed/cancelled), compile-time embedded asset access (`EmbeddedAsset`, target-scoped iterators, required-hook asset iterators/lookups) generated by `cli/build.rs` from the ephemeral crate-local `cli/assets/generated/config/{opencode,claude}/**` mirror plus `cli/assets/hooks/**`, and focused internal support seams for install-flow vs prompt-flow logic; `cli/src/services/setup/command.rs` owns `SetupCommand` and its `RuntimeCommand` impl. Its install engine/orchestrator stages embedded files and uses a unified remove-and-replace policy (removing existing targets before swapping staged content, with deterministic recovery guidance on swap failure and no backup artifact creation), and formats deterministic completion messaging; required-hook install orchestration (`install_required_git_hooks`) follows the same remove-and-replace policy (removing existing hooks before swapping staged content, with deterministic recovery guidance on swap failure). The setup command derives a repo-root-scoped context from the runtime `AppContext` before aggregating `ServiceLifecycle::setup` calls across lifecycle providers (config → local_db → auth_db → agent_trace_db → hooks when requested), so setup providers receive the runtime logger, telemetry, and capability objects instead of a setup-local replacement context. - `cli/src/services/setup/mod.rs` keeps those responsibilities inside one file for now, but the current ownership split is explicit: the inline `install` module owns repository-path normalization, staging/swap install behavior, required-hook installation, and filesystem safety guards, while the inline `prompt` module owns interactive target selection and prompt styling. - `cli/src/services/security.rs` provides shared security utilities for deterministic secret redaction (`redact_sensitive_text`) and directory write-permission probes (`ensure_directory_is_writable`) used by app/setup/observability surfaces. - `cli/src/services/doctor/mod.rs` owns the current doctor request/report surface while focused submodules (`doctor/inspect.rs`, `doctor/render.rs`, `doctor/fixes.rs`, `doctor/types.rs`) split report fact collection, rendering, manual fix reporting, and doctor-owned domain types into smaller seams; `cli/src/services/doctor/command.rs` owns `DoctorCommand` and its `RuntimeCommand` impl. Runtime doctor execution receives `AppContext`, requests the shared lifecycle provider catalog with hooks included for service-owned `diagnose` and `fix` behavior, adapts lifecycle-owned health/fix records into doctor-owned problem/fix records, and then renders stable text/JSON problem records with category/severity/fixability/remediation fields plus deterministic fix-result reporting in fix mode. Report fact collection still preserves current environment/repository/hook/integration display data, while service-owned lifecycle providers now own config validation, local DB and Agent Trace DB readiness/bootstrap, and hook rollout diagnosis/repair. diff --git a/context/cli/cli-command-surface.md b/context/cli/cli-command-surface.md index f6b0f290..b2b67ada 100644 --- a/context/cli/cli-command-surface.md +++ b/context/cli/cli-command-surface.md @@ -10,9 +10,9 @@ Operator onboarding currently comes from `sce --help`, command-local `--help` ou - Runtime shell and startup lifecycle owner: `cli/src/app.rs` - Top-level command metadata catalog: `cli/src/cli_schema.rs` - Custom top-level help renderer and known-command classifier: `cli/src/command_surface.rs` -- Turso adapters: `cli/src/services/local_db/mod.rs`, `cli/src/services/agent_trace_db/mod.rs`, and shared infrastructure in `cli/src/services/db/mod.rs` -- Service domains: `cli/src/services/{agent_trace_db,auth,auth_command,completion,config,db,default_paths,hooks,local_db,observability,output_format,resilience,security,setup,style,token_storage,version}` plus the split doctor module at `cli/src/services/doctor/{mod,command,inspect,render,fixes,types}.rs`; service-owned `command.rs` files now own the `RuntimeCommand` impls for help/version/completion/auth/config/setup/doctor/hooks -- Service lifecycle: `cli/src/services/lifecycle.rs` defines the `ServiceLifecycle` trait with `diagnose`, `fix`, and `setup` methods; `config`, `hooks`, `local_db`, and `agent_trace_db` services implement this trait in their respective `lifecycle.rs` files, and `doctor`/`setup` commands aggregate calls across all registered lifecycle providers +- Turso adapters: `cli/src/services/auth_db/mod.rs`, `cli/src/services/local_db/mod.rs`, `cli/src/services/agent_trace_db/mod.rs`, and shared infrastructure in `cli/src/services/db/mod.rs` +- Service domains: `cli/src/services/{agent_trace_db,auth,auth_command,auth_db,completion,config,db,default_paths,hooks,local_db,observability,output_format,resilience,security,setup,style,token_storage,version}` plus the split doctor module at `cli/src/services/doctor/{mod,command,inspect,render,fixes,types}.rs`; service-owned `command.rs` files now own the `RuntimeCommand` impls for help/version/completion/auth/config/setup/doctor/hooks +- Service lifecycle: `cli/src/services/lifecycle.rs` defines the `ServiceLifecycle` trait with `diagnose`, `fix`, and `setup` methods; `config`, `hooks`, `local_db`, `auth_db`, and `agent_trace_db` services implement this trait in their respective `lifecycle.rs` files, and `doctor`/`setup` commands aggregate calls across all registered lifecycle providers - Shared test temp-path helper: `cli/src/test_support.rs` (`TestTempDir`, test-only module) ## Onboarding documentation @@ -63,7 +63,7 @@ Deferred or gated command surfaces currently avoid claiming unimplemented behavi `setup` now also exposes compile-time embedded config assets for OpenCode/Claude targets, sourced from the generated `config/.opencode/**` and `config/.claude/**` trees via `cli/build.rs` with normalized forward-slash relative paths and target-scoped iteration APIs; the embedded asset set includes the OpenCode bash-policy plugin/runtime files generated from the canonical preset catalog (Claude bash-policy enforcement has been removed from generated outputs). `setup` additionally includes a repository-root install engine (`install_embedded_setup_assets`) that stages embedded files, intentionally leaves generated `skills/*/tile.json` manifests in `config/` only, skips those tile files during repo-root installs, and uses a unified remove-and-replace policy for `.opencode/`/`.claude/` (removing existing targets before swapping staged content, with deterministic recovery guidance on swap failure) while treating bash-policy enforcement files as first-class SCE-managed assets. `setup` now executes end-to-end and prints deterministic completion details including selected target(s) and per-target install count. -`doctor` now executes end-to-end with explicit diagnosis and repair-intent surfaces: `sce doctor` stays read-only, `sce doctor --fix` selects repair-intent mode, and text/JSON output expose stable mode/problem/fix-result/database-record scaffolding. The current runtime aggregates `ServiceLifecycle::diagnose` and `ServiceLifecycle::fix` calls across all registered service providers (`config`, `local_db`, `agent_trace_db`, `hooks`) plus integration checks, covering state-root resolution, global and repo-local `sce/config.json` readability/schema validation, local DB and Agent Trace DB path/health, DB-parent readiness barriers, an intentionally empty repo-scoped SCE database section for the active repository, the repo hook rollout slice when a repository target is detected, and repo-root installed OpenCode integration presence for `plugins`, `agents`, `commands`, and `skills`; those integration checks are presence-only and fail a group when any required installed file is missing. Fix mode delegates to each provider's `fix` implementation, which reuses the canonical setup hook install flow to repair missing/stale/non-executable required hooks and missing hooks directories, and it can bootstrap missing canonical database parent directories when the resolved paths match canonical owned locations. +`doctor` now executes end-to-end with explicit diagnosis and repair-intent surfaces: `sce doctor` stays read-only, `sce doctor --fix` selects repair-intent mode, and text/JSON output expose stable mode/problem/fix-result/database-record scaffolding. The current runtime aggregates `ServiceLifecycle::diagnose` and `ServiceLifecycle::fix` calls across all registered service providers (`config`, `local_db`, `auth_db`, `agent_trace_db`, `hooks`) plus integration checks, covering state-root resolution, global and repo-local `sce/config.json` readability/schema validation, local DB and Agent Trace DB path/health, DB-parent readiness barriers, an intentionally empty repo-scoped SCE database section for the active repository, the repo hook rollout slice when a repository target is detected, and repo-root installed OpenCode integration presence for `plugins`, `agents`, `commands`, and `skills`; those integration checks are presence-only and fail a group when any required installed file is missing. Fix mode delegates to each provider's `fix` implementation, which reuses the canonical setup hook install flow to repair missing/stale/non-executable required hooks and missing hooks directories, and it can bootstrap missing canonical database parent directories when the resolved paths match canonical owned locations. A user-invocable `sync` command is not wired in the current CLI surface; local DB and Agent Trace DB bootstrap currently happen through `setup`, and DB health/repair currently happens through `doctor`. Command wiring for `sce sync` is deferred to `0.4.0`. ## Command loop and error model @@ -85,10 +85,10 @@ A user-invocable `sync` command is not wired in the current CLI surface; local D ## Service contracts -- `cli/src/services/setup/mod.rs` defines setup parsing/selection contracts plus runtime install orchestration (`run_setup_for_mode`) over the embedded asset install engine; `cli/src/services/setup/command.rs` owns the setup runtime command handler. Setup now aggregates `ServiceLifecycle::setup` calls across registered providers (`config`, `local_db`, `agent_trace_db`, `hooks`) in order, using `AppContext` with resolved repository root. +- `cli/src/services/setup/mod.rs` defines setup parsing/selection contracts plus runtime install orchestration (`run_setup_for_mode`) over the embedded asset install engine; `cli/src/services/setup/command.rs` owns the setup runtime command handler. Setup now aggregates `ServiceLifecycle::setup` calls across registered providers (`config`, `local_db`, `auth_db`, `agent_trace_db`, `hooks`) in order, using `AppContext` with resolved repository root. - `cli/src/services/setup/mod.rs` now keeps its larger internal responsibilities behind focused inline support modules: `install` owns repository canonicalization, staging/swap install flows, required-hook installation, and repo/writeability guards, while `prompt` owns interactive target selection and styled prompt labels. - `cli/src/services/config/mod.rs` defines config parser/runtime contracts (`show`, `validate`, `--help`), strict config-file key/type validation, deterministic text/JSON rendering, repo-configured bash-policy preset/custom validation and reporting under `policies.bash`, and shared auth-key metadata that declares env key, config-file key, and optional baked-default eligibility for supported auth runtime values starting with `workos_client_id` (`WORKOS_CLIENT_ID` vs `workos_client_id`); auth-key provenance/preference metadata stays on `show`, while `validate` stays trimmed to validation status plus issues/warnings. `cli/src/services/config/lifecycle.rs` implements `ServiceLifecycle` for config health checks and setup (global/local config validation and repo-local config bootstrap). -- `cli/src/services/doctor/mod.rs` defines the implemented doctor request/report contract (`DoctorRequest`, `DoctorMode`, `run_doctor`) while focused submodules under `cli/src/services/doctor/` handle runtime command dispatch (`command.rs`), diagnosis (`inspect.rs`), rendering (`render.rs`), fix execution (`fixes.rs`), and doctor-owned domain types (`types.rs`). Together they preserve explicit fix-mode parsing, stable text/JSON problem and database-record rendering, deterministic fix-result reporting, and aggregation of `ServiceLifecycle::diagnose`/`ServiceLifecycle::fix` across registered providers (`config`, `local_db`, `agent_trace_db`, `hooks`). The doctor module coordinates state-root/config/database reporting and validation, an empty default repo-scoped database inventory, path-source detection plus required-hook presence/executable/content checks when a repository target is detected, repo-root installed OpenCode integration presence inventory for `plugins`, `agents`, `commands`, and `skills` derived from the embedded OpenCode setup asset catalog, shared-style bracketed human status token rendering (`[PASS]`, `[FAIL]`, `[MISS]`) with simplified `label (path)` text rows, and repair-mode delegation to service-owned fix implementations. +- `cli/src/services/doctor/mod.rs` defines the implemented doctor request/report contract (`DoctorRequest`, `DoctorMode`, `run_doctor`) while focused submodules under `cli/src/services/doctor/` handle runtime command dispatch (`command.rs`), diagnosis (`inspect.rs`), rendering (`render.rs`), fix execution (`fixes.rs`), and doctor-owned domain types (`types.rs`). Together they preserve explicit fix-mode parsing, stable text/JSON problem and database-record rendering, deterministic fix-result reporting, and aggregation of `ServiceLifecycle::diagnose`/`ServiceLifecycle::fix` across registered providers (`config`, `local_db`, `auth_db`, `agent_trace_db`, `hooks`). The doctor module coordinates state-root/config/database reporting and validation, an empty default repo-scoped database inventory, path-source detection plus required-hook presence/executable/content checks when a repository target is detected, repo-root installed OpenCode integration presence inventory for `plugins`, `agents`, `commands`, and `skills` derived from the embedded OpenCode setup asset catalog, shared-style bracketed human status token rendering (`[PASS]`, `[FAIL]`, `[MISS]`) with simplified `label (path)` text rows, and repair-mode delegation to service-owned fix implementations. - `cli/src/services/version/mod.rs` defines the version parser/output contract (`parse_version_request`, `render_version`) with deterministic text/JSON output modes; `cli/src/services/version/command.rs` owns the version runtime command handler. - `cli/src/services/completion/mod.rs` defines the completion output contract (`render_completion`) using clap_complete to generate deterministic shell scripts for Bash, Zsh, and Fish; `cli/src/services/completion/command.rs` owns the completion runtime command handler. - `cli/src/services/hooks/mod.rs` defines production local hook runtime parsing/dispatch (`HookSubcommand`, `run_hooks_subcommand`) for `pre-commit`, `commit-msg`, `post-commit`, `post-rewrite`, and `diff-trace`; `cli/src/services/hooks/command.rs` owns the hook runtime command handler. Current runtime behavior is commit-msg-only attribution behind the disabled-default attribution gate; `pre-commit` and `post-rewrite` are deterministic no-ops; `post-commit` is an active intersection + Agent Trace DB persistence path (captures current commit patch, combines/intersects recent `diff_traces`, persists intersection metadata to `post_commit_patch_intersections`, then persists built Agent Trace payload with range-level `content_hash` values to `agent_traces`); and `diff-trace` performs STDIN JSON intake, required non-empty `sessionID`/`diff`/`model_id`/`tool_name`, required nullable/non-empty `tool_version`, plus required `u64` `time` (Unix epoch milliseconds) validation, non-lossy AgentTraceDb `time_ms` conversion, collision-safe parsed-payload `context/tmp/-000000-diff-trace.json` persistence, and command-failing AgentTraceDb insertion. `cli/src/services/hooks/lifecycle.rs` implements `ServiceLifecycle` for hook health checks, fix, and setup (hook rollout integrity and required-hook installation). @@ -120,7 +120,7 @@ A user-invocable `sync` command is not wired in the current CLI surface; local D - `LocalDbLifecycle` in `cli/src/services/local_db/lifecycle.rs` - `AgentTraceDbLifecycle` in `cli/src/services/agent_trace_db/lifecycle.rs` - `doctor` command aggregates `diagnose`/`fix` across all registered lifecycle providers. -- `setup` command aggregates `setup` across all registered lifecycle providers in order (config → local_db → agent_trace_db → hooks). +- `setup` command aggregates `setup` across all registered lifecycle providers in order (config → local_db → auth_db → agent_trace_db → hooks). ## Parser-focused tests diff --git a/context/cli/service-lifecycle.md b/context/cli/service-lifecycle.md index d62a76e9..bd8ad58c 100644 --- a/context/cli/service-lifecycle.md +++ b/context/cli/service-lifecycle.md @@ -13,7 +13,7 @@ - `SetupOutcome` is a minimal lifecycle-owned carrier for current setup result shapes: - optional lifecycle-owned `RequiredHooksInstallOutcome` - `LifecycleProvider` aliases boxed lifecycle providers, and `lifecycle_providers(include_hooks)` is the shared provider catalog/factory used by command orchestrators. -- Provider order is deterministic: `ConfigLifecycle` → `LocalDbLifecycle` → `AgentTraceDbLifecycle` → `HooksLifecycle` when hooks are included. +- Provider order is deterministic: `ConfigLifecycle` → `LocalDbLifecycle` → `AuthDbLifecycle` → `AgentTraceDbLifecycle` → `HooksLifecycle` when hooks are included. ## Current boundaries diff --git a/context/context-map.md b/context/context-map.md index 74a7b51a..117ce1db 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -15,7 +15,7 @@ Feature/domain context: - `context/cli/styling-service.md` (CLI text-mode output styling with `owo-colors` and `comfy-table`, TTY/`NO_COLOR` policy, shared helper API for human-facing surfaces, and per-column right-to-left RGB gradient banner rendering) - `context/cli/config-precedence-contract.md` (implemented `sce config` show/validate command contract, deterministic `flags > env > config file > defaults` resolution order, canonical `$schema` acceptance for startup-loaded `sce/config.json` files, shared auth-key env/config/optional baked-default support starting with `workos_client_id`, shared runtime resolution for flat logging observability keys, canonical Pkl-generated `sce/config.json` schema ownership plus CLI embedding/reuse contract, config-file selection order, `show` provenance output, trimmed `validate` output contract, and opt-in compiled-binary config-precedence E2E coverage contract) - `context/cli/capability-traits.md` (current broad CLI dependency-injection capability seam in `cli/src/services/capabilities.rs`, including `FsOps`/`StdFsOps`, `GitOps`/`ProcessGitOps`, git root/hooks resolution behavior, AppContext wiring with capability accessors plus repo-root-scoped context derivation, and test-only unimplemented stubs; current service internals do not consume these traits until later lifecycle migration tasks) -- `context/cli/service-lifecycle.md` (current compile-safe `ServiceLifecycle` seam in `cli/src/services/lifecycle.rs`, including default no-op diagnose/fix/setup methods against `AppContext`, lifecycle-owned health/fix/setup result types, doctor/setup adapter boundaries, the shared lifecycle provider catalog/factory, hook/config/local_db/agent_trace_db lifecycle providers, implemented doctor aggregation over diagnose/fix providers, and implemented setup aggregation over `setup` providers in order config → local_db → agent_trace_db → hooks when requested) +- `context/cli/service-lifecycle.md` (current compile-safe `ServiceLifecycle` seam in `cli/src/services/lifecycle.rs`, including default no-op diagnose/fix/setup methods against `AppContext`, lifecycle-owned health/fix/setup result types, doctor/setup adapter boundaries, the shared lifecycle provider catalog/factory, hook/config/local_db/auth_db/agent_trace_db lifecycle providers, implemented doctor aggregation over diagnose/fix providers, and implemented setup aggregation over `setup` providers in order config → local_db → auth_db → agent_trace_db → hooks when requested) - `context/sce/cli-observability-contract.md` (implemented config-backed runtime observability contract for the flat logging config-file shape with env-over-config fallback, concrete logger/telemetry runtime behavior plus logger and object-safe telemetry trait boundaries, AppContext observability wiring, operator-facing `sce config show` observability reporting, and the trimmed `sce config validate` status-only validation surface) - `context/sce/shared-context-code-workflow.md` - `context/sce/shared-context-plan-workflow.md` (canonical `/change-to-plan` workflow, clarification/readiness gate contract, and one-task/one-atomic-commit task-slicing policy) @@ -43,7 +43,7 @@ Feature/domain context: - `context/sce/local-db.md` (implemented `cli/src/services/local_db/mod.rs` local database spec with `LocalDb = TursoDb`, canonical local DB path resolution, zero local migrations, and inherited blocking `execute`/`query` methods using the shared Turso adapter) - `context/sce/shared-turso-db.md` (current shared `cli/src/services/db/mod.rs` Turso database infrastructure seam, including `DbSpec`, generic `TursoDb`, `EncryptedTursoDb` encrypted constructor path with env key `SCE_DB_ENCRYPTION_KEY` + strict `aegis256` selection via Turso `EncryptionOpts`, encrypted-adapter sync `execute`/`query` wrappers plus migration execution parity, sync `query_map` on `TursoDb`, per-database `__sce_migrations` tracking, generic embedded migration execution, and current concrete wrappers for `LocalDb`, `AgentTraceDb`, and encrypted `AuthDb`) - `context/sce/agent-trace-db.md` (implemented `cli/src/services/agent_trace_db/mod.rs` Agent Trace database wrapper with canonical `/sce/agent-trace.db` path, ordered `diff_traces`, `post_commit_patch_intersections`, `diff_traces(time_ms, id)` index, `agent_traces`, nullable `diff_traces.model_id`, nullable `diff_traces.tool_name`, nullable `diff_traces.tool_version`, and nullable `agent_traces.agent_trace_id` migrations applied through shared migration metadata, typed parameterized insert helpers for diff traces including `model_id` + tool metadata, post-commit intersection rows, and built `agent_traces` rows with `agent_trace_id` plus schema-validated trace JSON containing range `content_hash`, inclusive bounded chronological recent `diff_traces` query/parse support with malformed-row skip accounting, registered setup/doctor lifecycle provider, and active hook writers for `diff_traces` intake plus post-commit intersection/agent-trace persistence) -- `context/sce/auth-db.md` (current encrypted auth DB foundation: canonical `/sce/auth.db` path resolver, `AuthDb = EncryptedTursoDb` wrapper, ordered `auth_tokens` table/index migrations, and `AuthDbLifecycle` provider registered in the shared lifecycle catalog) +- `context/sce/auth-db.md` (current encrypted auth DB foundation: canonical `/sce/auth.db` path resolver, `AuthDb = EncryptedTursoDb` wrapper, baseline migration 001 creating `auth_credentials` without `user_id`, with `updated_at`, and 002 creating the `updated_at` auto-refresh trigger instead of a `user_id` index, and `AuthDbLifecycle` provider registered in the shared lifecycle catalog) - `context/sce/agent-trace-core-schema-migrations.md` (historical reference for removed local DB schema bootstrap behavior; T03 now implements the actual local DB with migrations) - `context/sce/agent-trace-retry-queue-observability.md` (inactive local-hook retry path plus historical retry/metrics reference) - `context/sce/agent-trace-local-hooks-mvp-contract-gap-matrix.md` (T01 Local Hooks MVP production contract freeze and deterministic gap matrix for `agent-trace-local-hooks-production-mvp`) diff --git a/context/glossary.md b/context/glossary.md index 2cbae071..1d7d7a12 100644 --- a/context/glossary.md +++ b/context/glossary.md @@ -31,7 +31,7 @@ - `sce dependency baseline`: Current crate dependency set declared in `cli/Cargo.toml` (`anyhow`, `clap`, `clap_complete`, `dirs`, `hmac`, `inquire`, `reqwest`, `serde`, `serde_json`, `sha2`, `tokio`, `tracing`, `tracing-subscriber`, `turso`) and validated through normal compile/test coverage. - `local Turso adapter`: Module in `cli/src/services/local_db/mod.rs` that defines `LocalDbSpec` and exposes `LocalDb` as a `TursoDb` alias. It resolves the canonical local DB path with `local_db_path()`, currently declares zero migrations, and inherits `new()`, `execute()`, and `query()` from the shared generic adapter. - `encrypted Turso adapter`: Generic adapter seam in `cli/src/services/db/mod.rs` exposed as `EncryptedTursoDb`, structurally parallel to `TursoDb` (connection, tokio runtime bridge, spec typing). Its constructor resolves `SCE_DB_ENCRYPTION_KEY`, rejects empty keys, enables Turso local encryption with strict `aegis256` cipher selection through `turso::EncryptionOpts`, and runs embedded migrations after connect; the adapter also exposes synchronous `execute`, `query`, and `run_migrations` helpers with `__sce_migrations` tracking parity. -- `auth DB adapter`: Module in `cli/src/services/auth_db/mod.rs` that defines `AuthDbSpec` and exposes `AuthDb` as an `EncryptedTursoDb` alias. It resolves the canonical `/sce/auth.db` path with `auth_db_path()` and embeds ordered migrations for the `auth_tokens` table plus `idx_auth_tokens_email`. Auth runtime token-storage replacement is not wired yet. +- `auth DB adapter`: Module in `cli/src/services/auth_db/mod.rs` that defines `AuthDbSpec` and exposes `AuthDb` as an `EncryptedTursoDb` alias. It resolves the canonical `/sce/auth.db` path with `auth_db_path()` and embeds ordered auth migrations where baseline SQL creates `auth_credentials` without `user_id`, with `updated_at`, and a trigger that auto-refreshes `updated_at` on row updates. Auth runtime token-storage replacement is not wired yet. - `AuthDbLifecycle`: Lifecycle provider in `cli/src/services/auth_db/lifecycle.rs` that implements `ServiceLifecycle` for encrypted auth DB setup/doctor integration. `diagnose` collects auth DB path health problems, `fix` bootstraps missing auth DB parent directory, and `setup` calls `AuthDb::new()`. Registered as `LifecycleProviderId::AuthDb` in the shared lifecycle catalog. - `agent trace DB adapter`: Module in `cli/src/services/agent_trace_db/mod.rs` that defines `AgentTraceDbSpec`, exposes `AgentTraceDb` as a `TursoDb` alias, resolves `/sce/agent-trace.db` through `agent_trace_db_path()`, embeds an ordered split fresh-start migration set (`001..005`) that creates `diff_traces`, `post_commit_patch_intersections`, and `agent_traces` plus `idx_diff_traces_time_ms_id` and `idx_agent_traces_agent_trace_id`, with `agent_traces.agent_trace_id` enforced as `NOT NULL UNIQUE`; provides typed parameterized insert helpers for diff traces including `model_id` + tool metadata, post-commit intersection rows, and built agent-trace rows (including `agent_trace_id`); exposes chronological recent `diff_traces` query/parse support with malformed-row skip accounting; has `AgentTraceDbLifecycle` for setup/doctor integration; and is written by `sce hooks diff-trace` (`diff_traces`) plus `sce hooks post-commit` (`post_commit_patch_intersections` and built `agent_traces`). - `Agent Trace SCE metadata`: Implementation-owned top-level metadata emitted by `build_agent_trace(...)` as `metadata.sce.version`; the value is sourced from the compiled `sce` CLI package version via `env!("CARGO_PKG_VERSION")`, is schema-validated with the rest of the payload, and is persisted in AgentTraceDb `agent_traces.trace_json` without changing the top-level Agent Trace payload/schema `version`. @@ -42,7 +42,7 @@ - `__sce_migrations`: Per-database migration metadata table created by `TursoDb::run_migrations()`; records applied migration IDs after successful execution so later setup/lifecycle initialization applies only migrations not yet recorded, while existing metadata-less DBs are brought forward by re-applying the current idempotent migration set and recording each ID. - `sync command deferral`: Current plan/state note that a user-invocable `sce sync` command is not wired yet and is deferred to `0.4.0`; local DB and Agent Trace DB bootstrap now flow through lifecycle providers aggregated by the setup command, and DB health/repair flows through the doctor surface. - `CLI bounded resilience wrapper`: Shared policy in `cli/src/services/resilience.rs` (`RetryPolicy`, `run_with_retry`) that applies deterministic retries/timeouts/capped backoff to transient operations, emits retry observability events, and returns actionable terminal failure guidance. -- `setup service orchestration`: Setup execution logic in `cli/src/services/setup/command.rs` that resolves the repository root, derives a repo-root-scoped `AppContext` from the runtime command context, aggregates `ServiceLifecycle::setup` calls across lifecycle providers (config → local_db → agent_trace_db → hooks when requested), handles interactive target selection for config asset installation, and emits deterministic success messaging per target. +- `setup service orchestration`: Setup execution logic in `cli/src/services/setup/command.rs` that resolves the repository root, derives a repo-root-scoped `AppContext` from the runtime command context, aggregates `ServiceLifecycle::setup` calls across lifecycle providers (config → local_db → auth_db → agent_trace_db → hooks when requested), handles interactive target selection for config asset installation, and emits deterministic success messaging per target. - `setup target flags`: Mutually-exclusive `sce setup` target selectors (`--opencode`, `--claude`, `--both`) that force non-interactive mode for automation. - `setup mode contract`: `cli/src/services/setup/mod.rs` model where `SetupMode::Interactive` is the default and `SetupMode::NonInteractive(SetupTarget)` is selected only when exactly one target flag is provided. - `setup interactive target prompt`: `inquire::Select` flow in `cli/src/services/setup/mod.rs` (`InquireSetupTargetPrompter`) that presents OpenCode, Claude, and Both when `sce setup` runs without target flags. diff --git a/context/sce/agent-trace-hook-doctor.md b/context/sce/agent-trace-hook-doctor.md index d8635f2e..ef2e4555 100644 --- a/context/sce/agent-trace-hook-doctor.md +++ b/context/sce/agent-trace-hook-doctor.md @@ -284,7 +284,7 @@ Services implementing `ServiceLifecycle`: - `AgentTraceDbLifecycle` in `cli/src/services/agent_trace_db/lifecycle.rs`: validates Agent Trace DB path/health, bootstraps DB parent directory The `doctor` command aggregates `diagnose` and `fix` across all registered providers. -The `setup` command aggregates `setup` across all registered providers in order (config → local_db → agent_trace_db → hooks). +The `setup` command aggregates `setup` across all registered providers in order (config → local_db → auth_db → agent_trace_db → hooks). ## Output shape contract diff --git a/context/sce/auth-db.md b/context/sce/auth-db.md index 9cf7803c..d5849f2c 100644 --- a/context/sce/auth-db.md +++ b/context/sce/auth-db.md @@ -12,11 +12,11 @@ The encrypted auth DB foundation currently consists of a thin Rust wrapper plus - Migration directory: `cli/migrations/auth/`. - Ordered migrations: - `001_create_auth_tokens.sql` - - `002_create_auth_tokens_email_index.sql` + - `002_create_auth_credentials_updated_at_trigger.sql` ## Schema baseline -`auth_tokens` is created idempotently with: +`auth_credentials` is created idempotently with: - `id TEXT PRIMARY KEY NOT NULL` - `access_token TEXT NOT NULL` @@ -25,12 +25,15 @@ The encrypted auth DB foundation currently consists of a thin Rust wrapper plus - `refresh_token TEXT NOT NULL` - `scope TEXT` (nullable) - `stored_at_unix_seconds INTEGER NOT NULL` -- `email TEXT NOT NULL` - `created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))` +- `updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))` -The email lookup index is `idx_auth_tokens_email` on `auth_tokens(email)`. +Current migration baseline: -## Lifecycle integration (T03) +- `001_create_auth_tokens.sql` creates `auth_credentials` without `user_id`, with `updated_at`. +- `002_create_auth_credentials_updated_at_trigger.sql` creates `auth_credentials_set_updated_at` trigger to auto-refresh `updated_at` on row updates. + +## Lifecycle integration `AuthDbLifecycle` is registered in `cli/src/services/auth_db/lifecycle.rs` following the existing DB lifecycle pattern: - `diagnose` collects auth DB path health problems. diff --git a/context/sce/setup-repo-local-config-bootstrap.md b/context/sce/setup-repo-local-config-bootstrap.md index d434f4c1..4abcb5be 100644 --- a/context/sce/setup-repo-local-config-bootstrap.md +++ b/context/sce/setup-repo-local-config-bootstrap.md @@ -20,7 +20,7 @@ Task `setup-repo-gate-and-local-config-bootstrap` T02 and `turso-local-db-sync` - `cli/src/services/agent_trace_db/lifecycle.rs` implements `AgentTraceDbLifecycle::setup()` for Agent Trace DB initialization. - The function uses `RepoPaths::sce_config_file()` and `RepoPaths::sce_dir()` from `default_paths` for path resolution. - The canonical payload constant is `REPO_LOCAL_CONFIG_BOOTSTRAP_PAYLOAD`. -- `cli/src/services/setup/command.rs` derives a repo-root-scoped `AppContext` after `ensure_git_repository`, then aggregates lifecycle providers in config → local_db → agent_trace_db → hooks order; `ConfigLifecycle::setup()` calls `bootstrap_repo_local_config(...)`, `LocalDbLifecycle::setup()` initializes the local DB, and `AgentTraceDbLifecycle::setup()` initializes the Agent Trace DB. +- `cli/src/services/setup/command.rs` derives a repo-root-scoped `AppContext` after `ensure_git_repository`, then aggregates lifecycle providers in config → local_db → auth_db → agent_trace_db → hooks order; `ConfigLifecycle::setup()` calls `bootstrap_repo_local_config(...)`, `LocalDbLifecycle::setup()` initializes the local DB, `AuthDbLifecycle::setup()` initializes the auth DB, and `AgentTraceDbLifecycle::setup()` initializes the Agent Trace DB. ## Relationship to other setup contracts diff --git a/context/sce/shared-turso-db.md b/context/sce/shared-turso-db.md index de1479b5..4963112c 100644 --- a/context/sce/shared-turso-db.md +++ b/context/sce/shared-turso-db.md @@ -26,7 +26,7 @@ The shared module is exported from `cli/src/services/mod.rs` and compile-checked - `cli/src/services/local_db/mod.rs`: `LocalDb = TursoDb`, with `LocalDbSpec` resolving `local_db_path()` and declaring zero migrations. - `cli/src/services/agent_trace_db/mod.rs`: `AgentTraceDb = TursoDb`, with `AgentTraceDbSpec` resolving `agent_trace_db_path()` and loading ordered Agent Trace migrations for `diff_traces` and `post_commit_patch_intersections`. -- `cli/src/services/auth_db/mod.rs`: `AuthDb = EncryptedTursoDb`, with `AuthDbSpec` resolving `auth_db_path()` and loading ordered auth migrations for `auth_tokens` plus the email index. +- `cli/src/services/auth_db/mod.rs`: `AuthDb = EncryptedTursoDb`, with `AuthDbSpec` resolving `auth_db_path()` and loading ordered auth migrations where baseline SQL creates `auth_credentials` without `user_id`, with `updated_at`, and a trigger that auto-refreshes `updated_at` on row updates. All three database wrappers (local DB, auth DB, Agent Trace DB) have lifecycle providers. `lifecycle_providers(include_hooks)` registers database providers in order `LocalDbLifecycle` → `AuthDbLifecycle` → `AgentTraceDbLifecycle` before optional hooks, so setup initializes all three databases and doctor diagnoses/fixes all three canonical DB paths. From 4fe0001cb66aab152edbc25515391d6eed06e9f7 Mon Sep 17 00:00:00 2001 From: Ivan Ivic Date: Tue, 26 May 2026 12:50:43 +0200 Subject: [PATCH 4/7] token-storage: Migrate persistence from JSON file to encrypted AuthDb Replace the legacy JSON file-based token storage (`~/.local/state/sce/auth/tokens.json`) with persistence through the encrypted `AuthDb` (`auth_credentials` table in `/sce/auth.db`) using a `OnceLock` lazy singleton and constant row ID 1. - `save_tokens`/`load_tokens`/`delete_tokens` now execute SQL against `auth_credentials` instead of file read/write/delete; `TokenStorageError` replaces `Io`/`Serialization`/ `CorruptedTokenFile`/`Permission` variants with a single `Database` variant - `auth.rs` public functions decouple HTTP from storage: `*_returning_token` variants return `TokenResponse`; callers in `auth_command/mod.rs` explicitly invoke `token_storage::save_tokens` outside tokio `block_on` to avoid nested-runtime panics - `default_paths.rs` removes `auth_tokens_file()`, `AuthTokens` artifact ID, and corresponding entry from the artifact-locations catalog - Migration DDL fixes `auth_credentials.id` from `TEXT` to `INTEGER PRIMARY KEY` - `EncryptedTursoDb` gains `query_map` for typed synchronous row mapping Co-authored-by: SCE --- .../auth/001_create_auth_tokens.sql | 2 +- cli/src/services/auth.rs | 95 +++---- cli/src/services/auth_command/mod.rs | 119 ++++----- cli/src/services/db/mod.rs | 33 +++ cli/src/services/default_paths.rs | 26 +- cli/src/services/token_storage.rs | 244 +++++++----------- context/architecture.md | 2 +- context/cli/cli-command-surface.md | 4 +- context/cli/default-path-catalog.md | 1 - context/glossary.md | 2 +- context/overview.md | 2 +- context/sce/auth-db.md | 15 +- 12 files changed, 235 insertions(+), 310 deletions(-) diff --git a/cli/migrations/auth/001_create_auth_tokens.sql b/cli/migrations/auth/001_create_auth_tokens.sql index 974a00c6..8d56cacb 100644 --- a/cli/migrations/auth/001_create_auth_tokens.sql +++ b/cli/migrations/auth/001_create_auth_tokens.sql @@ -1,5 +1,5 @@ CREATE TABLE IF NOT EXISTS auth_credentials ( - id TEXT PRIMARY KEY NOT NULL, + id INTEGER PRIMARY KEY NOT NULL, access_token TEXT NOT NULL, token_type TEXT NOT NULL, expires_in INTEGER NOT NULL, diff --git a/cli/src/services/auth.rs b/cli/src/services/auth.rs index 32c97c49..037c6dc3 100644 --- a/cli/src/services/auth.rs +++ b/cli/src/services/auth.rs @@ -5,22 +5,16 @@ use anyhow::anyhow; use serde::{Deserialize, Serialize}; use crate::services::resilience::{run_with_retry, RetryPolicy}; -use crate::services::token_storage::{load_tokens, save_tokens, StoredTokens, TokenStorageError}; +use crate::services::token_storage::{StoredTokens, TokenStorageError}; pub const DEVICE_CODE_GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:device_code"; -#[allow(dead_code)] pub const REFRESH_TOKEN_GRANT_TYPE: &str = "refresh_token"; pub const WORKOS_DEFAULT_BASE_URL: &str = "https://api.workos.com"; pub const DEFAULT_DEVICE_POLL_INTERVAL_SECONDS: u64 = 5; -#[allow(dead_code)] const TOKEN_EXPIRY_SKEW_SECONDS: u64 = 30; -#[allow(dead_code)] const TOKEN_REFRESH_MAX_ATTEMPTS: u32 = 3; -#[allow(dead_code)] const TOKEN_REFRESH_TIMEOUT_MS: u64 = 10_000; -#[allow(dead_code)] const TOKEN_REFRESH_INITIAL_BACKOFF_MS: u64 = 250; -#[allow(dead_code)] const TOKEN_REFRESH_MAX_BACKOFF_MS: u64 = 2_000; #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] @@ -46,7 +40,6 @@ pub struct DeviceTokenPollRequest { } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -#[allow(dead_code)] pub struct RefreshTokenRequest { pub grant_type: String, pub refresh_token: String, @@ -147,88 +140,74 @@ impl From for AuthError { } } -pub async fn start_device_auth_flow( - client: &reqwest::Client, - api_base_url: &str, - client_id: &str, -) -> Result { - if client_id.trim().is_empty() { - return Err(AuthError::MissingClientId); - } - - let authorization = request_device_authorization(client, api_base_url, client_id).await?; - let stored_tokens = - complete_device_auth_flow(client, api_base_url, client_id, &authorization).await?; - - Ok(DeviceAuthFlowResult { - authorization, - stored_tokens, - }) -} - -pub async fn complete_device_auth_flow( +/// HTTP-only device auth completion: polls for the device token and returns +/// the raw `TokenResponse` without calling `save_tokens`. +pub(crate) async fn complete_device_auth_flow_returning_token( client: &reqwest::Client, api_base_url: &str, client_id: &str, authorization: &DeviceAuthorizationResponse, -) -> Result { +) -> Result { if client_id.trim().is_empty() { return Err(AuthError::MissingClientId); } let token = poll_for_device_token(client, api_base_url, client_id, authorization).await?; - let stored_tokens = save_tokens(&token)?; - Ok(stored_tokens) + Ok(token) } -#[allow(dead_code)] -pub async fn ensure_valid_token( +/// HTTP-only token validation: checks an existing stored token for expiry and +/// refreshes if needed. Does NOT call any `token_storage` functions. +/// Returns the raw `TokenResponse` (re-using the stored token if not expired). +pub(crate) async fn ensure_valid_token_returning_token( client: &reqwest::Client, api_base_url: &str, client_id: &str, -) -> Result { + stored: &StoredTokens, +) -> Result { if client_id.trim().is_empty() { return Err(AuthError::MissingClientId); } - let Some(stored) = load_tokens()? else { - return Err(AuthError::Unauthorized( - String::from("No stored WorkOS credentials were found. Try: run 'sce login' before running authenticated commands."), - )); - }; - let now_unix_seconds = current_unix_timestamp_seconds()?; - if !is_token_expired(&stored, now_unix_seconds) { - return Ok(stored); + if !is_token_expired(stored, now_unix_seconds) { + return Ok(TokenResponse { + access_token: stored.access_token.clone(), + token_type: stored.token_type.clone(), + expires_in: stored.expires_in, + refresh_token: stored.refresh_token.clone(), + scope: stored.scope.clone(), + }); } - let refreshed = refresh_access_token(client, api_base_url, client_id, &stored.refresh_token) + refresh_access_token(client, api_base_url, client_id, &stored.refresh_token) .await - .map_err(map_refresh_failure_for_public_cli)?; - let updated = save_tokens(&refreshed)?; - Ok(updated) + .map_err(map_refresh_failure_for_public_cli) } -pub async fn renew_stored_token( +/// HTTP-only token renewal: accepts a refresh token directly and calls the +/// private `refresh_access_token`. Does NOT call any `token_storage` functions. +/// Returns the raw `TokenResponse`. +pub(crate) async fn renew_stored_token_from_refresh_token( client: &reqwest::Client, api_base_url: &str, client_id: &str, -) -> Result { + refresh_token: &str, +) -> Result { if client_id.trim().is_empty() { return Err(AuthError::MissingClientId); } - let Some(stored) = load_tokens()? else { + if refresh_token.trim().is_empty() { return Err(AuthError::Unauthorized( - String::from("No stored WorkOS credentials were found. Try: run 'sce auth login' before running authenticated commands."), + "Stored WorkOS refresh token is missing. Try: run 'sce auth login' to authenticate again." + .to_string(), )); - }; + } - let refreshed = refresh_access_token(client, api_base_url, client_id, &stored.refresh_token) + refresh_access_token(client, api_base_url, client_id, refresh_token) .await - .map_err(map_refresh_failure_for_public_cli)?; - let updated = save_tokens(&refreshed)?; - Ok(updated) + .map_err(map_refresh_failure_for_public_cli) } pub fn is_stored_token_expired(stored: &StoredTokens) -> Result { @@ -271,7 +250,7 @@ pub async fn request_device_authorization( )) } -async fn poll_for_device_token( +pub(crate) async fn poll_for_device_token( client: &reqwest::Client, api_base_url: &str, client_id: &str, @@ -339,7 +318,6 @@ fn poll_decision_for_error_code(code: &str) -> PollDecision { } } -#[allow(dead_code)] fn is_token_expired(stored: &StoredTokens, now_unix_seconds: u64) -> bool { let lifetime_seconds = stored.expires_in.saturating_sub(TOKEN_EXPIRY_SKEW_SECONDS); let expires_at = stored @@ -348,7 +326,6 @@ fn is_token_expired(stored: &StoredTokens, now_unix_seconds: u64) -> bool { now_unix_seconds >= expires_at } -#[allow(dead_code)] fn current_unix_timestamp_seconds() -> Result { SystemTime::now() .duration_since(UNIX_EPOCH) @@ -360,7 +337,6 @@ fn current_unix_timestamp_seconds() -> Result { }) } -#[allow(dead_code)] async fn refresh_access_token( client: &reqwest::Client, api_base_url: &str, @@ -426,7 +402,6 @@ async fn refresh_access_token( )) } -#[allow(dead_code)] fn map_refresh_terminal_error(code: &str, description: Option<&str>) -> AuthError { let detail = description .map(str::trim) diff --git a/cli/src/services/auth_command/mod.rs b/cli/src/services/auth_command/mod.rs index 258a296c..70a54489 100644 --- a/cli/src/services/auth_command/mod.rs +++ b/cli/src/services/auth_command/mod.rs @@ -1,8 +1,8 @@ pub mod command; +use std::io::Write; use std::sync::OnceLock; use std::time::{SystemTime, UNIX_EPOCH}; -use std::{io::Write, path::Path}; use anyhow::{anyhow, Context, Result}; use serde_json::json; @@ -82,19 +82,7 @@ pub fn run_login(format: AuthFormat) -> Result { match format { AuthFormat::Text => run_text_login_with_runtime(runtime, &client, &client_id), - AuthFormat::Json => run_login_with( - format, - || Ok(client_id), - |client_id| { - runtime - .block_on(auth::start_device_auth_flow( - &client, - auth::WORKOS_DEFAULT_BASE_URL, - client_id, - )) - .map_err(|e| map_login_error(&e)) - }, - ), + AuthFormat::Json => run_login_json(runtime, &client, &client_id, format), } } @@ -120,22 +108,26 @@ pub fn run_renew(format: AuthFormat, force: bool) -> Result { }; let was_expired = auth::is_stored_token_expired(&stored_tokens)?; - let updated = if force { - runtime - .block_on(auth::renew_stored_token( + let updated: StoredTokens = if force { + let token = runtime + .block_on(auth::renew_stored_token_from_refresh_token( &client, auth::WORKOS_DEFAULT_BASE_URL, &client_id, + &stored_tokens.refresh_token, )) - .map_err(|e| map_login_error(&e))? + .map_err(|e| map_login_error(&e))?; + token_storage::save_tokens(&token)? } else { - runtime - .block_on(auth::ensure_valid_token( + let token = runtime + .block_on(auth::ensure_valid_token_returning_token( &client, auth::WORKOS_DEFAULT_BASE_URL, &client_id, + &stored_tokens, )) - .map_err(|e| map_login_error(&e))? + .map_err(|e| map_login_error(&e))?; + token_storage::save_tokens(&token)? }; render_renew_result(&updated, force || was_expired, format) @@ -178,16 +170,6 @@ fn shared_runtime() -> Result<&'static tokio::runtime::Runtime> { Ok(AUTH_RUNTIME.get_or_init(|| runtime)) } -fn run_login_with(format: AuthFormat, resolve_client_id: R, start_flow: S) -> Result -where - R: FnOnce() -> Result, - S: FnOnce(&str) -> Result, -{ - let client_id = resolve_client_id()?; - let result = start_flow(&client_id)?; - render_login_result(&result, format) -} - fn maybe_renew_expired_credentials( runtime: &tokio::runtime::Runtime, client: &reqwest::Client, @@ -201,12 +183,13 @@ fn maybe_renew_expired_credentials( return Ok(None); } - match runtime.block_on(auth::ensure_valid_token( + match runtime.block_on(auth::ensure_valid_token_returning_token( client, auth::WORKOS_DEFAULT_BASE_URL, client_id, + &stored_tokens, )) { - Ok(updated) => Ok(Some(updated)), + Ok(token) => Ok(Some(token_storage::save_tokens(&token)?)), Err(_) => Ok(None), } } @@ -220,12 +203,13 @@ fn maybe_refresh_tokens_for_status(stored_tokens: &StoredTokens) -> Result Ok(Some(updated)), + Ok(token) => Ok(Some(token_storage::save_tokens(&token)?)), Err(_) => Ok(None), } } @@ -245,8 +229,8 @@ fn run_text_login_with_runtime( write_login_prompt(&authorization)?; - let stored_tokens = runtime - .block_on(auth::complete_device_auth_flow( + let token = runtime + .block_on(auth::complete_device_auth_flow_returning_token( client, auth::WORKOS_DEFAULT_BASE_URL, client_id, @@ -254,6 +238,8 @@ fn run_text_login_with_runtime( )) .map_err(|e| map_login_error(&e))?; + let stored_tokens = token_storage::save_tokens(&token)?; + render_login_result( &DeviceAuthFlowResult { authorization, @@ -263,6 +249,40 @@ fn run_text_login_with_runtime( ) } +fn run_login_json( + runtime: &tokio::runtime::Runtime, + client: &reqwest::Client, + client_id: &str, + format: AuthFormat, +) -> Result { + let authorization = runtime + .block_on(auth::request_device_authorization( + client, + auth::WORKOS_DEFAULT_BASE_URL, + client_id, + )) + .map_err(|e| map_login_error(&e))?; + + let token = runtime + .block_on(auth::complete_device_auth_flow_returning_token( + client, + auth::WORKOS_DEFAULT_BASE_URL, + client_id, + &authorization, + )) + .map_err(|e| map_login_error(&e))?; + + let stored_tokens = token_storage::save_tokens(&token)?; + + render_login_result( + &DeviceAuthFlowResult { + authorization, + stored_tokens, + }, + format, + ) +} + fn resolve_login_client_id() -> Result { let cwd = std::env::current_dir() .context("failed to determine current directory for auth config resolution")?; @@ -273,31 +293,6 @@ fn resolve_login_client_id() -> Result { .unwrap_or_default()) } -#[allow(dead_code)] -fn resolve_login_client_id_with( - cwd: &Path, - env_lookup: FEnv, - read_file: FRead, - path_exists: fn(&Path) -> bool, - resolve_global_config_path: FGlobalPath, -) -> Result -where - FEnv: Fn(&str) -> Option, - FRead: Fn(&Path) -> Result, - FGlobalPath: Fn() -> Result, -{ - Ok(config::resolve_auth_runtime_config_with( - cwd, - env_lookup, - read_file, - path_exists, - resolve_global_config_path, - )? - .workos_client_id - .value - .unwrap_or_default()) -} - fn write_login_prompt(authorization: &auth::DeviceAuthorizationResponse) -> Result<()> { let mut stdout = std::io::stdout().lock(); let browser_url = authorization diff --git a/cli/src/services/db/mod.rs b/cli/src/services/db/mod.rs index 16d10b87..957b5945 100644 --- a/cli/src/services/db/mod.rs +++ b/cli/src/services/db/mod.rs @@ -472,6 +472,39 @@ impl EncryptedTursoDb { }) } + /// Execute a SQL query and synchronously map all returned rows. + pub fn query_map( + &self, + sql: &str, + params: impl turso::params::IntoParams, + mut map_row: F, + ) -> Result> + where + F: FnMut(&turso::Row) -> Result, + { + self.runtime.block_on(async { + let mut rows = self + .conn + .query(sql, params) + .await + .map_err(|e| anyhow::anyhow!("{} query failed: {sql}: {e}", M::db_name()))?; + let mut results = Vec::new(); + + while let Some(row) = rows + .next() + .await + .map_err(|e| anyhow::anyhow!("{} row fetch failed: {sql}: {e}", M::db_name()))? + { + results.push( + map_row(&row) + .with_context(|| format!("{} row mapping failed: {sql}", M::db_name()))?, + ); + } + + Ok(results) + }) + } + /// Run all embedded migrations in order. /// /// Applied migration IDs are recorded in `__sce_migrations` so later diff --git a/cli/src/services/default_paths.rs b/cli/src/services/default_paths.rs index db312052..e7ac7d0f 100644 --- a/cli/src/services/default_paths.rs +++ b/cli/src/services/default_paths.rs @@ -77,28 +77,13 @@ mod roots { self.roots.config_root().join("sce").join("config.json") } - pub(crate) fn auth_tokens_file(&self) -> PathBuf { - self.roots - .state_root() - .join("sce") - .join("auth") - .join("tokens.json") - } - #[allow(dead_code)] pub(crate) fn persisted_artifact_locations(&self) -> Vec { - vec![ - super::PersistedArtifactLocation { - id: super::PersistedArtifactId::GlobalConfig, - root_kind: super::PersistedArtifactRootKind::Config, - path: self.global_config_file(), - }, - super::PersistedArtifactLocation { - id: super::PersistedArtifactId::AuthTokens, - root_kind: super::PersistedArtifactRootKind::State, - path: self.auth_tokens_file(), - }, - ] + vec![super::PersistedArtifactLocation { + id: super::PersistedArtifactId::GlobalConfig, + root_kind: super::PersistedArtifactRootKind::Config, + path: self.global_config_file(), + }] } } @@ -320,7 +305,6 @@ pub(crate) enum PersistedArtifactRootKind { #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(crate) enum PersistedArtifactId { GlobalConfig, - AuthTokens, } #[allow(dead_code)] diff --git a/cli/src/services/token_storage.rs b/cli/src/services/token_storage.rs index 9b67b88c..19dab8b9 100644 --- a/cli/src/services/token_storage.rs +++ b/cli/src/services/token_storage.rs @@ -1,13 +1,29 @@ use std::fmt; -use std::fs::{self, OpenOptions}; -use std::io::Write; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; +use std::sync::OnceLock; use std::time::{SystemTime, UNIX_EPOCH}; use serde::{Deserialize, Serialize}; use crate::services::auth::TokenResponse; -use crate::services::default_paths::resolve_sce_default_locations; +use crate::services::auth_db::AuthDb; +use crate::services::default_paths::auth_db_path; + +/// Constant row ID for the single token row in `auth_credentials`. +const DEFAULT_TOKEN_ROW_ID: i64 = 1; + +/// Lazy singleton for the encrypted auth database. +/// +/// Stores `Result` so initialization failures are preserved across calls. +static AUTH_DB: OnceLock> = OnceLock::new(); + +fn get_auth_db() -> Result<&'static AuthDb, TokenStorageError> { + let result = AUTH_DB.get_or_init(|| AuthDb::new().map_err(|e| e.to_string())); + match result { + Ok(db) => Ok(db), + Err(msg) => Err(TokenStorageError::Database(msg.clone())), + } +} #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct StoredTokens { @@ -36,11 +52,7 @@ impl StoredTokens { #[derive(Debug)] pub enum TokenStorageError { PathResolution(String), - Io(std::io::Error), - Serialization(serde_json::Error), - CorruptedTokenFile(String), - #[allow(dead_code)] - Permission(String), + Database(String), } impl fmt::Display for TokenStorageError { @@ -50,21 +62,9 @@ impl fmt::Display for TokenStorageError { f, "Unable to resolve token storage path: {reason}. Try: set a valid user home/state directory and retry." ), - Self::Io(error) => write!( - f, - "Failed to read or write authentication tokens: {error}. Try: verify file permissions for the auth state directory." - ), - Self::Serialization(error) => write!( + Self::Database(reason) => write!( f, - "Failed to serialize authentication tokens: {error}. Try: rerun login to regenerate credentials." - ), - Self::CorruptedTokenFile(reason) => write!( - f, - "Stored authentication tokens are invalid: {reason}. Try: run 'sce logout' and then 'sce login'." - ), - Self::Permission(reason) => write!( - f, - "Unable to apply secure token file permissions: {reason}. Try: verify local account permissions and retry." + "Token storage database error: {reason}. Try: ensure SCE_DB_ENCRYPTION_KEY is set and the auth database is accessible." ), } } @@ -72,93 +72,85 @@ impl fmt::Display for TokenStorageError { impl std::error::Error for TokenStorageError {} -impl From for TokenStorageError { - fn from(value: std::io::Error) -> Self { - Self::Io(value) - } -} - -impl From for TokenStorageError { - fn from(value: serde_json::Error) -> Self { - Self::Serialization(value) - } -} - pub fn save_tokens(token: &TokenResponse) -> Result { - let token_path = token_file_path()?; + let db = get_auth_db()?; let stored = StoredTokens::from_token_response(token)?; - save_tokens_at_path(&token_path, &stored)?; - Ok(stored) -} -pub fn load_tokens() -> Result, TokenStorageError> { - let token_path = token_file_path()?; - load_tokens_from_path(&token_path) -} - -pub fn delete_tokens() -> Result { - let token_path = token_file_path()?; - delete_tokens_at_path(&token_path) -} + #[allow(clippy::cast_possible_wrap)] + let expires_in: i64 = stored.expires_in as i64; + #[allow(clippy::cast_possible_wrap)] + let stored_at_unix_seconds: i64 = stored.stored_at_unix_seconds as i64; + + let sql = "INSERT OR REPLACE INTO auth_credentials \ + (id, access_token, token_type, expires_in, refresh_token, scope, stored_at_unix_seconds) \ + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"; + + db.execute( + sql, + ( + DEFAULT_TOKEN_ROW_ID, + stored.access_token.as_str(), + stored.token_type.as_str(), + expires_in, + stored.refresh_token.as_str(), + stored.scope.as_deref(), + stored_at_unix_seconds, + ), + ) + .map_err(|e| TokenStorageError::Database(e.to_string()))?; -pub fn token_file_path() -> Result { - resolve_sce_default_locations() - .map(|locations| locations.auth_tokens_file()) - .map_err(|error| TokenStorageError::PathResolution(error.to_string())) + Ok(stored) } -fn save_tokens_at_path(path: &Path, stored: &StoredTokens) -> Result<(), TokenStorageError> { - ensure_parent_directory(path)?; - - let mut file = OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .open(path)?; - - apply_secure_file_permissions(path)?; - - let encoded = serde_json::to_vec_pretty(stored)?; - file.write_all(&encoded)?; - file.write_all(b"\n")?; - file.sync_all()?; +pub fn load_tokens() -> Result, TokenStorageError> { + let db = get_auth_db()?; + + let sql = "SELECT access_token, token_type, expires_in, refresh_token, scope, \ + stored_at_unix_seconds FROM auth_credentials WHERE id = ?1"; + + let rows: Vec = db + .query_map(sql, (DEFAULT_TOKEN_ROW_ID,), |row| { + let access_token: String = row.get(0)?; + let token_type: String = row.get(1)?; + let expires_in: i64 = row.get(2)?; + let refresh_token: String = row.get(3)?; + let scope: Option = row.get(4)?; + let stored_at_unix_seconds: i64 = row.get(5)?; + + #[allow(clippy::cast_sign_loss)] + let expires_in: u64 = expires_in as u64; + #[allow(clippy::cast_sign_loss)] + let stored_at_unix_seconds: u64 = stored_at_unix_seconds as u64; + + Ok(StoredTokens { + access_token, + token_type, + expires_in, + refresh_token, + scope, + stored_at_unix_seconds, + }) + }) + .map_err(|e| TokenStorageError::Database(e.to_string()))?; - Ok(()) + Ok(rows.into_iter().next()) } -fn load_tokens_from_path(path: &Path) -> Result, TokenStorageError> { - let content = match fs::read_to_string(path) { - Ok(content) => content, - Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None), - Err(error) => return Err(TokenStorageError::Io(error)), - }; +pub fn delete_tokens() -> Result { + let db = get_auth_db()?; - let parsed: StoredTokens = serde_json::from_str(&content).map_err(|error| { - TokenStorageError::CorruptedTokenFile(format!("{} ({error})", path.display())) - })?; + let affected = db + .execute( + "DELETE FROM auth_credentials WHERE id = ?1", + (DEFAULT_TOKEN_ROW_ID,), + ) + .map_err(|e| TokenStorageError::Database(e.to_string()))?; - Ok(Some(parsed)) + Ok(affected > 0) } -fn delete_tokens_at_path(path: &Path) -> Result { - match fs::remove_file(path) { - Ok(()) => Ok(true), - Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false), - Err(error) => Err(TokenStorageError::Io(error)), - } -} - -fn ensure_parent_directory(path: &Path) -> Result<(), TokenStorageError> { - let Some(parent) = path.parent() else { - return Err(TokenStorageError::PathResolution(format!( - "token path '{}' has no parent directory", - path.display() - ))); - }; - - fs::create_dir_all(parent)?; - apply_secure_directory_permissions(parent)?; - Ok(()) +pub fn token_file_path() -> Result { + auth_db_path().map_err(|error| TokenStorageError::PathResolution(error.to_string())) } fn current_unix_timestamp_seconds() -> Result { @@ -169,61 +161,3 @@ fn current_unix_timestamp_seconds() -> Result { })? .as_secs()) } - -#[cfg(unix)] -fn apply_secure_directory_permissions(path: &Path) -> Result<(), TokenStorageError> { - use std::os::unix::fs::PermissionsExt; - - fs::set_permissions(path, fs::Permissions::from_mode(0o700))?; - Ok(()) -} - -#[cfg(not(unix))] -fn apply_secure_directory_permissions(_path: &Path) -> Result<(), TokenStorageError> { - Ok(()) -} - -#[cfg(unix)] -fn apply_secure_file_permissions(path: &Path) -> Result<(), TokenStorageError> { - use std::os::unix::fs::PermissionsExt; - - fs::set_permissions(path, fs::Permissions::from_mode(0o600))?; - Ok(()) -} - -#[cfg(windows)] -fn apply_secure_file_permissions(path: &Path) -> Result<(), TokenStorageError> { - use std::process::Command; - - let username = std::env::var("USERNAME").map_err(|_| { - TokenStorageError::Permission( - "USERNAME environment variable is unavailable on Windows".to_string(), - ) - })?; - - let grant_rule = format!("{username}:(R,W)"); - let output = Command::new("icacls") - .arg(path) - .arg("/inheritance:r") - .arg("/grant:r") - .arg(grant_rule) - .output() - .map_err(|error| { - TokenStorageError::Permission(format!("failed to execute icacls: {error}")) - })?; - - if output.status.success() { - Ok(()) - } else { - let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); - Err(TokenStorageError::Permission(format!( - "icacls failed for '{}': {stderr}", - path.display() - ))) - } -} - -#[cfg(not(any(unix, windows)))] -fn apply_secure_file_permissions(_path: &Path) -> Result<(), TokenStorageError> { - Ok(()) -} diff --git a/context/architecture.md b/context/architecture.md index 8c3d5daa..abd2d87e 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -106,7 +106,7 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - `cli/src/services/auth_command/mod.rs` defines the implemented auth command surface for `sce auth login|renew|logout|status`, including device-flow login, stored-token renewal (`--force` supported for renew), logout, and status rendering in text/JSON formats; `cli/src/services/auth_command/command.rs` owns the `AuthCommand` struct and its `RuntimeCommand` impl. - `cli/src/services/db/mod.rs` provides the shared generic Turso infrastructure seam: `DbSpec` supplies a service-specific name, path, and ordered embedded migrations, while `TursoDb` owns parent-directory creation, `Builder::new_local(...)` initialization, Turso connection setup, tokio current-thread runtime bridging, blocking `execute`/`query`/`query_map` wrappers, and generic migration execution with per-database `__sce_migrations` metadata. Existing DB files without migration metadata are upgraded by re-applying the current idempotent migration set and recording each migration ID, so setup/lifecycle initialization applies later migrations to already-created databases. The same module owns shared DB lifecycle helpers for path-health problem collection and DB parent-directory bootstrap. - `cli/src/services/local_db/mod.rs` provides the concrete local DB spec and `LocalDb` type alias over the shared generic `TursoDb` adapter. `LocalDbSpec` resolves the deterministic persistent runtime DB target through the shared default-path seam and declares no local migrations; `TursoDb` supplies blocking `execute`/`query`, parent-directory creation, Turso connection setup, tokio current-thread runtime bridging, and generic migration execution. -- `cli/src/services/auth_db/mod.rs` provides the encrypted auth DB spec and `AuthDb` type alias over `EncryptedTursoDb`. `AuthDbSpec` resolves `/sce/auth.db` through the shared default-path seam and embeds ordered auth migrations where baseline SQL creates `auth_credentials` without `user_id`, with `updated_at`, and a trigger that auto-refreshes `updated_at` on row updates. Auth DB lifecycle setup/doctor integration is wired through `AuthDbLifecycle`; auth command/token-storage reads/writes are not redirected to this DB yet. +- `cli/src/services/auth_db/mod.rs` provides the encrypted auth DB spec and `AuthDb` type alias over `EncryptedTursoDb`. `AuthDbSpec` resolves `/sce/auth.db` through the shared default-path seam and embeds ordered auth migrations where baseline SQL creates `auth_credentials` without `user_id`, with `updated_at`, and a trigger that auto-refreshes `updated_at` on row updates. Auth DB lifecycle setup/doctor integration is wired through `AuthDbLifecycle`; auth command/token-storage reads/writes are directed through `token_storage.rs`, which now persists tokens via the `auth_credentials` table instead of a JSON file. - `cli/src/services/agent_trace_db/mod.rs` provides the Agent Trace DB spec and `AgentTraceDb` type alias over `TursoDb`. `AgentTraceDbSpec` resolves `/sce/agent-trace.db` through the shared default-path seam and embeds an ordered split fresh-start baseline migration set (`001_create_diff_traces`, `002_create_post_commit_patch_intersections`, `003_create_agent_traces`, `004_create_diff_traces_time_ms_id_index`, `005_create_agent_traces_agent_trace_id_index`) without `AUTOINCREMENT`; `agent_traces.agent_trace_id` is `NOT NULL UNIQUE` and indexed by `idx_agent_traces_agent_trace_id`. The module adds `DiffTraceInsert<'_>`/`insert_diff_trace()` (including `model_id`, `tool_name`, and nullable `tool_version` writes), `PostCommitPatchIntersectionInsert<'_>`/`insert_post_commit_patch_intersection()`, and `AgentTraceInsert<'_>`/`insert_agent_trace()` for parameterized writes plus `recent_diff_trace_patches(cutoff_time_ms, end_time_ms)` for inclusive chronological `diff_traces` reads that parse valid raw patch text and return skipped malformed-row reports. `cli/src/services/agent_trace_db/lifecycle.rs` registers Agent Trace DB setup/doctor lifecycle behavior; runtime writes come from `sce hooks diff-trace` (`diff_traces`) and `sce hooks post-commit` (`post_commit_patch_intersections` + built `agent_traces`). - `cli/src/test_support.rs` provides a shared test-only temp-directory helper (`TestTempDir`) used by service tests that need filesystem fixtures. - `cli/src/services/setup/mod.rs` defines the setup command contract (`SetupMode`, `SetupTarget`, `SetupRequest`, CLI flag parser/validator), an `inquire`-backed interactive target prompter (`InquireSetupTargetPrompter`), setup dispatch outcomes (proceed/cancelled), compile-time embedded asset access (`EmbeddedAsset`, target-scoped iterators, required-hook asset iterators/lookups) generated by `cli/build.rs` from the ephemeral crate-local `cli/assets/generated/config/{opencode,claude}/**` mirror plus `cli/assets/hooks/**`, and focused internal support seams for install-flow vs prompt-flow logic; `cli/src/services/setup/command.rs` owns `SetupCommand` and its `RuntimeCommand` impl. Its install engine/orchestrator stages embedded files and uses a unified remove-and-replace policy (removing existing targets before swapping staged content, with deterministic recovery guidance on swap failure and no backup artifact creation), and formats deterministic completion messaging; required-hook install orchestration (`install_required_git_hooks`) follows the same remove-and-replace policy (removing existing hooks before swapping staged content, with deterministic recovery guidance on swap failure). The setup command derives a repo-root-scoped context from the runtime `AppContext` before aggregating `ServiceLifecycle::setup` calls across lifecycle providers (config → local_db → auth_db → agent_trace_db → hooks when requested), so setup providers receive the runtime logger, telemetry, and capability objects instead of a setup-local replacement context. diff --git a/context/cli/cli-command-surface.md b/context/cli/cli-command-surface.md index b2b67ada..3f88b5db 100644 --- a/context/cli/cli-command-surface.md +++ b/context/cli/cli-command-surface.md @@ -95,7 +95,7 @@ A user-invocable `sync` command is not wired in the current CLI surface; local D - `cli/src/services/resilience.rs` defines shared bounded retry/timeout/backoff execution policy (`RetryPolicy`, `run_with_retry`) with deterministic failure messaging and retry observability hooks. - No `cli/src/services/sync.rs` module exists in the current codebase; `sce sync` command wiring is deferred, while local DB initialization and health ownership are split between setup and doctor. - `cli/src/services/default_paths.rs` defines the canonical per-user persisted-location seam for config/state/cache roots plus named default file paths for current persisted artifacts (`global config`, `auth tokens`, `local DB`, `agent trace DB`) used by config discovery, token storage, database adapters, and doctor diagnostics; its internal `roots` seam now owns the platform-aware root-directory resolution so non-test production modules consume shared path accessors instead of resolving owned roots directly. -- `cli/src/services/token_storage.rs` defines WorkOS token persistence (`save_tokens`, `load_tokens`, `delete_tokens`) with shared default-path-seam resolution for the default token file, JSON payload storage including `stored_at_unix_seconds`, graceful missing-file deletion behavior, missing/corrupted-file handling, and restrictive on-disk permissions (`0600` on Unix; Windows best-effort ACL hardening via `icacls`). +- `cli/src/services/token_storage.rs` defines WorkOS token persistence (`save_tokens`, `load_tokens`, `delete_tokens`) via the encrypted `AuthDb` `auth_credentials` table using a `OnceLock` lazy singleton with constant integer row ID `1`. `token_file_path()` returns the auth DB path. `TokenStorageError` exposes `PathResolution` and `Database` variants. No JSON file I/O remains. - `cli/src/services/auth_command/mod.rs` defines the auth command orchestration surface (`AuthRequest`, `AuthSubcommand`, `run_auth_subcommand`) for `login`, `renew`, `logout`, and `status`, including shared text/JSON rendering, token refresh/forced renewal handling for `sce auth renew`, token-storage-backed logout deletion with path-aware remediation guidance, expiry-aware status reporting, canonical credentials-file path reporting sourced from the shared default-path seam, precedence-aware client-ID guidance sourced from the shared auth-runtime resolver instead of env-only assumptions, and a lazily initialized current-thread Tokio runtime with both I/O and time enabled so the auth flows can drive the WorkOS device/refresh paths without the prior I/O-disabled panic; `cli/src/services/auth_command/command.rs` owns the `AuthCommand` struct and its `RuntimeCommand` impl. - `cli/src/app.rs` parses `auth`, `config`, `setup`, `doctor`, `hooks`, `version`, and `completion` into service-owned runtime command handlers so runtime messages are sourced from domain modules instead of inline strings. @@ -140,7 +140,7 @@ A user-invocable `sync` command is not wired in the current CLI surface; local D - `cli/Cargo.toml` currently declares: `anyhow`, `clap`, `clap_complete`, `comfy-table`, `dirs`, `hmac`, `inquire`, `owo-colors`, `reqwest`, `serde`, `serde_json`, `sha2`, `tokio`, `tracing`, `tracing-subscriber`, and `turso`. - `tokio` is pinned with `default-features = false` and keeps a constrained runtime footprint for current-thread `Runtime::block_on` usage, plus timer-backed bounded retry/timeout behavior in resilience-wrapped operations. -- `cli/src/services/auth.rs` now includes both the T03 Device Authorization Flow runtime (`start_device_auth_flow`) and T04 token-refresh runtime (`ensure_valid_token`) for WorkOS: it requests device codes, polls `/oauth/device/token` at fixed API interval (adding 5 seconds on `slow_down`), maps RFC 8628 terminal errors to actionable `Try:` guidance, checks token expiry from persisted `stored_at_unix_seconds + expires_in` with a bounded skew guard, refreshes expired access tokens through `/oauth/token` using `grant_type=refresh_token`, retries transient refresh failures via the shared resilience wrapper, and persists rotated tokens via `cli/src/services/token_storage.rs`. +- `cli/src/services/auth.rs` provides HTTP-only async functions for the WorkOS Device Authorization Flow (`request_device_authorization`, `poll_for_device_token`, `complete_device_auth_flow_returning_token`) and token-refresh operations (`ensure_valid_token_returning_token`, `renew_stored_token_from_refresh_token`, `is_stored_token_expired`): it requests device codes, polls at fixed API interval (adding 5 seconds on `slow_down`), maps RFC 8628 terminal errors to actionable `Try:` guidance, checks token expiry from persisted `stored_at_unix_seconds + expires_in` with a bounded skew guard, refreshes expired access tokens through `/oauth/token` using `grant_type=refresh_token`, and retries transient refresh failures via the shared resilience wrapper. Token storage (`save_tokens`, `load_tokens`) is called by the caller in `cli/src/services/auth_command/mod.rs` outside of any tokio `block_on` context to avoid nested-runtime panics. ## Scope boundary for this phase diff --git a/context/cli/default-path-catalog.md b/context/cli/default-path-catalog.md index dac3e491..ec5933c7 100644 --- a/context/cli/default-path-catalog.md +++ b/context/cli/default-path-catalog.md @@ -15,7 +15,6 @@ ### Per-user persisted paths - global config: `/sce/config.json` -- auth tokens: `/sce/auth/tokens.json` - auth DB: `/sce/auth.db` - local DB: `/sce/local.db` - agent trace DB: `/sce/agent-trace.db` diff --git a/context/glossary.md b/context/glossary.md index 1d7d7a12..eab7cd65 100644 --- a/context/glossary.md +++ b/context/glossary.md @@ -31,7 +31,7 @@ - `sce dependency baseline`: Current crate dependency set declared in `cli/Cargo.toml` (`anyhow`, `clap`, `clap_complete`, `dirs`, `hmac`, `inquire`, `reqwest`, `serde`, `serde_json`, `sha2`, `tokio`, `tracing`, `tracing-subscriber`, `turso`) and validated through normal compile/test coverage. - `local Turso adapter`: Module in `cli/src/services/local_db/mod.rs` that defines `LocalDbSpec` and exposes `LocalDb` as a `TursoDb` alias. It resolves the canonical local DB path with `local_db_path()`, currently declares zero migrations, and inherits `new()`, `execute()`, and `query()` from the shared generic adapter. - `encrypted Turso adapter`: Generic adapter seam in `cli/src/services/db/mod.rs` exposed as `EncryptedTursoDb`, structurally parallel to `TursoDb` (connection, tokio runtime bridge, spec typing). Its constructor resolves `SCE_DB_ENCRYPTION_KEY`, rejects empty keys, enables Turso local encryption with strict `aegis256` cipher selection through `turso::EncryptionOpts`, and runs embedded migrations after connect; the adapter also exposes synchronous `execute`, `query`, and `run_migrations` helpers with `__sce_migrations` tracking parity. -- `auth DB adapter`: Module in `cli/src/services/auth_db/mod.rs` that defines `AuthDbSpec` and exposes `AuthDb` as an `EncryptedTursoDb` alias. It resolves the canonical `/sce/auth.db` path with `auth_db_path()` and embeds ordered auth migrations where baseline SQL creates `auth_credentials` without `user_id`, with `updated_at`, and a trigger that auto-refreshes `updated_at` on row updates. Auth runtime token-storage replacement is not wired yet. +- `auth DB adapter`: Module in `cli/src/services/auth_db/mod.rs` that defines `AuthDbSpec` and exposes `AuthDb` as an `EncryptedTursoDb` alias. It resolves the canonical `/sce/auth.db` path with `auth_db_path()` and embeds ordered auth migrations where baseline SQL creates `auth_credentials` without `user_id`, with `updated_at`, and a trigger that auto-refreshes `updated_at` on row updates. Auth runtime token-storage is now wired through `cli/src/services/token_storage.rs`, which persists tokens via the `auth_credentials` table in the encrypted auth DB instead of a JSON file. - `AuthDbLifecycle`: Lifecycle provider in `cli/src/services/auth_db/lifecycle.rs` that implements `ServiceLifecycle` for encrypted auth DB setup/doctor integration. `diagnose` collects auth DB path health problems, `fix` bootstraps missing auth DB parent directory, and `setup` calls `AuthDb::new()`. Registered as `LifecycleProviderId::AuthDb` in the shared lifecycle catalog. - `agent trace DB adapter`: Module in `cli/src/services/agent_trace_db/mod.rs` that defines `AgentTraceDbSpec`, exposes `AgentTraceDb` as a `TursoDb` alias, resolves `/sce/agent-trace.db` through `agent_trace_db_path()`, embeds an ordered split fresh-start migration set (`001..005`) that creates `diff_traces`, `post_commit_patch_intersections`, and `agent_traces` plus `idx_diff_traces_time_ms_id` and `idx_agent_traces_agent_trace_id`, with `agent_traces.agent_trace_id` enforced as `NOT NULL UNIQUE`; provides typed parameterized insert helpers for diff traces including `model_id` + tool metadata, post-commit intersection rows, and built agent-trace rows (including `agent_trace_id`); exposes chronological recent `diff_traces` query/parse support with malformed-row skip accounting; has `AgentTraceDbLifecycle` for setup/doctor integration; and is written by `sce hooks diff-trace` (`diff_traces`) plus `sce hooks post-commit` (`post_commit_patch_intersections` and built `agent_traces`). - `Agent Trace SCE metadata`: Implementation-owned top-level metadata emitted by `build_agent_trace(...)` as `metadata.sce.version`; the value is sourced from the compiled `sce` CLI package version via `env!("CARGO_PKG_VERSION")`, is schema-validated with the rest of the payload, and is persisted in AgentTraceDb `agent_traces.trace_json` without changing the top-level Agent Trace payload/schema `version`. diff --git a/context/overview.md b/context/overview.md index 6d009b5f..8a384f52 100644 --- a/context/overview.md +++ b/context/overview.md @@ -46,7 +46,7 @@ The targeted support commands (`handover`, `commit`, `validate`) keep their thin The prior no-git-wrapper Agent Trace design artifacts under `context/sce/agent-trace-*.md` are retained only as historical reference; the current CLI runtime no longer wires the removed Agent Trace schema adaptation, payload building, retry replay, or rewrite handling paths into local hook execution. The hooks service now uses a minimal attribution-only runtime: `commit-msg` is the only hook that mutates behavior, conditionally injecting exactly one canonical SCE trailer when the attribution-hooks gate is enabled and `SCE_DISABLED` is false; `pre-commit` and `post-rewrite` remain deterministic no-op entrypoints; `post-commit` is an active intersection entrypoint that captures current commit patch, queries recent `diff_traces` from past 7 days, combines/intersects patches, persists intersection metadata to `post_commit_patch_intersections`, and persists the schema-validated built Agent Trace payload, including optional top-level `tool` metadata from recent diff-trace rows, top-level `metadata.sce.version` from the compiled `sce` CLI package version, and range-level `content_hash` values, to AgentTraceDb `agent_traces` (DB-only, no post-commit Agent Trace file artifact); and `diff-trace` currently validates/persists required non-empty `sessionID`/`diff`/`model_id`/`tool_name`, required `tool_version` (must be present and either `null` or a non-empty string), plus required `u64` millisecond `time`, with non-lossy AgentTraceDb `time_ms` conversion and collision-safe timestamp+attempt artifact filenames. The CLI now also includes an approved operator-environment doctor contract documented in `context/sce/agent-trace-hook-doctor.md`; the runtime now matches the implemented T06 slice for `sce doctor --fix` parsing/help, stable problem/fix-result reporting, canonical hook-repair reuse, and bounded doctor-owned local-DB directory bootstrap for the missing SCE-owned DB parent path. -The local DB service now provides `LocalDb` as a thin `TursoDb` alias in `cli/src/services/local_db/mod.rs`; `LocalDbSpec` resolves the canonical local DB path from the shared default-path catalog and currently declares zero migrations. Shared Turso infrastructure lives in `cli/src/services/db/mod.rs`, where `DbSpec` and generic `TursoDb` support dual-mode operation — local mode via `turso::Builder::new_local()` when `SCE_SYNC_URL`+`SCE_SYNC_TOKEN` are absent, or sync (Turso Cloud) mode via `turso::sync::Builder::new_remote()` when both are set. It owns parent-directory creation, connection setup, tokio current-thread runtime bridging, synchronous `execute`/`query`/`query_map`, generic migration execution, sync operations (`push`/`pull`/`checkpoint`/`stats`) that are no-ops in local mode (sync is never triggered automatically from `execute()`), and shared DB lifecycle helpers for service-specific database wrappers. Auth DB persistence now has a thin encrypted wrapper in `cli/src/services/auth_db/mod.rs`: `AuthDb = EncryptedTursoDb` resolves `/sce/auth.db` and embeds ordered `auth_tokens` table/index migrations, with lifecycle registration wired through `AuthDbLifecycle` in `cli/src/services/auth_db/lifecycle.rs`; auth runtime token-storage replacement remains deferred. Agent Trace persistence now has its own `cli/src/services/agent_trace_db/mod.rs` wrapper, canonical `/sce/agent-trace.db` path, a split fresh-start baseline migration set (`001..005`) covering `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, and indexes (`idx_diff_traces_time_ms_id`, `idx_agent_traces_agent_trace_id`) without `AUTOINCREMENT`, plus `agent_traces.agent_trace_id` as `NOT NULL UNIQUE`; it also provides typed parameterized insert helpers for diff traces, post-commit intersection rows, and built agent-trace rows, chronological recent `diff_traces` query/parse support with malformed-row skip accounting, registered setup/doctor lifecycle provider, active `sce hooks diff-trace` writes for `diff_traces`, and active `sce hooks post-commit` writes for built `agent_traces` payloads. +The local DB service now provides `LocalDb` as a thin `TursoDb` alias in `cli/src/services/local_db/mod.rs`; `LocalDbSpec` resolves the canonical local DB path from the shared default-path catalog and currently declares zero migrations. Shared Turso infrastructure lives in `cli/src/services/db/mod.rs`, where `DbSpec` and generic `TursoDb` support dual-mode operation — local mode via `turso::Builder::new_local()` when `SCE_SYNC_URL`+`SCE_SYNC_TOKEN` are absent, or sync (Turso Cloud) mode via `turso::sync::Builder::new_remote()` when both are set. It owns parent-directory creation, connection setup, tokio current-thread runtime bridging, synchronous `execute`/`query`/`query_map`, generic migration execution, sync operations (`push`/`pull`/`checkpoint`/`stats`) that are no-ops in local mode (sync is never triggered automatically from `execute()`), and shared DB lifecycle helpers for service-specific database wrappers. Auth DB persistence now has a thin encrypted wrapper in `cli/src/services/auth_db/mod.rs`: `AuthDb = EncryptedTursoDb` resolves `/sce/auth.db` and embeds ordered `auth_tokens` table/index migrations, with lifecycle registration wired through `AuthDbLifecycle` in `cli/src/services/auth_db/lifecycle.rs`; auth runtime token-storage is now wired through `token_storage.rs`, which persists tokens via the `auth_credentials` table instead of a JSON file. Agent Trace persistence now has its own `cli/src/services/agent_trace_db/mod.rs` wrapper, canonical `/sce/agent-trace.db` path, a split fresh-start baseline migration set (`001..005`) covering `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, and indexes (`idx_diff_traces_time_ms_id`, `idx_agent_traces_agent_trace_id`) without `AUTOINCREMENT`, plus `agent_traces.agent_trace_id` as `NOT NULL UNIQUE`; it also provides typed parameterized insert helpers for diff traces, post-commit intersection rows, and built agent-trace rows, chronological recent `diff_traces` query/parse support with malformed-row skip accounting, registered setup/doctor lifecycle provider, active `sce hooks diff-trace` writes for `diff_traces`, and active `sce hooks post-commit` writes for built `agent_traces` payloads. The hooks command surface now also supports concrete runtime subcommand routing (`pre-commit`, `commit-msg`, `post-commit`, `post-rewrite`, `diff-trace`) with deterministic argument/STDIN validation. Current runtime behavior keeps attribution disabled by default: the attribution gate enables canonical trailer insertion in `commit-msg`, `pre-commit`/`post-rewrite` remain deterministic no-ops, `post-commit` is the active bounded recent-diff-trace intersection path, and `diff-trace` is the active intake path for parsed STDIN `{ sessionID, diff, time, model_id, tool_name, tool_version }` payload persistence with required non-empty `tool_name`, required nullable/non-empty `tool_version`, required `u64` millisecond `time`, non-lossy AgentTraceDb `time_ms` conversion, and collision-safe timestamp+attempt artifact filenames. This behavior is documented in `context/sce/agent-trace-hooks-command-routing.md`. The setup service now also exposes deterministic required-hook embedded asset accessors (`iter_required_hook_assets`, `get_required_hook_asset`) backed by canonical templates in `cli/assets/hooks/` for `pre-commit`, `commit-msg`, and `post-commit`; this behavior is documented in `context/sce/setup-githooks-hook-asset-packaging.md`. The setup service now also includes required-hook install orchestration (`install_required_git_hooks`) that resolves repository root and effective hooks path from git truth, enforces deterministic per-hook outcomes (`Installed`/`Updated`/`Skipped`), and uses a unified remove-and-replace policy that removes existing hooks before swapping staged content with deterministic recovery guidance on swap failures; this behavior is documented in `context/sce/setup-githooks-install-flow.md`. diff --git a/context/sce/auth-db.md b/context/sce/auth-db.md index d5849f2c..6f65a456 100644 --- a/context/sce/auth-db.md +++ b/context/sce/auth-db.md @@ -1,6 +1,6 @@ # Auth DB -The encrypted auth DB foundation currently consists of a thin Rust wrapper plus path and migration assets. Runtime auth-token reads/writes still use the existing token-storage path. +The encrypted auth DB foundation provides a thin Rust wrapper, path and migration assets, and is consumed by `cli/src/services/token_storage.rs` for runtime auth-token persistence. ## Implemented surface @@ -18,7 +18,7 @@ The encrypted auth DB foundation currently consists of a thin Rust wrapper plus `auth_credentials` is created idempotently with: -- `id TEXT PRIMARY KEY NOT NULL` +- `id INTEGER PRIMARY KEY NOT NULL` - `access_token TEXT NOT NULL` - `token_type TEXT NOT NULL` - `expires_in INTEGER NOT NULL` @@ -42,8 +42,13 @@ Current migration baseline: - `LifecycleProviderId::AuthDb` is the provider identifier. - The lifecycle provider is registered in deterministic order: config → local_db → auth_db → agent_trace_db → hooks. -## Not yet wired +## Token storage integration -- Existing auth command token storage still uses the current runtime path; auth reads/writes are not redirected to this DB. +- `cli/src/services/token_storage.rs` now uses `AuthDb` for all persistence operations (`save_tokens`, `load_tokens`, `delete_tokens`) via a `OnceLock>` lazy singleton. +- `token_file_path()` returns the auth DB path from `auth_db_path()` instead of a JSON file path. +- `TokenStorageError` exposes `PathResolution` and `Database` variants; former `Io`, `Serialization`, `CorruptedTokenFile`, and `Permission` variants have been removed. +- No JSON file I/O remains in `token_storage.rs`. +- The `auth_credentials` row uses constant integer ID `1` for single-row token storage. +- Encryption is required: `SCE_DB_ENCRYPTION_KEY` must be set; failures surface as `TokenStorageError::Database`. -See also: [shared-turso-db.md](shared-turso-db.md), [../cli/default-path-catalog.md](../cli/default-path-catalog.md), [../context-map.md](../context-map.md) +See also: [shared-turso-db.md](shared-turso-db.md), [../cli/default-path-catalog.md](../cli/default-path-catalog.md), [../context-map.md](../context-map.md), [../../context/plans/token-storage-db-migration.md](../../context/plans/token-storage-db-migration.md) From 2314b2cc61f65d8efa69dd1f8a9de1cf2e35299c Mon Sep 17 00:00:00 2001 From: stefanskoricdev Date: Tue, 26 May 2026 19:02:40 +0200 Subject: [PATCH 5/7] db: Add OS credential store encryption key management Encryption keys are now managed via the platform-native credential store (macOS Keychain, Linux keyutils, Windows Credential Store). Add cli/src/services/db/encryption_key.rs with: - get_or_create_encryption_key() that generates a 32-byte random key on first use and retrieves it from the credential store on subsequent calls - ensure_default_store() for platform-specific store initialization - Clear remediation messaging for missing keys (e.g. Linux keyutils expiry) or unsupported platforms - Hex encoding helpers and unit tests Update EncryptedTursoDb::new() to consume the new module. Update error messages in token_storage.rs and context docs. New dependencies: keyring-core v1, rand v0.8, and platform-specific keyring store crates (linux-keyutils-keyring-store, apple-native-keyring-store, windows-native-keyring-store). Co-authored-by: SCE --- cli/Cargo.lock | 58 ++++++++ cli/Cargo.toml | 11 ++ cli/src/services/db/encryption_key.rs | 196 ++++++++++++++++++++++++++ cli/src/services/db/mod.rs | 19 +-- cli/src/services/token_storage.rs | 2 +- context/architecture.md | 2 +- context/context-map.md | 2 +- context/glossary.md | 3 +- context/sce/auth-db.md | 2 +- context/sce/shared-turso-db.md | 17 ++- 10 files changed, 291 insertions(+), 21 deletions(-) create mode 100644 cli/src/services/db/encryption_key.rs diff --git a/cli/Cargo.lock b/cli/Cargo.lock index de28c7d3..ca4e6ad4 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -157,6 +157,17 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "apple-native-keyring-store" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7be2f067ccd8d4b4d4a66ddafe0f32a5dff31732f32dbff85fefc40929b1f72" +dependencies = [ + "keyring-core", + "log", + "security-framework", +] + [[package]] name = "arc-swap" version = "1.9.1" @@ -1786,6 +1797,15 @@ dependencies = [ "uuid-simd", ] +[[package]] +name = "keyring-core" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb1e621458ca9c51aa110bd0339d4751a056b9576bf1253aee1aa560dda0fc9d" +dependencies = [ + "log", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1870,6 +1890,26 @@ dependencies = [ "syn", ] +[[package]] +name = "linux-keyutils" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83270a18e9f90d0707c41e9f35efada77b64c0e6f3f1810e71c8368a864d5590" +dependencies = [ + "bitflags", + "libc", +] + +[[package]] +name = "linux-keyutils-keyring-store" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39fbed79f71dc21eb21d3d07c0e908a3c58ff9a1fdbf5cf44230fb3deb6d994b" +dependencies = [ + "keyring-core", + "linux-keyutils", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -3010,6 +3050,7 @@ name = "shared-context-engineering" version = "0.2.0" dependencies = [ "anyhow", + "apple-native-keyring-store", "chrono", "clap", "clap_complete", @@ -3017,8 +3058,11 @@ dependencies = [ "hmac", "inquire", "jsonschema", + "keyring-core", + "linux-keyutils-keyring-store", "murmur3", "owo-colors 4.3.0", + "rand 0.8.6", "reqwest", "serde", "serde_json", @@ -3027,6 +3071,7 @@ dependencies = [ "tracing", "turso", "uuid", + "windows-native-keyring-store", ] [[package]] @@ -4272,6 +4317,19 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-native-keyring-store" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063426e76fdec7438d56bb777f67e318a84a25c707b07e575cb8b78e10c028f8" +dependencies = [ + "byteorder", + "keyring-core", + "regex", + "windows-sys 0.61.2", + "zeroize", +] + [[package]] name = "windows-result" version = "0.4.1" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 8ea220fa..ae1f3134 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -36,7 +36,9 @@ dirs = "6" hmac = "0.13" inquire = "0.9" jsonschema = "0.46" +keyring-core = "1" owo-colors = { version = "4", features = ["supports-colors"] } +rand = { version = "0.8", default-features = false, features = ["std", "std_rng"] } reqwest = { version = "0.13", default-features = false, features = ["json", "form", "rustls"] } serde = { version = "1", features = ["derive"] } serde_json = "1" @@ -47,6 +49,15 @@ tracing = "0.1" uuid = { version = "1", features = ["v4", "v7"] } murmur3 = "0.5.2" +[target.'cfg(target_os = "linux")'.dependencies] +linux-keyutils-keyring-store = "1" + +[target.'cfg(target_os = "macos")'.dependencies] +apple-native-keyring-store = { version = "1", features = ["keychain"] } + +[target.'cfg(target_os = "windows")'.dependencies] +windows-native-keyring-store = "1" + [build-dependencies] sha2 = "0.11" diff --git a/cli/src/services/db/encryption_key.rs b/cli/src/services/db/encryption_key.rs new file mode 100644 index 00000000..73a8e374 --- /dev/null +++ b/cli/src/services/db/encryption_key.rs @@ -0,0 +1,196 @@ +//! Encryption key management backed by the OS credential store. +//! +//! Provides a single entry point to get-or-create a 64-character hex +//! encryption key stored in the platform-native credential store +//! (macOS Keychain, Linux keyutils, Windows Credential Store). +//! +//! On first use for a given database name (when the database file does +//! not yet exist), a random 32-byte key is generated, hex-encoded, +//! persisted in the credential store, and returned. Subsequent calls +//! read the key from the credential store. + +use std::path::Path; +use std::sync::Mutex; + +use anyhow::{Context, Result}; +use keyring_core::Entry; + +/// Guards the one-time registration of the platform-native credential store. +/// +/// A `Mutex` is used instead of `OnceLock::get_or_try_init` because that +/// API is still unstable in the current toolchain (1.95.0). The mutex +/// ensures thread-safe single initialization and naturally retries on +/// transient failures (the lock is released when the error propagates). +static DEFAULT_STORE: Mutex = Mutex::new(false); + +fn ensure_default_store() -> Result<()> { + let mut guard = DEFAULT_STORE + .lock() + .map_err(|_| anyhow::anyhow!("internal error: credential store mutex poisoned"))?; + + if !*guard { + #[cfg(target_os = "linux")] + { + keyring_core::set_default_store( + linux_keyutils_keyring_store::Store::new() + .context("failed to create Linux keyutils keyring store")?, + ); + } + #[cfg(target_os = "macos")] + { + keyring_core::set_default_store( + apple_native_keyring_store::keychain::Store::new() + .context("failed to create macOS keychain store")?, + ); + } + #[cfg(target_os = "windows")] + { + keyring_core::set_default_store( + windows_native_keyring_store::Store::new() + .context("failed to create Windows credential store")?, + ); + } + #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] + { + anyhow::bail!( + "unsupported platform: no OS credential store available for encryption key \ + management. Try: run 'sce' on a supported platform (Linux, macOS, or Windows)." + ); + } + + *guard = true; + } + + Ok(()) +} + +/// Retrieve or create the encryption key for the named database. +/// +/// The key is stored in the platform-native credential store under the +/// service name `"sce"` with the database name as the user/account +/// identifier. +/// +/// # Arguments +/// * `db_path` — Canonical path to the database file. Used to decide +/// whether this is a first-use (generate new key) or a subsequent +/// access (read existing key). +/// * `db_name` — Logical database name used as the credential store +/// username (e.g. `"auth_db"`, `"agent_trace_db"`). +/// +/// # Returns +/// A 64-character lowercase hex string that is the encryption key. +/// +/// # Errors +/// - Returns an error if the credential store cannot be initialised on +/// the current platform. +/// - Returns an error if the database file exists but the keyring entry +/// is missing (e.g. keyring was cleared or has expired on Linux). +/// - Returns an error if key generation or credential store I/O fails. +pub fn get_or_create_encryption_key(db_path: &Path, db_name: &str) -> Result { + ensure_default_store()?; + + let entry = Entry::new("sce", db_name).with_context(|| { + format!( + "failed to create keyring entry for service 'sce' / user '{db_name}'. \ + Try: ensure the OS credential store is available and accessible." + ) + })?; + + // Try to retrieve an existing password from the credential store. + if let Ok(password) = entry.get_password() { + return Ok(password); + } + + // No existing key was found. If the database file does not exist, + // this is a first-use scenario: generate and store a new key. + if !db_path.exists() { + let hex_key = generate_key(); + + entry.set_password(&hex_key).with_context(|| { + format!( + "failed to store encryption key for '{db_name}' in credential store. \ + Try: ensure the OS credential store is operational (e.g. 'gnome-keyring' \ + or 'secret-service' on Linux, Keychain Access on macOS)." + ) + })?; + + return Ok(hex_key); + } + + // The database file exists but the keyring entry is missing. + // This can happen on Linux when the persistent keyring expires + // (kernel keyutils is in-memory). Provide clear remediation. + anyhow::bail!( + "encryption key for '{db_name}' not found in credential store.\n\ + The database file exists at '{}' but no matching credential was found \ + for service 'sce' / user '{db_name}'.\n\ + On Linux, this can happen when the kernel keyring session expires. \ + Try: ensure the OS credential store is available.\n\ + If the database file is also stale or you no longer need its data, \ + delete it and the key will be regenerated automatically on next use.", + db_path.display() + ); +} + +/// Generate a 64-character lowercase hex key from 32 random bytes. +fn generate_key() -> String { + use rand::Rng; + + let key: [u8; 32] = rand::thread_rng().gen(); + hex_encode(&key) +} + +/// Hex-encode a byte slice into a lowercase hex string. +fn hex_encode(bytes: &[u8]) -> String { + use std::fmt::Write; + + let mut hex = String::with_capacity(bytes.len() * 2); + for &b in bytes { + let _ = write!(hex, "{b:02x}"); + } + hex +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_hex_encode_empty() { + assert_eq!(hex_encode(&[]), ""); + } + + #[test] + fn test_hex_encode_32_bytes() { + let bytes = [0xdeu8; 32]; + let expected = "de".repeat(32); + assert_eq!(hex_encode(&bytes), expected); + } + + #[test] + fn test_hex_encode_mixed() { + let bytes = [0x00u8, 0x01, 0xfe, 0xff, 0xab, 0xcd]; + assert_eq!(hex_encode(&bytes), "0001feffabcd"); + } + + #[test] + fn test_generate_key_length() { + let key = generate_key(); + assert_eq!(key.len(), 64); + } + + #[test] + fn test_generate_key_lowercase() { + let key = generate_key(); + assert!(key + .chars() + .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit())); + } + + #[test] + fn test_generate_key_is_random() { + let key1 = generate_key(); + let key2 = generate_key(); + assert_ne!(key1, key2); + } +} diff --git a/cli/src/services/db/mod.rs b/cli/src/services/db/mod.rs index 957b5945..2cb1e175 100644 --- a/cli/src/services/db/mod.rs +++ b/cli/src/services/db/mod.rs @@ -22,9 +22,10 @@ const MIGRATIONS_TABLE_SQL: &str = "CREATE TABLE IF NOT EXISTS __sce_migrations )"; const SELECT_MIGRATION_SQL: &str = "SELECT id FROM __sce_migrations WHERE id = ?1 LIMIT 1"; const INSERT_MIGRATION_SQL: &str = "INSERT INTO __sce_migrations (id) VALUES (?1)"; -const DB_ENCRYPTION_KEY_ENV: &str = "SCE_DB_ENCRYPTION_KEY"; const ENCRYPTION_CIPHER_AEGIS256: &str = "aegis256"; +pub mod encryption_key; + /// Service-specific Turso database configuration. #[allow(dead_code)] pub trait DbSpec { @@ -246,7 +247,6 @@ pub struct TursoDb { /// /// Mirrors the structural seams of [`TursoDb`] while reserving encrypted local /// database initialization for services that require at-rest encryption. -#[allow(dead_code)] pub struct EncryptedTursoDb { conn: turso::Connection, runtime: tokio::runtime::Runtime, @@ -374,7 +374,6 @@ impl TursoDb { } } -#[allow(dead_code)] impl EncryptedTursoDb { /// Open or create the encrypted database at the spec-provided canonical /// path. @@ -384,16 +383,7 @@ impl EncryptedTursoDb { pub fn new() -> Result { let db_name = M::db_name(); let db_path = M::db_path().with_context(|| format!("failed to resolve {db_name} path"))?; - let encryption_key = std::env::var(DB_ENCRYPTION_KEY_ENV).with_context(|| { - format!( - "missing or invalid {DB_ENCRYPTION_KEY_ENV} for {db_name}. Try: export {DB_ENCRYPTION_KEY_ENV} with a valid encryption key and rerun the command." - ) - })?; - if encryption_key.trim().is_empty() { - anyhow::bail!( - "missing or invalid {DB_ENCRYPTION_KEY_ENV} for {db_name}. Try: export {DB_ENCRYPTION_KEY_ENV} with a non-empty 64-character hex key and rerun the command." - ); - } + let encryption_key = encryption_key::get_or_create_encryption_key(&db_path, db_name)?; ensure_db_parent_dir(db_name, &db_path)?; @@ -416,7 +406,7 @@ impl EncryptedTursoDb { .await .map_err(|e| { anyhow::anyhow!( - "failed to open encrypted {db_name} database at {} with cipher {ENCRYPTION_CIPHER_AEGIS256}. Try: verify {DB_ENCRYPTION_KEY_ENV} is a valid key and that local Turso encryption support is available: {e}", + "failed to open encrypted {db_name} database at {} with cipher {ENCRYPTION_CIPHER_AEGIS256}. Try: verify the credential store encryption key is valid and that local Turso encryption support is available: {e}", db_path.display() ) })?; @@ -463,6 +453,7 @@ impl EncryptedTursoDb { /// /// # Returns /// A `turso::Rows` iterator over the result set. + #[allow(dead_code)] pub fn query(&self, sql: &str, params: impl turso::params::IntoParams) -> Result { self.runtime.block_on(async { self.conn diff --git a/cli/src/services/token_storage.rs b/cli/src/services/token_storage.rs index 19dab8b9..29517703 100644 --- a/cli/src/services/token_storage.rs +++ b/cli/src/services/token_storage.rs @@ -64,7 +64,7 @@ impl fmt::Display for TokenStorageError { ), Self::Database(reason) => write!( f, - "Token storage database error: {reason}. Try: ensure SCE_DB_ENCRYPTION_KEY is set and the auth database is accessible." + "Token storage database error: {reason}. Try: ensure the OS credential store is available and the auth database is accessible." ), } } diff --git a/context/architecture.md b/context/architecture.md index abd2d87e..5ab62d8a 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -104,7 +104,7 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - `cli/src/services/capabilities.rs` defines the current broad CLI dependency-injection capability traits consumed by `AppContext`: `FsOps` with `StdFsOps` for filesystem operations and `GitOps` with `ProcessGitOps` for git command execution plus repository-root/hooks-directory resolution. Existing services do not consume these traits internally yet; doctor/setup/hooks/config migration is deferred to later lifecycle/AppContext tasks. - `cli/src/services/lifecycle.rs` defines the current compile-safe `ServiceLifecycle` trait seam. It has default no-op `diagnose(&AppContext)`, `fix(&AppContext, &[HealthProblem])`, and `setup(&AppContext)` methods, with lifecycle-owned health, fix, and setup result types so the trait contract is not publicly anchored to doctor/setup module types. The same module owns the shared lifecycle provider catalog/factory, returning providers in deterministic order (config → local_db → auth_db → agent_trace_db → hooks when requested). Hooks exposes a `HooksLifecycle` provider in `cli/src/services/hooks/lifecycle.rs` for hook rollout diagnosis/fix/setup using lifecycle-owned health records plus the canonical required-hook installer. Config exposes a `ConfigLifecycle` provider in `cli/src/services/config/lifecycle.rs` for global/repo-local config validation and repo-local `.sce/config.json` bootstrap. local_db exposes a `LocalDbLifecycle` provider in `cli/src/services/local_db/lifecycle.rs` for canonical local DB path health, parent-directory readiness/bootstrap, and `LocalDb::new()` setup. auth_db exposes an `AuthDbLifecycle` provider in `cli/src/services/auth_db/lifecycle.rs` for canonical auth DB path health, parent-directory readiness/bootstrap, and `AuthDb::new()` setup. agent_trace_db exposes an `AgentTraceDbLifecycle` provider in `cli/src/services/agent_trace_db/lifecycle.rs` for canonical Agent Trace DB path health, parent-directory readiness/bootstrap, and `AgentTraceDb::new()` setup. Doctor runtime aggregates the full provider catalog for `diagnose` and `fix` and adapts lifecycle records into doctor report/fix records at the orchestration boundary; setup command aggregates the shared catalog for `setup` with hooks included only when requested and adapts hook setup outcomes before rendering setup-owned messages. - `cli/src/services/auth_command/mod.rs` defines the implemented auth command surface for `sce auth login|renew|logout|status`, including device-flow login, stored-token renewal (`--force` supported for renew), logout, and status rendering in text/JSON formats; `cli/src/services/auth_command/command.rs` owns the `AuthCommand` struct and its `RuntimeCommand` impl. -- `cli/src/services/db/mod.rs` provides the shared generic Turso infrastructure seam: `DbSpec` supplies a service-specific name, path, and ordered embedded migrations, while `TursoDb` owns parent-directory creation, `Builder::new_local(...)` initialization, Turso connection setup, tokio current-thread runtime bridging, blocking `execute`/`query`/`query_map` wrappers, and generic migration execution with per-database `__sce_migrations` metadata. Existing DB files without migration metadata are upgraded by re-applying the current idempotent migration set and recording each migration ID, so setup/lifecycle initialization applies later migrations to already-created databases. The same module owns shared DB lifecycle helpers for path-health problem collection and DB parent-directory bootstrap. +- `cli/src/services/db/mod.rs` provides the shared generic Turso infrastructure seam: `DbSpec` supplies a service-specific name, path, and ordered embedded migrations, while `TursoDb` owns parent-directory creation, `Builder::new_local(...)` initialization, Turso connection setup, tokio current-thread runtime bridging, blocking `execute`/`query`/`query_map` wrappers, and generic migration execution with per-database `__sce_migrations` metadata. The same module also provides `EncryptedTursoDb`, a structurally parallel encrypted adapter that resolves the encryption key from the OS credential store via `encryption_key::get_or_create_encryption_key()`, enables Turso local encryption with strict `aegis256` cipher selection, and exposes the same synchronous `execute`/`query`/`query_map` wrappers plus migration execution. Existing DB files without migration metadata are upgraded by re-applying the current idempotent migration set and recording each migration ID, so setup/lifecycle initialization applies later migrations to already-created databases. The same module owns shared DB lifecycle helpers for path-health problem collection and DB parent-directory bootstrap. `cli/src/services/db/encryption_key.rs` provides the keyring-backed credential store module for automatic encryption key generation and retrieval. - `cli/src/services/local_db/mod.rs` provides the concrete local DB spec and `LocalDb` type alias over the shared generic `TursoDb` adapter. `LocalDbSpec` resolves the deterministic persistent runtime DB target through the shared default-path seam and declares no local migrations; `TursoDb` supplies blocking `execute`/`query`, parent-directory creation, Turso connection setup, tokio current-thread runtime bridging, and generic migration execution. - `cli/src/services/auth_db/mod.rs` provides the encrypted auth DB spec and `AuthDb` type alias over `EncryptedTursoDb`. `AuthDbSpec` resolves `/sce/auth.db` through the shared default-path seam and embeds ordered auth migrations where baseline SQL creates `auth_credentials` without `user_id`, with `updated_at`, and a trigger that auto-refreshes `updated_at` on row updates. Auth DB lifecycle setup/doctor integration is wired through `AuthDbLifecycle`; auth command/token-storage reads/writes are directed through `token_storage.rs`, which now persists tokens via the `auth_credentials` table instead of a JSON file. - `cli/src/services/agent_trace_db/mod.rs` provides the Agent Trace DB spec and `AgentTraceDb` type alias over `TursoDb`. `AgentTraceDbSpec` resolves `/sce/agent-trace.db` through the shared default-path seam and embeds an ordered split fresh-start baseline migration set (`001_create_diff_traces`, `002_create_post_commit_patch_intersections`, `003_create_agent_traces`, `004_create_diff_traces_time_ms_id_index`, `005_create_agent_traces_agent_trace_id_index`) without `AUTOINCREMENT`; `agent_traces.agent_trace_id` is `NOT NULL UNIQUE` and indexed by `idx_agent_traces_agent_trace_id`. The module adds `DiffTraceInsert<'_>`/`insert_diff_trace()` (including `model_id`, `tool_name`, and nullable `tool_version` writes), `PostCommitPatchIntersectionInsert<'_>`/`insert_post_commit_patch_intersection()`, and `AgentTraceInsert<'_>`/`insert_agent_trace()` for parameterized writes plus `recent_diff_trace_patches(cutoff_time_ms, end_time_ms)` for inclusive chronological `diff_traces` reads that parse valid raw patch text and return skipped malformed-row reports. `cli/src/services/agent_trace_db/lifecycle.rs` registers Agent Trace DB setup/doctor lifecycle behavior; runtime writes come from `sce hooks diff-trace` (`diff_traces`) and `sce hooks post-commit` (`post_commit_patch_intersections` + built `agent_traces`). diff --git a/context/context-map.md b/context/context-map.md index 117ce1db..bc37c1bc 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -41,7 +41,7 @@ Feature/domain context: - `context/sce/agent-trace-post-rewrite-local-remap-ingestion.md` (current post-rewrite no-op baseline plus historical remap-ingestion reference) - `context/sce/agent-trace-rewrite-trace-transformation.md` (current post-rewrite no-op baseline plus historical rewrite-transformation reference) - `context/sce/local-db.md` (implemented `cli/src/services/local_db/mod.rs` local database spec with `LocalDb = TursoDb`, canonical local DB path resolution, zero local migrations, and inherited blocking `execute`/`query` methods using the shared Turso adapter) -- `context/sce/shared-turso-db.md` (current shared `cli/src/services/db/mod.rs` Turso database infrastructure seam, including `DbSpec`, generic `TursoDb`, `EncryptedTursoDb` encrypted constructor path with env key `SCE_DB_ENCRYPTION_KEY` + strict `aegis256` selection via Turso `EncryptionOpts`, encrypted-adapter sync `execute`/`query` wrappers plus migration execution parity, sync `query_map` on `TursoDb`, per-database `__sce_migrations` tracking, generic embedded migration execution, and current concrete wrappers for `LocalDb`, `AgentTraceDb`, and encrypted `AuthDb`) +- `context/sce/shared-turso-db.md` (current shared `cli/src/services/db/mod.rs` Turso database infrastructure seam, including `DbSpec`, generic `TursoDb`, `EncryptedTursoDb` encrypted constructor path with keyring-backed encryption key via `encryption_key::get_or_create_encryption_key()` + strict `aegis256` selection via Turso `EncryptionOpts`, encrypted-adapter `execute`/`query`/`query_map` wrappers plus migration execution parity, per-database `__sce_migrations` tracking, generic embedded migration execution, `cli/src/services/db/encryption_key.rs` keyring-backed credential-store get-or-create helper, and current concrete wrappers for `LocalDb`, `AgentTraceDb`, and encrypted `AuthDb`) - `context/sce/agent-trace-db.md` (implemented `cli/src/services/agent_trace_db/mod.rs` Agent Trace database wrapper with canonical `/sce/agent-trace.db` path, ordered `diff_traces`, `post_commit_patch_intersections`, `diff_traces(time_ms, id)` index, `agent_traces`, nullable `diff_traces.model_id`, nullable `diff_traces.tool_name`, nullable `diff_traces.tool_version`, and nullable `agent_traces.agent_trace_id` migrations applied through shared migration metadata, typed parameterized insert helpers for diff traces including `model_id` + tool metadata, post-commit intersection rows, and built `agent_traces` rows with `agent_trace_id` plus schema-validated trace JSON containing range `content_hash`, inclusive bounded chronological recent `diff_traces` query/parse support with malformed-row skip accounting, registered setup/doctor lifecycle provider, and active hook writers for `diff_traces` intake plus post-commit intersection/agent-trace persistence) - `context/sce/auth-db.md` (current encrypted auth DB foundation: canonical `/sce/auth.db` path resolver, `AuthDb = EncryptedTursoDb` wrapper, baseline migration 001 creating `auth_credentials` without `user_id`, with `updated_at`, and 002 creating the `updated_at` auto-refresh trigger instead of a `user_id` index, and `AuthDbLifecycle` provider registered in the shared lifecycle catalog) - `context/sce/agent-trace-core-schema-migrations.md` (historical reference for removed local DB schema bootstrap behavior; T03 now implements the actual local DB with migrations) diff --git a/context/glossary.md b/context/glossary.md index eab7cd65..7b2c14fb 100644 --- a/context/glossary.md +++ b/context/glossary.md @@ -30,7 +30,7 @@ - `RuntimeCommand seam`: Internal command-execution abstraction where clap-parsed commands are converted into boxed command objects with `name()` and `execute(&AppContext)` methods, allowing app lifecycle orchestration to log and run commands without a single central dispatch `match` covering every command; the `RuntimeCommand` trait and `RuntimeCommandHandle` type alias are defined in `cli/src/services/command_registry.rs`, and the `CommandRegistry` struct maps command names to zero-arg constructor functions for dispatch. Migrated commands (`HelpCommand`, `HelpTextCommand`, `VersionCommand`, `CompletionCommand`, `AuthCommand`, `ConfigCommand`, `SetupCommand`, `DoctorCommand`, `HooksCommand`) live in service-owned `command.rs` files; parsed request construction lives in `cli/src/services/parse/command_runtime.rs` when user-provided options or subcommands are required. - `sce dependency baseline`: Current crate dependency set declared in `cli/Cargo.toml` (`anyhow`, `clap`, `clap_complete`, `dirs`, `hmac`, `inquire`, `reqwest`, `serde`, `serde_json`, `sha2`, `tokio`, `tracing`, `tracing-subscriber`, `turso`) and validated through normal compile/test coverage. - `local Turso adapter`: Module in `cli/src/services/local_db/mod.rs` that defines `LocalDbSpec` and exposes `LocalDb` as a `TursoDb` alias. It resolves the canonical local DB path with `local_db_path()`, currently declares zero migrations, and inherits `new()`, `execute()`, and `query()` from the shared generic adapter. -- `encrypted Turso adapter`: Generic adapter seam in `cli/src/services/db/mod.rs` exposed as `EncryptedTursoDb`, structurally parallel to `TursoDb` (connection, tokio runtime bridge, spec typing). Its constructor resolves `SCE_DB_ENCRYPTION_KEY`, rejects empty keys, enables Turso local encryption with strict `aegis256` cipher selection through `turso::EncryptionOpts`, and runs embedded migrations after connect; the adapter also exposes synchronous `execute`, `query`, and `run_migrations` helpers with `__sce_migrations` tracking parity. +- `encrypted Turso adapter`: Generic adapter seam in `cli/src/services/db/mod.rs` exposed as `EncryptedTursoDb`, structurally parallel to `TursoDb` (connection, tokio runtime bridge, spec typing). Its constructor resolves the encryption key from the OS credential store via `encryption_key::get_or_create_encryption_key(&db_path, db_name)`, enables Turso local encryption with strict `aegis256` cipher selection through `turso::EncryptionOpts`, and runs embedded migrations after connect; the adapter also exposes synchronous `execute`, `query`, `query_map`, and `run_migrations` helpers with `__sce_migrations` tracking parity. - `auth DB adapter`: Module in `cli/src/services/auth_db/mod.rs` that defines `AuthDbSpec` and exposes `AuthDb` as an `EncryptedTursoDb` alias. It resolves the canonical `/sce/auth.db` path with `auth_db_path()` and embeds ordered auth migrations where baseline SQL creates `auth_credentials` without `user_id`, with `updated_at`, and a trigger that auto-refreshes `updated_at` on row updates. Auth runtime token-storage is now wired through `cli/src/services/token_storage.rs`, which persists tokens via the `auth_credentials` table in the encrypted auth DB instead of a JSON file. - `AuthDbLifecycle`: Lifecycle provider in `cli/src/services/auth_db/lifecycle.rs` that implements `ServiceLifecycle` for encrypted auth DB setup/doctor integration. `diagnose` collects auth DB path health problems, `fix` bootstraps missing auth DB parent directory, and `setup` calls `AuthDb::new()`. Registered as `LifecycleProviderId::AuthDb` in the shared lifecycle catalog. - `agent trace DB adapter`: Module in `cli/src/services/agent_trace_db/mod.rs` that defines `AgentTraceDbSpec`, exposes `AgentTraceDb` as a `TursoDb` alias, resolves `/sce/agent-trace.db` through `agent_trace_db_path()`, embeds an ordered split fresh-start migration set (`001..005`) that creates `diff_traces`, `post_commit_patch_intersections`, and `agent_traces` plus `idx_diff_traces_time_ms_id` and `idx_agent_traces_agent_trace_id`, with `agent_traces.agent_trace_id` enforced as `NOT NULL UNIQUE`; provides typed parameterized insert helpers for diff traces including `model_id` + tool metadata, post-commit intersection rows, and built agent-trace rows (including `agent_trace_id`); exposes chronological recent `diff_traces` query/parse support with malformed-row skip accounting; has `AgentTraceDbLifecycle` for setup/doctor integration; and is written by `sce hooks diff-trace` (`diff_traces`) plus `sce hooks post-commit` (`post_commit_patch_intersections` and built `agent_traces`). @@ -160,5 +160,6 @@ - `build_agent_trace`: Public function in `cli/src/services/agent_trace.rs` that computes `intersection_patch = intersect_patches(constructed_patch, post_commit_patch)`, iterates over `post_commit_patch` files and hunks, classifies each hunk against `intersection_patch`, validates `AgentTraceMetadataInput.commit_timestamp` as RFC 3339, derives UUIDv7 `AgentTrace.id` from that same commit-time moment, and returns `Result` with top-level metadata fields plus one `Conversation` per `post_commit_patch` hunk; consumed by the active post-commit hook flow, with no standalone `sce agent-trace` command surface. - `agent-trace plugin diff extraction seam`: Exported helper `extractDiffTracePayload` in `config/lib/agent-trace-plugin/opencode-sce-agent-trace-plugin.ts` that reads `input.event` and returns `{ sessionID, diff, time, model_id }` only when the event is `message.updated` with `properties.info.role === "user"`; extracts `sessionID` from `info.sessionID` (falling back to `"unknown"` when missing/empty), joins object-entry `patch` fields from `info.summary?.diffs[]` with `\n` while preserving empty patch strings for Rust-side validation, uses `Date.now()` for `time`, and builds `model_id` as `info.model.providerID/info.model.modelID`; returns `undefined` for non-`message.updated` events, non-user messages, messages without a non-empty `summary.diffs` array, or diffs arrays without object entries. - `agent-trace plugin diff extraction seam`: Helper `extractDiffTracePayload` in `config/lib/agent-trace-plugin/opencode-sce-agent-trace-plugin.ts` that accepts a typed `message.updated` event and returns `{ sessionID, diff, time, model_id }` only for user-role messages with non-empty `summary?.diffs`; it joins present object-entry `patch` fields with `\n`, skips entries without `patch`, returns `undefined` when no usable patches remain, uses `Date.now()` for `time`, and builds `model_id` as `providerID/modelID` from `event.properties.info.model`. +- `get_or_create_encryption_key`: Public keyring-backed helper in `cli/src/services/db/encryption_key.rs` that retrieves or generates a 64-character hex encryption key from the OS credential store (macOS Keychain, Linux keyutils, Windows Credential Store); uses `keyring_core::Entry` with service name `"sce"` and the database name as username. Actively consumed by `EncryptedTursoDb::new()` via the shared adapter constructor. - `agent-trace plugin diff-trace hook handoff seam`: Internal helper `runDiffTraceHook` in `config/lib/agent-trace-plugin/opencode-sce-agent-trace-plugin.ts` that invokes `sce hooks diff-trace`, streams `{ sessionID, diff, time, model_id, tool_name, tool_version }` to STDIN JSON (`tool_name` fixed to `opencode`, `tool_version` session-derived when available), and surfaces deterministic invocation failures. - `agent-trace plugin secondary diff artifact ownership`: Current runtime contract where `buildTrace` no longer writes diff-trace artifacts or database rows directly; extracted diff payloads are forwarded to CLI `diff-trace` intake and the Rust hook runtime owns AgentTraceDb insertion plus collision-safe per-invocation artifact persistence. diff --git a/context/sce/auth-db.md b/context/sce/auth-db.md index 6f65a456..511161de 100644 --- a/context/sce/auth-db.md +++ b/context/sce/auth-db.md @@ -49,6 +49,6 @@ Current migration baseline: - `TokenStorageError` exposes `PathResolution` and `Database` variants; former `Io`, `Serialization`, `CorruptedTokenFile`, and `Permission` variants have been removed. - No JSON file I/O remains in `token_storage.rs`. - The `auth_credentials` row uses constant integer ID `1` for single-row token storage. -- Encryption is required: `SCE_DB_ENCRYPTION_KEY` must be set; failures surface as `TokenStorageError::Database`. +- Encryption is required: the encryption key is resolved from the OS credential store via `encryption_key::get_or_create_encryption_key()`; failures surface as `TokenStorageError::Database`. See also: [shared-turso-db.md](shared-turso-db.md), [../cli/default-path-catalog.md](../cli/default-path-catalog.md), [../context-map.md](../context-map.md), [../../context/plans/token-storage-db-migration.md](../../context/plans/token-storage-db-migration.md) diff --git a/context/sce/shared-turso-db.md b/context/sce/shared-turso-db.md index 4963112c..2044fa55 100644 --- a/context/sce/shared-turso-db.md +++ b/context/sce/shared-turso-db.md @@ -14,12 +14,25 @@ - parent-directory creation - synchronous `execute()`, `query()`, and row-mapping `query_map()` wrappers - generic embedded migration execution through `run_migrations()` with per-database `__sce_migrations` metadata -- `EncryptedTursoDb`: encrypted-adapter seam parallel to `TursoDb` with the same structural shape (connection, runtime bridge, and spec marker). `EncryptedTursoDb::new()` now resolves `SCE_DB_ENCRYPTION_KEY`, enforces non-empty key input, enables Turso experimental local encryption, applies strict `aegis256` cipher selection through `turso::EncryptionOpts` during local DB open/connect, and runs embedded migrations after connect. -- `EncryptedTursoDb` now also exposes synchronous `execute()` and `query()` wrappers plus generic `run_migrations()` with the same `__sce_migrations` metadata flow used by `TursoDb`. +- `EncryptedTursoDb`: encrypted-adapter seam parallel to `TursoDb` with the same structural shape (connection, runtime bridge, and spec marker). `EncryptedTursoDb::new()` resolves the encryption key from the OS credential store via `encryption_key::get_or_create_encryption_key()`, enables Turso experimental local encryption, applies strict `aegis256` cipher selection through `turso::EncryptionOpts` during local DB open/connect, and runs embedded migrations after connect. +- `EncryptedTursoDb` also exposes synchronous `execute()`, `query()`, and `query_map()` wrappers plus generic `run_migrations()` with the same `__sce_migrations` metadata flow used by `TursoDb`. - Shared lifecycle helpers: - `collect_db_path_health()` emits common parent/path health problems for DB-backed services. - `bootstrap_db_parent()` creates the resolved DB parent directory for repair/setup flows. +## Encryption key management + +`cli/src/services/db/encryption_key.rs` provides OS-credential-store-backed encryption key +get-or-create logic using `keyring-core` v1. Exposes +`get_or_create_encryption_key(db_path: &Path, db_name: &str) -> Result`. +This module is consumed by `EncryptedTursoDb::new()` to replace the previous +`SCE_DB_ENCRYPTION_KEY` environment variable approach. On first use for a given +database (file does not exist), a 32-byte random key is generated, hex-encoded to +64 characters, and persisted in the platform credential store (macOS Keychain, +Linux keyutils, Windows Credential Store). On subsequent use, the key is read +from the credential store. If the DB file exists but the key is missing (e.g. +Linux keyutils expiry), a clear remediation error is returned. + ## Current integration state The shared module is exported from `cli/src/services/mod.rs` and compile-checked. Current concrete wrappers: From 7efe3448dfec6b843c169c1e14f0de48ada4e935 Mon Sep 17 00:00:00 2001 From: stefanskoricdev Date: Wed, 27 May 2026 13:06:43 +0200 Subject: [PATCH 6/7] token_storage: Replace truncating casts with safe integer conversions Replace `as i64` and `as u64` truncating casts with `TryFrom` conversions that return explicit errors when values are out of range. Error messages are mapped to `TokenStorageError::Database` in `save_tokens` and to `anyhow::Error` in `load_tokens`. Removes the `#[allow(clippy::cast_possible_wrap)]` and `#[allow(clippy::cast_sign_loss)]` attributes that were suppressing warnings for these unsafe casts. Co-authored-by: SCE --- cli/src/services/token_storage.rs | 27 +++++--- context/plans/encrypted-auth-db.md | 103 ----------------------------- 2 files changed, 19 insertions(+), 111 deletions(-) delete mode 100644 context/plans/encrypted-auth-db.md diff --git a/cli/src/services/token_storage.rs b/cli/src/services/token_storage.rs index 29517703..c6b3a997 100644 --- a/cli/src/services/token_storage.rs +++ b/cli/src/services/token_storage.rs @@ -76,10 +76,16 @@ pub fn save_tokens(token: &TokenResponse) -> Result` adapter, embed ordered SQL migrations from a new auth migration directory, and expose lifecycle setup/doctor integration through `lifecycle.rs`. - -## Success criteria - -- `cli/src/services/auth_db/mod.rs` exists and follows the thin database-wrapper pattern used by `local_db` and `agent_trace_db`. -- `AuthDb` is a type alias for `EncryptedTursoDb`. -- `AuthDbSpec` implements `DbSpec` with a canonical auth DB path, diagnostic name, and ordered embedded migrations. -- A new auth migration directory exists under `cli/migrations/auth/`. -- The baseline migration creates a single `auth_tokens` table with: - - `id` required primary key, without `AUTOINCREMENT` - - `access_token` required - - `token_type` required - - `expires_in` required - - `refresh_token` required - - `scope` optional - - `stored_at_unix_seconds` required - - `email` required - - `created_at` required -- All `auth_tokens` columns are `NOT NULL` except `scope`. -- An index exists on `auth_tokens(email)`. -- `cli/src/services/auth_db/lifecycle.rs` follows the existing `ServiceLifecycle` pattern for path diagnosis, parent bootstrap, and setup initialization through `AuthDb::new()`. -- Required module/path/lifecycle wiring compiles without changing runtime auth-token read/write behavior yet. -- `nix flake check` passes. - -## Constraints and non-goals - -- **In scope**: new `auth_db` module files, new auth migration SQL files, canonical path resolver, service export, lifecycle provider registration, and context sync for the new current-state DB surface. -- **Out of scope**: replacing existing auth token storage, adding auth runtime reads/writes, token refresh behavior, token encryption/key management beyond using the existing `EncryptedTursoDb` adapter and `SCE_DB_ENCRYPTION_KEY`, cloud sync behavior, and any schema beyond the requested single `auth_tokens` table plus email index. -- Reuse existing dependencies and database infrastructure; do not add a new database library. -- Follow existing naming, migration embedding, lifecycle, and error-context conventions from `local_db` and `agent_trace_db`. -- Use forward-only embedded migrations consistent with current database modules. - -## Assumptions - -- The module path is `cli/src/services/auth_db/{mod.rs,lifecycle.rs}`. -- The table name is `auth_tokens`. -- The canonical database path should be added to the shared default-path catalog as `/sce/auth.db`, unless implementation review identifies an already-approved auth DB path in the PR. -- “Wiring” means the minimal non-runtime integration needed for the new module to compile and participate in setup/doctor lifecycle flows: `services/mod.rs`, `default_paths.rs`, and `services/lifecycle.rs` updates. It does not include changing auth command/token-storage behavior. - -## Task stack - -- [x] T01: `Add auth DB path and migration files` (status:done) - - Task ID: T01 - - Goal: Add the canonical auth DB path resolver and auth migration SQL files that define the requested encrypted database schema. - - Boundaries (in/out of scope): In — add `auth_db_path()` to `cli/src/services/default_paths.rs`; create `cli/migrations/auth/001_create_auth_tokens.sql`; create `cli/migrations/auth/002_create_auth_tokens_email_index.sql` or equivalent ordered split migrations. Out — no Rust `auth_db` module implementation yet, no auth runtime writes, no lifecycle provider registration. - - Done when: The path resolver returns `/sce/auth.db`; the baseline table migration creates `auth_tokens` with the requested columns, `id` primary key without `AUTOINCREMENT`, and all columns `NOT NULL` except `scope`; the email index migration creates an index on `email`; migration SQL is idempotent in the same style as Agent Trace DB migrations. - - Verification notes (commands or checks): Inspect SQL for schema compliance; run targeted Rust compile/format checks during implementation if path changes require compile validation. - - Completed: 2026-05-25 - - Files changed: `cli/src/services/default_paths.rs`, `cli/migrations/auth/001_create_auth_tokens.sql`, `cli/migrations/auth/002_create_auth_tokens_email_index.sql` - - Evidence: `nix develop -c sh -c 'cd cli && cargo check'` passed; `nix develop -c sh -c 'cd cli && cargo fmt -- --check'` passed; SQL inspection confirmed the required `auth_tokens` table columns/constraints and `idx_auth_tokens_email` idempotent index. - - Context sync classification: localized implementation change with default-path/current-state drift repaired in root and domain context during `sce-context-sync`. - -- [x] T02: `Create auth_db mod.rs using EncryptedTursoDb` (status:done) - - Task ID: T02 - - Goal: Create `cli/src/services/auth_db/mod.rs` as the encrypted database wrapper for auth token persistence. - - Boundaries (in/out of scope): In — define `AuthDbSpec`, `pub type AuthDb = EncryptedTursoDb`, embed ordered auth migrations with `include_str!`, implement `DbSpec` using `auth_db_path()`, and expose `pub mod lifecycle;`. Out — no domain-specific insert/query helpers unless needed for compile-only tests, no auth command integration, no token-storage replacement. - - Done when: `AuthDb::new()` would open the encrypted auth DB through `EncryptedTursoDb`, require `SCE_DB_ENCRYPTION_KEY` via the shared adapter, and run the auth migrations through `__sce_migrations`. - - Verification notes (commands or checks): `nix develop -c sh -c 'cd cli && cargo check'`; inspect that the module mirrors `local_db`/`agent_trace_db` naming and migration style. - - Completed: 2026-05-25 - - Files changed: `cli/src/services/auth_db/mod.rs`, `cli/src/services/mod.rs` - - Evidence: `nix develop -c sh -c 'cd cli && cargo check'` passed; `nix develop -c sh -c 'cd cli && cargo fmt -- --check'` passed; inspection confirmed `AuthDbSpec` uses `auth_db_path()`, migration IDs remain ordered, and `AuthDb` aliases `EncryptedTursoDb`. - - Notes: `pub mod lifecycle;` and `cli/src/services/auth_db/lifecycle.rs` remain deferred to T03 per readiness decision; `AuthDb` is marked `#[allow(dead_code)]` until lifecycle/runtime wiring consumes it. - - Context sync classification: important localized DB-service state change; updated auth DB and shared Turso/domain context plus root discoverability/current-state references. - -- [x] T03: `Add auth DB lifecycle integration` (status:done) - - Task ID: T03 - - Goal: Create `cli/src/services/auth_db/lifecycle.rs` and wire the provider into the shared lifecycle catalog. - - Boundaries (in/out of scope): In — implement `AuthDbLifecycle` with `diagnose`, `fix`, and `setup` following `LocalDbLifecycle` and `AgentTraceDbLifecycle`; use shared `collect_db_path_health()` and `bootstrap_db_parent()` helpers; add a `LifecycleProviderId::AuthDb`; register `AuthDbLifecycle` in deterministic provider order; export `pub mod auth_db;` from `services/mod.rs`. Out — no doctor renderer redesign, no setup output shape changes beyond existing lifecycle aggregation behavior. - - Done when: Setup initializes the encrypted auth DB through lifecycle aggregation, doctor/fix can diagnose/bootstrap the auth DB parent path, and provider order remains deterministic with auth DB placed alongside the other DB providers. - - Verification notes (commands or checks): `nix develop -c sh -c 'cd cli && cargo check'`; inspect `lifecycle_providers(include_hooks)` order and provider ID coverage. - - Completed: 2026-05-25 - - Files changed: `cli/src/services/auth_db/lifecycle.rs`, `cli/src/services/auth_db/mod.rs`, `cli/src/services/lifecycle.rs` - - Evidence: `cargo check` passed; `cargo fmt -- --check` passed; provider order confirmed: config → `local_db` → `auth_db` → `agent_trace_db` → hooks; `LifecycleProviderId::AuthDb` added to enum. - - Context sync classification: important change — new lifecycle provider and enum variant affect cross-cutting service lifecycle behavior. - -- [ ] T04: `Add focused tests for auth DB schema and lifecycle wiring` (status:todo) - - Task ID: T04 - - Goal: Add narrow tests that prove the new auth DB migration list and lifecycle wiring stay deterministic. - - Boundaries (in/out of scope): In — module-level tests or existing lifecycle tests verifying migration IDs/order, `auth_tokens` schema/index SQL presence, path resolver behavior if covered by existing default-path tests, and lifecycle provider inclusion/order. Out — integration tests that require real auth login, WorkOS calls, or production token data. - - Done when: Tests fail if the auth DB provider is not registered, migration ordering changes unexpectedly, or the required table/index SQL is missing required constraints. - - Verification notes (commands or checks): Prefer targeted test commands during implementation, then rely on `nix flake check` for final coverage. - -- [ ] T05: `Sync context for encrypted auth DB current state` (status:todo) - - Task ID: T05 - - Goal: Update durable context so future sessions know the auth DB module, encrypted adapter usage, migration schema, path, and lifecycle registration exist. - - Boundaries (in/out of scope): In — update focused context files such as `context/sce/shared-turso-db.md`, a new or existing auth DB context file, `context/context-map.md`, and glossary/overview entries only if the change is important at those scopes. Out — completed-work narration in durable context, unrelated Agent Trace or local DB rewrites. - - Done when: Context describes the resulting current state rather than task history, and no stale statements conflict with the new auth DB surface. - - Verification notes (commands or checks): Review context files against code truth after implementation; ensure `context/plans/encrypted-auth-db.md` remains the active execution artifact. - -- [ ] T06: `Final validation and cleanup` (status:todo) - - Task ID: T06 - - Goal: Run the full repo validation pass, remove temporary scaffolding, and confirm all plan success criteria are met. - - Boundaries (in/out of scope): In — full test/lint/format validation, generated-output parity, temporary-file cleanup, and success-criteria evidence capture in this plan. Out — new auth DB runtime features or schema expansion. - - Done when: `nix flake check` passes, `nix run .#pkl-check-generated` passes, no task-owned temporary scaffolding remains, and this plan records validation evidence for every success criterion. - - Verification notes (commands or checks): `nix run .#pkl-check-generated`; `nix flake check`. - -## Open questions - -None for planning. The implementation should call out before coding if the current PR already introduced a canonical auth DB path that differs from the `/sce/auth.db` assumption. From 310c824839d5a14b58a592ff2833e181b00facdb Mon Sep 17 00:00:00 2001 From: stefanskoricdev Date: Wed, 27 May 2026 18:25:18 +0200 Subject: [PATCH 7/7] db: Switch Linux encryption-key store to zbus Secret Service Replace Linux keyring backend for encryption key management from linux-keyutils-keyring-store to zbus-secret-service-keyring-store. Update target dependencies and lockfile to match the new Secret Service backend, and align keyring error/help text with Secret Service behavior. Co-authored-by: SCE --- cli/Cargo.lock | 525 ++++++++++++++++++++++++-- cli/Cargo.toml | 4 +- cli/src/services/db/encryption_key.rs | 17 +- context/glossary.md | 2 +- context/sce/shared-turso-db.md | 7 +- 5 files changed, 515 insertions(+), 40 deletions(-) diff --git a/cli/Cargo.lock b/cli/Cargo.lock index ca4e6ad4..6d89a53d 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -183,6 +183,126 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfdc70193dadb9d7287fa4b633f15f90c876915b31f6af17da307fc59c9859a8" +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.1.4", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 1.1.4", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.1.4", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.89" @@ -312,6 +432,15 @@ dependencies = [ "wyz", ] +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "block-buffer" version = "0.12.0" @@ -321,6 +450,28 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "bon" version = "3.9.1" @@ -405,6 +556,15 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + [[package]] name = "cc" version = "1.2.62" @@ -833,13 +993,24 @@ dependencies = [ "syn", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", + "subtle", +] + [[package]] name = "digest" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "block-buffer", + "block-buffer 0.12.0", "const-oid", "crypto-common 0.2.1", "ctutils", @@ -919,6 +1090,33 @@ dependencies = [ "serde", ] +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "env_filter" version = "1.0.1" @@ -965,6 +1163,27 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "fallible-iterator" version = "0.3.0" @@ -1103,6 +1322,19 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.32" @@ -1310,13 +1542,31 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac 0.12.1", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "hmac" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" dependencies = [ - "digest", + "digest 0.11.3", ] [[package]] @@ -1598,6 +1848,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ + "block-padding", "generic-array", ] @@ -1890,26 +2141,6 @@ dependencies = [ "syn", ] -[[package]] -name = "linux-keyutils" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83270a18e9f90d0707c41e9f35efada77b64c0e6f3f1810e71c8368a864d5590" -dependencies = [ - "bitflags", - "libc", -] - -[[package]] -name = "linux-keyutils-keyring-store" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39fbed79f71dc21eb21d3d07c0e908a3c58ff9a1fdbf5cf44230fb3deb6d994b" -dependencies = [ - "keyring-core", - "linux-keyutils", -] - [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -2241,6 +2472,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "outref" version = "0.5.2" @@ -2281,6 +2522,12 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -2322,6 +2569,17 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkg-config" version = "0.3.33" @@ -2388,6 +2646,15 @@ dependencies = [ "syn", ] +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -2935,6 +3202,25 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "secret-service" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a62d7f86047af0077255a29494136b9aaaf697c76ff70b8e49cded4e2623c14" +dependencies = [ + "aes", + "cbc", + "futures-util", + "generic-array", + "getrandom 0.2.17", + "hkdf", + "num", + "once_cell", + "serde", + "sha2 0.10.9", + "zbus", +] + [[package]] name = "security-framework" version = "3.7.0" @@ -3007,6 +3293,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3025,6 +3322,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + [[package]] name = "sha2" version = "0.11.0" @@ -3033,7 +3341,7 @@ checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", "cpufeatures 0.3.0", - "digest", + "digest 0.11.3", ] [[package]] @@ -3055,23 +3363,23 @@ dependencies = [ "clap", "clap_complete", "dirs", - "hmac", + "hmac 0.13.0", "inquire", "jsonschema", "keyring-core", - "linux-keyutils-keyring-store", "murmur3", "owo-colors 4.3.0", "rand 0.8.6", "reqwest", "serde", "serde_json", - "sha2", + "sha2 0.11.0", "tokio", "tracing", "turso", "uuid", "windows-native-keyring-store", + "zbus-secret-service-keyring-store", ] [[package]] @@ -3557,7 +3865,9 @@ dependencies = [ "libc", "mio", "pin-project-lite", + "signal-hook-registry", "socket2", + "tracing", "windows-sys 0.61.2", ] @@ -3584,6 +3894,36 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] + [[package]] name = "tower" version = "0.5.3" @@ -3940,6 +4280,17 @@ dependencies = [ "syn", ] +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + [[package]] name = "uncased" version = "0.9.10" @@ -4513,6 +4864,15 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.51.0" @@ -4645,6 +5005,79 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zbus" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3bcbf15c8708d7fc1be0c993622e0a5cbd5e8b52bfa40afa4c3e0cd8d724ac1" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix 1.1.4", + "serde", + "serde_repr", + "tokio", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus-secret-service-keyring-store" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ccede190ba363386a24e8021c7f3848393976609ec9f5d1f8c6c09ef37075b4" +dependencies = [ + "keyring-core", + "secret-service", + "zbus", +] + +[[package]] +name = "zbus_macros" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51fa5406ad9175a8c825a931f8cf347116b531b3634fcb0b627c290f1f2516ff" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" +dependencies = [ + "serde", + "winnow", + "zvariant", +] + [[package]] name = "zerocopy" version = "0.8.48" @@ -4758,3 +5191,43 @@ dependencies = [ "cc", "pkg-config", ] + +[[package]] +name = "zvariant" +version = "5.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c1567a6ec68df868cbbfde844cfc6d81649fe5109a62b116b19fabd53e618ee" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d5b780599bbde114e39d9a0799577fad1ced5105d38515745f7b3099d8ceda" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d464f5733ffa07a3164d656f18533caace9d0638596721355d73256a410d691" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn", + "winnow", +] diff --git a/cli/Cargo.toml b/cli/Cargo.toml index ae1f3134..43e30545 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -49,8 +49,8 @@ tracing = "0.1" uuid = { version = "1", features = ["v4", "v7"] } murmur3 = "0.5.2" -[target.'cfg(target_os = "linux")'.dependencies] -linux-keyutils-keyring-store = "1" +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies] +zbus-secret-service-keyring-store = { version = "1.0.0", features = ["rt-tokio-crypto-rust"] } [target.'cfg(target_os = "macos")'.dependencies] apple-native-keyring-store = { version = "1", features = ["keychain"] } diff --git a/cli/src/services/db/encryption_key.rs b/cli/src/services/db/encryption_key.rs index 73a8e374..93a768a3 100644 --- a/cli/src/services/db/encryption_key.rs +++ b/cli/src/services/db/encryption_key.rs @@ -2,7 +2,7 @@ //! //! Provides a single entry point to get-or-create a 64-character hex //! encryption key stored in the platform-native credential store -//! (macOS Keychain, Linux keyutils, Windows Credential Store). +//! (macOS Keychain, Linux Secret Service via zbus, Windows Credential Store). //! //! On first use for a given database name (when the database file does //! not yet exist), a random 32-byte key is generated, hex-encoded, @@ -32,8 +32,8 @@ fn ensure_default_store() -> Result<()> { #[cfg(target_os = "linux")] { keyring_core::set_default_store( - linux_keyutils_keyring_store::Store::new() - .context("failed to create Linux keyutils keyring store")?, + zbus_secret_service_keyring_store::Store::new() + .context("failed to create Linux Secret Service (zbus) keyring store")?, ); } #[cfg(target_os = "macos")] @@ -83,8 +83,8 @@ fn ensure_default_store() -> Result<()> { /// # Errors /// - Returns an error if the credential store cannot be initialised on /// the current platform. -/// - Returns an error if the database file exists but the keyring entry -/// is missing (e.g. keyring was cleared or has expired on Linux). + /// - Returns an error if the database file exists but the keyring entry + /// is missing (e.g. keyring was cleared on Linux). /// - Returns an error if key generation or credential store I/O fails. pub fn get_or_create_encryption_key(db_path: &Path, db_name: &str) -> Result { ensure_default_store()?; @@ -118,13 +118,14 @@ pub fn get_or_create_encryption_key(db_path: &Path, db_name: &str) -> Result` with top-level metadata fields plus one `Conversation` per `post_commit_patch` hunk; consumed by the active post-commit hook flow, with no standalone `sce agent-trace` command surface. - `agent-trace plugin diff extraction seam`: Exported helper `extractDiffTracePayload` in `config/lib/agent-trace-plugin/opencode-sce-agent-trace-plugin.ts` that reads `input.event` and returns `{ sessionID, diff, time, model_id }` only when the event is `message.updated` with `properties.info.role === "user"`; extracts `sessionID` from `info.sessionID` (falling back to `"unknown"` when missing/empty), joins object-entry `patch` fields from `info.summary?.diffs[]` with `\n` while preserving empty patch strings for Rust-side validation, uses `Date.now()` for `time`, and builds `model_id` as `info.model.providerID/info.model.modelID`; returns `undefined` for non-`message.updated` events, non-user messages, messages without a non-empty `summary.diffs` array, or diffs arrays without object entries. - `agent-trace plugin diff extraction seam`: Helper `extractDiffTracePayload` in `config/lib/agent-trace-plugin/opencode-sce-agent-trace-plugin.ts` that accepts a typed `message.updated` event and returns `{ sessionID, diff, time, model_id }` only for user-role messages with non-empty `summary?.diffs`; it joins present object-entry `patch` fields with `\n`, skips entries without `patch`, returns `undefined` when no usable patches remain, uses `Date.now()` for `time`, and builds `model_id` as `providerID/modelID` from `event.properties.info.model`. -- `get_or_create_encryption_key`: Public keyring-backed helper in `cli/src/services/db/encryption_key.rs` that retrieves or generates a 64-character hex encryption key from the OS credential store (macOS Keychain, Linux keyutils, Windows Credential Store); uses `keyring_core::Entry` with service name `"sce"` and the database name as username. Actively consumed by `EncryptedTursoDb::new()` via the shared adapter constructor. +- `get_or_create_encryption_key`: Public keyring-backed helper in `cli/src/services/db/encryption_key.rs` that retrieves or generates a 64-character hex encryption key from the OS credential store (macOS Keychain, Linux Secret Service via zbus, Windows Credential Store); uses `keyring_core::Entry` with service name `"sce"` and the database name as username. Actively consumed by `EncryptedTursoDb::new()` via the shared adapter constructor. - `agent-trace plugin diff-trace hook handoff seam`: Internal helper `runDiffTraceHook` in `config/lib/agent-trace-plugin/opencode-sce-agent-trace-plugin.ts` that invokes `sce hooks diff-trace`, streams `{ sessionID, diff, time, model_id, tool_name, tool_version }` to STDIN JSON (`tool_name` fixed to `opencode`, `tool_version` session-derived when available), and surfaces deterministic invocation failures. - `agent-trace plugin secondary diff artifact ownership`: Current runtime contract where `buildTrace` no longer writes diff-trace artifacts or database rows directly; extracted diff payloads are forwarded to CLI `diff-trace` intake and the Rust hook runtime owns AgentTraceDb insertion plus collision-safe per-invocation artifact persistence. diff --git a/context/sce/shared-turso-db.md b/context/sce/shared-turso-db.md index 2044fa55..2272487b 100644 --- a/context/sce/shared-turso-db.md +++ b/context/sce/shared-turso-db.md @@ -29,9 +29,10 @@ This module is consumed by `EncryptedTursoDb::new()` to replace the previous `SCE_DB_ENCRYPTION_KEY` environment variable approach. On first use for a given database (file does not exist), a 32-byte random key is generated, hex-encoded to 64 characters, and persisted in the platform credential store (macOS Keychain, -Linux keyutils, Windows Credential Store). On subsequent use, the key is read -from the credential store. If the DB file exists but the key is missing (e.g. -Linux keyutils expiry), a clear remediation error is returned. +Linux Secret Service via zbus, Windows Credential Store). On subsequent use, +the key is read from the credential store. If the DB file exists but the key +is missing (for example, keyring entry removed), a clear remediation error is +returned. ## Current integration state