Closes#135. The daemon stages per-spawn launch.sh/env.sh under the jail root;
the previous location /var/run/colibri-stage is root-owned, so the daemon
(running as clawdie) could not create per-spawn subdirs there — the second
jail-spawn EACCES, worked around in #134 by pre-creating the dir in
agent-jail-bootstrap.sh.
Move the default staging root to the daemon user's home,
/home/clawdie/.cache/colibri/stage, which clawdie owns by construction of the
jail account. create_dir_all now succeeds with no privileged pre-creation step,
and /home is persistent (unlike a tmpfs /var/run). The path is overridable via
COLIBRI_JAIL_STAGE_DIR, matching the daemon's other env-configurable paths.
- spawner.rs: const → staged_jail_run_dir() resolver; updated unit test.
- agent-jail-bootstrap.sh: drop the now-unnecessary install -d staging block
and DAEMON_USER var (the #134 workaround).
- docs: update jailed-spawn design + truss analysis to the new location.
clippy clean; spawner suite green (21 tests); sh -n clean; touched docs pass
the markdown gate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Second root cause of the jail-spawn EACCES (found via truss, docs PR #132):
for staged spawns the daemon writes launch.sh/env.sh under
<jail_root>/var/run/colibri-stage/<stage_id>/, but nothing created
/var/run/colibri-stage. The daemon runs as clawdie and cannot mkdir under
root-owned /var/run, so staging failed with Permission denied.
agent-jail-bootstrap.sh now pre-creates the dir owned by the daemon user
(0700), replacing the runtime `chmod 777` workaround — durable across jail
rebuilds and not world-writable (staged files are sourced as shell, so a
world-writable staging dir would be a privilege footgun). DAEMON_USER is
overridable, defaulting to clawdie.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Second root cause of the jail-spawn EACCES (found via truss, docs PR #132):
for staged spawns the daemon writes launch.sh/env.sh under
<jail_root>/var/run/colibri-stage/<stage_id>/, but nothing created
/var/run/colibri-stage. The daemon runs as clawdie and cannot mkdir under
root-owned /var/run, so staging failed with Permission denied.
agent-jail-bootstrap.sh now pre-creates the dir owned by the daemon user
(0700), replacing the runtime `chmod 777` workaround — durable across jail
rebuilds and not world-writable (staged files are sourced as shell, so a
world-writable staging dir would be a privilege footgun). DAEMON_USER is
overridable, defaulting to clawdie.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two root causes found via truss:
1. Bare command names (sudo, jexec) unresolved under daemon(8) PATH
→ fixed by resolve_program() in PR #131
2. Jail staging directory owned by root, unwritable by clawdie
→ fixed by chmod 777 <jail_root>/var/run/colibri-stage
Trace saved at /tmp/daemon.truss (1964 lines, successful spawn).
The jail spawn path launches its wrapper by bare name (sudo / jexec / mdo)
and relies on execvp + the daemon's inherited PATH. Under daemon(8)/rc the
PATH is often empty or reordered, so execvp either misses the binary (ENOENT)
or hits a non-executable same-named entry first and returns EACCES — the spawn
"Permission denied" seen on FreeBSD even though the identical command runs from
a shell.
- resolve_program() absolutizes a bare program name against a fixed search
list (first regular executable wins), leaving slash-bearing paths untouched
and falling back to the bare name so the OS still reports a real error.
- spawn_prepared_child now logs the resolved program, requested name, full
argv, and PATH before spawning. The previous "attempting spawn" log carried
no spawn-context detail, which is why the failure was opaque.
This removes the PATH-search EACCES as a variable so a truss/ktrace run can
attribute any remaining denial to an actual kernel/MAC policy instead.
Tests: resolve_program pass-through, absolutization, and missing-name fallback.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes#122. Creates packaging/freebsd/clawdie-npm-profile.sh as
the single source for npm PATH + npm config. The agent-jail
bootstrap installs it with NPM_PREFIX baked in, replacing the
inline heredoc. The clawdie-iso build.sh installs the same file.
Before: two divergent heredocs, different filenames, different
prefixes. Now: one file, both environments, parameterized prefix.
Move the backoff spawn operation into a named async helper so older tooling does not trip over || async syntax, and add a jail sudo wrapping unit test. Document sudo as an interim validated-host privilege mode.\n\nValidation: ./scripts/check-format.sh; cargo fmt --check; cargo check -p colibri-daemon; cargo test -p colibri-daemon jail_tests -- --nocapture.
Uses 'sudo -n' to wrap jail commands. Set via
COLIBRI_JAIL_PRIV_MODE=sudo. Requires sudoers entry:
clawdie ALL=(root) NOPASSWD: /usr/sbin/jexec *
The daemon's async spawn closure (edition 2015) may need a
follow-up to fully use this mode — the env var and wrapping
logic are correct, verified via manual jexec test.
Two changes to the clawdie deploy binary:
1. Service user renamed from 'clawdie' to '_clawdie' — follows FreeBSD
daemon convention (underscore prefix). Avoids collision with the
operator's interactive 'clawdie' user on existing hosts like OSA.
2. User creation is now idempotent — exit code 65 (pw: user already
exists) is treated as success via the new allowed_exit_codes field
on Action::Run. Deploy can safely re-run without failing.
Full end-to-end test on OSA file-backed pool: all 7 steps (ZFS
datasets, user, chown, rc.d write, sysrc enable) complete.
1. VAULT-PROVISION-FIRST-PROOF.md — refresh to the clean CLI now that the
three gaps are closed (#101/#102 via PR #107; #92 via PR #119):
- Step 3: raw SQLite INSERT →
- Step 4: raw JSON →
- Status header: mark all three closed; note the proof validates the
production deployment pattern (bare-metal Clawdie service runs this model)
- Chain-resolution section: document the #92/#119 containment guard
(canonicalize + assert under COLIBRI_JAIL_ROOT_BASE before any write)
- Follow-ups: record what landed vs. what's still open (no delete-tenant
verb; CI runner intermittently down)
2. Sweep markdown corruption introduced by #126 (merged while CI runner was
down, so the prettier gate never ran):
- AGENTS.md — prettier reflow
- COLIBRI-SKILLS-PLAN.md — Ownership table had a row split across two
lines ('consumer.' orphan + a duplicated Agents row); restored to 5
clean logical rows
Checks: npx prettier@3 --check across all docs + AGENTS.md + README.md →
0 warnings; cargo fmt --check clean.
Co-Authored-By: Hermes & Sam <hello@clawdie.si>
PR #124 applied the positive-instruction-framing convention across docs but
was self-merged without the markdown format gate, leaving 6 files failing
prettier and a few structural defects. This repairs them:
- prettier --write on the 6 files that failed ./scripts/check-format.sh
(AGENTS.md, CLAWDIE-STUDIO-PROPOSAL, COLIBRI-SKILLS-PLAN, HEADROOM-SIDECAR,
MULTI-AGENT-HOST-PLAN, VAULT-PROVISION-FIRST-PROOF).
- COLIBRI-SKILLS-PLAN.md: fix a table row split across two lines by a stray
newline injected mid-cell.
- CLAWDIE-STUDIO-PROPOSAL.md: remove an orphaned "together." left dangling
by a reworded sentence; restore the editor-bridge (MCP) guardrail bullet
that was dropped, reworded positively; restore the guardrail list structure.
- CLAWDIE-STUDIO-PROPOSAL.md: plain-language the three implementation
guardrails (MCP foundation, opt-in/guarded tools, set-cost-mode scope).
./scripts/check-format.sh -> green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Convert 'do not', 'cannot', 'never', 'avoid', 'don't' patterns across
AGENTS.md, README.md, and 11 docs/*.md files into positive,
actionable instructions that tell the reader what TO do.
Preserved: hard safety constraints (MUST NOT agent boundaries,
vault credential confinement intent) — these are enforceable
guardrails where the prohibition IS the instruction.
No repo AGENTS.md noted the AGPL->MIT relicense or the unified 0.11.0
version. Record both in colibri's Project Identity so contributors see the
current license/version without digging through Cargo.toml.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root cause of the recurring "pi/bw not found in jail" bug: the npm-global-on-PATH
fix was solved canonically in the clawdie-iso image (/etc/profile.d/clawdie.sh,
all login shells), but the agent jail is a separate environment that never reused
it — a fresh Bastille jail doesn't inherit the image's profile.d, and the
bootstrap set no PATH. PR #120 band-aided it with a hardcoded append to one
user's ~/.profile (sh-only, drifts from NPM_PREFIX).
Replace that band-aid with the same mechanism the image uses, scoped to the jail:
- write one managed /etc/profile.d/clawdie-npm.sh derived from NPM_PREFIX
- source it from /etc/profile (covers all sh/bash login shells, system-wide),
idempotently
- delete the per-user ~/.profile append from #120
Now the PATH content lives in a single file tied to NPM_PREFIX, so it can't miss
shells or drift from the prefix. Follow-up (not here): hoist the snippet into one
shared file installed by both clawdie-iso and the jail bootstrap, so a future new
environment can't re-grow this.
Verified: sh -n clean; smoke test — snippet expands NPM_PREFIX (keeps $PATH
literal), /etc/profile sources it, append is idempotent, sourced shell resolves
the npm-global bin onto PATH.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PR #91 added a string-equality registered-vs-spawned root check, which doesn't
catch `..`, symlinks, or a root pointing outside the jails tree. Add a real
containment guard in colibri-vault::provision, the layer that writes the .env:
- Before create_dir_all, canonicalize the target (resolving `..`/symlinks) and
assert it is STRICTLY under the allowed jail-root base; refuse otherwise.
Running before create_dir_all means a traversal/symlink target can't even
create a directory outside the tree, let alone an .env.
- Allowed base defaults to /usr/local/bastille/jails (FreeBSD/Bastille),
overridable via COLIBRI_JAIL_ROOT_BASE for Linux/Docker volume roots.
- Fail-closed: returns VaultError::TargetEscapesRoot; the daemon spawn hook
already treats provision errors as fail-soft (no .env written).
- Tests: child accepted; base-itself / nonexistent / `..`-escape / symlink-escape
all refused (no tempfile dep — uses std temp_dir).
Acceptance (#92): a target with `..`, a symlink, or resolving outside the jail
root is refused, no .env written. fmt + clippy --all-targets clean;
cargo test --workspace 230 passed / 0 failed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes the gap left after #70/PR #81: the agent-jail package set is hand-synced
across two repos (this bootstrap's PKGS= and clawdie-iso pkg-list-jails.txt
"# agent-jail" section) with nothing catching future drift.
- check-agent-jail-pkgs.sh: pure POSIX sh; extracts PKGS= here, fetches the
clawdie-iso list over HTTP (ISO_PKG_LIST_URL overridable), diffs the two sets,
reports the delta, exits non-zero on mismatch.
- ci.yml: new `agent-jail-pkgs` job runs it on every push/PR.
Same shape as the CARGO_CRATES drift check. Verified: green in sync (5 pkgs);
negative test flags missing packages and exits 1; ci.yml valid YAML.
Single-sided (fires on colibri CI); the clawdie-iso list is fetched from main.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Version: unify colibri with the Clawdie release version 0.11.0 (matches
clawdie-iso ISO_VERSION). Cargo.toml 0.0.1 -> 0.11.0, Cargo.lock refreshed,
port DISTVERSION 0.0.1 -> 0.11.0, port README example tag v0.11.0.
License: relicense all 12 crates from AGPL-3.0-only to MIT, matching the rest of
the project (layered-soul is MIT; nothing was BSD-3). Add a LICENSE file with
the same MIT text + holder (clawdie, 2026). Port: LICENSE=MIT + LICENSE_FILE.
Validation: CARGO_CRATES drift check green (346); markdown gate clean; no AGPL
references remain. Edition stays 2021 (2024 migration is a separate tested task).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make the canonical sysutils/colibri port install its rc.d services so
`pkg install colibri` registers them — the one functional bit the (now-retiring)
clawdie-iso port duplicate carried. Poudriere builds in a clean jail that only
sees the port dir, so the rc.d templates live in files/ (mirrored from the
canonical packaging/freebsd/ copies).
- files/colibri_daemon.in, files/colibri_bridge.in (rc.d templates)
- do-install: INSTALL_SCRIPT both into PREFIX/etc/rc.d/ (binary path
PREFIX/bin/colibri-daemon already matches the daemon rc.d expectation)
- pkg-plist: add the two etc/rc.d entries
- README: document files/ + that this is the single canonical port
Validation: rc.d sh -n clean; CARGO_CRATES drift check green (346); markdown
gate clean. Port remains poudriere-build-unproven until the first mother-build run.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a top-of-file usage banner showing how to run it: `sh ...` / `./...`.
It is a POSIX shell wrapper that calls python3 internally. Comment-only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up to #109 (which generated the 346-crate CARGO_CRATES block). Make that
list self-maintaining so it can't silently drift from the source deps:
- check-cargo-crates.sh: parses Cargo.lock (registry crates only; skips the 13
workspace-local crates and any git deps) and diffs against the Makefile's
CARGO_CRATES block. Reports MISSING / STALE, exits non-zero on drift. No
network, pure tomllib — runs on any host. Independently confirms #109's list
is complete and correct (346/346 in sync).
- ci.yml: new `port` job (python:3.12) runs the check on every push/PR, so a
dependency change that forgets `make cargo-crates` fails CI.
- Makefile: replace the stale "Empty in this draft" comment (CARGO_CRATES is now
populated) with accurate regenerate/verify guidance.
- README: CARGO_CRATES is committed now (only distinfo is build-host-generated);
document the checker and trim the build steps.
Verified: checker green at 346 crates; both drift directions (missing/stale)
detected in negative tests; ci.yml is valid YAML; port README prettier-clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
docs/MULTI-AGENT-HOST-PLAN.md and docs/README.md had table-column formatting
drift that fails the markdown CI gate (prettier --check '**/*.md') on main.
Formatting only — pure table-padding re-alignment, no content change. Unblocks
the markdown job so the CI pipeline goes green again.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>