External MCP servers are arbitrary third-party binaries — at least as untrusted
as the agents the spawner already jails — but the #36 prototype spawned them
directly on the host. Close that gap by reusing the existing confinement
primitive instead of growing a second one.
- ExternalMcpServer gains `jail: Option<JailConfig>` (#[serde(default)]).
- ExternalMcpSession::start routes Command::new through
colibri_daemon::spawner::jail_wrap with the shared COLIBRI_JAIL_PRIV_MODE
policy (mdo live / helper deploy). No jail => unchanged. stdio (incl. the
piped JSON-RPC stdin/stdout) flows through jexec/jail/mdo unaffected.
- docs/COLIBRI-EXTERNAL-MCP-PROTOTYPE: document the `jail` field + confinement.
- 3 tests (no-jail passthrough, jexec wrap, registry jail deserialize).
colibri-mcp already depends on colibri-daemon, so no new dep. Build/test/clippy/
fmt green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wires JailConfig through the control-plane socket so a jailed agent spawn is
reachable end-to-end:
- ColibriCommand::SpawnAgent gains `jail: Option<JailConfig>` (#[serde(default)],
so existing/raw JSON clients are unaffected).
- socket dispatch + cmd_spawn_agent thread it onto AgentSpawnConfig.jail, where
jail_wrap applies it.
- colibri-client::spawn_agent sets jail: None (signature unchanged); the typed
CLI keeps its own separate Command enum. A client/CLI flag to actually request
a jail is a follow-up — the socket now carries the field for internal callers
(scheduler/supervisor) and any JSON client.
daemon+client build clean; daemon lib tests (58) green; clippy -D warnings clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds stdio external MCP server registry support to colibri-mcp with read-only discovery by default and explicit COLIBRI_MCP_EXTERNAL_CALL gating for proxying external tools. Also smooths the merged jail-spawn formatting/FreeBSD command parameter edge so repository gates pass.\n\nChecks: cargo test -p colibri-mcp --all-targets; cargo fmt --check; ./scripts/check-format.sh; git diff --check; fake stdio MCP server smoke via colibri-mcp --external-config --external-call
Implements the spawner half of docs/COLIBRI-JAILED-AGENT-SPAWN-DESIGN.md so
Colibri can confine a spawned agent (e.g. pi) in a FreeBSD jail. zot untouched.
- PrivMode {Mdo, Helper, None}: how the (unprivileged) daemon gets the root that
jail attach/create needs. Resolved from COLIBRI_JAIL_PRIV_MODE (default mdo —
the live-USB posture); deployed hosts set helper. Only consulted when a spawn
requests a jail.
- JailConfig {name, path, ip4, user}: `name` enters a persistent jail (jexec,
precedence); `path` makes an ephemeral `jail -c command=` that self-cleans on
exit. Neither set = no-op. (Refines the design's `ephemeral` flag into the
clearer name-vs-path choice.)
- jail_wrap(): pure (binary,args)->(program,argv) wrapper. No-op without a jail.
jexec runs without -l so injected COLIBRI_*/provider env is inherited; stdio
flows through mdo/jexec/jail so glasspane ingestion is unchanged.
- AgentSpawnConfig gains `jail: Option<JailConfig>` (#[serde(default)]); spawn()
resolves PrivMode/helper once and routes the command through jail_wrap.
- kill(): documented jail teardown semantics + the in-jail process-group reaping
follow-up.
- 7 jail_wrap unit tests. Full daemon lib suite (58) green; clippy -D warnings clean.
Not wired through the SpawnAgent socket command yet (it builds AgentSpawnConfig
with jail=None) — that protocol field is the next small step.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The `clawdie` crate (Telegram+DeepSeek mini-agent over the control-plane core)
was an experimental operator-lane candidate. Per the agent-harness
consolidation, the live USB runs colibri_daemon + the zot agent, and the
deployed `service clawdie` is a reserved name, not this binary — so the
mini-binary is dead weight. Remove it and its now-orphaned docs.
- delete crates/clawdie (leaf crate; nothing depended on it)
- delete packaging/freebsd/clawdie.in (its rc.d candidate)
- delete docs/CLAWDIE-AGENT-WIKI.md + docs/CLAWDIE-BUILD.md (only described it)
- drop it from workspace members + Cargo.lock; tidy the strip-profile comment
- README: 11 → 10 crates, remove the clawdie row
- COLIBRI-TOKENOMICS-TRIFECTA: drop the stale clawdie-lane scope note
No "relay" existed in this repo (already gone). zot is untouched. The Clawdie
brand, the clawdie operator user, and the reserved deployed `service clawdie`
name are unaffected — this only removes the experimental Rust mini-binary.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cleans stale Herdr socket/API naming after the Colibri socket rename, preserves Herdr as an optional Linux/macOS display client, marks the clawdie mini-binary service as experimental rather than ISO/deployed-service contract, and removes old internal session logs.\n\nChecks: ./scripts/check-format.sh; cargo fmt --check; git diff --check; sh -n packaging/freebsd/colibri_daemon.in packaging/freebsd/clawdie.in
The colibri-daemon's own control-plane socket was named after Herdr (the AGPL
Linux supervision tool whose protocol shape it borrowed), which made logs/types
("Herdr socket API listening", HerdrCommand/HerdrResponse) look like a Herdr
dependency. There is none — no herdr crate, process, or network call. zot is the
agent; this is Colibri's control-plane socket.
Renamed Colibri's OWN API only:
- HerdrCommand -> ColibriCommand, HerdrResponse -> ColibriResponse (daemon defs +
socket.rs + colibri-client usages).
- log/doc/Cargo strings: "Herdr socket API"/"operator dashboard"/"Herdr Unix
socket" -> "Colibri control-plane socket" (daemon, clawdie).
Wire-compatible: the JSON `cmd` values come from #[serde(rename=...)] and are
unchanged. Kept legitimate references to Herdr *the tool* (glasspane lineage
"reimplements Herdr's glasspane capability", "Herdr's 5-state model"; client
"display clients (Herdr on Linux…)"; tui "herdr-like").
build + test + clippy -D warnings + fmt --check clean; runtime confirms the
daemon now logs "Colibri control-plane socket listening".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The daemon already spawns agents and streams stdout JSONL into the glasspane
(cmd_spawn_agent: take_stdout + attach_pane + stream_agent_stdout_to_glasspane),
but the streaming ingestor was Pi-only — zot panes were only *incidentally*
correct (shared lifecycle names) and dropped zot's tool/streaming events to the
default arm.
- PiJsonlIngestor: add a `runtime` field; ingest_line_at normalizes via
zot_event_type for Zot panes (tool_use_* -> tool_execution_*, response
success:false -> error, response/usage -> skipped), raw type for Pi/Local.
- SupervisedPane::new_with_runtime + PaneSupervisor::attach_pane_with_runtime
(existing new/attach_pane_at delegate with Pi — no behavior change).
- socket.rs cmd_spawn_agent: derive runtime from the binary basename
(`zot` -> Zot, else Pi) and attach the pane with it. The stdout streamer is
unchanged — it now ingests with the pane's runtime.
Tests: 34 pass incl. a new runtime-aware test feeding RAW zot lines (tool_use_*,
turn_end) through PaneSupervisor.ingest_line_at -> Working mid tool-loop, Done at
turn_end. clippy -D warnings clean.
Completes the daemon spawn -> glasspane wiring for zot (ADR migration step).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Re-landed on current main (the earlier branch never merged — main moved under
it). Operators hit "permission denied" connecting to the colibri daemon from
colibri-tui / the `clawdie` helper: socket.rs binds the Unix socket but never
sets its mode, so it stays at the umask default (0755 = owner-only write).
Connecting needs WRITE perm, so a colibri-group member (clawdie) gets EACCES.
chmod the socket to 0770 after bind. Shared socket::serve, so it covers both
colibri-daemon and the clawdie agent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The colibri CLI's parse_args and run handler referenced Command::ListSkills
and Command::RegisterSkill, but both variants were missing from the enum
definition. This caused a build failure on Linux (rustc 1.95.0).
Added the missing variants with their fields (name, description, category
for RegisterSkill). Build + test + format gate verified green.
Co-Authored-By: Hermes Agent <hermes@clawdie.si>
Manifest captured from the clawdie-iso operator USB:
- FreeBSD 15.0-RELEASE, Node v24.14.1, pi 0.78.0
- Validates the RuntimeInventory contract parses live USB data
- DaemonClient: list_skills() and register_skill() methods
- CLI: list-skills and register-skill subcommands
- Parsing: --description and --category options for register-skill
- Usage text and examples updated
The daemon socket already had ListSkills and RegisterSkill commands;
this exposes them through the colibri CLI binary.
New `clawdie` crate: the operator-friendly face of Colibri. One small Rust
binary that reuses the proven control-plane core (glasspane supervision +
Herdr Unix-socket API + 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.
Deliberately lifted vs. the full control plane: cost modes, quota accounting,
context budgets, multi-provider fallback (OpenRouter/Anthropic), per-user
limits. One DeepSeek key serves both the chat lane and the daemon routing.
- crates/clawdie: main (core + bridge wiring), telegram (long-poll bridge),
deepseek (minimal one-key chat), build.rs (bakes CLAWDIE_TG_TOKEN +
CLAWDIE_DEEPSEEK_KEY build flags; runtime env overrides).
- packaging/freebsd/clawdie.in: rc.d service, daemon(8)-supervised, restart on
crash, dedicated clawdie user — starts as a service like Clawdie-AI.
- release profile strips symbols (binary ~7.6 MB stripped).
- docs/CLAWDIE-AGENT-WIKI.md (mindmap), docs/CLAWDIE-BUILD.md (build + ISO +
next-build XFCE USB fixes), README workspace table.
Build/clippy/fmt green; headless start smoke-tested (socket + sessions bind).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds local_args field to HerdrCommand::SpawnAgent, enabling the
Local provider to pass argv to the agent binary without a wrapper
script. Backward-compatible — local_args defaults to None.
Real Pi spawn on FreeBSD is now:
spawn-agent provider=local model=pi local_args=['--mode','json','--no-tools','-p','task']
Previously required a wrapper script because only an executable
path was accepted. This closes the OSA wrapper caveat from PR #9.
Build: pass | Tests: workspace green
When scheduler_prompt_injection is enabled and a session_id is
provided on spawn-agent, the daemon builds a PromptAssembly from
the session, serializes it as COLIBRI_SESSION_CONTEXT env var,
and passes COLIBRI_COST_MODE to the spawned agent process.
Config-gated (default: disabled) via COLIBRI_SCHEDULER_PROMPT_INJECTION.
No cache warming — that's PR3b (separate).
Build: pass | Tests: workspace green | Clippy: clean | Fmt: clean
Validates: Colibri spawns agent process (fake-pi-agent.py) → reads
JSONL stdout → glasspane ingests → snapshot shows Done state with
correct session ID.
Uses scripts/fake-pi-agent.py which emits the colibri-pi-events
JSONL taxonomy (session, agent_start, turn_start, turn_end,
agent_end). Proves the spawn→ingest→glasspane pipeline without
requiring the real pi binary.
The real Pi binary path (when installed) follows the same pattern:
pi --mode json is spawned with identical spawner code.
Build: pass | Tests: 1/1 green | Workspace: all green
Structural only — no behavior change. Introduces:
- PromptAssembly: named 3-region wrapper around build_prompt_messages()
with to_messages(), immutable_prefix, appendable_log, volatile_scratch,
total_bytes, estimated_tokens.
- CacheMetrics: per-session cache-hit tracking with hit_rate() and
record().
- Session::build_prompt_assembly() wraps existing build_prompt_messages()
with no logic change.
- 5 golden tests: assembly structure, empty volatile, hit rate
calculations, record accumulation.
- Linked T1.4-PROMPT-DISCIPLINE-PLAN.md from COLIBRI-CUTOVER-PLAN.md.
No trimming, no escalation, no scheduler changes — PR 2 and 3 follow.
Parked branches (colibri-skills, zot harness) untouched.
Build: pass | Tests: 41/41 green (+5 new) | Clippy: clean | Fmt: clean
Phase 1 of zot agent harness integration. Adds:
- AgentRuntime enum (Pi, Zot, Local) with serde support
- zot_event_type() — parse zot RPC NDJSON lines to normalized
Colibri event types. Permissive: handles tool_use_start,
tool_use_args, tool_use_end in addition to documented events.
- apply_zot_event() / fold_zot_events() — state transitions
reusing the existing apply_pi_event() logic via normalization.
- Critical: assistant_message does NOT end the pane (zot may
emit them during tool loops). Only turn_end/done signals
completion.
- 16 new tests using real captured zot RPC event lines, including
tool call sequences and error/success response handling.
- PiJsonlIngestor and existing tests unchanged (33/33 green).
No spawner, socket, or process changes — this is the event
taxonomy foundation only. Wrapper + daemon integration follow
in Phase 2.
Build: pass | Tests: 33/33 green | Clippy: clean | Fmt: clean
Phase 1: structs + type system + 12 tests. No IO, no SQLite yet.
Compiles against full workspace (9 crates now, up from 8).
The colibri-skills crate is the read-only runtime consumer for
skill artifacts authored in Clawdie-AI. It does NOT store or author
skills — it indexes committed, reviewed skill bundles.
Seeded from the astro-howto artifact (PR #6 in clawdie-ai):
- Skill, SkillManifest, SkillArtifact, SkillChunk structs
- ArtifactType classifier (document, image, script, transcript, etc.)
- ImportSummary + SearchResult types
- SQLite schema documented in doc/COLIBRI-SKILLS-PLAN.md
Build: pass | Tests: 12/12 green | Clippy: pending
"Cene že še češnje je" — š/č/ž are 2-byte UTF-8, same family as
German umlauts. Complements the existing CJK+umlaut test. Ensures
compact_tool_result never panics on Slavic diacritics in FreeBSD
locale output, pkg descriptions, and agent logs.
cost.rs:124 sliced at a raw byte boundary which panics on multibyte
UTF-8 characters (ä, ö, ü, CJK, etc). FreeBSD tool output and agent
logs regularly contain non-ASCII.
Fix: use str::floor_char_boundary() to round down to the nearest valid
char boundary before slicing. This never panics and produces valid
UTF-8 output at or below the requested byte limit.
Added test: multibyte truncation with CJK + umlaut input.
The operator CLI was already a subcommand dispatcher; drop the -ctl suffix so
it reads `colibri status` / `colibri snapshot` / `colibri spawn-local …` — one
`colibri` entrypoint (same single-binary-with-subcommands shape as just, but a
control plane). Renames the bin + its source file, updates usage strings, and
points the forward-looking docs at `colibri`. Dated session/handoff records
are left as historical. (Task-board subcommands intake/create/list are the
follow-up T1.3c slice.)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Regression guard for the scheduler store deadlock fixed upstream in d760536:
scheduler.tick held a non-reentrant std::sync::Mutex (state.store) across the
match scrutinee, so relocking inside the arm deadlocked the first time any
intake/scheduled task fired.
This test (independently authored on domedog) submits an intake-task over the
real Unix socket, runs run_loop with a fast scheduler tick, and asserts the
task is drained into SQLite. It hangs (>8min) against the pre-fix code and
passes in 0.09s against d760536 — so it cross-validates that fix and guards the
regression. Closes osa-smoke rec #2.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two improvements to Hermes' run_loop wiring (9717ce7):
1. Add scheduler_secs to the daemon loop startup log — the most
important interval for the OSA re-smoke was missing from the
heartbeat/rotation/handoff log line.
2. Replace tokio::select! with tokio::join! in main.rs — select!
returned when the first task finished, leaving the other dangling.
join! waits for both the socket server and the daemon loop to
complete before proceeding to shutdown.
89 tests pass, clippy clean.
The scheduler tick was wired into daemon::run_loop but main.rs only
started the socket server, never the background loop. intake-task
commands queued into the scheduler's in-memory queue were never
processed. Fix: spawn daemon::run_loop as a second tokio task
alongside the socket server.
The Tab / BackTab key arms wrapped their whole body in
`if !app.sessions.is_empty()`. clippy (-D warnings) flagged both as
collapsible_match; lift the guard onto the match arm. Empty `sessions` now
falls through to the catch-all — same no-op as before, behavior unchanged.
Gates on c2655d1 + this: build ok, cargo test --workspace 65 passed/0 failed,
cargo clippy --workspace --all-targets -- -D warnings clean, cargo fmt --check clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1. Terminal cleanup on all exit paths: move disable_raw_mode +
LeaveAlternateScreen to main() with a restore_terminal() helper,
install a panic hook so raw mode is never left active on panic or
io::Error early-return from run().
2. Safe observed_at slice: replace &snap.observed_at[..19] (panics on
short strings) with .get(..19).unwrap_or(&snap.observed_at).
3. Stalled state unified in icon column: state_color() and state_icon()
now accept a stalled bool — stalled panes show magenta ⚠ in the icon
cell instead of a green/yellow primary state icon, so the eye lands on
a single column. Separate Stalled column kept for redundancy.
4. Footer j/k navigation hint added alongside q and r.
---
Build: pass | Tests: pass — 50 passed