herdr manages sandboxed agent sessions; each session runs pi on the host
with read/write/edit/bash/! tool calls routed into a checkpointed
gondolin QEMU VM. Sage also provides process_list
and process_signal tools for structured VM process inspection, plus
VM-backed file_search and content_search tools for bounded local file
search, backed by a guest-installed sage-fff wrapper around FFF. Web access
is delegated to
pi-web-access, which registers
web_search for discovery/current information and fetch_content for exact
HTTP(S) page contents. Oversized tool output is captured by
@spences10/pi-context
into a local SQLite sidecar, then retrieved with context_search,
context_get, and context_export. sage install-pi-packages installs
host-side Pi packages into Pi's package cache. Sage sessions disable global Pi
package discovery and explicitly load only the Sage extension plus the installed
@spences10/pi-context and pi-web-access packages, so unrelated or
previously installed Pi packages do not affect Sage. Override package sources
with SAGE_CONTEXT_PACKAGE or SAGE_WEB_ACCESS_PACKAGE; set either empty to
skip installing that package. Because every local filesystem and process action
executes inside the sandbox, pi can be run fully auto-approved.
Sage mounts two host-backed paths into the VM:
/workspaceis the Sage git worktree and contains deliverable work./scratchis a per-session scratch directory for temporary files, extracted archives, generated logs/fixtures, downloads, and bulky intermediates that should not be merged back.
See the design doc for the full rationale, architecture, and network model:
~/.local/share/kilo/plans/sage-sandboxed-agent.md.
Toolchain installed and the custom guest image builds and boots
successfully (x86_64, AlmaLinux host, QEMU/KVM backend). The image includes git,
SSH, jq, QEMU tooling,
Node/npm/pnpm, pi/gondolin CLIs, Python/pip/uv, Rust/cargo, GCC/G++,
Clang/LLVM/lld, CMake, Ninja, Conan, pkgconf, gdb, and autotools/libtool. A
live pi session routes filesystem/shell tool calls, structured file and
process inspection, and file/content search through the VM. Web discovery and
content extraction are provided by pi-web-access. Large text tool outputs are
stored and retrieved through @spences10/pi-context. VM-local state outside
/workspace is saved to a per-session Gondolin disk checkpoint on shutdown and
restored on reattach; sage remove deletes that checkpoint with the worktree.
Sage also mounts a per-session scratch directory at /scratch; sage remove
deletes Sage's automatic scratch directory too.
Known open items (see plan doc "Risks / open questions" for more):
image/build-config.jsonsets"arch": "x86_64"(matches this authoring machine). Re-checkuname -mon whatever machine actually runsgondolin build— a mismatched arch means the VM won't boot.- Sage uses Gondolin's QEMU backend with the
q35machine type, KVM acceleration, host CPU model, 1 vCPU, and 1G RAM. Gondolin launches QEMU with-nodefaultsand an explicit virtio device set, which keeps the VM close to a minimal VM on hosts whose QEMU packages do not provide themicrovmmachine type. - gondolin's built-in rootfs init mounts a fresh
tmpfsover/root(and/tmp,/var/tmp,/var/cache,/var/log) when a brand-new VM boots. Sage reattach resumes from a per-session disk checkpoint, so VM-local state can survive within that Sage session. Anything baked into/rootduringimage/build.sh(e.g. arustup-installed toolchain under/root/.cargo) is still invisible at first runtime. Install language toolchains via apk packages instead (seerust/cargoinbuild-config.json) or pointCARGO_HOME/RUSTUP_HOMEoutside/root. image/build.shuses Gondolin's Podman container build path sopostBuild.commandscan run withoutsudo. Rootless Podman must be usable on the host.image/build.shfirst compilestools/sage-fffin an Alpine container and copies the resulting binary into the guest image as/usr/local/bin/sage-fff.
- Host support for QEMU/KVM.
- Node.js ≥ 24.15.0.
- pnpm, git, jq, curl.
- herdr (
curl -fsSL https://herdr.dev/install.sh | sh)herdr integration install pi.
- pi installed and on
PATH. - Image builds: rootless Podman usable by the current user. The build runs
inside a container because
postBuild.commandsneed chroot-like privileges. - A running ssh-agent with
SSH_AUTH_SOCKset and your git key loaded, plus the target git host(s) in~/.ssh/known_hosts(for SSH-git egress). Without this, HTTP(S) tool calls still work but SSH git remotes won't.
sage/
├─ packages/pi-sage-sandbox/ # pi extension: routes tools into gondolin
│ ├─ index.ts # extension entry point
│ └─ src/
│ ├─ paths.ts # toGuestPath / shQuote
│ ├─ gondolin-ops.ts # read/write/edit/bash op adapters
│ ├─ file-search.ts # VM-backed path/tree search
│ ├─ content-search.ts # VM-backed content search
│ └─ config.ts # image dir + network policy resolution
├─ packages/pi-sage-memory/ # pi extension: host-side durable memory tools
├─ tools/sage-fff/ # Rust FFF JSON CLI installed in the guest image
├─ image/
│ ├─ build-config.json # gondolin custom image build config
│ └─ build.sh # wrapper around `gondolin build`
├─ herdr-plugin/
│ ├─ herdr-plugin.toml # manifest: "new-session" action
│ └─ bin/new-session.sh # worktree create + launch sandboxed pi
└─ bin/sage # user-facing session manager and internal agent launcher
-
Install the
sagecommand:./install.sh
This links
~/.local/bin/sagetobin/sagein this checkout and installs Node dependencies ifnode_modulesis absent. It also installs Sage's Pi packages into Pi settings so package extensions and skills are both loaded. SetSAGE_SKIP_PI_PACKAGE_INSTALL=1to skip that part. Ensure~/.local/binis onPATH.To install or refresh only the Pi packages:
sage install-pi-packages
-
Install workspace deps manually if you skipped
install.sh:pnpm install
-
Install the prebuilt guest image from GitHub Releases:
sage install-image
By default this downloads:
https://github.com/computerquip/sage/releases/latest/download/sage-gondolin-image-<arch>.tar.gz https://github.com/computerquip/sage/releases/latest/download/sage-gondolin-image-<arch>.tar.gz.sha256Override with
SAGE_RELEASE_REPO,SAGE_IMAGE_VERSION,SAGE_IMAGE_URL, orSAGE_IMAGE_SHA256_URL. -
Or build the custom guest image locally (first compiles
sage-fff, then bakes in git/node/python/rust/C++, pi/gondolin, jq, QEMU tooling, and the search helper):./image/build.sh
Verify the image boots through Sage's QEMU options:
./bin/sage --no-attach
Raw
gondolin exec --vmm qemuuses Gondolin's default QEMU machine type, which may not boot on hosts where Sage'sq35override is required. -
Try it in any git repo:
cd /path/to/some/project sageThis creates a Herdr worktree on a
sage/<timestamp>branch, starts a sandboxed pi agent inside that worktree, boots or resumes a gondolin VM, mounts the worktree read-write at/workspace, mounts session scratch at/scratch, and routes pi tools into the VM. -
List, attach, resume, or remove Sage sessions:
sage list # live + stopped Sage worktrees for this repo sage history # alias for sage list sage attach # attach newest; starts it first if stopped sage attach 2 # attach/resume by list index sage resume 2 # alias for sage attach 2 sage --no-attach # create/start and print attach command sage remove 2 # remove a Sage worktree by list index sage remove # remove the newest Sage worktree
-
Bring work back from a Sage worktree:
sage status # status + diffstat for the newest Sage worktree sage diff # full diff of newest Sage worktree against current branch sage diff --stat 2 # diffstat for the worktree at sage list index 2 sage merge 2 # commit pending Sage edits, then merge branch into cwd sage merge --remove 2 # merge, then remove the Sage worktree sage push 2 # commit pending Sage edits, then push its branch to origin
sage mergerefuses to run if the current checkout has uncommitted changes, so local work is not mixed with agent output by accident. SetSAGE_BASE_REFwhensage diffshould compare against something other than the current branch. -
Wire up the herdr plugin for local dev:
herdr plugin link ./herdr-plugin herdr plugin action list --plugin sage.sandbox herdr plugin action invoke sage.sandbox.new-session
This creates a new git worktree on a
sage/<timestamp>branch as its own herdr workspace, then starts a sandboxed pi agent inside it (visible in the herdr sidebar viaherdr integration install pi).
gondolin does not give the guest a real network stack — the host mediates every outbound connection. Sage's policy:
| Traffic | Policy |
|---|---|
| HTTP/HTTPS search/fetch | wide open (SAGE_HTTP_ALLOWED_HOSTS, defaults to *) |
| SSH-git | allowlist only (SAGE_SSH_HOSTS, defaults to github.com), authenticated via host ssh-agent — keys never enter the guest |
| Raw TCP | off by default; add case-by-case later if a task needs it |
Env vars (all optional):
SAGE_IMAGE_DIR— path to the built gondolin image (default:${XDG_CACHE_HOME:-~/.cache}/sage/gondolin-image; repo-local.gondolin-imageis still used as a fallback for local development).SAGE_IMAGE_INSTALL_DIR— wheresage install-imagewrites the downloaded image (default:${XDG_CACHE_HOME:-~/.cache}/sage/gondolin-image).SAGE_CACHE_DIR— Sage cache directory used to derive the default image path (default:${XDG_CACHE_HOME:-~/.cache}/sage).SAGE_RELEASE_REPO— GitHub repo containing image release assets (default:computerquip/sage).SAGE_IMAGE_VERSION— GitHub release tag to download (default:latest).SAGE_IMAGE_URL/SAGE_IMAGE_SHA256_URL— explicit download URL overrides.SAGE_CONTEXT_PACKAGE— Pi package source for the artifact sidecar (defaultnpm:@spences10/pi-context@0.1.3; empty skipssage install-pi-packages).SAGE_CONTEXT_EXTENSION— explicitpi-contextextension path (default:${PI_CODING_AGENT_DIR:-~/.pi/agent}/npm/node_modules/@spences10/pi-context/dist/index.js).SAGE_CONTEXT_DB— explicitpi-contextSQLite database path. If unset, Sage exportsMY_PI_CONTEXT_DBto${SAGE_CACHE_DIR:-${XDG_CACHE_HOME:-~/.cache}/sage}/context.db.SAGE_MEMORY_EXTENSION— explicit Sage memory extension path (default:$SAGE_HOME/packages/pi-sage-memory/index.ts).SAGE_MEMORY_DISABLE— set to1,true, oryesto disable durable memory.SAGE_MEMORY_DIR— durable memory database directory (default:${SAGE_CACHE_DIR:-${XDG_CACHE_HOME:-~/.cache}/sage}/memory).SAGE_MEMORY_USER_ID/SAGE_MEMORY_AGENT_ID— durable memory scope identifiers (defaults:$USERandsage).SAGE_MEMORY_FASTEMBED_CACHE_DIR— cache directory for the local FastEmbed model (default:${SAGE_CACHE_DIR:-${XDG_CACHE_HOME:-~/.cache}/sage}/memory/fastembed).SAGE_MEMORY_EMBED_MODEL— local FastEmbed embedding model (default:fast-bge-small-en-v1.5, also accepted asBAAI/bge-small-en-v1.5).SAGE_MEMORY_EMBED_DIMENSION— local embedding dimension (default:384; must match the configured model).SAGE_MEMORY_LLM_MAX_TOKENS— maximum output tokens formemory_add infer=trueextraction calls through the selected Pi model (default:2048).SAGE_WEB_ACCESS_PACKAGE— Pi package source for web tools (defaultnpm:pi-web-access@0.13.0; empty skipssage install-pi-packages).SAGE_WEB_ACCESS_EXTENSION— explicitpi-web-accessextension path (default:${PI_CODING_AGENT_DIR:-~/.pi/agent}/npm/node_modules/pi-web-access/index.ts).SAGE_WEB_ACCESS_SKILLS— explicitpi-web-accessskills directory (default:${PI_CODING_AGENT_DIR:-~/.pi/agent}/npm/node_modules/pi-web-access/skills).SAGE_HTTP_ALLOWED_HOSTS— comma-separated HTTP/HTTPS allowlist (default*).SAGE_SSH_HOSTS— comma-separated SSH-git allowlist (defaultgithub.com).SAGE_HOME— repo root, auto-detected from script location if unset.SAGE_AGENT_NAME— Herdr agent name override (default:sage-<timestamp>).SAGE_QEMU_MACHINE_TYPE— QEMU machine-type override (defaultq35; usemicrovmon hosts that support it).SAGE_QEMU_ACCEL— QEMU accelerator override (defaultkvm).SAGE_QEMU_CPU— QEMU CPU model override (defaulthost).SAGE_QEMU_APPEND— kernel cmdline override (default:console=ttyS0 panic=1 reboot=k pci=lastbus=0).SAGE_VM_CPUS— VM vCPU count (default1).SAGE_VM_MEMORY— Gondolin VM memory size (default1G).SAGE_VM_CHECKPOINT— explicit per-session Gondolin checkpoint path. Sage sets this automatically for sessions it starts.SAGE_VM_CHECKPOINT_DIR— directory for automatic per-session checkpoints (default:${SAGE_CACHE_DIR:-${XDG_CACHE_HOME:-~/.cache}/sage}/vm-checkpoints).SAGE_VM_CHECKPOINT_DISABLE— set to1,true, oryesto disable VM checkpoint/resume.SAGE_SCRATCH_DIR— explicit host directory mounted at/scratch. Sage does not delete explicit scratch overrides onsage remove.SAGE_SCRATCH_ROOT— root directory for automatic per-session scratch directories (default:${SAGE_CACHE_DIR:-${XDG_CACHE_HOME:-~/.cache}/sage}/scratch).
Sage loads an in-repo pi-sage-memory extension that provides
memory_status, memory_add, memory_search, memory_get, and
memory_delete. Memory is host-side shared agent state, not VM-routed
filesystem/process state. It is backed by Mem0 OSS with Sage-scoped SQLite
databases under SAGE_MEMORY_DIR.
Memory embeddings are local-only through FastEmbed via Mem0's langchain
embedder adapter. The default embedder is
FastEmbed's default BAAI/bge-small-en-v1.5 at 384 dimensions. First use may
download the FastEmbed model into SAGE_MEMORY_FASTEMBED_CACHE_DIR; after
that, embedding is local CPU inference. Set SAGE_MEMORY_EMBED_MODEL and
SAGE_MEMORY_EMBED_DIMENSION together for another mapped FastEmbed model.
Normal memory_add calls store exactly the provided fact with infer=false,
so no hosted model entitlement is required and no chat model is pulled.
memory_add infer=true uses Pi's currently selected chat model through Pi's
provider/auth routing, including OAuth, API keys, Bedrock, and custom providers.
Durable memory should fail loudly when the local embedder, selected Pi model, or
provider auth is unavailable rather than falling back to hosted embeddings.
herdr worktree remove --workspace <workspace_id> [--force]This runs git worktree remove on the checkout; it never deletes the branch.
When invoked through sage remove, Sage also deletes that session's VM
checkpoint and its automatic /scratch directory.
The worktree is a normal git branch checkout — merge it like any other
branch (git merge sage/<timestamp>, or open a PR) once the sandboxed agent
is done.
Build and package the image:
./image/build.sh
./image/package-release.shimage/build.sh writes to Sage's default image cache unless you pass an
explicit output directory. image/package-release.sh packages that same image
by default.
This creates:
dist/sage-gondolin-image-<arch>.tar.gz
dist/sage-gondolin-image-<arch>.tar.gz.sha256
Upload both files to a GitHub Release. sage install-image downloads the
matching architecture asset and verifies the SHA256 file before replacing the
local image directory.