Dependency update runbook
This runbook is the step-by-step procedure for the two kinds of dependency session that ADR-0017 allows: the monthly update session, which brings every dependency of one repository up to date in one batch, and the security-alert session, which answers one Dependabot security alert. The session prompts in plan/06-agent-workflow.md start these sessions; this page is what the agent then follows. It was written for work package X-003, which also added the automatic checks it refers to.
A dependency here is anything the project downloads instead of writing itself: library packages (Rust crates, npm packages), toolchains (Rust, Node.js, pnpm, Qt), command-line tools (cargo-deny, grype, CMake and the like), Python tools used in CI, container images, and the GitHub Actions that the workflows run. A pin is the place where the project names the exact version of one of them, often together with a hash: a fingerprint of the file that lets a download be checked byte for byte.
The rules every session follows
These are ADR-0017’s rules, as they apply to the work of a session:
- 24-hour minimum age. Never install, pin or upgrade to a version published less than 24 hours ago. Before the session starts, write down its cutoff: the current time in UTC minus 24 hours. Every new version must have been published before the cutoff, and the pull request states each one’s publish date. A newer version that is too young is skipped and noted for next month.
- Exact pins and committed lockfiles. Versions are exact (
=1.2.3in Cargo manifests,1.2.3inpackage.json,==1.2.3with hashes for Python tools, a full commit SHA for an action, a digest for a container image).Cargo.lockandpnpm-lock.yamlare regenerated by the tools, never edited by hand. - No update bots. Never add Dependabot version updates, Renovate or anything like them. The supply-chain workflow fails on their configuration files.
- Install scripts stay disabled. Never allow a package’s install or build script (
allowBuildsentries stayfalse); a Rust crate with a build script is reviewed before it is added. - Every gate stays green and strong. Run the repository’s full verification gate, fix what breaks, and never weaken, skip or disable a check to get green. If a check is genuinely wrong, fix it in its own commit and explain why.
- Licenses. A new package must use a license on the ADR-0017 allowlist. cargo-deny enforces this for Rust crates; for npm packages, Python tools and everything else, check the license yourself and state it in the pull request (bayan-web’s licence-notice check only makes sure that the license text of every bundled package ships with the app).
- One pull request per repository per session, whose description lists every change (see The pull request). If the change alters a repository’s dependency posture (a new mechanism, a new kind of exception), update its
AGENTS.mddependency section, and ADR-0017 if needed, in the same pull request.
How the rules are enforced
Knowing what the machines check tells you what is left for you to check by hand.
| Repository | Checked automatically | Your job in the session |
|---|---|---|
| bayan-core, bayan-server | cargo xtask check-exact-pins (exact pins) and cargo xtask check-lockfile-age (Cargo builds exactly what each Cargo.lock lists, no crate is built from a folder or added from a Git repository, and every package version added to a Cargo.lock was published at least 24 hours before its commit, with the checksum crates.io published), in xtask/src/supply_chain/, identical in both repositories. They read every Cargo workspace of the repository, which they find by the Cargo.lock files that Git tracks: the main workspace and, today, bayan-core’s fuzz/ and bayan-server’s spikes/mls, which may also depend by path on members of their repository’s main workspace (and so build them from their folders) and on nothing else outside themselves. Three guards make both checks fail instead of passing over what they cannot see: a Cargo.lock that Cargo would not use (in a folder that is not the root of a workspace), a file named like Cargo.lock in another letter case, and a Cargo.toml that Git tracks outside every checked workspace (a workspace whose Cargo.lock is not committed, or a package in a folder that a workspace excludes), which Cargo could build from a lockfile that no check has seen; commit its Cargo.lock, make it a member of a checked workspace, or remove it. The age check compares each Cargo.lock with its own version at the merge base (or checks all of it, if it did not exist there), and resolves each workspace with every feature switched on (cargo metadata --all-features), as Cargo resolves the lockfile, so that the packages that only the MLS spike’s provisional-pq feature brings in are checked too. Both run before anything else is compiled (they compile only xtask, which has no dependencies), in the first step of cargo xtask verify and in CI: in bayan-core’s verify workflow, the Rust job of its CodeQL workflow and its fuzzing workflow (.github/workflows/fuzz.yml), and in bayan-server’s “Supply-chain checks” job, its verification gate and the Rust job of its CodeQL workflow. cargo deny check (advisories, licenses, sources, banned crates), in bayan-core also for the fuzz workspace against its own fuzz/deny.toml | Resolve with a Cargo that respects the minimum age (below); check toolchain, tool and image dates by hand. No Cargo.lock needs a hand check of dates and checksums, the fuzz workspace’s and the MLS spike’s included (BayanDocs/bayan-core#22) |
| bayan-web | pnpm itself (minimumReleaseAge: 1440, strict, re-checked on every frozen install), scripts/check-policy.ts (exact pins; pnpm-workspace.yaml and .npmrc read strictly, with only the settings it lists; and the settings pnpm applies from every source, from pnpm config list --json), scripts/check-lockfile-integrity.ts (registry sources and SHA-512 hashes, reading only the layout pnpm writes; no pnpmfile and no configuration dependencies), scripts/audit.ts (decides from pnpm’s full list of advisories, and fails on every high or critical one that no exception names) | Check Node.js, pnpm, browser and Ubuntu snapshot dates by hand; keep @types/node on Node.js’s major version |
| bayan-desktop | scripts/check-qt-release-age.py (the Qt release is at least 24 hours older than the commit that pinned it; CI runs it before it installs Qt), pip --require-hashes for the build tools, scripts/build-engine-sdk.sh (fetches exactly the bayan-core commit that deps/bayan-core.json pins, checks that it is on bayan-core’s main, and builds the engine’s C SDK from bayan-core’s Cargo.lock with --locked) | Check the build tools’, Python’s and the Ubuntu snapshot’s dates by hand |
| every repository | The supply-chain workflow: no update-bot configuration, and pip-audit on every hash-pinned *requirements*.txt (with a canary that proves pip-audit still finds known vulnerabilities). The workflow-lint workflow: every action pinned to a full commit SHA that is at least a day old (pinact), and no finding by zizmor, including actions with known vulnerabilities | Regenerate the shared Python pins together in all five repositories; check the dates of the CI tools that workflows download (zizmor, pinact, Scorecard, the CodeQL bundle) by hand |
| docs | scripts/tests/test_repository_baseline.py (the REUSE pins equal the cloud environment’s) | Check every pin of the cloud environment’s setup script by hand, and run the script |
Finding publish dates
| What | Where its publish time is |
|---|---|
| Rust crate | The crates.io sparse index: curl -sS -A "BayanDocs dependency session (+https://github.com/BayanDocs)" https://index.crates.io/<path> shows one JSON line per version with its pubtime (the path is 1/<name>, 2/<name>, 3/<first letter>/<name>, or <first two letters>/<next two letters>/<name>, all lower case). cargo xtask check-lockfile-age prints the date of every version a change adds. |
| npm package | pnpm view <package> time --json (every version with its publish time). |
| Python package | https://pypi.org/pypi/<package>/<version>/json (upload_time_iso_8601 of each file); scripts/python-tool-hashes.py only considers files uploaded before the cutoff you give it. |
| GitHub release (cargo-deny, grype, mdBook, lychee, pinact, cargo-fuzz, Scorecard, the CodeQL bundle) | The release page, or gh release view <tag> --repo <owner>/<repository>. Where GitHub’s API is out of reach, git ls-remote --tags https://github.com/<owner>/<repository> lists the tags and a clone shows each tag’s date (git for-each-ref --sort=-creatordate refs/tags), but a date in Git is written by whoever made the tag or commit, so prefer the release’s publish time, which GitHub records. Take the SHA-256 from the release’s published checksum file when it has one, and say so in the pin’s comment. |
| GitHub Action | Two checks. pinact run --check --verify-min-age --min-age 1 reports every pinned action whose commit is less than a day old, and the workflow-lint job runs the same check on every pull request. pinact reads the commit’s committer date, which whoever made the commit writes and can set back, so for every action that you pin anew or move to another commit, also confirm that GitHub published the release of its version tag at least 24 hours before your commit: gh release view <tag> --repo <owner>/<repository> --json publishedAt, or the date on the release page, which GitHub records itself. If the tag has no GitHub release, keep the previous pin for this month unless you can show in another way that the tag is at least a day old, and say how in the pull request. |
| Rust toolchain | The release announcement on blog.rust-lang.org, or the date in https://static.rust-lang.org/dist/channel-rust-<version>.toml. Nightly toolchains are named by their date. |
| Node.js | https://nodejs.org/dist/index.json (date of each version) and the release’s SHASUMS256.txt, whose signature by a Node.js release key must be checked before its hashes are used. |
| Qt | The release announcement on qt.io and the <ReleaseDate> in the repository index (https://download.qt.io/online/qtsdkrepository/<platform>/desktop/qt6_<version>/qt6_<version>/Updates.xml). Record the later of the two, as qt_released in bayan-desktop’s deps/qt.json. |
| Container image | Docker Hub’s https://hub.docker.com/v2/namespaces/library/repositories/<name>/tags/<tag>: digest is the image to pin and last_updated is when the tag last moved to a new image. Tags are rebuilt from time to time, so if last_updated is after the cutoff, that image is too young: keep the current pin this month. docker buildx imagetools inspect <image>:<tag> shows the same digest. |
| Ubuntu packages | The snapshot date itself (UBUNTU_SNAPSHOT, APT_SNAPSHOT): choose a date before the cutoff. |
Before you start
- Read docs/AGENTS.md §5, ADR-0017 with its amendments, and the “Dependency mechanisms” section of the repository’s
AGENTS.md. - Start from the latest
main, on the branch your session was given. - Write down the cutoff (now minus 24 hours, in UTC), for example
2026-11-02T09:00:00Z. - Check your tools: in a BayanDocs cloud session,
bayandocs-toolslists them. Install pip-audit withdocs/scripts/dev-setup.sh --pip-audit; the cloud environment will run that command itself once the batch of cloud-environment changes collected in issue #7 is applied. For bayan-desktop, load. ~/.local/share/bayandocs/desktop-tools/env.shfirst. - Run the repository’s verification gate once before changing anything, so that you know which failures, if any, were there before you.
Monthly update session
The common steps first; the sections after them list, for each repository, every pin and how to update it.
- Inventory. Go through the repository’s table below and note, for each pin, the current version, the newest version published before the cutoff, and any newer version you are skipping because it is too young.
- Read the changelogs of every update, looking for breaking changes, security fixes, new install or build scripts, changed licenses and new dependencies (each new transitive dependency must be on the license allowlist; a new direct dependency needs the justification of AGENTS.md §5, which a routine update does not add).
- Update the pins, in the order of the repository’s section, regenerating lockfiles and hash lists with the commands given there. Keep the pins that several files repeat equal (the sections name them). Then, for every pin you moved, search every BayanDocs repository, this runbook included, for its old version (also in the shorter form some copies use, such as
1.xfor Rust1.x.0) and for its old hashes, and update every copy that states the current pin, also where no test checks it:AGENTS.mdfiles, READMEs, and the comments of scripts and workflows. Leave dated records, such as reviews and spike reports, as they are. - Run the gate and fix what breaks, in the code or the configuration, never by weakening a check. A major version that would need a large change can stay where it is for this month; say so in the pull request and propose a work package.
- Run the shared checks:
python3 .github/supply-chain/check_update_bots.pyandpython3 .github/supply-chain/audit_python_tools.py(afterdocs/scripts/dev-setup.sh --pip-audit). - Open the pull request described below.
Shared files (all five repositories)
Some files are identical in all five repositories. When one of them changes, change it in all five, in the same session, so that the five pull requests carry the same copy.
| File | What it pins | How to update it |
|---|---|---|
.github/reuse/requirements.txt, .github/reuse/build-requirements.txt | REUSE and its build backend, for the REUSE workflow | python3 scripts/python-tool-hashes.py 'reuse==<version>' <cutoff> and python3 scripts/python-tool-hashes.py 'poetry-core==<version>' <cutoff> in the docs repository (they need uv); keep the files’ header comments and update their dates. The same lists are req_reuse and req_poetry_core in docs/scripts/cloud-environment-setup.sh, and the docs repository’s tests fail until both agree. |
.github/supply-chain/pip-audit-requirements.txt | pip-audit and its dependencies | python3 scripts/python-tool-hashes.py 'pip-audit==<version>' <cutoff>; keep the header and update the publish dates in it. |
.github/workflows/dco.yml, reuse.yml, supply-chain.yml, workflow-lint.yml, scorecard.yml and the other workflows | GitHub Actions, by full commit SHA with the version in a comment | pinact run --update --min-age 1, then confirm with pinact run --check --verify-min-age --min-age 1 and zizmor --no-config --no-ignores --collect=all --strict-collection ., the commands of the workflow-lint job; if the check reports a commit younger than a day, put that action’s previous pin back for this month. pinact and zizmor’s online audits ask GitHub’s API about the actions’ repositories; set GITHUB_TOKEN if pinact hits the rate limit. In a BayanDocs cloud session the network proxy refuses API requests about repositories that are not attached to the session, so there run zizmor with --offline and pinact with --fix=false --no-api (pins only, no dates), and let the workflow-lint job of the pull request run the full checks. Keep every shared workflow identical in all five repositories. |
.github/workflow-lint/requirements.txt | zizmor, for the workflow-lint workflow | The same pin as req_zizmor in docs/scripts/cloud-environment-setup.sh (the docs repository’s tests fail until both agree): python3 scripts/python-tool-hashes.py 'zizmor==<version>' <cutoff> in the docs repository; keep the file’s header comments and update the version and date in them. |
.github/workflows/workflow-lint.yml | pinact (PINACT_VERSION, PINACT_SHA256) | The same release as the pinact line of BINARIES in docs/scripts/cloud-environment-setup.sh (the docs repository’s tests fail until both agree); the SHA-256 of pinact_linux_amd64.tar.gz from the release’s pinact_<version>_checksums.txt. |
.github/workflows/scorecard.yml, .github/scorecard/policy.yml | OpenSSF Scorecard (SCORECARD_VERSION, SCORECARD_SHA256), and which of its checks raise code-scanning alerts | The newest release of ossf/scorecard published before the cutoff, with the SHA-256 of scorecard_<version>_linux_amd64.tar.gz from the release’s scorecard_checksums.txt. If the new release adds or renames checks, add them to policy.yml with the threshold of policies/template.yml in the newest ossf/scorecard-action release (Scorecard refuses to write its SARIF file when the policy lacks a check). |
.github/workflows/codeql.yml (the same in every repository, pins included, except for its languages, the paths a repository leaves out and the Rust or C++ job) | github/codeql-action (by pinact, like every action) and the CodeQL bundle (CODEQL_VERSION, CODEQL_BUNDLE_SHA256) | After pinact has moved the action, set CODEQL_VERSION to the bundleVersion that the action names in its src/defaults.json at the pinned commit (git show <commit>:src/defaults.json in a clone), and CODEQL_BUNDLE_SHA256 to the hash in that release’s codeql-bundle-linux64.tar.zst.checksum.txt; the bundle’s release must also be at least a day old. CodeQL’s Rust extractor installs, with rustup, the Rust version that the bundle names (FIXED_RUST_TOOLCHAIN in the rust/extractor/src/toolchain.rs of github/codeql at the tag codeql-cli/v<version>; 1.98.1 for 2.27.2), so check that release’s date too. Compare that version with the rust-version of bayan-core and bayan-server (1.99 today): while it is older, the extractor’s cargo check stops at once; once it is the same or newer, cargo check runs the dependencies’ build scripts and procedural macros inside the CodeQL Rust job, whose token may upload results to code scanning and dismiss alerts (security-events: write). Before taking such a bundle, either split those jobs so that the analysis runs with a read-only token and a second job uploads its SARIF file (as scorecard.yml does), or ask the owner to accept the risk and record that in the pull request. |
.github/supply-chain/pip-audit-canary.txt | A deliberately vulnerable pin (jinja2 3.1.4) that proves pip-audit still works | Never update it; it must stay vulnerable. |
After changing any of them, run python3 .github/supply-chain/audit_python_tools.py, which audits every hash-pinned requirements file of the repository.
bayan-core
| Pin | Where | How to update |
|---|---|---|
| Rust toolchain | rust-toolchain.toml (channel), rust-version in Cargo.toml, RUST_STABLE in docs/scripts/cloud-environment-setup.sh, and bayan-server’s copies (the two code repositories use the same version) | The newest stable release published before the cutoff. The gate checks that rust-toolchain.toml and rust-version agree. Rust 1.100 (2026-11-12) is the first release whose Cargo enforces global-min-publish-age: from the session that adopts it, remove the advice to resolve with a nightly (below) and the note about Cargo’s warning from both repositories’ AGENTS.md. |
Float methods banned by clippy.toml | clippy.toml and REQUIRED_FLOAT_METHODS in xtask/src/policy.rs | When the new toolchain stabilizes a floating-point method whose result is not exactly specified (for example gamma, erf or exp10), add it to both and call it in xtask/lint-canary (ADR-0005). |
| Crates | [workspace.dependencies] in Cargo.toml (exact =x.y.z) and Cargo.lock | Raise each exact requirement to the newest version published before the cutoff (see its index line). Then resolve the lockfile with a Cargo that enforces the minimum age: until the workspace is on Rust 1.100, that is the pinned nightly of the cloud environment, cargo +nightly-2026-10-02 update (the name is RUST_NIGHTLY in the setup script), which prints “as of 24 hours ago” and leaves out younger versions. Build and test with the pinned stable toolchain as usual. cargo xtask check-lockfile-age (also part of the gate) then lists every package version the update adds, with its publish date, and checks that each has the checksum crates.io published: copy that list into the pull request. Never edit Cargo.lock by hand: the check reads only the layout Cargo writes. A crate that newly has a build script, or compiles C or C++ code, needs a review of that code and, for native code, an ADR amendment (ADR-0006). |
| wasm-bindgen | WASM_BINDGEN_VERSION and the SHA-256 of each platform’s release archive in scripts/dev-setup.sh, and WASM_BINDGEN_VERSION and the Linux x86-64 archive’s SHA-256 in .github/workflows/artifacts.yml and .github/workflows/text-pipeline.yml; the version must equal the wasm-bindgen crate in Cargo.lock (pinned in [workspace.dependencies]), because the two halves of wasm-bindgen’s glue must match. xtask tests check every copy of the version against the crate and both workflows’ hash against the script’s, and cargo xtask wasm-package refuses a tool of another version | It moves with the wasm-bindgen crate of the Crates row: the newest release published before the cutoff, for the crate and the tool together. Take each archive’s SHA-256 from the .sha256sum file published next to it, check it against the archive you download, and update the version in the script’s archive names. bayan-server pins wasm-bindgen separately for its MLS spike (see its table). |
| Fuzz workspace | fuzz/Cargo.toml (libfuzzer-sys, exact =x.y.z) and fuzz/Cargo.lock, a Cargo workspace of its own for cargo-fuzz | As for the crates: raise the exact requirement, then run cargo +nightly-2026-10-02 update in fuzz/. What the update adds to fuzz/Cargo.lock needs no hand check of dates and checksums any more (BayanDocs/bayan-core#22): the first step of cargo xtask verify checks this workspace like the main one, with cargo xtask check-exact-pins (every dependency pinned exactly; path dependencies only on its own crates and on members of the main workspace, such as bayan-opc and bayan-xml) and cargo xtask check-lockfile-age (Cargo builds exactly what fuzz/Cargo.lock lists, and every package version the update adds to it was published on crates.io at least 24 hours before the commit that adds it, with the checksum crates.io published); copy the list of the versions the update adds to fuzz/Cargo.lock, with their publish dates, from the age check’s output into the pull request (The pull request). cargo xtask verify also checks this workspace with cargo-deny against fuzz/deny.toml (the main policy plus libFuzzer’s NCSA license, ADR-0017, amendment 2026-10-08). Then build the targets and run each one briefly with fuzz/ci.py, which runs them with CI’s settings (fuzz/README.md shows how); the pull request then runs the fuzzing workflow too, because it changes fuzz/, and that workflow also runs both checks first, before it fetches or builds anything of this workspace. |
| Fuzzing in CI | .github/workflows/fuzz.yml: FUZZ_TOOLCHAIN, equal to MIRI_TOOLCHAIN in xtask/src/artifacts.rs and scripts/dev-setup.sh (xtask tests keep all three equal) and written out in fuzz/README.md and fuzz/ci.py, where no test checks it; CARGO_FUZZ_VERSION and CARGO_FUZZ_SHA256, and cargo-fuzz’s version written out in AGENTS.md and fuzz/README.md, where no test checks it either | The same nightly as RUST_NIGHTLY, and the same cargo-fuzz version and SHA-256 as the cargo-fuzz line of BINARIES, both in docs/scripts/cloud-environment-setup.sh: move them together. Step 3’s search finds every other copy: search for the nightly’s old name, and for cargo-fuzz’s old version number on its own (bayan-core’s AGENTS.md writes the tool’s name in bold before it). Where the release publishes no checksum file (cargo-fuzz’s release workflow makes none today), hash the archive you download, check that its cargo-fuzz reports the new version, and say so in the pin’s comment. The pull request then runs the fuzzing workflow, because the workflow changed. |
| Miri nightly | MIRI_TOOLCHAIN in xtask/src/artifacts.rs and scripts/dev-setup.sh (a test keeps them equal); the script installs that nightly with its miri and rust-src components, and CI’s Miri job in .github/workflows/artifacts.yml runs the script | The same nightly as FUZZ_TOOLCHAIN (the row above) and RUST_NIGHTLY: move them together, to a nightly that has the Miri component (rustup refuses to install one that lacks it), and run cargo xtask miri. cargo miri setup downloads the standard library’s own dependencies at the versions of that nightly’s lockfile, outside Cargo.lock and cargo-deny. |
| Node.js | NODE_VERSION in xtask/src/verify.rs, and NODE_VERSION with the SHA-256 of every archive it downloads in .github/workflows/verify.yml, .github/workflows/artifacts.yml, .github/workflows/text-pipeline.yml and scripts/dev-setup.sh (an xtask test keeps the five versions equal). The archives differ: the workflows download the Linux x86-64, macOS arm64 and Windows x64 builds (verify.yml Windows’s node.exe, the other two its .zip), scripts/dev-setup.sh the Linux x86-64, macOS arm64 and macOS x86-64 ones | The same release as bayan-web’s Node.js (its row below) and bayan-server’s scripts/dev-setup.sh, and as the cloud environment’s once the batch of issue #7 is applied (until then it has 24.21.0); no test spans the repositories, so move them together. Take every hash from the release’s SHASUMS256.txt after checking its signature, and find every copy with step 3’s search for the old version and each old hash: text-pipeline.yml writes the Linux hash in three jobs, and AGENTS.md and the header of scripts/dev-setup.sh write the version out. The test compares only the versions, and a wrong hash shows only when its download runs. |
| Chrome for Testing | CHROME_VERSION and CHROME_SHA256 in .github/workflows/text-pipeline.yml: Chromium’s headless shell for Linux x86-64, the browser in which the workflow runs the text pipeline spike; no test checks them | The same build and archive as the headless shell that bayan-web pins for its Playwright (BROWSER_PINS in its scripts/dev-setup.sh, see its Playwright row) and as bayan-server’s chrome-headless-shell: no test spans the repositories, so move the three together. The SHA-256 is that of bayan-web’s linux64 archive, hashed from storage.googleapis.com and confirmed on npmmirror. The pull request then runs the text pipeline workflow, because the workflow changed. |
| Text pipeline fonts | spikes/text-pipeline/fonts/manifest.txt: every download (archive lines) and font (font lines) with its SHA-256, size and address, and in the comment above each the release or commit it comes from; the two fonts marked committed are copies in that folder, which the gate’s determinism test compiles in | For each font, the newest release published before the cutoff, from the source its comment names; update its lines, and replace a committed copy. A new font changes the probes’ layout and raster hashes: run the spike as spikes/text-pipeline/REPORT.md shows, replace spikes/text-pipeline/results/expected.json with the new expectation that node spikes/text-pipeline/js/compare.mjs --expect <results.json> prints, and say in the pull request why the hashes changed (ADR-0004 §4). An update of the spike’s crates (bayan-core’s AGENTS.md lists them) can change the hashes in the same way. The gate’s determinism step and the text pipeline workflow, which the pull request then runs, compare them. |
| cargo-deny | CARGO_DENY_VERSION and the three SHA-256 hashes in .github/workflows/verify.yml, CARGO_DENY_VERSION in xtask/src/verify.rs (a test keeps them equal), bayan-server’s scripts/dev-setup.sh, and the cargo-deny line of BINARIES in docs/scripts/cloud-environment-setup.sh | The newest release published before the cutoff; take each archive’s SHA-256 from its published .sha256 file and check it against the archive you download. |
deny.toml exceptions | [advisories] ignore in deny.toml | As in bayan-server’s row of the same name below, which also covers bayan-core’s six exceptions for crates inside Loro. |
| Shared files | see above |
Gate: cargo xtask verify.
bayan-server
| Pin | Where | How to update |
|---|---|---|
| Rust toolchain | rust-toolchain.toml, rust-version in Cargo.toml and in spikes/mls/Cargo.toml, the builder image in Dockerfile | The same version as bayan-core. The builder image docker.io/library/rust:<version>-alpine<release> is pinned by digest (@sha256:…) with its creation date in the comment above it; the image build fails if its Rust differs from rust-toolchain.toml. No check compares the two rust-version fields with it. |
| Crates | [workspace.dependencies] in Cargo.toml and Cargo.lock | As in bayan-core: raise the exact requirements, cargo +nightly-2026-10-02 update, then the gate. If sqlx changes, regenerate the query metadata with cargo xtask sqlx-prepare against a PostgreSQL server (the command for a throwaway server is in the repository’s AGENTS.md) and commit crates/bayan-db-*/.sqlx/. |
| MLS spike workspace | [workspace.dependencies] in spikes/mls/Cargo.toml (exact =x.y.z) and spikes/mls/Cargo.lock, a Cargo workspace of its own | As for the server’s crates: raise the exact requirements, then run cargo +nightly-2026-10-02 update in spikes/mls/. What the update adds to spikes/mls/Cargo.lock needs no hand check of dates and checksums any more (BayanDocs/bayan-core#22): the first step of cargo xtask verify checks this workspace like the server’s, with cargo xtask check-exact-pins (every dependency pinned exactly; path dependencies only on its own crates and on members of the server’s workspace) and cargo xtask check-lockfile-age (Cargo builds exactly what spikes/mls/Cargo.lock lists, resolved with every feature switched on, so that the packages that only the provisional-pq feature brings in are checked too, and every package version the update adds to it was published on crates.io at least 24 hours before the commit that adds it, with the checksum crates.io published); copy the list of the versions the update adds to spikes/mls/Cargo.lock, with their publish dates, from the age check’s output into the pull request (The pull request). cargo xtask verify also formats, lints, tests, documents and audits it. Keep its wasm-bindgen crate equal to the tool of the next row. |
| MLS spike’s WebAssembly test tools | scripts/dev-setup.sh: the lines of wasm-bindgen (with its test runner), Node.js, chromedriver and Chromium’s headless shell (version, address and SHA-256 of each archive), and the list of versions and dates in its header; CI’s MLS spike job installs them with that script. No test compares them with their copies | wasm-bindgen: the version of the wasm-bindgen crate in spikes/mls/Cargo.lock (its test runner refuses any other), with the SHA-256 from the .sha256sum file published next to the archive. Node.js: the same release as bayan-web’s and bayan-core’s, with the SHA-256 from SHASUMS256.txt after checking its signature. The headless shell: the same build and archive as bayan-web’s and bayan-core’s (see bayan-core’s Chrome for Testing row), and chromedriver the same version, which Chrome for Testing publishes with it; hash both from storage.googleapis.com and confirm them on npmmirror, as the script’s header says. Then run cargo xtask mls-spike-wasm. |
| PostgreSQL image | compose.yaml, the postgres job of .github/workflows/ci.yml, and the docker run example in AGENTS.md | The newest patch of the same major version, created before the cutoff, by digest. All three must agree. |
| cargo-deny and grype | scripts/dev-setup.sh (version, address and SHA-256 of each archive): cargo-deny equal to bayan-core’s and to the cargo-deny line of BINARIES in docs/scripts/cloud-environment-setup.sh; grype equal to entry 1 of issue #7, which the live cloud environment already installs (its setup script, version 2026-10-05, came from that issue), although the repository’s copy of the setup script has no grype line until the batch is applied | As for cargo-deny in bayan-core; grype’s checksums are in its release’s checksums.txt. |
deny.toml exceptions | [advisories] ignore, [licenses] exceptions, [bans] wrappers | Remove any exception that is no longer needed; never add one in a monthly session without a reason in the pull request. In bayan-core, re-check every session the six advisory exceptions for crates inside Loro (accepted by the owner, ADR-0008, amendment 2026-10-08): each reason still holds and Loro still depends on the crate. Remove each one as soon as Loro no longer needs it, and adopt the Loro release that moves to the maintained replacements, imbl and imbl-sized-chunks. In bayan-server, re-check every session the advisory exception for RUSTSEC-2026-0387, a timing leak in crypto-bigint 0.5.5, which only the MLS spike reaches, through the RustCrypto providers of OpenMLS and mls-rs (accepted by the owner on 2026-10-27, BayanDocs/bayan-server#8): deny.toml is shared by the server and the spike, so confirm that the server still does not depend on crypto-bigint, and remove the exception once releases of openmls_rust_crypto and mls-rs-crypto-rustcrypto on elliptic-curve 0.14 are at least 24 hours old and the spike has moved to them, or once the spike is retired. |
| Shared files | see above |
Gate: cargo xtask verify. CI also builds the container and runs scripts/container-smoke-test.sh and grype, and the PostgreSQL tests (cargo xtask test-postgres, cargo xtask sqlx-prepare --check); run them locally when you have Docker and PostgreSQL.
bayan-web
| Pin | Where | How to update |
|---|---|---|
| Node.js | .nvmrc, engines.node in package.json, NODE_VERSION and the per-platform SHA-256 hashes in scripts/dev-setup.sh, and NODE_VERSION and NODE_SHA256 in docs/scripts/cloud-environment-setup.sh; bayan-core and bayan-server pin the same release (see their tables), and no test spans the repositories | The newest patch release of the Active LTS line published before the cutoff, for all of them. Take the hashes from SHASUMS256.txt after checking its signature. Moving to the next LTS major is a decision for the owner when that line becomes Active LTS. |
| pnpm | packageManager and engines.pnpm in package.json, the first YAML document of pnpm-lock.yaml, and PNPM_VERSION and PNPM_SHA512 in docs/scripts/cloud-environment-setup.sh | The newest release published before the cutoff. Write packageManager as pnpm@<version>+sha512.<hex>, where <hex> is the hexadecimal form of pnpm view pnpm@<version> dist.integrity (convert it with python3 -c "import base64, sys; print(base64.b64decode(sys.argv[1].split('-', 1)[1]).hex())" <integrity>); pnpm self-update writes the field without the hash, which the policy check rejects. Set engines.pnpm to the version, then run pnpm install, which records the new pnpm and the integrity of every platform’s @pnpm/exe.<platform> package in the lockfile’s first document. A new pnpm can report settings in pnpm config list --json that scripts/check-policy.ts does not know yet, change its built-in registries or write its files in a new layout; the gate then fails, so review each change and teach scripts/pnpm-settings.ts or scripts/strict-yaml.ts about it, with a test, and update the recorded output in tests/unit/fixtures/pnpm-config.ts. The cloud environment pins the Linux binary separately: PNPM_SHA512 is the hexadecimal form of pnpm view @pnpm/exe.linux-x64@<version> dist.integrity. |
| npm packages | dependencies and devDependencies in package.json (exact versions) and pnpm-lock.yaml | pnpm outdated lists what is behind. pnpm update --latest moves every package to its newest version that pnpm’s 24-hour minimum age allows, and keeps the versions exact. Then put back what must not move: @types/node follows the major version of Node.js in .nvmrc (for Node.js 24, the newest 24.x), and @types/react and @types/react-dom follow React. Look at each new version’s date with pnpm view <package> time --json. |
| Playwright browsers | PLAYWRIGHT_VERSION and the browser archive hashes (BROWSER_PINS) in scripts/dev-setup.sh; bayan-core (CHROME_VERSION and CHROME_SHA256) and bayan-server (chrome-headless-shell and chromedriver in its scripts/dev-setup.sh) use the same build of Chromium’s headless shell, and no test spans the repositories | They change only together with @playwright/test; the header of scripts/dev-setup.sh explains how to list the archives and confirm every hash with both the publisher and npmmirror. Every session also re-checks against npmmirror the two WebKit archives for macOS 26 that the script marks NOT INDEPENDENTLY CONFIRMED (owner decision of 2026-10-04, recorded there): once npmmirror’s copies match the pinned hashes, remove the mark; if they differ, stop and ask the owner. When the headless shell’s build changes, move bayan-core’s and bayan-server’s copies to it (their rows above). |
| Ubuntu packages (the browsers’ libraries) | APT_SNAPSHOT in scripts/dev-setup.sh | The same snapshot date as bayan-desktop’s UBUNTU_SNAPSHOT and the cloud environment’s APT_SNAPSHOT; see bayan-desktop below. |
| bayan-core (the engine) | bayan-core.pin: the bayan-core commit whose WebAssembly package the app serves. scripts/check-engine-pin.sh refuses a commit that is not one of the commits of bayan-core’s main itself (its first parents), or that reached main less than 24 hours before now or before the commit that pinned it, and the check:engine step of pnpm verify fails when engine-package/ was built from another commit | Not part of this session’s pull request: bayan-web changes the pin only in a pull request of its own, saying what changed in bayan-core between the two commits (its AGENTS.md), for example when a work package needs a newer engine. The engine’s Rust toolchain, wasm-bindgen and crates come from that commit, at the versions bayan-core’s own gate checked. Even when the pin does not move, check:engine needs engine-package/: build it with scripts/engine-package.sh before you run the gate. |
| Shared files | see above |
Gate: pnpm verify (it re-installs nothing; run scripts/dev-setup.sh first, which does a frozen install of the new lockfile). The lockfile-integrity check and the audit are part of it.
bayan-desktop
| Pin | Where | How to update |
|---|---|---|
| Qt | deps/qt.json (every field), QT_VERSION in docs/scripts/cloud-environment-setup.sh | ADR-0013 tracks the newest Qt 6 minor release; take its newest patch published before the cutoff. Record qt_released as described in Finding publish dates, and update every repository path, package name and archive list (they contain the version). Install it with scripts/dev-setup.sh, then compare the new installation’s sbom/ folder with the old one: a newly bundled component whose licenses offer no permissive or LGPL option needs the owner’s approval (ADR-0017, amendment “what the license allowlist covers”). The gate’s supply_chain.qt_release_age test fails if the release is younger than 24 hours at your commit; commit the pin before you run it, or it measures against now. |
| Build and lint tools | deps/requirements-tools.txt (CMake, Ninja, clang-format, clang-tidy, with hashes for every platform) | Regenerate it with the uv pip compile … --exclude-newer <cutoff> command in its header, then put the header back and update the versions and publish dates in it. Keep clang-format and clang-tidy on the same LLVM release. |
| Python in CI | Nothing: every job uses the runner image’s own Python 3 (each job of .github/workflows/ci.yml checks that it is 3.11 or newer and prints its version), as in every BayanDocs workflow, so no Python is downloaded | Nothing to update. If a runner image moves to a Python that a hash-pinned requirements file has no wheel for, pip --only-binary :all: fails the job; then regenerate that file. |
| Ubuntu packages | UBUNTU_SNAPSHOT (and the package list UBUNTU_PACKAGES) in .github/workflows/ci.yml and in the C++ job of .github/workflows/codeql.yml, bayan-web’s APT_SNAPSHOT in scripts/dev-setup.sh, and APT_SNAPSHOT in docs/scripts/cloud-environment-setup.sh | A snapshot date before the cutoff, usually the day before the session; keep all four equal. The desktop test supply_chain.workflow_ubuntu_packages fails when the two workflows differ. |
| bayan-core (the real engine, for CI and the tests that use it) | deps/bayan-core.json: bayan-core’s repository URL, the full commit hash (commit) and its date (committed) | During Phase 0, CI builds bayan-core’s C SDK from this commit (ADR-0002 §4). Move it only to a commit on bayan-core’s main (scripts/build-engine-sdk.sh refuses any other), when a work package needs a newer engine or in this session: update commit and committed, then run scripts/build-engine-sdk.sh and BAYAN_REQUIRE_MOCK_DOCUMENT=1 scripts/test-engine-sdk.sh --sdk build/bayan-core-sdk. The Rust version follows that commit’s rust-toolchain.toml, and the crates its Cargo.lock, which bayan-core’s own gate checked. The commit itself need not be 24 hours old, but the Rust version and the crates it brings in must be (ADR-0017, amendment of 2026-10-12). A newer engine can change what the canvas tests expect of the mock document; update them in the same pull request. |
| Shared files | see above |
Gate: cmake --workflow --preset verify; scripts/test-engine-sdk.sh when the change could affect how the engine is linked; and when the bayan-core pin moves, scripts/build-engine-sdk.sh and BAYAN_REQUIRE_MOCK_DOCUMENT=1 scripts/test-engine-sdk.sh --sdk build/bayan-core-sdk.
docs
| Pin | Where | How to update |
|---|---|---|
| Agent environment | scripts/cloud-environment-setup.sh: RUST_STABLE and RUST_NIGHTLY, NODE_VERSION and NODE_SHA256, PNPM_VERSION and PNPM_SHA512, QT_VERSION, the BINARIES list (version, address and SHA-256 of each release archive), the req_* Python lists, and APT_SNAPSHOT | Toolchains equal the code repositories’ pins after their sessions. Code repositories copy three lines of BINARIES, so each moves together with its copies there (no test spans the repositories): cargo-deny (bayan-core’s .github/workflows/verify.yml and xtask/src/verify.rs, bayan-server’s scripts/dev-setup.sh), cargo-fuzz (bayan-core’s .github/workflows/fuzz.yml) and pinact (.github/workflows/workflow-lint.yml, the same file in all five repositories, whose copy here this repository’s tests compare with BINARIES); the grype and wasm-bindgen lines that issue #7 proposes would join them, as copies of bayan-server’s grype and of bayan-core’s and bayan-server’s wasm-bindgen. Each release binary’s SHA-256 comes from its published checksums where there are any; regenerate each req_* list with python3 scripts/python-tool-hashes.py '<tool>==<version>' <cutoff>. Raise SCRIPT_VERSION (and the version in the header), run the script in the session as root (bash scripts/cloud-environment-setup.sh), check bayandocs-tools, and ask the owner to paste the new version into the cloud environment’s settings. Between sessions, changes to the setup script are proposed as entries in issue #7, not in pull requests, so that they land together; when the owner decides to apply that batch, follow the issue’s steps for it in the same pull request. |
| mdBook, lychee, typos | The BINARIES list and req_typos in scripts/cloud-environment-setup.sh | scripts/dev-setup.sh reads its pins from there, so CI uses the same versions. |
| Mermaid | site/mermaid/ | Follow the update steps in site/mermaid/README.md (a maintenance note that is not a page of the website); it also explains why the site stays on Mermaid 11 for now. |
| Shared files | see above |
Gate: scripts/verify.sh.
The pull request
The description follows the hand-off template. In “Dependencies added or changed”, give one row per change:
| Name | Old | New | Published | Notes |
|---|---|---|---|---|
| hyper | 1.11.1 | 1.12.0 | 2026-10-06 15:57 UTC | changelog highlights; what broke and how it was fixed |
Also list the newer versions you skipped because they were younger than 24 hours, so that the next session picks them up, and any update you deferred (with the reason). For Rust repositories, paste the output of cargo xtask check-lockfile-age. If a pin of the cloud environment changed, end with a reminder to the owner to paste the new setup script.
Security-alert session
A Dependabot security alert (repository → Security → Dependabot) names a vulnerable package, the affected versions and, usually, the fixed versions. The nightly CI runs can raise the same alarm first: cargo deny check (bayan-core, bayan-server), scripts/audit.ts (bayan-web, high and critical advisories) and pip-audit (every repository) fail when a new advisory affects a pinned version.
- Understand the advisory. Note the package, the ecosystem, the affected and fixed versions, the severity, and the advisory’s identifiers (GHSA, RUSTSEC, PYSEC or CVE).
- Assess the exposure. Find out which repositories use the package and how:
cargo tree -i <crate>(Rust),pnpm why <package>(web), the requirements files (Python tools, which run only in CI or agent sessions). Read the advisory’s description of the vulnerable function or feature and search the code for it. A package that is only a build or test tool is a smaller exposure than one that ships, but it is still fixed. - Find the lowest fixed version that is at least 24 hours old. Look up the publish times of the fixed versions as in Finding publish dates.
- If such a version exists, update to it, with whatever must move with it, and nothing else:
- Rust: raise the exact requirement in
[workspace.dependencies]if the crate is a direct dependency; thencargo +nightly-2026-10-02 update -p <crate> --precise <version>(until Rust 1.100; after that, the pinned stable Cargo). - npm:
pnpm add -E <package>@<version>(or-Dfor a development dependency); for a package that only arrives through another one, first try updating the package that brings it in; otherwise add an override of the exact registry version tooverridesinpnpm-workspace.yaml(the lockfile-integrity check refuses any override that is not a registry version), and remove it in the next monthly session if it is no longer needed. - Python tools: regenerate the affected requirements file with
scripts/python-tool-hashes.pyand a cutoff 24 hours in the past, in every repository that has the file. - Qt: move to the fixed Qt patch release as in the monthly session.
- Rust: raise the exact requirement in
- If the only fixed version is younger than 24 hours, do not install it. Never bypass the age rule: no
minimumReleaseAgeExclude, no hand-edited lockfile, nocargo updatewith a toolchain that ignores the minimum age, no disabled check. Instead:- Mitigate, in this order of preference: pin an unaffected older version if the vulnerability came with a recent release; switch off the affected feature (a Cargo feature, a configuration setting, a code path you can avoid); or add a narrow guard in our own code that keeps attacker-controlled input away from the vulnerable function. Explain to the owner, in plain language, what remains exposed until the fix can be installed.
- Keep the gates honest while you wait. If the advisory makes a gate fail on every pull request, add a temporary, documented exception, never a broader one: in
deny.toml, an entry in[advisories] ignoreof the form{ id = "RUSTSEC-YYYY-NNNN", reason = "<why we are not exposed or how it is mitigated>; the fix <crate> <version> becomes eligible on <date>; remove then" }; in bayan-web, the advisory’s GHSA identifier inauditConfig.ignoreGhsasofpnpm-workspace.yaml(pnpm audit --ignore <GHSA>writes it) with a comment line above it giving the same information (the checks refuse a comment after the identifier), whichscripts/audit.tsrepeats on every run; for a Python tool, the fix is usually eligible before anyone needs the gate, so wait. A moderate or lower npm advisory does not fail the gate, so it needs no exception. - Come back when the fix is eligible: note the date and time (publish time plus 24 hours), ask the owner to start the follow-up session then (or schedule it, where the session can), and in that session install the fix and remove the exception.
- Run the gate and open one pull request that explains the vulnerability, our exposure, and the change or mitigation, in plain language, with the advisory’s identifiers and links, the fix’s publish date, and (for a mitigation) the date the fix becomes eligible.
When something does not fit
Stop and ask the owner, with your options and a recommendation, when a fix needs a version younger than 24 hours and no mitigation is possible, when an update needs a new dependency, a new license or a new kind of exception, when a check seems wrong, or when a tool behaves differently from what this runbook says. In the last case, also correct this runbook in a pull request to this repository, so that the next session finds it right.