kolu
Docs

operate

Keep kolu running.

kolu’s home-manager module runs the web server as a systemd user service on Linux or a launchd LaunchAgent on macOS. Both supervisors restart a failed server, while padi and kaval keep workspace state and live shells below it.

Enable the service

Import the module and select the package for the current system:

{ pkgs, kolu, ... }:
{
  imports = [ kolu.homeManagerModules.default ];

  services.kolu = {
    enable = true;
    package = kolu.packages.${pkgs.stdenv.hostPlatform.system}.default;
  };
}

Apply the home-manager configuration as usual, then open http://127.0.0.1:7681. The module also puts the matching kolu, padi-tui, and kaval-tui commands on PATH.

A generation built this way carries every agent kolu can send to a remote host, so a first connect copies binaries this machine already holds instead of fetching them from a cache or asking the remote machine to build them. Nothing to warm, nothing to run before switching. The set comes from services.kolu.agentPackages, which the module’s flake defaults to the agents kolu is allowed to provision — override it only to pin particular builds.

The complete, build-tested flake is in nix/home/example. CI boots its systemd configuration in a NixOS VM and builds its launchd activation package on macOS.

On a headless NixOS box, also set users.users.<you>.linger = true in the system config so the user systemd — and this service — starts at boot without a login.

Ubuntu (and other non-NixOS Linux)

Same module, applied with standalone home-manager. Install Nix with flakes first (NixOS Asia guide), then a flake whose homeConfigurations output imports the module:

{
  description = "kolu on this box";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
    home-manager.url = "github:nix-community/home-manager";
    home-manager.inputs.nixpkgs.follows = "nixpkgs";
    kolu.url = "github:juspay/kolu";
  };

  outputs = { nixpkgs, home-manager, kolu, ... }:
    let
      system = "x86_64-linux"; # aarch64-linux on ARM
    in {
      homeConfigurations.you = home-manager.lib.homeManagerConfiguration {
        pkgs = nixpkgs.legacyPackages.${system};
        modules = [
          kolu.homeManagerModules.default
          ({ pkgs, ... }: {
            home.username = "you";
            home.homeDirectory = "/home/you";
            home.stateVersion = "24.11";
            services.kolu = {
              enable = true;
              package = kolu.packages.${pkgs.stdenv.hostPlatform.system}.default;
            };
          })
        ];
      };
    };
}

Replace you and x86_64-linux to match the box. From that directory:

nix run home-manager -- switch --flake .#you
sudo loginctl enable-linger you

Linger keeps the user systemd running after SSH disconnects — without it the service dies when you log out, which is the opposite of a headless box. Then open http://127.0.0.1:7681. Address, origins, and logs below apply unchanged. The From a Mac tutorial walks this as the first step of the Mac-window setup.

Choose the address

The service listens on loopback port 7681 by default. Change either value in the module:

services.kolu = {
  host = "0.0.0.0";
  port = 8080;
};

Bind a different address

A one-off nix run (or kolu web) listens on loopback too. To expose it on your LAN, pass an address and port after --:

nix --refresh run github:juspay/kolu -- web --bind 0.0.0.0 --port 8080

Open the address you chose. For another device, prefer the private HTTPS setup in From a Mac or From another device; plain LAN HTTP does not unlock install prompts, badges, or notifications.

First run compiling? Enable the binary cache

Every kolu commit is pre-built and pushed to a public binary cache, https://cache.nixos.asia/oss. The flake declares it, but Nix only honors a flake-declared cache if you accept it and your user is trusted by the Nix daemon — and on a standard multi-user install it isn’t, so the setting is dropped with a one-line warning (ignoring untrusted flake configuration setting 'extra-substituters') and the first run compiles kolu from source, including downloading a Rust toolchain for one of its helper binaries.

To download pre-built binaries instead, add the cache to the system Nix configuration (/etc/nix/nix.conf):

extra-substituters = https://cache.nixos.asia/oss
extra-trusted-public-keys = oss:KO872wNJkCDgmGN3xy9dT89WAhvv13EiKncTtHDItVU=

Or, on NixOS:

nix.settings = {
  extra-substituters = [ "https://cache.nixos.asia/oss" ];
  extra-trusted-public-keys = [ "oss:KO872wNJkCDgmGN3xy9dT89WAhvv13EiKncTtHDItVU=" ];
};

