# Changelog

> Review historical and current user-facing changes across tentaflake releases.

Canonical page: https://docs.tentaflake.dev/reference/changelog/

> Source: [`CHANGELOG.md`](https://github.com/timfewi/tentaflake/blob/e21be475a3afc6e95c5087003edd0b7f780e4df4/CHANGELOG.md) at `e21be475a3af` · docs version **0.4.37**

All notable changes to tentaflake are documented here.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Fixed
- Preserve completed worker results without redispatching stale requests, and
  retain complete inbox copies when legacy private pending state is damaged.
- Identify the exact quota loop backing image inside systemd namespaces;
  `ReadWritePaths` bind mounts must not bypass underlying-data checks or prove
  quota ownership readiness.
- Reject quota mount-source and parent symlinks before privileged permission
  changes; generated-helper regressions preserve unrelated data and permissions.
- Preserve optional bounded `x-opencode-session` conversation-affinity metadata
  on both LLM broker routes for JSON and SSE. Reject malformed values before
  budget reservation or upstream dispatch, omit session IDs from audit logs,
  and keep arbitrary client headers excluded. Clients still own ID propagation.
### Added
- Durable worker pending/running claims, conservative interrupted-job recovery,
  per-agent operator/worker exclusion and aggregate inbox/private queue ceilings.
- Optional fixed-size private agent state through the existing Btrfs quota
  module, with exact adapter path/ownership, mount ordering, startup dependencies
  and selected Restic state/workspace sources. Existing data needs explicit
  offline migration; no automatic conversion or activation is performed.
- Record the product roadmap for general-purpose AI employees, company workflows,
  business connectors and shared runtime configuration, with linked GitHub milestones
  and acceptance criteria. Existing issues are reused and missing scope is tracked
  separately. This documents planned work; website publication remains separate.
- Add a lazy agent adapter registry and `mkAgent`, preserving Hermes/ZeroClaw
  public arguments, container/unit identities, ownership and existing state.
  Explicit metadata drives CLI inventory; versioned generic JSON is additive
  to legacy arrays and rejects unknown fields/adapters and identity collisions.
- Investigate OpenClaw v2026.9.7 and add an explicitly stopped refusal scaffold.
  A reviewed image, broker transport, actual Research discovery and worker tool
  routing remain acceptance prerequisites; this is not operational support.
  Versioned source confirms built-in OpenAI streaming requests; the opt-in
  broker SSE transport still needs acceptance with that runtime. Default
  rejection probes retain the existing budget and credential boundaries.
### Changed
- Pin the published `tentaflake-research` revision 91ce540 (replacing e31fce2):
  independent client/service split, neutral provider defaults, streamed-HTML and
  crawl fixes, typed failure details, constrained package sources, formatted HTML
  text and selectable VPN readiness evidence (WireGuard handshake and peer pins,
  egress-path evidence, planned draining; default off). No fixture or example
  relied on an implicit search provider. The Research policy, module-evaluation and
  adapter checks, the Research integration VM, the flake check, the CI-selection
  checks and the generated installed flake passed; no host was activated and the
  rest of the lockfile is unchanged. Tracked as #110.
### Security
- Automatic-start admission checks the effective OCI setting, closing an
  override path that could start a stopped capsule without its required
  broker, worker, workspace quota and Research declarations.
- Research capability validation uses registered adapter metadata rather than
  trusting runtime-like OCI names. Upstream settings hooks share the existing
  exact read-only closure/socket projection; remote MCP remains rejected.
- Research-enabled controllers use only `secure-research-tool` for web access.
  Native web tools and legacy fetch are disabled, per-agent Unix relays preserve
  distinct job ownership, and model requests stay on the LLM broker. The broker
  rejects provider-hosted tools and web extensions before budget reservation.
- Container mounts reject non-normalized path components, including `..` escapes
  from an approved source or into a sensitive container destination.
- Selectively adopt the archive's exact Docker/Podman posture comparisons and
  bounded OCI subprocess capture: incomplete evidence remains unknown, while
  changed mounts, networks, ports, tmpfs, or resource limits report drift.
  Broker health probes run in bounded batches without serial endpoint delays.
  OCI inspection also caps combined stdout/stderr at 8 MiB and rejects
  overflow as unknown instead of retaining unbounded output until timeout.
- Broker services now have memory/task/file ceilings and optional aggregate
  admission limits. Dual-mode agents count twice for concurrency and rate.
  Credential setup uses only its private preserved runtime directory under
  strict filesystem protection, retaining keys across service restarts.
  VM verification additionally caught the nested runtime directory exposing
  its parent and empty capability lists omitting the directive; explicit
  read-only/writable paths and empty-string capability sets close both gaps.
- Update the broker's locked TLS dependency to `rustls 0.23.45` and its
  required `rustls-webpki 0.103.15`, removing the affected version reported by
  OSV for `RUSTSEC-2026-0285` without changing broker policy or adding code.
- Updated the pinned Dev Containers CLI dependencies `@humanfs/node`,
  `brace-expansion` (all three locked majors), `ip-address`, and `js-yaml` to
  remove current OSV findings. The required HumanFS dependencies and offline
  cache hash move together; the CLI release and prior security backports stay
  unchanged.
### Changed
- Shorten all seven bundled skills into focused, source-linked procedures and
  remove unverified upstream catalogs and credential-dumping instructions.
  Correct read-only Hermes configuration guidance and Agenix recipient/runtime
  separation, host identity setup and environment-file verification.
- Add opt-in bounded SSE forwarding on the existing LLM broker's Chat Completions
  and Responses routes. Streams retain model/tool allowlists and host-only keys,
  cancel on disconnect, enforce byte/time limits and never replay dispatched
  calls. Conservative budget reservations remain charged when usage is missing
  or an exchange aborts; terminal audit admission precedes successful output.
- Refresh documentation for adapter inventory, secure startup prerequisites,
  lifecycle management, skills and credential ownership. Link the public
  website and document authoritative-source and release synchronization rules.
- Link the retained project artwork from the README and an asset overview,
  making logo and wallpaper variants discoverable after removing the old
  ASCII assets.
- Installer and VM fixture import the same disk library, replacing fragile
  extraction of executable code by fixed comment strings.
- GitHub selects host/research VMs separately from an imported path policy;
  contributor-only changes skip them, while unknown/shared changes run both.
- Rust packages include only their own source plus workspace manifests, so
  changes to one binary preserve the other binaries' Nix cache entries.
- Cargo workspace metadata supplies the package/image version and package
  descriptions. Worker image references follow image metadata; shared agent
  UID/GID defaults now import `lib/constants.nix`.
- New installations and fixed-size agent workspaces use Btrfs instead of ext4.
  Workspace images require at least 128 MiB and use read-only offline checks.
- Rust CLI, broker, and worker packages share one builder and a Cargo/crate
  source fileset, removing repeated packaging policy and keeping unrelated
  documentation and host-configuration edits out of their rebuild inputs.
- Secure controllers and brokers now share systemd exponential restart
  backoff from 10 seconds to one minute, avoiding permanent start-limit
  exhaustion after a transient outage. Explicit stops still prevent retries.
### Added
- `just fast` runs the contributor checks, CI-selection regressions and flake
  evaluation without VM suites or the installer ISO.
- Pinned public MIT `tentaflake-research` service, `tentaflake.research.agents`,
  policy checks and a real gVisor stdio-MCP integration fixture. Secure auto-start
  now requires a research relay alongside the broker, worker and workspace quota.
### Fixed
- Keep Tailscale's up-only `--advertise-tags` out of `extraSetFlags`, allowing
  hostname/SSH preferences to apply after manual enrollment. Auth-key enrollment
  retains all three flags through `extraUpFlags`; manual enrollment requests the
  tag explicitly. Regression checks parse the generated flags with the pinned CLI.
- Pin the Dev Containers CLI's transitive `basic-ftp` dependency to 6.2.1
  for `GHSA-c475-qrg2-pj4r`, with the patched Yarn resolution and offline-cache
  hash updated together. The CLI release stays at 0.88.0.
- Installer disk cleanup no longer stops swap or LVM globally. It follows the
  selected device stack, preserves unrelated volumes and refuses cross-disk VGs
  before mutation; encrypted target mappings are identified by device type.
- Pin the verified research client recovery: new MCP calls reconnect after an
  upstream session loss, without replaying interrupted operations.
- `just` recipes load the pinned contributor tools from an ordinary shell,
  avoiding missing Cargo/Statix errors. `just list` now lists available recipes.
- Generated installed flakes now pin and import the research module explicitly,
  avoiding a missing module argument and infinite recursion during installation.
- Broker crash recovery preserves dependent controller processes with systemd
  direct retries. Six consecutive crashes no longer interrupt the controller;
  explicit broker stops still stop it and require a separate controller start.
- Host diagnostics no longer report success when systemd/disk queries fail
  or agent state is unknown. `health` shares `doctor` checks and exit codes;
  intentionally stopped agents remain valid. Text `doctor --hide` now
  redacts host and agent names as advertised, using shared name rendering
  and a single agent-state snapshot instead of duplicated report logic.
- CLI help (`help`, `--help`, `-h`) now works before host configuration is
  installed or while generated inputs are damaged, keeping usage available
  during setup and recovery. Management commands still require those inputs.
- Restic backups now include enabled quota workspaces inside selected state
  paths as separate sources instead of silently skipping their files under
  `--one-file-system`. Required mounts and mount assertions prevent an
  unavailable workspace from producing a misleading successful backup.
- Corrected the Dev Containers CLI offline dependency-cache hash for its
  committed patched lockfile, restoring the package build and full flake
  gate without changing source or dependency versions.
  CI now builds that non-VM check so a stale cache pin cannot pass evaluation
  without exercising the package build.
### Breaking
- Existing ext4 workspace images require a verified backup/restore migration
  before agents can start with the new Btrfs quota module; no automatic conversion.
- Remove the optional Editor, Hive Research, and Piper TTS modules, their flake
  exports, and Piper voice assets. Consumers must remove these imports/options
  and own any replacement integration in their flake.
- Secure controller resource validation now rejects memory/swap above 1 TiB,
  tmpfs above 64 GiB, CPU above 1024, or CPU precision beyond five fractional
  digits, keeping generated limits representable by both OCI backends.

## [0.4.0] - 2026-08-23

### Changed
- GitHub Actions now skips the expensive VM integration build for
  documentation-only changes while retaining the visible `vm-test` job for
  branch-protection compatibility. Every non-Markdown path remains
  conservatively VM-relevant.

### Added
- `just security` now 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`.

### Fixed
- Contributor E2E now uses a source- and dependency-hash-pinned Dev Containers
  CLI 0.88.0 with the upstream `proxy-from-env` WHATWG URL parsing change
  backported for its CommonJS consumer, removing the legacy `url.parse()` path
  that produced Node.js `DEP0169` security 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 `nixfmt` package instead of its
  deprecated `nixfmt-rfc-style` alias, removing the flake evaluation warning.
- The local `just ci` gate 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 `null` user to a string matcher
  and failing evaluation in the `dev` compatibility 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.

## [0.4.0-dev] - 2021-08-21
### 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-auditd` package, 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
  select `dev`; `strict` fails 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.egress` is renamed to
  `tentaflake.networking.legacyPortEgress` and is dev-only. It was only a host
  OUTPUT port filter, not a destination allowlist or container FORWARD policy.

### Added
- `just security` now 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-020` makes 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 `ENOSPC` coverage, migration documentation, and posture finding
  `TFSEC-021`.
- Explicit Docker `docker-default` AppArmor selection for secure controllers
  and workers, unconfined Seccomp/AppArmor override rejection, posture findings
  `TFSEC-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-024` when 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
  `runsc` execution, 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, `/usr` immutability, 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.

### Fixed
- Contributor E2E now uses a source- and dependency-hash-pinned Dev Containers
  CLI 0.88.0 with the upstream `proxy-from-env` WHATWG URL parsing change
  backported for its CommonJS consumer, removing the legacy `url.parse()` path
  that produced Node.js `DEP0169` security 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 `nixfmt` package instead of its
  deprecated `nixfmt-rfc-style` alias, removing the flake evaluation warning.
- The local `just ci` gate 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 `null` user to a string matcher
  and failing evaluation in the `dev` compatibility 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 `StateDirectory` instead of requiring a nonexistent
  `ReadWritePaths` target before its script could create it. This prevents a
  `226/NAMESPACE` failure 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
  (`local` for Docker and `k8s-file` for Podman) instead of sending file
  rotation options to a `journald` default 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+found` readable
  without adding `CAP_CHOWN` to 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 root `approve` and `deny` commands 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 `cp` view 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 `runsc` sandbox 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 `LoadCredential` files used with `DynamicUser`. 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 `openat2` as the primary no-symlink workspace
  opener and use a descriptor-relative, component-by-component `openat`
  fallback only when the kernel reports `ENOSYS`. 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=simple` process start as socket readiness.
- Quota-backed workspaces now mount after ordinary local filesystems without a
  `local-fs.target`/`basic.target` ordering 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 stop` transaction 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 `runsc` selection without a silent runc fallback.
- `tentaflake doctor --security` with stable machine-readable `TFSEC-*`
  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-host` tag.
- Exact canonical GitHub remote and branch allowlists for host-side
  `gitAutoPush`, repository-scoped `safe.directory`, fail-closed errors, and
  malicious-URL regression tests.

### Previous development notes
- `tentaflake status` now 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 `/boot` and `/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 `--hide` view keeps this non-identifying capacity telemetry, and `tentaflake-status --selftest` covers root, mounted data, unmounted, and excluded-device cases with synthetic commands.
- `tentaflake health` — host vitals dashboard, the machine-level counterpart to `stats`. Same renderer again (logo, header, bars, colour thresholds), with a **VITALS** section — 24-cell bars for CPU busy (a 0.4 s `/proc/stat` delta, 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` / `✗ critical` verdict, the worst finding on the same thresholds the bars are painted with. `--live` (default 2 s, `--live=5` to slow it) redraws in place on the alternate screen, so Ctrl-C leaves the scrollback untouched and colours survive — unlike `watch`. It always exits 0: `tentaflake doctor` remains the scriptable check with the nonzero exit. `--hide` applies here too and prints a *count* of failed units instead of their names (a unit name carries its container's); `tentaflake-status --selftest` now renders the masked health view alongside the masked wide view and fails if either leaks. `./scripts/banner-test.sh --health` previews 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 the `docker stats` scale (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 number `docker stats` reports) instead of `docker 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` / `-H` on `status` and `stats`: masks host name, tailnet IP and agent names (`agent-1`…`agent-N`, the failed-agents hint included) for screenshot-safe output, with a yellow `· redacted` header marker. Telemetry stays. `tentaflake-status --selftest` renders 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; see `DCO.txt` and `CONTRIBUTING.md`.
- `nix develop` now greets you: the dev shell prints the tentaflake logo with branch/HEAD/working-tree/Nix-version rows and a short `just` cheat sheet, and sets a `(tentaflake)` prompt marker. Same braille art and layout as the host login banner — both read `public/tentaflake-shell-logo.txt`. The banner is skipped when stdout is not a TTY (`nix develop --command …` in CI stays clean) and `TENTAFLAKE_NO_BANNER=1` disables it entirely. The dev shell moved from an inline `pkgs.mkShell` in `flake.nix` to `lib/devshell.nix`; the package set is unchanged, and the banner script is built with `writeShellApplication`, so `nix flake check` shellchecks it.

## [0.3.1] — 2026-07-19

### ⚠ 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 written `nixosConfigurations.${hostName}`, so the two cannot drift), which means **`nix build .#nixosConfigurations.agent-host` and `nixos-rebuild --flake .#agent-host` no longer resolve** — use `.#tentaflake`. Same for the check attr `checks.<system>.agent-host` → `checks.<system>.tentaflake`. Installed machines are unaffected: the installer generates `nixosConfigurations.${uc.hostName}` from the hostname you typed, so `tentaflake rebuild` keeps 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 ISO `hermes-env-detect`/`hermes-data-mount` → `tentaflake-env-detect`/`tentaflake-data-mount`. The old unit names are **gone** — update `systemctl`/`journalctl` scripts. (The console unit was already `tentaflake-console`.)
  - **module options**: `tentaflake.hermes-auditd.*` → `tentaflake.auditd.*`. Old names still evaluate with a deprecation warning via `lib.mkRenamedOptionModule` and 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 top` now execs the new name). The package installs a deprecated `hermes-top` symlink for one release.
  - **USB labels** (live ISO unattended boot; the interactive `tentaflake agent add` needs no label at all): `TENTAFLAKE_ENV`/`TENTAFLAKE_DATA` are the primary labels; the legacy `HERMES_ENV`/`HERMES_DATA` labels are still accepted, so existing sticks keep working.
  - **live-ISO env dir**: `/run/hermes` → `/run/tentaflake`, with a `/run/hermes` compat symlink for configs that hardcode the old `envFile` path.
  - **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 default `dbPath` `/var/lib/hermes-audit/events.db`, and the `hermes-audit` user/group. These rename in a future major with a migration.
  - Also: the `hermes-<name>`/`zeroclaw-<name>` container/user/state-dir prefixes, the `HERMES_*` env contract, and `mkHermesAgent` are unchanged — they name the Hermes runtime, not the infrastructure.
- Digest pinning is **enforced**, not merely defaulted (`lib/pinnedImage.nix`): a previously-valid `image = "repo:tag"` is now a hard eval `throw` in **all three** builders (`mkHermesAgent`, `mkZeroClawAgent`, `mkOpenCodeAgent`). Any fork or consumer flake that overrides `image` with a tag **fails `nixos-rebuild` outright**, before a single derivation is built. Repin to `repo@sha256:<digest>` (the error message prints the `skopeo inspect` command that reports it), or set `allowMutableImage = true` on that agent to acknowledge giving up reproducibility. Note that `repo: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`, default `true`). 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-only `modules/ssh.nix` with `tentaflake.adminAuthorizedKeys`. See Changed below.
- The host Nix daemon is locked down (`modules/nix-settings.nix`): `allowed-users` is now `root` and `@wheel` **only**, so any non-wheel user or service account that used to run `nix build`/`nix-store` against the host daemon is refused outright; and `sandbox-fallback = false` turns a build that cannot be sandboxed into a hard failure instead of a silent unsandboxed one. See Changed below.

### Security
- `tentaflake-auditd` hardening (#8): the audit DB now sets `secure_delete` and 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. (No `fs.inotify.max_user_watches` sysctl 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`, `mkZeroClawAgent` and `mkOpenCodeAgent` reject an unpinned `image` at eval time, so a fork cannot silently switch back to a mutable tag; the check runs *after* the `extraContainerConfig` merge, which could otherwise override `image` past it. Locally-built images opt out per-agent with `allowMutableImage = true`. Covered by the new `image-pinning` flake check.
- Image references are also character-whitelisted, **including** under `allowMutableImage`. nixpkgs' `oci-containers` module splices `container.image` into the container unit's shell command without `escapeShellArg` (unlike every neighbouring element), so a reference carrying whitespace or a shell metacharacter could inject extra `docker run` flags — or a command substitution running as root — behind a digest-shaped suffix.
- Pinned references are written `registry/repository@sha256:digest`, never `repository: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 every `tentaflake.containerBackend = "podman"` host.
- CI workflow hardening (`.github/workflows/check.yml`): a top-level `permissions: contents: read`, and all five third-party actions pinned to full 40-character commit SHAs with a trailing version comment. `GITHUB_TOKEN` is otherwise write-capable on pushes while every step in this workflow only reads the repo, and a tag like `@v4` is re-pointable by whoever owns the action — an unpinned action is arbitrary code execution in a job that holds the repo token. `golangci-lint-action` moves 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.yaml` is the v2 format, so pinning v6 would have failed outright. The linter itself is pinned to `v2.12.2` instead of `latest`, so an upstream lint release can no longer break CI on an unrelated PR. (v9 additionally validates `.golangci.yaml` against its JSONSchema, which v6 skipped — worth watching on the first run.)

### Added
- `mkOpenCodeAgent` — a third agent runtime builder ([OpenCode](https://opencode.ai)) alongside `mkHermesAgent`/`mkZeroClawAgent`. Runs `opencode serve` (headless HTTP, documented OpenAPI 3.1) in the same isolation contract (own container/uid/`0700` state dir + `/workspace`, secrets via `--env-file`), forwarding a loopback `hostPort` to the gateway with an optional `tailscale serve` on `servePort`. Ideal for external orchestrators (n8n, CI): `POST /session` then `POST /session/<id>/message`. It joins the other two runtimes' safety contract: the image defaults to a digest pin in `lib/constants.nix` and goes through the same enforced `pinnedImage` check (mutable tags are an eval error; `allowMutableImage = true` opts a locally-built image out). Setting `servePort` now **requires** an `envFile` or `agenixFile` — the container is started with `--hostname 0.0.0.0` so the gateway is reachable across the container boundary, and `opencode serve` only turns on HTTP basic auth when `OPENCODE_SERVER_PASSWORD` is set (username defaults to `opencode`), so publishing without one put an unauthenticated agent on the tailnet; the assertion checks the env file is wired up, and `allowUnauthenticatedServe = true` is the explicit acknowledgement. Because Nix cannot read a runtime/agenix file at eval time, the serve unit re-checks at start: an `ExecStartPre` greps the env file(s) for a non-empty `OPENCODE_SERVER_PASSWORD` and 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 runs `tailscale serve --https=<servePort> off` on that path, because `tailscale serve --bg` state 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 in `envFile`/`agenixFile` and **not** `extraEnvironment` — that attrset is rendered into the container unit's start script in the world-readable Nix store. The `tailscale serve` unit also gained an `ExecStop` (`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 (`settings` base_url + env-file key, host-portable) or an opt-in read-only `authFile`. Exported from `lib.<system>.mkOpenCodeAgent` and passed to `my-agents.nix` (older files taking only the previous builders keep working via `intersectAttrs`). Wired into the same multi-runtime plumbing as the other runtimes: the `tentaflake` CLI/status banner discovery, the `tentaflake-auditd` state-dir watcher (Go `agentNameFromPath` + NixOS auto-discovery), and the installer's env-placeholder/USB-persistence loops all recognize the `opencode-` prefix; the VM integration test now declares an OpenCode agent and asserts its unit + `0700` state dir. See `docs/08-opencode.md` and `my-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 in `tentaflake-status` and `btop`'s graphs rendered as boxes — no `console.font` could 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 via `tentaflake.modernConsole.fontSize` (default 14). The keymap follows `tentaflake.consoleKeyMap` through xkb, since kmscon replaces getty. Disable it on hardware where kmscon cannot grab the framebuffer — the legacy VT then uses the new `tentaflake.consoleFont` (Terminus `ter-v16n` by default, still far better than the kernel font for box drawing). `consoleFont` applies *only* when kmscon is off, and `null` skips the boot-time `setfont` entirely; both ISOs use `null`, because their `dialog` TUIs 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-launches `installer.sh` and the live ISO's firstboot wizard (the latter also writes straight to `/dev/tty1`). See `docs/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-tracked `agents.json` (schema v1: name/provider/model/base_url/ports/envFile path) read by `lib/agentsFromData.nix`, additive to a hand-written `my-agents.nix`. API keys are read with `read -rs` (or imported from a USB stick, see below) and written only to a root-owned `0600` file at `/var/lib/tentaflake/secrets/<runtime>-<name>.env` — never into `agents.json` or the Nix store. See `docs/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 itself `ro,nosuid,nodev,noexec`; it looks two directories deep for `.env` files carrying the provider's `KEY=` line and for `.txt`/`.key` files holding a bare token. **No filesystem label and no filename convention are required** — the `TENTAFLAKE_ENV` label 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, **never `source`d, `eval`d 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 (DECSTBM `CSI top;bottom r`), no curses and no new dependency; `tput` is unusable here because the installed console is kmscon and `TERM=kmscon` has no terminfo entry, so geometry comes from `stty size`. Restored via an `EXIT`/`INT`/`TERM`/`HUP` trap. Degrades to plain sequential output when not on a tty, under `NO_COLOR`, or on `TERM=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=dumb` conditions, independent of window size, so all `tentaflake` output is clean when piped.
- NixOS VM integration test (`checks.x86_64-linux.vm-integration`, `tests/integration.nix`): boots a VM built from `nixosModules.default` and asserts the runtime path — the `tentaflake` CLI runs (`help`, `ps`), the status banner renders and names the host, `tentaflake-auditd` comes up and creates its SQLite event DB, and each declared agent produces its systemd unit and `0700` state dir (the Hermes agent also its system user; the OpenCode agent runs as an anonymous uid, so it has none, and gets its `/workspace` checked instead). The daemon subtest waits for `/var/lib/hermes-audit/events.db` rather than a port, because `tentaflake-auditd` **opens no socket at all** — it writes events to SQLite and the separate `tentaflake-console` service is the HTTP surface — and `Type=simple` marks the unit active before the binary has run, so `wait_for_unit` alone would race the DB creation. Agents are declared `autoStart = false` so 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 own `hostName`/`adminUser` (`agent-host`/`admin`) instead of inheriting the template defaults, so the banner assertion is unaffected by the `agent-host` → `tentaflake` rename above; it also sets `nixSettings.enable = false`, since that module defines `nixpkgs.config.allowUnfree` and the test harness declares `nixpkgs.config` read-only. Runs as part of `nix flake check` and CI. Previously CI only proved the config *built*; nothing exercised runtime behavior.
- CI now runs `golangci-lint` on `pkgs/tentaflake-auditd` (`.github/workflows/check.yml`), enforcing the lint standard the contributor docs already required.
- `tentaflake` CLI host-management subcommands (#51): `rebuild` (nixos-rebuild switch on the system flake — same command as the `rebuild` alias), `update` (`nix flake update` on `/etc/nixos`, shows the `flake.lock` diff, asks y/N, then rebuilds), `doctor` (deep health check — failed systemd units, root disk ≥90%, Tailscale, `tentaflake-auditd`/`tentaflake-console` service state when enabled, per-agent unit state — every problem paired with its exact fix command; nonzero exit when problems found), `console` (Agent Console URL + the `tailscale serve` publish one-liner, or how to enable the console when it's off), and `backup <name>` (one-shot `sudo tar` snapshot 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 custom `stateDir`s are honored).
- Opt-in egress filtering (`tentaflake.networking.egress.enable`, **default off**): an nftables output chain (table `tentaflake-egress`, policy drop) allowing loopback, established/related, ICMP/ICMPv6, and configurable outbound ports (`allowedTCPPorts` default `[443]`, `allowedUDPPorts` default `[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](https://github.com/timfewi/tentaflake/blob/v0.3.1/docs/07-operations.md#egress-filtering-opt-in))
- `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 from `tentaflake.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 weekly `flake.lock` update 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, alongside `tentaflake-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` (also `just banner`) — renders the `tentaflake-status` banner with a stubbed `systemctl` and 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 hidden `tentaflake agent __selftest` (env/bare key parsing including a "must not execute" fixture, masking, header geometry and degradation, scroll-region teardown).

### Changed
- Live agent ISO: the debug sshd no longer accepts password auth (`PasswordAuthentication = false`) — Tailscale SSH covers remote debugging on the live ISO.
- `pidsLimit` parameter on `mkHermesAgent` and `mkZeroClawAgent` — renders `--pids-limit=<n>` on the agent container (default `512`, generous because agents compile code; `null` skips the flag). Hermes containers additionally gain `--security-opt=no-new-privileges:true` by default (ZeroClaw containers already had it). Opt-in `--cap-drop=ALL` recipe and the docker-group-is-root-equivalent tradeoff (podman as the rootless alternative) documented in `docs/02-agent-tips.md` and `docs/07-operations.md`. (#3)
- Kernel hardening (#4): `hardening.nix` gains 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 via `security.lsm` (`landlock,yama,apparmor,bpf`); `boot.nix` disables the systemd-boot menu editor (blocks `init=/bin/sh`).
- Git hygiene: `.gitignore` now covers `age.key` and `*.log`; `CONTRIBUTING.md` documents 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 the `tentaflake-auditd` audit trail (UTC timestamps), preserve the state dir, rotate secrets and rebuild. (#11)
- `docs/07-operations.md`: **Backup & restore** (state dirs, audit DB via `sqlite3 .backup` — never `cp` a live WAL db — off-host age identity, restic example for forks) and **Log forwarding** (`services.journald.upload` snippet) sections. (#11)
- `modules/piper-tts-server.nix`: systemd unit hardened to parity with `tentaflake-auditd`/`hive-research` (`ProtectSystem=strict`, `ProtectHome`, `PrivateDevices`, `RestrictNamespaces`, `LockPersonality`, empty capability bounding set, `RestrictAddressFamilies`, `SystemCallFilter=@system-service`) plus resource limits: `TasksMax=64` and a new `services.piper-tts-server.memoryMax` option (default `2G` — raise for larger voice models). (#12)
- `modules/nix-settings.nix`: Nix daemon hardening — `allowed-users` restricted to `root` and `@wheel` (agents live in containers and never talk to the host daemon), explicit `sandbox = true` with `sandbox-fallback = false` (no silent downgrade to unsandboxed builds), and `min-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-*` and `zeroclaw-*`, previously `hermes-*` 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.yaml` migrated 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-status` login banner redesigned: braille-art octopus-snowflake logo in cyan (embedded at build time from `public/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; `AGENTS` header now cyan with a fleet count (`total · active · inactive`, plus `failed` in 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.

### Fixed
- Installer: adding the **first agent** on an installed machine broke `tentaflake rebuild` with `error: infinite recursion encountered`. The flake `installer.sh` generates for the target passed only `mkHermesAgent` and `constants` in `specialArgs`, but `configuration.nix` also takes `mkZeroClawAgent` and `agentsFromData` — and consumes them inside `imports`. A missing helper there does not fail loudly: Nix falls back to `config._module.args`, which needs `config`, which needs `imports`, so the rebuild dies in recursion instead of naming the absent argument. It stayed hidden on a fresh install because `lib.optionals (builtins.pathExists ./agents.json)` leaves `agentsFromData` unforced until an `agents.json` exists — so the very first `tentaflake agent add` triggered it. The generated flake now splats the whole `lib/default.nix` helper set, so it cannot drift from what `configuration.nix` asks for again. Covered by the new `scripts/generated-flake-test.sh`. **Machines already installed need a one-line manual patch** — see [docs/08-agent-cli.md](https://github.com/timfewi/tentaflake/blob/v0.3.1/docs/08-agent-cli.md#infinite-recursion-encountered-on-the-first-rebuild-after-agent-add).
- Installer: `nixos-install` failed evaluating the target flake with a `readFile` error — the redesigned `tentaflake-status` banner reads `public/tentaflake-shell-logo.txt` from `modules/shell.nix`, but the installer never copied `public/` into `/mnt/etc/nixos`. The installer now copies the logo file (just that file, not the multi-MB imagery in `public/`).
- USB data persistence (`tentaflake-data-mount`) and the env-file placeholder loop (`tentaflake-env-detect`) never ran at all. Neither unit declared a `path`, so both got only the default service PATH (coreutils, findutils, gnugrep, gnused, systemd) — no `blkid`, `mount`/`umount` or `awk`. `blkid` exiting 127 was swallowed by the surrounding `|| true`, leaving `USB_DEV` empty and tripping the `[ -z "$USB_DEV" ] && exit 0` guard on every boot, so a `TENTAFLAKE_DATA` stick was silently ignored and agents never got their placeholder `.env` files (which is what made `docker run --env-file` fail on a cold boot). Both units now declare `path = [ pkgs.util-linux pkgs.gawk ]`.
- USB data persistence could **destroy agent state** (`installer/firstboot.nix`). `tentaflake-data-mount` used to `mkdir -p` the target on the USB stick, then `rm -rf` the 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-owned `0755`, so an agent running as its own uid could no longer write its own `0700` state 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 the `rm -rf` ran regardless, so the agent's state was simply gone. Persistence now goes through a `stage_to_usb()` helper that mirrors the original uid/gid/mode onto the USB directory and re-`stat`s 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 hand` and 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 the `rm -rf` succeeded but the symlink did not, the state is on the stick, and the message is `ERROR: … removed but symlink … failed — state is on the USB, relink by hand`. The helper's exit status is captured rather than left to `errexit`, 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 by `scripts/data-mount-test.sh`.
- `modules/piper-tts-server.nix`: every `POST /v1/audio/speech` returned `500 piper binary not found` while `/health` and `/v1/models` kept answering, so the service looked healthy and only synthesis was dead. The server shells out to `piper` via `subprocess.run`, but `/run/current-system/sw/bin` — where `environment.systemPackages` lands — is not on a systemd unit's default PATH. The unit now declares `path = [ 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.
- `tentaflake` re-enabled terminal echo on exit even when it had never disabled it. `tf_cleanup` runs from an `EXIT` trap on every invocation — including `tentaflake help` — and called `stty echo` unconditionally, corrupting any caller on a `-echo` terminal (serial console, scripted pty, the NixOS test driver's backdoor console, where it desynced the driver outright). Echo is now restored only when a masked `read -rs` actually turned it off.

## [0.2.0] — 2026-07-11

### ⚠ Breaking
- The host operator CLI is now **`tentaflake`**. The `hermes` host command remains in this release as a deprecated shim (prints a warning to stderr, execs `tentaflake`) and **will be removed in a future release** — update scripts and habits. (The `hermes` CLI *inside* Hermes agent containers is unrelated and unchanged.)
- `tentaflake.shell.hermesCli.enable` → `tentaflake.shell.tentaflakeCli.enable`. The old option still evaluates via `lib.mkRenamedOptionModule` with a deprecation warning and will be removed together with the shim.
- `my-agents.nix` is now expected to accept `{ mkHermesAgent, mkZeroClawAgent }`. Inside this template, `configuration.nix` only passes the arguments your file declares, so old `{ mkHermesAgent }:`-only files keep evaluating — but flake-input consumers that call `import ./my-agents.nix { inherit mkHermesAgent mkZeroClawAgent; }` directly must update old files to the new signature (or add `...`).
- `tentaflake ps` behaves differently from the old `hermes 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 top` label non-Hermes agents with a runtime prefix (`zeroclaw-<name>`). Hermes agents keep their bare labels, so existing audit DBs and dashboards are unaffected.

### Added
- `lib/mkZeroClawAgent.nix` — second agent runtime alongside Hermes: OCI container (`ghcr.io/zeroclaw-labs/zeroclaw`), TOML `settings` attrset generated into a read-only `config.toml`, a `tailscale serve` unit for tailnet HTTPS access, and an optional `seedDir` copied into the state dir on first boot. State dir `/var/lib/zeroclaw-<name>`, secrets via `agenixFile` (`--env-file`).
- `zeroclaw.env.example` — mirrors `hermes.env.example`; documents the `ZEROCLAW_<section>__<sub>__<key>` env-var convention ZeroClaw uses for config overrides, with an OpenRouter `api_key` example.
- `my-agents.nix.example` now takes `{ mkHermesAgent, mkZeroClawAgent }` and defines both a `hermesAgents` list and a `zeroclawAgents` list (`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 (now `hermesAgents`, see above) with a fully-commented generic reference agent documenting every `settings` option (model + fallbacks, auxiliary routing, compression, memory, toolsets, approvals, web/terminal backends, provider timeouts, TTS/STT) plus seed/volume/env/container fields. Active agent renamed `default` → `coding` to 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. Plus `tentaflake.shell.zoxide.enable` (smart-cd, cross-shell, default on) and `tentaflake.shell.lazygit.enable` (+ `lg` alias). Aliases moved to `environment.shellAliases` so they apply to bash and zsh.
- `modules/shell.nix`: **tmux** (`tentaflake.shell.tmux.enable`, configured). Convenience aliases `rebuild` (→ `nixos-rebuild switch --flake /etc/nixos#<host>`), `reload` (`exec $SHELL`) and `cls`. tmux moved out of the `tools` package 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 the `nvf` flake input (kept out of `nixosModules.default` so 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.nix` as `tentaflake.*` toggles (nvf also injects the rev-pinned input + editor module import).
- `hermes-top` — live TUI dashboard (bubbletea) of agent filesystem activity, launched via `tentaflake top`. Reads the `hermes-auditd` SQLite DB directly (no network surface — runs over Tailscale SSH). Implements the read side the daemon previously discarded (the `internal/store` already had the query methods).
- `hermes-auditd` is now wired up: enabled by default on `agent-host`, `watchDirs` auto-derives from the agents defined in `my-agents.nix`, runs as an unprivileged `hermes-audit` user with only `CAP_DAC_READ_SEARCH`, and stores its DB group-readable so the admin can run `tentaflake top` without sudo.
- `internal/store`: `Since(afterID, limit)` (incremental tail) and `AgentRows(window)` (per-agent activity summary) read helpers.
- `modules/shell.nix` — operator shell experience for SSH/console: the agent-management CLI (`tentaflake`, see Changed), dynamic `tentaflake-status` login banner, Starship/bash prompt, completion, and a curated modern CLI tool set. Toggle via `tentaflake.shell.*`. See [`docs/06-shell.md`](https://docs.tentaflake.dev/operations/cli-shell/).
- `modules/piper-tts-server.nix` — local TTS via Piper (OpenAI-compatible `/v1/audio/speech`)
- `modules/hermes-firstboot.nix` — USB env detection + first-boot TUI wizard
- `nixosConfigurations.live-agent` + `nix build .#live-agent-iso` — bootable ISO with Hermes + TTS

### 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`/`ps` and the login banner list agents across all runtimes with a runtime column. A deprecated `hermes` shim still works: it prints a deprecation note to stderr and execs `tentaflake`.
- `tentaflake.shell.hermesCli.enable` renamed to `tentaflake.shell.tentaflakeCli.enable` (old name still accepted via `lib.mkRenamedOptionModule`).
- `modules/hermes-auditd.nix`: `watchDirs` auto-discovery widened from `hermes-*` 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-auditd` 0.1.3: event attribution (`agentNameFromPath`) and `hermes-top` path shortening now recognize `/var/lib/zeroclaw-<name>` state dirs — ZeroClaw activity is labelled `zeroclaw-<name>` instead of lumped into `unknown`; Hermes labels stay bare for existing DBs.
- `flake.nix` now exports `mkZeroClawAgent` alongside `mkHermesAgent` (`lib.${system}`, `specialArgs`); `configuration.nix` passes each builder to `my-agents.nix` only if the file's function asks for it (`lib.functionArgs` intersection), so old `{ mkHermesAgent }`-only files keep working unmodified.

### Fixed
- `store.go`: fixed event time-window comparisons (`Stats`, `Prune`) — stored RFC3339 timestamps were string-compared against SQLite's `datetime('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 via `datetime(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 via `hermes-top`).
- `watcher.go`: added `Close()` method to fix fsnotify file descriptor leak
- `flake.nix`: fixed `mkHermesAgent` import (was attrset, now unwrapped function)
- `watcher.go`: fixed `FlushAll` timer race with `entry.timer = nil` guard after `Stop()`
- CI: added `go test` step to GitHub Actions workflow
- `installer.sh`: removed dead `spinner()` function
- `store.go`: removed unused `done` channel
- Docs: fixed nftables attribution (`hardening.nix` → `networking.nix`)

## [0.1.0] — 2026-06-19

### Added
- Initial release: NixOS flake template for multi-agent Hermes orchestration
- `lib/mkHermesAgent.nix` — declarative Docker-based Hermes agent creation
- `lib/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 SQLite
- `installer/` — 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 check` on PR and push to main

[Unreleased]: https://github.com/timfewi/tentaflake/compare/v0.4.0...main
[0.4.0]: https://github.com/timfewi/tentaflake/compare/v0.3.9...v0.4.0
[0.3.1]: https://github.com/timfewi/tentaflake/compare/v0.2.0...v0.3.1
[0.2.0]: https://github.com/timfewi/tentaflake/compare/v0.1.0...v0.2.0
[0.1.0]: https://github.com/timfewi/tentaflake/releases/tag/v0.1.0
