Test staged-env shell-quoting helpers; rescope ISO priority 1 to boot validation #66

Merged
clawdie merged 1 commit from staged-env-tests-and-iso-priority-update into main 2026-06-14 15:07:50 +02:00
2 changed files with 109 additions and 49 deletions

View file

@ -966,3 +966,73 @@ mod jail_tests {
assert!(err.to_string().contains("requires JailConfig.root_path"));
}
}
#[cfg(test)]
mod staged_env_tests {
use super::*;
use std::collections::HashMap;
#[test]
fn shell_single_quote_plain() {
assert_eq!(shell_single_quote("bar"), "'bar'");
}
#[test]
fn shell_single_quote_escapes_single_quote() {
// a'b -> 'a'"'"'b' : close quote, escaped quote, reopen quote
assert_eq!(shell_single_quote("a'b"), r#"'a'"'"'b'"#);
}
#[test]
fn shell_single_quote_neutralizes_metachars() {
// Shell metacharacters stay literal inside the single quotes.
let payload = "$(rm -rf /); `id`; a&&b || c; $HOME";
let quoted = shell_single_quote(payload);
assert!(quoted.starts_with('\''));
assert!(quoted.ends_with('\''));
// No unescaped single quote ever appears, so the shell cannot break out.
assert_eq!(quoted, format!("'{payload}'"));
}
#[test]
fn valid_env_key_accepts_good_keys() {
for key in ["PATH", "_X", "A1", "COLIBRI_FOO", "_"] {
assert!(valid_env_key(key), "expected {key} to be valid");
}
}
#[test]
fn valid_env_key_rejects_bad_keys() {
for key in ["", "1A", "A-B", "A B", "A.B", "A=B"] {
assert!(!valid_env_key(key), "expected {key:?} to be rejected");
}
}
#[test]
fn build_env_script_sorts_and_quotes() {
let mut env = HashMap::new();
env.insert("FOO".to_string(), "bar".to_string());
env.insert("BAZ".to_string(), "x y".to_string());
let script = build_env_script(&env).unwrap();
assert_eq!(script, "export BAZ='x y'\nexport FOO='bar'\n");
}
#[test]
fn build_env_script_escapes_values() {
let mut env = HashMap::new();
env.insert("TOKEN".to_string(), "a'b".to_string());
let script = build_env_script(&env).unwrap();
assert_eq!(
script,
format!("export TOKEN={}\n", shell_single_quote("a'b"))
);
}
#[test]
fn build_env_script_rejects_invalid_key() {
let mut env = HashMap::new();
env.insert("BAD KEY".to_string(), "v".to_string());
let err = build_env_script(&env).unwrap_err();
assert!(err.to_string().contains("invalid env key"));
}
}

View file

@ -14,53 +14,39 @@ the final step. Items can be worked in parallel by different agents.
---
## Priority 1: Wire the ISO staging script into the clawdie-iso build
## Priority 1: Validate the staged colibri_daemon boots and runs on the ISO
### Why this is #1
`scripts/stage-colibri-iso.sh` already exists and copies binaries, rc.d,
directories, and a sample rc.conf into a DESTDIR. But it has **never been run
against the actual ISO image root**, and the clawdie-iso build process does
not call it. Without this wiring, colibri_daemon cannot boot on the ISO, which
blocks Gate 1 (passive service) entirely.
The build-side wiring is **done**. The clawdie-iso build now stages the
Colibri binaries, installs the rc.d script, creates the `colibri` user, and
enables the service. What has **not** happened yet is booting a freshly built
image and confirming `colibri_daemon` actually starts and the acceptance
runbook passes on real FreeBSD. Until that boot/runtime validation is done,
Gate 1 (passive service) is unproven.
### What exists
### What's done (build wiring)
| Artifact | Location | Status |
| ------------------ | ------------------------------------------ | ----------------------------------------------------------------------------------------------- |
| staging script | `scripts/stage-colibri-iso.sh` | done — copies `colibri-daemon`, `colibri`, `colibri-smoke-agent`, rc.d, newsyslog, creates dirs |
| rc.d script | `packaging/freebsd/colibri_daemon.in` | done — `start_precmd`, pidfile, daemon(8) wrapper, `COLIBRI_COST_MODE` propagation |
| newsyslog config | `packaging/freebsd/newsyslog-colibri.conf` | done |
| rc.conf.sample | generated by staging script | done |
| acceptance runbook | `docs/ISO-ACCEPTANCE-RUNBOOK.md` | done |
| Artifact / step | Location | Status |
| -------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------------------------- |
| staging script | `scripts/stage-colibri-iso.sh` | done — copies `colibri-daemon`, `colibri`, `colibri-smoke-agent`, rc.d, newsyslog, creates dirs |
| rc.d script | `packaging/freebsd/colibri_daemon.in` | done — `start_precmd`, pidfile, daemon(8) wrapper, `COLIBRI_COST_MODE` propagation |
| newsyslog config | `packaging/freebsd/newsyslog-colibri.conf` | done |
| rc.conf.sample | generated by staging script | done |
| acceptance runbook | `docs/ISO-ACCEPTANCE-RUNBOOK.md` | done |
| build integration | clawdie-iso `build.sh::install_colibri_service` | done — calls `stage-colibri-iso.sh` against the image root |
| `colibri` user/group | clawdie-iso `build.sh` (`pw useradd colibri`) | done — created in the image during build |
| service enable | clawdie-iso `build.sh` (`colibri_daemon_enable`) | done — written into image rc.conf |
| prebuilt binaries | build-host Rust toolchain (preflight-gated) | done — `build.sh` stages prebuilt release binaries and fails preflight if missing |
### What's missing
### What's missing (boot/runtime validation)
1. **`colibri` user/group creation in the image.**
The staging script documents this as a manual step in
`ETC_DIR/README.iso` but does not create the user. Options:
- Add `pw useradd`/`pw groupadd` to the staging script (requires root, or
runs during image build which has root)
- Add it to the clawdie-iso firstboot wizard
- Add it as an `etc/passwd`/`etc/group` entry in the image root
1. **Boot a freshly built image on FreeBSD** (bhyve VM or hardware) and confirm
the `colibri` user, binaries, rc.d script, and rc.conf entry are present in
the running system.
2. **clawdie-iso build integration.**
The clawdie-iso build process needs to:
- Cross-compile (or pre-build) `colibri-daemon` for `x86_64-unknown-freebsd`
- Call `stage-colibri-iso.sh` against the image root
- Merge `rc.conf.sample` into `/etc/rc.conf.d/colibri_daemon` or `/etc/rc.conf`
- Ensure the `colibri` user exists in `/etc/passwd` and `/etc/group` in the image
2. **Run the acceptance runbook on the booted image:**
3. **Pre-built FreeBSD binaries.**
Linux agents cannot produce FreeBSD binaries directly (no cross-compile
target in the workspace). Either:
- The FreeBSD agent (Codex) builds `cargo build --workspace --release` on
FreeBSD and the ISO build consumes `target/release/*.bin`
- A CI step on a FreeBSD runner produces the artifacts
- The ISO build host has Rust installed and builds in-place
4. **Verification on FreeBSD.**
After staging, run the acceptance runbook commands on the booted image:
```sh
service colibri_daemon start
colibri status
@ -72,19 +58,23 @@ blocks Gate 1 (passive service) entirely.
service colibri_daemon stop
```
3. **Confirm logging + lifecycle:** pidfile is created, newsyslog rotation
config is in place, and `service colibri_daemon stop` cleanly stops the
daemon and removes the pidfile.
### Key files
- `scripts/stage-colibri-iso.sh` — the staging script (line 44-93: dir creation, bin copy, rc.d install, rc.conf.sample generation)
- `scripts/stage-colibri-iso.sh` — the staging script (dir creation, bin copy, rc.d install, rc.conf.sample generation)
- `packaging/freebsd/colibri_daemon.in` — rc.d script
- `docs/ISO-ACCEPTANCE-RUNBOOK.md` — acceptance commands
- `docs/ISO-ACCEPTANCE-RUNBOOK.md` — acceptance commands to run on the booted image
- `docs/ISO-INTEGRATION-PLAN.md` §Lane A — full plan with gap audit
- clawdie-iso repo — image build scripts that need to call the staging script
- clawdie-iso `build.sh``install_colibri_service()` already wires staging, user creation, and service enable
### Suggested owner
ISO/build lane (Codex on FreeBSD for binary production + Sam for build
integration). Linux agents can prepare the staging script changes and
clawdie-iso build wiring.
ISO/build lane — FreeBSD agent (Codex) or Sam boots a built image and runs the
acceptance runbook. No Linux-side code change is required; this is a
runtime-proof step.
---
@ -284,11 +274,11 @@ wiring, no platform-specific behavior.
## Summary table
| # | Item | Blocks | Linux-doable | Effort |
| --- | --------------------- | ------------------- | ---------------------------------- | ------ |
| 1 | ISO staging wiring | Gate 1 | partially (needs FreeBSD binaries) | medium |
| 2 | Pi spawn end-to-end | Gate 2 | yes (with fake-pi-agent.py) | medium |
| 3 | Cost mode enforcement | core design promise | yes (pure logic) | medium |
| # | Item | Blocks | Linux-doable | Effort |
| --- | --------------------------- | ------------------- | --------------------------- | ------ |
| 1 | ISO boot/runtime validation | Gate 1 | no (needs FreeBSD boot) | small |
| 2 | Pi spawn end-to-end | Gate 2 | yes (with fake-pi-agent.py) | medium |
| 3 | Cost mode enforcement | core design promise | yes (pure logic) | medium |
All three are medium effort and can be worked in parallel. None require
FreeBSD to implement — only to validate the final result.