On this page

Choose a flake output

Pin the flake revision in the consuming deployment. An output containing a library is not an executable application. Platform-conditional outputs are absent on other platforms; verify the pinned Nixpkgs platform support before planning an upgrade.

OutputPurpose
default, agent-cli-static, agent-cliCLI packages. The default package uses the static Linux harness with runtime tools; agent-cli is the native build. The default app runs the CLI.
docsHaskell documentation server package/app and development shell.
agent-telegramTelegram gateway package and app; nixosModules.telegram supplies managed instances.
agent-serverHTTP server package/app; nixosModules.agent-server supplies the tenant service boundary.
agent-runtime-daemonLocal Unix-socket scheduler package/app.
agent-server-clientHaskell client library, not a web frontend.
agent-sandbox-runner, agent-sandbox-rootfsLinux-only tenant execution artifacts. Use the server module; do not bypass its admission checks.
agent-native-bridgeDarwin native foreign library. agent-native-bridge-library is the Haskell package.
agent-cli-macos-bundle, agent-cli-macos-archiveDarwin CLI release artifacts, not proof of a native conversation GUI.
agent-openai-loginPackaged OpenAI login executables and app.

Other named Haskell package outputs support embedding/development. Use nix develop for the harness or nix develop .#docs for this site's focused GHCi environment.

Provision the Linux sandbox boundary

The runner and root filesystem are a matched pair selected by the flake. On a Linux builder, inspect the artifacts without starting a tenant:

