pi-landstrip

Sandbox-aware Pi subagents and interactive command permissions

Packages

Package details

extension

Install pi-landstrip from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:pi-landstrip
Package
pi-landstrip
Version
0.17.40
Published
Jul 27, 2026
Downloads
10.6K/mo · 3,571/wk
Author
jarkkojs
License
Apache-2.0
Types
extension
Size
224.3 KB
Dependencies
3 dependencies · 3 peers
Pi manifest JSON
{
  "extensions": [
    "./dist/index.ts"
  ]
}

Security note

Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.

README

pi-landstrip

Subagents  3 active
├─ ● running  @explore  Design Pi entry parsing
├─ ● running  @general  Design trigger serialization
└─ ● running  @general  Design bounded summaries

pi-landstrip is a Pi extension that provides sandboxed Bash execution, OpenCode-compatible primary agents, and sandbox-aware subagents. It uses an Anthropic-compatible policy and delegates OS-level enforcement to landstrip.

The extension includes a shared sandbox policy and separate agent configuration. Global and trusted-project files can override both.

Process-backed subagents require Pi >= 0.82.0, and Node.js >= 22.19.0.

Installation

Automatic install

pi install npm:pi-landstrip

This installs pi-landstrip and its @landstrip/landstrip dependency. Native binaries are published for Linux, macOS, and Windows on x64 and Arm64.

Manual install

Add the extension to ~/.pi/agent/settings.json (global) or .pi/settings.json (project):

{
  "packages": ["npm:pi-landstrip"]
}

Alternatively, place the extension under ~/.pi/agent/extensions/ (global) or .pi/extensions/ (project). See Pi's extension documentation for details.

On unsupported platforms the extension loads but leaves sandboxing disabled.

On Windows, Pi's Git Bash/MSYS shell cannot run under LPAC. The packaged policy therefore selects standard AppContainer explicitly. This is weaker than LPAC because resources granted to ALL APPLICATION PACKAGES remain visible; /sandbox and the status line report the active backend and mode. There is no silent LPAC-to-standard fallback.

An optional restricted-user backend supports Git Bash without AppContainer. It requires one-time elevated host setup and persistent local accounts and WFP rules; see Windows restricted-user setup.

Disabling

Use the --no-sandbox flag, or set enabled to false in sandbox.json:

{
  "enabled": false
}

When sandboxing is explicitly disabled, subagents still run as separate Pi RPC processes, but without the outer Landstrip OS sandbox. The extension warns once per session. Agent tool permissions still apply, but they are not an OS isolation boundary.

Trusted project config overrides global config except for the Windows backend, which can be selected only in the trusted global file. /sandbox updates a trusted project sandbox file when present, otherwise the global file. Pi versions without a project-trust API use only global configuration.

Behavior

When Pi requests a sandboxed permission, the extension sends a host notification and opens a dialog. The user can allow once, allow for the session, persist for the project or globally, or reject. The dialog shows the exact path or domain being approved.

Project approvals are written to .pi/sandbox.json; global approvals are written to ~/.pi/agent/sandbox.json.

The main agent remains a normal Pi process. pi-landstrip replaces Bash execution, including AI bash calls and manually typed shell commands (! and !!), with a Landstrip-wrapped implementation. Network traffic uses an allowlist proxy when direct access is disabled and the platform policy permits loopback; otherwise it is blocked. Pi's filesystem tools and plugin callbacks remain trusted code outside the Landstrip sandbox.

By default, Bash and subagent processes have no direct network access. Reads are limited to the project, Git configuration, and /dev/null; writes are limited to the project and /dev/null.

Subagent startup adds read-only bootstrap access to the selected Pi runtime, global settings, model/auth configuration, installed plugins, skills, and the task's dedicated session directory. These paths are required to construct a normal Pi worker and are not persisted into sandbox.json. The worker receives write access only to its own session and temporary directories.

Use /sandbox to inspect the active policy and toggle sandboxing. Use /agents to select the primary role, inspect worker configuration and status, and set the global or trusted-project concurrency limit.

Primary agents

The /agents selector provides OpenCode-compatible build and plan roles by default. Build has normal development access; plan asks before shell commands and file changes. The selection controls the root system prompt and permissions and is restored with the session.

Subagents

Landstrip provides an OpenCode-compatible task tool. Each active task runs as a full pi --mode rpc process inside an outer Landstrip sandbox. The sandbox covers Pi, its plugins and tools, model requests, and descendant processes. The root Pi process supervises RPC, permissions, nesting, persistence, and result delivery.

Workers use normal Pi resource discovery and plugin loading, plus the pi-landstrip worker extension. Requested tools are composed inside the worker, so plugins can add or replace implementations. Each task starts a fresh Pi process and plugin instances; continuing a task_id restores its persisted Pi session in a new process.

The tool accepts the OpenCode task fields:

{
  "description": "Review sandbox boundary",
  "prompt": "Review the sandbox implementation and report concrete issues.",
  "subagent_type": "review",
  "task_id": "optional-session-id",
  "command": "optional-originating-command",
  "background": false
}

