Skip to content

Extensions and more isolation#76

Open
viktorku wants to merge 4 commits into
cjermain:mainfrom
viktorku:extensions
Open

Extensions and more isolation#76
viktorku wants to merge 4 commits into
cjermain:mainfrom
viktorku:extensions

Conversation

@viktorku
Copy link
Copy Markdown

Hello there! I was looking for some more security/isolation/sandboxing extensions for pi and I liked yours as a starter. Thanks for making this public! 🙏 I made some adjustments in my fork. 😁

So, here goes. We (Qwen, Claude and yours truly) added support for even more isolation and sandboxed security:

  • supports loading any extensions from the host's ~/.pi/agent/extensions (without host corruption wrt cross arch dependencies),
    • in the same vein implements a configurable "security" extension that intercepts both bash and built-in tool commands (all of the currently known) - this takes inspiration from the sandbox example
  • (environment) directory isolation: user-specified directories will not be overridden on the host (again to avoid cross-arch issues, but also avoiding potentially shady packages or common supply chain attacks nowadays)
  • "/dev/null-ing" files like .env* via "mount masking" to control what env vars are exposed to your agent (read and bash tool intercept is not exhaustive enough)

I decided to have the JSONC provide the single source of truth for configuration, but it can be split. Let me know what you think - I got no pressing need to merge any of this, just starting a discussion 🫡


LLM-generated summary

Adds extension support for pi and ships a first-party security extension that enforces read/write/bash rules inside the container, plus host-side mount primitives driven from the same JSONC config.

Why this matters

Guardrails without getting in the way. You can block the agent from touching ~/.ssh, your AWS creds, .env files, or arbitrary sudo/mkfs-class commands, with visible feedback (🔒 security: 5 read, 6 write, 3 bash in the footer, /security to inspect the active rules). Secrets like .env are mount-masked with /dev/null at the container boundary, so bash and child processes can't sneak around the tool-level rules.

Run host projects in the container without corrupting either side. The biggest pain with mounting your project into a Linux container from macOS is that node_modules, .venv, and similar dirs are full of host-built native binaries that don't run on the other arch — and if the container rebuilds them, you've now broken your host toolchain too. This PR introduces per-project overlay dirs: configure isolateDirs and each listed folder is shadowed by a dedicated Linux tree under ~/.pi/agent/overlays/<project-hash>/. The container gets a clean, persistent workspace; your host tree stays untouched. It just works across architectures.

One config, two enforcement layers. In-container rules (what the agent's tools can do) and host-side rules (what Docker will even mount) live in the same security.jsonc, so there's one place to reason about trust.

Also included

  • First-class extension loading: drop a package into ~/.pi/agent/extensions/, pi installs and loads it at startup.
  • Quieter startup — npm's deprecation/audit/funding chatter is suppressed without hiding real failures.
  • macOS build-flag regression fixed.

LLM-generated detailed commit description:

What's in here

Extension support (b57ff60)

  • _docker_flags: read-only mount of ~/.pi/agent/extensions at /host-extensions, tmpfs overlay at /pi-agent/extensions, and per-project overlay dirs (node_modules, .venv) under ~/.pi/agent/overlays/<hash>/ so host-built native binaries don't leak into the Linux container.
  • Dockerfile entrypoint seeds the tmpfs from /host-extensions and runs npm install per extension.
  • tasks/pi/build: fixes empty build-flag handling on macOS.

security extension (dcf1a9f)

  • New package at extensions/security/ (security.ts, config.ts, match.ts) wired via pi.extensions in package.json.
  • Hooks tool_call for read-ish (read/grep/find/ls), write-ish (write/edit), and bash tools, blocking when the path or command matches configured rules.
  • /security slash command prints the active config path and rule breakdown; footer shows 🔒 security: N read, M write, K bash at session start.
  • Rules live in ~/.pi/agent/extensions/security.jsonc (JSONC with comments); globs and re:<regex> both supported.
  • 59 unit tests covering config parsing, path matching, bash matching, session_start, /security, and tool_call hooks.

Quiet npm at startup (d61295a)

  • Entrypoint's per-extension npm install now runs with --loglevel=error --no-audit --no-fund. Drops globalignorefile/python builtin-config warnings (from the base image's npmrc), the node-domexception deprecation (transitive), and the vulnerability/funding summaries. set -e still fails the entrypoint on real install errors.

JSONC-driven host enforcement (a98f30c)

  • Adds filesystem.mountMask and filesystem.isolateDirs to the security config schema.
  • _docker_flags reads them via an inline read_security_config shell function (one-line JSONC stripper using a regex alternation trick — no separate helper file, no new dependency).
  • mountMask binds /dev/null over each listed path (relative to $(pwd)), so bash and child processes can't read secrets like .env even though the extension's denyRead only guards tool calls.
  • isolateDirs replaces the ad-hoc PI_ISOLATE_DIRS space-delimited env var.
  • Config schema, tests, and security.jsonc.example all updated to reflect the two-tier model (in-container rules vs host-side mounts).

