reversi apply, hot reload and a CI example.
There is no Terraform provider and no Kubernetes operator. Neither is built.
reversi apply (below) plus the
Helm chart is the alternative: a directory in git is the desired state, apply publishes only the
differences, and --dry-run --detailed-exitcode is the drift check.The project file: reversi.config.yaml
The CLI, the server and the gateway all read it. It is found in the current folder or any parent (or REVERSI_CONFIG, or
--config). Every key is optional. reversi init writes a starter, and reversi show-config prints what was found.
reversi.config.yaml
notifications (channels and routes,
notifications), otel (endpoint, headers, service_name, monitoring),
anchor (sinks of {type: file, path} or {type: webhook, url, secret}, plus interval, evidence),
egress (allow, block_private, allow_metadata, region) and alerts (interval, remind, denial_spike).
- Relative paths resolve against the folder holding the file.
http(s)URLs and absolute paths are kept. ${NAME}is replaced from the environment, and${NAME:-default}falls back. An unset variable without a default is an error, so secrets stay out of git.- Durations:
30s,15m,2h,7d. - An unknown key is an error naming it. All problems are listed together.
Registry and policy versions
A plain folder of manifest YAML is fine for a start. For review, history and editing, publish into a registry folder: every change is a new numbered version with author, note, checksum, status (draft, approved, deprecated) and optionally an Ed25519
signature. A policy versions folder does the same for the policy, with one active version.
reversi.config.yaml
- A folder counts as a registry once it holds
index.json, and as policy versions once it holdspolicy-index.json. Both appear on the first publish. The server and gateway treat an empty folder as an editable registry or policy folder, so the first publish turns it into one. - The server, gateway and console only offer editing when those folders are in use. With plain files they are read-only.
- Draft manifests are not enforceable, and a registry that holds only a draft of a tool simply has no manifest for it, so a call reports “no manifest” rather than “is a draft”. Approving one writes a new, signed version; with
--four-eyesthe author cannot approve their own. reversi manifests verify --trust sign.pubrecomputes every checksum and checks every signature.- Deprecating keeps a tool working and rolling back but flags new calls.
reversi validate,lint,test,doctorandplanread a registry folder as well as a folder of files (for a registry, the current non-draft version of each tool).
reversi apply
apply makes a registry (and policy folder) match a directory.
state/manifests/ does not exist, the manifest files are read from state/ itself. YAML and JSON both work. Every manifest
is validated first and a bad one aborts before anything changes; the same tool twice is an error.
- Idempotent. A second run prints
up to dateand changes nothing. - Nothing is deleted.
--prunedeprecates; the old versions stay in the registry. - Status. New versions are
approvedby default, on the basis that the directory is the reviewed source of truth.--status draftpublishes drafts for a person to approve in the console or withreversi manifests approve. - Signing.
--keysigns each new manifest version. Policy versions are not signed. - Not atomic. Manifests are applied before the policy. If the policy step fails (for example the policy folder does not exist), the manifests are already applied; fix the cause and run again.
--detailed-exitcodeexits 2 when there are changes, and only together with--dry-run. Without it a dry run exits 0.
Local or server
A server whose
manifests: points to plain files refuses apply --server with no manifest registry folder is configured:
start the server on an empty folder (the first apply --server turns it into a registry), or bootstrap with reversi manifests publish or a local reversi apply.
Both modes were exercised by hand and by the apply tests; the server routes are in the admin API.
Hot reload
The server and the gateway re-read manifests, the policy and the manifest public key while running. A configuration that fails to load is rejected and the previous one keeps serving. A successful change writes areloaded record to the ledger.
- File watch: on by default, polling every 2 s. Turn off or change with
reload: { watch: false }orreload: { interval: 10s }. SIGHUP:kill -HUP <pid>.- On demand:
POST /v1/reload(admin) on the server,reversi.reload(admin) on the gateway’s operator endpoint. - Publishing from the console,
apply --serverand the registry endpoints reload as part of the request. If the new configuration does not load, the request answers 409saved, but the new configuration did not load: ....
SIGHUP or a change to the file). The gateway’s own config file and reversi.config.yaml keys other than manifests and policy are read at start.
CI example
Lint and test on every pull request; apply on merge; check for drift on a schedule. The commands are the ones run locally for this page; the workflow file itself has not been run on GitHub..github/workflows/reversi.yml
lint --strictandtestexit 1 on warnings or failures, which fails the job.- The pull-request step prints the pending changes and passes on exit 2; the scheduled step fails on exit 2, which is drift (someone changed the registry by hand).
- Use a dedicated admin key for the apply job and keep it in the CI secret store. Keys are managed in the console or authentication.
- The server must be reachable from the runner. Reaching a private server from GitHub-hosted runners needs a self-hosted runner or a tunnel, which is outside Reversi.
What is not built
- A Terraform provider and a Kubernetes operator (custom resources reconciled in-cluster). Use
reversi applyfrom CI. - Applying is a push. There is no agent in the cluster that pulls from git.

