Quickstart
Two minutes, no cloud, no clone
The fastest way to see what this is: one binary, two generated documents, one command run twice. No cloud account, no credentials, no Go toolchain, nothing cloned.
Take the newest asset for your platform from
Releases — groundhold_linux_amd64,
groundhold_darwin_arm64 and so on. Every release also carries SHA256SUMS:
sha256sum -c SHA256SUMS --ignore-missing
chmod +x groundhold_linux_amd64 && mv groundhold_linux_amd64 groundhold
The README's download line pins an exact tag and is checked by the release workflow; this page deliberately does not repeat the version, so it cannot go stale on its own.
Now scaffold a contract and a candidate — the binary writes both, and the vocabulary is compiled in, so there is nothing else to fetch:
./groundhold example contract > my.contract.yaml
./groundhold example candidate my.contract.yaml > my.candidate.yaml
The candidate comes out with exactly ONE blank: service, the provider's own
name for the thing. Everything the contract pins is already filled, because the
scaffold answers what the contract asked and leaves only what it cannot know.
Put anything there — nothing reaches a cloud on the fake provider:
sed -i 's|service: ""|service: "rds"|' my.candidate.yaml
Then run the whole loop — verify, plan, apply, observe, convergence check — twice:
AT="$(date -u +%FT%TZ)"
./groundhold converge my.contract.yaml my.candidate.yaml \
--ledger try.jsonl --provider fake --at "$AT" --yes
# ... APPLIED
./groundhold converge my.contract.yaml my.candidate.yaml \
--ledger try.jsonl --provider fake --at "$AT" --yes
# the sealed plan carries no actions — the world already matches the candidate
# on everything this run can compare, so there is nothing to apply
# CONVERGED
The second run is the point. It banners CONVERGED because the runtime went
and looked, not because the first apply returned success — convergence is proven
against recorded reality. The first run says APPLIED, which is true and exactly
as much as was checked: the convergence check had not yet seen the world.
Everything below is the same loop with your own documents, and then with a real
cloud. The commands say ./groundhold throughout — if you built from source
instead (last section), that is bin/groundhold-go and nothing else changes.
Your first contract
db.contract.yaml — what must be TRUE (constraints, not resources):
apiVersion: contract/v0.1
kind: InfrastructureContract
meta: { id: orders, environment: production, version: 1 }
capabilities:
- id: db
type: capability.database.relational
constraints:
hard:
- id: c-region
subject: db
path: location.region
op: equals
value: europe-central2
verify: { method: static }
- id: c-private
subject: db
path: network.publicExposure
op: equals
value: false
verify: { method: static }
autonomy:
forbidden:
- delete_stateful: true
db.candidate.yaml — an agent's proposal (tiers and flags live in the
free-form implementation: block; the verifier judges semantics only):
apiVersion: candidate/v0.1
kind: ImplementationCandidate
contract: orders
capabilities:
db:
attributes:
location.region: europe-central2
network.publicExposure: false
engine.protocol: postgresql/16
service.managed: true
provider: gcp
service: cloudsql
implementation:
tier: db-custom-2-8192
Verify
./groundhold verify db.contract.yaml db.candidate.yaml
contract orders v1
✓ c-region satisfied location.region equals europe-central2: declared europe-central2
✓ c-private satisfied network.publicExposure equals false: declared false
2 satisfied, 0 violated, 0 unknown, 0 unverifiable
PROVEN
Converge on your laptop — no cloud, no credentials
The full loop (verify → plan → forecast → confirm → apply → observe →
convergence check) runs end to end against the built-in fake
provider. It can only observe one attribute honestly
(service.managed), so the laptop demo uses a minimal pair — shipped in
the repository as examples/laptop/, so you can run it without pasting
anything:
# lap.contract.yaml
apiVersion: contract/v0.1
kind: InfrastructureContract
meta: { id: orders, environment: production, version: 1 }
capabilities:
- id: db
type: capability.database.relational
constraints:
hard:
- id: c-managed
subject: db
path: service.managed
op: equals
value: true
verify: { method: static }
# lap.candidate.yaml
apiVersion: candidate/v0.1
kind: ImplementationCandidate
contract: orders
capabilities:
db:
attributes:
service.managed: true
provider: fake
service: sql
./groundhold converge \
examples/laptop/laptop.contract.yaml examples/laptop/laptop.candidate.yaml \
--ledger state/prod.jsonl --provider fake --at "$(date -u +%FT%TZ)" --yes
# ... apply, observe ...
# ✓ converged — verified against observed reality
# CONVERGED
./groundhold converge \
examples/laptop/laptop.contract.yaml examples/laptop/laptop.candidate.yaml \
--ledger state/prod.jsonl --provider fake --at "$(date -u +%FT%TZ)" --yes
# ✓ converged — the world already matches the candidate on every attribute
# this run can compare
# CONVERGED
The second run touching nothing IS the product: convergence is proven against recorded reality, not assumed from a successful apply.
Without --yes, converge stops at the plan and asks you to type
apply — in CI or a pipe that refuses (exit 2,
code: confirmation-required), deliberately. --ledger names a fresh
path; the directory is created for you.
One honest limit, on purpose: if a candidate declares attributes the
provider's observe cannot witness (on fake: anything beyond
service.managed), the first converge banners APPLIED — not
CONVERGED — because the convergence check came back inconclusive,
and a SECOND converge refuses with observation-required: drift
cannot be judged against observations that do not exist. Both are the
observation gate working, not bugs; on a real provider observe
covers the real attribute surface and CONVERGED is earned.
Real cloud
The same rich pair from Your first contract,
with a real driver (GCP shown). Credentials are read in this order:
GROUNDHOLD_GCP_ACCESS_TOKEN, then GROUNDHOLD_GCP_KEY_FILE (a
service_account key JSON), then the GCE metadata server — not
Application Default Credentials. gcloud auth application-default
login alone leaves the driver with nothing to use:
export GROUNDHOLD_GCP_ACCESS_TOKEN="$(gcloud auth print-access-token)"
./groundhold converge db.contract.yaml db.candidate.yaml \
--ledger state/prod.jsonl --provider gcp --project my-project \
--at "$(date -u +%FT%TZ)"
A plan with dataLoss: certain refuses under plain --yes and demands
--allow-data-loss. Refusals arrive verbatim with a machine code
(see Errors); add --explain for remediation.
Already have infrastructure?
./groundhold discover --provider gcp --project my-project
./groundhold hints terraform.tfstate # tf/pulumi state -> adoption hints
./groundhold adopt ... --map db=project:region:name
./groundhold converge ... # must report "converged" — the proof
Adoption refuses when the candidate disagrees with live observation: adoption must not lie.
Build from source
You only need this to change groundhold itself, or to run the conformance suite. Using it does not require building it.
git clone https://github.com/groundhold/groundhold.git && cd groundhold
make check # vet + tests + the full conformance suite (588 cases); 291 run through both implementations, the rest Go-only
cd go && go build -o ../bin/groundhold-go ./cmd/groundhold && cd .. # the CLI binary
Requirements: Go ≥ 1.25, Python ≥ 3.12 (reference implementation), PyYAML. Nothing else — the runtime is stdlib + yaml only.
The full attribute vocabulary is compiled into the binary, so it works
with no external files — --vocab <dir> is optional and only EXTENDS
the built-in set with your own types; --no-vocab forces the empty set.
Keep the code and the contract agreeing
If an agent crawled your application repo (the code-to-contract
skill persists its evidence table as a commit-pinned survey), CI can
hold the two sides of the mirror together:
./groundhold survey db.contract.yaml --survey .groundhold/survey/9f3c1a7.json
Exit 2 with code: survey-drift means the code and the contract
disagree about reality — a dependency the code now requires with no
capability behind it, or (under --complete) a capability no repo
witnesses anymore. audit watches the cloud side of the contract;
survey watches the code side.