No description
  • Nix 75.4%
  • Shell 20.4%
  • TypeScript 4.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-12 23:11:52 -04:00
checks memory-traces: run the built allod CLI, not the tools source tree 2026-09-12 22:58:56 -04:00
hooks Adopt profileDefinitions framework: source machine configs from secrets/inventory inputs 2026-07-14 09:49:48 -04:00
modules memory-traces: run the built allod CLI, not the tools source tree 2026-09-12 22:58:56 -04:00
nix Remove the microvm guest runtime from the framework 2026-09-08 11:01:22 +00:00
.gitignore initial public release 2026-07-10 15:07:15 -04:00
check-selftest.sh Add a public-host base for internet-facing machines 2026-08-28 15:44:14 -04:00
check.sh Add a public-host base for internet-facing machines 2026-08-28 15:44:14 -04:00
flake.lock flake.lock: update nexus 2026-09-12 23:11:52 -04:00
flake.nix Add hourly memory trace timer 2026-09-12 22:02:34 -04:00
LICENSE Add LICENSE (GPL-3.0-or-later) 2026-07-17 05:41:56 +00:00
README.md Fix round: rebase pull and retry a rejected push 2026-09-12 22:02:34 -04:00
setup.sh initial public release 2026-07-10 15:07:15 -04:00

archetypes

The Allod VM framework. This repo composes NixOS and Home Manager configurations for every inventory-backed machine archetype (dev, privacy, hypervisor) and exports reusable modules for public hosts. It builds a synthetic example fleet as-is; a deploy flake redirects profiles, inventory, and secrets at an operator's own repos to build real machines.

This repo was named profiles until it was renamed to archetypes — see History.

Ownership

This repo owns:

  • the archetype set and the per-archetype builders (mkDevVm, mkPrivacyVm, mkHypervisor)
  • profile-definition composition (composeProfileDefinitions) and the unknown-archetype, missing-definition, and override-collision guards
  • shared framework modules under modules/ (agent tooling, credentials, netrc, the shared home modules)
  • the public-host base module: the reverse proxy, administrative sshd, fail2ban and firewall an internet-facing machine needs, with the ports and vhosts a service contributes composed into them
  • the generic static-site host module and Zola builder; a site's source, hostname, and deployment entry stay with that site
  • the vmFacts derivation and the nixosConfigurations / nixosModules / checks outputs

This repo does not own:

  • machine profile definitions (which modules compose each machine), the per-machine profileData, and the preferences module: profiles
  • VM framework modules, disk layout, or base guest policy: vm
  • hypervisor framework modules and lifecycle scripts: nexus
  • machine inventory, VM specs, repository registry, IPs, MACs, and platforms: inventory
  • identity data, credential inventory, Forge hosts/users, token paths, and git policy data: secrets

The profiles input

The framework reads machine profile definitions from a single profiles input: profiles.lib.profileDefinitions (per-archetype module lists), profiles.lib.profileData (optional per-machine builder overrides), and profiles.homeModules.preferences. The public example allod/profiles repo ships synthetic definitions; a deploy flake redirects this input at an operator's own definitions repo exporting the same contract. Archetype-name validity and the composition guards live here, the framework being the fact's owner.

