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.
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: macOS (aarch64), Node ≥ 22.6 (type stripping available — no build step), npm.
- Required host tools:
smolCLI (~/.smol/smol, v≥1.6.13), Podman with a running machine, a local OCI registry atlocalhost:15000(start withpodman run -d --name registry -p 15000:5000 registry:2), Agent Vault running with API at127.0.0.1:14321and MITM proxy at127.0.0.1:14322, andAGENT_VAULT_ORCHESTRATOR_TOKENin 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; shipconfig.example.toml).
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)
- 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 registrylocalhost:15000. - 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 viasmol 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/workhost volume mount, and restarts it. - Agent Vault credential brokering — at session start the extension mints a scoped AV session from the orchestrator token. Each
smol machine execwrites the AV root CA into the guest via a bash prelude heredoc, fuses it into the TLS trust store, and pointsHTTPS_PROXYat 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.
- Input:
nixpkgsfromgithub:NixOS/nixpkgs/nixos-unstable. System:aarch64-linux. - Output:
packages.aarch64-linux.toolboxviapkgs.dockerTools.buildImage— single-layer (buildImage, notbuildLayeredImage) to avoid OCI whiteout/layer-resolution issues in smolvm's crun. Namepi-toolbox, taglatest. contentsincludes (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 sopython3lands 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 withautoPatchelfHook+stdenv.cc.cc.lib,dontStrip = true(it's a bun--compileartifact; 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 bundledskills/skill-datadirectories copied to$out/share/agent-browser. SameautoPatchelfHooktreatment,dontStrip = true. Chromium itself comes from nixpkgs.- Both derivations pin
version+sha256inline with a comment explaining how to bump.
config.Envmust set:PATH=/bin;SSL_CERT_FILE/GIT_SSL_CAINFOpointing at the cacert bundle;FONTCONFIG_PATHto the fontconfigoutpath (Chromium can't find/etc/fontsin 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"].
Behavior contract:
- Flags:
--push/-p(also push to registry),--update/-u(runnix flake updatefirst — 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:latestunder 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/tmpto/private/tmpfor the mount. - Build and extract in ONE container run — each
podman rungets a fresh/nix/store, so build +catof 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, thencatthe 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): checkcurl -sf http://localhost:15000/v2/,podman load, taglocalhost:15000/pi-toolbox:latest,podman push --tls-verify=false, thenpodman rmithe 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/vmsat the end.
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)andpi.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 kindsinfo | 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.
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 neverwait()s). Defaults: 4 cpus, 8192 MiB, storage 20 GiB, overlay 10 GiB (configurable). - Status: parse
smol machine ls --json/smol machine status --name pi-toolboxoutput; classifyrunning | 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
smolinvocations are a thin promise wrapper aroundspawncapturing stdout/stderr + exit code, with optional timeout.
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.
- 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."
- 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). - Track installs: parse the command text (not the output) for
npm install,pip install,go get|install,cargo install,gem installpatterns to feed the session report. - Build the prelude (joined with newlines):
- Base: set
GIT_CONFIG_GLOBALto a guest-local gitconfig and rungit config --global --add safe.directory '*'(fixes "dubious ownership" in/work); create/work/.gocacheand/work/.gopkgmod; exportGOCACHE/GOMODCACHEthere (persists Go caches on the host mount). - CA fusion (AV active):
set -e; verify/etc/ssl/certs/ca-bundle.crtexists; write the AV CA cert to/etc/av-certs/ca.pemvia a single-quoted heredoc with a collision-proof delimiter (if the content contains the delimiter, append_Xrepeatedly);cpthe system bundle to/run/av-merged-ca-bundle.crtand append the AV CA; exportSSL_CERT_FILE,GIT_SSL_CAINFO,CURL_CA_BUNDLE,REQUESTS_CA_BUNDLE,NODE_EXTRA_CA_CERTS,DENO_CERT,NIX_SSL_CERT_FILEto the merged bundle andSSL_CERT_DIR=/etc/ssl/certs. - Chromium NSS prelude: Chromium uses NSS, not OpenSSL —
SSL_CERT_FILEwon't help it. Create a per-call NSS DB at/run/chromium-nss(tmpfs) withcertutil -N(empty password), add only the AV root CA as trust anchor, and exportAGENT_BROWSER_ARGSappended 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 renderenvasexport K="..."lines andfilesasmkdir -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 ($HOMEworks). Validate env keys match^[A-Za-z_][A-Za-z0-9_]*$.
- Base: set
- 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,TERMalways, plus comma-separatedPI_SMOLVM_PASSTHRUVARS. Never forwardAGENT_VAULT_ORCHESTRATOR_TOKEN— if listed, log a refusal and skip it. - 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). - Soft cap: refuse commands after 500 per session.
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.
| 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 |
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
realpathSyncfrom 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 (scopecwd) or inside anreadAllowPathsentry (scopeallow).allowscope is read-only —edit/writeon allow-scoped paths are blocked with a clear message.
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.
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.
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.
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.
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).
- Mint lazily on first bash call:
new AgentVault({ token: orchestratorToken, address: 127.0.0.1:14321 })→vault(vaultName).sessions.create({ ttlSeconds: 604800 }). RequirecontainerConfig(its absence means MITM is disabled server-side — error instructing to re-enable) andexpiresAt. - Cache the session; a valid session is one whose
expiresAtis in the future. Expired or failed session ⇒ preflight fails closed withAgent Vault unavailable: …orAgent 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).
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.
- 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
ensureGoldendetects and reuses/restarts it rather than recreating. - Overlay ENOSPC: surface a remediation hint (stop →
smol machine update --overlay N→ start, or raiseoverlayGb). - AV TTL is 7 days; expiry surfaces in preflight and after failed commands.
- Never forward
AGENT_VAULT_ORCHESTRATOR_TOKENinto the guest under any circumstance — refuse with a log line. - Heredoc delimiters must be collision-proof (content can contain the delimiter — append
_Xuntil unique) and written with single quotes so content is literal. cdprefix stripping must escape the host cwd for the regex (special chars) and only strip when it is a true leadingcd <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-nixdiagnostic 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.
mounts.test.ts(node:test,npm test) must cover: replaces every persisted/workmount and adds cwd; removes duplicate sources once; preserves non-/workmounts; removes a configured source even when its host path no longer exists.tsc --noEmitpasses (strict).- 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 installsomething then confirm it's still present in a later command (overlay persistence); change directory and confirm the machine remounts/workwithoutBadActivate; confirmAGENT_VAULT_ORCHESTRATOR_TOKENis not visible in the guest; confirm a denied-path command is blocked; confirm a fake API key in command output appears as[REDACTED].
- No new runtime dependencies beyond
@infisical/agent-vault-sdkandsmol-toml. - Tool additions must be config-driven (
config.tomlservice 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.