Other notes / features

  • mise run pi:build picks up the Dockerfile change; startup is quiet (no npm warn / audit / fund lines).
  • /security inside pi prints the path to ~/.pi/agent/extensions/security.jsonc and correct rule counts.
  • As an example,cat .env inside the container returns empty (mount mask) even though denyRead only blocks the read tool.
  • node_modules and .venv inside the container don't shadow the host's macOS builds.
  • npm run check in extensions/security/ should make 59/59 tests pass.

@cjermain
Copy link
Copy Markdown
Owner

Thanks @viktorku for raising these. I'll review shortly.

@cjermain
Copy link
Copy Markdown
Owner

cjermain commented May 3, 2026

Nice work! These are meaningful additions to discuss. I'd like to incorporate them while staying true to the current design of pi-less-yolo being a lightweight shim.

Extension support

I like the overlay pattern, and I think it can be used to manage the extensions without corrupting host versions. In mise run pi:build, after docker build completes the extensions can be re-installed to match the new image:

for ext_dir in "${HOME}/.pi/agent/extensions/"/*/; do
  [[ -f "${ext_dir}package.json" ]] || continue
  ext_name=$(basename "${ext_dir%/}")
  overlay="${HOME}/.pi/agent/overlays/extensions/${ext_name}/node_modules"
  rm -rf "${overlay}" && mkdir -p "${overlay}"
  echo "Installing ${ext_name} dependencies..."
  "${DOCKER_CMD}" run --rm \
     --user "$(id -u):$(id -g)" \
     --volume "${ext_dir}:/ext:ro" \
      --volume "${overlay}:/ext/node_modules" \
      --workdir /ext \
      "${PI_IMAGE}" sh -c "npm install --loglevel=error --no-audit --no-fund"    done

This avoids the network and performance cost of installing extensions on mise run pi, and keeps with the existing strategy of "build once" for the dependencies. The _docker_flags become:

# Extension node_modules overlays: for each manually-placed extension with a
# package.json, shadow its node_modules with the Linux-compiled overlay built
# during pi:build. If the overlay doesn't exist (extension added after last
# build), the host directory is used as-is.
for ext_dir in "${HOME}/.pi/agent/extensions/"/*/; do
  [[ -f "${ext_dir}package.json" ]] || continue
  ext_name=$(basename "${ext_dir%/}")
  overlay="${HOME}/.pi/agent/overlays/extensions/${ext_name}/node_modules"
  [[ -d "${overlay}" ]] || continue
  DOCKER_FLAGS+=("--volume" "${overlay}:/pi-agent/extensions/${ext_name}/node_modules")
done 

Mount Mask

Most pi-less-yolo features are opt-in via environment variable. Instead of adding a Typescript extension and maintaining it, I think this can be achieved with environment variables.

# PI_MOUNT_MASK: comma-separated paths relative to $(pwd) to mask with
# /dev/null. Useful for .env files and other secrets in the project root.
# Example: PI_MOUNT_MASK=".env,.env.local" mise run pi
if [[ -n "${PI_MOUNT_MASK:-}" ]]; then
  IFS=',' read -ra _mask_entries <<< "${PI_MOUNT_MASK}"
  for rel in "${_mask_entries[@]}"; do
    rel="${rel#"${rel%%[![:space:]]*}"}"  # trim leading whitespace
    [[ -z "${rel}" ]] && continue
    [[ -e "$(pwd)/${rel}" ]] && DOCKER_FLAGS+=("--volume" "/dev/null:$(pwd)/${rel}:ro")
  done
fi 

Users can set it in their shell for permanent use across all projects:

export PI_MOUNT_MASK=".env,.env.local,.env.prod"

Or per-invocation when needed:

PI_MOUNT_MASK=".env" mise run pi

Isolated Directories

Applying the same pattern here makes this an opt-in feature based on PI_ISOLATE_DIRS.

# PI_ISOLATE_DIRS: comma-separated project-relative directories to shadow
# with per-project Linux overlays under ~/.pi/agent/overlays/. Useful for
# node_modules, .venv, and other dirs containing host-compiled binaries
# that won't run in the Linux container.
# Example: PI_ISOLATE_DIRS="node_modules,.venv" mise run pi
if [[ -n "${PI_ISOLATE_DIRS:-}" ]]; then
  _overlay_root=""
  IFS=',' read -ra _isolate_entries <<< "${PI_ISOLATE_DIRS}"
  for rel in "${_isolate_entries[@]}"; do
    rel="${rel#"${rel%%[![:space:]]*}"}"
    [[ -z "${rel}" ]] && continue
    if [[ -z "${_overlay_root}" ]]; then
      project_hash=$(printf '%s' "$(pwd)" | (sha256sum 2>/dev/null || shasum -a 256) | cut -c1-16)
      _overlay_root="${HOME}/.pi/agent/overlays/${project_hash}"
    fi
    mkdir -p "${_overlay_root}/${rel}"
    DOCKER_FLAGS+=("--volume" "${_overlay_root}/${rel}:$(pwd)/${rel}")
  done
fi

This can be exported or set for a single invocation when needed.

In-container deny

I think the in-container deny list is an interesting strategy. However, I think it makes sense for the security Typescript extension to be separate from pi-less-yolo. That keeps this project focused on the container level, allowing users to bring further extensions as needed to make it even more less YOLO. :)

I'm curious what your thoughts are on the above modifications. What do you think?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants