Skip to content

Instantly share code, notes, and snippets.

@mahalel
Created August 1, 2026 02:03
Show Gist options
  • Select an option

  • Save mahalel/c4e984292ff90bd4e1126955515815cd to your computer and use it in GitHub Desktop.

Select an option

Save mahalel/c4e984292ff90bd4e1126955515815cd to your computer and use it in GitHub Desktop.
Rebuild prompt: `smolvm-nix` — pi extension for hardware-isolated bash

Rebuild prompt: smolvm-nix — pi extension for hardware-isolated bash

Copy the block below into a fresh agent session to rebuild this extension from spec. The prompt targets functional equivalence: it encodes architecture, behavioral contracts, invariants, and edge cases; the rebuilding agent derives the code.


Mission

Build a pi extension that runs the coding agent's bash tool inside a persistent hardware-isolated Linux microVM (via the smol CLI), booted from a Nix-built toolbox OCI image, with credentials brokered through Agent Vault so no real secret ever enters the guest. The host working directory is mounted read/write at /work; everything else is sealed off.

You are reconstructing this extension from its specification. Produce a working, behaviorally equivalent implementation. Do not copy source verbatim; derive the code from the contracts below.

Host platform and prerequisites

  • Host: macOS (aarch64), Node ≥ 22.6 (type stripping available — no build step), npm.
  • Required host tools: smol CLI (~/.smol/smol, v≥1.6.13), Podman with a running machine, a local OCI registry at localhost:15000 (start with podman run -d --name registry -p 15000:5000 registry:2), Agent Vault running with API at 127.0.0.1:14321 and MITM proxy at 127.0.0.1:14322, and AGENT_VAULT_ORCHESTRATOR_TOKEN in the host environment.
  • The extension is installed at ~/.pi/agent/extensions/smolvm-nix/; its config lives at ~/.pi/agent/extensions/smolvm-nix/config.toml (gitignored; ship config.example.toml).

Deliverables (file layout)

package.json          # type: module, main: index.ts, deps: @infisical/agent-vault-sdk ^0.7.0, smol-toml ^1.7.0; dev: @types/node
                      # scripts.test = node --test --experimental-strip-types mounts.test.ts
tsconfig.json         # strict ES2022, noEmit, allowImportingTsExtensions, moduleResolution Bundler
index.ts              # the extension (see Component 2/3)
mounts.ts             # pure helper for smol volume-mount argument building (see §Mount reconciliation)
mounts.test.ts        # regression tests for the mount helper (node:test)
toolbox/flake.nix     # Nix flake producing the OCI image
toolbox/flake.lock    # pinned nixpkgs
toolbox/build.sh      # build + push script
config.example.toml   # documented config schema (see §Config)
README.md             # setup, security model, troubleshooting
docs/superpowers/…    # design/plan docs (optional)

Architecture (three components)

  1. Toolbox image — a Nix flake produces a reproducible, pinned OCI image (aarch64-linux) containing the full agent toolset. Built with Podman and pushed to the local registry localhost:15000.
  2. Persistent golden machine — named pi-toolbox. Created once per working directory, started at first bash call, kept running for the whole session. Every bash command runs inside it via smol machine exec --stream. Its overlay disk accumulates npm/pip/go installs across commands. When the working directory changes, the extension stops the machine, reconciles the /work host volume mount, and restarts it.
  3. Agent Vault credential brokering — at session start the extension mints a scoped AV session from the orchestrator token. Each smol machine exec writes the AV root CA into the guest via a bash prelude heredoc, fuses it into the TLS trust store, and points HTTPS_PROXY at the AV MITM proxy so real credentials are injected on the wire, never into the guest.

Data flow per bash call: host cwd is mounted at /work; the prelude configures trust + proxy + tool config; the guest sends dummy tokens; the AV MITM proxy (reached via TSI reissue at guest 127.0.0.1:14322) swaps in real ones en route.