runner=$(nix build .#agent-sandbox-runner --no-link --print-out-paths)
rootfs=$(nix build .#agent-sandbox-rootfs --no-link --print-out-paths)
test -x "$runner/bin/agent-sandbox-runner"
test -x "$rootfs/bin/agent-sandbox-worker"
test -s "$rootfs/nix-state-seed/db/db.sqlite"

The rootfs output is an unpacked immutable directory, not a Docker archive or bootable machine image. It contains the guest worker, shell, Git, Nix, document tools, certificates and a Nix registration seed. The runner contains the selected rootfs store path; do not substitute a separately modified tree. These build commands establish neither isolation nor a running server.

  1. Use a Linux NixOS host with unified cgroup v2, the cpu/memory/pids controllers and working unprivileged user/network namespaces. The packaged gVisor uses systrap; the runner uses slirp4netns and namespace-local nftables.
  2. Import haskell-agent.nixosModules.agent-server from the locked input. Provision non-overlapping workspaces, owner-only registry and credential files as described in tenant provisioning. List every workspace in the module's workspaceRoots.
  3. Apply the module fragment below in that NixOS configuration. Replace example paths with the already provisioned paths; this fragment does not create tenants or credentials.
{ haskell-agent, ... }: {
  imports = [ haskell-agent.nixosModules.agent-server ];
  services.haskell-agent.server = {
    enable = true;
    tenantRegistryFile = "/run/credentials/agent-tenants.json";
    workspaceRoots = [ "/srv/agent-workspaces/acme" ];
    host = "127.0.0.1";
    maxActiveTenants = 16;
  };
}

Deploy with sudo nixos-rebuild switch --flake .#agent-host, using your configuration's actual host name. Keep the initial listener on loopback. Remote exposure additionally requires TLS termination, authentication rate limits, allowRemote and an explicit listener configuration.

The module creates a dedicated unprivileged account without supplementary groups, removes every capability, sets NoNewPrivileges, and delegates exactly cpu/memory/pids to its supervisor subgroup. It installs the runner under root-owned /run/haskell-agent-server-runners/STATE/GENERATION/. A direct runner path in a conventional group-writable Nix-store ancestry is intentionally rejected. Do not relax the ancestry check or run as root to bypass a failure. Launching the runner from an ordinary shell does not reproduce the module's security boundary.

Runner protocol and resource boundary

This is a private, revision-coupled server/worker protocol, not an additional public automation API. The server invokes serve with --protocol-version 1, tenant UUID, workspace path, recorded workspace device/inode and state path. Stdin must be a read-only pipe; stdout carries newline-delimited JSON and stderr carries private diagnostics. Never insert logging on protocol stdout.

PhaseRequired interpretation
Readinesstype: ready, version, tenantId, generation UUID, workspace /workspace and state /state. The broker validates identity and mounts before admitting tools.
Requesttype: tool, version, tenantId, generation, requestId, sessionId, cwd, dialect and call. The call carries id, name, arguments, kind and argumentsEncrypted; async is optional. Encrypted arguments are rejected.
Progress and completionoutput frames carry output; result frames carry ok/output and optional images. Both bind tenantId, generation and requestId. A progress frame is not successful completion.
Limits4 MiB requests and 16 MiB response frames. The worker stops emitting progress frames after 8 MiB of accumulated streamed output; the broker has a separate 16 MiB defensive streaming limit. Readiness has a 60-second deadline and tools have a 15-minute deadline. Schema, identity, timeout or transport failures fail closed, never executing the tool on the host instead.

Only the tenant workspace and guest-data directory are writable host binds. The runner pins their directory descriptors and rejects workspace device/inode substitution. Each sandbox tree, including its network helper, is limited to two CPUs, 2 GiB RAM without swap and 512 processes; the rootfs has a private 4 GiB overlay and 256 MiB mutable Nix-state tmpfs. Workspace/state disk quotas and database quotas/backups remain operator responsibilities. Outbound networking rejects private, loopback, link-local, metadata, reserved, IPv6 and captured host addresses. Host address changes retire the sandbox; no inbound service is exposed.

Verify and recover without weakening isolation

systemctl status haskell-agent-server
journalctl -u haskell-agent-server -n 100 --no-pager
systemctl show haskell-agent-server -p User -p Group -p NoNewPrivileges -p Delegate -p ControlGroup
curl --fail http://127.0.0.1:4096/healthz

A healthy HTTP listener does not prove a sandbox can start. After authentication, run a read-only request that actually uses a workspace inspection tool and verify its result. For startup failures, inspect namespace/cgroup availability, runner generation, workspace ownership and registry identity; diagnostics may contain sensitive paths and belong in a private operator channel.

If cleanup cannot prove descendant termination, the runner retains the tenant lock and fail-stops; a stale tenant cgroup blocks replacement. Stop the service and verify that its complete control group is empty before investigating retained state. Never delete a lock or cgroup while processes remain. Preserve diagnostic state, correct the host prerequisite, then restart and revalidate a read-only tool call. An interrupted mutation may already have changed files or remote systems; inspect before retrying. Upgrades and rollback use generation-addressed runner paths, so existing processes never silently switch runner executables.

Build and inspect Darwin artifacts

Run these commands in the pinned checkout on a supported Darwin builder. The bridge output contains include/HaskellAgentBridge.h and lib/libhaskell-agent-bridge.dylib. It is not the relocatable CLI bundle, an application bundle, or a signed/notarized GUI release.

bridge=$(nix build .#agent-native-bridge --no-link --print-out-paths)
bundle=$(nix build .#agent-cli-macos-bundle --no-link --print-out-paths)
archive=$(nix build .#agent-cli-macos-archive --no-link --print-out-paths)
test -f "$bridge/include/HaskellAgentBridge.h"
otool -L "$bridge/lib/libhaskell-agent-bridge.dylib"
"$bundle/bin/agent-cli" --version
ls "$archive"

Compile native consumers against that output's header, not a header from another revision. For the highlight.c example in native integration, a linker invocation is:

nix develop -c cc -I "$bridge/include" highlight.c \
  -L "$bridge/lib" -lhaskell-agent-bridge \
  -Wl,-rpath,"$bridge/lib" -o highlight

Inspect otool -L output before distribution: the foreign-library output preserves its Nix dependency closure and is not independently portable. Keep that closure available on the development host. A distributing native application must separately supply data files, dependency relocation, signing and entitlement policy, and the host callbacks documented by the bridge. This command is a build recipe, not evidence that an external application was tested.

Install the complete portable CLI directory

The CLI archive is haskell-agent-macos-arm64.tar.gz on Apple Silicon or haskell-agent-macos-x86_64.tar.gz on Intel, if that architecture is supported by the pinned Nixpkgs. A sibling .sha256 file accompanies it. Verify the release source independently; a checksum detects corruption but is not a publisher signature.

shasum -a 256 -c haskell-agent-macos-arm64.tar.gz.sha256
mkdir -p "$HOME/Applications"
test ! -e "$HOME/Applications/haskell-agent-macos-arm64" && \
  tar -xzf haskell-agent-macos-arm64.tar.gz -C "$HOME/Applications"
"$HOME/Applications/haskell-agent-macos-arm64/bin/agent-cli" --version

The example assumes an Apple Silicon archive copied into the current directory and a previously unused installation destination. Use a fresh versioned destination for upgrades rather than merging two releases. Keep the complete extracted tree: bin includes agent-cli, FFmpeg, Bun, ripgrep and zstd; lib/deps contains relocated libraries; libexec/postgresql contains PostgreSQL 18; share contains prompts, skills, runtime configuration, syntax definitions, time zones, the portable marker and license notices. Copying only agent-cli loses these runtime dependencies. The bundle build applies ad-hoc signatures, not an Apple Developer ID/notarization guarantee. Do not instruct users to disable Gatekeeper; verify the distributor's release/signing procedure.

The archive targets the CLI, not an installable conversation GUI. Nix is needed to build these artifacts; the portable archive is designed to run without a Nix installation. Project-specific tools, provider access and operating-system permissions remain separate prerequisites. Preserve user data and back it up before changing application or database versions. These artifact and sandbox recipes were checked against source, not deployed to a Linux host or a clean non-Nix Mac as part of this documentation change.

Host the documentation

For the full deployment and upgrade walkthrough, see self-hosting the documentation. The following is a compact NixOS service example.

nix build .#docs
nix run .#docs
# Optional alternative listener:
DOCS_HOST=127.0.0.1 DOCS_PORT=8080 nix run .#docs

The default is 127.0.0.1:4321. DOCS_PORT must be 1–65535; DOCS_HOST changes the listener. DOCS_ASSET_DIRECTORY overrides the installed asset directory for development; normally leave it unset. The build output is an application with packaged assets, not a static HTML directory.

For a NixOS service, pass the pinned flake input as haskell-agent through your module arguments and use this module fragment:

{ pkgs, haskell-agent, ... }: {
  systemd.services.haskell-agent-documentation = {
    description = "Haskell Agent documentation";
    wantedBy = [ "multi-user.target" ];
    environment = { DOCS_HOST = "127.0.0.1"; DOCS_PORT = "4321"; };
    serviceConfig = {
      ExecStart = "${haskell-agent.packages.${pkgs.stdenv.hostPlatform.system}.docs}/bin/documentation-server";
      DynamicUser = true;
      Restart = "on-failure";
      NoNewPrivileges = true;
      ProtectSystem = "strict";
      ProtectHome = true;
      PrivateTmp = true;
    };
  };
}

After applying your system configuration, inspect systemctl status haskell-agent-documentation and journalctl -u haskell-agent-documentation -n 100 --no-pager. Verify the loopback URL before configuring a proxy. In an existing TLS-enabled Nginx virtual host, proxy the root path to the listener:

location / {
    proxy_pass http://127.0.0.1:4321;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
}

The site uses root-relative links; host it at a domain root rather than assuming a subpath prefix works. A public documentation service needs no model credentials, workspace access or PostgreSQL. Binding to 0.0.0.0 exposes it to other machines. These deployment fragments have been reviewed against the application, not exercised on your production host.

Telegram NixOS option reference

Import haskell-agent.nixosModules.telegram. All options below belong under services.haskell-agent.telegram.instances.NAME. Names start with a lowercase letter, contain lowercase letters/digits/hyphens, and have at most 16 characters. See the setup example before customizing an instance. Rebuild/restart the service after changing its declaration. Keep credentials outside the Nix store.

OptionType and defaultEffect
enableBoolean, falseEnable this instance.
packagePackage, flake agent-telegramExecutable distribution.
userString, haskell-agent-NAMEService account.
groupString, user valueService group.
createUserBoolean, trueCreate dedicated system user/group.
homeDirectoryString, /var/lib/haskell-agent-telegram-NAMEPrivate persistent home.
workingDirectoryRequired stringExisting absolute project path.
tokenFileRequired stringAbsolute secret file loaded through systemd credentials.
providerEnum, openaiopenai, xai, openrouter, gemini or claude-code.
modelNullable string, nullNull selects the provider default.
effortNullable string, nullOptional reasoning effort.
yoloBoolean, falseAutomatic mutation approval; review host access first.
allowedUsersUnsigned integer list, emptyNumeric Telegram user IDs.
respondToAllGroupMessagesBoolean, falseAmbient participation; does not disable allowlisting.
codexHomeString, HOME/.codexOpenAI credential directory.
postgresPackagePackage, PostgreSQL 18Managed private database executable.
postgresPortPort, 55432Cluster port; Unix-socket isolation permits reuse across instances.
extraPackagesPackage list, emptyAdditional tools on the agent's PATH.
environmentString attribute set, emptyNon-secret service variables.
environmentFilesString list, emptySystemd environment files read at startup; prefix an absolute path with '-' to allow a missing file.
mcpInitStrategyEnum, autoauto or progressive.
mcpServersNullable server attribute set, nullNull preserves existing catalog; an attrset owns the entire catalog.

Declarative MCP ownership

mcpServers = null leaves the existing catalog untouched. An attribute set takes ownership of the complete mcpServers value in the service user's configuration. An empty attribute set clears that catalog. Do not combine ad-hoc edits with a declaration and expect both sources to merge.

services.haskell-agent.telegram.instances.assistant.mcpServers = {
  project = {
    command = "/absolute/path/to/mcp-server";
    args = [ "--stdio" ];
    enabled = true;
    startupTimeoutSeconds = 30;
    requestTimeoutSeconds = 60;
  };
};

The command is a placeholder for an installed stdio MCP server, not supplied by this example. Declare exactly one of command or URL per server.

Server fieldType/defaultMeaning
enabledBoolean, trueConnect this server.
commandNullable string, nullLocal stdio executable; mutually exclusive with URL.
urlNullable string, nullRemote endpoint; mutually exclusive with command.
argsString list, emptyLocal command arguments.
cwdNullable string, nullLocal server working directory.
environmentString attribute set, emptyNon-secret server environment. Nix values are store-readable.
startupTimeoutSecondsPositive integer, 30Startup deadline.
requestTimeoutSecondsPositive integer, 60Request deadline.

Operate and upgrade services

  1. Record the current flake lock and service configuration before upgrading.
  2. Back up PostgreSQL using database tools; do not copy a live data directory. Preserve gateway state separately and protect credential recovery.
  3. Apply the new declaration and inspect the service journal. A running process is not proof of valid provider authentication or correct project ownership.
  4. Run a read-only message/request and confirm the intended workspace and account before allowing mutations.

For an existing service user, set createUser = false only after provisioning its user/group and private home yourself. The project must already exist and be writable when mutations are allowed. Do not solve ownership failures with automatic approval. PostgreSQL package upgrades require an explicit database migration plan; changing postgresPackage does not itself migrate data. A failed migration is not fixed by deleting the database.

Database and gateway restore checklist

For a PostgreSQL major-version change, retain the old package and database directory until recovery is proven. Stop the Telegram gateway to prevent new requests while taking a consistent logical database dump with the old version's database tools. Record database roles/ownership and the configured port; protect the dump as conversation data. Back up the gateway state and credentials under the same service identity separately.

  1. Restore the logical dump into a newly initialized target-version database in an isolated test environment. Never point two PostgreSQL versions at the same data directory, and never run two gateway pollers with the same bot token.
  2. Check conversation counts and representative session history, service-user ownership and database connectivity before promoting the restored database.
  3. Keep the gateway stopped while switching its database configuration. Start one gateway, inspect startup logs, then issue a read-only message.
  4. Inspect retained replies and pending work before using retry: restoring earlier state cannot retract Telegram deliveries or remote tool effects.
  5. If validation fails, stop the new gateway and database, preserve their diagnostic data, and return to the untouched old database/package and matching gateway backup. Reconcile any post-cutover effects before resuming work.

Use your PostgreSQL version's documented dump/restore commands with the actual connection details; the package override supplies binaries, not an automated major-version migrator. This operational sequence is source-reviewed guidance, not a claim that a production restore was exercised.

Environment conflicts and restarts

Keep each secret in one configured environment file rather than also assigning it in the Nix environment map. An optional file prefixed with - may be absent; use a required file when absence should block startup. A changed file does not update an already running process: restart the instance, inspect its journal and verify a read-only request. If the wrong account remains selected, compare configured variable names and file ordering, not printed secret values; provider credential stores may also supply authentication. Remove the duplicate assignment at its source and restart again. Do not dump the service environment into a public issue.

For multi-tenant API hosting, use the separate server deployment requirements. Its sandbox, authentication and storage policy are not the Telegram module's policy.