[{"data":1,"prerenderedAt":4},["ShallowReactive",2],{"CxcxwZaykX":3},"\u003Cp align=\"center\">\n  \u003Cimg src=\"assets/logo-wordmark.svg\" alt=\"infra\" width=\"300\">\n\u003C/p>\n\n\u003Cp align=\"center\">\n  \u003Cem>Infrastructure as code, in Lean 4 — an unrealisable target is a compile error.\u003C/em>\u003Cbr>\n  \u003Ca href=\"https://typednotes.github.io/infra/\">Website\u003C/a> ·\n  \u003Ca href=\"docs/tutorial.md\">Tutorial\u003C/a> ·\n  \u003Ca href=\"docs/coverage.md\">Coverage\u003C/a>\n\u003C/p>\n\n[![CI](https://github.com/typednotes/infra/actions/workflows/lean_action_ci.yml/badge.svg)](https://github.com/typednotes/infra/actions/workflows/lean_action_ci.yml)\n[![Lean](https://img.shields.io/badge/Lean-v4.33.1-blue)](https://leanprover.github.io/)\n[![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)\n\nTerraform/OpenTofu-style infrastructure as code, defined in Lean instead of a\nbespoke DSL. Target and observed cloud state are dependently-typed Lean\nvalues, so an unrealisable target is a compile error rather than a runtime\nsurprise.\n\n- **One portable spec, many clouds.** A resource declared with a portable\n  `Kind` (object store, compute, queues, secrets, Postgres, ...) can be\n  pushed to AWS or Scaleway without change — the provider only enters at\n  apply time, through a `Backend`.\n- **Provider-local escape hatches.** When a portable abstraction can't carry\n  a provider-specific field, a `Kind` scoped to that one provider (e.g.\n  Scaleway's `scalewayFunction`, `scalewayContainer`) fills the gap without\n  weakening the portable kind's guarantees.\n- **Diffing is a Lean function, not a side effect.** Plan vs. observed state\n  is compared structurally over the Lean values themselves; `plan` prints\n  what it would do and only `apply` changes anything.\n\nSee [`docs/architecture.md`](docs/architecture.md) for the full design and\nthe portability rules.\n\n## What 0.9.0 covers\n\n**3 clouds** (AWS, Scaleway, GCP) · **14 resource kinds** (7 portable, 7\nprovider-local) · every `(provider, kind)` pair implemented.\n\nAll seven portable kinds have live clients on **all three clouds** — on GCP:\nPub/Sub, Cloud Storage, Secret Manager, Artifact Registry, Cloud Run, IAM\nservice accounts and Cloud SQL. Create-and-destroy round trips run in CI on\n**all three clouds** — AWS 12 resources, Scaleway 12, Google Cloud 10,\ncovering thirteen of the fourteen kinds and 22 (cloud, kind) pairs. Each leg\napplies five declarations in sequence: the whole fleet, a scale up, a scale\ndown, a version with resources dropped, then one that declares nothing. After\nevery stage the account must hold exactly what that stage declares, so a\ndropped resource has to be *destroyed* rather than abandoned. The five-stage\nsequence has not yet been passed honestly on any cloud: the 2026-09-08 runs\nfound two defects, both fixed and neither re-verified — `docs/coverage.md`\nsays what each run showed. All three\ndependency patterns are exercised live: a\nchain, a fan-out, and a fan-in through both key and expression references.\n\nTwo GCP limits are stated rather than papered over. A serverless `postgres`\ndeclaration **raises**, because Cloud SQL has no capacity range that scales to\na floor and picking a tier from `minCapacity` would invent a bill you did not\nwrite down. And `iam` reads the roles bound to a service account but refuses to\nwrite them: granting a role on GCP is a read-modify-write of the whole\nproject's IAM policy, and getting that wrong removes other identities' access,\nso a declared policy shows up in `plan` and is refused at apply with the\n`gcloud` command that would bind it.\n\nVerification varies by kind, and it is worth knowing which before you rely on\nany one of them:\n\n| | |\n|---|---|\n| Verified against a real account | a three-stage sequence on **all three clouds**: 32 resources across 11 of the 14 kinds, created, converged, partly dropped, and destroyed. Stage 2 deletes resources whose lines are *gone* from the declaration, so it cannot pass unless membership works |\n| Verified offline, every build | signing, diffing, DAG scheduling, credentials, composed secrets, ledger adoption, and that a sweep deletes only what it created |\n| **Never run against an account** | AWS Lambda and RDS, Scaleway's `postgres` and `scalewayFunction`, GCP Cloud SQL — the kinds a test cannot arrange. Most `update` paths: only `queues` has one that runs, and only on two clouds |\n\nIt converts both ways: `toHcl` writes `.tf` from a fleet (with real HCL\nreferences, and a `# TODO` for anything HCL cannot express), and\n`fleetOfState` reads `terraform show -json` back into a fleet declaration.\n\n[`docs/coverage.md`](docs/coverage.md) is the full breakdown — kinds, features,\nwhat is verified how, and the known defects. It is kept current deliberately,\nincluding the parts that are embarrassing.\n\nEarly and evolving: breaking changes to the Lean API should be expected before\na first tagged release.\n\n## Requirements\n\n- [`elan`](https://github.com/leanprover/elan) (Lean's toolchain manager) —\n  `lean-toolchain` pins the exact version this project builds with\n  (`leanprover/lean4:v4.33.1`).\n- Linux or macOS. Native FFI dependencies for `libpq`, OpenSSL headers, and\n  the OS keychain (`libsecret` on Linux, Keychain on macOS) — see the\n  `lean_action_ci.yml` install steps for the exact packages if `lake build`\n  fails looking for a header.\n\n## Start a project\n\nAn infra project is an ordinary Lean project with one dependency, so it starts\nthe ordinary way. `lake init`, add the dependency, then one command turns it\ninto a declaration repository:\n\n```sh\nlake init my_infra && cd my_infra\n```\n\nAdd `infra` to the `lakefile.toml` Lake just wrote:\n\n```toml\n[[require]]\nname = \"infra\"\ngit = \"https://github.com/typednotes/infra\"\nrev = \"v0.10.0\"\n```\n\nThen:\n\n```sh\nlake update                # fetch infra\nlake exe infra init        # turn this project into an infra project\nlake build\nlake exe my_infra          # offline plan — free, no credentials\nlake exe my_infra plan     # read your real accounts, change nothing\nlake exe my_infra apply    # make it so\n```\n\n`lake exe infra` runs the scaffolder straight out of the dependency, so there\nis nothing to install and nothing to keep on your `PATH`.\n\n**What `infra init` does to the project.** It adds `Fleet.lean` (the\ndeclaration you edit), `Catalogue.lean` (every resource kind, declared once,\nto copy from), rewrites Lake's stub `Main.lean` to run the fleet, adds a\n`.gitignore` that excludes the state cache, and adds CI for **GitHub Actions,\nGitLab CI, CircleCI, Azure Pipelines and Jenkins** — each with the same\nplan/apply split, so a plan runs on every push and an apply waits for a person\nto press the button. Delete the ones you do not use. It writes only what is\nabsent and names what it kept, so it is safe to re-run and safe on a project\nwith work in it. Your own libraries and executables are preserved.\n\n`Catalogue.lean` is **compiled and never applied**: `Main.lean` runs\n`Fleet.plan` and nothing else, so nothing in it is created or billed. Compiling\nit is the point — commented-out examples drift from the API and nothing\nnotices, whereas these are type-checked by your own `lake build` against the\nversion of `infra` you actually depend on. Delete the file when it stops being\nuseful; nothing imports it.\n\nIt also **converts `lakefile.toml` to `lakefile.lean`**, keeping the original\nas `lakefile.toml.replaced-by-infra`. That conversion is not cosmetic: the\nnative link flags are computed on the build machine by running `pkg-config`,\nwhich TOML cannot express, and they are not optional because Lake does not\npropagate a dependency's `moreLinkArgs`. Without them the link fails on\nundefined symbols from the FFI. If the TOML contains anything the converter\ndoes not recognise it refuses and says so, rather than rewriting a lakefile on\na guess.\n\nYour declaration is a Lean program, so `lake exe my_infra` *is* the CLI —\nthere is no separate binary to keep in step with your code, and no state file\nto commit: what is managed is marked on the resources themselves, and `.infra/`\nis a disposable local record. Neither holds a secret. See\n`docs/persistence.md`.\n\n### Starting from nothing\n\n`infra new \u003Cdir>` does all of the above *and* the `lake init`, for a directory\nthat does not exist yet:\n\n```sh\nlake exe infra new my_infra   # from any project that has infra\ncd my_infra && lake update && lake build\n```\n\nBoth commands produce the same project. `new` is the shortcut when there is\nnothing there yet; `init` is the one to use on a project that already exists.\n\n## Build this repository\n\n```sh\nlake build\nlake exe infra check   # offline self-checks; no cloud, no credentials needed\nlake test              # the test driver, offline\n```\n\n## Running against real accounts\n\n`infra` needs credentials for both clouds — see\n[`docs/authentication.md`](docs/authentication.md) for the config file /\nkeychain / environment-variable chain it tries, in that order.\n\n```sh\nlake exe infra check            # offline self-checks (default, no cloud)\nlake exe infra refresh          # observe both clouds, cache to .infra/\nlake exe infra plan             # show what would change, no changes made\nlake exe infra plan --destroy   # show what tearing the fleet down would delete\nlake exe infra apply            # actually reconcile\nlake exe infra destroy          # delete everything the fleet declares\n```\n\n**Deleting a resource from the declaration destroys it.** A resource is yours\nif it carries the marker tag this tool writes on everything it creates, it is\ninside the realm your declaration names, and it is not on the exclusion list —\nand if two fleets share an account, each can put its own name in that tag\n(`boundary := { fleetName := some \"…\" }`) so the other's resources read as\nforeign and are left alone —\nfor the kinds a backend can read tags for; a kind that cannot yet falls back\nto a row in the local ledger under `.infra/`. Either way, a resource whose line\nyou deleted can still be named after the fact — the declaration no longer\nmentions it, so nothing else can, until the ledger or the marker does. Saying\n`.absent` within the declaration does the same thing; `destroy` is `apply`\nagainst an empty declaration. All three end at the same call, and deletions\nrun in the reverse of creation order so a resource goes before whatever it\ndepends on.\n\nNothing about that needs committing, which is deliberate: membership is a\nconsequence of applying, not a statement of intent, so CI never has to write\nback to your branch. `Infra/Core/Ownership.lean` records the reasoning, and\nwhich way each rule fails. The ledger is a local cache of the decision, not the\ndecision itself, for a kind the marker covers — `lake exe infra discover`\nrebuilds it straight from the account if it is ever lost. For a kind not yet\ntaught to read tags, the ledger is still the only thing that can name an\norphan, so losing it strands one.\n\nTo stop managing something *without* destroying it, say so:\n\n```lean\nforget scaleway queues \"old-queue\"\n```\n\nwhich drops its ledger row and leaves the cloud alone. It is checked: a\n`forget` for something the fleet still declares does not compile.\n\nResources you never declared are untouched throughout. They have no ledger\nrow, so nothing here can name them.\n\n`plan` never touches a cloud. Treat `apply` like you would `terraform apply`:\nread the plan first. Output is coloured by verb when stdout is a terminal —\ngreen to create, yellow to update, magenta to replace, red to delete — and\nplain when piped, so a redirect or a CI step summary stays free of escape\ncodes. `NO_COLOR` disables it, `FORCE_COLOR` forces it on.\n\n## Examples\n\n### Pulling Scaleway state alone\n\n`example/ScalewayPull.lean` is a smaller, self-contained slice: authenticate\nto **Scaleway only** (no AWS credentials read or required), pull whatever the\naccount reports for every `Kind`, and write it to `out/scaleway/` — once as\nJSON, once as elaborable Lean source.\n\n```\n$ lake exe scaleway-pull\nauthenticating to Scaleway...\nauthenticated (region fr-par)\n  object-store: 2 resource(s) -> out/scaleway/object-store.json, out/scaleway/object-store.lean\n  compute: 1 resource(s) -> out/scaleway/compute.json, out/scaleway/compute.lean\ndone: 3 resource(s) across every kind Scaleway reported\n```\n\nOnly Scaleway credentials are needed for this one — `~/.config/scw/config.yaml`,\nthe OS keychain, or `SCW_ACCESS_KEY`/`SCW_SECRET_KEY` (see\n`docs/authentication.md`). Output lands under the gitignored `out/`, so it is\nsafe to inspect and delete.\n\n### Declaring and pushing a Scaleway queue\n\n`example/ScalewayQueue.lean` is the counterpart to the one above: instead of\nlisting what already exists, it declares a target and reconciles it. It is also\nthe shortest file in the repo, and deliberately so — the whole declaration is:\n\n```lean\nfleet exampleQueue where\n  resource scaleway queues \"infra-example\"\n    { visibilityTimeoutSec := 30 }\n```\n\n```\n$ lake exe scaleway-queue          # offline: the plan, from placeholders\nwould CREATE scaleway/queues/infra-example\n(dry run — nothing changed)\n\n$ lake exe scaleway-queue apply\nCREATE scaleway/queues/infra-example ... ok\n```\n\nA real, billable resource in your Scaleway account. Re-running `plan`\nafterwards prints `nothing to do`, since the queue already matches the target.\nRemoving the line and applying deletes it, so `lake exe scaleway-queue destroy`\nand deleting the line are two ways of saying the same thing. Use\n`forget scaleway queues \"infra-example\"` if you want to keep the queue and stop\nmanaging it.\n\n### Two instances behind a security group\n\n`example/ParisInstances.lean` is the one to read for what the types actually\nbuy. `AwsInstanceSpec.securityGroup` is a **required** reference, so an\ninstance with no security group, one naming a group outside the fleet, and one\nnaming something that is not a group are all compile errors — the file quotes\nthe three messages verbatim. The group is scheduled before both instances\nbecause of that reference, not because of the order it is written in.\n\n```\n$ lake exe paris-instances\nwould CREATE aws/security-group/web\nwould CREATE aws/aws-instance/web-1\nwould CREATE aws/aws-instance/web-2\n```\n\nRead its header before applying: the AMI id is unverified and the EC2 backend\nhas never been run against a real account. The *region* is declared —\n`fleet paris in paris where …` puts it in `eu-west-3`, so `AWS_REGION` is\nneither read nor needed, and the same file no longer builds a different fleet\nfor each operator who runs it.\n\n### One fleet across four regions\n\n`example/MultiRegion.lean` places resources per *resource* rather than per\ncloud, with blocks that nest and scope like a `with` in Python:\n\n```lean\nfleet spread in paris where\n  provider aws where\n    resource s3Bucket \"eu-assets\" { versioning := true }   -- the fleet's Paris\n    in nVirginia where\n      resource s3Bucket \"us-east-assets\" { versioning := true }\n  provider scaleway where\n    in amsterdam where\n      resource objectStore \"nl-cache\" { versioning := true }\n```\n\nOne `in paris` reaches both clouds with each one's own code; a block overrides\nonly what is nested inside it; and a resource placed where its cloud has no\nregion — a Scaleway one inside `in oregon` — is a compile error. The regions a\npull has to list are derived from the declaration, so a single-region fleet\nstill lists once.\n\n### One fleet across both clouds\n\n`example/CrossCloud.lean` puts the same portable `objectStore` declaration\nunder both clouds, Object Lock on the AWS-only `s3Bucket`, and a Scaleway\nfunction that reads the AWS bucket — a reference crossing clouds, which is what\norders the bucket first.\n\n```\n$ lake exe cross-cloud\nwould CREATE aws/object-store/typednotes-assets\nwould CREATE aws/s3-bucket/typednotes-archive\nwould CREATE scaleway/object-store/typednotes-assets\nwould CREATE scaleway/scaleway-function/reindex\n```\n\nThe only example needing *both* clouds' credentials to run live. S3 bucket\nnames are globally unique, so change them before applying.\n\n### All three share one entry point\n\nA bare invocation is offline: it plans against the placeholder backends, needs\nno credentials and creates nothing. `plan` reads the real account; `apply`\nchanges it. That is `Infra.Cli.run`, the same front end `infra`'s own\nbinary and a consumer repo both use — the examples deliberately contain no\nargument parsing, credential loading or backend wiring of their own.\n\nAny of them will refuse to touch the wrong account if you say which you expect:\n\n```sh\nexport INFRA_EXPECT_AWS_ACCOUNT=\u003Cid>\nexport INFRA_EXPECT_SCALEWAY_ORG=\u003Cid>\n```\n\n```\n$ lake exe cross-cloud plan\naws: account 123456789012 ok\nscaleway: organization 4d7c630f-… ok\n```\n\n## Documentation\n\nStart here:\n\n- [`docs/coverage.md`](docs/coverage.md) — **what this version actually does**,\n  and how far each part has been exercised\n- [`docs/tutorial.md`](docs/tutorial.md) — **getting started**: an empty\n  directory to a fleet in two clouds, with the commands, credentials,\n  placement, references and secrets explained in order. Every snippet in it\n  compiles.\n\nThen the design documents, which explain *why* and are worth reading before\nextending anything:\n\n- [`docs/architecture.md`](docs/architecture.md) — overall design and the portability rules\n- [`docs/internals.md`](docs/internals.md) — **how it works**: the pipeline\n  from source to API call, the type stack, the scheduler, and the membership\n  mechanism, with diagrams. The one to read before changing the engine\n- [`docs/authentication.md`](docs/authentication.md) — where credentials come from\n- [`docs/permissions.md`](docs/permissions.md) — **what those credentials must\n  be allowed to do**: the AWS actions each kind calls, an adaptable operator\n  policy, and why the ownership marker needs two grants per kind rather than one\n- [`docs/persistence.md`](docs/persistence.md) — the two local records, and\n  why membership is not intent\n- [`docs/branding.md`](docs/branding.md) — the logo, the colours, and the\n  trademark policies that constrain them\n- [`docs/ci-auth.md`](docs/ci-auth.md) — how CI authenticates without storing\n  a key, for AWS and GCP, with the policies in [`ci/`](ci/)\n- [`CHANGELOG.md`](CHANGELOG.md) — what changed, and when\n- [`docs/providers.md`](docs/providers.md) — how each `Kind` maps onto each cloud's API, and what is actually verified live\n- [`docs/diff-semantics.md`](docs/diff-semantics.md) — how target vs. observed state is compared\n\n## Contributing\n\nIssues and PRs are welcome — this is early-stage, so a design discussion\nbefore a large PR will save rework. When extending a `Kind`, grep for its\nexisting cases first: every provider/kind pair is a total match across\nseveral files by design (`Infra/Core/Kind.lean`, `Infra/Specs/Basic.lean`,\n`Infra/Core/Action.lean`, `Infra/Core/Diverge.lean`,\n`Infra/Core/Settle.lean`, `Infra/Providers/Live.lean`,\n`Infra/Providers/Placeholder.lean`), so a missed site is a compile error\nrather than a silent gap.\n\nThis project depends on [`linen`](https://github.com/typednotes/linen) for\nits native (FFI-backed) building blocks — SigV4 signing, TLS, the OS\nkeychain. If something you need is missing there, propose the addition to\n`linen` directly rather than working around it here.\n\n## License\n\nApache License 2.0 — see [`LICENSE`](LICENSE).\n",1789317496900]