Egress model (important): the machine is created with --net (full TSI networking) — smol 1.6.13 pulls images inside the VM and this requires --net on first boot. Egress control therefore rests on: the AV proxy as the primary channel (HTTPS_PROXY with the AV session token in its URL userinfo), plus denylist + output scrubber + fail-closed preflight as defense in depth. Document this honestly.

Component 1 — Nix toolbox image

toolbox/flake.nix

  • Input: nixpkgs from github:NixOS/nixpkgs/nixos-unstable. System: aarch64-linux.
  • Output: packages.aarch64-linux.toolbox via pkgs.dockerTools.buildImagesingle-layer (buildImage, not buildLayeredImage) to avoid OCI whiteout/layer-resolution issues in smolvm's crun. Name pi-toolbox, tag latest.
  • contents includes (list is the contract): bashInteractive, coreutils, gnugrep, gnused, findutils, diffutils, file, which, less, curl, wget, gnutar, gzip, unzip, xz, zstd, bind.dnsutils, bind.host, glibc.getent, chromium, xorg.xvfb, fontconfig, git, ripgrep, gawk, jq, sqlite, yq, fd, tree, htop, procps, go, gcc, templ, gh, tea, vja, python3.withPackages (ps: [ps.pip]) (wrapper so python3 lands on PATH), cacert, nss.tools (certutil), tini, plus two custom derivations:
    • hunk — Hunk diff viewer. Prebuilt release binary fetched from GitHub releases (hunkdiff-linux-arm64.tar.gz), wrapped with autoPatchelfHook + stdenv.cc.cc.lib, dontStrip = true (it's a bun --compile artifact; stripping corrupts it). Rationale: building from source via bun2nix fails inside the Podman builder (bun's hardlink install hits EPERM on the container overlayfs).
    • agent-browser — agent-browser CLI. Prebuilt Rust binary extracted from the npm tarball (agent-browser-<version>.tgz), plus bundled skills/skill-data directories copied to $out/share/agent-browser. Same autoPatchelfHook treatment, dontStrip = true. Chromium itself comes from nixpkgs.
    • Both derivations pin version + sha256 inline with a comment explaining how to bump.
  • config.Env must set: PATH=/bin; SSL_CERT_FILE/GIT_SSL_CAINFO pointing at the cacert bundle; FONTCONFIG_PATH to the fontconfig out path (Chromium can't find /etc/fonts in a minimal image); AGENT_BROWSER_EXECUTABLE_PATH=/bin/chromium; AGENT_BROWSER_ARGS=--no-sandbox,--disable-dev-shm-usage (dynamic args like proxy/cert-errors are added per-call by the prelude). config.Cmd = ["/bin/bash"].

toolbox/build.sh

Behavior contract:

  • Flags: --push/-p (also push to registry), --update/-u (run nix flake update first — needs a writable mount), --verbose/-v (--show-trace), --help/-h (prints usage from comments). Exit codes: 0 ok, 1 build/eval failure, 2 environment/preflight failure.
  • Why a container: the host is aarch64-darwin with no linux-builder; run native aarch64-linux Nix inside nixos/nix:latest under the Podman machine, fetching prebuilt store paths from cache.nixos.org.
  • Preflight: verify podman exists and the machine responds; verify the flake dir is under a Podman-machine auto-shared path (/Users, /private/tmp) and warn otherwise; translate a leading /tmp to /private/tmp for the mount.
  • Build and extract in ONE container run — each podman run gets a fresh /nix/store, so build + cat of the store path must happen in the same container. The container script: nix build .#toolbox --no-link --print-out-paths, echo the store path to stderr, then cat the artifact to stdout. Host side: stdout → toolbox.tar (never corrupt it), all Nix stderr → a temp log.
  • Substituter resilience: pass --option download-attempts 25 --option connect-timeout 20 --option stalled-download-timeout 30 — some Fastly-backed cache.nixos.org edges truncate large NAR fetches and Nix needs many range-resume attempts (~12 chunks for a ~90MB NAR). Document that this must not be lowered without retesting.
  • Diagnostics: on failure, diagnose() greps the log for known failure classes and prints targeted hints: undefined variable (suggest checking which channel has the package, with the exact eval command), missing attribute, fixed-output hash mismatch (show specified vs got), network error, generic eval error. Always print the log tail.
  • Push (--push): check curl -sf http://localhost:15000/v2/, podman load, tag localhost:15000/pi-toolbox:latest, podman push --tls-verify=false, then podman rmi the local copies (registry is canonical). If a registry isn't running, print the start command and exit 1.
  • Hygiene: after --update, chmod u+rw flake.lock (container writes it as root); clear the stale ephemeral cache at $HOME/Library/Caches/smolvm/vms at the end.

Component 2 — extension core (index.ts + mounts.ts)

Host API surface (consult pi docs for exact signatures)

The extension exports a default function receiving ExtensionAPI (pi):

  • pi.registerTool({ ...createBashTool(cwd), label: "bash (smolvm-nix)", execute }) — wrap bash so every call goes through preflight + the VM operations.
  • pi.on("user_bash", handler) — return { operations } so the user's own bash UI also runs in the VM.
  • pi.on("tool_call", handler) and pi.on("tool_result", handler) — the file-tool guard and output scrubber (see Component 3).
  • pi.registerCommand("smolvm-nix", { handler }) — diagnostic status command.
  • Context provides ui.notify(message, kind) with kinds info | warning | error.

createBashTool(cwd, { operations }) accepts BashOperations whose exec(command, hostCwd, { onData, signal, timeout }) returns Promise<{ exitCode: number | null }>. onData(Buffer) streams output chunks. Use this contract — do not reimplement the tool layer.

Golden machine lifecycle

Constants: machine name pi-toolbox; default image localhost:15000/pi-toolbox:latest; default smol path ~/.smol/smol; AV API http://127.0.0.1:14321; AV MITM proxy http://127.0.0.1:14322; AV session TTL 604800s; guest CA file /etc/av-certs/ca.pem; merged bundle /run/av-merged-ca-bundle.crt.

  • Create (first use): smol machine create --name pi-toolbox --image <image> --net -v <cwd>:/work -w /work --cpus N --mem M [--storage S] [--overlay O] -- /bin/tini -- /bin/tail -f /dev/null. Tini runs as PID 1 and reaps zombies (orphaned Chromium subprocesses accumulate otherwise because the bare tail workload never wait()s). Defaults: 4 cpus, 8192 MiB, storage 20 GiB, overlay 10 GiB (configurable).
  • Status: parse smol machine ls --json / smol machine status --name pi-toolbox output; classify running | stopped | crashed | not-found.
  • ensureGolden(cwd): cache the last-known golden cwd. If unchanged and running, no-op; if not-found, create+start; if stopped/crashed, start. If the cwd changed: read the current mounts first, then stop, then update the volume, then start (see §Mount reconciliation).
  • All smol invocations are a thin promise wrapper around spawn capturing stdout/stderr + exit code, with optional timeout.

Mount reconciliation (this is the subtle one)

mounts.ts exports a pure function buildWorkMountUpdateArgs(name, persistedMounts, cwd) returning smol machine update arguments: for every persisted mount whose target is /work, emit one --remove-volume <source>:/work (each source exactly once, dedupe); then append --volume <cwd>:/work --workdir /work. Keep all mounts that target something else untouched. Persisted mounts come from the machine's runtime config file agent.config.json in the directory returned by smol machine data-dir --name pi-toolbox (shape { mounts: [{source, target, read_only}] }).

Critical sequencing: read the mount list before smol machine stop — smol deletes agent.config.json during stop, and a stale/deleted old mount path makes smol fail at VM startup with BadActivate. If the data directory or config is unreadable, fail closed: report the error, do not append another /work mount. This was the bug that motivated the helper: smol machine update -v <cwd>:/work adds a mount rather than replacing it, so old project paths accumulated.

The bash execution pipeline (per command)

  1. Deny-filter the command first (Component 3); blocked commands throw a message ending with "If this is legitimate, ask the user to adjust the smolvm-nix config."
  2. Sanitize: strip a leading cd <hostCwd> &&|; prefix via a regex that escapes the cwd (pi already cd's the VM, so re-cd'ing is redundant and can fail).
  3. Track installs: parse the command text (not the output) for npm install, pip install, go get|install, cargo install, gem install patterns to feed the session report.
  4. Build the prelude (joined with newlines):
    • Base: set GIT_CONFIG_GLOBAL to a guest-local gitconfig and run git config --global --add safe.directory '*' (fixes "dubious ownership" in /work); create /work/.gocache and /work/.gopkgmod; export GOCACHE/GOMODCACHE there (persists Go caches on the host mount).
    • CA fusion (AV active): set -e; verify /etc/ssl/certs/ca-bundle.crt exists; write the AV CA cert to /etc/av-certs/ca.pem via a single-quoted heredoc with a collision-proof delimiter (if the content contains the delimiter, append _X repeatedly); cp the system bundle to /run/av-merged-ca-bundle.crt and append the AV CA; export SSL_CERT_FILE, GIT_SSL_CAINFO, CURL_CA_BUNDLE, REQUESTS_CA_BUNDLE, NODE_EXTRA_CA_CERTS, DENO_CERT, NIX_SSL_CERT_FILE to the merged bundle and SSL_CERT_DIR=/etc/ssl/certs.
    • Chromium NSS prelude: Chromium uses NSS, not OpenSSL — SSL_CERT_FILE won't help it. Create a per-call NSS DB at /run/chromium-nss (tmpfs) with certutil -N (empty password), add only the AV root CA as trust anchor, and export AGENT_BROWSER_ARGS appended with --ignore-certificate-errors,--proxy-server=http://127.0.0.1:14322. Result: with AV active, Chromium can only browse through the AV MITM proxy — any direct connect fails TLS validation and is proxy-blocked; bypass is architecturally impossible. Without AV: only --ignore-certificate-errors.
    • Service setup (config-driven): for each [agentVault.services] entry render env as export K="..." lines and files as mkdir -p "$(dirname ...)" + single-quoted heredoc writes. {host}, {name}, {user} placeholders render host-side; file contents are written literally; paths/env values get bash double-quote expansion ($HOME works). Validate env keys match ^[A-Za-z_][A-Za-z0-9_]*$.
  5. Run: smol machine exec --stream --name pi-toolbox -w /work [-e K=V …] [--timeout Ns] -- /bin/bash -c "<prelude>\n<sanitised>". Env flags: proxy env from the AV SDK (buildProxyEnv(containerConfig, mergedBundlePath) + SSL_CERT_DIR + NIX_SSL_CERT_FILE), plus passthrough env. Passthrough: HOME, LANG, TERM always, plus comma-separated PI_SMOLVM_PASSTHRUVARS. Never forward AGENT_VAULT_ORCHESTRATOR_TOKEN — if listed, log a refusal and skip it.
  6. Stream: pipe stdout/stderr through the secret scrubber (Component 3) with a 64-byte carry buffer so secrets spanning chunk boundaries still redact; hold stderr in a buffer for failure reporting. Honor the abort signal (SIGKILL the child). On exit: nonzero exits are recorded as failed commands; if nonzero and the AV session expired, append a hint (Agent Vault session may have expired — restart pi).
  7. Soft cap: refuse commands after 500 per session.

Session report

Track {failed: [{command, exitCode, stderr, timestamp}], packages: [{pkg, manager, timestamp}]}. After the second command of the session, print a report via ui.notify: === smolvm-nix session report ===, failed-command previews (truncate >80 chars, first 3 stderr lines), and packages grouped by manager (npm, pip, go, cargo, gem) with a note "consider baking into flake.nix". Also printed on demand by the /smolvm-nix command. Only print if there's something to report.

Component 3 — security layers (defense in depth)

Layer Mechanism
Hardware isolation microVM via smol
Egress clamping --net with AV proxy as primary egress control (see Architecture)
Credential brokering AV MITM — dummy tokens in guest, real secrets on the wire
Read guard path containment to cwd + readAllowPaths
Denylist pattern matching on file-tool paths
Bash command filter token analysis rejecting commands referencing denied paths
Output scrubber regex redaction of secrets from tool output
Secret-file scan advisory preflight warning on secret-like files in cwd

Path containment

  • isInside(parent, child): parent==child or relative path without .. escapes and non-absolute.
  • Symlink resolution: resolve a path to its physical location by walking up realpathSync from the deepest existing ancestor (handles not-yet-existing leaf components).
  • isPathAllowed: expand ~, translate guest paths (/work → host cwd), resolve lexically, then physically; allowed if inside cwd (scope cwd) or inside an readAllowPaths entry (scope allow). allow scope is read-onlyedit/write on allow-scoped paths are blocked with a clear message.

Denylist

Defaults (data contract — include all): .env, .env.*, .envrc, .npmrc, .pypirc, .netrc, .yarnrc, .yarnrc.yml, .gitconfig, .git-credentials, .aws/credentials, .aws/config, .ssh/id_*, .ssh/known_hosts, .ssh/authorized_keys, .ssh/config, .gcp/credentials, .azure/, .kube/config, .docker/config.json, *.pem, *.key, *.p12, *.pfx, *.cer, *.crt, *.keystore, *.jks, id_rsa, id_ed25519, id_ecdsa, id_dsa, credentials, credentials.*, secrets, secrets.*, secrets.yaml, secrets.yml, secrets.json, service-account*.json, service_account*.json, *secret*, *token*, *password*, *apikey*, *api_key*. Config patterns are additive. Matching: patterns containing / are substring matches on the full path; others are glob matches against the basename (*.*, ?., regex-escape the rest). For the bash filter, expand broad patterns (*secret* etc.) into all four slash-position variants. Cache compiled patterns.

Bash command filter

Tokenize the command respecting single/double quotes and whitespace; strip leading redirection prefixes (2>, >&, &>, with optional digit); reject if any token matches a deny pattern.

Output scrubber

Redact these from stdout/stderr (data contract): Anthropic sk-ant-…/sk-…, Google AIza…, AWS AKIA…, GitHub ghp_/github_pat_/ghs_/ghr_, Slack xox[a|b|p|r|s]-…, Stripe sk_live_/sk_test_/rk_live_/rk_test_, PEM private-key blocks (RSA/EC/DSA/OPENSSH/PGP variants), and KEY|TOKEN|SECRET|PASSWORD|API_KEY|APIKEY|PASSWD|PWD=… assignments. Replace matches with [REDACTED]. Honor optional user scrubSecretAllowRegex exemptions (whole-chunk test). Stream-aware: keep a carry of the last 64 bytes between chunks and flush at the end so multi-chunk secrets are caught.

Secret-file scan (advisory, once per session)

Walk cwd with readdirSync (skip node_modules, .git, .hg, .svn, .venv, venv, __pycache__, dist, build, out, target, .next, .nuxt, .cache, coverage, .gradle, .idea, .terraform, vendor; cap at 20 000 files and depth 12), match relative paths against the denylist, and if any match, warn via ui.notify listing up to 10 matches with "… and N more" + truncation note. Advisory only — never blocks.

File-tool guard (tool_call + tool_result)

For tools read, grep, find, ls, edit, write: block on denylist match; block when the path escapes cwd/allowList (with the resolved path in the message); block writes outside cwd. Also hook tool_result as a post-execution backstop: if a file tool somehow returned content for a path that should have been blocked, replace the result with an error message. Scrub grep/read results for secrets (when scrubbing is enabled).

Agent Vault session

  • Mint lazily on first bash call: new AgentVault({ token: orchestratorToken, address: 127.0.0.1:14321 })vault(vaultName).sessions.create({ ttlSeconds: 604800 }). Require containerConfig (its absence means MITM is disabled server-side — error instructing to re-enable) and expiresAt.
  • Cache the session; a valid session is one whose expiresAt is in the future. Expired or failed session ⇒ preflight fails closed with Agent Vault unavailable: … or Agent Vault session expired — restart pi to re-mint.
  • If [agentVault] is absent from config, run in AV-less mode (no CA fusion, no proxy env, Chromium gets only --ignore-certificate-errors).

Config schema (config.toml, TOML — parse with smol-toml)

Top level: smolBin (path), image (OCI ref), resources { cpus, memoryMb, storageGb, overlayGb }, readAllowPaths (string[]), denyPathPatterns (string[]), denyBashReads (bool, default true), scrubSecretOutput (bool, default true), scrubSecretAllowRegex (string[]), [agentVault] { vault, services[] }. Each service: name, host, user?, env? (Record<string,string>), files? [{path, content}]. Ship config.example.toml with every key commented, including the two commented example services (forgejo/tea and vikunja/vja) that illustrate the {host}/{name}/{user} templating. Unknown/missing config must degrade gracefully to defaults. Config is read once at load.

Behaviors, edge cases, and gotchas to preserve

  • Fail closed everywhere: preflight errors mean bash refuses to run with a clear message rather than silently running on the host or in a misconfigured VM.
  • Crash recovery: if pi crashes, the golden machine may still be running; on next load ensureGolden detects and reuses/restarts it rather than recreating.
  • Overlay ENOSPC: surface a remediation hint (stop → smol machine update --overlay N → start, or raise overlayGb).
  • AV TTL is 7 days; expiry surfaces in preflight and after failed commands.
  • Never forward AGENT_VAULT_ORCHESTRATOR_TOKEN into the guest under any circumstance — refuse with a log line.
  • Heredoc delimiters must be collision-proof (content can contain the delimiter — append _X until unique) and written with single quotes so content is literal.
  • cd prefix stripping must escape the host cwd for the regex (special chars) and only strip when it is a true leading cd <cwd> (&&|;) construct.
  • The scrubber's carry buffer must not drop the first 64 bytes of output — emit combined minus the retained tail, keep the tail for the next chunk, flush on close.
  • The golden machine workload is tini+tail — never leave the VM without a reaper.
  • The /smolvm-nix diagnostic command must report: cwd vs golden cwd, smol bin presence, image, egress model, resources, commands used vs the 500 cap, failed/package counts, read-allow list (read-only), guarded tools, env passthru, denylist/scrub toggles, AV vault + services + session state, orchestrator-token presence, and preflight state.

Tests and acceptance criteria

  1. mounts.test.ts (node:test, npm test) must cover: replaces every persisted /work mount and adds cwd; removes duplicate sources once; preserves non-/work mounts; removes a configured source even when its host path no longer exists.
  2. tsc --noEmit passes (strict).
  3. Live smoke test (document, don't automate): with smol + registry + AV running, npm install, ./toolbox/build.sh --push, start pi in a scratch project, run a bash command → it executes in the VM (/work == host cwd, uname -m == aarch64); pip install something then confirm it's still present in a later command (overlay persistence); change directory and confirm the machine remounts /work without BadActivate; confirm AGENT_VAULT_ORCHESTRATOR_TOKEN is not visible in the guest; confirm a denied-path command is blocked; confirm a fake API key in command output appears as [REDACTED].

Constraints

  • No new runtime dependencies beyond @infisical/agent-vault-sdk and smol-toml.
  • Tool additions must be config-driven (config.toml service blocks), never new extension code.
  • The extension must be tool-agnostic: adding a brokered tool is data, not code.
  • Do not commit or push; deliver a working extension in place.
  • If any requirement is ambiguous, choose the safest interpretation (fail closed, least privilege) and document the decision.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment