Adds the canonical ADR referenced by build.cfg and the consolidation work
(from docs/adr-agent-harness-consolidation, which merges clean). Prepends a
dated Update note: the ADR's "remove Pi" guidance is superseded — Pi is DEMOTED
to a spawnable backend (kept on-image, Node stays), zot is the primary harness,
per docs/COLIBRI-JAILED-AGENT-SPAWN-DESIGN.md. Original record preserved.
Co-authored-by: Sam & Claude (original ADR)
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>
A prettier-style pass in #36 mangled the jailed-spawn design doc — `mac_do`
became `mac*do` (eating the underscore and opening stray italics) and the
`_which_` / `_not_` emphasis turned into broken `\_which*` / `\_not*`. Restore
the text and wrap `mac_do` in backticks so a future formatter leaves it alone.
Also correct the README status line ("11 crates" → "10 crates") to match the
workspace table; clawdie was removed in #34 and #36 added no new crate.
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>
Colibri already spawns pi (spawner.rs) and captures its JSONL for glasspane;
this documents adding optional jail confinement to that existing path rather
than touching zot (whose swarm is self-only + no isolation — keeps the mirror
clean).
Covers: JailConfig + jail_wrap at the Command::new site, jail-aware teardown,
and the privilege decision for the root-only jexec step —
- live USB → `mdo -u root` (reuses mac_do; daemon == operator trust domain)
- deployed → setuid/Capsicum helper (narrow root surface on exposed hosts)
mac_do rules are identity-based (gid=0>uid=0), not command-filtered, so mdo
grants the daemon full root; that's acceptable on the single-operator live USB
but not on a deployed/exposed box, hence the split. Selected via PrivMode at
daemon config time so one spawner serves both.
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>
Remove the transition-era docs that no longer guide anyone and just pollute
context: MIGRATION-INVENTORY, CALLER-INVENTORY, GATE5-MIGRATION-GRAPH,
COLIBRI-CUTOVER-PLAN, and the rolling .agent-handoff.md. (History stays in git.)
Fix references in kept docs: drop the cutover/caller pointers (README,
COLIBRI-DAEMON-GLASSPANE-INTEGRATION), and repoint handoff mentions (AGENTS,
tools/README, MULTIAGENT-WORKFLOW-IMPROVEMENTS) to the ephemeral per-task
`doc/<FEATURE>-HANDOFF.md` convention. Dated session logs left as historical
record. Markdown gate green.
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>
colibri had no CI, so a non-compiling main slipped through (the build-break PR
#23 had to fix) and the markdown gate from #22 was honor-system only.
- .forgejo/workflows/ci.yml: runs on push-to-main + PRs. Two jobs — rust
(cargo fmt --check, clippy -D warnings, cargo test --workspace) and markdown
(./scripts/check-format.sh).
- scripts/ci-checks.sh: same gates in one script, runnable locally before
pushing (the workflow and humans share it).
Validated: scripts/ci-checks.sh passes end-to-end on this branch (exit 0).
NOTE: requires a registered Forgejo Actions runner with an `ubuntu-latest`
label that can pull the rust/node images. Adjust runs-on to match the runner.
Stacked on `all-checks-green` (the fmt fix) so the first CI run is green; merge
that PR first.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`cargo fmt --check` was failing on main in two spots:
- crates/colibri-client/src/bin/colibri.rs (register-skill error arm)
- crates/colibri-contracts/tests/golden.rs (USB_INV const)
Ran `cargo fmt` to resolve them. No behavior change. With this, the whole
project is green: fmt, clippy (-D warnings), tests (workspace), and the markdown
gate — so the CI workflow (separate PR) lands green on arrival.
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>
colibri had no Prettier config or gate, so its markdown drifted freely (22/31
files failed Prettier). Mirror the clawdie-iso gate so docs stay consistent:
- .prettierrc: same as clawdie-iso — proseWrap=preserve, printWidth=80, and
embeddedLanguageFormatting=off for *.md so fenced code (JSON/mermaid/shell in
the graph + design docs) is left exactly as written.
- .prettierignore: target/, scratch dirs, CHANGELOG.
- scripts/check-format.sh: `prettier@3 --check '**/*.md'` (run before pushing).
- AGENTS.md: "Markdown Formatting Gate" section documenting the workflow.
- One-shot `prettier --write` across all markdown. Pure formatting — only
emphasis-marker (*x* -> _x_), list-bullet, table-padding, and blank-line
normalization; no prose/command/code-fence content changed.
Gate now green (`./scripts/check-format.sh` → all matched files pass).
Docs-only + tooling — no Rust touched, no rebuild.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
Follow-up to #17. Two issues with the daemon(8) `-r` + child-pidfile pattern:
1. Stop semantics (both services): with `-r`, rc.subr's default stop sends
SIGTERM to the *child* pid — and the still-running daemon(8) supervisor
respawns it ~1s later, so `service … stop` never actually stops it. Fix:
add a `-P` supervisor pidfile and a custom stop_cmd that SIGTERMs the
supervisor (which forwards to the child and exits without restarting),
waits up to 30s, SIGKILL fallback, then cleans pidfiles. Child pidfile +
unique procname are kept for accurate start/status.
2. clawdie.in parity: it still carried the pre-#17 pattern (`-P ${pidfile}`
as the only pidfile + procname="/usr/sbin/daemon"), so `service clawdie
status` could match tailscaled/colibri_daemon on a stale pidfile. Brought
it to the same shape as colibri_daemon.in: child pidfile, procname="clawdie",
supervisor pidfile, stop_cmd, socket-ready poststart, socket cleanup
poststop, and a `health` command.
Packaging-only — no Rust touched, no rebuild needed. `sh -n` clean on both;
stop algorithm exercised standalone (kills supervisor, idempotent). FreeBSD
start/stop/status/restart validation still owed on OSA.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fix three bugs identified in live USB diagnostics (COLIBRI-XFCE-HANDOFF-04.JUN.2026):
1. procname collision: 'colibri-daemon' instead of '/usr/sbin/daemon'
so service status finds OUR process, not tailscaled or any other
daemon(8)-managed service.
2. pidfile flag: -p (child pidfile) instead of -P (supervisor pidfile)
so the pidfile holds the colibri-daemon PID, which rc.subr can
match against the unique procname.
3. Cascading bypass: fixing procname prevents rc.subr from skipping
daemon(8) entirely. Process tree should now be:
rc.d → daemon(8) → colibri-daemon (with crash restart)
Run Prettier on the PR #15 tokenomics doc after the clawdie scope and model-name fixes.\n\nChecks: npx --yes prettier@3 --check docs/COLIBRI-TOKENOMICS-TRIFECTA.md; cargo fmt --check; git diff --check.
Per Claude review: the tokenomics doc implied cost-modes/metering as
universal Colibri behaviour, but the clawdie lane deliberately strips
all of it. Added explicit scope block referencing CLAWDIE-AGENT-WIKI.md.
Also aligned example model name deepseek-v4-flash with harness docs.