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.0.tar.gz (197.6 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.0-py3-none-manylinux_2_28_x86_64.whl (2.8 MB view details)

Uploaded Python 3manylinux: glibc 2.28+ x86-64

verg-0.8.0-py3-none-manylinux_2_28_aarch64.whl (2.5 MB view details)

Uploaded Python 3manylinux: glibc 2.28+ ARM64

verg-0.8.0-py3-none-macosx_11_0_arm64.whl (2.5 MB view details)

Uploaded Python 3macOS 11.0+ ARM64

verg-0.8.0-py3-none-macosx_10_12_x86_64.whl (2.6 MB view details)

Uploaded Python 3macOS 10.12+ x86-64

File details

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

File metadata

  • Download URL: verg-0.8.0.tar.gz
  • Upload date:
  • Size: 197.6 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.0.tar.gz
Algorithm Hash digest
SHA256 b67483cda23d8f828005eea5c835fd074c8f2f7c803c1c58cb7c646f5d5d9b93
MD5 dc8a1899872931343ec6d9044d7cbe23
BLAKE2b-256 11ce879981faa1852fdb3e4b48de0b476a25a081197f33a6ee8e7742feefe4e2

See more details on using hashes here.

File details

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

File metadata

  • Download URL: verg-0.8.0-py3-none-manylinux_2_28_x86_64.whl
  • Upload date:
  • Size: 2.8 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.0-py3-none-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 7cd9934df7955080572f383dbc06d95b74eb34740e8b6b914c771916149d4cbb
MD5 07d867e3653c76787e8affad2d3633ef
BLAKE2b-256 8cd466597883001e854b382ddb9599399f56834556b93eead3b7492550db32cf

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for verg-0.8.0-py3-none-manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 c90f92c66dfb0285b19ceb745efe6adc1af3dc860d9d661547df97b16fc54208
MD5 49d2ae947f336796a636210d33dfb488
BLAKE2b-256 e7edb8b2a7a3959a47344c4866932facd4a0ca1b59bc888715326902b4e9d030

See more details on using hashes here.

File details

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

File metadata

  • Download URL: verg-0.8.0-py3-none-macosx_11_0_arm64.whl
  • Upload date:
  • Size: 2.5 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.0-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 de008783c56cf0751736047b23024e3a28a74d9fc9ceed14fbfc1baa0945a3e5
MD5 7e6d42b1e7b201e668c14531db8c5a4d
BLAKE2b-256 42f1e418ac3ed375e8b64ef315d26be8565dc2e3cc720989151844286c7544f8

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for verg-0.8.0-py3-none-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 4689cf23ae27f3216bfc2ccef0d9615859b752cfe6d43f8628e7bc2be899aace
MD5 1fbbc68829ab6849e53e5473921ca677
BLAKE2b-256 fe08bf85a1244af8ab66c9323daaa633cbb132a05bddf40d2127bd8c3cb94c54

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