Compose: production README, env knobs, first-owner bootstrap — deployed & live

Made the Docker Compose route turn-key for a real self-host, then stood
the whole stack up and drove a live chat through it.

- First-owner bootstrap (cm-auth::bootstrap_owner): a fresh local-auth
  install has no users and no signup route, so the initial Owner +
  workspace are provisioned ONCE from CLAWMATES_BOOTSTRAP_* env on first
  boot — idempotent, never clobbers an existing install (keys on 'any
  workspace exists'). Two real-Postgres tests (creates + signs in;
  second call is a no-op). Wired into server boot, guarded on a
  non-empty password
- deploy/compose/README.md: full production bring-up — services, the
  security topology, every config knob, Anthropic vs local-LLM, the
  broker-key backup, ops, and TLS/SSE proxy notes
- .env.example fleshed out (bootstrap, LLM, auth mode, OTLP); compose
  uses optional env_file so only the knobs you set are injected (unset
  options never override clawmates.toml with empty strings)
- volume-init one-shot chowns the broker's named volumes so the non-root
  scratch broker can write its socket + generated master key

Deployed locally and verified end to end: all 5 containers healthy,
broker generated its key, server bootstrapped owner@…, login + /api/user/me
work, and a real message streamed a live Anthropic response through the
gateway. Captured screenshots of login, workspace home, chat, and the
Computer panel.

166 Rust tests (+2 bootstrap).

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-10 14:06:15 -05:00
co-authored by Claude Fable 5
parent b9fdec9173
commit 447f7039d8
7 changed files with 318 additions and 0 deletions
+23
View File
@@ -74,6 +74,29 @@ async fn run() -> Result<(), String> {
e2e::seed(&pool).await?;
}
// First-owner bootstrap (self-hosted local-auth installs). Provisions
// the initial workspace + Owner from env exactly once; a no-op on
// every later boot. CLAWMATES_BOOTSTRAP_OWNER_PASSWORD is the trigger.
if let Some(password) = std::env::var("CLAWMATES_BOOTSTRAP_OWNER_PASSWORD")
.ok()
.filter(|p| !p.is_empty())
{
let email = std::env::var("CLAWMATES_BOOTSTRAP_OWNER_EMAIL")
.unwrap_or_else(|_| "[email protected]".into());
let workspace = std::env::var("CLAWMATES_BOOTSTRAP_WORKSPACE")
.unwrap_or_else(|_| "My Workspace".into());
let credits = std::env::var("CLAWMATES_BOOTSTRAP_CREDITS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(1250);
let created = cm_auth::bootstrap_owner(&pool, &workspace, &email, &password, credits)
.await
.map_err(|e| format!("bootstrap owner: {e}"))?;
if created {
println!("clawmates-server: bootstrapped first owner {email} in '{workspace}'");
}
}
let provider = build_provider(&config)?;
let blob: std::sync::Arc<dyn cm_files::BlobStore> = match config.storage.backend {
cm_config::StorageBackend::Local => std::sync::Arc::new(cm_files::LocalBlobStore::new(