Quick Start · Book · Specification · Benchmarks
Forjar is a single-binary IaC tool written in Rust. It manages bare-metal machines over SSH using YAML configs, BLAKE3 content-addressed state, and deterministic DAG execution. No cloud APIs, no runtime dependencies, no remote state backends.
forjar.yaml → parse → resolve DAG → plan → codegen → execute → BLAKE3 lock
| Terraform | Ansible | Forjar | |
|---|---|---|---|
| Runtime | Go + providers | Python + SSH | Single Rust binary |
| State | S3 / Consul / JSON | None | Git (BLAKE3 YAML) |
| Drift detection | API calls | None | Local hash compare |
| Bare metal | Weak | Strong | First-class |
| Dependencies | ~200 Go modules | ~50 Python pkgs | 17 crates |
| Apply speed | Seconds–minutes | Minutes | Milliseconds–seconds |
# Install from source
cargo install --path .
# Initialize a project
forjar init my-infra && cd my-infra
# Edit forjar.yaml (see Configuration below)
# Preview changes
forjar plan -f forjar.yaml
# Apply
forjar apply -f forjar.yaml
# Check for unauthorized changes
forjar drift --state-dir state
# View current state
forjar status --state-dir stateA forjar.yaml declares machines, resources, and policy:
version: "1.0"
name: home-lab
description: "Sovereign AI stack provisioning"
params:
data_dir: /mnt/data
machines:
gpu-box:
hostname: lambda
addr: 192.168.50.100
user: noah
ssh_key: ~/.ssh/id_ed25519
arch: x86_64
roles: [gpu-compute]
resources:
base-packages:
type: package
machine: gpu-box
provider: apt
packages: [curl, htop, git, tmux, ripgrep]
data-dir:
type: file
machine: gpu-box
state: directory
path: "{{params.data_dir}}"
owner: noah
mode: "0755"
depends_on: [base-packages]
app-config:
type: file
machine: gpu-box
path: /etc/app/config.yaml
content: |
data_dir: {{params.data_dir}}
log_level: info
owner: noah
mode: "0644"
depends_on: [data-dir]
policy:
failure: stop_on_first
tripwire: true
lock_file: true| Type | States | Key Fields |
|---|---|---|
package |
present, absent | provider (apt/cargo/uv), packages |
file |
file, directory, symlink, absent | path, content, owner, group, mode |
service |
running, stopped, enabled, disabled | name, enabled, restart_on |
mount |
mounted, unmounted, absent | source, path, fstype, options |
user |
present, absent | name, groups, shell, home, ssh_keys |
docker |
running, stopped, absent | image, ports, environment, volumes |
cron |
present, absent | name, schedule, command, user |
network |
present, absent | port, protocol, action, from_addr |
pepita |
present, absent | name, cgroups, overlayfs, netns, seccomp |
model |
present, absent | name, source, format, quantization, checksum, cache_dir |
gpu |
present, absent | driver_version, cuda_version, devices, persistence_mode, compute_mode |
Use {{params.key}} to reference global parameters in any string field. Templates are resolved before codegen.
Reusable, parameterized resource patterns (like Homebrew formulae):
# recipes/dev-tools.yaml
name: dev-tools
version: "1.0"
inputs:
user:
type: string
required: true
shell:
type: enum
values: [bash, zsh, fish]
default: zsh
resources:
packages:
type: package
provider: apt
packages: [build-essential, cmake, pkg-config]
dotfiles:
type: file
state: directory
path: "/home/{{inputs.user}}/.config"
owner: "{{inputs.user}}"
mode: "0755"- Parse — Read
forjar.yaml, validate schema and references - Resolve — Expand templates, build dependency DAG (Kahn's toposort, alphabetical tie-break)
- Plan — Diff desired state against BLAKE3 lock file (hash comparison, no API calls)
- Codegen — Generate shell scripts per resource type
- Execute — Run scripts locally or via SSH (stdin pipe, not argument passing). Files > 1MB use copia delta sync (only changed blocks transferred)
- State — Atomic lock file write (temp + rename), append to JSONL event log
On first failure, execution stops immediately. Partial state is preserved in the lock file. No cascading damage. Re-run to continue from where it stopped.
- Local:
bashvia stdin pipe (for127.0.0.1/localhost) - SSH:
ssh -o BatchMode=yeswith stdin pipe (no argument length limits)
cargo bench| Operation | Input | Mean | 95% CI |
|---|---|---|---|
| BLAKE3 hash | 64 B string | 27 ns | +/- 0.5 ns |
| BLAKE3 hash | 1 KB string | 92 ns | +/- 1.2 ns |
| BLAKE3 hash | 1 MB file | 172 us | +/- 0.4 us |
| YAML parse | 500 B config | 20.7 us | +/- 0.2 us |
| Topo sort | 100 nodes | 34.6 us | +/- 0.4 us |
| Copia signature | 1 MB file | 294 us | +/- 0.3 us |
| Copia signature | 4 MB file | 1.19 ms | +/- 0.01 ms |
| Copia delta | 4 MB, 2% change | 1.18 ms | +/- 0.01 ms |
| Copia patch gen | 1 MB, 10% change | 60 us | +/- 0.3 us |
Criterion.rs, 100 samples, 3s warm-up. Run locally to reproduce.
10 testable claims with linked tests (click to expand)
BLAKE3 of identical inputs always produces identical outputs.
Tests: test_fj014_hash_file_deterministic, test_fj014_hash_string
Same dependency graph always produces the same execution order.
Tests: test_fj003_topo_sort_deterministic, test_fj003_alphabetical_tiebreak
Second apply on unchanged config produces zero changes.
Tests: test_fj012_idempotent_apply, test_fj004_plan_all_unchanged
Circular dependencies are rejected at parse time.
Tests: test_fj003_cycle_detection
Lock hashes are derived from desired state, not timestamps.
Tests: test_fj004_hash_deterministic, test_fj004_plan_all_unchanged
Lock writes use temp file + rename. No corruption on crash.
Tests: test_fj013_atomic_write, test_fj013_save_and_load
Invalid typed inputs are rejected before expansion.
Tests: test_fj019_validate_inputs_type_mismatch, test_fj019_validate_inputs_enum_invalid
Single-quoted heredoc prevents shell expansion in file content.
Tests: test_fj007_heredoc_safe
Fewer than 20 direct crate dependencies (currently 17 runtime + 1 build). Single binary output.
Verify: cargo metadata --no-deps --format-version 1 | jq '[.packages[0].dependencies[] | select(.kind == null)] | length'
First failure stops execution. Previously converged state is preserved.
Tests: test_fj012_apply_local_file
cargo test # 2159 unit tests
cargo test -- --nocapture # with output
cargo test planner # specific module
cargo bench # Criterion benchmarks
cargo clippy -- -D warnings # lintMIT OR Apache-2.0