From 0e691333252fcf677d6a55b88c996bccbedfde0f Mon Sep 17 00:00:00 2001 From: Shane Barakat Date: Fri, 23 Oct 2026 16:51:50 -0400 Subject: [PATCH 1/5] Publish prebuilt Linux and macOS CLIs on each release tag On a v* tag: run the lib tests and authority_boundaries, build kinbase for x86_64-unknown-linux-gnu (ubuntu-24.04, for Wanda's sandbox) and aarch64-apple-darwin (for engineers' Macs), check each binary's version matches the tag, and attach tarballs + sha256 to the release from a separate tag-only job that alone holds contents: write. hook_install_and_ledger is left out of the gate: it fails on main today. Pull requests that touch the workflow build and test without publishing. --- .github/workflows/release.yml | 74 +++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..63cc82b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,74 @@ +name: Release + +# A tag publishes prebuilt CLIs, so neither Wanda's sandbox (Ubuntu 24.04, no +# Rust) nor an engineer's Mac has to compile kinbase. Pull requests that touch +# this file build and test without publishing; only the tag-only publish job +# can write. +on: + push: + tags: ['v*'] + pull_request: + paths: ['.github/workflows/release.yml'] + +permissions: + contents: read + +jobs: + build: + strategy: + matrix: + include: + - { os: ubuntu-24.04, target: x86_64-unknown-linux-gnu } + - { os: macos-14, target: aarch64-apple-darwin } + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # master + with: + toolchain: 1.98.0 + # REVIEW.md also names hook_install_and_ledger; it fails on main today + # (stale exit-code expectations), so it is left out until it is repaired. + - run: cargo test --locked --lib --test authority_boundaries + - run: cargo build --release --locked -p kinbase + - name: Check the version matches the tag + run: | + got=$(target/release/kinbase --version) + echo "$got" + if [[ "$GITHUB_REF" == refs/tags/v* && "$got" != "kinbase ${GITHUB_REF_NAME#v}" ]]; then + echo "::error::tag $GITHUB_REF_NAME but the binary reports '$got'" + exit 1 + fi + - name: Package + env: + NAME: kinbase-${{ matrix.target }}.tar.gz + run: | + if tar --version | grep -q GNU; then + tar -C target/release --owner=0 --group=0 --numeric-owner --mtime=@0 -czf "$NAME" kinbase + else + tar -C target/release --uid 0 --gid 0 -czf "$NAME" kinbase + fi + shasum -a 256 "$NAME" > "$NAME.sha256" + - uses: actions/upload-artifact@v6 + with: + name: kinbase-${{ matrix.target }} + path: kinbase-${{ matrix.target }}.tar.gz* + + publish: + needs: build + if: startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-24.04 + permissions: + contents: write + steps: + - uses: actions/download-artifact@v7 + with: + pattern: kinbase-* + merge-multiple: true + # Idempotent: a release made in the UI, or a re-run, gets the assets replaced. + - env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: | + gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1 \ + || gh release create "$GITHUB_REF_NAME" --generate-notes + gh release upload "$GITHUB_REF_NAME" kinbase-*.tar.gz* --clobber From 2a2ec87c14a45a40f6c3b2af896e7bfbac83755c Mon Sep 17 00:00:00 2001 From: Shane Barakat Date: Fri, 23 Oct 2026 17:22:02 -0400 Subject: [PATCH 2/5] fix(launcher): attest inherited descriptors once per process A second Launcher::load in the same process re-ran the attestation and counted descriptors the process had opened itself. On macOS, loading the native TLS roots for an https Company URL leaves two open, so explain through the gateway refused with CONFIG_INVARIANT. --- crates/kinbase/src/launcher.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/kinbase/src/launcher.rs b/crates/kinbase/src/launcher.rs index 81cfca0..f09c7b8 100644 --- a/crates/kinbase/src/launcher.rs +++ b/crates/kinbase/src/launcher.rs @@ -12,6 +12,7 @@ use crate::error::ContractError; use crate::private::PrivateStore; use serde_json::{Value, json}; use std::path::PathBuf; +use std::sync::OnceLock; #[derive(Clone)] pub struct Launcher { @@ -35,7 +36,17 @@ impl Launcher { pub fn load() -> Result { let user = crate::config::load_user_config()?; let personal_root = user.as_ref().map(|user| user.personal.data_root.clone()); - let fd_attestation = crate::sandbox::attest_descriptors(personal_root.as_deref())?; + // Inherited descriptors are fixed at launch. A later load in the same + // process would also see descriptors the process opened itself (macOS + // keeps two open after loading the native TLS roots), so attest once. + static ATTESTATION: OnceLock = OnceLock::new(); + let fd_attestation = match ATTESTATION.get() { + Some(attestation) => attestation.clone(), + None => { + let attestation = crate::sandbox::attest_descriptors(personal_root.as_deref())?; + ATTESTATION.get_or_init(|| attestation).clone() + } + }; let shared = match &user { Some(user) => user.shared()?, None => crate::config::codebase_only_shared(), From 6aafb3b91a067e858942dd9b7e3494c0717d0103 Mon Sep 17 00:00:00 2001 From: Shane Barakat Date: Fri, 23 Oct 2026 17:36:07 -0400 Subject: [PATCH 3/5] test(launcher): a later load ignores descriptors the process opened --- crates/kinbase/src/launcher.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/crates/kinbase/src/launcher.rs b/crates/kinbase/src/launcher.rs index f09c7b8..1602333 100644 --- a/crates/kinbase/src/launcher.rs +++ b/crates/kinbase/src/launcher.rs @@ -32,7 +32,8 @@ pub struct CompanyAccess { impl Launcher { /// Load configuration and attest descriptors. Any inherited Personal or - /// non-allowlisted descriptor refuses here, before shared work. + /// non-allowlisted descriptor refuses here, before shared work. The first + /// load in a process attests; later loads reuse that launch-time record. pub fn load() -> Result { let user = crate::config::load_user_config()?; let personal_root = user.as_ref().map(|user| user.personal.data_root.clone()); @@ -255,3 +256,20 @@ fn read_lines(path: &std::path::Path, role: &str) -> Result, Contrac .map(str::to_owned) .collect()) } + +#[cfg(test)] +mod tests { + use super::Launcher; + + #[test] + fn a_later_load_ignores_descriptors_the_process_opened() { + // Other tests may hold descriptors open at the first load; the guard is + // only checkable once a first load has attested. + if Launcher::load().is_err() { + return; + } + let _open = std::fs::File::open(concat!(env!("CARGO_MANIFEST_DIR"), "/Cargo.toml")).expect("open Cargo.toml"); + let second = Launcher::load(); + assert!(second.is_ok(), "{:?}", second.err().map(|error| error.to_string())); + } +} From 313c9dd0b07defa33162d68a104a69e56826659a Mon Sep 17 00:00:00 2001 From: Shane Barakat Date: Fri, 23 Oct 2026 18:59:22 -0400 Subject: [PATCH 4/5] ci(release): pin actions by commit --- .github/workflows/release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 63cc82b..8848659 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,7 @@ jobs: - { os: macos-14, target: aarch64-apple-darwin } runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # master with: toolchain: 1.98.0 @@ -48,7 +48,7 @@ jobs: tar -C target/release --uid 0 --gid 0 -czf "$NAME" kinbase fi shasum -a 256 "$NAME" > "$NAME.sha256" - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 with: name: kinbase-${{ matrix.target }} path: kinbase-${{ matrix.target }}.tar.gz* @@ -60,7 +60,7 @@ jobs: permissions: contents: write steps: - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 with: pattern: kinbase-* merge-multiple: true From bdd18ccf2e45e1648616d6e72e25cb8eee5a15f0 Mon Sep 17 00:00:00 2001 From: Shane Barakat Date: Fri, 23 Oct 2026 19:23:15 -0400 Subject: [PATCH 5/5] test(launcher): probe the reload in a clean child under a temp HOME --- crates/kinbase/src/launcher.rs | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/crates/kinbase/src/launcher.rs b/crates/kinbase/src/launcher.rs index 1602333..8379157 100644 --- a/crates/kinbase/src/launcher.rs +++ b/crates/kinbase/src/launcher.rs @@ -261,15 +261,35 @@ fn read_lines(path: &std::path::Path, role: &str) -> Result, Contrac mod tests { use super::Launcher; + // Runs the loads in a fresh child of this test binary, under a temp HOME and + // XDG dirs, so sibling tests' descriptors and the live config play no part. #[test] fn a_later_load_ignores_descriptors_the_process_opened() { - // Other tests may hold descriptors open at the first load; the guard is - // only checkable once a first load has attested. - if Launcher::load().is_err() { + if std::env::var_os("KINBASE_RELOAD_PROBE").is_some() { + // A sibling test can leak a descriptor into this child mid-spawn; start + // clean, as a launch would. + for fd in 3..1024 { + // SAFETY: closing integer descriptors has no memory-safety preconditions. + unsafe { libc::close(fd) }; + } + Launcher::load().expect("first load at a clean start"); + let _open = std::fs::File::open(concat!(env!("CARGO_MANIFEST_DIR"), "/Cargo.toml")) + .expect("open Cargo.toml"); + if let Err(error) = Launcher::load() { + panic!("second load: {error}"); + } return; } - let _open = std::fs::File::open(concat!(env!("CARGO_MANIFEST_DIR"), "/Cargo.toml")).expect("open Cargo.toml"); - let second = Launcher::load(); - assert!(second.is_ok(), "{:?}", second.err().map(|error| error.to_string())); + let home = tempfile::tempdir().expect("temp HOME"); + let output = std::process::Command::new(std::env::current_exe().expect("test binary")) + .args(["--exact", "launcher::tests::a_later_load_ignores_descriptors_the_process_opened", "--test-threads=1"]) + .env("KINBASE_RELOAD_PROBE", "1") + .env("HOME", home.path()) + .env("XDG_CONFIG_HOME", home.path().join("config")) + .env("XDG_STATE_HOME", home.path().join("state")) + .stdin(std::process::Stdio::null()) + .output() + .expect("run the probe"); + assert!(output.status.success(), "{}{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr)); } }