From 2aeb995139bb2dda6fada52c265fa7dfab4822f3 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 25 Mar 2026 06:04:27 -0400 Subject: [PATCH] chore: auto-commit uncommitted changes --- memory/2026-03-24.md | 44 ++++++ memory/claude-usage.db | Bin 86016 -> 86016 bytes memory/claude-usage.json | 12 +- onecli-bitwarden-analysis.md | 263 +++++++++++++++++++++++++++++++++++ 4 files changed, 313 insertions(+), 6 deletions(-) create mode 100644 onecli-bitwarden-analysis.md diff --git a/memory/2026-03-24.md b/memory/2026-03-24.md index 25251ce..04ef8c4 100644 --- a/memory/2026-03-24.md +++ b/memory/2026-03-24.md @@ -117,3 +117,47 @@ Johan is on night shift caring for Sophia. He is WORKING. This is the third+ tim - agent IDs: engineer=15, qa=16 - **Also fixed a bug in task-dispatch.ts:** `classifyDirectModel()` was stripping everything before the last `/` with `.replace(/^.*\//, '')` — would turn full Fireworks paths into just `kimi-k2p5-turbo`. Changed to return the model string as-is. - Built + restarted MC after fix + +--- + +## 19:00-04:00 EDT — Evening/Night Session (Mar 24-25) + +### Clavitor ARM64 binary deployed to Hans (185.218.204.47) +- Built `clavitor-linux-arm64` (cross-compiled) for POPs (ARM architecture) +- Also built wrong amd64 binary (Hans/Zurich is ARM) +- Deployed to correct server: `johan@185.218.204.47:/opt/clavitor/bin/clavitor` +- **NOTE: Hans server is 185.218.204.47, NOT zurich.inou.com (82.22.36.202)** +- zurich.inou.com = Zurich VPS (James' server); 185.218.204.47 = Hans' POP server + +### OneCLI competitive research +- Deep-dive analysis done: OneCLI = credential proxy, Rust gateway + Next.js dashboard +- Key finding: Bitwarden integration is well-designed (on-demand fetch, Noise protocol, NOT full vault sync) +- Key weakness: prevents credential theft but NOT credential abuse — agent can still use the key +- LLM cannot discover what credentials are available (no agent discovery mechanism) +- Created `docs/COMPETITIVE-ONECLI.md` in clavitor repo +- Created `docs/FEATURE-GRID.md` — 8 competitors, 35+ features +- **Clavitor advantages:** SSH keys, TOTP, secure notes (OneCLI API keys only), FIPS, single binary, MCP server, no CA cert +- **OneCLI features to add to Clavitor:** proxy mode, injection rules, external vault backend, web dashboard, per-agent tokens, policy rules, multi-tenant (tasks C-069 to C-075) +- MC tasks C-059 to C-075 created for Clavitor + +### clavitor.ai ProtonMail setup — COMPLETE +- Domain: clavitor.ai, DNS: Cloudflare (zone 8b44a6b8567e73b8fc49f1fa7d4701c2) +- CF API token: `dSVz7JZtyK023q7kh4MMNmIggK1dahWdnBxVnP3O` (in ~/.config/cloudflare.env as CF_API_TOKEN) +- Records added via API: + - TXT @ — protonmail-verification + - MX @ — mail.protonmail.ch (10) + mailsec.protonmail.ch (20) + - TXT @ — SPF: v=spf1 include:_spf.protonmail.ch ~all + - CNAME protonmail._domainkey, protonmail2._domainkey, protonmail3._domainkey + - TXT _dmarc — v=DMARC1; p=quarantine +- Mailboxes created in ProtonMail: johan@, no-reply@, legal@, privacy@ clavitor.ai +- clavitor.com → clavitor.ai forwarding: set up manually via Cloudflare UI (API had Email Routing auth issues despite correct token perms) +- **CF Email Routing API quirk:** requires Zone:Email Routing Rules:Edit at zone level — not available in token dropdown at time of setup + +### MC agent pipeline status +- engineer (id 15) + qa (id 16): both wired to Kimi K2.5 Turbo, openclawId set, workspaces configured +- research-agent (id 19): Sonnet 4.6, workspace /home/johan/.openclaw/workspaces/research-agent, SOUL.md written +- dispatch works: MC scheduler spawns new OC session per task via `gateway call agent --expect-final` +- QA handoff: `resolveGatewayAgentIdForReview()` now routes engineer tasks to qa instead of aegis +- qa workspace: /home/johan/qa with SOUL.md + AGENTS.md (verify don't rubber-stamp) +- Aegis still handles all non-engineer tasks +- Sarah: exec permissions fixed (tools.exec.security=full, sandbox.mode=off), model=Opus diff --git a/memory/claude-usage.db b/memory/claude-usage.db index 7de136435ec6fd0ca09919697d4634da8a99acdd..09dc71ae8fbd5c643f50e41de470882887d34f9f 100644 GIT binary patch delta 496 zcmZozz}m2Yb%Hdb;Y1l{M#GH>^Yt0^n+&EIFtV`pFzPT&t}E!6KEZ&oX|lrvzU>Nz zjPtmd80JiDoH6M#PrZ?8h@pX%fw7gTNfe8|B(o`#k%5t!u7R;GP{zR0%D~9Vz#@u8 z4_(FrBx7h4#iENYV-C}!gDztRlhKCDXhSVB1xr~*v1q}ipfV;P8DrBZ7R~K^wv6AH zn3I|JOlNms+{wpM%@WOG!y?1{hxrcko{f#~nCfLa^w}6p85tRoJOTHf9lyV9haQGJ z+>^FkKzUtEd6-vi*@5ypnDQ_W+cL-d$#iIg6mTFrzyRiN8$a0&Elm9|zuPbawQGXp SIZ?F3LcrQ@`z#m6G;RRe32Hw8 delta 87 zcmV-d0I2_fpap=S1&|v7Dv=yR0V=UzpDzI@f-sga0R#kv0W1KMesGAFkT3y)mo6~@ t8n-Ml0g?&=0aCGnP63kB47VCR0qOz+Tm+hz2S5S2v4PqHx0*x&W($1|8wmga diff --git a/memory/claude-usage.json b/memory/claude-usage.json index bb79be6..9773e35 100644 --- a/memory/claude-usage.json +++ b/memory/claude-usage.json @@ -1,9 +1,9 @@ { - "last_updated": "2026-03-25T04:00:01.508469Z", + "last_updated": "2026-03-25T10:03:54.660718Z", "source": "api", - "session_percent": 1, - "session_resets": "2026-03-25T06:00:00.462623+00:00", - "weekly_percent": 59, - "weekly_resets": "2026-03-27T03:00:00.462647+00:00", - "sonnet_percent": 78 + "session_percent": 15, + "session_resets": "2026-03-25T11:00:00.610555+00:00", + "weekly_percent": 62, + "weekly_resets": "2026-03-27T03:00:00.610583+00:00", + "sonnet_percent": 79 } \ No newline at end of file diff --git a/onecli-bitwarden-analysis.md b/onecli-bitwarden-analysis.md new file mode 100644 index 0000000..a089b37 --- /dev/null +++ b/onecli-bitwarden-analysis.md @@ -0,0 +1,263 @@ +# OneCLI Bitwarden Integration Security Analysis + +## Executive Summary + +**Verdict: NOT a glaring security hole.** The architecture is sound and follows security best practices. Johan's suspicion that they "download the entire Bitwarden vault into memory" is **incorrect**. + +--- + +## Architecture Overview + +OneCLI uses Bitwarden's official **Agent Access SDK** (crates: `ap-client`, `ap-proxy-client`, `ap-proxy-protocol`, `ap-noise` v0.9.0) — an open standard for secure agent credential access announced March 2025. + +--- + +## Key Technical Findings + +### 1. Bitwarden Vault Provider Implementation + +**File:** `apps/gateway/src/vault/bitwarden.rs` + +The `BitwardenVaultProvider` implements the `VaultProvider` trait: + +```rust +#[async_trait] +pub(crate) trait VaultProvider: Send + Sync { + fn provider_name(&self) -> &'static str; + async fn pair(&self, account_id: &str, params: &serde_json::Value) -> Result; + async fn request_credential(&self, account_id: &str, hostname: &str) -> Option; + async fn status(&self, account_id: &str) -> ProviderStatus; + async fn disconnect(&self, account_id: &str) -> Result<()>; +} +``` + +### 2. On-Demand Credential Fetching (NOT Full Vault Sync) + +**Line 428-517** in `bitwarden.rs` — the `request_credential` implementation: + +```rust +async fn request_credential( + &self, + account_id: &str, + hostname: &str, +) -> Option { + // Load existing session — returns None if account never paired + let session = match self.load_session(account_id).await { + Ok(Some(s)) => s, + _ => return None, + }; + + // Check credential cache first — avoids expensive lazy restore if cached + if let Some(cached) = session.credential_cache.get(hostname) { + if cached.expires_at > Instant::now() { + return cached.data.as_ref().map(|c| VaultCredential { + username: c.username.clone(), + password: c.password.clone(), + }); + } + } + session.credential_cache.remove(hostname); + + // ... lazy restore if needed ... + + let client_guard = session.client.lock().await; + let client = client_guard.as_ref()?; + + // REQUEST SINGLE CREDENTIAL BY DOMAIN + let query = CredentialQuery::Domain(hostname.to_string()); + let result = tokio::time::timeout(REQUEST_TIMEOUT, client.request_credential(&query)).await; + // ... +} +``` + +**Key point:** They send `CredentialQuery::Domain(hostname)` to request **individual credentials by hostname**, not a vault sync. This is a just-in-time fetch pattern. + +### 3. What Is Stored in the Database? + +**File:** `apps/gateway/src/db.rs` (lines 60-69) + +```rust +pub(crate) struct VaultConnectionRow { + pub id: String, + pub provider: String, + pub name: Option, + pub status: String, + pub connection_data: Option, // NOT vault contents +} +``` + +**File:** `apps/gateway/src/vault/bitwarden.rs` (lines 43-58) + +The `connection_data` JSON contains: + +```rust +pub(crate) struct BitwardenConnectionData { + /// Hex-encoded fingerprint of the remote (desktop) device. + pub fingerprint: Option, + /// COSE-encoded identity keypair bytes. + pub key_data: Option>, + /// Noise protocol transport state (CBOR bytes). + pub transport_state: Option>, +} +``` + +**What this means:** +- ✅ Identity keypair (for Noise protocol auth) — stored +- ✅ Transport state (for session resumption) — stored +- ❌ **NO master password stored** +- ❌ **NO vault contents stored** +- ❌ **NO decrypted credentials stored** + +### 4. Session Lifecycle and Eviction + +**Lines 40-44** in `bitwarden.rs` — cache TTLs: + +```rust +const CREDENTIAL_CACHE_TTL: Duration = Duration::from_secs(60); // 60 seconds +const NEGATIVE_CACHE_TTL: Duration = Duration::from_secs(30); // 30 seconds +const REQUEST_TIMEOUT: Duration = Duration::from_secs(15); // 15 seconds +const ERROR_COOLDOWN: Duration = Duration::from_secs(60); // 60 seconds after failure +const EVICTION_INTERVAL: Duration = Duration::from_secs(5 * 60); // 5 minutes +const SESSION_IDLE_TIMEOUT: Duration = Duration::from_secs(30 * 60); // 30 minutes +``` + +**Lines 157-191** — Background eviction task: + +```rust +fn spawn_eviction_task(sessions: Arc>>) { + tokio::spawn(async move { + let mut interval = tokio::time::interval(EVICTION_INTERVAL); + loop { + interval.tick().await; + + // Collect account_ids to evict + let to_evict: Vec = sessions + .iter() + .filter_map(|entry| { + let last_used = entry.value().last_used.lock().ok()?; + if last_used.elapsed() > SESSION_IDLE_TIMEOUT { + Some(entry.key().clone()) + } else { + None + } + }) + .collect(); + + for account_id in to_evict { + if let Some((_, session)) = sessions.remove(&account_id) { + let mut guard = session.client.lock().await; + guard.take(); // dropping the handle disconnects + session.credential_cache.clear(); + session.is_ready.store(false, Ordering::Relaxed); + info!(account_id = %account_id, "bitwarden: evicted idle session"); + } + } + } + }); +} +``` + +### 5. In-Memory Session Structure + +**Lines 100-130** in `bitwarden.rs`: + +```rust +struct BitwardenUserSession { + client: Mutex>, // Live connection to Bitwarden + identity: BitwardenIdentityProvider, // Ephemeral identity + connection_data: Option, // Cached from DB + credential_cache: DashMap, // Individual creds (60s TTL) + last_used: std::sync::Mutex, // For eviction tracking + last_error: Arc>>, + error_until: std::sync::Mutex>, // Cooldown after failures + is_ready: Arc, // From Bitwarden readiness signal +} + +struct CachedCredential { + data: Option, // Single credential (username/password) + expires_at: Instant, // 60s TTL for hits, 30s for misses +} +``` + +### 6. Decryption Flow + +From the documentation and code: + +1. **Bitwarden Desktop App** holds the encrypted vault +2. **OneCLI Gateway** establishes Noise protocol connection to the desktop app +3. **On credential request:** Gateway sends `CredentialQuery::Domain(hostname)` +4. **Desktop app:** + - Unlocks vault with master password (entered by user, NOT stored in OneCLI) + - Finds matching credential + - Decrypts it locally + - Sends plaintext back via Noise-encrypted channel +5. **OneCLI:** Receives already-decrypted credential, caches for 60s, injects into HTTP request + +**Critical:** Decryption happens in the Bitwarden desktop app, NOT in OneCLI. OneCLI never has the master password or encryption keys. + +### 7. Request Flow + +**File:** `apps/gateway/src/gateway.rs` (lines 246-257) + +```rust +// Vault fallback: if no DB secrets matched, try vault providers for this user. +if let Some(cred) = state.vault_service.request_credential(aid, &hostname).await { + let vault_rules = inject::vault_credential_to_rules(&hostname, &cred); + if !vault_rules.is_not_empty() { + injection_rules = vault_rules; + tracing::info!( + agent_id = %agent_id, + hostname = %hostname, + "using vault credential" + ); + } +} +``` + +--- + +## Security Assessment + +| Concern | Status | Evidence | +|---------|--------|----------| +| Full vault download | ❌ **NO** | Per-credential fetching via `CredentialQuery::Domain(hostname)` | +| All credentials in memory | ❌ **NO** | Only individual requested credentials cached (60s TTL) | +| Master password stored | ❌ **NO** | Identity keypair used for Noise auth; decryption in Bitwarden app | +| Decrypted vault at rest | ❌ **NO** | Only transport state stored in DB | +| Session persistence | ✅ Bounded | 30-min idle eviction, 60s credential cache | +| End-to-end encryption | ✅ Yes | Noise protocol over WebSocket | +| Just-in-time access | ✅ Yes | Credentials fetched on-demand per request | + +--- + +## Potential Concerns (Minor) + +1. **Identity keypair in DB:** The COSE-encoded identity keypair is stored in the database. If an attacker gains DB access, they could impersonate the gateway to the Bitwarden desktop app (but the desktop app still controls actual credential decryption). + +2. **Memory cache window:** Credentials exist in memory for up to 60 seconds after first fetch. This is a reasonable trade-off for performance. + +3. **Single credential at a time:** The design fetches one credential per request — no bulk operations that could leak multiple secrets. + +--- + +## Conclusion + +**Johan's suspicion is incorrect.** OneCLI does NOT download the entire Bitwarden vault. It uses Bitwarden's official Agent Access SDK to: + +1. Establish an end-to-end encrypted Noise protocol channel to the Bitwarden desktop app +2. Request **individual credentials by domain** on-demand (just-in-time) +3. Cache only the specific credential for 60 seconds +4. Evict idle sessions after 30 minutes + +This is a **well-architected, security-conscious design** that follows the principle of least privilege and minimizes credential exposure. + +--- + +## References + +- `apps/gateway/src/vault/bitwarden.rs` — Provider implementation +- `apps/gateway/src/vault/bitwarden_db.rs` — Identity and connection storage +- `apps/gateway/src/vault/mod.rs` — VaultProvider trait +- `apps/gateway/src/db.rs` — Database queries (vault_connections table) +- `apps/gateway/Cargo.toml` — Dependencies (`ap-client = "0.9.0"`) +- Bitwarden Agent Access SDK announcement: https://bitwarden.com/blog/introducing-agent-access-sdk/