Changelog
Source:
CHANGELOG.mdat2081f31a099e· docs version 0.4.0
All notable changes to tentaflake are documented here.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
0.4.0 - 2026-08-23
Section titled “0.4.0 - 2026-08-23”Breaking
Section titled “Breaking”- OpenCode support is removed completely: its builder, image pin, generated services, CLI discovery, examples, tests, and current documentation are no longer part of the template. Version 0.4 supports Hermes and ZeroClaw; OpenClaw is intentionally deferred.
- Phase 0 removes the Go
tentaflake-auditdpackage, SQLite event database, custom Agent Console,tentaflake top, and the interactive agent wizard. The operator CLI is now a Rust workspace package verified with Rustfmt, Clippy, and Cargo tests. - The live-agent ISO and its firstboot/persistence scripts are removed. The installer ISO is the only bootable image.
- Editor, Hive Research, and Piper TTS leave the core import set and are exported as explicit optional modules.
- Installed systems now default to
tentaflake.security.profile = "balanced". Direct credential env files, host networking, published ports, dashboards, extra devices/networks, sensitive mounts, and mutable images fail evaluation in a secure profile. Existing trusted development systems must explicitly selectdev;strictfails closed until a tested separate-kernel boundary exists. - Automatically started balanced agents now fail evaluation unless their exact container key has an enabled broker, disposable worker, and fixed-size workspace quota. Deliberately stopped migration scaffolds may remain offline.
tentaflake.networking.egressis renamed totentaflake.networking.legacyPortEgressand is dev-only. It was only a host OUTPUT port filter, not a destination allowlist or container FORWARD policy.
just securitynow combines a pinned, telemetry-free Semgrep source scan with current OSV checks for the Rust and Dev Containers CLI dependency lockfiles; GitHub Actions referenced by scanned workflows are commit-pinned.- Contributor E2E recipes now cover the complete automated gate, a frozen-lock Dev Container smoke test, and an interactive installer/installed-system UEFI VM backed only by an isolated persistent QCOW2 file.
- A digest-pinned Dev Container can bootstrap Nix and preload the repository’s
lock-file-backed development shell without mounting runtime sockets or
credentials, giving local editors and Codespaces the same contributor
toolchain as
nix develop. - A complete threat model covering assets, adversaries, trust boundaries, control/evidence mapping, explicit non-goals, residual risks, and the deployment acceptance checklist.
- A per-agent disposable Rust tool-worker: descriptor-safe bounded workspace snapshots, Nix-built offline toolchain image, short-lived no-network gVisor capsules, read-only result import, cleanup, log/audit limits, action classes, and host approval/denial over a private immutable pending request.
TFSEC-020makes a missing disposable worker a high-severity posture finding for secure-profile controllers; module and VM tests cover mismatched policy, no-egress execution, forbidden/approval-required actions, and cleanup.- Fixed-size per-agent ext4 workspace volumes with fail-closed empty-workspace
creation, exact-size drift rejection, checked mount/ownership dependencies,
VM
ENOSPCcoverage, migration documentation, and posture findingTFSEC-021. - Explicit Docker
docker-defaultAppArmor selection for secure controllers and workers, unconfined Seccomp/AppArmor override rejection, posture findingsTFSEC-022/TFSEC-023, and documented Podman evidence limits. - Optional per-agent Cosign key/keyless verification units that gate OCI
startup, an all-secure-agents enforcement switch, module tests, and posture
finding
TFSEC-024when no signature policy is configured. - Opt-in systemd hardware-watchdog settings with no assumed device and an explicit target-host validation warning.
- Observability textfile metrics and Prometheus rules for broker policy/fetch denials, request/token/cost budgets, restart flapping, and root-disk pressure.
- Security-doctor live probes for root-disk pressure, Tailscale Serve/Funnel, and Restic freshness, including explicit unknown-evidence warnings and a hardened post-success backup timestamp unit.
- Non-interactive live OCI inspection for the core privilege, user, runtime, network, capability, root-filesystem, resource, and mount invariants, with unavailable evidence kept explicitly unknown.
- Podman live inspection now uses its documented
OCIRuntime, effective and bounding capability sets, AppArmor profile, and compatible HostConfig fields; missing version-specific fields remain unknown instead of false-critical. - The VM integration definition now includes a Podman node that validates its
generated secure unit, deterministic internal bridge, local image loading,
runsc, non-root/empty capabilities, read-only root, limits, and no egress. - Security manifests now carry exact configured broker modes/endpoints; the doctor distinguishes unreachable evidence from an explicit failed credential/policy/audit health response and detects manifest inconsistency.
- VM negative tests now include a separate external attacker node, actual
runscexecution, direct DNS/Internet/private/metadata denials, PID and worker-runtime ceilings, sensitive-state invisibility, and a successful scoped LLM-broker request from the isolated agent network. - VM acceptance coverage now also exercises host loopback, the individual
private/tailnet/link-local/multicast classes,
/usrimmutability, a changed Git remote, privileged-request denial, unsafe-doctor exit status, and a controlled reboot. Docker live inspection confirms CPU/RAM/swap/PID limits; redirect and DNS-rebinding policy uses a deterministic resolver fixture in the Rust suite. - Broker networks now use deterministic bridge interfaces; host INPUT accepts broker ports only when interface and source subnet both match, preventing a source-address-only allow rule from acting as the authority boundary.
- Contributor E2E now uses a source- and dependency-hash-pinned Dev Containers
CLI 0.88.0 with the upstream
proxy-from-envWHATWG URL parsing change backported for its CommonJS consumer, removing the legacyurl.parse()path that produced Node.jsDEP0169security deprecation warnings. - The installer ISO now embeds its repository below
/etc/tentaflake/source, avoiding a parent-path collision with generated runtime manifests such as/etc/tentaflake/security.tsv. - The development shell now uses the canonical
nixfmtpackage instead of its deprecatednixfmt-rfc-stylealias, removing the flake evaluation warning. - The local
just cigate no longer stops on existing Statix and Deadnix findings; repeated Nix attribute paths are grouped and unused arguments are removed without changing their resulting module options. - Security-manifest generation now records an OCI container with no configured
user as not non-root instead of passing its
nulluser to a string matcher and failing evaluation in thedevcompatibility profile. - The VM integration test now starts its rebooted node with QEMU reboot support enabled and asks systemd to reboot directly instead of relying on a virtual key combination. Its headless agent fixtures also omit kmscon, which can crash on QEMU’s synthetic bochs DRM device, and the stopped-controller fixture now declares the broker that its reboot assertion expects to return. The controlled-reboot assertion no longer waits indefinitely or checks an undeclared boot service.
- The hardened backup-success timestamp service now declares its persistent
path with systemd
StateDirectoryinstead of requiring a nonexistentReadWritePathstarget before its script could create it. This prevents a226/NAMESPACEfailure after an otherwise successful Restic run, and the VM restore and broker-restart probes now report failures within 30 seconds instead of waiting for the global 15-minute retry timeout. - Disposable workers now select backend-compatible bounded log drivers
(
localfor Docker andk8s-filefor Podman) instead of sending file rotation options to ajournalddefault that rejects them. Docker disables compression for its single retained local log file, as required by Engine. - Disposable host snapshots now remain root-owned while source-owner read and
execute access is mirrored to the declared capsule GID for their read-only
bind mount. This keeps private ext4 entries such as
lost+foundreadable without addingCAP_CHOWNto the host worker. Capsule copies still discard host ownership metadata, and VM worker failures now print service diagnostics within 30 seconds instead of waiting for the global 15-minute retry timeout. Direct rootapproveanddenycommands adopt that same declared worker group before touching state, so approved jobs do not create root-group snapshots that their non-root capsule cannot read. - Disposable artifact export now happens before the wrapper exits, while its
bounded workspace tmpfs still exists. Artifacts stream from inside the gVisor
mount namespace instead of relying on an OCI
cpview that cannot see that tmpfs. A bounded importer rejects unsafe paths, links, and special files; the stable completion-marker handshake preserves the real command status without adding a writable host bind before releasing and removing the capsule. - gVisor VM probes now keep a small but startable PID budget. The previous
limit of 16 charged
runscsandbox helpers against the same cgroup and could reject sandbox startup before the test workload ran. - Disposable-worker VM fixtures now request half a CPU, allowing their secure capsules to run inside the integration test’s single-vCPU guest while the production default remains unchanged.
- Path-activated disposable workers no longer use systemd’s aggregate service start counter, which also counted successful oneshot queue drains and disabled a healthy worker after five ordinary jobs. Infrastructure failures retain the ten-second restart delay, while per-job resource and timeout limits remain.
- Broker health and authentication now recognize systemd’s read-only named-ACL
access for
LoadCredentialfiles used withDynamicUser. The ACL mask is accepted only for exact files below$CREDENTIALS_DIRECTORY; ordinary group-readable files and all other-readable files remain rejected. - VM broker-readiness checks now fail after 30 seconds with the raw health response, route, listener, firewall chains, unit status, and journal instead of waiting for the test driver’s 15-minute timeout.
- Disposable worker units now use a statically declared host group matching
the container GID, preserve tmpfiles’ setgid result mode without a blocked
chmod, and inspect NixOS’ generated OCI start script when testing secure runtime flags. - VM secure-unit assertions now inspect NixOS’ generated OCI start scripts for both Docker and Podman instead of expecting runtime flags in wrapper units.
- Disposable workers now retain
openat2as the primary no-symlink workspace opener and use a descriptor-relative, component-by-componentopenatfallback only when the kernel reportsENOSYS. Relative paths, dot/parent traversal, symlinks, and non-directory components remain rejected. - VM broker readiness assertions now retry the actual health response instead
of treating systemd’s
Type=simpleprocess start as socket readiness. - Quota-backed workspaces now mount after ordinary local filesystems without a
local-fs.target/basic.targetordering cycle; worker path watches start only after mounted ownership and control-directory setup completes. - Broker health probes no longer append audit events; startup proves audit writability once, later readiness checks reject symlinks and sync without allowing health polling to churn the bounded audit history. LLM input-token budgets now reserve the conservative request-byte upper bound, and bridge interface hash collisions fail evaluation.
- Secure controller restart/dependency overrides now merge into the actual
OCI-generated systemd service attribute instead of a parallel key ending in
.service; broker and workspace-mount ordering is therefore present in the rendered container unit. - Per-agent Phase B brokered egress: dedicated internal OCI networks, disabled container DNS/IPv6, subnet-scoped host INPUT/FORWARD firewall rules, and a random runtime-only virtual credential per agent.
- A Rust LLM credential broker with exact route/model allowlists, completion
clamps, concurrency/rate/daily token/cost budgets, provider credential
substitution through systemd
LoadCredential, DNS pinning, no redirects or environment proxies, and prompt-free JSONL audit. - A Rust SSRF-safe fetch broker with exact HTTPS host/media allowlists, public IP enforcement, redirect revalidation, DNS-rebinding defense, bounded responses, mode-0700 quarantine, and explicit untrusted-content envelopes.
- Bounded systemd restart policy and credential/audit readiness endpoints for
brokers, plus a combined
tentaflake stoptransaction that stops an agent’s container and all loaded broker endpoints. - An opt-in encrypted Restic backup module with explicit state/audit paths, runtime-only repository/password files, retention, integrity checks, and a VM backup/restore drill.
- A loopback-only observability profile with Prometheus, Grafana, Loki, Alloy, systemd credential loading, and provisioned data sources.
- A separate Falco modern-eBPF runtime-detection profile. Consumers must supply a reviewed pinned Falco package because the pinned nixpkgs has none.
- Direct module-evaluation coverage for core, observability, and Falco profiles.
- One post-merge container security policy shared by Hermes and ZeroClaw:
explicit non-root user, no privileges/capabilities/ports/devices,
read-only root, bounded hardened tmpfs, CPU/RAM/swap/PID/ulimit controls,
sensitive-mount and secret-like-environment rejection, and declarative
gVisor
runscselection without a silent runc fallback. tentaflake doctor --securitywith stable machine-readableTFSEC-*findings and a Nix-generated desired-state manifest.- Secure profiles require the private Tailscale management path, reject the
public OpenSSH module, and advertise the documented
tag:agent-hosttag. - Exact canonical GitHub remote and branch allowlists for host-side
gitAutoPush, repository-scopedsafe.directory, fail-closed errors, and malicious-URL regression tests.
Previous development notes
Section titled “Previous development notes”tentaflake statusnow inventories every non-empty physical disk instead of showing only the root filesystem. A mounted disk is reported once using/or its first data-like mount (excluding/bootand/nix/store); an unmounted disk remains visible by device name and raw capacity. Loop, RAM, zram, and zero-byte card-reader devices are excluded. The screenshot-safe--hideview keeps this non-identifying capacity telemetry, andtentaflake-status --selftestcovers root, mounted data, unmounted, and excluded-device cases with synthetic commands.tentaflake health— host vitals dashboard, the machine-level counterpart tostats. Same renderer again (logo, header, bars, colour thresholds), with a VITALS section — 24-cell bars for CPU busy (a 0.4 s/proc/statdelta, 100% = every core busy), memory, swap, the hottest thermal zone in °C, and every block-device filesystem — and a CHECKS section covering failed systemd units, agent states and Tailscale. The header carries a● healthy/▲ degraded/✗ criticalverdict, the worst finding on the same thresholds the bars are painted with.--live(default 2 s,--live=5to slow it) redraws in place on the alternate screen, so Ctrl-C leaves the scrollback untouched and colours survive — unlikewatch. It always exits 0:tentaflake doctorremains the scriptable check with the nonzero exit.--hideapplies here too and prints a count of failed units instead of their names (a unit name carries its container’s);tentaflake-status --selftestnow renders the masked health view alongside the masked wide view and fails if either leaks../scripts/banner-test.sh --healthpreviews and regression-checks it on a dev box.tentaflake stats— fleet dashboard sharing the login banner’s renderer (same logo, header and rows, so the two cannot drift), one density wider: per-agent CPU%, PID count, memory against the container limit, and a fleet total against host RAM. CPU is a 0.4 s cgroup delta on thedocker statsscale (100% = one busy core); for a live view,watch -cn2 tentaflake stats.- Per-agent memory column in the login banner. Read straight from each container’s cgroup (
memory.current - inactive_file, the exact numberdocker statsreports) instead ofdocker stats’ ~2 s sample window, so login stays fast. Known docker and podman cgroup layouts are probed; an agent without a readable cgroup simply gets no cell. --hide/-Honstatusandstats: masks host name, tailnet IP and agent names (agent-1…agent-N, the failed-agents hint included) for screenshot-safe output, with a yellow· redactedheader marker. Telemetry stays.tentaflake-status --selftestrenders the masked wide view and exits nonzero if any identifying string leaks.- DCO enforcement for pull requests: every non-merge commit must include a
Signed-off-by:line; seeDCO.txtandCONTRIBUTING.md. nix developnow greets you: the dev shell prints the tentaflake logo with branch/HEAD/working-tree/Nix-version rows and a shortjustcheat sheet, and sets a(tentaflake)prompt marker. Same braille art and layout as the host login banner — both readpublic/tentaflake-shell-logo.txt. The banner is skipped when stdout is not a TTY (nix develop --command …in CI stays clean) andTENTAFLAKE_NO_BANNER=1disables it entirely. The dev shell moved from an inlinepkgs.mkShellinflake.nixtolib/devshell.nix; the package set is unchanged, and the banner script is built withwriteShellApplication, sonix flake checkshellchecks it.
0.3.1 — 2026-07-19
Section titled “0.3.1 — 2026-07-19”⚠ Breaking
Section titled “⚠ Breaking”- Default hostname and admin user renamed:
agent-host→tentaflake,admin→user(lib/constants.nix). The built-in host’s flake attribute follows the hostname (it is now writtennixosConfigurations.${hostName}, so the two cannot drift), which meansnix build .#nixosConfigurations.agent-hostandnixos-rebuild --flake .#agent-hostno longer resolve — use.#tentaflake. Same for the check attrchecks.<system>.agent-host→checks.<system>.tentaflake. Installed machines are unaffected: the installer generatesnixosConfigurations.${uc.hostName}from the hostname you typed, sotentaflake rebuildkeeps working. The installer’s suggested defaults also change (agent-machine→tentaflake,agent→user); both remain editable in the wizard. - The fleet-generic infrastructure drops its
hermes-branding (#52); “Hermes” now only names the Hermes agent runtime. What renames, and what bridges the transition for one release:- systemd units:
hermes-auditd→tentaflake-auditd, and on the live ISOhermes-env-detect/hermes-data-mount→tentaflake-env-detect/tentaflake-data-mount. The old unit names are gone — updatesystemctl/journalctlscripts. (The console unit was alreadytentaflake-console.) - module options:
tentaflake.hermes-auditd.*→tentaflake.auditd.*. Old names still evaluate with a deprecation warning vialib.mkRenamedOptionModuleand will be removed in a future release. - flake package attr:
packages.hermes-auditd→packages.tentaflake-auditd. The old attr remains as a deprecated alias to the same derivation. - binaries:
hermes-auditd→tentaflake-auditd,hermes-top→tentaflake-top(tentaflake topnow execs the new name). The package installs a deprecatedhermes-topsymlink for one release. - USB labels (live ISO unattended boot; the interactive
tentaflake agent addneeds no label at all):TENTAFLAKE_ENV/TENTAFLAKE_DATAare the primary labels; the legacyHERMES_ENV/HERMES_DATAlabels are still accepted, so existing sticks keep working. - live-ISO env dir:
/run/hermes→/run/tentaflake, with a/run/hermescompat symlink for configs that hardcode the oldenvFilepath. - Go module (fork-relevant only):
tentaflake/hermes-auditd→tentaflake/tentaflake-auditd;internal/hermes→internal/event. - NOT renamed (deliberately, so existing deployments keep their audit history): the audit DB state dir
/var/lib/hermes-audit, the defaultdbPath/var/lib/hermes-audit/events.db, and thehermes-audituser/group. These rename in a future major with a migration. - Also: the
hermes-<name>/zeroclaw-<name>container/user/state-dir prefixes, theHERMES_*env contract, andmkHermesAgentare unchanged — they name the Hermes runtime, not the infrastructure.
- systemd units:
- Digest pinning is enforced, not merely defaulted (
lib/pinnedImage.nix): a previously-validimage = "repo:tag"is now a hard evalthrowin all three builders (mkHermesAgent,mkZeroClawAgent,mkOpenCodeAgent). Any fork or consumer flake that overridesimagewith a tag failsnixos-rebuildoutright, before a single derivation is built. Repin torepo@sha256:<digest>(the error message prints theskopeo inspectcommand that reports it), or setallowMutableImage = trueon that agent to acknowledge giving up reproducibility. Note thatrepo:tag@sha256:...is rejected too — keep the version in a comment, not in the reference. See Security below for the full rationale. - The physical console is rendered by kmscon on TTY1 by default on installed systems (
tentaflake.modernConsole.enable, defaulttrue). On hardware where kmscon cannot grab the framebuffer the operator loses the physical console — there is no local login left to fix it from, only Tailscale SSH or a rescue boot. On unfamiliar hardware, check the console on the first rebuild while remote access is still up;tentaflake.modernConsole.enable = false;falls back to the legacy VT. Both ISOs are unaffected — they keep the VT deliberately. See Added below. - The live agent ISO’s debug sshd no longer accepts password auth (
PasswordAuthentication = false). Logging into a booted live ISO with the live user’s password stops working — use Tailscale SSH, or the key-onlymodules/ssh.nixwithtentaflake.adminAuthorizedKeys. See Changed below. - The host Nix daemon is locked down (
modules/nix-settings.nix):allowed-usersis nowrootand@wheelonly, so any non-wheel user or service account that used to runnix build/nix-storeagainst the host daemon is refused outright; andsandbox-fallback = falseturns a build that cannot be sandboxed into a hard failure instead of a silent unsandboxed one. See Changed below.
Security
Section titled “Security”tentaflake-auditdhardening (#8): the audit DB now setssecure_deleteand is capped at 10000 pages (~40 MB) so an agent flooding filesystem events cannot fill the host disk; inotify directory watches are capped at 10000 per daemon (with a one-time warning when hit); the console clamps?limit=to 1000 and every store read is bounded by a 5 s query timeout. (Nofs.inotify.max_user_watchessysctl needed — nixpkgs already defaults it to 524288.)- Default Hermes, ZeroClaw and OpenCode container images, plus the documented Nix build image, are pinned to exact multi-platform OCI manifest digests (
lib/constants.nix). Identical configurations no longer resolve mutable tags to different image contents across machines or over time. Refresh the pins with./scripts/update-agent-images.sh(it prints upstream digests for all three runtimes; the bump stays a manual edit). - Digest pinning is now enforced, not just defaulted (
lib/pinnedImage.nix).mkHermesAgent,mkZeroClawAgentandmkOpenCodeAgentreject an unpinnedimageat eval time, so a fork cannot silently switch back to a mutable tag; the check runs after theextraContainerConfigmerge, which could otherwise overrideimagepast it. Locally-built images opt out per-agent withallowMutableImage = true. Covered by the newimage-pinningflake check. - Image references are also character-whitelisted, including under
allowMutableImage. nixpkgs’oci-containersmodule splicescontainer.imageinto the container unit’s shell command withoutescapeShellArg(unlike every neighbouring element), so a reference carrying whitespace or a shell metacharacter could inject extradocker runflags — or a command substitution running as root — behind a digest-shaped suffix. - Pinned references are written
registry/repository@sha256:digest, neverrepository:tag@sha256:digest. The docker CLI accepts the tag-plus-digest form but podman and skopeo reject it outright (“Docker references with both a tag and digest are currently not supported”), which would have broken everytentaflake.containerBackend = "podman"host. - CI workflow hardening (
.github/workflows/check.yml): a top-levelpermissions: contents: read, and all five third-party actions pinned to full 40-character commit SHAs with a trailing version comment.GITHUB_TOKENis otherwise write-capable on pushes while every step in this workflow only reads the repo, and a tag like@v4is re-pointable by whoever owns the action — an unpinned action is arbitrary code execution in a job that holds the repo token.golangci-lint-actionmoves v6 → v9.3.0 in the same pass rather than being pinned where it was: v6 only drives golangci-lint v1, and this repo’s.golangci.yamlis the v2 format, so pinning v6 would have failed outright. The linter itself is pinned tov2.12.2instead oflatest, so an upstream lint release can no longer break CI on an unrelated PR. (v9 additionally validates.golangci.yamlagainst its JSONSchema, which v6 skipped — worth watching on the first run.)
mkOpenCodeAgent— a third agent runtime builder (OpenCode) alongsidemkHermesAgent/mkZeroClawAgent. Runsopencode serve(headless HTTP, documented OpenAPI 3.1) in the same isolation contract (own container/uid/0700state dir +/workspace, secrets via--env-file), forwarding a loopbackhostPortto the gateway with an optionaltailscale serveonservePort. Ideal for external orchestrators (n8n, CI):POST /sessionthenPOST /session/<id>/message. It joins the other two runtimes’ safety contract: the image defaults to a digest pin inlib/constants.nixand goes through the same enforcedpinnedImagecheck (mutable tags are an eval error;allowMutableImage = trueopts a locally-built image out). SettingservePortnow requires anenvFileoragenixFile— the container is started with--hostname 0.0.0.0so the gateway is reachable across the container boundary, andopencode serveonly turns on HTTP basic auth whenOPENCODE_SERVER_PASSWORDis set (username defaults toopencode), so publishing without one put an unauthenticated agent on the tailnet; the assertion checks the env file is wired up, andallowUnauthenticatedServe = trueis the explicit acknowledgement. Because Nix cannot read a runtime/agenix file at eval time, the serve unit re-checks at start: anExecStartPregreps the env file(s) for a non-emptyOPENCODE_SERVER_PASSWORDand refuses to publish without one — so the documented proxy-key pattern (an env file carrying only an LLM key) fails closed instead of satisfying the assertion and publishing anyway. It also runstailscale serve --https=<servePort> offon that path, becausetailscale serve --bgstate is persisted by tailscaled and restored on boot: dropping the password de-publishes the agent rather than leaving a stale mapping in front of a now-unauthenticated session API. The password must live inenvFile/agenixFileand notextraEnvironment— that attrset is rendered into the container unit’s start script in the world-readable Nix store. Thetailscale serveunit also gained anExecStop(tailscale serve --https=<servePort> off), so removing or stopping an agent tears its tailnet mapping down instead of leaving a dangling public endpoint pointed at a dead port. Credentials via a local LLM proxy (settingsbase_url + env-file key, host-portable) or an opt-in read-onlyauthFile. Exported fromlib.<system>.mkOpenCodeAgentand passed tomy-agents.nix(older files taking only the previous builders keep working viaintersectAttrs). Wired into the same multi-runtime plumbing as the other runtimes: thetentaflakeCLI/status banner discovery, thetentaflake-auditdstate-dir watcher (GoagentNameFromPath+ NixOS auto-discovery), and the installer’s env-placeholder/USB-persistence loops all recognize theopencode-prefix; the VM integration test now declares an OpenCode agent and asserts its unit +0700state dir. Seedocs/08-opencode.mdandmy-agents.nix.example.- Modern physical console (
tentaflake.modernConsole.enable, on by default): the installed system renders TTY1 with kmscon (KMS/DRI + pango) instead of the legacy Linux VT, so the console gets real TTF fonts and full Unicode. A Linux VT maps at most 512 glyphs and the kernel’s built-in font ships 256 CP437-era ones, so on the machine’s own screen the braille logo intentaflake-statusandbtop’s graphs rendered as boxes — noconsole.fontcould fix that, the ceiling is the VT. Default font is Cascadia Mono (one of the few modern monospace fonts covering U+2800–28FF; JetBrains Mono, Hack and Fira Mono do not), size viatentaflake.modernConsole.fontSize(default 14). The keymap followstentaflake.consoleKeyMapthrough xkb, since kmscon replaces getty. Disable it on hardware where kmscon cannot grab the framebuffer — the legacy VT then uses the newtentaflake.consoleFont(Terminuster-v16nby default, still far better than the kernel font for box drawing).consoleFontapplies only when kmscon is off, andnullskips the boot-timesetfontentirely; both ISOs usenull, because theirdialogTUIs draw in ASCII anyway and the fbcon reconfiguration flickers on some Intel panels (pipe A FIFO underrun). Both ISOs deliberately keep the legacy VT: kmscon hands the login a pty and owns the VT in graphics mode, which would defeat the[ "$(tty)" = /dev/tty1 ]guard that auto-launchesinstaller.shand the live ISO’s firstboot wizard (the latter also writes straight to/dev/tty1). Seedocs/06-shell.md#physical-console-kmscon. tentaflake agent add|list|set-model|remove— an interactive CLI wizard for declaring Hermes and ZeroClaw agents without writing Nix. Config lands in a flat, non-secret, git-trackedagents.json(schema v1: name/provider/model/base_url/ports/envFile path) read bylib/agentsFromData.nix, additive to a hand-writtenmy-agents.nix. API keys are read withread -rs(or imported from a USB stick, see below) and written only to a root-owned0600file at/var/lib/tentaflake/secrets/<runtime>-<name>.env— never intoagents.jsonor the Nix store. Seedocs/08-agent-cli.md.- USB key import in
tentaflake agent add— nobody wants to type a 73-character OpenRouter key. The wizard searches already-mounted media (/run/media/*/*,/media/*,/mnt/*) and, only if that finds no key, unmounted removable partitions which it mounts itselfro,nosuid,nodev,noexec; it looks two directories deep for.envfiles carrying the provider’sKEY=line and for.txt/.keyfiles holding a bare token. No filesystem label and no filename convention are required — theTENTAFLAKE_ENVlabel is unchanged and undeprecated, it stays the deterministic marker for the live ISO’s unattended boot and here merely ranks a volume first. Findings are presented with a masked preview (first 6 + last 4 + length) and their source volume/file, never silently consumed; “type or paste it myself” is always available and the wizard works with no stick at all. Files on removable media are parsed with a regex, neversourced,evald or executed, and every mount is released on abort, error or Ctrl-C. Typed keys now also get a masked echo-back and a retry, so a truncated paste is visible. - Pinned logo during
tentaflake agent add— the logo stays at the top of the terminal for the whole wizard using the terminal’s own scroll region (DECSTBMCSI top;bottom r), no curses and no new dependency;tputis unusable here because the installed console is kmscon andTERM=kmsconhas no terminfo entry, so geometry comes fromstty size. Restored via anEXIT/INT/TERM/HUPtrap. Degrades to plain sequential output when not on a tty, underNO_COLOR, or onTERM=dumb; a window under 24×60 (or a legacy Linux VT, which has no braille glyphs) gets a compact two-row header instead, down to 12 rows × 40 columns. Colour is now gated globally on the tty /NO_COLOR/TERM=dumbconditions, independent of window size, so alltentaflakeoutput is clean when piped. - NixOS VM integration test (
checks.x86_64-linux.vm-integration,tests/integration.nix): boots a VM built fromnixosModules.defaultand asserts the runtime path — thetentaflakeCLI runs (help,ps), the status banner renders and names the host,tentaflake-auditdcomes up and creates its SQLite event DB, and each declared agent produces its systemd unit and0700state dir (the Hermes agent also its system user; the OpenCode agent runs as an anonymous uid, so it has none, and gets its/workspacechecked instead). The daemon subtest waits for/var/lib/hermes-audit/events.dbrather than a port, becausetentaflake-auditdopens no socket at all — it writes events to SQLite and the separatetentaflake-consoleservice is the HTTP surface — andType=simplemarks the unit active before the binary has run, sowait_for_unitalone would race the DB creation. Agents are declaredautoStart = falseso the sandboxed, offline VM never tries to pull an image: the test proves each unit is defined, not that a container runs. The VM node pins its ownhostName/adminUser(agent-host/admin) instead of inheriting the template defaults, so the banner assertion is unaffected by theagent-host→tentaflakerename above; it also setsnixSettings.enable = false, since that module definesnixpkgs.config.allowUnfreeand the test harness declaresnixpkgs.configread-only. Runs as part ofnix flake checkand CI. Previously CI only proved the config built; nothing exercised runtime behavior. - CI now runs
golangci-lintonpkgs/tentaflake-auditd(.github/workflows/check.yml), enforcing the lint standard the contributor docs already required. tentaflakeCLI host-management subcommands (#51):rebuild(nixos-rebuild switch on the system flake — same command as therebuildalias),update(nix flake updateon/etc/nixos, shows theflake.lockdiff, asks y/N, then rebuilds),doctor(deep health check — failed systemd units, root disk ≥90%, Tailscale,tentaflake-auditd/tentaflake-consoleservice state when enabled, per-agent unit state — every problem paired with its exact fix command; nonzero exit when problems found),console(Agent Console URL + thetailscale servepublish one-liner, or how to enable the console when it’s off), andbackup <name>(one-shotsudo tarsnapshot of the agent’s state dir to./tentaflake-<name>-<UTC timestamp>.tar.gz, with an active-agent consistency warning and the matching restore one-liner). Agent records now carry each agent’s state dir, derived from the container’s first volume mount (so customstateDirs are honored).- Opt-in egress filtering (
tentaflake.networking.egress.enable, default off): an nftables output chain (tabletentaflake-egress, policy drop) allowing loopback, established/related, ICMP/ICMPv6, and configurable outbound ports (allowedTCPPortsdefault[443],allowedUDPPortsdefault[53 67 123 547 41641]— DNS, DHCP, NTP, DHCPv6, tailscale). Because agent containers use host networking, the host rules cover the whole fleet. (#9, docs) modules/ssh.nix— opt-in hardened OpenSSH server (tentaflake.ssh.enable, default off): key-only auth (no passwords, no keyboard-interactive, no root login, max 3 auth tries), fail2ban, and TCP 22 opened in the otherwise deny-all firewall. Tailscale SSH remains the primary access path; admin keys come fromtentaflake.adminAuthorizedKeys.- CI security workflows: CodeQL analysis of the Go code (
.github/workflows/codeql.yml, push/PR to main + weekly), gitleaks full-history secret scanning (.github/workflows/gitleaks.yml), and a weeklyflake.lockupdate PR via DeterminateSystems/update-flake-lock (.github/workflows/update-flake-lock.yml). .pre-commit-config.yaml— optional local hooks mirroring the CI gates (gitleaks, shellcheck, gofmt,go vet,nix fmt); setup documented in CONTRIBUTING.md..agents/skills/tentaflake-change-review/— a bundled skill carrying this repo’s change-review discipline, alongsidetentaflake-repo-guidance. It borrows one idea from the heavyweight AI-engineering frameworks — traceability: every change ties back to a stated reason (issue/requirement) and forward to a verification step, with doc sync before it counts as done — and strips the ceremony. Three gates, no lifecycle, no review matrices; sized for a ~30-file NixOS template maintained by Conventional Commits and PRs, and explicitly skipped for formatting, typo and comment-only edits. Its target is orphaned changes (code with no stated reason), silent assumptions and doc drift.scripts/banner-test.sh(alsojust banner) — renders thetentaflake-statusbanner with a stubbedsystemctland a fake mixed-runtime fleet (active/inactive/failed) so the banner can be previewed and regression-checked on any dev machine; self-checks cover fleet counters, duration formatting, logo loading, and logo/info-column alignment. The wizard’s own logic is covered by the hiddententaflake agent __selftest(env/bare key parsing including a “must not execute” fixture, masking, header geometry and degradation, scroll-region teardown).
Changed
Section titled “Changed”- Live agent ISO: the debug sshd no longer accepts password auth (
PasswordAuthentication = false) — Tailscale SSH covers remote debugging on the live ISO. pidsLimitparameter onmkHermesAgentandmkZeroClawAgent— renders--pids-limit=<n>on the agent container (default512, generous because agents compile code;nullskips the flag). Hermes containers additionally gain--security-opt=no-new-privileges:trueby default (ZeroClaw containers already had it). Opt-in--cap-drop=ALLrecipe and the docker-group-is-root-equivalent tradeoff (podman as the rootless alternative) documented indocs/02-agent-tips.mdanddocs/07-operations.md. (#3)- Kernel hardening (#4):
hardening.nixgains sysctls (kexec/sysrq off, BPF JIT hardening, TCP RFC 1337, ICMP broadcast/bogus-error ignore, loose rp_filter — Docker-compatible, martian logging, ARP ignore/announce) ,boot.kernelParams(slab_nomerge,init_on_alloc/free,pti=on,vsyscall=none,debugfs=off,randomize_kstack_offset=on), and an explicit LSM order viasecurity.lsm(landlock,yama,apparmor,bpf);boot.nixdisables the systemd-boot menu editor (blocksinit=/bin/sh). - Git hygiene:
.gitignorenow coversage.keyand*.log;CONTRIBUTING.mddocuments SSH commit/tag signing and signed release tags (git tag -s). (#10) docs/04-agenix-secrets.md: “Key Rotation & Recovery” section — rotating secret values, rekeying out a compromised recipient (old ciphertext in Git history stays exposed), recovering from a lost host SSH key via any surviving recipient, and an example rotation cadence for forks. (#6)SECURITY.md: operator incident response runbook — isolate the container, revoke provider keys, inspect thetentaflake-auditdaudit trail (UTC timestamps), preserve the state dir, rotate secrets and rebuild. (#11)docs/07-operations.md: Backup & restore (state dirs, audit DB viasqlite3 .backup— nevercpa live WAL db — off-host age identity, restic example for forks) and Log forwarding (services.journald.uploadsnippet) sections. (#11)modules/piper-tts-server.nix: systemd unit hardened to parity withtentaflake-auditd/hive-research(ProtectSystem=strict,ProtectHome,PrivateDevices,RestrictNamespaces,LockPersonality, empty capability bounding set,RestrictAddressFamilies,SystemCallFilter=@system-service) plus resource limits:TasksMax=64and a newservices.piper-tts-server.memoryMaxoption (default2G— raise for larger voice models). (#12)modules/nix-settings.nix: Nix daemon hardening —allowed-usersrestricted torootand@wheel(agents live in containers and never talk to the host daemon), explicitsandbox = truewithsandbox-fallback = false(no silent downgrade to unsandboxed builds), andmin-free/max-free(2 GiB / 8 GiB) so builds cannot fill the disk. (#5)- Live-ISO firstboot env detection genericized (#52): agent env-file placeholders are now created for
docker-/podman-units of both runtimes (hermes-*andzeroclaw-*, previouslyhermes-*only), and the USB data-mount persists/var/lib/zeroclaw-*state dirs too. Hermes env files keep their bare names (coding.env); other runtimes keep the runtime prefix (zeroclaw-scout.env). .golangci.yamlmigrated to the golangci-lint v2 config format — the v1 file no longer loaded under golangci-lint 2.x, so lint was silently broken; the errcheck findings that had accumulated unseen are fixed.tentaflake-statuslogin banner redesigned: braille-art octopus-snowflake logo in cyan (embedded at build time frompublic/tentaflake-shell-logo.txt, the single source of truth) with the header and host facts rendered as a column to its right, and the container backend in the tagline;AGENTSheader now cyan with a fleet count (total · active · inactive, plusfailedin red when present); each runtime gets its own color (hermes yellow, zeroclaw blue, other magenta) on dot/runtime/status; agents sorted by name; active agents show their uptime (active 2d 4h); inactive agents render dimmed; a failed agent adds a red⚠ failed: <name> — tentaflake logs <name>hint. Also: memory/disk lines gain usage-colored percentages (green/yellow ≥75%/red ≥90%), host uptime is read from/proc/uptime(fixes the duplicated load average in the old fallback), and a separator rule divides host facts from the agent list.
-
Installer: adding the first agent on an installed machine broke
tentaflake rebuildwitherror: infinite recursion encountered. The flakeinstaller.shgenerates for the target passed onlymkHermesAgentandconstantsinspecialArgs, butconfiguration.nixalso takesmkZeroClawAgentandagentsFromData— and consumes them insideimports. A missing helper there does not fail loudly: Nix falls back toconfig._module.args, which needsconfig, which needsimports, so the rebuild dies in recursion instead of naming the absent argument. It stayed hidden on a fresh install becauselib.optionals (builtins.pathExists ./agents.json)leavesagentsFromDataunforced until anagents.jsonexists — so the very firsttentaflake agent addtriggered it. The generated flake now splats the wholelib/default.nixhelper set, so it cannot drift from whatconfiguration.nixasks for again. Covered by the newscripts/generated-flake-test.sh. Machines already installed need a one-line manual patch — see docs/08-agent-cli.md. -
Installer:
nixos-installfailed evaluating the target flake with areadFileerror — the redesignedtentaflake-statusbanner readspublic/tentaflake-shell-logo.txtfrommodules/shell.nix, but the installer never copiedpublic/into/mnt/etc/nixos. The installer now copies the logo file (just that file, not the multi-MB imagery inpublic/). -
USB data persistence (
tentaflake-data-mount) and the env-file placeholder loop (tentaflake-env-detect) never ran at all. Neither unit declared apath, so both got only the default service PATH (coreutils, findutils, gnugrep, gnused, systemd) — noblkid,mount/umountorawk.blkidexiting 127 was swallowed by the surrounding|| true, leavingUSB_DEVempty and tripping the[ -z "$USB_DEV" ] && exit 0guard on every boot, so aTENTAFLAKE_DATAstick was silently ignored and agents never got their placeholder.envfiles (which is what madedocker run --env-filefail on a cold boot). Both units now declarepath = [ pkgs.util-linux pkgs.gawk ]. -
USB data persistence could destroy agent state (
installer/firstboot.nix).tentaflake-data-mountused tomkdir -pthe target on the USB stick, thenrm -rfthe real/var/lib/<runtime>-<name>and symlink it — with nothing verifying that the copy had landed or that the target filesystem could even hold Unix ownership. The target was created root-owned0755, so an agent running as its own uid could no longer write its own0700state dir; on the vfat/exfat stick most people hand-make, “state dir is 0700 and owned by the container uid” quietly evaporated altogether. Any non-empty target was treated as authoritative, so resuming after a partially-copied stick replaced a complete local state dir with a truncated one and reported success. And on a full or read-only stick therm -rfran regardless, so the agent’s state was simply gone. Persistence now goes through astage_to_usb()helper that mirrors the original uid/gid/mode onto the USB directory and re-stats to confirm it stuck (a filesystem that cannot hold Unix ownership is detected and skipped), then branches on the state that is actually present:- USB target empty — seed it from the local dir, verify by comparing entry counts, and only then remove the original and symlink.
- Local dir empty — the USB copy wins and is symlinked in. This is the ordinary reboot path: tmpfiles recreated an empty state dir and the real state is on the stick, so there is nothing to lose.
- Both sides hold state — refuse. Copying either way discards the other and nothing can know which the operator wants, so it logs
… both hold state — refusing to discard either; resolve by handand touches neither side. Resolving a conflict is never done by discarding.
A failure leaves the original directory fully intact and logs
WARNING: could not persist … — keeping local state, no USB persistence. The one path that cannot honestly claim that says so instead: if therm -rfsucceeded but the symlink did not, the state is on the stick, and the message isERROR: … removed but symlink … failed — state is on the USB, relink by hand. The helper’s exit status is captured rather than left toerrexit, so one unpersistable agent no longer aborts the whole loop, and it sits inside the existing loop, so all three runtimes (hermes-*,zeroclaw-*,opencode-*) are covered at once. Covered byscripts/data-mount-test.sh. -
modules/piper-tts-server.nix: everyPOST /v1/audio/speechreturned500 piper binary not foundwhile/healthand/v1/modelskept answering, so the service looked healthy and only synthesis was dead. The server shells out topiperviasubprocess.run, but/run/current-system/sw/bin— whereenvironment.systemPackageslands — is not on a systemd unit’s default PATH. The unit now declarespath = [ pkgs.piper-tts ]. Pre-existing since the module landed in 0.2.0, not caused by this release’s sandbox hardening; found while reviewing it. -
tentaflakere-enabled terminal echo on exit even when it had never disabled it.tf_cleanupruns from anEXITtrap on every invocation — includingtentaflake help— and calledstty echounconditionally, corrupting any caller on a-echoterminal (serial console, scripted pty, the NixOS test driver’s backdoor console, where it desynced the driver outright). Echo is now restored only when a maskedread -rsactually turned it off.
0.2.0 — 2026-07-11
Section titled “0.2.0 — 2026-07-11”⚠ Breaking
Section titled “⚠ Breaking”- The host operator CLI is now
tentaflake. Thehermeshost command remains in this release as a deprecated shim (prints a warning to stderr, execstentaflake) and will be removed in a future release — update scripts and habits. (ThehermesCLI inside Hermes agent containers is unrelated and unchanged.) tentaflake.shell.hermesCli.enable→tentaflake.shell.tentaflakeCli.enable. The old option still evaluates vialib.mkRenamedOptionModulewith a deprecation warning and will be removed together with the shim.my-agents.nixis now expected to accept{ mkHermesAgent, mkZeroClawAgent }. Inside this template,configuration.nixonly passes the arguments your file declares, so old{ mkHermesAgent }:-only files keep evaluating — but flake-input consumers that callimport ./my-agents.nix { inherit mkHermesAgent mkZeroClawAgent; }directly must update old files to the new signature (or add...).tentaflake psbehaves differently from the oldhermes ps: it lists agent containers of all runtimes including stopped ones (--all+ anchored name filters) instead of--filter name=hermes-on running containers only.- Agent Console /
tentaflake toplabel non-Hermes agents with a runtime prefix (zeroclaw-<name>). Hermes agents keep their bare labels, so existing audit DBs and dashboards are unaffected.
lib/mkZeroClawAgent.nix— second agent runtime alongside Hermes: OCI container (ghcr.io/zeroclaw-labs/zeroclaw), TOMLsettingsattrset generated into a read-onlyconfig.toml, atailscale serveunit for tailnet HTTPS access, and an optionalseedDircopied into the state dir on first boot. State dir/var/lib/zeroclaw-<name>, secrets viaagenixFile(--env-file).zeroclaw.env.example— mirrorshermes.env.example; documents theZEROCLAW_<section>__<sub>__<key>env-var convention ZeroClaw uses for config overrides, with an OpenRouterapi_keyexample.my-agents.nix.examplenow takes{ mkHermesAgent, mkZeroClawAgent }and defines both ahermesAgentslist and azeroclawAgentslist (map mkHermesAgent hermesAgents ++ map mkZeroClawAgent zeroclawAgents), with a fully-commented generic ZeroClaw reference agent (schema_version, model, agentic runtime profile, supervised risk profile).my-agents.nix.example: rewritten around a per-runtime agent list (nowhermesAgents, see above) with a fully-commented generic reference agent documenting everysettingsoption (model + fallbacks, auxiliary routing, compression, memory, toolsets, approvals, web/terminal backends, provider timeouts, TTS/STT) plus seed/volume/env/container fields. Active agent renameddefault→codingto match the docs. Quickstart and agent-tips docs updated to the new structure.modules/shell.nix: optional zsh (tentaflake.shell.zsh.enable) — Oh My Zsh + autosuggestions + syntax-highlighting + fzf-tab, with Starship as the prompt; becomes the admin login shell when enabled. Plustentaflake.shell.zoxide.enable(smart-cd, cross-shell, default on) andtentaflake.shell.lazygit.enable(+lgalias). Aliases moved toenvironment.shellAliasesso they apply to bash and zsh.modules/shell.nix: tmux (tentaflake.shell.tmux.enable, configured). Convenience aliasesrebuild(→nixos-rebuild switch --flake /etc/nixos#<host>),reload(exec $SHELL) andcls. tmux moved out of thetoolspackage set into its own toggle.modules/editor.nix+nixosModules.editor: optional Neovim via nvf (tentaflake.editor.nvf.enable) — LSP, treesitter, telescope, gitsigns, blink-cmp; lean language set (nix/bash/lua/markdown/yaml). Added thenvfflake input (kept out ofnixosModules.defaultso external consumers aren’t forced to have it).- Installer: a feature checklist (zsh / zoxide / nvf / lazygit / modern tools) whose selections are written into the generated
/etc/nixos/flake.nixastentaflake.*toggles (nvf also injects the rev-pinned input + editor module import). hermes-top— live TUI dashboard (bubbletea) of agent filesystem activity, launched viatentaflake top. Reads thehermes-auditdSQLite DB directly (no network surface — runs over Tailscale SSH). Implements the read side the daemon previously discarded (theinternal/storealready had the query methods).hermes-auditdis now wired up: enabled by default onagent-host,watchDirsauto-derives from the agents defined inmy-agents.nix, runs as an unprivilegedhermes-audituser with onlyCAP_DAC_READ_SEARCH, and stores its DB group-readable so the admin can runtentaflake topwithout sudo.internal/store:Since(afterID, limit)(incremental tail) andAgentRows(window)(per-agent activity summary) read helpers.modules/shell.nix— operator shell experience for SSH/console: the agent-management CLI (tentaflake, see Changed), dynamictentaflake-statuslogin banner, Starship/bash prompt, completion, and a curated modern CLI tool set. Toggle viatentaflake.shell.*. Seedocs/06-shell.md.modules/piper-tts-server.nix— local TTS via Piper (OpenAI-compatible/v1/audio/speech)modules/hermes-firstboot.nix— USB env detection + first-boot TUI wizardnixosConfigurations.live-agent+nix build .#live-agent-iso— bootable ISO with Hermes + TTS
Changed
Section titled “Changed”- Host operator CLI renamed
hermes→tentaflake(same subcommands:status|logs|restart|start|stop|shell|exec|ps|top|help), now multi-runtime aware —tentaflake status/psand the login banner list agents across all runtimes with a runtime column. A deprecatedhermesshim still works: it prints a deprecation note to stderr and execstentaflake. tentaflake.shell.hermesCli.enablerenamed totentaflake.shell.tentaflakeCli.enable(old name still accepted vialib.mkRenamedOptionModule).modules/hermes-auditd.nix:watchDirsauto-discovery widened fromhermes-*containers to all declarative agent containers (hermes-*/zeroclaw-*prefixes), so ZeroClaw state dirs are watched too — unrelated oci-containers are still excluded. Auto-derived console roots keep bare names for Hermes (coding) and runtime-prefixed names otherwise (zeroclaw-assistant).hermes-auditd0.1.3: event attribution (agentNameFromPath) andhermes-toppath shortening now recognize/var/lib/zeroclaw-<name>state dirs — ZeroClaw activity is labelledzeroclaw-<name>instead of lumped intounknown; Hermes labels stay bare for existing DBs.flake.nixnow exportsmkZeroClawAgentalongsidemkHermesAgent(lib.${system},specialArgs);configuration.nixpasses each builder tomy-agents.nixonly if the file’s function asks for it (lib.functionArgsintersection), so old{ mkHermesAgent }-only files keep working unmodified.
store.go: fixed event time-window comparisons (Stats,Prune) — stored RFC3339 timestamps were string-compared against SQLite’sdatetime('now', …)(space-separated) form, which only agreed when the date differed, so same-day events outside the window were mis-counted and never pruned. Now normalized viadatetime(timestamp).cmd/hermes-auditd/main.go: removed the misleading “HTTP/WebSocket server not implemented — notify channel discarded” warning; the notify channel is now drained intentionally (data is read back viahermes-top).watcher.go: addedClose()method to fix fsnotify file descriptor leakflake.nix: fixedmkHermesAgentimport (was attrset, now unwrapped function)watcher.go: fixedFlushAlltimer race withentry.timer = nilguard afterStop()- CI: added
go teststep to GitHub Actions workflow installer.sh: removed deadspinner()functionstore.go: removed unuseddonechannel- Docs: fixed nftables attribution (
hardening.nix→networking.nix)
0.1.0 — 2026-06-19
Section titled “0.1.0 — 2026-06-19”- Initial release: NixOS flake template for multi-agent Hermes orchestration
lib/mkHermesAgent.nix— declarative Docker-based Hermes agent creationlib/constants.nix— template-level defaults (stateVersion, locale, hostname)modules/— reusable NixOS modules (boot, locale, networking, users, hardening, tailscale, etc.)pkgs/hermes-auditd/— Go daemon for filesystem event auditing with SQLiteinstaller/— interactive TUI installer ISO (nix build .#installer-iso)docs/— quickstart guide, agent tips, skill index, and 4 bundled Hermes skills- GitHub Actions CI:
nix flake checkon PR and push to main