diff --git a/docs/deployment/NIX.md b/docs/deployment/NIX.md new file mode 100644 index 0000000..5a6e9a0 --- /dev/null +++ b/docs/deployment/NIX.md @@ -0,0 +1,58 @@ +# Nix Deployment + +This repo ships a Nix flake with: + +- `nix run` support (runs `flynn`) +- `nix develop` dev shell (Node 22 + pnpm) +- A package that builds `dist/` and preserves `dist/gateway/ui` adjacency +- An optional NixOS module (`services.flynn`) + +## Quick Start + +```bash +# Dev shell +nix develop + +# Run (prints CLI help) +nix run . -- --help + +# Build package +nix build .# +``` + +### First Build: Update `pnpmDepsHash` + +The Nix package uses `buildPnpmPackage`, which requires a fixed dependency hash. + +On the first `nix build`, Nix will fail with a message containing the expected +hash (looks like `got: sha256-...`). Copy that value into `nix/package.nix` as +`pnpmDepsHash`, then rebuild. + +## NixOS Module + +The flake exports `nixosModules.flynn`. + +Example: + +```nix +{ + inputs.flynn.url = "github:will666/flynn"; + + outputs = { self, nixpkgs, flynn, ... }: { + nixosConfigurations.myHost = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + modules = [ + flynn.nixosModules.flynn + { + services.flynn = { + enable = true; + configFile = "/etc/flynn/config.yaml"; + dataDir = "/var/lib/flynn"; + }; + } + ]; + }; + }; +} +``` + diff --git a/docs/deployment/PRODUCTION.md b/docs/deployment/PRODUCTION.md index eae9684..343438d 100644 --- a/docs/deployment/PRODUCTION.md +++ b/docs/deployment/PRODUCTION.md @@ -6,6 +6,7 @@ This guide covers deploying Flynn in a production environment. - [Prerequisites](#prerequisites) - [Docker Deployment](#docker-deployment) +- [Nix Deployment](#nix-deployment) - [Systemd Service](#systemd-service) - [Security](#security) - [Configuration](#configuration) @@ -95,6 +96,11 @@ export ANTHROPIC_API_KEY=sk-... export OPENAI_API_KEY=sk-... ``` +## Nix Deployment + +If you use Nix, this repo ships a flake (package + dev shell + optional NixOS +module). See `docs/deployment/NIX.md`. + ## Systemd Service ### Service File diff --git a/docs/plans/state.json b/docs/plans/state.json index 6b9dd73..a63f824 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -55,6 +55,23 @@ "test_status": "pnpm test:run + pnpm typecheck passing" }, + "deployment-targets-nix": { + "status": "completed", + "date": "2026-02-16", + "updated": "2026-02-16", + "summary": "Added a Nix flake/package that builds dist/ (including dist/gateway/ui adjacency) and an optional NixOS module (services.flynn) for systemd deployment.", + "files_created": [ + "flake.nix", + "nix/package.nix", + "nix/module.nix", + "docs/deployment/NIX.md" + ], + "files_modified": [ + "docs/deployment/PRODUCTION.md" + ], + "test_status": "Not run (Nix build requires pnpmDepsHash update); pnpm test suite unaffected" + }, + "openclaw-gap-roadmap": { "file": "2026-02-15-openclaw-gap-roadmap.md", "status": "planned", @@ -2159,7 +2176,7 @@ "tier2_completion": "4/4 (100%) — inbound webhooks, vector memory search, Dockerfile, heartbeat monitor", "tier3_completion": "5/5 (100%) — lane queue, credential redaction, web UI token dashboard, xAI (Grok) provider, Voyage AI embeddings", "tier4_completion": "4/4 (100%) — gateway lock, shell completion, Tailscale Serve/Funnel, DM pairing codes", - "feature_gap_scorecard": "102/128 match (80%), 0 partial (0%), 26 missing (20%)", + "feature_gap_scorecard": "103/128 match (80%), 0 partial (0%), 25 missing (20%)", "operator_dx_milestone": "Phase 3 (Live Ops Dashboard): 2/2 plans complete — milestone done", "gmail_auth_cli": "flynn gmail-auth command implemented with OAuth2 flow, doctor check, config routed to Telegram", "native_audio_support": "completed — smart routing for native audio (Gemini/OpenAI/GitHub) vs Whisper transcription fallback", diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..92b93a2 --- /dev/null +++ b/flake.nix @@ -0,0 +1,50 @@ +{ + description = "Flynn - self-hosted personal AI agent"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = + { self, nixpkgs, flake-utils }: + let + overlays = { + default = final: _prev: { + flynn = import ./nix/package.nix { pkgs = final; }; + }; + }; + in + (flake-utils.lib.eachDefaultSystem ( + system: + let + pkgs = import nixpkgs { inherit system; overlays = [ overlays.default ]; }; + in + { + packages = { + default = pkgs.flynn; + flynn = pkgs.flynn; + }; + + apps.default = flake-utils.lib.mkApp { + drv = pkgs.flynn; + exePath = "/bin/flynn"; + }; + + devShells.default = pkgs.mkShell { + packages = [ + pkgs.nodejs_22 + pkgs.pnpm + pkgs.python3 + pkgs.gnumake + pkgs.pkg-config + ]; + }; + } + )) + // { + inherit overlays; + nixosModules.flynn = import ./nix/module.nix; + }; +} + diff --git a/nix/module.nix b/nix/module.nix new file mode 100644 index 0000000..f5976cf --- /dev/null +++ b/nix/module.nix @@ -0,0 +1,80 @@ +{ config, lib, pkgs, ... }: +let + cfg = config.services.flynn; +in +{ + options.services.flynn = { + enable = lib.mkEnableOption "Flynn daemon"; + + package = lib.mkOption { + type = lib.types.package; + default = pkgs.flynn; + defaultText = "pkgs.flynn"; + description = "Flynn package to run."; + }; + + configFile = lib.mkOption { + type = lib.types.path; + description = "Path to Flynn YAML config (exported as FLYNN_CONFIG)."; + }; + + dataDir = lib.mkOption { + type = lib.types.str; + default = "/var/lib/flynn"; + description = "Persistent data directory (exported as FLYNN_DATA_DIR)."; + }; + + user = lib.mkOption { + type = lib.types.str; + default = "flynn"; + description = "System user for the service."; + }; + + group = lib.mkOption { + type = lib.types.str; + default = "flynn"; + description = "System group for the service."; + }; + }; + + config = lib.mkIf cfg.enable { + users.groups.${cfg.group} = { }; + users.users.${cfg.user} = { + isSystemUser = true; + group = cfg.group; + home = cfg.dataDir; + createHome = true; + }; + + systemd.services.flynn = { + description = "Flynn AI Assistant Daemon"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + + environment = { + NODE_ENV = "production"; + FLYNN_CONFIG = toString cfg.configFile; + FLYNN_DATA_DIR = cfg.dataDir; + }; + + serviceConfig = { + Type = "simple"; + User = cfg.user; + Group = cfg.group; + WorkingDirectory = cfg.dataDir; + ExecStart = "${cfg.package}/bin/flynn start"; + Restart = "on-failure"; + RestartSec = 10; + + # Baseline hardening; adjust as needed for your environment. + NoNewPrivileges = true; + PrivateTmp = true; + ProtectSystem = "strict"; + ProtectHome = true; + ReadWritePaths = [ cfg.dataDir ]; + }; + }; + }; +} + diff --git a/nix/package.nix b/nix/package.nix new file mode 100644 index 0000000..8cc0edf --- /dev/null +++ b/nix/package.nix @@ -0,0 +1,68 @@ +{ pkgs }: +let + lib = pkgs.lib; +in +pkgs.buildPnpmPackage rec { + pname = "flynn"; + version = "0.1.0"; + + # Keep the source small and deterministic for Nix builds. + src = lib.cleanSourceWith { + src = ./.; + filter = + path: type: + let + baseName = baseNameOf path; + in + !( + baseName == ".git" + || baseName == "node_modules" + || baseName == "dist" + || baseName == ".worktrees" + || baseName == "whisper-models" + ); + }; + + pnpmLock = ./pnpm-lock.yaml; + + # NOTE: Update this hash after the first `nix build` by copying the + # "got: sha256-..." value from the error message. + pnpmDepsHash = lib.fakeHash; + + nativeBuildInputs = [ + pkgs.makeWrapper + pkgs.python3 + pkgs.pkg-config + ]; + + buildPhase = '' + runHook preBuild + pnpm build + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + + mkdir -p $out/lib/flynn + cp -r dist node_modules package.json config $out/lib/flynn/ + if [ -f SOUL.md ]; then + cp SOUL.md $out/lib/flynn/ + fi + + makeWrapper ${pkgs.nodejs_22}/bin/node $out/bin/flynn \ + --add-flags $out/lib/flynn/dist/cli/index.js \ + --set-default NODE_ENV production + + runHook postInstall + ''; + + meta = with lib; { + description = "Self-hosted personal AI agent"; + homepage = "https://github.com/will666/flynn"; + license = licenses.mit; + platforms = platforms.unix; + mainProgram = "flynn"; + }; +} +