Skip to main content

Desired-state infrastructure convergence engine

Project description

CI Crates.io PyPI License: MIT codecov

verg

Desired-state infrastructure convergence engine. A fast, stateless alternative to Ansible, built in Rust.

Pre-1.0: verg is under active development. Expect breaking changes between minor versions.

Features

  • Fast - pushes a single static binary to each target over SSH; the agent executes locally, so there are no per-task SSH round-trips and no Python dependency on the target.
  • Stateless - every run reads actual system state and converges to desired state. No state files are kept on the control host between runs.
  • Dynamic inventory - generate hosts from a command (cloud, CMDB, scripts) and merge them with static hosts.
  • Simple - declare state in TOML files with Jinja2 templating for dynamic values. Strict config validation catches typos before any SSH connection is opened.
  • Agent-friendly - --json output, verg schema for machine-readable resource schemas, and structured exit codes make verg easy to drive from scripts and AI agents.
  • Secure by default - agent binary is checksum-verified before and after transfer; strict SSH host key checking; config validated locally before touching any target.
  • Continuous enforcement - agents pull and re-converge on a schedule with no central server. See docs/continuous-enforcement.md.
  • Encrypted secrets - secrets are encrypted at rest with age, decrypted on the control host at build time, and injected under the secret.* template namespace. See docs/secrets.md.
  • Extensible - define your own resource types as TOML data in verg/resources/; no recompile required.
  • Native providers - custom resource types as embedded-source scripts (any language) over a JSON protocol, self-contained in the bundle.

Install

# Cargo (crates.io)
cargo install verg

# PyPI
pip install verg

# Homebrew (tap)
brew install rvben/tap/verg

Quick Start

# Scaffold a new project in ./verg/
verg init

# Edit your inventory and desired state
$EDITOR verg/hosts.toml
$EDITOR verg/state/base.toml

# Preview what would change (dry-run, no modifications)
verg diff --targets all

# Apply to all hosts
verg apply --targets all

How It Works

verg (control CLI, runs on your machine) reads TOML state files and your host inventory, builds a host-specific bundle for each target, then pushes a compiled verg-agent binary over SSH. The agent reads the bundle from stdin, checks actual system state, converges each resource to desired state in dependency order, and returns a JSON summary to stdout. The control CLI aggregates results and presents them.

The agent is a static binary with no runtime dependencies. It is checksum-verified before deployment and runs entirely on the target - no per-resource SSH calls from the control host are needed once the agent is running.

Concepts

Project layout

A verg project lives in a directory named verg/ by default (override with --path or the VERG_PATH environment variable). Inside it:

verg/
  hosts.toml       # host inventory
  groups/          # one .toml per group, each with a [vars] table
  state/           # desired-state declarations; loaded in lexicographic order
  files/           # static files referenced by file resources
  templates/       # Jinja2 template files
  .verg/logs/      # structured apply logs (written automatically)

verg init scaffolds this layout with a starter hosts.toml and state/base.toml.

Inventory and selectors

Each host in hosts.toml has these fields:

Field Required Default Description
address yes - IP address or hostname
user no "root" SSH user
port no 22 SSH port
groups no [] Group memberships
[hosts.NAME.vars] no - Host-specific variables

The --targets flag accepts a selector expression:

Syntax Meaning
all Every host
web Hosts named web or in group web
a,b Union of selectors a and b
a:b Intersection (hosts in both a and b)
!x Exclude x
prod:!db In group prod but not group db

An unknown selector name is an error (exit code 6). The one exception is an exclusion (!x): excluding a group or host that matches nothing excludes nothing rather than erroring, so prod:!down still works when the down group is empty (and a misspelled exclusion is silently a no-op). Parentheses are not supported.

Hosts can also be generated dynamically by a command via an [inventory] section in hosts.toml. See docs/inventory.md for the static fields, the dynamic-inventory command, its JSON output format, and how static and dynamic hosts merge.

State files

State files live in state/ and are loaded in lexicographic order. Each file is a TOML document with this shape:

targets = ["web"]          # optional: limit to hosts in group/name "web"
                           # omit to apply to all hosts

[resource.<type>.<name>]   # one table per resource
property = "value"

A state file with no targets key applies to every host. A state file with targets = ["web"] applies only when the target host belongs to group web or has the name web. Only targets and resource are valid top-level keys; unrecognized keys are rejected by default.

See docs/resources.md for the full resource reference.

Variables and facts

Variables are interpolated using Jinja2 syntax ({{ var }}). Template sources:

Source Precedence
Host vars ([hosts.NAME.vars]) Highest
Group vars (groups/<g>.toml [vars]) Below host vars
Facts gathered at runtime Available as fact.arch, fact.os, etc.
Group membership Available as group.<name>
Inventory context (hosts.*, groups.*) Lowest

Available facts: fact.arch, fact.hostname, fact.os, fact.os_release, fact.os_version.