Foreground tasks return the child result directly. Background tasks return a queued result and deliver completion automatically. Task rows show lifecycle state, current activity, tool-call count, elapsed time, and expandable output. Use /subagents to open an inspector with Agents and Sessions tabs. The Agents tab lists every available subagent type and its built-in, global, or local source. The Sessions tab inspects child transcripts; task rows can open a specific one with /subagents <id>. Completed and failed task metadata remains available after reload, and persisted sessions can be continued with task_id.

Session switching or shutdown stops live workers. After an unclean restart, unfinished work is marked interrupted; completed but undelivered background results are delivered when the root session resumes.

OpenCode-style permissions wrap each worker's composed tools: deny blocks, ask prompts in the root UI, and allow runs the tool. Other workers may continue while a prompt is open. Forwarded worker dialogs identify the agent, task summary, and task ID. An "Allow for this session" decision applies to the root session and its descendants.

Tool permissions cannot widen the outer Landstrip policy. Sandbox approvals continue to use .pi/sandbox.json and ~/.pi/agent/sandbox.json.

Subagents fail closed when sandboxing is expected but unavailable, unsupported, or missing. An explicit --no-sandbox flag or enabled: false configuration is treated as an intentional opt-out and uses the warned unsandboxed process path. Unsupported Pi versions still fail task startup.

Platform behavior

  • Linux: seccomp query traps can grant a blocked filesystem or network operation dynamically, so an approved worker can continue without restart.
  • macOS: Seatbelt policy is fixed when the worker starts. Update sandbox.json, then restart or continue the task in a fresh worker to apply additional filesystem access. Main-agent Bash can retry a command, but there is no live worker-policy update.
  • Windows AppContainer: the packaged policy selects standard AppContainer for Pi's Git Bash/MSYS runtime. With windows.allowLoopback: false (the default), the extension does not start an unreachable proxy and the worker has no network. Setting windows.allowLoopback to true enables the domain proxy, but also gives the AppContainer access to every local loopback service, not only the proxy port. Setting network.allowNetwork to true instead gives the entire worker unrestricted network access; domain lists are then not a boundary. Both opt-ins are explicit security downgrades and produce warnings.
  • Windows restricted user: WFP blocks restricted accounts except for the installed proxy port range, so domain-filtered proxying works without a broad loopback exemption. network.allowNetwork: true leases a separately provisioned unrestricted account. Filesystem grants are journaled and revoked after every process tree exits.

Credentials

Model requests originate inside each worker. Pi's auth.json is readable but not writable by workers. Inherited credential environment variables also cross the sandbox boundary, and plugins loaded in that worker share access to these credentials. Landstrip does not provide a credential broker: load only trusted plugins and use credentials appropriate for the sandboxed process. An OAuth refresh that needs to rewrite auth.json must be completed in the root Pi first.

Configuration

Subagent configuration is read from ~/.pi/agent/subagents.json and, for trusted projects, .pi/subagents.json. Project values override global values; both are merged over the packaged defaults. The packaged subagent types are explore, general, and the OpenCode-compatible scout reconnaissance agent.

Sandbox policy remains in ~/.pi/agent/sandbox.json and .pi/sandbox.json. Pi's settings.json only needs the normal package entry for the extension:

{
  "packages": ["npm:pi-landstrip"]
}

Optional OpenCode import flags live in subagents.json as top-level opencode:

{
  "maxSubagents": 0,
  "opencode": {
    "showGlobalAgents": false,
    "showLocalAgents": false
  },
  "subagents": {
    "agent": {}
  }
}

Both flags default to false. When enabled:

  • showGlobalAgents imports Markdown agents from the OpenCode global config directory ($OPENCODE_CONFIG_DIR, or $XDG_CONFIG_HOME/opencode, or ~/.config/opencode) under agent/ and agents/.
  • showLocalAgents imports Markdown agents from the trusted project's .opencode/agent/ and .opencode/agents/ directories.

OpenCode local agents override OpenCode global agents. Pi agent definitions from subagents.json (built-in, global, and local) take precedence over OpenCode imports with the same name; conflicts are silent. Local OpenCode agents are skipped when the project is untrusted, regardless of the flag.

subagents.json accepts top-level maxSubagents, opencode, and subagents; sandbox fields belong in sandbox.json.

The Windows sandbox fields are:

{
  "windows": {
    "backend": "appContainer",
    "appContainerMode": "standard",
    "allowLoopback": false
  }
}

backend is "appContainer" or "restrictedUser". It is a trusted host setting: only ~/.pi/agent/sandbox.json can change it, and project configuration cannot override it. appContainerMode is "lpac" or "standard"; core Landstrip defaults to LPAC, while the Pi package defaults to standard mode for Git Bash compatibility. allowLoopback is independently opt-in and applies only to AppContainer.

Windows restricted-user setup

Provision the backend once before selecting it in global configuration:

npx @landstrip/landstrip windows setup
npx @landstrip/landstrip windows status

