- Nix 75.4%
- Shell 20.4%
- TypeScript 4.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| checks | ||
| hooks | ||
| modules | ||
| nix | ||
| .gitignore | ||
| check-selftest.sh | ||
| check.sh | ||
| flake.lock | ||
| flake.nix | ||
| LICENSE | ||
| README.md | ||
| setup.sh | ||
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
vmFactsderivation and thenixosConfigurations/nixosModules/checksoutputs
This repo does not own:
- machine profile definitions (which modules compose each machine), the
per-machine
profileData, and thepreferencesmodule: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 throughallod.archetypes.publicHost.virtualHostsand a listener throughextraTCPPorts, 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 ontopublicHost. ItssiteRootmust 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 bynexusas<flake>#vmFacts; one entry per non-hypervisor machine, each carryingip,runtime,username,forgeKey,hostKeys.{active,staged}andhostKeySecretFile.profilesSource— the store path of theprofilesinput 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>"