String values starting with $env. (e.g. password = "$env.MY_SECRET") are resolved from the environment at build time.

Inline content in a file resource is always interpolated. To interpolate a file loaded via source, set template = true.

cmd resources can register their stdout for use in downstream resources as {{ register.NAME }}. See docs/resources.md for details.

Ordering and handlers

Resources declare dependencies with after = ["type.name", ...]. verg sorts resources into execution layers using Kahn's topological sort; within a layer, resources run in FQN order. A resource whose dependency fails is skipped with "dependency failed".

Resources with handler = true are skipped unless another resource lists their FQN in notify. Handlers run after all normal resources; shorthand notify actions (e.g. reload:nginx, daemon-reload) run after handlers. Each shorthand runs at most once per host per run.

when expressions conditionally skip resources based on facts and group membership (e.g. when = "fact.os == 'ubuntu'").

See docs/resources.md for the full common attributes reference.

Resource Types

Type Purpose
pkg Install or remove system packages (apt-get, dnf, pacman - auto-detected)
file Manage a file's content, permissions, and ownership
directory Ensure a directory exists with the right permissions and ownership
service Manage a systemd service's running state and boot enablement
cmd Run a shell command with an idempotency guard
user Create or remove a system user; update shell, home, and groups on existing users
cron Manage a cron job file in /etc/cron.d/
sysctl Set a Linux kernel parameter
apt_repo Add or remove an APT repository with its GPG key
docker_compose Manage a Docker Compose stack
download Download a file from a URL, optionally extracting an archive
hostname Set the static system hostname (requires systemd)
timezone Set the system timezone by IANA name (requires systemd)
mount Manage an /etc/fstab entry and mount state
git Clone a git repository and keep it checked out at a ref

You can also define your own resource types as data in verg/resources/. See docs/resources.md - Custom resource types for the definition format, param schema, and a worked example.

For resource types that need a full scripting language or a richer protocol, native providers let you write the logic in any language and communicate over JSON-on-stdio. The script is embedded in the bundle at build time, so pull-mode agents are self-contained. See docs/providers.md for the declaration format, wire protocol, and a complete worked example.

Complete Example

A minimal nginx setup using pkg, file, and service resources with dependency ordering.

# verg/hosts.toml
[hosts.web1]
address = "192.0.2.10"
user = "root"
groups = ["web"]

[hosts.web1.vars]
http_port = "80"
server_name = "example.com"
# verg/state/nginx.toml
targets = ["web"]

[resource.pkg.nginx]
name = "nginx"
state = "present"

[resource.file.nginx-conf]
path = "/etc/nginx/sites-available/default"
content = """
server {
    listen {{ http_port }};
    server_name {{ server_name }};
    root /var/www/html;
}
"""
mode = "0644"
after = ["pkg.nginx"]
notify = ["reload:nginx"]

[resource.service.nginx]
name = "nginx"
state = "running"
enabled = true
after = ["file.nginx-conf"]

The FQN for each resource is type.name (e.g. pkg.nginx, file.nginx-conf, service.nginx). Every after reference in this example resolves to a resource defined in the same file.

Commands

Command Key args Description
verg apply --targets <TARGETS> (required) Converge targets to desired state
verg diff --targets <TARGETS> (default: all), --limit, --offset, --fields Show what would change without applying
verg check --targets <TARGETS> (default: all) Verify targets match desired state (exits 0 when drift found, 1 when all match)
verg schema - Print resource type schemas as JSON
verg init --force Scaffold a new project directory
verg completions <bash|fish|zsh|powershell|elvish> Generate shell completions
verg publish --targets <TARGETS>, --dest <DIR> Build per-host bundles offline for pull-mode agents

apply has no default target. --targets is required to prevent accidental mass applies. Running apply in a non-interactive pipeline without --yes exits with code 5 (confirmation required).

Global Flags

Flag Default Description
-o, --output <auto|text|json> auto Output format; auto selects JSON when stdout is not a TTY
--json - Force JSON output (hidden alias for -o json)
-q, --quiet - Suppress per-resource lines; print only the final summary
-y, --yes - Proceed when stdin is not a TTY (required for CI/pipelines)
--path <PATH> ./verg Path to the verg project directory (VERG_PATH)
--parallel <N> 10 Maximum parallel SSH connections
--ssh-config <PATH> - Path to SSH config file (VERG_SSH_CONFIG)
--agent-dir <PATH> - Directory containing verg-agent binaries per architecture (VERG_AGENT_DIR)
--host-key-checking <yes|accept-new|no> yes SSH host key checking policy
--ssh-known-hosts <PATH> - Path to a known_hosts file
--skip-agent-checksum - Skip agent binary checksum verification (air-gapped or local builds)
--lax-config - Downgrade config validation errors (unknown keys/types) to warnings
--timeout <SECONDS> 600 Per-host timeout in seconds

Exit Codes