Setup requests UAC elevation and defaults to eight restricted-network accounts, two unrestricted-network accounts, and proxy ports 60080-60111. Run npx @landstrip/landstrip windows setup --help for pool and port options. Then set "backend": "restrictedUser" in ~/.pi/agent/sandbox.json and restart Pi.

The extension checks installation health and reads the configured proxy range before launching a restricted worker. Restricted-user mode does not support windows.allowLoopback or network.allowLocalBinding. Remove the persistent accounts, WFP rules, runner, and recovered per-run ACL grants with:

npx @landstrip/landstrip windows uninstall

A subagent configuration example follows:

{
  "maxSubagents": 2,
  "subagents": {
    "agent": {
      "review": {
        "description": "Review code without modifying it",
        "mode": "subagent",
        "prompt": "Review the requested code and report concrete findings.",
        "permission": {
          "edit": "deny",
          "bash": "ask"
        }
      }
    },
    "permission": {
      "task": {
        "*": "deny",
        "review": "allow"
      }
    }
  }
}

maxSubagents is an integer from 0 through 16 controlling concurrent workers. The packaged default is 0, which removes the task tool while retaining primary roles. There is no separate subagent enable switch. The Settings tab in /agents edits this limit for global and trusted-project configuration.

Agent modes, hidden/disabled agents, prompts, and ordered allow/ask/deny permissions apply to primary agents and subagents. Subagent workers also honor model selection, supported Pi thinking-level variants, and step limits. Primary agent activation currently changes the prompt and permissions only. Later matching permission rules win. Put provider-specific values under options; unknown agent fields are rejected. Agent permissions cannot weaken an enabled OS sandbox, grant filesystem access, or grant network access.

Configuration migration

There is no compatibility loader for previous subagent configuration sources. Legacy keys in settings.json produce a migration warning. Move configuration as follows, then remove the old fields:

  • Move agent and permission from Pi settings.json or OpenCode JSON under subagents.agent and subagents.permission.
  • Convert legacy tools booleans to explicit permission rules.
  • Move Markdown agent prompts into each subagents.agent.<name>.prompt string.
  • Put the worker limit in top-level maxSubagents; zero disables delegation.
  • Move landstrip.opencode flags from Pi settings.json to top-level opencode in subagents.json.
  • Use ~/.pi/agent/subagents.json for global configuration and .pi/subagents.json for trusted project configuration.
  • Leave sandbox policy in ~/.pi/agent/sandbox.json and .pi/sandbox.json; those files are unchanged.

Do not put these fields in Pi's settings.json; pi-landstrip does not read them there.

Plugin API

Pi extensions can discover the active Landstrip runtime through the versioned API exported by pi-landstrip/api:

import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
import { useLandstrip } from 'pi-landstrip/api';

export default function (pi: ExtensionAPI) {
  const dispose = useLandstrip(pi, (landstrip) => {
    const context = landstrip.getContext();
    // Register Landstrip-aware behavior here.
  });
  pi.on('session_shutdown', dispose);
}

Discovery is callback-based so it works regardless of extension load order. Do not wait for discovery from an extension factory: Pi initializes extension factories sequentially. The returned function stops future notifications.

The runtime exposes:

  • getContext() for the host, role, sandbox state, task identity, agent and nesting depth.
  • createBashTool() for extensions that need Landstrip execution with their own tool presentation.
  • prepareProcess() for single-use process launches under the effective sandbox policy.
  • registerWorkerExtension() to load an absolute extension entry in Landstrip subagents. The returned function removes the registration.
  • on() for typed sandbox.changed, subagent.start and subagent.end lifecycle events.

Registered worker extensions are trusted code. Landstrip adds their canonical directories and dependency roots to worker read access, loads them with Pi's --extension option, and propagates them to nested tasks.

Workers receive a base64url JSON LANDSTRIP_CONTEXT environment variable. Extensions can read it without runtime discovery:

import { contextFromEnvironment } from 'pi-landstrip/api';

const context = contextFromEnvironment();

This public context is informational and contains no permission rules, policy, credentials or proxy secrets. Environment values can be spoofed outside a Landstrip worker and must not be treated as authorization.

Generic process preparation fails closed on Windows when sandboxing is disabled. Windows cannot reliably reclaim an unsandboxed descendant tree after its leader has exited; enabled Landstrip launches use an AppContainer job object instead.

Limits

pi-landstrip grants at most maxSubagents scheduler permits to active subagent work and allows nesting to three levels. A foreground parent returns its permit while waiting for a child and reacquires it before resuming, preventing nested-task deadlocks. A worker receives a nested task tool only when its agent has an explicit task permission. Nested tasks are separate Pi processes supervised by the root and Landstrip-wrapped unless sandboxing is explicitly disabled, so an inactive parent process can remain alive during the handoff. Persisted sessions remain resumable by task_id.

License

pi-landstrip is licensed under Apache-2.0. See LICENSE.

The bundled @landstrip/landstrip package is licensed separately as Apache-2.0 AND LGPL-2.1-or-later.