Status: maintained alongside the code
Related: README, style guide, API contracts, and command reference.
The toolchain is pinned in rust-toolchain.toml; rustup installs it on first use. No
system library is needed: the Linux Secret Service client is pure Rust.
cargo build --locked
./target/debug/voltage --helpvoltage --version names the build after the Cargo version. A clean build of its own
release tag shows the commit and date, such as voltage 0.1.0 (a627981 2026-09-24).
Any other build shows git describe, such as voltage 0.1.0 (v0.1.0-14-g0e6078b-dirty):
14 commits after the tag, with uncommitted changes. When no release tag is reachable it
shows only the commit. Compare it with git rev-parse --short HEAD to catch a stale
binary. -dirty reflects the tree when build.rs last ran, which is after a commit,
checkout, tag, or change under src/, Cargo.toml, or Cargo.lock. A build outside a
git checkout shows the Cargo version alone.
Project-local tool installs belong under the gitignored .work/ directory, for example
cargo install --root .work/tools ..., so the global ~/.cargo/bin stays untouched.
Install Nix with flakes enabled and run nix develop from the repository root. The
committed flake.lock pins nixpkgs and rust-overlay; the shell reads the exact Rust
version and components from rust-toolchain.toml and includes Python 3 and
cargo-machete. On entry from the repository root it runs cargo build --locked and
adds the checkout's target/debug to PATH, so voltage --help uses the development
binary. Cargo's incremental build does not recompile unchanged sources. If the build
fails, the shell remains available but does not add the binary to PATH. Once inside,
run the verification gate normally.
No OpenSSL or D-Bus development packages are required. Real Linux native keychain use
still needs a running, unlocked Secret Service.
For automatic activation, install direnv and enable its shell hook (for example,
eval "$(direnv hook zsh)" in .zshrc, or use bash in .bashrc). Optionally install
nix-direnv for cached activation, then run direnv allow in the repository root. Leave
the directory to unload the shell. .envrc only activates the flake: it does not load
.env, credentials, or staging endpoints.
After editing Rust code in an already-open shell, run cargo build --locked to
update the same voltage binary on PATH (or use cargo run --locked -- ... to
build and run in one command). Direnv does not watch Rust source changes. After
editing the shell, run nix develop again (or direnv reload if using direnv).
To update the pinned sources deliberately, run nix flake update, review the
flake.lock diff, rerun the full gate in nix develop, and commit the lock file.
When changing Rust versions or components, update rust-toolchain.toml as well and
verify rustc --version in the shell. Supported shell targets are aarch64/x86_64
macOS and Linux; all four shell derivations evaluate, but only aarch64 macOS was built
and tested with Nix. The other Nix shells remain unverified at runtime (CI builds those
platforms using rustup). Native Windows Nix is not
supported. This is a development environment, not a release artifact or a Nix package.
Nix and direnv are not required: rustup toolchain install installs the pinned
Rust toolchain, and cargo install cargo-machete --version 0.9.2 --locked supplies
the extra verification tool for the existing rustup workflow.
Run before every push. CI runs the same commands.
cargo fmt --check
cargo clippy --locked --all-targets -- -D warnings
cargo machete --with-metadata
cargo test --locked
python3 scripts/check-coverage.pyIntegration tests start mock HTTP servers on port 0 and never contact a real service. They cover every operation in the contract, query encoding, complete JSON bodies, credential selection, describe-by-default changes, secret output, pagination, payment waits, uncertain submissions, checkout tokens and streams, and the credential lifecycle. Live payment tests must use an explicitly configured test-network wallet; routine tests never send a live payment.
The manual CRAP risk check is described in the style guide.
.github/workflows/ci.yml runs on pull requests, pushes to master, and on demand:
lint(Ubuntu): formatting, Clippy with warnings denied,cargo machete, and the contract mapping check.test(macOS arm64 and x86-64, Ubuntu x86-64 and arm64): compiles and runs the whole suite, then packages the release archive and smoke-tests the extracted binary.windows: Clippy and the unit tests, then the package smoke test. The integration suite asserts Unix ownership and permissions, so it does not run on Windows.
.github/workflows/audit.yml audits Cargo.lock against RustSec daily and whenever a
manifest or lock file changes.
Releases are tag driven (.github/workflows/release.yml). Push a stable vX.Y.Z tag that
matches the Cargo.toml version and sits on master:
git tag v0.2.0 && git push origin v0.2.0The workflow verifies the tag, reruns the gate on every platform, builds the five archives
(scripts/package.sh), attests each archive's SLSA build provenance (actions/attest),
writes SHA256SUMS and the Homebrew formula (scripts/release-manifest.py), signs the
checksums with Sigstore keyless signing (SHA256SUMS.sigstore.json), and opens a draft
GitHub release. Publish the draft only after the staging acceptance below is recorded.
The API snapshot, the explicit command mapping, and the auth contract live under api/;
api/README.md explains their provenance and how to refresh each one.
build.rs generates the operation registry from the snapshot and mapping and fails the
build when they disagree. docs/commands.md is generated; regenerate it with
python3 scripts/check-coverage.py --write after a contract change and never edit it by hand.
scripts/voltage-localruns the development binary with isolated credentials, loopback auth on port 8081, and a disabled API endpoint.--buildcompiles it first.scripts/voltage-stagingruns the development binary against staging. It reads the staging endpoints (VOLTAGE_STAGING_API_URL,VOLTAGE_STAGING_AUTH_URL) and, only for API-key runs, the optionalVOLTAGE_API_KEY,VOLTAGE_ORGANIZATION_ID,VOLTAGE_ENVIRONMENT_ID, andVOLTAGE_WALLET_IDfrom the repository's private.env(copy.env.example, fill it in,chmod 600 .env) without evaluating it as shell code, keeps CLI state under.work/staging/cli, and refuses--config-dir,--api-url, and--auth-urlso a staging command cannot target another deployment. Browser login and profiles need no credential variables. The repository itself names no deployment. Before creating a payment, confirm the wallet's reported network is a test network.scripts/package.sh TARGETbuilds the release archive for one Rust target and smoke-tests the extracted binary.scripts/release-manifest.py VERSION DIRwritesSHA256SUMSandvoltage.rbfor the archives inDIR.scripts/check-coverage.pyverifies that the API snapshot and the command mapping agree, that every auth-service route the login, credential, and profile commands use exists in the auth contract, and thatdocs/commands.mdis current (--writeregenerates it).scripts/crap-report.shmeasures the CRAP score of every function; see the style guide.
scripts/voltage-staging is a drop-in voltage for the staging deployment. Alias it for the
shell session so commands read exactly as they do in the README, then work through a profile
the same way you would against production.
-
Copy
.env.exampleto.env, setVOLTAGE_STAGING_API_URLandVOLTAGE_STAGING_AUTH_URL(ask the team for the current hosts), andchmod 600 .env. Leave the credential variables commented out; browser login needs none of them. -
From the repository root, alias the wrapper and build the development binary:
alias voltage="$PWD/scripts/voltage-staging" voltage --build
-
Sign in through the browser and discover your organization and environment. These two list commands require a browser login; an API key cannot use them.
--credential-store filekeeps the login under.work/staging/cliwith the rest of the staging state.voltage login --account me --credential-store file voltage organizations list --account me voltage environments list --account me --org ORG_ID
-
Save a profile and use it exactly as the README quick start does for production:
voltage profiles create staging --account me --org ORG_ID --env ENV_ID voltage wallets list --profile staging voltage payments receive --profile staging --wallet WALLET_ID \ --currency btc --kind bolt11 --amount 1000 --unit sats --qr
Once the profile exists, fold it into the alias to drop the flag as well:
alias voltage="$PWD/scripts/voltage-staging --profile staging". -
When finished, revoke the session and remove every trace of the staging state:
voltage logout --account me rm -rf .work/staging unalias voltage
The wrapper keeps staging state apart from ~/.config/voltage, passes the endpoints from
.env as --api-url and --auth-url, and refuses those flags and --config-dir on the
command line. A profile ignores VOLTAGE_WALLET_ID and the other ambient variables, so pass
--wallet explicitly. Confirm a wallet's reported network is a test network before creating
a payment. Browser login needs the device-login release deployed on the staging auth service.
Complete these steps against the deployed staging services before publishing a release, and record the deployed revisions and results in the validation baseline below.
- Confirm browser login, refresh, and organization exchange against the deployed auth service, and that its verification URL points at the deployed approval page.
- Complete CLI approval with an existing browser session, with a fresh login, and with MFA.
- Verify denial, expiry, polling slowdown, single redemption, and independent sessions on two devices.
- Verify organization discovery, organization switching, exchanged API access, and permission rejection.
- Verify concurrent local processes, refresh rotation, replay detection, logout, and global signout.
- Check native credential storage on each release platform and explicit file storage on headless Linux.
- Verify ingress rate limits and code/token redaction.
- Run payment and checkout acceptance with an explicitly selected test-network wallet.