Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
18 changes: 14 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -75,10 +78,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: |
Expand All @@ -98,6 +102,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 }}
Expand All @@ -113,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
Expand All @@ -129,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
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 14 additions & 14 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ documentation: <https://loadtable.osvauld.com>.
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.

Expand All @@ -27,13 +30,14 @@ documentation: <https://loadtable.osvauld.com>.
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
Expand All @@ -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
Expand Down
54 changes: 54 additions & 0 deletions crates/tasks-cli/src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
) -> 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<dyn Client>, env: &dyn Fn(&str) -> Option<String>) -> 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())
Expand All @@ -31,3 +57,31 @@ pub(crate) fn run(client: Arc<dyn Client>, env: &dyn Fn(&str) -> Option<String>)
}
}
}

#[cfg(test)]
mod tests {
use super::no_display;

fn env_of<'a>(vars: &'a [(&'a str, &'a str)]) -> impl Fn(&str) -> Option<String> + '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);
}
}
1 change: 1 addition & 0 deletions docs/REGRESSIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<you>/.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 |
17 changes: 9 additions & 8 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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`.
Expand All @@ -56,8 +59,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.
Expand Down
27 changes: 27 additions & 0 deletions docs/releases/v0.1.2.md
Original file line number Diff line number Diff line change
@@ -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.
Loading