Releases
Release keys
Releases are signed with minisign. The binary embeds two public keys: the current key and the next key. The next key is embedded one release before its first use, so a rotation never needs a flag day.
| Label | Key id | File | Public key |
|---|---|---|---|
| current | 5F6E09C78F555F34 | keys/vibeke-2026.pub | RWQ0X1WPxwluX2gFO4vO586PSTdpSfJqrb+xsQnZ2ctND/VDw7VCWx5z |
| next | 69536A23D04E2C7C | keys/vibeke-next.pub | RWR8LE7QI2pTaSsb4srEFbF1j78fXZzbORy4KGRzHErddJwSJLxwqH3x |
The key id is the fingerprint minisign prints for a key. To check a download by hand:
minisign -V -P RWQ0X1WPxwluX2gFO4vO586PSTdpSfJqrb+xsQnZ2ctND/VDw7VCWx5z -m SHA256SUMS
sha256sum -c --ignore-missing SHA256SUMS
The keys live in vk_remote::bootstrap (KEY_CURRENT, KEY_NEXT, RELEASE_KEYS) and in scripts/install.sh. A test (scripts/tests/release-sign-test.sh) checks that those two and keys/*.pub agree. The secret keys stay on the maintainer's Mac in ~/.vibeke-release-keys/ and are never committed, uploaded to CI or read by any script in this repository. Only minisign reads them, and it asks for the password on the terminal.
What verification requires
Every release path requires a valid signature by the current or the next key. The error messages name both key ids.
| Path | What is verified |
|---|---|
scripts/install.sh | SHA256SUMS.minisig over SHA256SUMS with the minisign tool (brew install minisign), then the binary against its SHA256SUMS entry. |
vibeke ssh, vibeke machine upgrade (bootstrap = "push") | A cached or VIBEKE_ARTIFACT_DIR binary: its SHA256SUMS entry and the signature, before upload. The remote re-checks the sha256 before the switch. |
bootstrap = "remote-download" | The laptop downloads and verifies manifest.json and manifest.json.minisig. The signature must name version:<v> in its trusted comment and <v> must be this build's version. The remote then downloads the file and checks the manifest's sha256. |
vibeke update | A cached or --from binary: checksum and signature before it is executed. |
vibeke integration update (manifest channel) | index.json.minisig over the index, by the same two keys. The channel has no key of its own. |
Unsigned development builds
VIBEKE_ALLOW_UNSIGNED=1 accepts an artifact whose signature cannot be verified, with a warning that names the file and its SHA-256. It does not waive the checksum:
- The expected checksum must come from
SHA256SUMSor a<binary>.sha256file next to the binary. - A missing checksum file or a mismatch is always refused.
- The installer accepts the opt-in the same way, with
SHA256SUMSfrom the download. remote-downloadhas no unsigned mode, because there is no local file to checksum.- The manifest channel keeps its separate development opt-in
VIBEKE_ALLOW_UNSIGNED_MANIFESTS=1. The sha256 and serial checks still apply.
SSH can upload the running binary when the host platforms match, but only with VIBEKE_ALLOW_UNSIGNED=1. Without an external checksum, its hash protects only the transfer.
The remote host checks the uploaded file before it changes current. On mismatch, it preserves the previous version. Where mv -T is available, the change uses an atomic rename:
ln -s versions/<v> current.new && mv -Tf current.new current
Other systems use ln -sfn, which is not atomic. For cached updates, the directory name supplies the version. With --from, the verified binary supplies it.
Checksums alone detect corruption. They do not prove who built the file: an attacker who replaces both files passes an unsigned check. The signature is what proves the maintainer's key signed the checksum list.
Release repository and tokens
The default release URL is https://github.com/MidgardAI/vibeke/releases/download/v<version>. Set VIBEKE_RELEASE_URL for a mirror, or VIBEKE_INSTALL_FROM=<dir> for offline installation.
While the repository is private, GitHub does not serve the plain download URL without authentication. The installer and remote-download therefore honour VIBEKE_GITHUB_TOKEN, else GITHUB_TOKEN:
- With a token and a
github.com/<owner>/<repo>/releases/download/...URL, the file is fetched through the GitHub API asset endpoint (/repos/<owner>/<repo>/releases/assets/<id>) withAccept: application/octet-streamandAuthorization: Bearer <token>. - curl reads the headers from stdin, so the token is never on a command line (
ps). Nothing prints it. Error messages say only that a token is needed. - The token is sent only to
github.comrelease URLs and the GitHub API, never to a mirror inVIBEKE_RELEASE_URL. curl drops it when GitHub redirects to its storage host. - For
remote-downloadthe laptop resolves the asset URL. The remote then runscurlwith the headers from a here-document on the stdin ofsh -s, so the token is not visible in the remote's process list or shell history either. - A fine-grained token with read access to the repository's contents is enough.
Public releases need no token. VIBEKE_GITHUB_API_URL overrides the API base (tests, GitHub Enterprise).
Build release files locally
scripts/release-build.sh <version> builds the artifacts reproducibly into dist/<version>/. It checks that <version> matches the workspace Cargo.toml, builds with SOURCE_DATE_EPOCH from the commit, --remap-path-prefix, --locked and stripped symbols (the environment scripts/repro-check.sh uses), and writes a .sha256 file per binary plus SHA256SUMS. It signs and publishes nothing.
| Artifact | Target triple | Built by |
|---|---|---|
vibeke-macos-aarch64 | aarch64-apple-darwin | cargo build --release, on an Apple-silicon Mac |
vibeke-linux-x86_64 | x86_64-unknown-linux-musl | cargo zigbuild --release |
vibeke-linux-aarch64 | aarch64-unknown-linux-musl | cargo zigbuild --release |
A target whose toolchain is missing is skipped with a notice. Use mise exec -- sh scripts/release-build.sh <version> so Zig and cargo-zigbuild are on PATH. --repro-check first runs scripts/repro-check.sh for both Linux targets. --only macos|linux builds one family.
Linux binaries use static musl linking. Zig 0.16 provides the C toolchain for libghostty-vt and linking. mise.toml selects the Zig version.
mise run dist (scripts/dist.sh) is the development variant. It also copies the files to ~/.cache/vibeke/releases/<version>/, which SSH installation and vibeke update read. Set VIBEKE_RELEASES_DIR to use another cache directory. Its output is unsigned, so using it needs VIBEKE_ALLOW_UNSIGNED=1 or a signed SHA256SUMS.
Release steps
Signing happens on the maintainer's Mac. CI never holds a signing secret.
- Update the workspace version in
Cargo.tomland commit it. - Run
mise run ci, thenmise run repro-check(see the hardening guide). - Tag and push:
git tag v<version> && git push origin v<version>. - The
releaseworkflow (.github/workflows/release.yml) builds the macOS and Linux artifacts and creates a draft releasev<version>with the unsigned binaries and.sha256files. It needs no secret beyond the repository's ownGITHUB_TOKEN. - Download the draft's files into one directory, for example with
gh release download v<version> --dir dist/<version>(a private repository needsgh auth login). Alternatively build locally withscripts/release-build.sh <version>and usedist/<version>/. If you built both, compare the sha256 of the files first: they should be identical. - Sign:
scripts/release-sign.sh dist/<version>. The script writesSHA256SUMSandmanifest.json, then runs minisign twice (minisign -S -s ~/.vibeke-release-keys/vibeke-2026.key -m <file> -t "vibeke v<version>"). Enter the key password when minisign asks. The script finishes by verifying both signatures with the public key embedded in the binary, and fails if they do not verify. - Upload the four signing outputs to the draft:
gh release upload v<version> dist/<version>/SHA256SUMS dist/<version>/SHA256SUMS.minisig dist/<version>/manifest.json dist/<version>/manifest.json.minisig. Also uploadscripts/install.shif the release should carry the installer. - Check the result with a fresh checkout of the files:
minisign -V -P <public key> -m SHA256SUMS. - Publish the draft (
gh release edit v<version> --draft=false). - Smoke test: run
scripts/install.shwithVIBEKE_VERSION=<version>(andGITHUB_TOKENwhile the repository is private) on a cleanHOME.
manifest.json lists {version, artifacts: [{target, sha256, url}]}. Its signature carries the trusted comment vibeke v<version> version:<version>, so an old manifest cannot be replayed under a new version.
Key rotation
The binary embeds the current and the next key. To rotate:
- Release N is signed with the current key and already embeds the next key. Binaries from release N accept signatures by either key.
- Release N+1 is signed with the next key:
scripts/release-sign.sh --key next dist/<version>(it uses~/.vibeke-release-keys/vibeke-next.keyand verifies againstKEY_NEXT). Binaries from release N accept it, so they can update to N+1. - In release N+1 (or later), promote the keys in
vk_remote::bootstrapandscripts/install.sh: the old next key becomes the current key, and a newly generated key becomes next. Updatekeys/and the tables here. - Announce each rotation one release ahead.
If a key is lost or exposed, publish the replacement through a separate trusted channel and state the first version that uses it. Binaries that embed only the exposed key need a manual reinstall.
Not provided
- Sigstore build provenance. Spec 09 §10 lists it as a later addition.
- An Apple Developer ID: the macOS binary is not notarized, so Gatekeeper may warn when it is downloaded in a browser. A binary fetched by
curlor the installer carries no quarantine flag.
API compatibility and generated references
The server method tables generate docs/api/methods.json and docs/api/README.md. docs/api/vibeke-1.frozen.json records protected methods and access flags.
The compatibility test rejects removals or changes to protected access flags. Additions pass. The policy remains a draft until version 1.0.
Before a release, review the catalog changes. Then update the snapshot:
VIBEKE_UPDATE_API_FREEZE=1 cargo test -p vibeke --test api_docs vibeke_1_freeze
Use VIBEKE_API_FREEZE_ALLOW_BREAK=1 only for a deliberate compatibility change before version 1.0.
After a method, command, or configuration change, regenerate the reference:
VIBEKE_UPDATE_DOCS=1 cargo test -p vibeke --test api_docs
Commit the generated files in docs/api/ and docs/site/src/reference/.
mise run docs builds the mdBook site when mdbook is installed.
Fuzz tests, performance limits, and reproducible builds
This page describes the checks from spec 11 M6 and spec 10, sections 1 and 6.
The bounded fuzz tests in crates/vk-fuzz run with the normal test suite.
The other checks are separate from mise run ci.
Fuzz tests
Each target is a function with the type fn(&[u8]) in crates/vk-fuzz/src/targets.rs.
The functions must not panic, stop responding, or allocate memory without a limit.
Two test systems use these functions.
| Test system | Location | Toolchain | Runs in CI |
|---|---|---|---|
| Deterministic tests with random bytes and modified seeds | crates/vk-fuzz/tests/random.rs | Stable Rust | Yes. Default: 300 cases per target. The vt_parse and mux_frame_decode targets run 30 cases each. |
| cargo-fuzz with libFuzzer | fuzz/ | Nightly Rust and cargo-fuzz | No |
The stable tests use the generator and mutator in crates/vk-fuzz/src/rng.rs.
They do not add proptest, arbitrary, or bolero to Cargo.lock.
The libfuzzer-sys dependency belongs to the separate workspace in fuzz/Cargo.toml.
It does not change the results of cargo deny check for the root workspace.
Targets
| Target | Input | Required result |
|---|---|---|
holder_proto_decode | FrameBuf chunks and raw postcard data as ToHolder / FromHolder | No panic. Reject oversized input. |
render_frame_decode | FrameBuf chunks and raw postcard data as ServerFrame / ClientFrame | No panic |
jsonrpc_decode | Line-based parsing of rpc::Request / Response / Notification | No panic |
key_grammar | parse_key, parse_binding, expand_range | format_key followed by parse_key returns the original value |
vt_parse | Random bytes and resizes through vk_term::Engine::feed | No panic or abort. Complete within the time limit. |
socks5_handshake | Bytes passed to vk-preview socks::handshake | No panic |
mux_frame_decode | Bytes passed to the vk-remote frame decoder and a live Mux | No panic or blocked response |
hook_payloads | Arbitrary JSON passed to interaction_from_hook for each harness and event | No panic |
policy_match | URLs, allow rules, and IP addresses passed to vk-browser::policy | No panic. Block metadata and link-local addresses unless a rule permits them. |
kitty_probe | Terminal replies and SGR mouse reports passed to vk-browser::probe | No panic. Consumed length stays within bounds. |
compat_import | Herdr config TOML and session JSON | No panic |
manifest_toml | Harness manifests passed to parse_raw, Loaded::new, its methods, and VersionReq | No panic |
Seed files in fuzz/corpus/<target>/ include valid messages, test fixtures, and boundary cases.
To regenerate them, run:
cargo test -p vk-fuzz --test random export_seed_corpus -- --ignored
Run the tests
For a bounded run with stable Rust, use:
mise run fuzz-smoke
For more cases with arithmetic overflow checks, use a debug build:
VK_FUZZ_CASES=100000 VK_FUZZ_SEED=3 cargo test -p vk-fuzz --test random
For a libFuzzer run, install nightly Rust and cargo-fuzz first. Then run:
FUZZ_TIME=300 mise run fuzz
To test one target, set FUZZ_TARGET:
FUZZ_TARGET=policy_match mise run fuzz
If nightly Rust or cargo-fuzz is missing, mise run fuzz prints installation instructions.
It then exits with status 0 without running the tests.
A failed test prints the exact input. Add that input to a regression test beside the affected code. Also add it to the seed files for that target.
Fixed defects
The policy tests found an IPv4-mapped IPv6 prefix error.
A rule such as ::ffff:10.0.0.7/128 became an IPv4 address but kept its IPv6 prefix length.
The expression u32::MAX << (32 - bits) then overflowed.
This caused a panic in debug builds and an incorrect mask in release builds.
The prefix now starts at bit 96.
A mapped rule with a prefix below 96 matches no addresses.
The regression test is mapped_v6_rule_prefix_is_relative_to_bit_96.
Planned OSS-Fuzz integration
The target functions and cargo-fuzz directory already support separate builds. The remaining work is:
- Add input dictionaries where necessary. Examples include terminal escape sequences, JSON-RPC method names, and key names.
- Submit
projects/vibeke/togoogle/oss-fuzzwith the files below. - Build the native VT engine with the OSS-Fuzz
$CCand$CFLAGSvariables. This lets ASan check the C code. - Set
-rss_limit_mbto 256 forvt_parse. - Add a regression test and a seed for each reported defect.
- Run nightly tests in project CI for one CPU-hour per target.
- Before version 1.0, run all targets under OSS-Fuzz for seven days without a new crash.
| File | Contents |
|---|---|
project.yaml | Rust language, repository, contact, libFuzzer engine, address sanitizer, and x86_64 architecture. Add the undefined behavior sanitizer after the native VT engine passes its checks. |
Dockerfile | Base image gcr.io/oss-fuzz-base/base-builder-rust, Zig 0.16, and the repository. Zig builds the vendored libghostty-vt library. |
build.sh | Build with cargo fuzz build --fuzz-dir fuzz -O. Copy binaries to $OUT. Create $OUT/<target>_seed_corpus.zip from each seed directory. |
Some planned targets still need test support:
vt_resize_interleaveandserializeneed an API to compare snapshots.compat_socketneeds a socket test driver.transcript_parseneeds a target.osc_imageneeds tests for decoder limits.
Performance limits
mise run perf-budgets runs scripts/perf-budgets.sh.
It compares measurements with the limits in spec 10, section 1.
Results go to target/perf-budgets/.
By default, the command reports results and exits with status 0.
Set PERF_STRICT=1 to fail the command when a checked limit is exceeded.
| Measurement | Command or source | Limit |
|---|---|---|
| VT parser throughput | cargo test --release -p vk-term --test recovery throughput -- --ignored | At least 300 MB/s |
| Added keystroke latency | vibeke debug latency | p50 at most 1 ms. p99 at most 3 ms. |
| Remote bandwidth | vibeke debug bandwidth, with PERF_MACHINE=<label> | Idle: 0 B/s. Unfocused spinner: at most 2 KiB/s. Focused spinner: at most 8 KiB/s. Results require manual review. |
| Idle CPU, memory, and wakeups | vibeke debug idle. Set PERF_IDLE=0 to skip this check. | Server: at most 0.3% CPU, 2 wakeups/s, and 25 MiB. Holder: at most 2 MiB. TUI: at most 30 MiB. |
| VT conformance | cargo test -p vk-term --test conformance, also in mise run test | No unexpected failures. Set VK_CONFORMANCE_REPORT=1 to print category counts and expected failures. |
| Browser frames | cargo run -p vk-browser --example bench, with PERF_BROWSER=1 | Manual review against spec 10, section 1.6 |
The idle test uses a separate server with 30 idle panes and an attached TUI without a display. It records the system load. On a busy host, it marks results with “(loaded)”.
These limits apply to the two reference machines in spec 10, section 2.1.
Results from other hardware give an estimate.
CI does not yet reject a pull request when these measurements exceed a limit.
That check needs a store of past results.
The planned check also rejects regressions above 10% against the median of the last five main runs.
Reproducible builds
mise run repro-check runs scripts/repro-check.sh.
The script builds the Linux musl release binary twice in separate target directories.
It uses cargo zigbuild --release --locked and compares the SHA-256 hashes.
Set REPRO_TARGETS to select targets.
The default is x86_64-unknown-linux-musl.
Both builds use these settings:
SOURCE_DATE_EPOCHis the HEAD commit time unless explicitly set. Releases use the tag commit time.--remap-path-prefixcovers the workspace,CARGO_HOME,RUSTUP_HOME, and target directory.- Compiler settings include
-C strip=symbolsandCARGO_INCREMENTAL=0. - Environment settings include
TZ=UTCandLC_ALL=C.
Recorded result
On October 6, 2026, two clean builds on one macOS arm64 host produced the same binary.
The source was commit 858036a with the M6 documentation changes.
The tools were Rust 1.99.0 and Zig 0.16.0.
The target was x86_64-unknown-linux-musl.
Each cold build took approximately 14 minutes.
Both builds produced this SHA-256 hash:
40e33616587d25f66ba32e9665da2eebec01bd3d9ce7f008aa8f9017d91257a2
This result covers two builds on one machine.
It does not verify aarch64-unknown-linux-musl, separate machines, different checkout paths, or different host operating systems.
It also does not use a fixed container image.
The check does not calculate a separate hash for the vendored libghostty-vt Zig cache. The final binary comparison includes the output of the Zig build.
dist.sh does not yet use these build settings.
Thus, the result does not cover artifacts from mise run dist.
CI does not run this check yet.
Separate CI runners and published release digests remain planned work.