Return an error from the socket server when another daemon owns the Unix socket or bind setup fails, and broadcast shutdown so the daemon does not stay alive without a control socket. Also format the PR docs.\n\nChecks: cargo fmt --check; ./scripts/check-format.sh; git diff --check; cargo test -p colibri-daemon clear_stale_socket -- --nocapture; cargo test -p colibri-daemon --test sigterm_shutdown -- --nocapture.
The rc.d "rm stale socket on prestart" fix (07e4660) was a band-aid over
two daemon-side defects that surfaced on the live FreeBSD host:
1. colibri-daemon never handled SIGTERM. main.rs awaited only ctrl_c()
(SIGINT), so `service stop`/`restart` — which sends SIGTERM via
daemon(8) to the child — killed it on the default disposition with no
cleanup. The graceful path (socket removal, agent reaping) never ran,
leaking the socket file and orphaning spawned agents across restarts.
Now wait_for_shutdown_signal() selects on SIGTERM or SIGINT, so the
same graceful path runs on a normal service stop. New integration test
(tests/sigterm_shutdown.rs) spawns the binary, sends SIGTERM, and
asserts the socket is removed.
2. Stale-socket cleanup had no liveness check — both the daemon
(socket.rs) and the rc prestart would unconditionally rm the socket
before bind, which could delete a *running* instance's socket if
rc.subr's pid detection misfires and starts a second daemon. Cleanup
now probes first (clear_stale_socket): connect succeeds -> refuse to
start; refused/dead -> remove and bind. Unit-tested for absent, stale,
and live cases.
With the daemon owning safe socket cleanup, the rc prestart no longer
removes the socket (only stale pidfiles), eliminating the restart-time
clobber hazard. This also makes the SIGTERM shutdown described in
ISO-SERVICE-LAYOUT.md (PR #75) actually true.
Gates: cargo fmt --check, clippy -D warnings, cargo test --workspace all
green on Linux; sh -n on the rc script OK. FreeBSD runtime validation
still pending per FREEBSD-BUILD-LANE-HANDOFF.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rename the local deterministic launch helper from colibri-smoke-agent to colibri-test-agent, update CLI/TUI/tests/docs, and teach the FreeBSD rc.d service to source /usr/local/etc/colibri/provider.env plus set a service PATH for local spawns.\n\nChecks: cargo fmt --check; ./scripts/check-format.sh; git diff --check; cargo check -p colibri-daemon -p colibri-client -p colibri-glasspane-tui; cargo check -p colibri-client --bins; cargo test -p colibri-client --test live_socket_check -- --nocapture.
Priority 3 — Cost mode enforcement:
- Removed session_max_bytes/max_uncompacted_turns from DaemonConfig; cost
mode string is now the single source of truth for all thresholds
- maybe_compact_or_rollover() derives thresholds from CostMode, not static
config fields
- compact_oldest_turns() takes a keep parameter (derived from cost mode)
- compact_tool_result() wired into build_prompt_messages() — tool results
are truncated when cost mode says to compact
- trim_to_budget() called in build_prompt_assembly()
- auto_escalate() wired into session_rotation() — escalates cost mode
when compaction is insufficient
- set-cost-mode socket command now updates runtime cost_mode (RwLock on
DaemonState) instead of just acknowledging
Priority 2 — Pi spawn path end-to-end:
- poll_tasks() now queries claimed tasks, spawns the configured agent
binary (COLIBRI_AGENT_BINARY), creates a session, wires stdout to
glasspane, and transitions the task to Started
- stream_agent_stdout_to_glasspane made pub for cross-module access
- poll_tasks called from scheduler_tick_fn after the scheduler runs
- New integration test: poll_tasks_spawns_agent_for_claimed_task validates
the full path: create task → claim → poll_tasks spawns → glasspane
observes Idle → Working → Blocked → Done lifecycle
Gates: fmt/clippy/test all green (207 tests, 0 failures).
Unbreaks the workspace clippy gate: prepare_spawn_command has 8 args (8/7), so
clippy::too_many_arguments fails under -D warnings on main.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- scripts/headroom-sidecar.py: Unix socket server (from headroom import compress)
- cost.rs: HeadroomSidecar struct with connect/compress methods
- session.rs: build_prompt_messages() now accepts optional sidecar
- daemon.rs: spawns sidecar on startup if COLIBRI_HEADROOM_ENABLED=true
- config.rs: headroom_enabled + headroom_socket_path config fields
- socket.rs: cmd_status reports headroom status, cmd_get_session uses sidecar
- All test fixtures updated with new DaemonConfig fields
40-50% token savings on tool outputs with zero accuracy loss.
Disabled by default (COLIBRI_HEADROOM_ENABLED=false).
Works identically on Linux and FreeBSD.
Use the clawdie service user in the generated FreeBSD rc.d script, chown state directories after the user is created, and reject unknown existing ZFS pools before rendering/applying a plan. Update the FreeBSD validation handoff to cover these checks.\n\nFreeBSD checks: cargo fmt --check; ./scripts/check-format.sh; git diff --check; cargo test -p clawdie -- --nocapture; cargo clippy -p clawdie --all-targets -- -D warnings; cargo build -p clawdie --release; target/release/clawdie discover; target/release/clawdie plan; target/release/clawdie apply --pool zroot (dry-run); target/release/clawdie plan --pool does-not-exist (expected error).
clawdie chooses storage per host:
- FreeBSD: ZFS required (datasets under the pool)
- Linux with ZFS + a pool: datasets under the pool
- Linux without ZFS: plain-dir fallback, reporting ZFS benefits + spare disks
- --create-pool /dev/DEV runs `zpool create` (needs --pool NAME)
Pool creation is destructive and guarded: refused unless the disk is detected
empty (no partitions/filesystem/mount, not the root disk) or --force is given,
and only with --yes. `discover` lists block devices with candidacy. New
disk-candidacy parser + storage resolver are unit-tested (13 tests).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace inherited env delivery for jailed agent and external MCP spawns with staged launcher/env files under the jail-visible root. Add JailConfig.root_path for named jails that need staged payload delivery.
Tests: pass — cargo fmt --all; cargo test -p colibri-daemon jail_tests -- --nocapture; cargo test -p colibri-mcp -- --nocapture
New crates/clawdie binary. Discovers a host's ZFS layout and provisions the
clawdie service, cross-platform via a Platform backend (FreeBSD rc.d + native
ZFS; Linux systemd + ZFS-on-Linux).
- discover: read-only OS + pool/dataset inspection
- plan: render the ZFS layout + service-install steps (dry-run)
- apply: executes the plan, and only with --yes (dry-run otherwise)
apply writes to disk only with --yes. Discovery + plan logic is unit-tested (7);
the disk-touching path must be validated on real hosts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Salvages Codex's FreeBSD-validated fix from
fix/jail-spawner-freebsd-command-format, taking ONLY the spawner.rs change onto
current main. (The original branch also re-ran a markdown formatter and would
have re-corrupted the jailed-spawn design doc + README, so those are dropped.)
My merged jail_wrap emitted the ephemeral jail command as two argv tokens
("command", binary), but jail(8) expects a single name=value parameter
(command=<binary>). Without this the `jail -c` ephemeral path fails on FreeBSD.
Fixes the ephemeral builder and the two jail_tests expectations.
Co-authored-by: Sam & Codex (fix/jail-spawner-freebsd-command-format)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.