From 449088c5f040d1b62c6913046640567f0a6d1402 Mon Sep 17 00:00:00 2001 From: "M.Hoffmann" Date: Wed, 21 Oct 2026 12:15:27 -0300 Subject: [PATCH] feat(rust-agent): the Rust toolchain for a weather-alert agent CLI New lab. It pins rustc, cargo, clippy, rustfmt, rust-analyzer, cargo-nextest, cargo-deny, cargo-audit, cargo-machete (cargo-udeps needs nightly), pkg-config, openssl, jq and xh from the lab's own nixos-unstable. It ships the toolchain only: the agent code will live in the consumer. checks.smoke builds and tests a std-only crate with the pinned toolchain, and runs clippy -D warnings and rustfmt --check on it. The ci.yml matrix line is not in this commit: it needs the human's approval. Tested: nix flake check --no-update-lock-file in labs/rust-agent (smoke, lab-json, versions pass on x86_64-linux); nixpkgs-fmt, statix, deadnix clean; just 1.21.0 --summary parses the justfile; reuse==5.0.2 lint passes Co-Authored-By: Claude --- DESCRIPTION.md | 2 +- README.md | 1 + labs/rust-agent/.gitignore | 3 + labs/rust-agent/README.md | 59 ++++++++++++++ labs/rust-agent/flake.lock | 27 +++++++ labs/rust-agent/flake.nix | 123 ++++++++++++++++++++++++++++++ labs/rust-agent/justfile | 22 ++++++ labs/rust-agent/lab.json | 4 + labs/rust-agent/smoke/Cargo.lock | 7 ++ labs/rust-agent/smoke/Cargo.toml | 6 ++ labs/rust-agent/smoke/src/main.rs | 17 +++++ 11 files changed, 270 insertions(+), 1 deletion(-) create mode 100644 labs/rust-agent/.gitignore create mode 100644 labs/rust-agent/README.md create mode 100644 labs/rust-agent/flake.lock create mode 100644 labs/rust-agent/flake.nix create mode 100644 labs/rust-agent/justfile create mode 100644 labs/rust-agent/lab.json create mode 100644 labs/rust-agent/smoke/Cargo.lock create mode 100644 labs/rust-agent/smoke/Cargo.toml create mode 100644 labs/rust-agent/smoke/src/main.rs diff --git a/DESCRIPTION.md b/DESCRIPTION.md index 845b9f7..63616dd 100644 --- a/DESCRIPTION.md +++ b/DESCRIPTION.md @@ -1,6 +1,6 @@ # picasso (formerly nix-config), for the LinkedIn Projects section -Self-contained Nix flakes ("labs") that pin scientific, publishing and agentic toolchains for other repositories, with deterministic builds. Ten labs: WAVEWATCH III (gfortran, OpenMPI, NetCDF), pandoc + TeX Live publishing with its own GitHub Action, Markdown to PDF for Portuguese in one Rust + Typst binary (25.2 MiB static, against 729 MiB for pandoc + XeLaTeX), Markdown papers for arXiv, a Gleam toolchain for static sites, a lint set, a sandbox for coding agents, CUDA and PyTorch host setup, a presenter's desk, and a code graph of any repository. The code is only Nix and Rust. Any repo can borrow a lab as a flake input, a submodule or an Action, and CI builds each one on every push. It is the toolchain layer under WW3 GPU Lab, marola and ptbr-writer. Inspired by gvolpe/nix-config. +Self-contained Nix flakes ("labs") that pin scientific, publishing and agentic toolchains for other repositories, with deterministic builds. Eleven labs: WAVEWATCH III (gfortran, OpenMPI, NetCDF), pandoc + TeX Live publishing with its own GitHub Action, Markdown to PDF for Portuguese in one Rust + Typst binary (25.2 MiB static, against 729 MiB for pandoc + XeLaTeX), Markdown papers for arXiv, a Gleam toolchain for static sites, a lint set, a Rust toolchain for a weather-alert agent, a sandbox for coding agents, CUDA and PyTorch host setup, a presenter's desk, and a code graph of any repository. The code is only Nix and Rust. Any repo can borrow a lab as a flake input, a submodule or an Action, and CI builds each one on every push. It is the toolchain layer under WW3 GPU Lab, marola and ptbr-writer. Inspired by gvolpe/nix-config. ## One line (the GitHub "About" box) diff --git a/README.md b/README.md index 6e832fe..ff7ebfb 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ Each lab README has the recipes, what the lab pins (`What it pins`) and how to r | [`ptbr-pdf`](labs/ptbr-pdf) | Markdown to PDF for Portuguese: one Rust binary, `to-pdf`, Typst as a library, six fonts inside | `nix run github:h0ffmann/picasso?dir=labs/ptbr-pdf -- texto.md` | | [`ptbr-tools`](labs/ptbr-tools) | The ptbr-writer plugin's runtime scripts (hook, reading time, router) as one static Rust binary, 0.54 MiB, std only | `nix build github:h0ffmann/picasso?dir=labs/ptbr-tools#static` | | [`lint`](labs/lint) | hadolint, actionlint, shellcheck, ruff, pyflakes, cloc, coverage.py, pdoc as one list | `lib..tools` in your `mkShell` | +| [`rust-agent`](labs/rust-agent) | rustc, cargo, clippy, rust-analyzer, nextest, deny, audit, machete, jq, xh for a Rust CLI that reads weather alerts and calls an LLM | `lib..tools` in your `mkShell` | | [`paper`](labs/paper) | Markdown papers to PDF and arXiv source: pandoc-crossref, XeLaTeX, pdfLaTeX | `just shell`, `just smoke` | | [`gleam-website`](labs/gleam-website) | Gleam (JavaScript target), node, esbuild, just: the toolchain of this repo's site | `just shell`, `just smoke` | | [`agentic`](labs/agentic) | ai-jail, OpenCode, Open Code Review, gh; `jail-run`, `gh-token`, `clip`, `clip-relay` | `just jco`, `just test` | diff --git a/labs/rust-agent/.gitignore b/labs/rust-agent/.gitignore new file mode 100644 index 0000000..33f99d6 --- /dev/null +++ b/labs/rust-agent/.gitignore @@ -0,0 +1,3 @@ +/result +.direnv/ +/smoke/target/ diff --git a/labs/rust-agent/README.md b/labs/rust-agent/README.md new file mode 100644 index 0000000..30cfea2 --- /dev/null +++ b/labs/rust-agent/README.md @@ -0,0 +1,59 @@ +# rust-agent + +The tools to build a Rust CLI that reads official Brazilian weather alerts (INMET, CEMADEN, +Defesa Civil: JSON, XML, CAP and RSS feeds), calls an LLM over Anthropic's HTTP API, and writes +static reports. They come from one locked nixpkgs. Anthropic has no official Rust SDK, so the +agent uses plain HTTPS and JSON. This lab ships the toolchain only. The agent code is in the +consumer. + +``` +just shell # the tools on PATH, in your current directory (RUST_SRC_PATH is set) +just versions # every tool's --version, built in the Nix sandbox (what CI checks) +just smoke # build, test, clippy and rustfmt the smoke crate in the sandbox (what CI builds) +just test # cargo test in the smoke crate, in the dev shell +``` + +## Reusing it from another repository + +```nix +{ + inputs.rust-agent = { url = "github:h0ffmann/picasso?dir=labs/rust-agent"; inputs.nixpkgs.follows = "nixpkgs"; }; + outputs = { nixpkgs, rust-agent, ... }: { + devShells.x86_64-linux.default = nixpkgs.legacyPackages.x86_64-linux.mkShell { + packages = [ /* the project's own tools */ ] ++ rust-agent.lib.x86_64-linux.tools; + RUST_SRC_PATH = "${nixpkgs.legacyPackages.x86_64-linux.rustPlatform.rustLibSrc}"; + }; + }; +} +``` + +`follows` keeps one nixpkgs closure in the consumer's shell. The price is that this lab's CI +uses its own lock and the consumer uses its own, so a tool can differ by a patch version. +`nix flake update rust-agent` in the consumer picks up a new revision of this lab. + +`packages..` re-exports each tool for `nix run`. `checks..versions` fails +the flake check if a nixpkgs bump breaks a tool. `checks..smoke` builds and tests a +small std-only crate (`smoke/`) with the pinned toolchain, and runs `clippy -D warnings` and +`rustfmt --check` on it. + +No consumer yet. The first one will be [marola-site](https://github.com/marola-dev/marola-site) +(issue 172). + +## TLS + +Prefer `rustls` in the agent's crates. A static musl build then needs neither `openssl` nor +`pkg-config`. The lab pins both only for crates that need native TLS. + +## What it pins + +| Type | Program | +| :--- | :-----: | +| Compiler | [rustc and cargo](https://www.rust-lang.org/) | +| Lint and format | [clippy](https://github.com/rust-lang/rust-clippy), [rustfmt](https://github.com/rust-lang/rustfmt) | +| Editor | [rust-analyzer](https://rust-analyzer.github.io/) | +| Tests | [cargo-nextest](https://nexte.st/) | +| Supply chain | [cargo-deny](https://github.com/EmbarkStudios/cargo-deny) (licences, bans), [cargo-audit](https://github.com/rustsec/rustsec) (advisories) | +| Unused crates | [cargo-machete](https://github.com/bnjbvr/cargo-machete) (works on stable; cargo-udeps needs nightly) | +| Native TLS | [pkg-config](https://www.freedesktop.org/wiki/Software/pkg-config/), [openssl](https://www.openssl.org/) | +| Feeds and API | [jq](https://jqlang.github.io/jq/), [xh](https://github.com/ducaale/xh) | +| Reuse | `lib..tools` (the list), `packages..`, `checks..{lab-json,versions,smoke}` | diff --git a/labs/rust-agent/flake.lock b/labs/rust-agent/flake.lock new file mode 100644 index 0000000..886d2d9 --- /dev/null +++ b/labs/rust-agent/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1792502884, + "narHash": "sha256-IoNisKcGbUjdd45qCqywGMCYmZ5EnkxezvLII0FWwNA=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "02c087f3fe673c9e317f206a627cb6b910336dbe", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/labs/rust-agent/flake.nix b/labs/rust-agent/flake.nix new file mode 100644 index 0000000..ca29f88 --- /dev/null +++ b/labs/rust-agent/flake.nix @@ -0,0 +1,123 @@ +{ + description = "rust-agent — the Rust toolchain for a CLI that reads Brazilian weather alerts and calls an LLM over HTTPS: rustc, cargo, clippy, rustfmt, rust-analyzer, nextest, deny, audit, machete"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + }; + + outputs = { self, nixpkgs }: + let + inherit (nixpkgs) lib; + systems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" ]; + forAll = f: lib.genAttrs systems (s: f nixpkgs.legacyPackages.${s}); + + # The one place the tools are named. A consumer appends this to its own mkShell packages. + toolsFor = pkgs: with pkgs; [ + rustc + cargo + clippy + rustfmt + rust-analyzer + cargo-nextest + cargo-deny + cargo-audit + cargo-machete # reads Cargo.toml and the sources; cargo-udeps needs nightly + pkg-config + openssl # only for crates with native TLS; prefer rustls (see README) + jq + xh + ]; + + # `nix flake check` builds this: every tool answers --version in the sandbox, so a nixpkgs + # bump that drops or breaks one fails here, not in a consumer's CI. + versionsFor = pkgs: pkgs.runCommand "rust-agent-versions" { nativeBuildInputs = toolsFor pkgs; } '' + { + echo "nixpkgs ${nixpkgs.rev or "dirty"}" + echo "rustc $(rustc --version)" + echo "cargo $(cargo --version)" + echo "clippy $(cargo-clippy --version)" + echo "rustfmt $(rustfmt --version)" + echo "rust-analyzer $(rust-analyzer --version)" + echo "nextest $(cargo-nextest nextest --version | head -1)" + echo "deny $(cargo-deny --version)" + echo "audit $(cargo-audit --version)" + echo "machete $(cargo-machete --version)" + echo "pkg-config $(pkg-config --version)" + echo "openssl $(openssl version)" + echo "jq $(jq --version)" + echo "xh $(xh --version | head -1)" + } | tee versions.txt + mkdir -p "$out" && cp versions.txt "$out"/ + ''; + + # No dependencies, so no vendoring: the lock file has only the crate itself. Tests run in + # the build (checkPhase). + smokeFor = pkgs: pkgs.rustPlatform.buildRustPackage { + pname = "rust-agent-smoke"; + version = "0.1.0"; + src = lib.fileset.toSource { + root = ./smoke; + fileset = lib.fileset.unions [ ./smoke/Cargo.toml ./smoke/Cargo.lock ./smoke/src ]; + }; + cargoLock.lockFile = ./smoke/Cargo.lock; + meta.mainProgram = "rust-agent-smoke"; + }; + + # Same sources, `cargo clippy -D warnings` as the build. + clippyFor = pkgs: (smokeFor pkgs).overrideAttrs (old: { + pname = "rust-agent-smoke-clippy"; + nativeBuildInputs = old.nativeBuildInputs ++ [ pkgs.clippy ]; + buildPhase = "cargo clippy --release --all-targets --offline -- -D warnings"; + doCheck = false; + installPhase = "touch $out"; + }); + + fmtFor = pkgs: pkgs.runCommand "rust-agent-smoke-fmt" { nativeBuildInputs = [ pkgs.rustfmt ]; } '' + rustfmt --check --edition 2021 ${./smoke/src/main.rs} + touch $out + ''; + + # lab.json, the card the profile README renders: its shape is asserted at evaluation, and the + # build writes each headline attribute's version from this lab's own nixpkgs. Copied in every + # lab, since a lab references nothing outside itself. + labCardFor = pkgs: + let + card = lib.importJSON ./lab.json; + version = attr: (lib.getAttrFromPath (lib.splitString "." attr) pkgs).version; + in + assert lib.assertMsg (lib.isString card.summary && card.summary != "" && lib.stringLength card.summary <= 100) + "lab.json: summary must be one non-empty line of at most 100 characters"; + assert lib.assertMsg (lib.isList card.headline && card.headline != [ ] && lib.all lib.isString card.headline) + "lab.json: headline must be a non-empty list of nixpkgs attribute paths"; + pkgs.writeText "lab-json" (lib.concatMapStrings (a: "${a} ${version a}\n") card.headline); + in + { + lib = forAll (pkgs: { + tools = toolsFor pkgs; + }); + + packages = forAll (pkgs: { + versions = versionsFor pkgs; + inherit (pkgs) rustc cargo clippy rustfmt rust-analyzer cargo-nextest cargo-deny cargo-audit cargo-machete pkg-config openssl jq xh; + }); + + devShells = forAll (pkgs: { + default = pkgs.mkShell { + name = "rust-agent"; + packages = toolsFor pkgs; + RUST_SRC_PATH = "${pkgs.rustPlatform.rustLibSrc}"; + shellHook = ''echo "rust-agent: $(rustc --version) | $(cargo --version)"''; + }; + }); + + checks = forAll (pkgs: { + lab-json = labCardFor pkgs; + versions = self.packages.${pkgs.stdenv.hostPlatform.system}.versions; + smoke = pkgs.linkFarm "rust-agent-smoke-checks" [ + { name = "build"; path = smokeFor pkgs; } + { name = "clippy"; path = clippyFor pkgs; } + { name = "fmt"; path = fmtFor pkgs; } + ]; + }); + }; +} diff --git a/labs/rust-agent/justfile b/labs/rust-agent/justfile new file mode 100644 index 0000000..1938cf4 --- /dev/null +++ b/labs/rust-agent/justfile @@ -0,0 +1,22 @@ +# rust-agent — the Rust toolchain for a climate-agent CLI. `just` lists recipes. +set shell := ["bash", "-euo", "pipefail", "-c"] +here := justfile_directory() + +default: + @just --list + +# Enter the shell: the Rust toolchain and the feed tools on PATH, in your current directory. +shell: + nix develop "{{here}}" + +# Print the exact tool versions this lab pins (builds checks.versions in the sandbox). +versions: + nix build "{{here}}#versions" --print-build-logs --out-link "{{here}}/result" && cat "{{here}}/result/versions.txt" + +# Build and test the std-only smoke crate in the sandbox, with clippy and rustfmt (what CI builds). +smoke: + nix build "{{here}}#checks.$(nix eval --impure --raw --expr builtins.currentSystem).smoke" --print-build-logs --no-link + +# cargo test in the smoke crate, inside the dev shell. +test: + nix develop "{{here}}" --command cargo test --manifest-path "{{here}}/smoke/Cargo.toml" diff --git a/labs/rust-agent/lab.json b/labs/rust-agent/lab.json new file mode 100644 index 0000000..d3718e3 --- /dev/null +++ b/labs/rust-agent/lab.json @@ -0,0 +1,4 @@ +{ + "summary": "rustc, cargo, clippy, rust-analyzer, nextest, deny, audit for a Rust agent, one list", + "headline": ["rustc", "cargo", "rust-analyzer", "cargo-nextest"] +} diff --git a/labs/rust-agent/smoke/Cargo.lock b/labs/rust-agent/smoke/Cargo.lock new file mode 100644 index 0000000..798eb31 --- /dev/null +++ b/labs/rust-agent/smoke/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "rust-agent-smoke" +version = "0.1.0" diff --git a/labs/rust-agent/smoke/Cargo.toml b/labs/rust-agent/smoke/Cargo.toml new file mode 100644 index 0000000..25532e1 --- /dev/null +++ b/labs/rust-agent/smoke/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "rust-agent-smoke" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/labs/rust-agent/smoke/src/main.rs b/labs/rust-agent/smoke/src/main.rs new file mode 100644 index 0000000..525082e --- /dev/null +++ b/labs/rust-agent/smoke/src/main.rs @@ -0,0 +1,17 @@ +fn main() { + println!("{}", greeting("climate-agent")); +} + +fn greeting(name: &str) -> String { + format!("hello, {name}") +} + +#[cfg(test)] +mod tests { + use super::greeting; + + #[test] + fn greets_by_name() { + assert_eq!(greeting("inmet"), "hello, inmet"); + } +}