After editing nix.conf, restart the daemon (sudo systemctl restart nix-daemon on Linux, sudo launchctl kickstart -k system/org.nixos.nix-daemon on macOS).

Machines you add as another machine can inherit the cache through kolu: provisioning copies the agent’s binaries from this machine onto the host, cross-architecture included. A Linux host is sent them content-addressed (each store path named by the hash of what it holds), which its Nix accepts from anyone — no trusted-users, no cache key, nothing in its nix.conf. A Mac is sent them as signed (rewriting would break macOS’s code signatures), so that copy lands only if the Mac trusts your ssh user (trusted-users) or the cache’s key; when it trusts neither, kolu shows Nix’s refusal in the connect log and the Mac realises the agent itself, so the block above applied there lets it download instead of build.

The coding agents kolu puts on terminals’ PATH (AI coding agents) follow a different rule. On the machine running kolu they ship inside kolu itself — the whole set, the profiles you list included — so they come down with kolu from the same cache on the two systems the cache is filled for (x86_64 Linux and Apple Silicon). On aarch64 Linux the cache has no build of them, so the first nix run there compiles them, which takes a while. For a remote host this machine needs none of that: it reads which build the host needs from agent-distro’s published index and has the host download it over its own HTTPS from agent-distro’s cache (the same https://cache.nixos.asia/oss above), never building it — and keeps every machine current the same way, four times a day (Kept up to date). This machine’s Nix trusts nothing for it. A Linux host’s needn’t either — on Nix 2.17 through 2.34 it gets the set content-addressed — while a Mac, or a Linux host on Nix 2.35 or later, gets it signed and needs the trust above (trusted-users for the login, or the cache’s key; see When a delivery fails). On the machine running kolu, the set kolu shipped with keeps working either way.

Whether this machine has those binaries to copy depends on how you installed kolu. If you run kolu as a service through the home-manager module, it always does: the installed generation carries every agent closure kolu can provision, so the cache above is only about kolu’s own first build, never about a remote connect. If you launch kolu with nix run or a plain nix profile install, the agent closure is fetched on demand — from this cache, and from source if the cache doesn’t have it yet.

Allow a reverse-proxy origin

kolu accepts its own browser origin by default. A reverse proxy or tailscale serve can present a public browser origin that differs from the Host forwarded to kolu. Add that exact origin:

services.kolu.allowedOrigins = [
  "https://box.tailnet.ts.net"
];

This sets KOLU_ALLOWED_ORIGINS for both the WebSocket and HTTP RPC paths. The architecture page explains why the gate exists.

Read service logs

On Linux, use the systemd user journal:

systemctl --user status kolu
journalctl --user -u kolu -f

On macOS, launchd writes the two streams to:

~/Library/Logs/kolu.out.log
~/Library/Logs/kolu.err.log

Both work because a service manager owns the server’s output. Started by hand, nothing does — kolu web writes to your terminal and that is the only copy, so redirect it if the run is meant to outlive the shell:

kolu web > ~/kolu-server.log 2>&1

This is the log that records why a host’s daemon was stopped or replaced; Operations lists the full set and which question each answers.

Crashes and startup failures remain visible after the process exits.

Capture memory diagnostics

If the server or kaval daemon grows without bound, set an absolute diagnostics directory:

services.kolu.diagnostics.dir =
  "${config.home.homeDirectory}/.local/state/kolu-diagnostics";

Each restart gets a timestamped subdirectory containing:

  • a baseline heap snapshot after five minutes;
  • periodic memory statistics;
  • automatic snapshots near the V8 heap limit; and
  • snapshots requested with SIGUSR2.

To capture a snapshot on demand:

kill -USR2 <pid>

The snapshot lands in the current run’s diagnostics directory. Compare snapshots offline with MemLab to find what retains the growing objects.

Diagnostics are disabled by default and add no overhead until dir is set. Browser-tab or GPU growth has a different checklist in Troubleshooting.

Stop it, roll it back, or test beside it

Stopping the whole stack by hand — the daemons included — is not systemctl stop alone, and it is what a rollback to an older release needs. Running a second kolu on a machine that already has one needs its own state directories, or the test instance takes production’s daemons over. Both procedures, and the vocabulary they share, are in Operations.