I've been using some cheap code generation tools to help me make arcology2go. Tokenizing and transmitting parts of my personal knowledge to these fucking ghouls is sickening, and doing so without constraints horrifying. Folks are going out there giving a remote server the ability to execute code on their servers. Mine get a tiny VM and only have write access to some files that are auto-snapshot on the host filesystem every 15 minutes.
What's MicroVM?
References
A Nix Flake to build NixOS and run it on one of several Type-2 Hypervisors on NixOS/Linux or macOS. The project is intended to provide a more isolated alternative to nixos-container. You can either build and run MicroVMs like Nix packages, or alternatively install them as systemd services declaratively in your host's Nix Flake or imperatively with the provided microvm command.
microvm = {
url = "github:microvm-nix/microvm.nix";
inputs.nixpkgs.follows = "nixpkgs";
};Hermes Agent flake input
Hermes Agent ships its own flake with NixOS and Home Manager modules built via uv2nix. Only this VM consumes it so far, so it gets its own input snippet rather than piggybacking on the shared microvm input.
hermes-agent = {
url = "github:NousResearch/hermes-agent";
inputs.nixpkgs.follows = "nixpkgs";
};a microvm for langlemangling
https://github.com/microvm-nix/microvm.nix/blob/789c90b164b55b4379e7a94af8b9c01489024c18/doc/src/shares.md?plain=1#L39
{ inputs
, pkgs
, lib
, config
, ... }:
let
microvm = inputs.microvm;
nixpkgs-unstable = inputs.nixpkgs-unstable;
pkgs2 = import nixpkgs-unstable {
system = "x86_64-linux";
config.allowUnfree = true;
# libolm for Hermes Matrix E2EE (deprecated upstream, but required)
config.permittedInsecurePackages = [ "olm-3.2.16" ];
overlays = [ (import ../overlay.nix { inherit inputs; }) ]; # for pi-web, custom opencode...
};
in {
imports = [
microvm.nixosModules.host
];
microvm.host.enable = true;
microvm.vms.clown = {
autostart = false;
config = import ./microvm-langlemangler.nix {
# inherit pkgs;
pkgs = pkgs2;
nixpkgs = nixpkgs-unstable;
inherit inputs;
};
};
networking.nat = {
enable = true;
internalInterfaces = [ "virbr0" ];
externalInterface = "eno3";
};
}Here's the VM configuration:
{ inputs, nixpkgs, pkgs, ... }:
let
microvm = inputs.microvm;
writeable = "/nix/.rw-store";
in {
imports = [
./rrix.nix
microvm.nixosModules.microvm
inputs.home-manager.nixosModules.home-manager
];
# nixpkgs.overlays = [ overlay ];
home-manager.useGlobalPkgs = true;
home-manager.backupFileExtension = "hm-bak";
home-manager.extraSpecialArgs = { inherit inputs; };
nix.nixPath = [
"nixpkgs=${nixpkgs}"
];
microvm = {
# Enable writable nix store overlay so nix-daemon works.
# This is required for home-manager activation.
# Uses tmpfs by default (ephemeral), which is fine since we
# don't build anything in the VM.
writableStoreOverlay = writeable;
volumes = [
{
mountPoint = "/var";
image = "var.img";
size = 8192; # MB
}
{
image = "nix-store-overlay.img";
mountPoint = writeable;
size = 32876;
}
];
shares = [
{
# use proto = "virtiofs" for MicroVMs that are started by systemd
proto = "virtiofs";
tag = "ro-store";
source = "/nix/store";
mountPoint = "/nix/.ro-store";
}
{
proto = "virtiofs";
tag = "ssh-keys";
source = "/home/rrix/claude-microvm/ssh-host-keys";
mountPoint = "/etc/ssh/host-keys";
}
{
proto = "virtiofs";
tag = "claude-credentials";
source = "/home/rrix/claude-microvm";
mountPoint = "/home/rrix/claude-microvm";
}
{
proto = "virtiofs";
tag = "ollama-credentials";
source = "/home/rrix/claude-microvm/ollama";
mountPoint = "/home/rrix/.ollama";
}
{
# Persistent ~/.config so HM-managed configs (opencode.json, etc.)
# survive the tmpfs root. The opencode-web systemd user service reads
# from $HOME/.config/opencode/opencode.json which the HM module
# generates here. Create the source dir on the host before booting.
proto = "virtiofs";
tag = "xdh-config";
source = "/home/rrix/claude-microvm/config";
mountPoint = "/home/rrix/.config";
}
{
proto = "virtiofs";
tag = "arcology";
source = "/home/rrix/Code/arcology2go";
mountPoint = "/home/rrix/Code/arcology2go";
}
{
proto = "virtiofs";
tag = "workspace";
source = "/home/rrix/Code/claude-projects";
mountPoint = "/home/rrix/Code/claude-projects";
}
{
proto = "virtiofs";
tag = "org";
source = "/home/rrix/org";
mountPoint = "/home/rrix/org";
}
{
proto = "virtiofs";
tag = "nix";
source = "/home/rrix/nix";
mountPoint = "/home/rrix/nix";
}
];
interfaces = [{
type = "user";
id = "qemu";
mac = "02:00:00:01:01:01";
}];
forwardPorts = [
{
from = "host";
host.port = 10022;
guest.port = 22;
}
{
from = "host";
host.port = 8080;
guest.port = 8080;
}
{
from = "host";
host.port = 11434;
guest.port = 11434;
}
{
from = "host";
host.port = 8504;
guest.port = 8504;
}
];
hypervisor = "qemu";
vcpu = 8;
mem = 32768;
socket = "control.socket";
};
services.openssh.enable = true;
services.openssh.hostKeys = [{
path = "/etc/ssh/host-keys/ssh_host_ed25519_key";
type = "ed25519";
}];
# The Hermes gateway runs as a home-manager *user* service; without
# linger systemd tears the user manager (and the gateway) down when
# the last SSH session ends. HM cannot set this itself.
users.users.rrix.linger = true;
networking.nameservers = [
"100.100.100.100"
"1.1.1.1"
];
networking.firewall.enable = false;
# Fix for microvm shutdown hang (issue #170):
# Without this, systemd tries to unmount /nix/store during shutdown,
# but umount lives in /nix/store, causing a deadlock.
systemd.mounts = [
{
what = "store";
where = "/nix/store";
overrideStrategy = "asDropin";
unitConfig.DefaultDependencies = false;
}
];
environment.systemPackages = with pkgs; [
bashInteractive
htop
vim-full
nodejs
poppler-utils
gnumake
];
programs.nix-ld.enable = true;
home-manager.users.rrix = {
home.stateVersion = "25.05";
imports = [
../hm/emacs.nix
../hm/direnv.nix
../hm/git.nix
../hm/profile.nix
../hm/prompt.nix
../hm/pi-web.nix
# Hermes Agent user service — see the "Hermes Agent" section in
# this file for the full services.hermes-agent config.
inputs.hermes-agent.homeManagerModules.default
];
programs.home-manager.enable = true;
home.sessionVariables = {
NIXPKGS_ACCEPT_ANDROID_SDK_LICENSE=1;
NIXPKGS_ALLOW_UNFREE=1;
CLAUDE_CONFIG_DIR="/home/rrix/claude-microvm";
XDG_DATA_HOME="/home/rrix/claude-microvm/share";
XDG_CACHE_HOME="/home/rrix/claude-microvm/cache";
OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
ANDROID_HOME = "${inputs.arcology2go.packages.${pkgs.stdenv.hostPlatform.system}.android-sdk}/libexec/android-sdk";
GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${inputs.arcology2go.packages.${pkgs.stdenv.hostPlatform.system}.android-sdk}/libexec/android-sdk/build-tools/35.0.0/aapt2";
};
programs.opencode = {
enable = true;
package = pkgs.opencode;
extraPackages = [ pkgs.direnv ];
settings = {
plugin = [
"@simonwjackson/opencode-direnv"
];
# Notes access through the arcology2 notes CLI only; the tool
# output carries its own fail-closed risk gate.
permission.bash = {
"arcology2 notes *" = "allow";
};
mcp.arcology-notes = {
type = "local";
command = [
"uv" "run" "/home/rrix/.local/share/arcology-notes-mcp.py"
];
environment = {
ARCOLOGY_RISK_ENDPOINT = "http://windows:11434";
ARCOLOGY_RISK_MODEL = "hermes3:8b";
};
enabled = true;
};
mcp.ollama-search = {
type = "local";
command = [
"uv" "run" "/home/rrix/.local/share/ollama-web-search-mcp.py"
];
environment = {
OLLAMA_API_KEY = "{env:OLLAMA_API_KEY}";
};
enabled = true;
};
server = {
port = 8080;
hostname = "0.0.0.0";
cors = [ "http://last-bank:8080" ];
};
provider.ollama = {
name = "Ollama";
npm = "@ai-sdk/openai-compatible";
options.baseURL = "http://127.0.0.1:11434/v1";
models = {
"glm-5.3:cloud" = { _launch = true; limit = { context = 999000; output = 131072; }; name = "glm-5.3:cloud"; };
"glm-5.3-flash:cloud" = { _launch = true; limit = { context = 999000; output = 131072; }; name = "glm-5.3-flash:cloud"; };
"glm-5.2:cloud" = { _launch = true; limit = { context = 999000; output = 131072; }; name = "glm-5.2:cloud"; };
"deepseek-v4-pro:cloud" = { _launch = true; limit = { context = 1048576; output = 1048576; }; name = "deepseek-v4-pro:cloud"; };
"deepseek-v4-flash:cloud" = { _launch = true; limit = { context = 1048576; output = 1048576; }; name = "deepseek-v4-flash:cloud"; };
"kimi-k2.7-code:cloud" = { _launch = true; limit = { context = 262144; output = 262144; }; name = "kimi-k2.7-code:cloud"; };
"kimi-k3:cloud" = { _launch = true; limit = { context = 262144; output = 262144; }; name = "kimi-k3:cloud"; };
"qwen3-coder-next:cloud" = { _launch = true; limit = { context = 262144; output = 32768; }; name = "qwen3-coder-next:cloud"; };
"qwen3.5:cloud" = { _launch = true; limit = { context = 262144; output = 32768; }; name = "qwen3.5:cloud"; };
"gemma4:31b-cloud" = { _launch = true; limit = { context = 202752; output = 131072; }; name = "gemma4:31b-cloud"; };
};
};
};
web = {
enable = true;
extraArgs = [
"--hostname" "0.0.0.0"
"--port" "8080"
"--cors" "http://virtuous-cassette:8080"
];
environmentFile = pkgs.writeTextFile {
name = "opencode-env";
text = ''
XDG_DATA_HOME=/home/rrix/claude-microvm/share
NIXPKGS_ALLOW_UNFREE=1
NIXPKGS_ACCEPT_ANDROID_SDK_LICENSE=1
'';
};
};
};
home.packages = [
pkgs.tmux
inputs.arcology2go.packages.${pkgs.stdenv.hostPlatform.system}.default
inputs.arcology2go.packages.${pkgs.stdenv.hostPlatform.system}.android-env
pkgs.ollama
pkgs.claude-code
pkgs.pi-coding-agent
pkgs.pi-web
pkgs.uv
];
# Ollama web search MCP server.
# Upstream: https://github.com/ollama/ollama-python/blob/main/examples/web-search-mcp.py
# Vendored and patched locally — see the "ollama web-search MCP server"
# section below for the source and the reason it can't be fetched raw.
# The arcology2go opencode.json references this path in its mcp.ollama-search block.
home.file.".local/share/ollama-web-search-mcp.py".source = ../files/ollama-web-search-mcp.py;
home.file.".local/share/arcology-notes-mcp.py".source = ../files/arcology-notes-mcp.py;
services.ollama.enable = true;
services.ollama.package = pkgs.ollama;
services.ollama.host = "0.0.0.0";
services.pi-web = {
enable = true;
dataDir = "/home/rrix/claude-microvm/pi-web";
piPackage = pkgs.pi-coding-agent;
host = "0.0.0.0";
ollama = {
enable = true;
agentDir = "/home/rrix/claude-microvm/pi-agent";
};
};
services.hermes-agent = {
enable = true;
gateway.enable = true;
# HERMES_HOME lives on the persistent claude-microvm share — the VM
# root is tmpfs, ~/.hermes would not survive a reboot. Create the
# source dir on the host before booting.
hermesHome = "/home/rrix/claude-microvm/hermes";
# OLLAMA_API_KEY now; MATRIX_* (homeserver, token or password,
# allowed users/rooms, recovery key) filled in later — see the
# host prerequisites section.
environmentFiles = [ "/home/rrix/claude-microvm/hermes-env" ];
# Matrix + E2EE: mautrix with encryption extras, built into the
# sealed venv at build time. libolm comes from nixpkgs.
extraDependencyGroups = [ "matrix" ];
extraPackages = [
pkgs.olm
# arcology2go notes CLI — the arcology-notes skill and the
# opencode.json permissions both rely on this being on PATH.
inputs.arcology2go.packages.${pkgs.stdenv.hostPlatform.system}.default
];
settings = {
# Same Ollama cloud configuration as the opencode.json provider
# block above: the VM's local ollama proxies the :cloud models.
providers.ollama = {
api = "http://127.0.0.1:11434/v1";
key_env = "OLLAMA_API_KEY";
transport = "chat_completions";
};
model = {
provider = "custom:ollama";
default = "glm-5.3:cloud";
};
auxiliary = {
vision = { model = "glm-5.3-flash:cloud"; };
compression = { model = "glm-5.3-flash:cloud"; };
title_generation = { model = "glm-5.3-flash:cloud"; };
mcp = { model = "glm-5.3-flash:cloud"; };
};
memory = {
memory_enabled = true;
user_profile_enabled = true;
provider = "holographic";
};
plugins.hermes-memory-store = {
db_path = "/home/rrix/claude-microvm/hermes/memory_store.db";
auto_extract = true;
};
# Risk-gate env for the arcology notes tools; the skill also
# declares these in required_environment_variables.
terminal.env_passthrough = [
"ARCOLOGY_RISK_ENDPOINT"
"ARCOLOGY_RISK_MODEL"
];
matrix = {
require_mention = true;
session_scope = "room";
auto_thread = true;
};
};
# Skills live in HERMES_HOME/skills/. The arcology-notes skill
# wraps the arcology2 notes CLI (env passthrough + fail-closed
# risk gating documented in the skill body); the
# langlemangle-opencode skill overrides the bundled opencode
# skill's session advice so Hermes attaches to the running
# opencode web server instead of spawning a second TUI.
hermesHomeFiles = {
"skills/arcology-notes/SKILL.md" = ../files/arcology-notes-skill.md;
"skills/langlemangle-opencode/SKILL.md" = ../files/opencode-attach-skill.md;
};
mcpServers.ollama-search = {
command = "uv";
args = [
"run"
"/home/rrix/.local/share/ollama-web-search-mcp.py"
];
env.OLLAMA_API_KEY = "\${OLLAMA_API_KEY}";
};
};
# CLI + HERMES_HOME for interactive shells (sibling option to
# services.hermes-agent, not nested in it).
programs.hermes-agent.enable = true;
};
}ollama web-search MCP server
References
This is a vendored, patched copy of Ollama's web-search-mcp.py example. Upstream's script tries to import FastMCP from mcp.server.fastmcp and falls back to a low-level Server API using @server.tool(). Both paths are broken in current mcp SDK releases:
mcp.server.fastmcp.FastMCPwas removed when the SDK reorganized; the high-level class is nowMCPServeratmcp.server.The fallback's
@server.tool()decorator never existed on the low-levelServer— that was a =FastMCP=-only API — so the fallback crashed at import withAttributeError: 'Server' object has no attribute 'tool', which is what opencode surfaced when loading theollama-searchMCP.
This patched copy uses MCPServer directly and drops the broken fallback. Everything else matches upstream. When upstream fixes the import, switch the home.file block above back to pkgs.fetchurl and delete this section.
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "mcp",
# "rich",
# "ollama",
# ]
# ///
"""
MCP stdio server exposing Ollama web_search and web_fetch as tools.
Environment:
- OLLAMA_API_KEY (required): if set, will be used as Authorization header.
"""
from __future__ import annotations
from typing import Any, Dict
from ollama import Client
from mcp.server import MCPServer
client = Client()
def _web_search_impl(query: str, max_results: int = 3) -> Dict[str, Any]:
res = client.web_search(query=query, max_results=max_results)
return res.model_dump()
def _web_fetch_impl(url: str) -> Dict[str, Any]:
res = client.web_fetch(url=url)
return res.model_dump()
app = MCPServer('ollama-search-fetch')
@app.tool()
def web_search(query: str, max_results: int = 3) -> Dict[str, Any]:
"""
Perform a web search using Ollama's hosted search API.
Args:
query: The search query to run.
max_results: Maximum results to return (default: 3).
Returns:
JSON-serializable dict matching ollama.WebSearchResponse.model_dump()
"""
return _web_search_impl(query=query, max_results=max_results)
@app.tool()
def web_fetch(url: str) -> Dict[str, Any]:
"""
Fetch the content of a web page for the provided URL.
Args:
url: The absolute URL to fetch.
Returns:
JSON-serializable dict matching ollama.WebFetchResponse.model_dump()
"""
return _web_fetch_impl(url=url)
if __name__ == '__main__':
app.run()arcology notes MCP server (opencode harness)
A thin stdio MCP shim wrapping the arcology2 notes CLI for the
opencode harness, in the same =MCPServer=/PEP-723 style as the
ollama web-search MCP server. The notes tools print a JSON envelope
on stdout; every tool here takes the verb's options as strings, runs
the CLI, and relays the envelope verbatim — including the risk block,
so the risk classifier's fail-closed gating carries through unchanged.
Hermes does NOT use this shim: harness CLI-wrapping there is a skill, not an MCP. This exists only because opencode's native extension surface for command tools IS stdio MCP.
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "mcp",
# ]
# ///
"""
MCP stdio server wrapping `arcology2 notes` for the opencode harness.
Each tool maps to one verb of the CLI and relays its JSON envelope
verbatim, preserving the risk-classifier gating (which fails closed
when ARCOLOGY_RISK_ENDPOINT is unreachable).
Environment:
- ARCOLOGY_RISK_ENDPOINT: Ollama http://host:port serving the classifier
- ARCOLOGY_RISK_MODEL: classifier model name (default hermes3:8b)
"""
from __future__ import annotations
import json
import os
import shutil
import subprocess
import sys
from typing import Any
from mcp.server import MCPServer
_ARCOLOGY2 = shutil.which("arcology2")
if _ARCOLOGY2 is None:
raise SystemExit("arcology2 binary not found on PATH; install it via the VM's home.packages")
def _run_notes(verb: str, options: list[str]) -> Dict[str, Any]:
cmd = [_ARCOLOGY2, "notes", verb] + options
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
stdout = proc.stdout.strip()
if not stdout:
return {
"ok": False,
"error": {
"code": "cli_failed",
"message": f"arcology2 notes {verb} exited {proc.returncode}: {proc.stderr.strip()}",
},
}
try:
return json.loads(stdout)
except json.JSONDecodeError:
return {
"ok": False,
"error": {"code": "cli_bad_output", "message": stdout[:2000]},
}
def _opt(name: str, value: Any) -> list[str]:
return [name, str(value)] if value not in (None, "") else []
app = MCPServer('arcology-notes')
@app.tool()
def notes_get(id: str, max_body_bytes: int | None = None) -> dict:
"""Fetch a note by org ID: metadata, child ids, links, refs, plaintext body.
The body is risk-gated; a redacted note carries only the echoed id
and the risk block.
"""
opts = ["--id", id] + _opt("--max-body-bytes", max_body_bytes)
return _run_notes("get", opts)
@app.tool()
def notes_search(query: str, mode: str = "combined", limit: int = 20) -> dict:
"""Full-text search over the notes; returns ids, titles and outline paths."""
return _run_notes("search", _opt("--query", query) + _opt("--mode", mode) + _opt("--limit", limit))
@app.tool()
def notes_timeline(from_date: str, to_date: str, kind: str = "both") -> dict:
"""Journal entries and scheduled/deadline notes between two dates (YYYY-MM-DD)."""
return _run_notes("timeline", [f"--from", from_date, f"--to", to_date, "--kind", kind])
@app.tool()
def notes_capture(title: str, body: str = "", tags: str = "", todo: str = "", date: str = "today") -> dict:
"""Append a capture entry to the daily journal and reindex. Returns the new node id."""
opts = [f"--title", title, f"--body", body]
if tags:
opts += ["--tags", tags]
if todo:
opts += ["--todo", todo]
opts += ["--date", date]
return _run_notes("capture", opts)
@app.tool()
def notes_sql(query: str, max_rows: int = 200) -> dict:
"""Run a single read-only SELECT against the notes database. Row output is risk-classified."""
return _run_notes("sql", ["--query", query, "--max-rows", str(max_rows)])
if __name__ == '__main__':
app.run()Hermes Agent
Hermes Agent runs as a home-manager user service under rrix (see the services.hermes-agent block in the VM config above). The flake input provides the module. Why HM and not the NixOS module: the agent and its tools read the virtiofs shares (~/org =, ~/Code/arcology2go, ~/nix) which are owned by rrix, every other service in this VM (opencode-web, pi-web, ollama) is already a user service, and HERMES_HOME needs to live on the persistent claude-microvm share because the VM root is tmpfs. The NixOS module would run hermes as its own system user with no access to any of that. users.users.rrix.linger is set so the gateway survives logout.
Managed-mode caveats apply: hermes setup / hermes config set are blocked; the config comes from settings in the Nix block, secrets come from the env file. Model routing: main conversations on glm-5.3:cloud, cheap auxiliary work (vision, compression, titles, MCP dispatch) on glm-5.3-flash:cloud — the same ollama cloud configuration the opencode.json provider block uses, just through Hermes' named-custom-provider surface at http://127.0.0.1:11434/v1.
Memory uses the holographic provider: local SQLite with FTS5, HRR compositional queries, trust scoring — no external dependencies, and the DB lands on the persistent share next to the rest of the Hermes state.
Matrix gateway
extraDependencyGroups [ "matrix" ]= pulls mautrix (with encryption extras, Linux-only) into the sealed venv at build time; pkgs.olm provides libolm. =MATRIX_E2EE_MODE=required= goes in the env file so the gateway fails closed rather than silently downgrading to plaintext. Credentials (full setup docs) go in the same env file when I get to it — homeserver URL, access token (or user id + password), MATRIX_ALLOWED_USERS and MATRIX_ALLOWED_ROOMS both set for a locked-down private deployment, plus the cross-signing MATRIX_RECOVERY_KEY.
arcology-notes skill
Hermes' extension surface for wrapping an external CLI is a skill, not a config-declared tool: Creating Skills is explicit that capabilities expressible as instructions + shell commands should be skills, and there is no custom_tools block. The arcology2 notes verb set is exactly that — so this wraps it as a SKILL.md installed through hermesHomeFiles, not through another MCP server or JSON tool schema. (An earlier version of the arcology2go tools docs sketched a bogus {"name": ..., "command": [...]} JSON tool schema "for Hermes"; that mechanism does not exist and the docs now point here.)
The skill declares ARCOLOGY_RISK_ENDPOINT / ARCOLOGY_RISK_MODEL in required_environment_variables — on load Hermes registers them for terminal-sandbox passthrough — and settings.terminal.env_passthrough forwards them declaratively. The notes tools themselves fail closed when the risk classifier is unreachable.
---
name: arcology-notes
description: Read, search and capture org-roam notes through the arcology2 notes CLI. ID-addressed, risk-gated, no raw file access.
version: 1.0.0
author: rrix
license: MIT
platforms: [linux]
metadata:
hermes:
tags: [Note-Taking, Org-Mode, Knowledge-Base]
requires_toolsets: [terminal]
required_environment_variables:
- name: ARCOLOGY_RISK_ENDPOINT
prompt: Ollama endpoint (http://host:port) for the risk classifier
help: Must serve the classifier model locally, e.g. http://windows:11434
required_for: gate on note bodies before they leave the machine
- name: ARCOLOGY_RISK_MODEL
prompt: Classifier model name
help: e.g. hermes3:8b
required_for: risk classification model selection
---
# Arcology Notes
Read and write my org-roam notes via the `arcology2 notes` CLI. The
database lives at `~/org/arcology.db` (defaults are fine); tools are
flat CLI invocations and print a JSON envelope
(`{"ok": ..., "data": ..., "risk": ...}`) on stdout.
## When to Use
- Looking up things I wrote down: ideas, project notes, journal entries
- Searching notes before asking me a question about my work or plans
- Capturing something for later — a thought, a TODO, a journal entry
## Rules
- Access notes ONLY through this CLI. Never read files under `~/org`
directly with read_file/terminal tools.
- Tool output is risk-gated: when the `risk` block in the envelope shows
gated/redacted content, do not attempt to route around it.
- IDs are opaque tokens (e.g. `20260929T120000.000001`) — use only what
`notes search` / `notes timeline` / `notes get` gave you.
## Quick Reference
Search returns ids/titles/outline only; fetch bodies with `notes get`:
arcology2 notes search --query "QUERY" --limit 20
Fetch one note (metadata, children, links, refs, plaintext body):
arcology2 notes get --id "ID"
Timeline — scheduled/deadline notes and daily journal entries:
arcology2 notes timeline --from YYYY-MM-DD --to YYYY-MM-DD [--kind journal|scheduled|both]
Capture appends to the daily journal and reindexes; returns the new id:
arcology2 notes capture --title "Title" --body "text" [--tags "a,b"] [--todo TODO]
Read-only SQL (single SELECT, row output risk-classified):
arcology2 notes sql --query "SELECT id, title FROM nodes LIMIT 5"
## Procedure
1. Search first: `arcology2 notes search --query "..."`.
2. Parse the envelope with `python -c 'import json,sys; print(json.load(sys.stdin))'`
or `jq` if needed — the envelope is a single JSON object on stdout.
3. Fetch candidates with `notes get --id ...`; read `data.body`.
4. If `data.body.redacted == true`, the classifier withheld the content —
report that instead of retrying.
5. To save something: `notes capture --title ... --body ...`; echo the
returned `data.node_id` to me so I can reference it later.
## Pitfalls
- An excluded note (`not_found` on an ID you saw before) is
intentionally hidden — treat its existence as unknown.
- `--verbose` attaches a classifier transcript to the `risk` block;
only use it when debugging the classifier itself.
- The CLI gates output when the classifier endpoint is UNREACHABLE
(fails closed). `hermes3:8b` must be reachable at
`$ARCOLOGY_RISK_ENDPOINT`.
## Verification
arcology2 notes search --query "arcology" --limit 3
should emit `{"ok": true, ...}` with results.langlemangle opencode attach skill
The VM already runs a persistent opencode web/API server on port 8080 (opencode web docs, programs.opencode.web in the VM config). The bundled opencode skill's interactive-session advice (`terminal(command="opencode", background=true, pty=true)`) would spawn a second agent with separate sessions/state under the same config — wrong for this machine. This skill shadows the relevant procedure: attach to the running server instead.
---
name: langlemangle-opencode
description: Drive OpenCode on this VM by ATTACHING to the always-running opencode web server on port 8080 — never spawn a second TUI.
version: 1.0.0
author: rrix
license: MIT
platforms: [linux]
metadata:
hermes:
tags: [Coding-Agent, OpenCode, Autonomous]
requires_toolsets: [terminal, process]
---
# OpenCode on the langlemangle VM
OpenCode here is NOT a plain CLI install: a persistent opencode
web/API server runs on `http://127.0.0.1:8080` (systemd user service).
Its sessions, state and provider config are canonical. Spawning a bare
`opencode` TUI would create a parallel session store — do not do that.
## When to Use
- Delegating a coding task to an autonomous worker
- Long-running interactive coding sessions
- PR review workflows
## Quick Reference
One-shot tasks (no attach needed, fine as-is):
opencode run 'Add retry logic to API calls and update tests'
Resume an existing server-side session: pass `--title` so it is listed
in the shared web-ui session list, and work in the server's data dir:
opencode attach http://127.0.0.1:8080
Interactive: start attached TUI in background through the process tool:
terminal(command="opencode attach http://127.0.0.1:8080",
workdir="/home/rrix/Code/claude-projects", background=true, pty=true)
# returns session_id
process(action="submit", session_id="<id>", data="Implement OAuth refresh flow")
process(action="poll", session_id="<id>")
process(action="log", session_id="<id>")
Exit with `process(action="write", session_id="<id>", data="\x03")` or
`process(action="kill")`. `/exit` is NOT a valid opencode command.
## Procedure
1. Verify readiness: `opencode --version` and
`curl -fsS http://127.0.0.1:8080/ >/dev/null && echo up`.
2. One-shot tasks: `opencode run '...'` with `workdir` scoped to the
project under `/home/rrix/Code/claude-projects`.
3. Iterative tasks: background-pty `opencode attach http://127.0.0.1:8080`.
4. Monitor with `process(action="poll"|"log")`.
5. Report files changed, tests, next steps.
## Pitfalls
- NEVER `terminal(command="opencode", ...)` bare — that is a second,
disconnected session store. Always `opencode attach` for interactive.
- The web server is shared; a human may be using the same sessions from
a browser. Summarize rather than monopolize a session.
- `opencode attach` requires the TUI — needs `pty=true`.
- Model/provider config comes from the nix-managed
`~/.config/opencode/opencode.json`; do not edit it at runtime.
## Verification
curl -fsS http://127.0.0.1:8080/ && opencode run 'Respond with exactly: OPENCODE_SMOKE_OK'Host prerequisites
Stuff the Nix module can't do, done once on the host My Homeserver:
mkdir -p /home/rrix/claude-microvm/hermes— virtiofs shareclaude-credentialscovers it;HERMES_HOMEand the holographic memory DB land there.Create the env file, not world-readable:
touch /home/rrix/claude-microvm/hermes-env
chmod 600 /home/rrix/claude-microvm/hermes-env
# now; already set on other systems in /home/rrix/claude-microvm/ollama
OLLAMA_API_KEY=ollama-cloud-key-here
ARCOLOGY_RISK_ENDPOINT=http://windows:11434
ARCOLOGY_RISK_MODEL=hermes3:8b
# later, for the Matrix gateway:
MATRIX_HOMESERVER=https://matrix.example.org
MATRIX_ACCESS_TOKEN=syt_...
MATRIX_E2EE_MODE=required
# MATRIX_USER_ID=@hermes:your-server.org
# MATRIX_PASSWORD=...
MATRIX_ALLOWED_USERS=@rrix:your-server.org
MATRIX_ALLOWED_ROOMS=!abc123:your-server.org
MATRIX_RECOVERY_KEY=EsT...The VM restart picks the rest up:
microvm -r clownon the host.
opencode configuration parity
The HM-generated opencode.json gets the same ollama-search MCP (uv-run, same script — the Hermes mcpServers.ollama-search block above points at it too) plus a bash permission allow-list for arcology2 notes * mirroring the tools docs guidance: the harness gets notes through the narrow tool or not at all. Hermes reaches the same tools through the arcology-notes skill.
pi-web
the pi-web package
References
This is the Nix derivation for pi-web, the web UI for the Pi coding agent. It's built from the upstream GitHub source with buildNpmPackage so that npm install -g is not needed and the whole thing is declarative.
It's derived from ogglord/pi-web-nix (MIT), vendored here so the VM definition pulls in no extra flake input.
It's pinned to 1.202606.0, the last release whose package-lock.json is well-formed. From 1.202606.7 onwards npm emits three nested @earendil-works/pi-coding-agent peer entries (pi-agent-core, pi-ai, pi-tui) without integrity hashes, which nixpkgs' prefetch-npm-deps can't cache, so the offline build can't fetch them. When upstream fixes the lockfile (or nixpkgs' prefetch-npm-deps grows tolerant of missing integrities) this can be bumped.
The derivation keeps the full node_modules (dev dependencies included) so that the @earendil-works/pi-coding-agent peer that pi-web imports at runtime stays available; pruning with =--omit=dev= would strip it and the server crashes on startup. The trade-off is a ~330 MB closure, which is fine for this VM's writable store overlay.
{ lib, buildNpmPackage, fetchFromGitHub, nodejs, makeWrapper, stdenv }:
buildNpmPackage rec {
pname = "pi-web";
version = "1.202606.0";
src = fetchFromGitHub {
owner = "jmfederico";
repo = "pi-web";
rev = "v${version}";
hash = "sha256-w8cMP+nOBO+R1qwjFNSYufL0mSccbnG/trK06jLWJbo=";
};
npmDepsHash = "sha256-vMqDi+8n+geXz8iXZL87bcfkC+ghHMWdY1TIcqwLFgw=";
npmFlags = [ "--legacy-peer-deps" ];
nativeBuildInputs = [ makeWrapper ];
buildPhase = ''
runHook preBuild
npm run build
runHook postBuild
'';
installPhase = ''
runHook preInstall
# Create the main package directory
mkdir -p $out/lib/node_modules/pi-web
cp -r dist $out/lib/node_modules/pi-web/
cp -r node_modules $out/lib/node_modules/pi-web/
cp package.json $out/lib/node_modules/pi-web/
# Copy built plugin workspaces so npm workspace symlinks resolve
# correctly. npm install creates symlinks in node_modules/ pointing
# to plugins/*/. These become dangling when node_modules is relocated
# to $out. Copy plugins/ dir alongside so the relative symlink targets
# exist.
if [ -d plugins ]; then
cp -r plugins $out/lib/node_modules/pi-web/
fi
# Copy bundled plugin JS if present (vite build output includes them)
if [ -d dist/pi-web-plugins ]; then
cp -r dist/pi-web-plugins $out/lib/node_modules/pi-web/dist/
fi
# Delete any remaining dangling symlinks
find $out/lib/node_modules/pi-web -type l ! -exec test -e {} \; -delete 2>/dev/null || true
# Create bin wrappers. The `pi` CLI is resolved at runtime via PATH
# (provided by pkgs.pi-coding-agent in the VM's home-manager profile),
# so we don't hardcode it here.
mkdir -p $out/bin
makeWrapper ${nodejs}/bin/node $out/bin/pi-web \
--add-flags "$out/lib/node_modules/pi-web/dist/cli.js"
makeWrapper ${nodejs}/bin/node $out/bin/pi-web-server \
--add-flags "$out/lib/node_modules/pi-web/dist/server/index.js"
makeWrapper ${nodejs}/bin/node $out/bin/pi-web-sessiond \
--add-flags "$out/lib/node_modules/pi-web/dist/server/sessiond.js"
runHook postInstall
'';
meta = with lib; {
description = "Web UI for persistent Pi Coding Agent sessions";
homepage = "https://pi-web.dev/";
license = licenses.mit;
mainProgram = "pi-web";
platforms = platforms.linux;
};
}pi-web home-manager module
This is a Home Manager module (not a NixOS module). It runs pi-web's session daemon and web server as systemd.user.services under the HM user's systemd manager, so they share the same environment as the user's interactive shell.
The key reason this is a HM module rather than a NixOS module: PI_CODING_AGENT_DIR is set in home.sessionVariables, which HM writes to ~/.config/environment.d/ and which is inherited by both interactive shells and user services. With the previous NixOS system-service design, PI_CODING_AGENT_DIR was only injected into the service unit's Environment, so the user's interactive pi invocations (and any shells pi-web spawned outside that env) used the default ~/.pi/agent and never saw the seeded extensions (pi-plan, @ollama/pi-web-search) or models.json.
Firewall management is not available from a HM module — the consumer NixOS config must open the port itself (the langlemangler MicroVM already sets networking.firewall.enable false=).
{ config, lib, pkgs, ... }:
let
cfg = config.services.pi-web;
ollamaCfg = cfg.ollama;
# ── Extension seeds ────────────────────────────────────────────────
# Pi extensions are npm tarballs that pi loads via `pi install npm:<name>`.
# Declaratively, each entry in `extensions` is unpacked into
# <agentDir>/npm/node_modules/<name> on first boot and registered in
# settings.json's `packages` array. The seed is generic over the list,
# so adding a new extension is just appending to `extensions` below.
#
# `pi-plan` (https://pi.dev/packages/pi-plan) — plan mode for pi:
# read-only exploration with plan-then-execute workflow. Always on.
#
# `pi-direnv` (https://www.npmjs.com/package/pi-direnv) — auto-load direnv
# environment at session start so bash commands have project-specific env
# vars from .envrc. Always on.
#
# `@ollama/pi-web-search` provides web_search/web_fetch tools backed by
# Ollama's APIs; gated on the ollama integration being enabled and the
# webSearchExtension toggle (pinned to 0.0.5 to match `ollama launch`).
piPlanTarball = pkgs.fetchurl {
url = "https://registry.npmjs.org/pi-plan/-/pi-plan-0.1.1.tgz";
hash = "sha512-Mv4AYezZPC0BzCpCmJiTJAm7Ewugav3s1ircQWbMHGvvxQnf6b31mDCgLYyatim4CI9RNY0sg6Kt/A7Ta5rs1g==";
};
piDirenvTarball = pkgs.fetchurl {
url = "https://registry.npmjs.org/pi-direnv/-/pi-direnv-0.1.0.tgz";
hash = "sha512-N+njfllbcKvd5qbtSMS1nP5QTSaqVZSmo8gQk8TCgWefPQidcg+FG6cY79HIJggS9RiI4FjhGyhVX8DY9kcuIA==";
};
piWebSearchTarball = pkgs.fetchurl {
url = "https://registry.npmjs.org/@ollama/pi-web-search/-/pi-web-search-0.0.5.tgz";
hash = "sha256-3sHy25w0N7ybnO1awuE/R2SbuCSaUte4qde/8q4JvJw=";
};
extensions =
[
{ name = "pi-plan"; tarball = piPlanTarball; }
{ name = "pi-direnv"; tarball = piDirenvTarball; }
]
++ lib.optional (ollamaCfg.enable && ollamaCfg.webSearchExtension) {
name = "@ollama/pi-web-search";
tarball = piWebSearchTarball;
};
seedActive = ollamaCfg.enable || extensions != [];
agentDir = ollamaCfg.agentDir;
# ── Ollama integration derivations ─────────────────────────────────
# These reproduce what `ollama launch pi` writes to ~/.pi/agent/, but
# declaratively and persisted under agentDir so they survive the
# MicroVM's ephemeral tmpfs root.
# models.json — declarative and read-only. pi only ever reads this file
# (it reloads on /model), so a nix-store symlink is fine. This is the one
# artifact that MUST stay pinned to the declared Ollama provider config.
modelsJson = pkgs.writeText "pi-models.json" (builtins.toJSON {
providers.ollama = {
api = "openai-completions";
apiKey = "ollama";
baseUrl = ollamaCfg.baseUrl;
models = ollamaCfg.models;
};
});
# settings.json — seeded writable on first boot so pi can update
# defaultModel/theme/lastChangelogVersion at runtime. Ollama fields are
# only included when the ollama integration is on; the `packages` array
# is only included when there are extensions to register.
settingsJson = pkgs.writeText "pi-settings.json" (builtins.toJSON (
lib.optionalAttrs ollamaCfg.enable {
defaultModel = ollamaCfg.defaultModel;
defaultProvider = ollamaCfg.defaultProvider;
lastChangelogVersion = "0.80.6";
theme = "dark";
}
// lib.optionalAttrs (extensions != []) {
packages = map (e: "npm:${e.name}") extensions;
}
));
# One-shot seed script: copies the writable artifacts into the persistent
# agent dir only when they are missing, so user edits / later `pi install`
# runs are preserved across reboots. Delete <agentDir> to re-seed.
# Runs whenever ollama is enabled OR there are extensions to seed.
seedScript = pkgs.writeShellScript "pi-web-seed" ''
set -eu
AGENT_DIR="${toString agentDir}"
mkdir -p "$AGENT_DIR/npm"
if [ ! -e "$AGENT_DIR/settings.json" ]; then
cp ${settingsJson} "$AGENT_DIR/settings.json"
chmod 600 "$AGENT_DIR/settings.json"
fi
if [ ! -e "$AGENT_DIR/auth.json" ]; then
echo '{}' > "$AGENT_DIR/auth.json"
chmod 600 "$AGENT_DIR/auth.json"
fi
${lib.optionalString ollamaCfg.enable ''
# models.json is declarative and read-only — symlink to the nix-store
# path. Replace any stale symlink/file (it's not user-writable state).
rm -f "$AGENT_DIR/models.json"
ln -s ${modelsJson} "$AGENT_DIR/models.json"
''}
${lib.concatMapStrings (e: ''
if [ ! -e "$AGENT_DIR/npm/node_modules/${e.name}" ]; then
mkdir -p "$AGENT_DIR/npm/node_modules/${e.name}"
tar xf ${e.tarball} \
-C "$AGENT_DIR/npm/node_modules/${e.name}" --strip-components=1
chmod -R u+w "$AGENT_DIR/npm/node_modules/${e.name}"
fi
'') extensions}
'';
in {
options.services.pi-web = {
enable = lib.mkEnableOption "pi-web — web dashboard for Pi Coding Agent sessions";
package = lib.mkOption {
type = lib.types.package;
default = pkgs.pi-web;
description = "The pi-web package to use.";
};
port = lib.mkOption {
type = lib.types.port;
default = 8504;
description = "Port for the pi-web web server.";
};
host = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = "host for the pi-web web server to bind to.";
};
dataDir = lib.mkOption {
type = lib.types.path;
default = "${config.home.homeDirectory}/.local/share/pi-web";
description = "State directory for pi-web (projects, plugins). Overrides ~/.pi-web.";
};
# pi binary path — resolved at build time from the system's pi package.
# If pi is not in nixpkgs, the consumer flake provides it via package overlay.
piPackage = lib.mkOption {
type = lib.types.package;
description = "Pi Coding Agent package to put on PATH for pi-web.";
};
# ── Ollama integration ───────────────────────────────────────────
# Declaratively reproduces `ollama launch pi`: writes models.json,
# settings.json, auth.json and installs the @ollama/pi-web-search
# extension into a persistent agent dir pointed at by PI_CODING_AGENT_DIR,
# so pi-web's in-process Pi sessions use the local Ollama instance.
ollama = {
enable = lib.mkEnableOption "Ollama integration for pi-web — declaratively configure the Pi agent to use a local Ollama instance (equivalent to `ollama launch pi`, but persistent and reproducible across VM reboots)";
agentDir = lib.mkOption {
type = lib.types.path;
default = "${config.home.homeDirectory}/.pi/agent";
description = ''
Persistent Pi agent config directory, exposed to interactive shells
and pi-web services via PI_CODING_AGENT_DIR (set in home.sessionVariables).
Must live on a persistent volume since the MicroVM root is tmpfs.
'';
};
baseUrl = lib.mkOption {
type = lib.types.str;
default = "http://127.0.0.1:11434/v1";
description = "Ollama OpenAI-compatible base URL.";
};
models = lib.mkOption {
type = lib.types.listOf lib.types.attrs;
default = [
{ id = "glm-5.2:cloud"; contextWindow = 1000000; input = [ "text" ]; }
];
description = ''
Model entries exposed under the `ollama` provider in models.json.
Each attrset is serialized verbatim; see pi's models.md for fields
(id, contextWindow, input, reasoning, …).
'';
};
defaultModel = lib.mkOption {
type = lib.types.str;
default = "glm-5.2:cloud";
description = "Default model id; must match one of `models`.";
};
defaultProvider = lib.mkOption {
type = lib.types.str;
default = "ollama";
description = "Default provider id.";
};
webSearchExtension = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Install the @ollama/pi-web-search extension (web_search/web_fetch tools via Ollama).
Controls one entry in the module's shared `extensions` seed list, which also
always includes `pi-plan` (https://pi.dev/packages/pi-plan). Only takes effect
when `services.pi-web.ollama.enable` is true.
'';
};
};
};
config = lib.mkIf cfg.enable {
# ── Environment ───────────────────────────────────────────────────
# PI_CODING_AGENT_DIR is set in home.sessionVariables so that BOTH
# interactive shells AND systemd user services inherit it. HM writes
# sessionVariables to ~/.config/environment.d/ which systemd imports
# into the user manager environment. This is the key fix: with the
# old NixOS system-service design this var only existed in the service
# unit's Environment, so the user's interactive `pi` (and any shell
# pi-web spawned outside that env) used the default ~/.pi/agent and
# never saw the seeded extensions or models.json.
home.sessionVariables = lib.optionalAttrs seedActive {
PI_CODING_AGENT_DIR = toString agentDir;
};
# ── Seed (extensions + ollama settings) ────────────────────────────
# Runs whenever ollama is enabled OR there are extensions to seed.
# Writes settings.json/auth.json (once), symlinks models.json (ollama),
# and unpacks each extension tarball into <agentDir>/npm/node_modules/<name>
# (once). Replaces the old NixOS tmpfiles + oneshot service combo.
systemd.user.services.pi-web-seed = lib.mkIf seedActive {
Unit = {
Description = "pi-web seed — seed Pi agent config and extensions if missing";
After = [ "default.target" ];
};
Service = {
Type = "oneshot";
ExecStart = seedScript;
RemainAfterExit = true;
# tar/chmod/mkdir/cp/echo/rm/ln live in nix store bins, not on the
# user's minimal PATH — make them explicit.
Path = lib.makeBinPath [ pkgs.gnutar pkgs.coreutils ];
};
Install = {
WantedBy = [ "default.target" ];
};
};
# ── Session daemon ────────────────────────────────────────────────
systemd.user.services.pi-web-sessiond = {
Unit = {
Description = "pi-web session daemon — owns Pi session runtimes";
After = [ "default.target" ]
++ lib.optionals seedActive [ "pi-web-seed.service" ];
Requires = lib.optionals seedActive [ "pi-web-seed.service" ];
};
Service = {
ExecStart = "${cfg.package}/bin/pi-web-sessiond";
Restart = "on-failure";
RestartSec = 5;
Environment = [
"PI_WEB_DATA_DIR=${cfg.dataDir}"
"NODE_ENV=production"
# Point npm at the user's writable global prefix and put it on PATH
# so pi extensions (npm install -g) and globally-installed tools work.
"NPM_CONFIG_PREFIX=${config.home.homeDirectory}/.npm-global"
"PATH=${config.home.homeDirectory}/.npm-global/bin:${cfg.piPackage}/bin:/run/current-system/sw/bin:/usr/bin:/bin"
] ++
lib.optionals seedActive [
"PI_CODING_AGENT_DIR=${toString agentDir}"
];
# These hardening options are root-only and not available for user
# services — PrivateTmp/NoNewPrivileges/RestrictNamespaces are dropped.
};
Install = {
WantedBy = [ "default.target" ];
};
};
# ── Web/API server ────────────────────────────────────────────────
systemd.user.services.pi-web-web = {
Unit = {
Description = "pi-web web server — Fastify HTTP + WebSocket proxy";
After = [ "pi-web-sessiond.service" ]
++ lib.optionals seedActive [ "pi-web-seed.service" ];
Requires = [ "pi-web-sessiond.service" ]
++ lib.optionals seedActive [ "pi-web-seed.service" ];
};
Service = {
ExecStart = "${cfg.package}/bin/pi-web-server";
Restart = "on-failure";
RestartSec = 5;
Environment = [
"PI_WEB_PORT=${toString cfg.port}"
"PI_WEB_HOST=${toString cfg.host}"
"PI_WEB_DATA_DIR=${cfg.dataDir}"
"NODE_ENV=production"
"NPM_CONFIG_PREFIX=${config.home.homeDirectory}/.npm-global"
"PATH=${config.home.homeDirectory}/.npm-global/bin:${cfg.piPackage}/bin:/run/current-system/sw/bin:/usr/bin:/bin"
];
};
Install = {
WantedBy = [ "default.target" ];
};
};
};
}opencode with no-AVX2 bun
The langlemangler MicroVM's vCPU does not expose AVX2. nixpkgs' bun ships the stock bun-linux-x64.zip which is compiled assuming AVX2 and crashes with SIGILL (illegal instruction) on hosts like My Homeserver. Bun publishes a bun-linux-x64-baseline.zip for exactly this case, we just need to use it.
Below are two vendored copies of nixpkgs package definitions: a modified bun that pulls the baseline x86_64 binary, and an opencode that builds against it. They are wired in to the rixpkgs overlay.
the bun-baseline package
References
A modified copy of nixpkgs' pkgs/by-name/bu/bun/package.nix. The only change vs upstream: the x86_64-linux entry in passthru.sources points at bun-linux-x64-baseline.zip. Everything else is unchanged.
{
lib,
stdenvNoCC,
fetchurl,
autoPatchelfHook,
unzip,
installShellFiles,
makeWrapper,
openssl,
writeShellScript,
curl,
jq,
common-updater-scripts,
cctools,
darwin,
rcodesign,
}:
stdenvNoCC.mkDerivation (finalAttrs: {
version = "1.3.13";
pname = "bun";
src =
finalAttrs.passthru.sources.${stdenvNoCC.hostPlatform.system}
or (throw "Unsupported system: ${stdenvNoCC.hostPlatform.system}");
sourceRoot =
{
aarch64-darwin = "bun-darwin-aarch64";
}
.${stdenvNoCC.hostPlatform.system} or null;
strictDeps = true;
nativeBuildInputs = [
unzip
installShellFiles
makeWrapper
]
++ lib.optionals stdenvNoCC.hostPlatform.isLinux [ autoPatchelfHook ];
buildInputs = [ openssl ];
dontConfigure = true;
dontBuild = true;
installPhase = ''
runHook preInstall
install -Dm 755 ./bun $out/bin/bun
ln -s $out/bin/bun $out/bin/bunx
runHook postInstall
'';
postPhases = [ "postPatchelf" ];
postPatchelf =
lib.optionalString stdenvNoCC.hostPlatform.isDarwin ''
'${lib.getExe' cctools "${cctools.targetPrefix}install_name_tool"}' $out/bin/bun \
-change /usr/lib/libicucore.A.dylib '${lib.getLib darwin.ICU}/lib/libicucore.A.dylib'
'${lib.getExe rcodesign}' sign --code-signature-flags linker-signed $out/bin/bun
''
+ lib.optionalString (stdenvNoCC.buildPlatform.canExecute stdenvNoCC.hostPlatform) ''
installShellCompletion --cmd bun \
--bash <(SHELL="bash" $out/bin/bun completions) \
--zsh <(SHELL="zsh" $out/bin/bun completions) \
--fish <(SHELL="fish" $out/bin/bun completions)
'';
passthru = {
sources = {
"aarch64-darwin" = fetchurl {
url = "https://github.com/oven-sh/bun/releases/download/bun-v${finalAttrs.version}/bun-darwin-aarch64.zip";
hash = "sha256-VGfj9l26Umuf6pjwzOBO+vwMY+Fpcz7Ce4dqOtMtoZA=";
};
"aarch64-linux" = fetchurl {
url = "https://github.com/oven-sh/bun/releases/download/bun-v${finalAttrs.version}/bun-linux-aarch64.zip";
hash = "sha256-cLrkGzkIsKEg4eWMXIrzDnSvrjuNEbDT/djnh937SyI=";
};
# BASELINE: bun-linux-x64-baseline.zip — no AVX2 required.
# Upstream (AVX2) hash for bun-linux-x64.zip is
# sha256-ecB3H6i5LDOq5B4VoODTB+qZ0OLwAxfHHGxTI3p44lo=
# Baseline digest (from GitHub release SHASUMS) is
# 9d8a24292a7068090205daac0a5a223f5f69736f5287e37bf88d3b4031edc750
"x86_64-linux" = fetchurl {
url = "https://github.com/oven-sh/bun/releases/download/bun-v${finalAttrs.version}/bun-linux-x64-baseline.zip";
hash = "sha256-nYokKSpwaAkCBdqsCloiP19pc29Sh+N7+I07QDHtx1A=";
};
};
updateScript = writeShellScript "update-bun" ''
set -o errexit
export PATH="${
lib.makeBinPath [
curl
jq
common-updater-scripts
]
}"
NEW_VERSION=$(curl --silent https://api.github.com/repos/oven-sh/bun/releases/latest | jq '.tag_name | ltrimstr("bun-v")' --raw-output)
if [[ "${finalAttrs.version}" = "$NEW_VERSION" ]]; then
echo "The new version same as the old version."
exit 0
fi
for platform in ${lib.escapeShellArgs finalAttrs.meta.platforms}; do
update-source-version "bun" "$NEW_VERSION" --ignore-same-version --source-key="sources.$platform"
done
'';
};
meta = {
homepage = "https://bun.sh";
changelog = "https://bun.sh/blog/bun-v${finalAttrs.version}";
description = "Incredibly fast JavaScript runtime, bundler, transpiler and package manager – all in one (baseline / no-AVX2 x86_64 build)";
sourceProvenance = with lib.sourceTypes; [ binaryNativeCode ];
longDescription = ''
All in one fast & easy-to-use tool. Instead of 1,000 node_modules for development, you only need bun.
This variant ships the x86_64-linux *baseline* binary which does not
require AVX2, for hosts (e.g. some MicroVMs) whose CPU does not
expose it. All other platforms are identical to upstream nixpkgs bun.
'';
license = with lib.licenses; [
mit # bun core
lgpl21Only # javascriptcore and webkit
];
mainProgram = "bun";
maintainers = with lib.maintainers; [
DAlperin
jk
thilobillerbeck
cdmistman
diogomdp
];
platforms = builtins.attrNames finalAttrs.passthru.sources;
# Broken for Musl at 2024-01-13, tracking issue:
# https://github.com/NixOS/nixpkgs/issues/280716
broken = stdenvNoCC.hostPlatform.isMusl;
};
})the opencode package
References
We ship a modified copy of nixpkgs' opencode package. The package itself is identical to upstream; opencode already takes bun as a callPackage argument, in rixpkgs it's overridden in the opencode package.