Files
my-pi/pi-permission-system/README.md
T

15 KiB

pi-permission-system logo

@gotgenes/pi-permission-system

npm version CI License: MIT TypeScript pnpm Pi Package

Permission enforcement extension for the Pi coding agent that provides centralized, deterministic permission gates over tool, bash, MCP, skill, and special operations.

Fork notice: This package is a full fork of MasuRii/pi-permission-system, published to npm as @gotgenes/pi-permission-system. It has diverged substantially from upstream in config format, internal architecture, and permission model.

my-pi maintenance note: This directory was imported from upstream tag pi-permission-system-v26.2.1 at commit ec4fdb11343dc94f7185b113e559a4cf9f8dc035. It is loaded from source and maintained directly by my-pi; npm dist output is not used. The local delegation envelope accepts an authorizer allow for built-in read-only path tools (read, find, grep, ls) accessing external_directory, while write, edit, bash, unknown tools, and all path asks still defer to human confirmation.

What It Does

  • Hides disallowed tools before the agent starts — no wasted turns probing for blocked tools
  • Enforces allow / ask / deny at tool-call time with UI confirmation dialogs
  • Reviews unmatched ask requests automatically with an integrated Codex Guardian-style reviewer; only defer reaches the human fallback
  • Applies a separate hard-guard policy first so project rules, session grants, and model review cannot override high-risk denies
  • Controls bash commands with wildcard pattern matching (git *: ask, rm -rf *: deny)
  • Gates MCP and skill access at server, tool, and skill-name granularity
  • Protects sensitive file patterns — cross-cutting path rules deny .env, ~/.ssh/*, etc. across all tools and bash at once, matching both the path as referenced and its symlink-resolved form so a deny cannot be evaded through a symlink alias
  • Guards external paths — prompts before file tools or bash commands reach outside cwd
  • Fails closed — an internal gate error blocks the tool (with a gate_error review-log entry), and an unparseable bash command — or an indirection wrapper that hides the gated command (bash -c/eval, sudo, env, xargs, find -exec, …) — prompts (ask) rather than passing silently
  • Forwards prompts from subagents — ask policies work even in non-UI execution contexts
  • Broadcasts UI prompt events — permissions:ui_prompt fires only when the permission system is about to invoke the active user-facing permission UI
  • Native in-process subagent integration — @gotgenes/pi-subagents and the source-maintained my-pi fork of tintinweb/pi-subagents register child sessions automatically, enabling per-agent policy enforcement and ask-state forwarding to the parent UI without configuration

Install

pi install npm:@gotgenes/pi-permission-system

Quick Start

  1. Create ordinary policy at ~/.pi/agent/extensions/pi-permission-system/policy.jsonc:

    {
      "authorizerChain": ["auto-review"],
      "permission": {
        "*": "ask",
        "read": "allow",
        "bash": {
          "*": "ask",
          "git status*": "allow"
        }
      }
    }
    
  2. Create independent hard guards at ~/.pi/agent/extensions/pi-permission-system/hard-guards.jsonc:

    {
      "version": 1,
      "permission": {
        "*": "allow",
        "*.env": "deny",
        "*.env.*": "deny",
        "*.env.example": "allow",
        "~/.ssh/**": "deny"
      }
    }
    
  3. Restart Pi or run /reload. Ordinary ask requests now reach AutoReview first; only defer opens the human dialog.

The path surface is a cross-cutting gate that applies to all file access — Pi tools, bash commands, MCP calls, and extension tools alike. Extension and MCP tools that operate on paths (via input.path, MCP's input.arguments.path, or a registered access extractor) are gated by default, so a path deny cannot be overridden by a per-tool allow — making it the right place to protect sensitive files like .env or ~/.ssh/* from every tool at once. A path pattern matches both the path as the agent references it and its canonical (symlink-resolved) form, so a deny still fires when a symlink aliases a sensitive target.

For per-tool path patterns (read, write, edit, find, grep, ls), patterns are matched against the file path from input.path. This lets you express rules like "allow reads but deny .env files" at the individual tool level. Like the cross-cutting path surface, per-tool patterns match both the referenced path and its canonical (symlink-resolved) form, so a per-tool deny resists symlink-alias evasion. When Pi's current working directory is known, relative path inputs also match their cwd-normalized absolute form, so src/App.jsx can match both src/* and /workspace/project/*.

The external_directory surface is the CWD-boundary gate: it decides whether reaching outside the working tree is allowed, and accepts a pattern map so you can allow specific outside-CWD directories without opening up all external access. This is the right surface for silencing repeated prompts on a local cache like ~/.cargo/registry — allow it here, not on path:

{
  "permission": {
    "external_directory": {
      "*": "ask",
      "~/.cargo/registry/*": "allow"
    }
  }
}

The trailing * is greedy and crosses subdirectory boundaries, so it allows every file beneath the directory; a bare ~/.cargo/registry matches only the directory entry itself.

Four layers compose with most-restrictive-wins: path (cross-cutting) → external_directory (CWD boundary) → per-tool patterns → bash command patterns. Because ask is more restrictive than allow, a path allow cannot loosen an external_directory: ask boundary — allow outside-CWD directories on external_directory. See docs/configuration.md for the full recipe.

Configuration

The integrated extension uses two independent JSONC files:

Purpose Scope Path
Ordinary policy, runtime knobs, AutoReview Global ~/.pi/agent/extensions/pi-permission-system/policy.jsonc
Ordinary policy override Project <cwd>/.pi/extensions/pi-permission-system/policy.jsonc
Non-overridable hard guards Global only ~/.pi/agent/extensions/pi-permission-system/hard-guards.jsonc

The bundle seeds missing files once with mode 0600 and never overwrites user edits. On first load, an existing legacy global config.json is copied to policy.jsonc; the legacy file is left untouched. Project policy is loaded only for a trusted project. Hard guards are never loaded from the project and are evaluated before ordinary policy and session grants.

Ordinary policy remains flat allow / ask / deny. The bundle default asks for unmatched requests and names the built-in auto-review authorizer. The evaluation order is:

  1. global hard guards (deny is final; allow is only an exception inside that file);
  2. ordinary global/project/per-agent policy and current session grants;
  3. built-in AutoReview for an ordinary ask;
  4. the existing human prompt only when AutoReview returns defer.

autoReview and analysis are regular top-level keys in policy.jsonc. AutoReview defaults to openai-codex/codex-auto-review, low reasoning, and a 90-second budget. Model/config/authentication/timeout errors fail safe to defer.

Completed model verdicts (allow and deny) are appended to an owner-only, sanitized count ledger. Rationales, transcripts, and raw tool payloads are not retained. After 500 new verdicts, agent_settled launches one isolated, tool-free background AgentSession using the configured review model. The validated proposal is persisted and delivered as a follow-up in the same conversation. It never edits either policy file automatically: the active agent must present the candidates and wait for the user's explicit selection and confirmation. A pending batch survives session shutdown and resumes in the next session.

For the full policy surface and merge semantics, see docs/configuration.md. The integrated reviewer provenance is recorded in docs/auto-review/UPSTREAM.md.

Upgrading

22.0.0 — project config requires project trust

Project-scoped configuration (the project policy.jsonc and project-agent frontmatter — both permission policy and runtime knobs such as yoloMode) is loaded only when Pi reports the project as trusted. In an untrusted directory, only global config applies; a skip is surfaced with a warning and a project_trust.skipped review-log entry. Grant project trust (or set defaultProjectTrust) to load a project's config. See docs/migration/0644-project-trust-gating.md.

16.0.0 — the bash gate now fails closed

The permission gate fails closed: an internal gate error blocks the tool (with a gate_error review-log entry) instead of running it ungated, and a non-empty bash command that cannot be parsed resolves to ask (sentinel <unparseable-bash-command>) rather than falling through to a permissive top-level *. Commands that previously slipped through silently on the error or empty-parse path now block or prompt.

If you relied on the old permissive behavior for bash, set an explicit permissive bash policy — "bash": { "*": "allow" } — which also suppresses the new startup warning emitted when a top-level "*": "allow" leaves bash ungated.

Documentation

Document Contents
docs/configuration.md Full policy reference, runtime knobs, per-agent overrides, recipes
docs/session-approvals.md Session-scoped rules, pattern suggestions, bash arity table
docs/cross-extension-api.md Cross-extension service accessor, event bus integration, prompt and decision broadcasts
docs/subagent-integration.md Permission forwarding, coexistence with subagent extensions
docs/guides/permission-frontmatter-for-subagent-extensions.md Convention guide for subagent extension authors
docs/opencode-compatibility.md OpenCode compatibility — shared concepts, divergences, porting guide
docs/troubleshooting.md Common issues, diagnostic logging, threat model
docs/migration/legacy-to-flat.md Migration from pre-v2 config layout
docs/migration/strict-config-validation.md Strict config validation (breaking) — rejected configs, and the cross-scope fail-closed clamp
docs/migration/0644-project-trust-gating.md Project-trust gating (breaking) — project config loads only after project trust
docs/migration/0745-prompt-payload-contracts.md Prompt payload contracts (breaking) — the forwarded wire, the ui_prompt broadcast, and the deprecated preview caps
docs/migration/0746-review-log-fields.md Review-log fields (breaking) — message replaced by request facts, and the reviewLogFieldMaxWidth bound

Development

pnpm run check       # Type-check TypeScript (no emit)
pnpm run lint        # Biome + ESLint + lint:md
pnpm run lint:md     # rumdl on README and docs
pnpm run test        # Run tests from ./test
pnpm run test:watch  # Run tests in watch mode

Pre-commit hooks

This project uses prek to run Biome, ESLint, and rumdl on staged files before each commit. Run pnpm install to set up hooks automatically.

Acknowledgments

This project began as a fork of MasuRii/pi-permission-system. Thank you to MasuRii for the original work that made this possible.

Thank you to the OpenCode team for the permission model design that inspired the flat config format and evaluation semantics used in this extension.

License

MIT