mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-26 23:50:17 +00:00
* refactor: encapsulate leaked abstractions from main.rs and app.rs into owning modules Move module-specific initialization logic out of main.rs (1222→665 lines, -46%) and app.rs (944→780 lines, -17%) into their respective owning modules as public factory functions. This enforces separation of concerns so that adding a new DB backend, MCP transport, or channel doesn't require editing main.rs/app.rs. Key changes: - Tracing init functions → src/tracing_fmt.rs - DB connection factory (connect_with_handles + DatabaseHandles) → src/db/mod.rs - Secrets store factory (create_secrets_store) → src/secrets/mod.rs - MCP transport dispatch factory (create_client_from_config) → src/tools/mcp/factory.rs - Orchestrator setup (setup_orchestrator + OrchestratorSetup) → src/orchestrator/mod.rs - WASM channel setup (setup_wasm_channels) → src/channels/wasm/setup.rs - Worker entry points (run_worker, run_claude_bridge) → src/worker/mod.rs - Shared CLI secrets init (init_secrets_store) → src/cli/mod.rs - Tunnel startup (start_managed_tunnel) → src/tunnel/mod.rs - Onboard check (check_onboard_needed) → src/setup/mod.rs - ExtensionManager unified MCP: uses create_client_from_config via McpProcessManager, enabling stdio/Unix transports for hot-activated MCP servers - Deduplicated ~130 lines of secrets store init across cli/mcp.rs and cli/tool.rs - CLAUDE.md updated with module-owned initialization guideline [skip-regression-check] Co-Authored-By: Claude Opus 4.6 <[email protected]> * refactor: address review feedback — deduplicate db factory, extract channel helper - connect_from_config() now delegates to connect_with_handles() to eliminate duplicated backend-matching logic (Copilot review feedback) - Extract register_channel() helper from setup_wasm_channels() loop body to improve readability (Gemini review feedback) [skip-regression-check] Co-Authored-By: Claude Opus 4.6 <[email protected]> * style: fix rustfmt line wrapping in setup_wasm_channels Co-Authored-By: Claude Opus 4.6 <[email protected]> * test: add integration test for module-owned initialization factories Exercises the full factory chain end-to-end to verify nothing was lost when initialization logic was moved from main.rs/app.rs into owning modules: - connect_with_handles returns Database + populated backend handles - connect_from_config delegates correctly (produces working Database) - secrets::create_secrets_store builds working store from DatabaseHandles - db::create_secrets_store standalone factory round-trips secrets - Both secrets factories produce compatible stores (cross-read works) - ExtensionManager constructs with McpProcessManager and is functional - DatabaseHandles default is empty All tests run without external services using libsql in-memory/tempfile. Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: wire cli/mcp.rs and cli/tool.rs to shared init_secrets_store() Both files had inline implementations identical to cli::init_secrets_store(). Replace with delegation to complete the claimed deduplication. [skip-regression-check] Co-Authored-By: Claude Opus 4.6 <[email protected]> * style: fix rustfmt line wrapping in integration test Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix(review): remove unused Config import and deduplicate Error Handling section - Remove `#[allow(unused_imports)]` and unused `use crate::config::Config` from cli/tool.rs (no longer needed after delegating to shared `cli::init_secrets_store()`) - Remove duplicate Error Handling subsection from CLAUDE.md Key Patterns (all four bullets already exist in Code Style section and review-discipline.md) Addresses Copilot review comments. [skip-regression-check] Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix(review): address remaining Copilot review comments - secrets/mod.rs: clarify docstring that None is a normal no-db condition - app.rs: add comment explaining the empty_handles fallback path - orchestrator/mod.rs: combine duplicated sandbox condition into single block - setup/mod.rs: document env var reads and thread-safety caveat [skip-regression-check] Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]> Co-authored-by: Henry Park <[email protected]>
112 lines
4.8 KiB
Rust
112 lines
4.8 KiB
Rust
//! Secrets management for secure credential storage and injection.
|
|
//!
|
|
//! This module provides:
|
|
//! - AES-256-GCM encrypted secret storage
|
|
//! - Per-secret key derivation (HKDF-SHA256)
|
|
//! - PostgreSQL persistence
|
|
//! - OS keychain integration for master key
|
|
//! - Access control for WASM tools
|
|
//!
|
|
//! # Security Model
|
|
//!
|
|
//! ```text
|
|
//! ┌─────────────────────────────────────────────────────────────────────────────┐
|
|
//! │ Secret Lifecycle │
|
|
//! │ │
|
|
//! │ User stores secret ──► Encrypt with AES-256-GCM ──► Store in PostgreSQL │
|
|
//! │ (per-secret key via HKDF) │
|
|
//! │ │
|
|
//! │ WASM requests HTTP ──► Host checks allowlist ──► Decrypt secret ──► │
|
|
//! │ & allowed_secrets (in memory only) │
|
|
//! │ │ │
|
|
//! │ ▼ │
|
|
//! │ Inject into request ──► Execute HTTP call │
|
|
//! │ (WASM never sees value) │
|
|
//! │ │ │
|
|
//! │ ▼ │
|
|
//! │ Leak detector scans ──► Return response to WASM │
|
|
//! │ response for secrets │
|
|
//! └─────────────────────────────────────────────────────────────────────────────┘
|
|
//! ```
|
|
//!
|
|
//! # Master Key Storage
|
|
//!
|
|
//! The master key for encrypting secrets can come from:
|
|
//! - **OS Keychain** (recommended for local installs): Auto-generated and stored securely
|
|
//! - **Environment variable** (for CI/Docker): Set `SECRETS_MASTER_KEY`
|
|
//!
|
|
//! # Example
|
|
//!
|
|
//! ```ignore
|
|
//! use ironclaw::secrets::{SecretsStore, PostgresSecretsStore, SecretsCrypto, CreateSecretParams};
|
|
//! use secrecy::SecretString;
|
|
//!
|
|
//! // Initialize crypto with master key from environment
|
|
//! let master_key = SecretString::from(std::env::var("SECRETS_MASTER_KEY")?);
|
|
//! let crypto = Arc::new(SecretsCrypto::new(master_key)?);
|
|
//!
|
|
//! // Create store
|
|
//! let store = PostgresSecretsStore::new(pool, crypto);
|
|
//!
|
|
//! // Store a secret
|
|
//! store.create("user_123", CreateSecretParams::new("openai_key", "sk-...")).await?;
|
|
//!
|
|
//! // Check if secret exists (WASM can call this)
|
|
//! let exists = store.exists("user_123", "openai_key").await?;
|
|
//!
|
|
//! // Decrypt for injection (host boundary only)
|
|
//! let decrypted = store.get_decrypted("user_123", "openai_key").await?;
|
|
//! ```
|
|
|
|
mod crypto;
|
|
pub mod keychain;
|
|
mod store;
|
|
mod types;
|
|
|
|
pub use crypto::SecretsCrypto;
|
|
#[cfg(feature = "libsql")]
|
|
pub use store::LibSqlSecretsStore;
|
|
#[cfg(feature = "postgres")]
|
|
pub use store::PostgresSecretsStore;
|
|
pub use store::SecretsStore;
|
|
pub use types::{
|
|
CreateSecretParams, CredentialLocation, CredentialMapping, DecryptedSecret, Secret,
|
|
SecretError, SecretRef,
|
|
};
|
|
|
|
pub use store::in_memory::InMemorySecretsStore;
|
|
|
|
/// Create a secrets store from a master key and database handles.
|
|
///
|
|
/// Returns `None` if no matching backend handle is available (e.g. when
|
|
/// running without a database). This is a normal condition in no-db mode,
|
|
/// not an error — callers should treat `None` as "secrets unavailable".
|
|
pub fn create_secrets_store(
|
|
crypto: std::sync::Arc<SecretsCrypto>,
|
|
handles: &crate::db::DatabaseHandles,
|
|
) -> Option<std::sync::Arc<dyn SecretsStore + Send + Sync>> {
|
|
let store: Option<std::sync::Arc<dyn SecretsStore + Send + Sync>> = None;
|
|
|
|
#[cfg(feature = "libsql")]
|
|
let store = store.or_else(|| {
|
|
handles.libsql_db.as_ref().map(|db| {
|
|
std::sync::Arc::new(LibSqlSecretsStore::new(
|
|
std::sync::Arc::clone(db),
|
|
std::sync::Arc::clone(&crypto),
|
|
)) as std::sync::Arc<dyn SecretsStore + Send + Sync>
|
|
})
|
|
});
|
|
|
|
#[cfg(feature = "postgres")]
|
|
let store = store.or_else(|| {
|
|
handles.pg_pool.as_ref().map(|pool| {
|
|
std::sync::Arc::new(PostgresSecretsStore::new(
|
|
pool.clone(),
|
|
std::sync::Arc::clone(&crypto),
|
|
)) as std::sync::Arc<dyn SecretsStore + Send + Sync>
|
|
})
|
|
});
|
|
|
|
store
|
|
}
|