Skip to content

fix(module): route writes through Runner, align Check with Apply, keep tests off the host #93

Closed

Description

@entelecheia

Context

Audit of the modules on 2026-11-09 against the AGENTS.md rules "file writes go through rc.Runner", "Check and Apply must agree" and "tests must not touch the host".

  • verified: ensureMetaDir (internal/module/module.go:240-262) migrates a pre-v0.1 single-file layout with os.Rename/os.MkdirAll and runs during --dry-run. The layout dates from the initial commit; 18 releases have shipped since.
  • inferred (agent read, re-check on implementation): saveGPUDB (gpu.go:782-786) writes with os.WriteFile + os.Rename (no dry-run, no backup); access.go:92 and doctor.go:129 os.MkdirAll("/run/sshd") inside read-only paths; users.go:1123 writes the users DB non-atomically.
  • inferred: security.go:241-251 reports Changed for time sync even when both commands only warned; Ubuntu Check hides a timedatectl error (security.go:157); several Change.Command strings hard-code apt-get install on paths that also run on Rocky (docker.go:43, nvidia.go:33, security.go:146, monitoring_exporters.go:49, network.go:46); locale Check shows update-locale while Apply writes the file.
  • inferred: tests with a dry-run Runner still execute real dpkg-query, ufw status, sshd -t, systemctl and read /etc (Runner.Query is not gated): ssh_test.go:62, docker_test.go:19,31, nvidia tests, GPU/storage tests via newDryRunRC. Most assert only non-nil.

Implementation starts only on the owner's go-ahead.

Decisions

  • Remove the legacy .rootfiles single-file migration; a file at that path becomes an error that says how to move it.
  • JSON databases (users, GPU) share one atomic read/write helper built on rc.Runner (same pattern as tenant writeIfChanged).
  • Module tests stub commands with fakeBin and path variables; a test that cannot be made host-free is deleted.
  • 2026-11-09: re-verified by the implementer against the fix(firewall): block SSH and UFW changes when firewall state is unknown #89 head. saveGPUDB already skips writes in dry-run; the gap is that its real write/rename bypasses Runner backups. saveUserMeta already uses Runner.WriteFile but is not atomic. The ensureMetaDir migration, the /run/sshd mkdir in access/doctor, and the security time-sync Changed/error handling are confirmed. Basis: implementer source check.

Design

  • internal/module/jsondb.go: readJSONDB(path, v) (found bool, err), writeJSONDB(rc, path, v) via Runner.WriteFile(tmp) + Runner.Rename. Missing file means empty DB for both callers.
  • Check-only paths stop creating /run/sshd; run sshd -t with a temp -o PidFile/privsep check or report "cannot validate" instead.
  • Change.Command uses packageInstallCommand.
  • A test guard: Runner.Query in tests fails when the binary resolves outside the fakeBin dir (opt-in helper used by newDryRunRC).

Changes

  • internal/module/{module,gpu,users,access,doctor,security,docker,nvidia,monitoring_exporters,network,locale}.go, new jsondb.go, the listed tests, internal/exec/runner.go (test hook only).

Acceptance criteria

  • AC1: apply --dry-run makes no filesystem change (test with a temp home base containing the legacy file: still a file afterwards, error message names it).
  • AC2: no os.WriteFile/os.Rename/os.MkdirAll/os.Remove in internal/module outside tests (grep check in a test or CI step).
  • AC3: Check output equals the commands Apply runs for docker/nvidia/security/network/locale on Rocky and Ubuntu stubs.
  • AC4: go test ./internal/module/... passes on a machine without ufw, dpkg-query, sshd, systemctl (CI macOS or a stripped container) with no host reads under /etc.

Tasks

  • T1: remove legacy migration (AC1)
  • T2: jsondb.go, switch GPU/users DB (AC2)
  • T3: read-only paths stop writing (AC2)
  • T4: Check/Apply command agreement, Changed accuracy (AC3)
  • T5: host-free tests (AC4)

Out of scope

  • Extracting an exec.Runner interface (decided against in the 2026-09 improvement plan; fakeBin stays).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingtestingTesting

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions