From 69bc45bbf070fc7e03a887d72853abe93dab821e Mon Sep 17 00:00:00 2001 From: Coding Agent Date: Fri, 4 Sep 2026 23:03:22 +0000 Subject: [PATCH] feat: declare locked NixOS development host Declare dev, scoped sudo/SSH, workspace ownership, Git and Neovim. Lock Nixpkgs, Home Manager and the unmodified dotfiles. Deploy user files declaratively and preserve a writable Lazy lockfile. Nix formatting and pure flake evaluation pass. Full build, empty-home deployment tests and activation of this reproducibility correction are still pending; the previous account baseline is live. --- .editorconfig | 12 +++++++ .gitignore | 15 +++++++++ README.md | 76 +++++++++++++++++++++++++++++++++++++++++++++ configuration.nix | 26 ++++++++++++++++ dev-authorized-keys | 1 + flake.lock | 68 ++++++++++++++++++++++++++++++++++++++++ flake.nix | 32 +++++++++++++++++++ neovim.nix | 51 ++++++++++++++++++++++++++++++ tools.nix | 14 +++++++++ users.nix | 35 +++++++++++++++++++++ 10 files changed, 330 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 README.md create mode 100644 configuration.nix create mode 100644 dev-authorized-keys create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 neovim.nix create mode 100644 tools.nix create mode 100644 users.nix diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8c52ff9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2089a65 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# Nix build results and local environment state. +/result +/result-* +/.direnv/ + +# Local secrets must never be imported into Nix or committed. +/.env +/.env.* +!/.env.example +/secrets/ + +# Editor temporary files. +*.swp +*.swo +*~ diff --git a/README.md b/README.md new file mode 100644 index 0000000..15967a5 --- /dev/null +++ b/README.md @@ -0,0 +1,76 @@ +# Development host + +Flat, explicit NixOS modules, with locked inputs. Required machine setup belongs in these configs—not a list of manual installs or dotfile copies. + +| File | Owns | +| --- | --- | +| `flake.nix`, `flake.lock` | Host entry point and exact Nixpkgs/Home Manager/dotfile revisions and content hashes | +| `configuration.nix` | EC2 base, platform, Nix features, compatibility version, imports | +| `users.nix` | `dev`, SSH/sudo, Home Manager integration, workspace/repo ownership | +| `dev-authorized-keys` | Public SSH keys for `dev`—never private keys | +| `tools.nix` | Git, declarative user Git policy, official Nix formatter | +| `neovim.nix` | Editor and deployment of the unchanged upstream dotfiles | + +## Account + +- Daily user: `dev`, UID 1001, home `/home/dev`, workspace `~/projects`. +- Passwordless sudo is scoped to `dev`; existing SSM-user administration and root SSH recovery remain intact. +- `dev` is authorized by the public key in this repo. No private key or password is embedded. +- `/etc/nixos` is writable by `dev` through a native tmpfiles ownership rule, which does not follow store symlinks. +- Nix daemon access stays untrusted for ordinary use. Sudo is a separate, explicit administrative capability. +- Bash is the temporary default; further shell/workflow preferences remain the user's choice. + +Enter from an administrator session with `sudo -iu dev`. + +Git author identity and remote destination are deliberately unset. `user.useConfigOnly` is now a Home Manager setting in `tools.nix`, not a required manual `git config` step. + +## Build and apply + +As `dev`, from `/etc/nixos`: + +```sh +nixfmt --check ./*.nix +nix flake check --no-update-lock-file +nix build .#nixosConfigurations.dev.config.system.build.toplevel --no-update-lock-file +``` + +New source files must be added to Git for flakes to see them. Keep `flake.lock` in version control. A build does **not** activate changes. + +Review and activate exactly the built closure: + +```sh +sudo nixos-rebuild dry-activate --store-path "$(readlink -f result)" +sudo nixos-rebuild switch --store-path "$(readlink -f result)" +``` + +For initial deployment on a compatible NixOS EC2 base where flakes are not enabled yet, check out this repo and run the build as an existing administrator with the temporary CLI flag: + +```sh +nix --extra-experimental-features 'nix-command flakes' build \ + .#nixosConfigurations.dev.config.system.build.toplevel --no-update-lock-file +``` + +Then review/apply the resulting closure as above. The configuration creates `dev`, sets permissions and deploys its files. **No separate Neovim clone, copy, useradd, chown or global Git-config recipe is required.** + +This host build does not use a mutable channel. NixOS's native flake integration also pins the `nixpkgs` registry entry and `` lookup to the system input. Dev-environment templates/composition remain deferred; there is no flake framework here. + +## Neovim: import, do not rewrite + +The input is [the existing neovim-dots repository](https://git.cyber.ayyalasomayajula.net/marsultor/neovim-dots), initially pinned to `380eb86778a7c53a0f1c18e84f14037456155347`. + +Home Manager deploys its files under `~/.config/nvim`, with the **Lua, AstroNvim, Lazy, Mason, plugins and keymaps unchanged**. `programs.neovim.configure` stays empty so Neovim discovers `init.lua` normally. No Nixvim or custom Lua loader. + +- Configuration files are linked from the pinned source and managed by Nix. Change the upstream repo and its input revision rather than editing generated links. +- Lazy's `lazy-lock.json` must remain writable. The config seeds a copy at `~/.local/state/nvim/locks/.json` and links to it. A new dotfile revision gets its own original lock; repeated activation preserves runtime changes to an existing lock. +- A declared migration preserves the earlier manual checkout intact at `~/projects/neovim-dots-before-nix`. It refuses to overwrite an existing backup. On a clean home this migration does nothing. + +**Reproducibility boundary:** Nix locks the host inputs and dotfile source, and reproduces their deployment. The existing Lua still bootstraps Lazy and manages plugin/Mason downloads at runtime. The supplied Lazy lock records plugin revisions, but it is writable and Mason's tool versions are not pinned by this Nix config. This is not a claim that every runtime download/cache is a Nix-reproducible build. Changing that policy requires a separate agreement; do not silently replace the user's plugin managers. + +## Updates and safety + +- To change a pinned source, update its revision in `flake.nix`, regenerate `flake.lock` with `nix flake lock`, review the diff, then check/build before switching. No unattended updates. +- Keep the EC2 module, sandboxing, signature verification and recovery access intact. +- `system.stateVersion` and `home.stateVersion` are both `26.05`; these preserve compatibility, not package versions. +- `test` activates changes too; it is not a dry run. Keep the original system generation. +- Rollbacks do not restore mutable user/application data, lockfile updates, backups, or this Git working tree. +- No private keys, plaintext secrets, build outputs or agent notes in this repo. diff --git a/configuration.nix b/configuration.nix new file mode 100644 index 0000000..339470e --- /dev/null +++ b/configuration.nix @@ -0,0 +1,26 @@ +{ inputs, modulesPath, ... }: + +{ + imports = [ + # Keep the image's EC2 boot, storage, metadata, SSH and SSM integration. + "${modulesPath}/virtualisation/amazon-image.nix" + ./users.nix + ./tools.nix + ./neovim.nix + ]; + + nixpkgs.hostPlatform = "x86_64-linux"; + + nix = { + channel.enable = false; + settings.experimental-features = [ + "nix-command" + "flakes" + ]; + }; + + system.configurationRevision = inputs.self.rev or inputs.self.dirtyRev or null; + + # Initial data/default compatibility, not the desired package release. + system.stateVersion = "26.05"; +} diff --git a/dev-authorized-keys b/dev-authorized-keys new file mode 100644 index 0000000..d34e443 --- /dev/null +++ b/dev-authorized-keys @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPQKGcukJjCK6wu4iMoHlEgaCN2gnR4pEKFjpMgn78yI krishna pub diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..5b540a5 --- /dev/null +++ b/flake.lock @@ -0,0 +1,68 @@ +{ + "nodes": { + "home-manager": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1787377438, + "narHash": "sha256-Sxu1NLTD/Ern6hFGLlZmtKCSct3YQXZI/lls8RE1XeM=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "65258d5c65a250189fde2e35f490d15e064c4c62", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "home-manager", + "rev": "65258d5c65a250189fde2e35f490d15e064c4c62", + "type": "github" + } + }, + "neovim-dots": { + "flake": false, + "locked": { + "lastModified": 1780499261, + "narHash": "sha256-ScMf9MqK4EyEU0/Ki134efrPa8azNxgALnTfgiaLA3w=", + "ref": "main", + "rev": "380eb86778a7c53a0f1c18e84f14037456155347", + "revCount": 17, + "type": "git", + "url": "https://git.cyber.ayyalasomayajula.net/marsultor/neovim-dots" + }, + "original": { + "ref": "main", + "rev": "380eb86778a7c53a0f1c18e84f14037456155347", + "type": "git", + "url": "https://git.cyber.ayyalasomayajula.net/marsultor/neovim-dots" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1787848966, + "narHash": "sha256-7gDpu5hpq0rOYnYMcOWqSzquK4HA/xU8Xf3CjUBOOJA=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "d57af924f160a5084293c71c2043f058bd1cdb60", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "d57af924f160a5084293c71c2043f058bd1cdb60", + "type": "github" + } + }, + "root": { + "inputs": { + "home-manager": "home-manager", + "neovim-dots": "neovim-dots", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..1ccc816 --- /dev/null +++ b/flake.nix @@ -0,0 +1,32 @@ +{ + description = "Development host"; + + inputs = { + # Keep the machine's existing Nixpkgs revision; this is not an upgrade. + nixpkgs.url = "github:NixOS/nixpkgs/d57af924f160a5084293c71c2043f058bd1cdb60"; + + home-manager = { + url = "github:nix-community/home-manager/65258d5c65a250189fde2e35f490d15e064c4c62"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + neovim-dots = { + url = "git+https://git.cyber.ayyalasomayajula.net/marsultor/neovim-dots?ref=main&rev=380eb86778a7c53a0f1c18e84f14037456155347"; + flake = false; + }; + }; + + outputs = + inputs@{ nixpkgs, home-manager, ... }: + { + nixosConfigurations.dev = nixpkgs.lib.nixosSystem { + specialArgs = { inherit inputs; }; + modules = [ + ./configuration.nix + home-manager.nixosModules.home-manager + ]; + }; + + formatter.x86_64-linux = nixpkgs.legacyPackages.x86_64-linux.nixfmt; + }; +} diff --git a/neovim.nix b/neovim.nix new file mode 100644 index 0000000..a70759f --- /dev/null +++ b/neovim.nix @@ -0,0 +1,51 @@ +{ inputs, ... }: + +{ + programs.neovim = { + enable = true; + defaultEditor = true; + # Leave configure empty: load the user's init.lua normally. + }; + + home-manager.users.dev = + { config, lib, ... }: + let + dots = inputs.neovim-dots; + configDir = "${config.xdg.configHome}/nvim"; + lockFile = "${config.xdg.stateHome}/nvim/locks/${dots.rev}.json"; + backup = "${config.home.homeDirectory}/projects/neovim-dots-before-nix"; + in + { + # Import the original repo, without translating or patching its Lua. + xdg.configFile."nvim" = { + source = lib.cleanSourceWith { + src = dots; + filter = path: _: baseNameOf path != "lazy-lock.json"; + }; + recursive = true; + }; + + # Lazy writes this file. Seed a writable copy per pinned config revision. + xdg.configFile."nvim/lazy-lock.json".source = config.lib.file.mkOutOfStoreSymlink lockFile; + home.activation.neovimLock = lib.hm.dag.entryBetween [ "linkGeneration" ] [ "writeBoundary" ] '' + if [[ ! -e "${lockFile}" ]]; then + run install -D -m 0600 "${dots}/lazy-lock.json" "${lockFile}" + fi + ''; + + # Preserve the earlier manual checkout intact; never overwrite a backup. + # On a clean machine this is a no-op, not a deployment prerequisite. + home.activation.neovimCheckoutBackup = + lib.hm.dag.entryBetween [ "linkGeneration" ] [ "writeBoundary" ] + '' + if [[ -d "${configDir}/.git" ]]; then + if [[ -e "${backup}" || -L "${backup}" ]]; then + echo "Refusing to overwrite Neovim checkout backup: ${backup}" >&2 + exit 1 + fi + run mkdir -p "$(dirname "${backup}")" + run mv -T "${configDir}" "${backup}" + fi + ''; + }; +} diff --git a/tools.nix b/tools.nix new file mode 100644 index 0000000..82add7a --- /dev/null +++ b/tools.nix @@ -0,0 +1,14 @@ +{ pkgs, ... }: + +{ + programs.git.enable = true; + home-manager.users.dev.programs.git = { + enable = true; + package = null; # The system module supplies Git. + settings.user.useConfigOnly = true; + }; + + environment.systemPackages = [ + pkgs.nixfmt + ]; +} diff --git a/users.nix b/users.nix new file mode 100644 index 0000000..2e829fb --- /dev/null +++ b/users.nix @@ -0,0 +1,35 @@ +{ + users.users.dev = { + isNormalUser = true; + uid = 1001; + description = "Development user"; + extraGroups = [ "wheel" ]; + openssh.authorizedKeys.keyFiles = [ ./dev-authorized-keys ]; + }; + + security.sudo.extraRules = [ + { + users = [ "dev" ]; + commands = [ + { + command = "ALL"; + options = [ "NOPASSWD" ]; + } + ]; + } + ]; + + home-manager = { + useGlobalPkgs = true; + useUserPackages = true; + backupFileExtension = "before-nix"; + users.dev.home.stateVersion = "26.05"; + }; + + systemd.tmpfiles.rules = [ + "d /home/dev/.config 0755 dev users -" + "d /home/dev/projects 0755 dev users -" + # Keep the working repo editable by dev. Z does not follow store symlinks. + "Z /etc/nixos - dev users -" + ]; +}