Exports

  • nixosConfigurations — the example fleet, composed from this flake's own locked inputs.
  • nixosModules.publicHost — the public-host base for an internet-facing machine: Caddy with ACME and a required contact address, administrative sshd, fail2ban, and the firewall, all set at machine scope. A service module contributes a vhost through allod.archetypes.publicHost.virtualHosts and a listener through extraTCPPorts, and nothing machine-wide.
  • nixosModules.rentedKvmGuest — the boot and disk shape of a rented KVM guest (qemu-guest profile, GRUB on a virtio disk, ext4 root by label). Separate from the base on purpose; the module's header says why.
  • nixosModules.staticSite — one directory-backed Caddy virtual host composed onto publicHost. Its siteRoot must be a store path with string context, so the content is in the machine closure instead of merely named by it, and the system build refuses a root that is itself a symlink, as well as any symlink inside it that escapes that root.
  • lib.buildZolaSite { pkgs, websiteSrc } — build a Zola source tree into a static-site directory after refusing every source symlink.
  • vmFacts — provisioning facts read host-side by nexus as <flake>#vmFacts; one entry per non-hypervisor machine, each carrying ip, runtime, username, forgeKey, hostKeys.{active,staged} and hostKeySecretFile.
  • profilesSource — the store path of the profiles input actually composed with.
  • secretsSource / inventorySource — the exact identity and machine-data inputs this framework composed with.
  • secretsConsumedInventorySource — the inventory revision the composed secrets contract used while validating Pi targets.
  • lib.composeProfileDefinitions { base, overlay } — merge two definition layers, enforcing the override-collision rule; for a profiles repo that layers its own machines onto the public examples.
  • lib.composedLayerCheck { pkgs, expectedProfiles, expectedSecrets, expectedInventory } — a canary derivation that fails when a deploy flake loses any profiles/secrets/inventory redirect, including the inventory revision consumed by secrets.
  • lib.validateComposedLayerSources { expectedProfiles, expectedSecrets, expectedInventory, observedProfiles?, observedSecrets?, observedInventory?, observedSecretsInventory? } — the pure validator behind that canary; deploy checks override one observed source at a time to prove each redirect is load-bearing.

Public hosts

An internet-facing machine is not in inventory and has no identity in secrets, so it does not come from a builder the way the fleet's machines do. What this repo gives it is a module its own flake imports beside its own service definition.

nixosModules.publicHost is the base measured out of two internet-facing machines that were written independently — one stateful, one stateless — and then diffed. Both had reimplemented the same machine-wide posture inside their service modules, which is correct while one service owns one host and is exactly what stops two services sharing one. The base sets that posture, and a service contributes a vhost and its ports.

What it sets: Caddy with ACME and a required contact address, serving the vhosts its consumers contribute; administrative sshd on a caller-chosen port, key-only, with PermitRootLogin at prohibit-password; fail2ban; and a firewall opening 80, 443, the administrative port and whatever a service adds through extraTCPPorts.

It is a default posture, not a guarantee about the finished machine. A consumer can open another port, add a firewall rule of its own, or switch one of these services off — that is what composing NixOS modules means, and the base does not try to make itself unbypassable. It asserts only two things, both of which end in a daemon that does not start: that the administrative SSH port is not 80 or 443, and that nothing has moved sshd off the port the base put it on.

nixosModules.staticSite imports that base and contributes only one Caddy virtual host. The caller supplies its hostname and a built directory in the Nix store, while lib.buildZolaSite supplies the first generator-specific way to produce such a directory. Site identity and source stay in a small consumer entry rather than becoming another machine profile here.

There is no public-host builder yet, and the note above builders in flake.nix says why: neither machine has been deployed, and a builder API fixed now would be derived from evidence that has never booted.

Credentials

A machine decrypts its own secrets. An installer places /etc/ssh/<name> once; sshd presents it as the machine's stable identity, and agenix opens every ciphertext the machine carries with that same file. Which credentials a dev machine carries follows from its identity: the Forge HTTPS credential and API token only when the identity has them, so the forgeAccess opt-out composes a machine with no age secrets at all. modules/netrc.nix fans the decrypted /root/.git-credentials out to the Nix daemon's netrc and the account's, and modules/github-credentials.nix deploys the machine's registered GitHub targets the same way. On a hypervisor, modules/host-user-credentials.nix places the human account's own Forge API token and site-hosting rclone config from the identity's userForgejoTokenFile and siteHostingConfigFile, each only when its field is non-null.

Session traces

modules/memory-traces.nix gives a dev VM an hourly user timer that distills local agent sessions into a configured checkout, then commits and pushes any changes. The checkout must be a private repository the machine can push to; workspace tooling clones it under ~/work, and the timer does nothing when the checkout is absent. Every dev VM shares one traces repository and writes only its own machine subtree, so the rebase the wrapper runs before pushing cannot conflict. The traces are redacted, but they remain private material.

Before reprovisioning a dev VM, run systemctl --user start memory-traces.service on it so the last hour of sessions is committed.

Managed Pi providers

Every dev VM composes modules/pi-provider-lifecycle.nix, including machines whose desired provider set is empty. The framework joins validated metadata from profiles.lib.piProviders to the provider-to-credential projection from secrets.

Every named token of every credential becomes a user-owned 0600 Age secret at the fixed volatile path /run/agenix/pi-provider-token/<credential>/<token>. Home Manager reconciles only the provider IDs recorded in its 0600 ownership manifest. It preserves unrelated auth.json and models.json data, refuses unowned collisions, uses Pi's auth.json.lock protocol, and removes retired entries. A manifest from the earlier single-token layout still records per-credential runtime links; the reconciler retires exactly those recorded links — treating an absent recorded path as already retired and refusing any foreign occupant — and then records credentialLinks as empty. No runtime link is ever created any more. Missing Age material is a warning-only whole-operation no-op that also leaves the installed extension unchanged; malformed JSON, malformed tokens, corrupt ownership state, or concurrent uncooperative writes fail without partial mutation.

Reconciliation runs from the Home Manager boot unit, so a machine configures Pi on first boot with nobody logged in. Rollback snapshots stage on tmpfs — the session runtime directory when a session exists, /dev/shm otherwise — and the reconciler refuses to run at all when neither is memory-backed.

Each managed models.json provider record carries only Pi's command-backed reference to the token selector. The extension applies remembered name, deployment default, then sole-delivered-token precedence at startup; Pi invokes the same selector at request time if no extension selection has replaced that reference. Managed providers write no stored auth.json credential: Pi resolves a stored credential in preference to any provider or extension API key, so one would shadow both the startup command and /token switching; the reconciler only retires previously recorded managed auth.json entries during that one-time legacy-link transition. Afterward, an operator credential stored by /login under a managed provider ID is preserved and reported as an outside-ownership collision because it would shadow /token. The generated /token extension shows and switches the named token for the current provider's credential through Pi's supported provider re-registration: idle switches apply immediately and are then remembered in a 0600 XDG state file holding names only, while switches during active or queued work stay pending until the agent settles. Detached startup pickers are serialized, and Pi refuses an early request on an unresolved credential with an actionable UI diagnostic instead of authenticating an unselected token. The selector validates the volatile token and writes it to Pi's stdout pipe; no bearer is copied into home, generated JSON, argv, the environment, or the Nix store. Pi resolves the registered command-backed key for each request, so a fresh request observes a rotated bearer at the same volatile path; an in-flight or already queued request keeps the old registration until agent_settled publishes a pending switch.

Common commands

./check.sh
nix build .#nixosConfigurations.nexus.config.system.build.toplevel --no-link

./check.sh replaces nix flake check for this repo because the native whole-flake command evaluates all machines and checks in one process, which is too memory-heavy for the dev VM under normal concurrent load. The script reads machine and check names from the flake, runs one nix process per step, keeps going after failures so one run reports every failing step, and refuses unknown or uncovered top-level outputs rather than letting gate coverage drift.

./check-selftest.sh proves the wrapper can still fail by running it against throwaway flakes that cover successful runs, failed machine/check steps, no-op runs, invalid checks system names, uncovered validated outputs, unknown output typos, throwing passive outputs, and valid attribute names that need structured enumeration rather than delimiter parsing.

Provisioning, rebuild, token rotation, and VM SSH host-key commands are hypervisor-side and owned by nexus.

History

This repo was allod/profiles until it was renamed to allod/archetypes; the example machine definitions it used to carry moved to a new allod/profiles repo with fresh history. All pre-rename commits remain fetchable at the allod/archetypes URL — renames only, no history rewrite. A historical deploy lock that pins an allod/profiles framework revision no longer resolves cold once the new definitions repo claims that name; re-resolve it by overriding the input URL:

--override-input <name> "git+https://forge.anarch.diy/allod/archetypes.git?rev=<rev>"