OXYGENHUB

flow runner register keeps the CA in the Flow home by absolute path, and a relative ca in runner.toml is read against the home, so a service finds it from any directory (closes #98) 0d663e91c598

Author
Andy McCall @andymccall <[email protected]>
via
agent agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key ed9cee5e545f
When
· 2026-10-27 18:22 UTC
Signature
◈ sealed by agent:claude@andymccall
Hash
0d663e91c598d7d2f8622364d706a06216b0a3e72062d9fbd8e972012c8aaf43
Depends on
1b2229dab1a4 46fe2c815cc3 47c1d64dec8a 5544c8998aa7 bb643c252515 c3db750ed96e e0918b887850 f98a8f7881e0
Depended on by
1a50d4dae89c 060d459ff6c6
In bookmarks
main
In tags
v0.5.1 v0.5.2 v0.5.3
Edits
9 atoms, 0 tree operations, 3 files

Browse the tree at this change

modified CHANGELOG.md +2 −0
…
3 unchanged lines
4 4
5 5
## Unreleased
6 6
7 +
- **`flow runner register --ca` keeps the CA in the Flow home (#98):** it wrote the path as given, so `--ca hub-ca.pem` left `ca = "hub-ca.pem"` in runner.toml and a service started elsewhere could not find it. Register now copies the file to the home's `hub-ca.pem` and records that copy's absolute path, and a relative `ca` already in runner.toml is read against the home.
8 +
7 9
- **The cheap checks run as WASM (#96):** `layout`, `todos`, `size` and `summary` are one WASI module, `crates/flow-checks`, built by `scripts/checks-wasm.sh` and committed as `.flow/checks.wasm`, so they need no image or microVM and any runner with the WASM tier takes them (the phone, plex, windows-1); each runs in under 100 ms. The fmt job fails unless the committed module is what the source builds, and the build image has the wasm32-wasip1 target for that.
8 10
9 11
## 0.5.0 (2026-10-27)
…
118 unchanged lines
modified crates/flow/src/home.rs +20 −1
…
39 unchanged lines
40 40
/// Where `flow runner register` keeps the runner's hub and credential.
41 41
pub const RUNNER_TOML: &str = "runner.toml";
42 42
43 +
/// The hub's CA certificate as `flow runner register --ca` keeps it (#98).
44 +
pub const CA_PEM: &str = "hub-ca.pem";
45 +
43 46
#[derive(Clone, Debug, Deserialize)]
44 47
pub struct RunnerConfig {
45 48
    pub hub: String,
…
121 unchanged lines
167 170
        let registered = root.join(RUNNER_TOML);
168 171
        if config.runner.is_none() && registered.is_file() {
169 172
            let text = std::fs::read_to_string(&registered).with_context(|| format!("read {}", registered.display()))?;
170 -
            config.runner = Some(toml::from_str(&text).with_context(|| format!("parse {}", registered.display()))?);
173 +
            let mut runner: RunnerConfig = toml::from_str(&text).with_context(|| format!("parse {}", registered.display()))?;
174 +
            // A relative CA there is the home's (#98): register keeps its
175 +
            // copy there, and the service may start in any directory.
176 +
            runner.ca = runner.ca.map(|ca| if ca.is_absolute() { ca } else { root.join(ca) });
177 +
            config.runner = Some(runner);
171 178
        }
172 179
        for d in ["cas", "work", "repos"] {
173 180
            std::fs::create_dir_all(root.join(d)).with_context(|| format!("create {}", root.join(d).display()))?;
…
125 unchanged lines
299 306
        assert_eq!(a.identity, b.identity, "the same key after a restart");
300 307
        let sig = b.secret.sign(oxy_core::identity::Domain::Attest, &b.identity.hash(), &oxy_core::hash::ObjectRef([1; 32]));
301 308
        oxy_core::identity::verify(&a.identity, &sig, oxy_core::identity::Domain::Attest, &oxy_core::hash::ObjectRef([1; 32])).unwrap();
309 +
    }
310 +
311 +
    /// #98: runner.toml's relative CA is the home's, wherever flow starts.
312 +
    #[test]
313 +
    fn a_registered_runners_relative_ca_is_in_its_home() {
314 +
        let dir = tempfile::tempdir().unwrap();
315 +
        std::fs::write(dir.path().join(RUNNER_TOML), "hub = \"https://hub\"\ntoken = \"t\"\nca = \"hub-ca.pem\"\n").unwrap();
316 +
        let home = Home::locate(Some(dir.path())).unwrap();
317 +
        assert_eq!(home.config.runner.unwrap().ca, Some(dir.path().join("hub-ca.pem")));
318 +
        std::fs::write(dir.path().join(RUNNER_TOML), "hub = \"https://hub\"\ntoken = \"t\"\nca = \"/etc/ssl/hub.pem\"\n").unwrap();
319 +
        let home = Home::locate(Some(dir.path())).unwrap();
320 +
        assert_eq!(home.config.runner.unwrap().ca, Some(PathBuf::from("/etc/ssl/hub.pem")));
302 321
    }
303 322
}
modified crates/flow/src/runner.rs +30 −1
…
399 unchanged lines
400 400
        got.credential, got.name
401 401
    );
402 402
    if let Some(ca) = &ca {
403 -
        text.push_str(&format!("ca = {:?}\n", ca.display().to_string()));
403 +
        let kept = keep_ca(&home.root, ca)?;
404 +
        text.push_str(&format!("ca = {:?}\n", kept.display().to_string()));
404 405
    }
405 406
    if !labels.is_empty() {
406 407
        text.push_str(&format!("labels = {labels:?}\n"));
…
10 unchanged lines
417 418
    Ok(got)
418 419
}
419 420
421 +
/// Copy the hub's CA certificate `ca` into the Flow home `root` (#98) and
422 +
/// give the copy's absolute path, for runner.toml: a service starts in
423 +
/// another directory, and the file given may move or go.
424 +
fn keep_ca(root: &Path, ca: &Path) -> Result<PathBuf> {
425 +
    let kept = root.join(crate::home::CA_PEM);
426 +
    let same = matches!((std::fs::canonicalize(ca), std::fs::canonicalize(&kept)), (Ok(a), Ok(b)) if a == b);
427 +
    if !same {
428 +
        std::fs::copy(ca, &kept).with_context(|| format!("copy {} into {}", ca.display(), root.display()))?;
429 +
    }
430 +
    Ok(std::fs::canonicalize(&kept)?)
431 +
}
432 +
420 433
/// Waits before trying a hub request again after a server error.
421 434
const RETRY_DELAYS: [Duration; 3] = [Duration::from_secs(1), Duration::from_secs(2), Duration::from_secs(4)];
422 435
…
1352 unchanged lines
1775 1788
1776 1789
#[cfg(test)]
1777 1790
mod tests {
1791 +
1792 +
    /// #98: register keeps a copy of the CA in the home, by absolute path,
1793 +
    /// and keeping the home's own copy again is a no-op.
1794 +
    #[test]
1795 +
    fn the_ca_is_kept_in_the_home_by_absolute_path() {
1796 +
        let home = tempfile::tempdir().unwrap();
1797 +
        let elsewhere = tempfile::tempdir().unwrap();
1798 +
        let given = elsewhere.path().join("ca.pem");
1799 +
        std::fs::write(&given, "PEM").unwrap();
1800 +
        let kept = super::keep_ca(home.path(), &given).unwrap();
1801 +
        assert!(kept.is_absolute() && kept.ends_with(crate::home::CA_PEM), "{kept:?}");
1802 +
        assert_eq!(std::fs::read_to_string(&kept).unwrap(), "PEM");
1803 +
        std::fs::remove_file(&given).unwrap();
1804 +
        assert_eq!(super::keep_ca(home.path(), &kept).unwrap(), kept, "the home's copy again");
1805 +
        assert_eq!(std::fs::read_to_string(&kept).unwrap(), "PEM");
1806 +
    }
1778 1807
1779 1808
    #[test]
1780 1809
    fn an_input_is_placed_only_when_it_is_what_the_receipt_names() {
…
546 unchanged lines