Code Name Meaning
0 SUCCESS Resources changed or drift detected
1 NOTHING_CHANGED All resources already in desired state
2 PARTIAL_FAILURE Some resources failed; others succeeded
3 TOTAL_FAILURE All resources on all targets failed
4 CONNECTION_ERROR All failures were SSH connection errors
5 INVALID_CONFIG Config is invalid, or apply requires --yes (ConfirmationRequired)
6 TARGET_NOT_FOUND No hosts matched the given target selector
7 INTERNAL_ERROR Unexpected internal error (I/O or other)
8 CONFLICT State conflict that cannot be automatically resolved
130 INTERRUPTED Process received SIGINT (Ctrl-C)

check exits 0 when drift is found and 1 when everything already matches. diff exits 0 on any successful run (even with no changes); only connection or partial failures yield 4 or 2.

Security

verg pushes a compiled agent binary to target hosts and runs it as the configured SSH user (root by default). The agent binary is SHA-256 verified locally before transfer and remotely after transfer using sha256sum -c. SSH host key checking is strict by default (StrictHostKeyChecking=yes). All state files are validated locally before any SSH connection is opened. Resources marked sensitive = true have their payload redacted from output and the apply changelog.

See SECURITY.md for the full threat model, supply chain details, secret handling, and vulnerability reporting.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

verg-0.8.1.tar.gz (209.1 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

verg-0.8.1-py3-none-manylinux_2_28_x86_64.whl (2.9 MB view details)

Uploaded Python 3manylinux: glibc 2.28+ x86-64

verg-0.8.1-py3-none-manylinux_2_28_aarch64.whl (2.6 MB view details)

Uploaded Python 3manylinux: glibc 2.28+ ARM64

verg-0.8.1-py3-none-macosx_11_0_arm64.whl (2.6 MB view details)

Uploaded Python 3macOS 11.0+ ARM64

verg-0.8.1-py3-none-macosx_10_12_x86_64.whl (2.7 MB view details)

Uploaded Python 3macOS 10.12+ x86-64

File details

Details for the file verg-0.8.1.tar.gz.

File metadata

  • Download URL: verg-0.8.1.tar.gz
  • Upload date:
  • Size: 209.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for verg-0.8.1.tar.gz
Algorithm Hash digest
SHA256 f72dfce2faadb081b5b04a467f14bf7222f17127a3fcb84687894392fc2b3cbd
MD5 20e013ebbf6c16bdfd02e5973c17f995
BLAKE2b-256 5c4157b69711a38f1515e8b6de67e20826691f3e2ff62bc0181b3e6cda5f636a

See more details on using hashes here.

File details

Details for the file verg-0.8.1-py3-none-manylinux_2_28_x86_64.whl.

File metadata

  • Download URL: verg-0.8.1-py3-none-manylinux_2_28_x86_64.whl
  • Upload date:
  • Size: 2.9 MB
  • Tags: Python 3, manylinux: glibc 2.28+ x86-64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for verg-0.8.1-py3-none-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 893b904940f9ecab2a604fcfd3987cea502f11cfbeae1769c9608e91e6139fc9
MD5 13100736193ce05777ead536977dd89d
BLAKE2b-256 1eb54a4983f0a0046e3fd771ed1f49a566b44ae54382aa46785ffd44ca3270a8

See more details on using hashes here.

File details

Details for the file verg-0.8.1-py3-none-manylinux_2_28_aarch64.whl.

File metadata

File hashes

Hashes for verg-0.8.1-py3-none-manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 6a1bbdbe04c69c845ca621de42aebd06bfe6d0f1c0923f1f723e8ce3ee671765
MD5 0816c6d5727d2b0c01eb08ce05f9c031
BLAKE2b-256 a697dc70b55928b78a916c6f074d17746ec8189e3364b572a8ff08da60aea3bc

See more details on using hashes here.

File details

Details for the file verg-0.8.1-py3-none-macosx_11_0_arm64.whl.

File metadata

  • Download URL: verg-0.8.1-py3-none-macosx_11_0_arm64.whl
  • Upload date:
  • Size: 2.6 MB
  • Tags: Python 3, macOS 11.0+ ARM64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for verg-0.8.1-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 2b42f89d90e1d2a07245da139fc105ae4b6ff6f9834b4dc5555d11370ce88771
MD5 45fc5cea9f191e8701ab9b5677b99d6a
BLAKE2b-256 f2265695e2b351ea90c308e9213c1df49b648f2ab9aad49d87ac447965c9be6c

See more details on using hashes here.

File details

Details for the file verg-0.8.1-py3-none-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for verg-0.8.1-py3-none-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 a3f7041c5023fe2b1a77eda05e2d8702e91ddb7be13173c73710a2a57f4944da
MD5 3f9d5ec4e085a5bca29b73bbe43561ac
BLAKE2b-256 952f122d72c8b43eefabae804a5dd56d2b57924baa1339e0e02dac8438763d3a

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page