From 60bcf12f8a16d496dea20cbbe55b6762eff66887 Mon Sep 17 00:00:00 2001 From: Abraham George Date: Thu, 29 Oct 2026 21:50:27 +0530 Subject: [PATCH 1/2] Release builds carry the desktop window; a shell with no display is told plainly (R-832) release.yml builds tasks-cli with mcp and ui: the window loads its graphics libraries only when it opens (ldd shows libc, libm and libgcc_s alone), so one binary serves desktops and headless workers. The smoke test checks that --help lists the window as built in. CI's clippy and tests include ui, so a release is no longer the first build of it. On Linux, loadtable ui with none of DISPLAY, WAYLAND_DISPLAY, WAYLAND_SOCKET set says there is no display and names loadtable status and list --active, instead of winit's EventLoopError. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 6 ++-- .github/workflows/release.yml | 9 ++++-- AGENTS.md | 4 +-- README.md | 10 +++++-- crates/tasks-cli/src/ui.rs | 54 +++++++++++++++++++++++++++++++++++ docs/REGRESSIONS.md | 1 + docs/RELEASING.md | 2 -- 7 files changed, 73 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2290c17..78646c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,8 +27,8 @@ jobs: with: shared-key: ci-linux - run: cargo fmt --all --check - - run: cargo clippy --workspace --all-targets --locked --features tasks-cli/mcp -- -D warnings - - run: cargo test --workspace --locked --features tasks-cli/mcp --no-fail-fast + - run: cargo clippy --workspace --all-targets --locked --features tasks-cli/mcp,tasks-cli/ui -- -D warnings + - run: cargo test --workspace --locked --features tasks-cli/mcp,tasks-cli/ui --no-fail-fast # macOS is a release target (the launchd service, the sampler without cgroups), so it has to build # and pass clippy natively, not only as a cross-check. @@ -41,7 +41,7 @@ jobs: - uses: Swatinem/rust-cache@v2 with: shared-key: ci-macos - - run: cargo clippy --workspace --all-targets --locked --features tasks-cli/mcp -- -D warnings + - run: cargo clippy --workspace --all-targets --locked --features tasks-cli/mcp,tasks-cli/ui -- -D warnings - run: cargo clippy --workspace --all-targets --locked --no-default-features -- -D warnings # the confinement profile, run for real under sandbox-exec (R-821, R-831) - run: cargo test -p tasks-platform --locked --test confine_macos diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9cb433a..7fa4ebf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -75,10 +75,11 @@ jobs: - uses: Swatinem/rust-cache@v2 with: shared-key: release-${{ matrix.target }} - # The CLI carries the MCP server (agents start it as `loadtable mcp`). The desktop window - # (`--features ui`) is left out: it needs graphics libraries a headless worker does not have. + # The CLI carries the MCP server (agents start it as `loadtable mcp`) and the desktop window + # (`loadtable ui`). The window loads its graphics libraries only when it opens, so the same + # binary runs on a headless worker; there `loadtable ui` says there is no display (R-832). - name: Build loadtable and loadtabled - run: cargo build --release --locked --target "$TARGET" -p tasks-daemon -p tasks-cli --features tasks-cli/mcp + run: cargo build --release --locked --target "$TARGET" -p tasks-daemon -p tasks-cli --features tasks-cli/mcp,tasks-cli/ui - name: Package shell: bash run: | @@ -98,6 +99,8 @@ jobs: "$bin/loadtabled" --version | grep -q "($(git rev-parse --short HEAD) " "$bin/loadtable" --help >/dev/null "$bin/loadtabled" --help >/dev/null + # the window is built in: its help is not the "built without ui" stub (R-199) + "$bin/loadtable" --help | grep -q "The desktop window (built in)" - uses: actions/upload-artifact@v4 with: name: release-${{ matrix.target }} diff --git a/AGENTS.md b/AGENTS.md index 49694b2..5bc5013 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,9 +13,9 @@ never bare `cargo` in a shell: ```sh loadtable run --who YOUR_AGENT --anywhere -- cargo fmt --all --check -loadtable run --who YOUR_AGENT --anywhere -- cargo clippy --workspace --all-targets --features tasks-cli/mcp -j 6 -- -D warnings +loadtable run --who YOUR_AGENT --anywhere -- cargo clippy --workspace --all-targets --features tasks-cli/mcp,tasks-cli/ui -j 6 -- -D warnings loadtable run --who YOUR_AGENT --anywhere -- cargo test -p tasks-daemon --lib FILTER -j 6 -loadtable run --who YOUR_AGENT --anywhere -- cargo test --workspace --features tasks-cli/mcp -j 6 # the full suite, before merging +loadtable run --who YOUR_AGENT --anywhere -- cargo test --workspace --features tasks-cli/mcp,tasks-cli/ui -j 6 # the full suite, before merging ``` - Name your test target (`-p X --lib FILTER`, `--test NAME`); a bare filter is refused unless diff --git a/README.md b/README.md index ffbed94..6e20773 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,9 @@ documentation: . audit log, with exact per-job resource figures where cgroups are available. - **Rules that agents can't ignore.** Broad `cargo test` runs, multi-command shell scripts and denied devices are refused at submit with a message that says what to run instead. +- **A desktop window.** `loadtable ui` shows every machine and its queue, each job's log, the + history, timings, the audit log and pressure stalls. It is in the release binary; on a machine without a display + the same binary runs headless. - **Jobs from other machines are confined.** A job a peer sends runs under Landlock (Linux) or Seatbelt (macOS): it reaches the build cache and toolchains, not the rest of your home folder. @@ -27,13 +30,14 @@ documentation: . curl -fsSL https://loadtable.osvauld.com/install.sh | sh # Linux x86-64/arm64, macOS loadtabled install # the daemon as a user service loadtable status +loadtable ui # the desktop window ``` Or from source, with a recent stable Rust: ```sh cargo install --locked --path crates/tasks-daemon --root ~/.local -cargo install --locked --path crates/tasks-cli --features mcp --root ~/.local # add ui for the window +cargo install --locked --path crates/tasks-cli --features mcp,ui --root ~/.local ``` Then [pair your machines](https://loadtable.osvauld.com/docs/pairing) and @@ -57,8 +61,8 @@ The crates keep their `tasks-*` names from before the project was named. ```sh cargo fmt --all --check -cargo clippy --workspace --all-targets --features tasks-cli/mcp -- -D warnings -cargo test --workspace --features tasks-cli/mcp +cargo clippy --workspace --all-targets --features tasks-cli/mcp,tasks-cli/ui -- -D warnings +cargo test --workspace --features tasks-cli/mcp,tasks-cli/ui ``` `docs/REGRESSIONS.md` lists every behaviour with the tests that hold it; a change that alters one diff --git a/crates/tasks-cli/src/ui.rs b/crates/tasks-cli/src/ui.rs index f308c06..24970d9 100644 --- a/crates/tasks-cli/src/ui.rs +++ b/crates/tasks-cli/src/ui.rs @@ -17,8 +17,34 @@ impl DaemonLink for ClientLink { } } +/// R-832: why the window cannot open in this shell, before winit says it in its own words: on +/// Linux a session with no display (an SSH shell, a headless worker) has none of the variables a +/// window needs. macOS needs none of them. +pub(crate) fn no_display( + linux: bool, + env: &dyn Fn(&str) -> Option, +) -> Option<&'static str> { + let set = |k: &&str| env(k).is_some_and(|v| !v.is_empty()); + if !linux + || ["DISPLAY", "WAYLAND_DISPLAY", "WAYLAND_SOCKET"] + .iter() + .any(set) + { + return None; + } + Some( + "no display here (DISPLAY and WAYLAND_DISPLAY are not set), so the window cannot open; \ + run `loadtable ui` in your desktop session, or use `loadtable status` and \ + `loadtable list --active` from this shell", + ) +} + /// Opens the window and returns the process exit code when it closes. pub(crate) fn run(client: Arc, env: &dyn Fn(&str) -> Option) -> i32 { + if let Some(why) = no_display(cfg!(target_os = "linux"), env) { + let _ = writeln!(std::io::stderr(), "loadtable ui: {why}"); + return crate::error::EXIT_FAILURE; + } let who = env("USER") .or_else(|| env("LOGNAME")) .filter(|u| !u.is_empty()) @@ -31,3 +57,31 @@ pub(crate) fn run(client: Arc, env: &dyn Fn(&str) -> Option) } } } + +#[cfg(test)] +mod tests { + use super::no_display; + + fn env_of<'a>(vars: &'a [(&'a str, &'a str)]) -> impl Fn(&str) -> Option + 'a { + move |k| { + let (_, v) = vars.iter().find(|(n, _)| *n == k)?; + Some((*v).to_string()) + } + } + + #[test] + fn r832_a_linux_shell_without_a_display_is_told_plainly_and_where_to_go() { + let why = + no_display(true, &env_of(&[("USER", "abe"), ("DISPLAY", "")])).unwrap_or_default(); + assert!(why.contains("no display here"), "{why}"); + assert!(why.contains("loadtable status"), "{why}"); + } + + #[test] + fn r832_any_display_variable_or_macos_lets_the_window_try() { + for var in ["DISPLAY", "WAYLAND_DISPLAY", "WAYLAND_SOCKET"] { + assert_eq!(no_display(true, &env_of(&[(var, ":0")])), None, "{var}"); + } + assert_eq!(no_display(false, &env_of(&[])), None); + } +} diff --git a/docs/REGRESSIONS.md b/docs/REGRESSIONS.md index bacddbe..c1af8ec 100644 --- a/docs/REGRESSIONS.md +++ b/docs/REGRESSIONS.md @@ -823,3 +823,4 @@ Tests of earlier rows whose expectations changed with the redesign, kept and edi | R-829 | A peer's fetch follows an output link (`target`, `android/build`, `android/app/build`) only when it leads into this machine's build cache; a link at that name that leads anywhere else, whether it came with the synced tree or a job put it there, is refused ("does not lead into the build cache"), as is any file that resolves outside the checkout and the cache. Found 2026-10-29: a committed `target -> /` let a paired machine fetch `target/home//.ssh/id_ed25519`, read by the daemon itself, outside R-821. | `r829_an_output_link_is_followed_only_into_the_build_cache` (outputs), `r405_the_fetch_policy_allows_the_placed_link_names_into_this_cache` (build_cache) | any | | R-830 | A fetch writes only the files it asked for, once each: a listing entry for any other path, or a second entry for the same path, is ignored and reported as `unasked` (the job log says "not asked for, not written"). Found 2026-10-29: a worker could answer a fetch of `target/app.apk` with its own `build.rs`, which the origin wrote into its checkout and ran unconfined on its next build. | `r830_only_the_files_asked_for_are_written_whatever_the_peer_lists` (outputs) | any | | R-831 | A confined job on macOS can look up, but not list, every folder between the closed home folder and an opened one (`~/.local`, `~/.local/state`, `~/Library`, …), so `realpath` and cargo work inside the synced copy. Found 2026-10-29: every confined cargo build on the Mac failed with "could not canonicalize path" for each parent of the peer-store and could not read `crates/*/Cargo.toml`. | `r831_the_folders_above_an_opened_one_can_be_looked_up_but_not_listed`, `r831_paths_outside_the_closed_folders_add_no_lookups` (confine), `r831_a_confined_job_resolves_its_own_folder_and_globs_inside_it`, `r831_the_folders_above_stay_unlisted_and_the_rest_of_home_closed` (confine_macos, macOS CI) | macos | +| R-832 | On Linux, `loadtable ui` in a shell with none of `DISPLAY`, `WAYLAND_DISPLAY`, `WAYLAND_SOCKET` set exits 1 with "no display here" and names `loadtable status` and `loadtable list --active`, before winit is reached; macOS is not checked. Release builds carry the window (the graphics libraries are loaded only when it opens), so this is what a headless machine sees. | `r832_a_linux_shell_without_a_display_is_told_plainly_and_where_to_go`, `r832_any_display_variable_or_macos_lets_the_window_try` (cli ui) | any | diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 6bb52b5..7d9861f 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -56,8 +56,6 @@ does not downgrade new installs. ## What release builds leave out -- The desktop window (`loadtable ui`): it links graphics libraries that headless workers lack. Build - from source with `--features ui,mcp` for it. - Code signing and notarization on macOS. `curl` does not set the quarantine flag, so the installer's binaries run; a browser download of the archive needs `xattr -d com.apple.quarantine` on each binary. From 9950dc19f0a3e23c9c831c3e46e9f8676b2f3950 Mon Sep 17 00:00:00 2001 From: Abraham George Date: Thu, 29 Oct 2026 22:12:33 +0530 Subject: [PATCH 2/2] Version 0.1.2, with release notes the draft release uses - Workspace version 0.1.2 (Cargo.toml and the workspace crates in Cargo.lock) - docs/releases/v0.1.2.md: the desktop window in the release (R-832) and confined cargo builds on a Mac worker (R-831) - release.yml: a tag without docs/releases/vX.Y.Z.md is refused in the version job, before any build; the draft release takes its notes from that file Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 9 ++++++++- Cargo.lock | 28 ++++++++++++++-------------- Cargo.toml | 2 +- docs/RELEASING.md | 15 +++++++++------ docs/releases/v0.1.2.md | 27 +++++++++++++++++++++++++++ 5 files changed, 59 insertions(+), 22 deletions(-) create mode 100644 docs/releases/v0.1.2.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7fa4ebf..1830412 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,6 +36,9 @@ jobs: raise SystemExit('Only stable X.Y.Z versions are released') if os.environ['GITHUB_REF_TYPE'] == 'tag' and os.environ['GITHUB_REF_NAME'] != f'v{version}': raise SystemExit(f'Tag {os.environ["GITHUB_REF_NAME"]} does not match the workspace version {version}') + notes = f'docs/releases/v{version}.md' + if os.environ['GITHUB_REF_TYPE'] == 'tag' and not (os.path.isfile(notes) and os.path.getsize(notes)): + raise SystemExit(f'No release notes for v{version}: add {notes}') with open(os.environ['GITHUB_OUTPUT'], 'a') as f: f.write(f'version={version}\n') PY @@ -116,6 +119,9 @@ jobs: permissions: contents: write steps: + - uses: actions/checkout@v4 + with: + sparse-checkout: docs/releases - uses: actions/download-artifact@v4 with: path: artifacts @@ -132,7 +138,8 @@ jobs: echo 'Refusing to replace the binaries of a published release'; exit 1; } else + # the notes were checked in the version job gh release create "$TAG" --verify-tag --draft --title "LoadTable $TAG" \ - --notes 'Candidate binaries for Linux (x86-64, arm64) and macOS (Apple silicon, Intel). Test before publishing; publishing copies them to dl.loadtable.osvauld.com.' + --notes-file "docs/releases/$TAG.md" fi gh release upload "$TAG" artifacts/* --clobber diff --git a/Cargo.lock b/Cargo.lock index 24b6def..65fd9ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5906,7 +5906,7 @@ dependencies = [ [[package]] name = "tasks-admission" -version = "0.1.1" +version = "0.1.2" dependencies = [ "tasks-core", "tasks-platform", @@ -5915,7 +5915,7 @@ dependencies = [ [[package]] name = "tasks-cli" -version = "0.1.1" +version = "0.1.2" dependencies = [ "anyhow", "clap", @@ -5937,7 +5937,7 @@ dependencies = [ [[package]] name = "tasks-core" -version = "0.1.1" +version = "0.1.2" dependencies = [ "serde", "serde_json", @@ -5946,7 +5946,7 @@ dependencies = [ [[package]] name = "tasks-daemon" -version = "0.1.1" +version = "0.1.2" dependencies = [ "iroh", "rustix 1.1.5", @@ -5971,7 +5971,7 @@ dependencies = [ [[package]] name = "tasks-identity" -version = "0.1.1" +version = "0.1.2" dependencies = [ "bip39", "bs58", @@ -5989,7 +5989,7 @@ dependencies = [ [[package]] name = "tasks-ledger" -version = "0.1.1" +version = "0.1.2" dependencies = [ "rusqlite", "serde_json", @@ -6002,7 +6002,7 @@ dependencies = [ [[package]] name = "tasks-mcp" -version = "0.1.1" +version = "0.1.2" dependencies = [ "serde", "serde_json", @@ -6016,7 +6016,7 @@ dependencies = [ [[package]] name = "tasks-peer" -version = "0.1.1" +version = "0.1.2" dependencies = [ "bao-tree", "base64 0.22.1", @@ -6039,7 +6039,7 @@ dependencies = [ [[package]] name = "tasks-placement" -version = "0.1.1" +version = "0.1.2" dependencies = [ "tasks-core", "tasks-timing", @@ -6048,7 +6048,7 @@ dependencies = [ [[package]] name = "tasks-platform" -version = "0.1.1" +version = "0.1.2" dependencies = [ "landlock", "libc", @@ -6060,7 +6060,7 @@ dependencies = [ [[package]] name = "tasks-policy" -version = "0.1.1" +version = "0.1.2" dependencies = [ "serde", "serde_json", @@ -6072,7 +6072,7 @@ dependencies = [ [[package]] name = "tasks-runner" -version = "0.1.1" +version = "0.1.2" dependencies = [ "tasks-core", "tasks-platform", @@ -6081,7 +6081,7 @@ dependencies = [ [[package]] name = "tasks-timing" -version = "0.1.1" +version = "0.1.2" dependencies = [ "serde", "serde_json", @@ -6091,7 +6091,7 @@ dependencies = [ [[package]] name = "tasks-ui" -version = "0.1.1" +version = "0.1.2" dependencies = [ "eframe", "jiff", diff --git a/Cargo.toml b/Cargo.toml index 7aec852..a9c969b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "3" members = ["crates/*"] [workspace.package] -version = "0.1.1" +version = "0.1.2" edition = "2024" rust-version = "1.92" license = "MIT" diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 7d9861f..1fb7c90 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -3,10 +3,12 @@ Three workflows in `.github/workflows`: - `ci.yml`: every push to main, dev or v2 and every pull request. Linux: fmt, clippy with warnings as - errors, every test (with the MCP server). macOS: clippy, with and without the iroh transport. -- `release.yml`: a pushed `vX.Y.Z` tag. Builds `loadtable` and `loadtabled` for - `x86_64-unknown-linux-gnu`, `aarch64-unknown-linux-gnu` (both on Ubuntu 22.04, glibc 2.35), - `aarch64-apple-darwin` and `x86_64-apple-darwin`, and drafts the GitHub release with them. + errors, every test (with the MCP server and the desktop window). macOS: clippy with and without + the iroh transport, and the confinement profile run under the real `sandbox-exec`. +- `release.yml`: a pushed `vX.Y.Z` tag. Builds `loadtable` (with the MCP server and the desktop + window) and `loadtabled` for `x86_64-unknown-linux-gnu`, `aarch64-unknown-linux-gnu` (both on + Ubuntu 22.04, glibc 2.35), `aarch64-apple-darwin` and `x86_64-apple-darwin`, and drafts the GitHub + release with them and the notes in `docs/releases/vX.Y.Z.md`. - `publish-r2.yml`: publishing that release (or a manual run with its tag). Copies the binaries to LoadTable's R2 bucket, served at `https://dl.loadtable.osvauld.com`, and moves `latest` forward. @@ -38,8 +40,9 @@ branches and tags allow only tags matching `v*`. ## Cutting a release -1. Set `[workspace.package] version` in `Cargo.toml` to the new `X.Y.Z` and merge it to main. The - tag must match it; the workflow refuses otherwise. +1. Set `[workspace.package] version` in `Cargo.toml` to the new `X.Y.Z` (and the workspace crates + in `Cargo.lock`), write the release notes in `docs/releases/vX.Y.Z.md`, and merge both to main. + The tag must match the version and the notes must exist; the workflow refuses otherwise. 2. `git tag vX.Y.Z && git push origin vX.Y.Z`. 3. Wait for Release builds; it drafts "LoadTable vX.Y.Z" with four archives, their `.sha256` files and `SHA256SUMS`. diff --git a/docs/releases/v0.1.2.md b/docs/releases/v0.1.2.md new file mode 100644 index 0000000..c208e3d --- /dev/null +++ b/docs/releases/v0.1.2.md @@ -0,0 +1,27 @@ +Binaries for Linux (x86-64, arm64) and macOS (Apple silicon, Intel). Install or upgrade with +`curl -fsSL https://loadtable.osvauld.com/install.sh | sh`, then restart the daemon when it is idle. + +## The desktop window is in the release + +`loadtable ui` now ships in the release binary, so the installer gives you the window too; no +source build needed. It shows every machine and its queue, each job's log, the history, timings, +the audit log and pressure stalls, and can cancel a job, move it up a queue class or drain the +machine. The window loads its graphics libraries only when it opens, so the same binary runs on a +headless worker; there `loadtable ui` says there is no display and points you to `loadtable status` +and `loadtable list --active` (R-832). + +## Fixes + +- **Confined cargo builds work on a Mac worker** (R-831). A job sent to a Mac failed with "could + not canonicalize path" for each folder above the synced copy, because the sandbox closed the home + folder to lookups too. The folders between the closed home folder and an opened one can now be + looked up, not listed; the rest of the home folder stays closed. CI now runs the profile under + the real `sandbox-exec`. + +## Upgrading + +Upgrade every machine; a Mac worker needs 0.1.2 for confined cargo builds. Restart with `systemctl --user restart loadtabled` (Linux) or +`launchctl kickstart -k gui/$(id -u)/loadtabled` (macOS) once `loadtable list --active` is empty. + +The macOS binaries are not signed or notarized. The installer's binaries run as they are; an +archive downloaded in a browser needs `xattr -d com.apple.quarantine` on each binary.