Rebrand: TeamClaw -> Clawmates (clawmates.work)
Full-depth rename per the approved plan; the 'claw' product vocabulary (claws, /claws routes, clawId, Claw Chat) stays — it is now the brand. - Display brand: Clawmates (manifest, titles, hero, login/rail logo 'clawmates'); default host app.clawmates.work; registry ghcr.io/clawmates - Crates tc-* -> cm-* (16 crates + all imports); binaries clawmates-server/broker/bundler; images clawmates/*; env prefix CLAWMATES_* (+ CM_TEST_DATABASE_URL / CM_LIVE_LLM); config clawmates.toml; helm chart deploy/helm/clawmates with clawmates-* resources; db names clawmates*; sockets /run/clawmates; cookie cm_session; kind cluster clawmates-test; seccomp node profile clawmates-agent-profile.json - All 9 Playwright brand assertions updated in lockstep; historical spec document left untouched as the only remaining 'TeamClaw' - Local env migrated: dev pg clawmates-dev-pg/clawmates_dev, shared test server clawmates-test-pg, kind cluster recreated with image + profile, compose images rebuilt under clawmates/* Verified end to end: 161 Rust + 68 frontend tests, 29 Playwright journeys, 4 live kind tests, helm/install/LOC/placeholder gates, and the clean-room install rehearsal serving the clawmates login page from a signed bundle of the rebuilt images. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5
parent
8046853feb
commit
add4f79fed
@@ -0,0 +1,270 @@
|
||||
//! Configuration loading for all Clawmates binaries.
|
||||
//!
|
||||
//! One TOML file plus `CLAWMATES_*` environment overrides (nested keys split
|
||||
//! on `__`, e.g. `CLAWMATES_DATABASE__URL`). The same configuration tree
|
||||
//! drives both deployment targets; semantic validation rejects combinations
|
||||
//! that would only fail at runtime (e.g. an OpenAI-compatible provider with
|
||||
//! no endpoint to call).
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::path::Path;
|
||||
|
||||
use figment::providers::{Env, Format, Toml};
|
||||
use figment::Figment;
|
||||
use serde::Deserialize;
|
||||
|
||||
/// Which of the two first-class deployment targets this instance runs as.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DeployTarget {
|
||||
AirGapped,
|
||||
Cloud,
|
||||
}
|
||||
|
||||
/// Which `LlmProvider` implementation the runtime instantiates.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum LlmProviderKind {
|
||||
Anthropic,
|
||||
#[serde(rename = "openai_compat")]
|
||||
OpenAiCompat,
|
||||
Scripted,
|
||||
}
|
||||
|
||||
/// Which `AuthProvider` implementation handles sign-in.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AuthMode {
|
||||
Local,
|
||||
Oidc,
|
||||
/// Clerk hosted identity: the instance's Frontend API URL is the OIDC
|
||||
/// issuer; session JWTs are verified against its JWKS. Cloud only —
|
||||
/// air-gapped installs use `local`.
|
||||
Clerk,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct DatabaseConfig {
|
||||
pub url: String,
|
||||
#[serde(default = "default_max_connections")]
|
||||
pub max_connections: u32,
|
||||
}
|
||||
|
||||
fn default_max_connections() -> u32 {
|
||||
10
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct LlmConfig {
|
||||
pub provider: LlmProviderKind,
|
||||
/// Endpoint for `openai_compat`; unused by other providers.
|
||||
pub base_url: Option<String>,
|
||||
pub model: String,
|
||||
/// Scenario file for the deterministic `scripted` provider.
|
||||
pub scenario_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct AuthConfig {
|
||||
pub mode: AuthMode,
|
||||
pub issuer_url: Option<String>,
|
||||
pub client_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum StorageBackend {
|
||||
Local,
|
||||
S3,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct StorageConfig {
|
||||
/// Root directory for file-drive blobs (volume-mounted in compose).
|
||||
pub data_dir: String,
|
||||
#[serde(default = "default_backend")]
|
||||
pub backend: StorageBackend,
|
||||
/// S3 backend settings; keys arrive via CLAWMATES_STORAGE__* env vars.
|
||||
pub s3_endpoint: Option<String>,
|
||||
pub s3_bucket: Option<String>,
|
||||
pub s3_access_key: Option<String>,
|
||||
pub s3_secret_key: Option<String>,
|
||||
}
|
||||
|
||||
fn default_backend() -> StorageBackend {
|
||||
StorageBackend::Local
|
||||
}
|
||||
|
||||
impl Default for StorageConfig {
|
||||
fn default() -> Self {
|
||||
StorageConfig {
|
||||
data_dir: "./data".into(),
|
||||
backend: StorageBackend::Local,
|
||||
s3_endpoint: None,
|
||||
s3_bucket: None,
|
||||
s3_access_key: None,
|
||||
s3_secret_key: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct BrokerConfig {
|
||||
/// Unix socket the secret broker daemon listens on.
|
||||
pub socket_path: String,
|
||||
}
|
||||
|
||||
impl Default for BrokerConfig {
|
||||
fn default() -> Self {
|
||||
BrokerConfig {
|
||||
socket_path: "/tmp/clawmates-broker.sock".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct SlackConfig {
|
||||
/// Slack API base; e2e points it at the local sink.
|
||||
pub base_url: String,
|
||||
}
|
||||
|
||||
impl Default for SlackConfig {
|
||||
fn default() -> Self {
|
||||
SlackConfig {
|
||||
base_url: "https://slack.com/api".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct SandboxConfig {
|
||||
/// Agent sandbox image (must exist locally / be preloaded in cluster).
|
||||
pub image: String,
|
||||
/// Chromium image for browser.goto (the only egress-enabled sandbox).
|
||||
pub browser_image: String,
|
||||
/// Disable to run without environment tools (shell.exec errors).
|
||||
pub enabled: bool,
|
||||
/// Pre-provisioned sandboxes kept ready (0 = provision on demand).
|
||||
pub warm_pool: usize,
|
||||
}
|
||||
|
||||
impl Default for SandboxConfig {
|
||||
fn default() -> Self {
|
||||
SandboxConfig {
|
||||
image: "clawmates/agent-base:dev".into(),
|
||||
browser_image: "clawmates/agent-browser:dev".into(),
|
||||
enabled: true,
|
||||
warm_pool: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize)]
|
||||
pub struct TelemetryConfig {
|
||||
/// OTLP/HTTP collector base (e.g. http://otel-collector:4318).
|
||||
/// Unset = no export; logs only. Nothing ever phones home uninvited.
|
||||
pub otlp_endpoint: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize)]
|
||||
pub struct OAuthConfig {
|
||||
/// Default identity provider for directory-app OAuth connects.
|
||||
pub issuer_url: Option<String>,
|
||||
pub client_id: Option<String>,
|
||||
pub client_secret: Option<String>,
|
||||
/// Public base URL of this server (builds the redirect_uri).
|
||||
pub redirect_base: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct AppConfig {
|
||||
pub deploy_target: DeployTarget,
|
||||
pub listen_addr: SocketAddr,
|
||||
pub database: DatabaseConfig,
|
||||
pub llm: LlmConfig,
|
||||
pub auth: AuthConfig,
|
||||
#[serde(default)]
|
||||
pub storage: StorageConfig,
|
||||
#[serde(default)]
|
||||
pub broker: BrokerConfig,
|
||||
#[serde(default)]
|
||||
pub slack: SlackConfig,
|
||||
#[serde(default)]
|
||||
pub oauth: OAuthConfig,
|
||||
#[serde(default)]
|
||||
pub sandbox: SandboxConfig,
|
||||
#[serde(default)]
|
||||
pub telemetry: TelemetryConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ConfigError {
|
||||
#[error("failed to load configuration: {0}")]
|
||||
Load(String),
|
||||
#[error("invalid configuration: {0}")]
|
||||
Invalid(String),
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
/// Loads configuration from `path`, overlaying `CLAWMATES_*` environment
|
||||
/// variables, and validates it.
|
||||
pub fn load_from(path: &Path) -> Result<AppConfig, ConfigError> {
|
||||
if !path.is_file() {
|
||||
return Err(ConfigError::Load(format!(
|
||||
"configuration file not found: {}",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
let config: AppConfig = Figment::new()
|
||||
.merge(Toml::file(path))
|
||||
.merge(Env::prefixed("CLAWMATES_").split("__"))
|
||||
.extract()
|
||||
.map_err(|e| ConfigError::Load(e.to_string()))?;
|
||||
config.validate()?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn validate(&self) -> Result<(), ConfigError> {
|
||||
match self.llm.provider {
|
||||
LlmProviderKind::OpenAiCompat if self.llm.base_url.is_none() => {
|
||||
return Err(ConfigError::Invalid(
|
||||
"llm.provider = \"openai_compat\" requires llm.base_url".into(),
|
||||
));
|
||||
}
|
||||
LlmProviderKind::Scripted if self.llm.scenario_path.is_none() => {
|
||||
return Err(ConfigError::Invalid(
|
||||
"llm.provider = \"scripted\" requires llm.scenario_path".into(),
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if self.storage.backend == StorageBackend::S3
|
||||
&& (self.storage.s3_endpoint.is_none() || self.storage.s3_bucket.is_none())
|
||||
{
|
||||
return Err(ConfigError::Invalid(
|
||||
"storage.backend = \"s3\" requires storage.s3_endpoint and storage.s3_bucket"
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
if self.auth.mode == AuthMode::Oidc {
|
||||
if self.auth.issuer_url.is_none() {
|
||||
return Err(ConfigError::Invalid(
|
||||
"auth.mode = \"oidc\" requires auth.issuer_url".into(),
|
||||
));
|
||||
}
|
||||
if self.auth.client_id.is_none() {
|
||||
return Err(ConfigError::Invalid(
|
||||
"auth.mode = \"oidc\" requires auth.client_id".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
if self.auth.mode == AuthMode::Clerk && self.auth.issuer_url.is_none() {
|
||||
return Err(ConfigError::Invalid(
|
||||
"auth.mode = \"clerk\" requires auth.issuer_url \
|
||||
(the instance's https://<slug>.clerk.accounts.dev)"
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user