collet 0.1.0

Relentless agentic coding orchestrator with zero-drop agent loops
Documentation
use serde::{Deserialize, Serialize};

use crate::config::secrets::decrypt_key;

/// `[remote]` section — remote gateway settings.
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct RemoteSection {
    /// Enable remote control gateway. Default: false.
    pub enabled: Option<bool>,
    /// Session idle timeout in seconds before eviction. Default: 300.
    pub session_timeout: Option<u64>,
    /// Default streaming level: "compact" or "full". Default: "compact".
    pub default_streaming: Option<String>,
    /// Default workspace scope: "project", "workspace", or "full". Default: "project".
    pub default_workspace: Option<String>,
    /// Default workspace directory used when a channel has no mapping and no
    /// explicit session. Supports `~` expansion. Default: `~/.collet/workspace`.
    pub workspace: Option<String>,
    /// Tool approval mode: "yolo" | "plan-only" | "cautious". Default: "yolo".
    ///
    /// - `yolo`      — All tools auto-approved (current behaviour).
    /// - `plan-only` — Only plan approval UI; tools run freely.
    /// - `cautious`  — Risky tools (bash, file_write, …) require per-call approval
    ///   via inline buttons with Allow / Allow-for-session / Deny.
    pub approval_mode: Option<String>,
    /// Fine-grained per-tool permission overrides.
    #[serde(default)]
    pub permissions: RemotePermissionsSection,
}

/// `[remote.permissions]` — tool-level allow/deny overrides.
///
/// Entries can be plain tool names (`"bash"`) or argument-prefix patterns:
/// `"bash(git status*)"` — approve all bash calls whose args start with "git status".
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct RemotePermissionsSection {
    /// Tools (or patterns) that are always approved without prompting.
    #[serde(default)]
    pub always_allow: Vec<String>,
    /// Tools (or patterns) that are always denied without prompting.
    #[serde(default)]
    pub always_deny: Vec<String>,
}

/// `[telegram]` section.
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct TelegramSection {
    /// Bot token (plain text — prefer env var or encrypted).
    pub token: Option<String>,
    /// AES-256-GCM encrypted bot token (base64).
    pub token_enc: Option<String>,
    /// Allowed Telegram user IDs.
    #[serde(default)]
    pub allowed_users: Vec<i64>,
}

/// `[slack]` section.
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct SlackSection {
    /// Bot token (xoxb-...).
    pub bot_token: Option<String>,
    /// App-level token for Socket Mode (xapp-...).
    pub app_token: Option<String>,
    /// Encrypted bot token.
    pub bot_token_enc: Option<String>,
    /// Encrypted app token.
    pub app_token_enc: Option<String>,
    /// Allowed Slack user IDs.
    #[serde(default)]
    pub allowed_users: Vec<String>,
}

/// `[discord]` section.
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct DiscordSection {
    /// Bot token.
    pub token: Option<String>,
    /// Encrypted bot token.
    pub token_enc: Option<String>,
    /// Allowed Discord user IDs.
    #[serde(default)]
    pub allowed_users: Vec<u64>,
    /// Restrict to specific guild IDs.
    #[serde(default)]
    pub guild_ids: Vec<u64>,
}

/// A single `[[channel_map]]` entry mapping a platform channel to a project.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChannelMappingEntry {
    /// Platform name: "telegram", "slack", "discord".
    pub platform: String,
    /// Platform-specific channel ID.
    pub channel: String,
    /// Optional project directory path. Falls back to `[remote].workspace` if unset.
    #[serde(default)]
    pub project: Option<String>,
    /// Human-readable name for display.
    #[serde(default)]
    pub name: String,
    /// Optional agent name to use for this channel (e.g., "architect", "code").
    /// When set, the session automatically applies this agent's model and system_prompt.
    #[serde(default)]
    pub agent: Option<String>,
}

/// `[web]` section — web server configuration.
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct WebSection {
    /// Hostname to bind (default: "127.0.0.1").
    pub host: Option<String>,
    /// Port to bind (default: 3080).
    pub port: Option<u16>,
    /// Login username (default: "collet").
    pub username: Option<String>,
    /// AES-256-GCM encrypted password (base64). Use `collet secure --web` to set,
    /// or pass via `COLLET_WEB_PASSWORD` env var.
    pub password_enc: Option<String>,
    /// Additional CORS origins (comma-separated).
    /// Default: only same-origin. Example: "http://localhost:5173"
    pub cors_origins: Option<String>,
}

/// Resolved web server configuration.
#[derive(Debug, Clone)]
pub struct WebConfig {
    pub host: String,
    pub port: u16,
    pub username: String,
    pub password: Option<String>,
    pub cors_origins: Vec<String>,
}

impl WebConfig {
    pub fn from_section(section: &WebSection) -> Self {
        let host = std::env::var("COLLET_WEB_HOST")
            .ok()
            .or_else(|| section.host.clone())
            .unwrap_or_else(|| "127.0.0.1".to_string());

        let port = std::env::var("COLLET_WEB_PORT")
            .ok()
            .and_then(|v| v.parse().ok())
            .or(section.port)
            .unwrap_or(3080);

        let username = std::env::var("COLLET_WEB_USERNAME")
            .ok()
            .or_else(|| section.username.clone())
            .unwrap_or_else(|| "collet".to_string());

        let password = std::env::var("COLLET_WEB_PASSWORD").ok().or_else(|| {
            section
                .password_enc
                .as_deref()
                .and_then(|enc| decrypt_key(enc).ok())
        });

        let cors_origins = std::env::var("COLLET_WEB_CORS")
            .ok()
            .or_else(|| section.cors_origins.clone())
            .map(|s| s.split(',').map(|o| o.trim().to_string()).collect())
            .unwrap_or_default();

        Self {
            host,
            port,
            username,
            password,
            cors_origins,
        }
    }
}