lmjtfy.git / flake.nix
1{
2  description = "lmjtfy — let me Jev that for you: a Rust Cloudflare Worker that puts typed questions to Jev (TypeSafe AI) and shows the call";
3
4  inputs = {
5    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
6
7    # The Rust toolchain comes from fenix, as in ~/jevsnes: one stable
8    # toolchain with the wasm32-unknown-unknown rust-std the Worker builds for.
9    fenix = {
10      url = "github:nix-community/fenix";
11      inputs.nixpkgs.follows = "nixpkgs";
12    };
13
14    # The estate's package set and facts, for the Cloudflare credentials:
15    # wrangler runs through a wrapper that reads the API token from 1Password
16    # per run, as ~/jevstrudel's does. Nothing is logged in and nothing is on disk.
17    nix-pkgs.url = "git+ssh://git@github.com/deizel/nix-pkgs";
18    nix-pkgs.inputs.nixpkgs.follows = "nixpkgs";
19    nix-facts.url = "git+ssh://git@github.com/deizel/nix-facts";
20  };
21
22  outputs =
23    {
24      nixpkgs,
25      fenix,
26      nix-pkgs,
27      nix-facts,
28      ...
29    }:
30    let
31      system = "x86_64-linux";
32      pkgs = nixpkgs.legacyPackages.${system};
33      rust = fenix.packages.${system};
34
35      rustToolchain = rust.combine [
36        rust.stable.rustc
37        rust.stable.cargo
38        rust.stable.clippy
39        rust.stable.rustfmt
40        rust.stable.rust-src
41        rust.stable.rust-analyzer
42        rust.targets.wasm32-unknown-unknown.stable.rust-std
43      ];
44
45      cloudflare = {
46        itemRef = nix-facts.facts.onePassword.cloudflareMasterKey;
47        accountId = nix-facts.facts.cloudflare.accounts.deizel;
48      };
49      # `lmjtfy-wrangler <dir> [wrangler args]`: wrangler with the account's
50      # token in its environment. Workers AI has no local emulation, so even
51      # `dev` needs it once the Worker calls a model.
52      wrangler = nix-pkgs.lib.infra.mkWranglerDeploy {
53        inherit pkgs;
54        inherit (cloudflare) itemRef accountId;
55        name = "lmjtfy-wrangler";
56      };
57
58      # `lmjtfy-eval [args]`: tools/eval with the account, and where in
59      # 1Password its token is, in its own environment only. The reference is
60      # an `op://` URL, and `op-env-run` resolves every such value it finds
61      # in the environment it is run from, with an account that cannot read
62      # this one. So it must not be a devshell variable (2026-10-02: it was,
63      # and the dev server's op-env-run refused to start).
64      evalTool = pkgs.writeShellApplication {
65        name = "lmjtfy-eval";
66        text = ''
67          export CLOUDFLARE_ACCOUNT_ID=${cloudflare.accountId}
68          export LMJTFY_OP_ITEM_REF=${pkgs.lib.escapeShellArg cloudflare.itemRef}
69          exec cargo run -q -p eval -- "$@"
70        '';
71      };
72
73      # `lmjtfy-secret <which>`: sets one of the deployed Worker's secrets.
74      #   op-env-run -- lmjtfy-secret jev      LMJTFY_TYPESAFE_API_KEY, from the environment
75      #   lmjtfy-secret account                CLOUDFLARE_ACCOUNT_ID, from the estate's facts
76      #   … | lmjtfy-secret analytics          CLOUDFLARE_ANALYTICS_TOKEN, from stdin:
77      #       nix run ~/config#infra-core -- output -raw lmjtfy-analytics-token-value | tail -n 1 | lmjtfy-secret analytics
78      #   op-env-run -- lmjtfy-secret github   LMJTFY_GITHUB_TOKEN, from the environment: the clone
79      #       proxy's read-only fine-grained token (made on GitHub; the API cannot mint one)
80      #
81      # Not `printf value | lmjtfy-wrangler … secret put`: that wrapper runs
82      # op.exe to fetch the Cloudflare token before it runs wrangler, op.exe
83      # reads the stdin it is given, and the value is gone by the time
84      # wrangler looks. That uploaded an empty secret on 2026-10-02 and the
85      # live site said Jev was offline. Here op gets no stdin.
86      secretTool = pkgs.writeShellApplication {
87        name = "lmjtfy-secret";
88        runtimeInputs = [ pkgs.wrangler ];
89        text = ''
90          case "''${1:-}" in
91            jev)
92              name=LMJTFY_TYPESAFE_API_KEY
93              value=''${LMJTFY_TYPESAFE_API_KEY:-}
94              ;;
95            account)
96              name=CLOUDFLARE_ACCOUNT_ID
97              value=${cloudflare.accountId}
98              ;;
99            analytics)
100              name=CLOUDFLARE_ANALYTICS_TOKEN
101              value=$(cat)
102              ;;
103            github)
104              name=LMJTFY_GITHUB_TOKEN
105              value=''${LMJTFY_GITHUB_TOKEN:-}
106              ;;
107            *)
108              echo "usage: lmjtfy-secret jev | account | analytics | github" >&2
109              exit 2
110              ;;
111          esac
112          if [ -z "$value" ]; then
113            echo "lmjtfy-secret: no value for $name" >&2
114            exit 1
115          fi
116          op=op
117          if command -v op.exe >/dev/null; then op=op.exe; fi
118          CLOUDFLARE_API_TOKEN=$("$op" read ${pkgs.lib.escapeShellArg cloudflare.itemRef}/Token </dev/null | tr -d '\r')
119          if [ -z "$CLOUDFLARE_API_TOKEN" ]; then
120            echo "lmjtfy-secret: no Cloudflare token from 1Password" >&2
121            exit 1
122          fi
123          export CLOUDFLARE_API_TOKEN
124          export CLOUDFLARE_ACCOUNT_ID=${cloudflare.accountId}
125          cd "$(git rev-parse --show-toplevel)/apps/lmjtfy"
126          printf %s "$value" | wrangler secret put "$name"
127        '';
128      };
129
130      # Everything the Worker builds and tests with, from public inputs only.
131      # Nix fetches a locked input only when an output uses it (measured on
132      # Nix 2.34, 2026-10-02), so anyone who clones can enter this shell
133      # without access to nix-pkgs or nix-facts.
134      public = [
135        rustToolchain
136
137        # The Worker build: worker-build runs cargo for wasm32, then
138        # wasm-bindgen, then writes build/index.js for wrangler.
139        pkgs.worker-build
140        pkgs.wasm-bindgen-cli
141        pkgs.binaryen
142        pkgs.esbuild
143        pkgs.wrangler
144
145        pkgs.curl
146        pkgs.jq
147      ];
148
149      # worker-build otherwise downloads its own wasm-bindgen, wasm-opt and
150      # esbuild into a cache. These are nix's. Cargo.toml pins the
151      # wasm-bindgen crate to this CLI's exact version, because the two
152      # must match.
153      buildTools = {
154        WASM_BINDGEN_BIN = "${pkgs.wasm-bindgen-cli}/bin/wasm-bindgen";
155        WASM_OPT_BIN = "${pkgs.binaryen}/bin/wasm-opt";
156        ESBUILD_BIN = "${pkgs.esbuild}/bin/esbuild";
157      };
158    in
159    {
160      packages.${system}.wrangler = wrangler;
161
162      devShells.${system} = {
163        # `nix develop`: build and test.
164        default = pkgs.mkShell ({ packages = public; } // buildTools);
165
166        # `nix develop .#owner`: the same, and the owner's tools, which read
167        # the Cloudflare account and the 1Password item from the private
168        # nix-facts: the credentialed wrangler, the secrets, the eval, and the
169        # pitchfork that supervises the dev server (from nix-pkgs, which
170        # carries a newer release than nixpkgs).
171        owner = pkgs.mkShell (
172          {
173            packages = public ++ [
174              wrangler
175              secretTool
176              evalTool
177              nix-pkgs.packages.${system}.pitchfork
178            ];
179          }
180          // buildTools
181        );
182      };
183    };
184}