cleanup: drop the experimental clawdie mini-binary #34
12 changed files with 5 additions and 868 deletions
14
Cargo.lock
generated
14
Cargo.lock
generated
|
|
@ -277,20 +277,6 @@ version = "1.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "clawdie"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"colibri-daemon",
|
||||
"colibri-glasspane",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colibri"
|
||||
version = "0.0.1"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
[workspace]
|
||||
members = ["crates/colibri-contracts", "crates/colibri-deepseek", "crates/colibri-runtime", "crates/colibri-glasspane", "crates/colibri-daemon", "crates/colibri-client", "crates/colibri-glasspane-tui", "crates/colibri-store", "crates/colibri-skills", "crates/colibri-mcp", "crates/clawdie"]
|
||||
members = ["crates/colibri-contracts", "crates/colibri-deepseek", "crates/colibri-runtime", "crates/colibri-glasspane", "crates/colibri-daemon", "crates/colibri-client", "crates/colibri-glasspane-tui", "crates/colibri-store", "crates/colibri-skills", "crates/colibri-mcp"]
|
||||
|
||||
[package]
|
||||
name = "colibri"
|
||||
|
|
@ -28,7 +28,7 @@ tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
|
|||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
# Lean release artifacts (notably the `clawdie` operator binary): strip symbols
|
||||
# so the staged ISO binaries stay small without a manual strip step.
|
||||
# Lean release artifacts: strip symbols so the staged ISO binaries stay small
|
||||
# without a manual strip step.
|
||||
[profile.release]
|
||||
strip = true
|
||||
|
|
@ -10,12 +10,11 @@ Next ISO integration plan: `docs/ISO-INTEGRATION-PLAN.md`.
|
|||
ISO acceptance runbook: `docs/ISO-ACCEPTANCE-RUNBOOK.md`.
|
||||
Clawdie Studio/Zed proposal: `docs/CLAWDIE-STUDIO-PROPOSAL.md`.
|
||||
|
||||
## Workspace — 11 crates
|
||||
## Workspace — 10 crates
|
||||
|
||||
| Crate | Role |
|
||||
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `colibri-mcp` | MCP bridge for editor integration (Zed, Claude Code) via stdio JSON-RPC |
|
||||
| `clawdie` | Simplified operator agent: glasspane + DeepSeek/Telegram in one small binary (build-flag configured). See `docs/CLAWDIE-AGENT-WIKI.md`. |
|
||||
| `colibri-contracts` | JSON schema contracts (golden tests) |
|
||||
| `colibri-deepseek` | DeepSeek cache-hit probe, prefix metering |
|
||||
| `colibri-runtime` | Host status ingestion, runtime inventory |
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
[package]
|
||||
name = "clawdie"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-only"
|
||||
description = "Clawdie — the simplified, operator-friendly Colibri agent (glasspane + supervision + DeepSeek/Telegram) as one small binary"
|
||||
|
||||
[[bin]]
|
||||
name = "clawdie"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
# Reuse the proven control-plane core: glasspane (supervision radar),
|
||||
# the Colibri control-plane socket, the coordination loop, and session lifecycle.
|
||||
colibri-daemon = { path = "../colibri-daemon" }
|
||||
colibri-glasspane = { path = "../colibri-glasspane" }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
//! Build-time configuration for the `clawdie` binary.
|
||||
//!
|
||||
//! The whole product surface is two credentials, and we want them bakeable at
|
||||
//! compile time so an operator ISO ships ready-to-run with no config files:
|
||||
//!
|
||||
//! CLAWDIE_TG_TOKEN — Telegram bot token (the chat front door)
|
||||
//! CLAWDIE_DEEPSEEK_KEY — DeepSeek API key (the one key for everything)
|
||||
//!
|
||||
//! Pass them as build flags, e.g.:
|
||||
//!
|
||||
//! CLAWDIE_TG_TOKEN=123:abc CLAWDIE_DEEPSEEK_KEY=sk-... cargo build --release
|
||||
//!
|
||||
//! Whatever is present at build time is re-exported into the compiled binary
|
||||
//! via `cargo:rustc-env`, where `option_env!` can read it. Runtime environment
|
||||
//! variables always win over baked-in values (see `src/main.rs`), so the same
|
||||
//! binary works for both "baked" ISO builds and "bring your own key" operators.
|
||||
|
||||
fn main() {
|
||||
// Re-run if the build-flag inputs change.
|
||||
println!("cargo:rerun-if-env-changed=CLAWDIE_TG_TOKEN");
|
||||
println!("cargo:rerun-if-env-changed=CLAWDIE_DEEPSEEK_KEY");
|
||||
|
||||
// Bake whatever was provided (empty string when unset — main.rs treats
|
||||
// empty as absent). We never print the secret values to the build log.
|
||||
let tg = std::env::var("CLAWDIE_TG_TOKEN").unwrap_or_default();
|
||||
let ds = std::env::var("CLAWDIE_DEEPSEEK_KEY").unwrap_or_default();
|
||||
println!("cargo:rustc-env=CLAWDIE_BUILTIN_TG_TOKEN={tg}");
|
||||
println!("cargo:rustc-env=CLAWDIE_BUILTIN_DEEPSEEK_KEY={ds}");
|
||||
}
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
//! Minimal DeepSeek chat lane for the Clawdie agent.
|
||||
//!
|
||||
//! Deliberately tiny: one model, one key, one request shape, no cost modes, no
|
||||
//! quota accounting, no provider fallback. That heavier discipline lives in
|
||||
//! `colibri-deepseek` / `colibri-daemon::cost` for the full control plane; the
|
||||
//! simplified Clawdie agent intentionally does **not** carry it.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// DeepSeek's OpenAI-compatible chat-completions endpoint.
|
||||
pub const DEFAULT_ENDPOINT: &str = "https://api.deepseek.com/chat/completions";
|
||||
/// Default chat model. Override at runtime with `DEEPSEEK_MODEL`.
|
||||
pub const DEFAULT_MODEL: &str = "deepseek-chat";
|
||||
|
||||
/// One DeepSeek chat lane, configured once at startup.
|
||||
#[derive(Clone)]
|
||||
pub struct DeepSeek {
|
||||
client: reqwest::Client,
|
||||
endpoint: String,
|
||||
model: String,
|
||||
api_key: String,
|
||||
system_prompt: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ChatMessage<'a> {
|
||||
role: &'a str,
|
||||
content: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ChatRequest<'a> {
|
||||
model: &'a str,
|
||||
messages: Vec<ChatMessage<'a>>,
|
||||
stream: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Choice {
|
||||
message: ResponseMessage,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ResponseMessage {
|
||||
#[serde(default)]
|
||||
content: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ChatResponse {
|
||||
#[serde(default)]
|
||||
choices: Vec<Choice>,
|
||||
}
|
||||
|
||||
impl DeepSeek {
|
||||
/// Build a lane. `api_key` must be non-empty (callers gate on this).
|
||||
pub fn new(api_key: String) -> Result<Self, reqwest::Error> {
|
||||
let endpoint =
|
||||
std::env::var("DEEPSEEK_ENDPOINT").unwrap_or_else(|_| DEFAULT_ENDPOINT.to_string());
|
||||
let model = std::env::var("DEEPSEEK_MODEL").unwrap_or_else(|_| DEFAULT_MODEL.to_string());
|
||||
let system_prompt = std::env::var("CLAWDIE_SYSTEM_PROMPT").unwrap_or_else(|_| {
|
||||
"You are Clawdie, a friendly, concise operator assistant running on a \
|
||||
FreeBSD live system. Answer directly and helpfully."
|
||||
.to_string()
|
||||
});
|
||||
let client = reqwest::Client::builder()
|
||||
.user_agent(concat!("clawdie/", env!("CARGO_PKG_VERSION")))
|
||||
.build()?;
|
||||
Ok(Self {
|
||||
client,
|
||||
endpoint,
|
||||
model,
|
||||
api_key,
|
||||
system_prompt,
|
||||
})
|
||||
}
|
||||
|
||||
/// Send one user message, return the assistant's reply text.
|
||||
pub async fn reply(
|
||||
&self,
|
||||
user_text: &str,
|
||||
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let body = ChatRequest {
|
||||
model: &self.model,
|
||||
messages: vec![
|
||||
ChatMessage {
|
||||
role: "system",
|
||||
content: &self.system_prompt,
|
||||
},
|
||||
ChatMessage {
|
||||
role: "user",
|
||||
content: user_text,
|
||||
},
|
||||
],
|
||||
stream: false,
|
||||
};
|
||||
let resp = self
|
||||
.client
|
||||
.post(&self.endpoint)
|
||||
.bearer_auth(&self.api_key)
|
||||
.json(&body)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
let parsed: ChatResponse = resp.json().await?;
|
||||
let text = parsed
|
||||
.choices
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(|c| c.message.content)
|
||||
.unwrap_or_default();
|
||||
Ok(text)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,170 +0,0 @@
|
|||
//! clawdie — the simplified, operator-friendly Colibri agent.
|
||||
//!
|
||||
//! One small binary, two credentials, zero ceremony. It bundles the proven
|
||||
//! control-plane core (glasspane supervision radar + the Colibri control-plane socket +
|
||||
//! the coordination loop — the "split brain") and puts a DeepSeek-backed
|
||||
//! Telegram bot in front of it. That is the entire out-of-the-box surface.
|
||||
//!
|
||||
//! What was deliberately **lifted** vs. the full Colibri control plane:
|
||||
//! - no cost modes / quota accounting / context-budget enforcement
|
||||
//! - no multi-provider fallback (OpenRouter / Anthropic) — one DeepSeek key
|
||||
//! - no per-user limits, no admin gating
|
||||
//!
|
||||
//! Credentials come from build flags (see `build.rs`) and are overridable at
|
||||
//! runtime by environment variables, so the same binary serves both a baked
|
||||
//! ISO and a bring-your-own-key operator.
|
||||
|
||||
mod deepseek;
|
||||
mod telegram;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use colibri_daemon::{daemon, session, socket, DaemonConfig, DaemonState, SharedState};
|
||||
use tracing::{info, warn};
|
||||
|
||||
/// Resolve a credential: runtime env wins, then the build-flag baked value,
|
||||
/// then nothing. Empty strings count as "not set".
|
||||
fn resolve(env_names: &[&str], baked: Option<&str>) -> Option<String> {
|
||||
for name in env_names {
|
||||
if let Ok(v) = std::env::var(name) {
|
||||
if !v.trim().is_empty() {
|
||||
return Some(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
baked
|
||||
.map(str::trim)
|
||||
.filter(|v| !v.is_empty())
|
||||
.map(str::to_string)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
|
||||
)
|
||||
.init();
|
||||
|
||||
info!("clawdie {} starting", env!("CARGO_PKG_VERSION"));
|
||||
|
||||
// ── Resolve the two and only credentials ────────────────────────────────
|
||||
let tg_token = resolve(
|
||||
&["CLAWDIE_TG_TOKEN", "TELEGRAM_BOT_TOKEN"],
|
||||
option_env!("CLAWDIE_BUILTIN_TG_TOKEN"),
|
||||
);
|
||||
let deepseek_key = resolve(
|
||||
&["CLAWDIE_DEEPSEEK_KEY", "DEEPSEEK_API_KEY"],
|
||||
option_env!("CLAWDIE_BUILTIN_DEEPSEEK_KEY"),
|
||||
);
|
||||
|
||||
// One key for everything: make the daemon's own provider routing see it too.
|
||||
if let Some(key) = &deepseek_key {
|
||||
std::env::set_var("DEEPSEEK_API_KEY", key);
|
||||
}
|
||||
|
||||
info!(
|
||||
telegram = tg_token.is_some(),
|
||||
deepseek = deepseek_key.is_some(),
|
||||
"credentials resolved (build flags + runtime env)"
|
||||
);
|
||||
|
||||
// ── Bring up the control-plane core (glasspane + control-plane socket + loop) ────
|
||||
let config = DaemonConfig::from_env();
|
||||
info!(
|
||||
host = %config.host,
|
||||
data_dir = %config.data_dir.display(),
|
||||
socket_path = %config.socket_path.display(),
|
||||
"control plane configured"
|
||||
);
|
||||
|
||||
let state: SharedState = Arc::new(DaemonState::new(config.clone()));
|
||||
|
||||
tokio::fs::create_dir_all(&config.data_dir).await?;
|
||||
tokio::fs::create_dir_all(config.data_dir.join("sessions")).await?;
|
||||
|
||||
// Reload any persisted sessions so supervision picks up where it left off.
|
||||
if let Ok(mut entries) = tokio::fs::read_dir(config.data_dir.join("sessions")).await {
|
||||
while let Ok(Some(entry)) = entries.next_entry().await {
|
||||
let path = entry.path();
|
||||
if path.extension().and_then(|e| e.to_str()) == Some("jsonl") {
|
||||
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
|
||||
let session_id = stem.to_string();
|
||||
let cfg = Arc::new(config.clone());
|
||||
match session::Session::load(session_id.clone(), cfg) {
|
||||
Ok(sess) => {
|
||||
state.sessions.insert(session_id, sess);
|
||||
}
|
||||
Err(e) => warn!(error = %e, "failed to load session file"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Colibri control-plane socket.
|
||||
let socket_state = state.clone();
|
||||
let socket_shutdown = state.shutdown_rx.resubscribe();
|
||||
let socket_handle = tokio::spawn(async move {
|
||||
socket::serve(socket_state, socket_shutdown).await;
|
||||
});
|
||||
|
||||
// Coordination / heartbeat / glasspane loop.
|
||||
let loop_state = state.clone();
|
||||
let loop_shutdown = state.shutdown_rx.resubscribe();
|
||||
let loop_handle = tokio::spawn(async move {
|
||||
daemon::run_loop(
|
||||
loop_state,
|
||||
daemon::DaemonLoopConfig::default(),
|
||||
loop_shutdown,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
|
||||
// ── Telegram front door (only if we have both a token and a key) ─────────
|
||||
let telegram_handle = match (tg_token, &deepseek_key) {
|
||||
(Some(token), Some(key)) => match deepseek::DeepSeek::new(key.clone()) {
|
||||
Ok(ds) => {
|
||||
let tg_shutdown = state.shutdown_rx.resubscribe();
|
||||
Some(tokio::spawn(async move {
|
||||
telegram::run(token, ds, tg_shutdown).await;
|
||||
}))
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(error = %e, "could not start DeepSeek lane; Telegram bridge disabled");
|
||||
None
|
||||
}
|
||||
},
|
||||
(Some(_), None) => {
|
||||
warn!("Telegram token present but no DeepSeek key — bridge disabled");
|
||||
None
|
||||
}
|
||||
(None, _) => {
|
||||
info!("no Telegram token — running headless (control plane only)");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
// Graceful shutdown on Ctrl-C / SIGTERM.
|
||||
let shutdown_state = state.clone();
|
||||
tokio::spawn(async move {
|
||||
tokio::signal::ctrl_c().await.ok();
|
||||
info!("interrupt received, shutting down");
|
||||
let _ = shutdown_state.shutdown_tx.send(());
|
||||
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
||||
for entry in shutdown_state.agents.iter() {
|
||||
let _ = entry.value().kill().await;
|
||||
}
|
||||
});
|
||||
|
||||
let (socket_result, loop_result) = tokio::join!(socket_handle, loop_handle);
|
||||
socket_result?;
|
||||
loop_result?;
|
||||
if let Some(h) = telegram_handle {
|
||||
let _ = h.await;
|
||||
}
|
||||
|
||||
info!("clawdie shut down cleanly");
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
//! Minimal Telegram bridge: long-poll `getUpdates`, hand each text message to
|
||||
//! the DeepSeek lane, and `sendMessage` the reply back.
|
||||
//!
|
||||
//! No webhooks, no command framework, no per-user quota — the simplest thing
|
||||
//! that gives an operator a working chat bot out of the box. Shuts down cleanly
|
||||
//! when the daemon broadcasts shutdown.
|
||||
|
||||
use serde::Deserialize;
|
||||
use tokio::sync::broadcast;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::deepseek::DeepSeek;
|
||||
|
||||
const API_BASE: &str = "https://api.telegram.org";
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct UpdatesResponse {
|
||||
#[serde(default)]
|
||||
ok: bool,
|
||||
#[serde(default)]
|
||||
result: Vec<Update>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Update {
|
||||
update_id: i64,
|
||||
#[serde(default)]
|
||||
message: Option<Message>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Message {
|
||||
#[serde(default)]
|
||||
text: Option<String>,
|
||||
chat: Chat,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Chat {
|
||||
id: i64,
|
||||
}
|
||||
|
||||
/// Run the bridge until `shutdown` fires. Network errors are logged and retried
|
||||
/// rather than fatal — the bot should survive flaky links on a live USB.
|
||||
pub async fn run(token: String, deepseek: DeepSeek, mut shutdown: broadcast::Receiver<()>) {
|
||||
let client = match reqwest::Client::builder()
|
||||
.user_agent(concat!("clawdie/", env!("CARGO_PKG_VERSION")))
|
||||
.build()
|
||||
{
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
warn!(error = %e, "telegram: failed to build HTTP client; bridge disabled");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let base = format!("{API_BASE}/bot{token}");
|
||||
let mut offset: i64 = 0;
|
||||
info!("telegram bridge online (long-poll)");
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = shutdown.recv() => {
|
||||
info!("telegram bridge shutting down");
|
||||
return;
|
||||
}
|
||||
updates = poll_once(&client, &base, offset) => {
|
||||
match updates {
|
||||
Ok(list) => {
|
||||
for upd in list {
|
||||
offset = offset.max(upd.update_id + 1);
|
||||
if let Some(msg) = upd.message {
|
||||
if let Some(text) = msg.text {
|
||||
handle_message(&client, &base, &deepseek, msg.chat.id, &text)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(error = %e, "telegram: getUpdates failed; retrying");
|
||||
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn poll_once(
|
||||
client: &reqwest::Client,
|
||||
base: &str,
|
||||
offset: i64,
|
||||
) -> Result<Vec<Update>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// 25s long-poll; keep below the client's default timeout.
|
||||
let resp: UpdatesResponse = client
|
||||
.get(format!("{base}/getUpdates"))
|
||||
.query(&[("timeout", "25"), ("offset", &offset.to_string())])
|
||||
.timeout(std::time::Duration::from_secs(35))
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await?;
|
||||
if !resp.ok {
|
||||
return Err("telegram getUpdates returned ok=false".into());
|
||||
}
|
||||
Ok(resp.result)
|
||||
}
|
||||
|
||||
async fn handle_message(
|
||||
client: &reqwest::Client,
|
||||
base: &str,
|
||||
deepseek: &DeepSeek,
|
||||
chat_id: i64,
|
||||
text: &str,
|
||||
) {
|
||||
let reply = match deepseek.reply(text).await {
|
||||
Ok(r) if !r.trim().is_empty() => r,
|
||||
Ok(_) => "(no reply)".to_string(),
|
||||
Err(e) => {
|
||||
warn!(error = %e, "telegram: deepseek reply failed");
|
||||
"Sorry — I couldn't reach the model just now.".to_string()
|
||||
}
|
||||
};
|
||||
if let Err(e) = client
|
||||
.post(format!("{base}/sendMessage"))
|
||||
.json(&serde_json::json!({ "chat_id": chat_id, "text": reply }))
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
warn!(error = %e, "telegram: sendMessage failed");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
# Clawdie Agent — wiki / mindmap
|
||||
|
||||
The **Clawdie agent** mini-binary is an experimental, operator-friendly face of
|
||||
Colibri: one small Rust binary (`clawdie`) that wires a Telegram bot and a
|
||||
DeepSeek lane on top of the control-plane core. It is not the current live-ISO
|
||||
service contract; the FreeBSD live USB runs `colibri_daemon` directly, while
|
||||
`service clawdie` is reserved for a future installed disk/server service once
|
||||
that implementation is chosen.
|
||||
|
||||
This is a mindmap-style wiki page (the format from the Herdr/Colibri capability
|
||||
graph work — see [`HERDR-VS-COLIBRI-GRAPH.md`](./HERDR-VS-COLIBRI-GRAPH.md) and
|
||||
[`COLIBRI-GLASSPANE-DESIGN.md`](./COLIBRI-GLASSPANE-DESIGN.md)).
|
||||
|
||||
## Mindmap
|
||||
|
||||
```mermaid
|
||||
mindmap
|
||||
root((clawdie<br/>one binary))
|
||||
Split brain
|
||||
Glasspane radar
|
||||
5-state machine
|
||||
idle/working/blocked/done/error
|
||||
derived from Pi JSON events
|
||||
Coordination
|
||||
task board
|
||||
agent registry
|
||||
session JSONL
|
||||
Front door
|
||||
Telegram bot
|
||||
long-poll getUpdates
|
||||
sendMessage reply
|
||||
DeepSeek lane
|
||||
one model
|
||||
one key
|
||||
no fallback
|
||||
Build flags
|
||||
CLAWDIE_TG_TOKEN
|
||||
CLAWDIE_DEEPSEEK_KEY
|
||||
baked at compile time
|
||||
runtime env overrides
|
||||
Lifted (NOT shipped)
|
||||
cost modes
|
||||
quota accounting
|
||||
context budgets
|
||||
OpenRouter / Anthropic
|
||||
per-user limits
|
||||
Runs as a service
|
||||
rc.d clawdie
|
||||
daemon(8) supervised
|
||||
restart on crash
|
||||
like Clawdie-AI
|
||||
```
|
||||
|
||||
## What "split brain" means here
|
||||
|
||||
Two halves of the same daemon, both kept:
|
||||
|
||||
| Half | Crate / module | Job |
|
||||
| ---------------- | ----------------------------------- | ----------------------------------------------------- |
|
||||
| **Glasspane** | `colibri-glasspane` + daemon socket | The "radar": agent state, panes, supervision snapshot |
|
||||
| **Coordination** | `colibri-store` + daemon loop | Task board, agent registry, session lifecycle |
|
||||
|
||||
The Clawdie mini-binary reuses these as-is over the Colibri control-plane socket.
|
||||
It adds the Telegram + DeepSeek front door and **removes** the cost/quota
|
||||
machinery from its own runtime path.
|
||||
|
||||
## Surface area (the whole product)
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
tg["Telegram<br/>(CLAWDIE_TG_TOKEN)"] -->|text| clawdie
|
||||
clawdie -->|chat| ds["DeepSeek<br/>(CLAWDIE_DEEPSEEK_KEY)"]
|
||||
ds -->|reply| clawdie
|
||||
clawdie --> gp["glasspane radar"]
|
||||
clawdie --> co["coordination board"]
|
||||
clawdie -. Colibri socket .- ui["operator tools (colibri CLI / TUI)"]
|
||||
```
|
||||
|
||||
## Files
|
||||
|
||||
| Path | Role |
|
||||
| -------------------------------- | ------------------------------------------------------- |
|
||||
| `crates/clawdie/src/main.rs` | Entrypoint: resolve creds, start core + Telegram bridge |
|
||||
| `crates/clawdie/src/telegram.rs` | Long-poll bridge |
|
||||
| `crates/clawdie/src/deepseek.rs` | Minimal one-key chat lane (no cost/quota) |
|
||||
| `crates/clawdie/build.rs` | Bakes the two credentials from build flags |
|
||||
| `packaging/freebsd/clawdie.in` | rc.d service (daemon(8)-supervised, like Clawdie-AI) |
|
||||
|
||||
## Credential resolution order
|
||||
|
||||
1. Runtime env (`CLAWDIE_TG_TOKEN` / `TELEGRAM_BOT_TOKEN`,
|
||||
`CLAWDIE_DEEPSEEK_KEY` / `DEEPSEEK_API_KEY`)
|
||||
2. Build-flag baked value (`option_env!` from `build.rs`)
|
||||
3. Absent → that half stays off (no token = headless control plane only)
|
||||
|
||||
One DeepSeek key serves both the Telegram chat lane and the daemon's own
|
||||
provider routing.
|
||||
|
||||
See [`CLAWDIE-BUILD.md`](./CLAWDIE-BUILD.md) for experimental build notes.
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
# Experimental `clawdie` mini-binary — build notes
|
||||
|
||||
The `clawdie` binary is an experimental Colibri-side candidate for a future
|
||||
deployed-system service (see [`CLAWDIE-AGENT-WIKI.md`](./CLAWDIE-AGENT-WIKI.md)).
|
||||
It is **not** the current FreeBSD live-ISO service contract. The live USB uses
|
||||
`colibri_daemon`; `service clawdie` remains reserved for an installed disk/server
|
||||
service once that implementation is chosen.
|
||||
|
||||
## 1. Build with baked credentials
|
||||
|
||||
Pass the two credentials as environment variables at build time. `build.rs`
|
||||
bakes whatever is present into the binary; runtime env still overrides.
|
||||
|
||||
```sh
|
||||
cd ~/colibri
|
||||
CLAWDIE_TG_TOKEN="123456:telegram-bot-token" \
|
||||
CLAWDIE_DEEPSEEK_KEY="sk-deepseek-key" \
|
||||
cargo build --release -p clawdie
|
||||
|
||||
ls -lh target/release/clawdie # release profile strips symbols automatically
|
||||
target/release/clawdie --version 2>/dev/null || true
|
||||
```
|
||||
|
||||
Build with **no** flags for a "bring your own key" binary — it reads
|
||||
`CLAWDIE_TG_TOKEN` / `TELEGRAM_BOT_TOKEN` and `CLAWDIE_DEEPSEEK_KEY` /
|
||||
`DEEPSEEK_API_KEY` from the runtime environment (or the rc.d env file) instead.
|
||||
|
||||
> Build credentials are secrets. Pass them inline on the build command (so they
|
||||
> are not persisted), never commit them, and prefer the runtime env file for
|
||||
> rotation. They are never printed to the build log.
|
||||
|
||||
## 2. What the binary does out of the box
|
||||
|
||||
- Starts the control-plane core: glasspane supervision + Colibri control-plane
|
||||
socket + coordination loop.
|
||||
- If a Telegram token **and** a DeepSeek key are present, runs the Telegram
|
||||
bridge (long-poll → DeepSeek → reply).
|
||||
- No token → headless (control plane only). Token but no key → bridge disabled
|
||||
with a warning.
|
||||
|
||||
No cost modes, quotas, context budgets, or provider fallback — those are lifted
|
||||
from this agent on purpose.
|
||||
|
||||
## 3. Run as an experimental service
|
||||
|
||||
On FreeBSD, install the rc.d script only on a throwaway test host or an explicit
|
||||
deployed-service experiment:
|
||||
|
||||
```sh
|
||||
pw groupadd clawdie
|
||||
pw useradd clawdie -g clawdie -d /var/db/clawdie -s /usr/sbin/nologin
|
||||
install -m 0555 packaging/freebsd/clawdie.in /usr/local/etc/rc.d/clawdie
|
||||
install -m 0555 target/release/clawdie /usr/local/bin/clawdie
|
||||
sysrc clawdie_enable=YES
|
||||
service clawdie start
|
||||
service clawdie status
|
||||
```
|
||||
|
||||
Per-host credential override (optional; binary already has baked defaults):
|
||||
|
||||
```sh
|
||||
install -d -m 0750 /usr/local/etc/clawdie
|
||||
cat > /usr/local/etc/clawdie/clawdie.env <<'EOF'
|
||||
CLAWDIE_TG_TOKEN=123456:telegram-bot-token
|
||||
CLAWDIE_DEEPSEEK_KEY=sk-deepseek-key
|
||||
EOF
|
||||
chmod 0600 /usr/local/etc/clawdie/clawdie.env
|
||||
service clawdie restart
|
||||
```
|
||||
|
||||
## 4. ISO status
|
||||
|
||||
The current `clawdie-iso` baseline does **not** stage this mini-binary or its
|
||||
rc.d script. ISO builds stage `colibri-daemon`, `colibri`, `colibri-smoke-agent`,
|
||||
and preferably `colibri-tui` from this checkout. If a deployed-system
|
||||
`service clawdie` lane is reintroduced later, it should get fresh packaging and
|
||||
acceptance criteria instead of silently treating this experiment as final.
|
||||
|
||||
## 5. Next ISO build — XFCE operator-USB fixes to carry forward
|
||||
|
||||
The next operator-USB image must retain the XFCE/live-GUI fixes already proven
|
||||
on real hardware (see `clawdie-iso/PLAN-OPERATOR-USB-NEXT.md` and
|
||||
`doc/AMD-ASUS-XFCE-LIVE-USB-FINDINGS.md`). Load-bearing items:
|
||||
|
||||
- **SDDM remains the supported live display manager** — keep it unless an
|
||||
alternative has equivalent real-hardware proof for Intel and AMD GUI boot.
|
||||
- **`clawdie-live-gpu`** — pre-SDDM rc.d service that does a conservative KMS
|
||||
pick (Intel iGPU works out of the box; AMD/NVIDIA picked conservatively)
|
||||
before the display manager starts.
|
||||
- **Hardened USB-root power policy** (commit `b9290ab`): `powerdxx`, C3 C-state
|
||||
default, `hw.usb.no_suspend=1`, and a boot-time `power_profile` — keeps
|
||||
USB-root laptops from stalling/suspending.
|
||||
- **Base runtime fixes already landed**: resolver bootstrap, internal-audio
|
||||
preference, touchpad XInput guards, loader branding, hardware-report capture,
|
||||
public probe-URL reporting, `hw-probe` upload dependency.
|
||||
|
||||
Final XFCE/session/input/audio behavior is only proven on **real hardware** —
|
||||
bhyve/nested VMs/static image inspection are build gates, not final proof.
|
||||
|
|
@ -5,9 +5,7 @@
|
|||
**Date:** 2026-06-01
|
||||
**Status:** Strategic vision — maps to existing T1.4/T1.5 work
|
||||
|
||||
> **Scope:** This applies to the full Colibri control plane. The simplified
|
||||
> `clawdie` operator lane intentionally ships none of this (no cost modes,
|
||||
> quotas, or metering) — see `docs/CLAWDIE-AGENT-WIKI.md`.
|
||||
> **Scope:** This applies to the full Colibri control plane.
|
||||
|
||||
## Core Thesis
|
||||
|
||||
|
|
|
|||
|
|
@ -1,181 +0,0 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# clawdie — experimental FreeBSD rc.d service candidate.
|
||||
#
|
||||
# The supported live-ISO service is colibri_daemon. This script is kept for
|
||||
# explicit deployed-service experiments only; do not treat it as the final
|
||||
# installed-system service contract without fresh acceptance criteria.
|
||||
#
|
||||
# Operator-friendly by design: enable it and start it. The two credentials
|
||||
# (Telegram bot token + DeepSeek key) are normally baked into the binary at
|
||||
# build time (see crates/clawdie/build.rs), so a baked ISO needs no config.
|
||||
# To override per-host, drop them in the env file (default
|
||||
# /usr/local/etc/clawdie/clawdie.env) or set them in rc.conf below.
|
||||
#
|
||||
# clawdie runs in the FOREGROUND (no self-daemonize, no pidfile), so rc.d runs
|
||||
# it under daemon(8), which backgrounds it, restarts on crash, drops privileges
|
||||
# to the clawdie user, and redirects stdout/stderr (tracing) to a logfile.
|
||||
#
|
||||
# Install:
|
||||
# pw groupadd clawdie
|
||||
# pw useradd clawdie -g clawdie -d /var/db/clawdie -s /usr/sbin/nologin
|
||||
# cp packaging/freebsd/clawdie.in /usr/local/etc/rc.d/clawdie
|
||||
# chmod 555 /usr/local/etc/rc.d/clawdie
|
||||
# sysrc clawdie_enable=YES
|
||||
# service clawdie start
|
||||
#
|
||||
# Requires the clawdie binary at /usr/local/bin/clawdie.
|
||||
|
||||
# PROVIDE: clawdie
|
||||
# REQUIRE: LOGIN NETWORKING cleanvar
|
||||
# KEYWORD: shutdown
|
||||
|
||||
. /etc/rc.subr
|
||||
|
||||
name="clawdie"
|
||||
rcvar="clawdie_enable"
|
||||
|
||||
load_rc_config $name
|
||||
|
||||
: ${clawdie_enable:="NO"}
|
||||
: ${clawdie_user:="clawdie"}
|
||||
: ${clawdie_group:="clawdie"}
|
||||
: ${clawdie_program:="/usr/local/bin/clawdie"}
|
||||
: ${clawdie_data_dir:="/var/db/clawdie"}
|
||||
: ${clawdie_run_dir:="/var/run/clawdie"}
|
||||
: ${clawdie_socket:="${clawdie_run_dir}/clawdie.sock"}
|
||||
: ${clawdie_db_path:="${clawdie_data_dir}/clawdie.sqlite"}
|
||||
: ${clawdie_logfile:="/var/log/clawdie/clawdie.log"}
|
||||
: ${clawdie_host:="$(/bin/hostname)"}
|
||||
: ${clawdie_env_file:="/usr/local/etc/clawdie/clawdie.env"}
|
||||
|
||||
pidfile="${clawdie_run_dir}/clawdie.pid"
|
||||
# Supervisor pidfile (the daemon(8) parent). Kept distinct from the child
|
||||
# pidfile so `stop` can target the supervisor — see clawdie_stop.
|
||||
supervisor_pidfile="${clawdie_run_dir}/clawdie-supervisor.pid"
|
||||
|
||||
# Run clawdie under daemon(8):
|
||||
# -P supervisor pidfile (the daemon(8) parent — used by stop)
|
||||
# -p child pidfile (writes the clawdie PID — used by start/status)
|
||||
# -r restart on crash, -t process title, -u drop to the clawdie user,
|
||||
# -o append stdout/stderr to log.
|
||||
command="/usr/sbin/daemon"
|
||||
command_args="-P ${supervisor_pidfile} -p ${pidfile} -r -t ${name} -u ${clawdie_user} \
|
||||
-o ${clawdie_logfile} ${clawdie_program}"
|
||||
|
||||
# Match the child binary so `service clawdie status` finds OUR process via the
|
||||
# child pidfile, not the generic /usr/sbin/daemon supervisor (which would
|
||||
# collide with tailscaled, colibri_daemon, and other daemon(8) services).
|
||||
procname="clawdie"
|
||||
|
||||
start_precmd="clawdie_prestart"
|
||||
start_postcmd="clawdie_poststart"
|
||||
stop_cmd="clawdie_stop"
|
||||
stop_postcmd="clawdie_poststop"
|
||||
extra_commands="health"
|
||||
|
||||
clawdie_prestart()
|
||||
{
|
||||
# /var/run is tmpfs on FreeBSD (wiped each boot) — recreate every start.
|
||||
install -d -o "${clawdie_user}" -g "${clawdie_group}" -m 0750 "${clawdie_run_dir}"
|
||||
install -d -o "${clawdie_user}" -g "${clawdie_group}" -m 0750 "${clawdie_data_dir}"
|
||||
install -d -o "${clawdie_user}" -g "${clawdie_group}" -m 0750 \
|
||||
"$(/usr/bin/dirname "${clawdie_logfile}")"
|
||||
|
||||
# Control-plane config passed to the child via the environment.
|
||||
# COLIBRI_DB_PATH is REQUIRED: without it the daemon falls back to
|
||||
# /var/db/colibri/colibri.sqlite (the full Colibri daemon's path, owned by
|
||||
# the colibri user), which the clawdie user cannot open — Store::open then
|
||||
# panics and daemon(8) -r restart-loops. Keep clawdie's DB in its own dir.
|
||||
export COLIBRI_DAEMON_DATA_DIR="${clawdie_data_dir}"
|
||||
export COLIBRI_DAEMON_SOCKET="${clawdie_socket}"
|
||||
export COLIBRI_DB_PATH="${clawdie_db_path}"
|
||||
export COLIBRI_HOST="${clawdie_host}"
|
||||
|
||||
# Optional per-host credential overrides (binary already has baked defaults).
|
||||
# File format: simple KEY=VALUE lines, e.g.
|
||||
# CLAWDIE_TG_TOKEN=123456:abc
|
||||
# CLAWDIE_DEEPSEEK_KEY=sk-...
|
||||
if [ -r "${clawdie_env_file}" ]; then
|
||||
set -a
|
||||
. "${clawdie_env_file}"
|
||||
set +a
|
||||
fi
|
||||
}
|
||||
|
||||
clawdie_poststart()
|
||||
{
|
||||
# Wait for the Colibri control-plane socket to appear (daemon forks, child binds socket).
|
||||
local timeout=10
|
||||
local waited=0
|
||||
while [ ! -S "${clawdie_socket}" ] && [ $waited -lt $timeout ]; do
|
||||
sleep 1
|
||||
waited=$((waited + 1))
|
||||
done
|
||||
|
||||
if [ -S "${clawdie_socket}" ]; then
|
||||
echo "clawdie socket ready after ${waited}s"
|
||||
else
|
||||
echo "WARNING: clawdie socket not ready after ${timeout}s"
|
||||
fi
|
||||
}
|
||||
|
||||
clawdie_stop()
|
||||
{
|
||||
# daemon(8) -r restarts the child if it is killed directly, so a plain
|
||||
# SIGTERM to the child pidfile would just be undone. Stop the supervisor
|
||||
# instead: on SIGTERM it forwards the signal to the child and exits without
|
||||
# restarting it.
|
||||
local _sup=""
|
||||
[ -f "${supervisor_pidfile}" ] && _sup=$(cat "${supervisor_pidfile}" 2>/dev/null)
|
||||
if [ -n "${_sup}" ] && kill -0 "${_sup}" 2>/dev/null; then
|
||||
echo "Stopping ${name} (daemon(8) supervisor pid ${_sup})."
|
||||
kill -TERM "${_sup}" 2>/dev/null
|
||||
local _n=0
|
||||
while kill -0 "${_sup}" 2>/dev/null && [ ${_n} -lt 30 ]; do
|
||||
sleep 1
|
||||
_n=$((_n + 1))
|
||||
done
|
||||
if kill -0 "${_sup}" 2>/dev/null; then
|
||||
echo "Supervisor did not exit in time; sending SIGKILL."
|
||||
kill -KILL "${_sup}" 2>/dev/null
|
||||
fi
|
||||
else
|
||||
echo "${name} is not running."
|
||||
fi
|
||||
# Belt-and-suspenders: terminate the child if it somehow outlived the
|
||||
# supervisor (e.g. supervisor SIGKILLed before it could clean up).
|
||||
local _ch=""
|
||||
[ -f "${pidfile}" ] && _ch=$(cat "${pidfile}" 2>/dev/null)
|
||||
if [ -n "${_ch}" ] && kill -0 "${_ch}" 2>/dev/null; then
|
||||
kill -TERM "${_ch}" 2>/dev/null
|
||||
fi
|
||||
rm -f "${supervisor_pidfile}" "${pidfile}"
|
||||
}
|
||||
|
||||
clawdie_poststop()
|
||||
{
|
||||
# Clean up tmpfs artifacts on graceful shutdown.
|
||||
if [ -S "${clawdie_socket}" ]; then
|
||||
rm -f "${clawdie_socket}"
|
||||
fi
|
||||
}
|
||||
|
||||
health_cmd="clawdie_health"
|
||||
clawdie_health()
|
||||
{
|
||||
if [ -S "${clawdie_socket}" ]; then
|
||||
if printf '{"cmd":"status"}\n' | nc -U "${clawdie_socket}" -w 2 >/dev/null 2>&1; then
|
||||
echo "clawdie is healthy (socket responding)"
|
||||
return 0
|
||||
else
|
||||
echo "clawdie socket exists but not responding"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
echo "clawdie socket not found"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
run_rc_command "$1"
|
||||
Loading…
Add table
Reference in a new issue