feat(pi): add opt-in permission modes - #2664
Open
edithomas wants to merge 4 commits into
Open
Conversation
added 4 commits
August 20, 2026 12:52
Add an optional Pi extension that exposes /fm-permissions off|plan|confirm for the Firstmate Pi primary, defaulting to off so behavior is preserved exactly until a mode is enabled. plan blocks the built-in edit and write tools and any non-read-only bash command while allowing genuinely read-only inspection; confirm requires an interactive approval before the mutating built-in file and shell tools and refuses rather than silently permits when no UI is available. Mode state is session-persisted through Pi appendEntry extension state (restored on resume and reload, fresh sessions start off) and is never written to a global config file; the active mode shows unobtrusively in the Pi footer. The gate is additive and never weakens the existing PreToolUse seatbelts or turn-end guard: a no-block decision lets them run afterward and a block short-circuits before them. Pure classification and mode decisions live in a testable module; docs/permission-modes.md states plainly that this is an advisory harness control, not an OS sandbox.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Implement a clean-room, opt-in Claude Code-inspired permission-mode vertical slice for the Firstmate Pi primary, using only public Pi documentation and observable behavior (no leaked or proprietary Anthropic material).
Build an optional Pi extension (.pi/extensions/fm-permission-modes.ts) that defaults to no behavioral change and exposes /fm-permissions off|plan|confirm. off (the default) preserves current Firstmate behavior exactly. plan blocks the built-in edit and write tools and any non-read-only bash command while allowing genuinely read-only inspection (read, ls, grep, find, and read-only bash). confirm requires an interactive approval before the mutating built-in file and shell tools (edit, write, and non-read-only bash); if no UI is available, refuse rather than silently permit. Read-only bash and read-only tools are not prompted in confirm mode.
Mode state is session-persisted through Pi supported extension state (pi.appendEntry), restored on session resume and reload, fresh sessions start off, and it is never written to a global config file. The active mode shows unobtrusively in the Pi footer (cleared when off). No delegation or second LLM planner is added. Operator documentation (docs/permission-modes.md) states plainly that this is an advisory harness control, not an OS sandbox, because Pi extensions execute with user privileges.
Decisions and tradeoffs: pure command classification and mode decisions are factored into a testable module (.pi/extensions/lib/fm-permission-policy.ts) with a conservative bash read-only classifier (allowlist of known read-only commands plus a blocklist of mutating patterns matched anywhere, defaulting unrecognized commands to non-read-only so plan blocks and confirm prompts rather than silently allowing a possible mutation). The gate is additive and never weakens the existing PreToolUse seatbelts or turn-end guard: a no-block decision returns the same empty no-block result shape the existing turnend-guard uses so Pi runs the existing seatbelts afterward, and a block short-circuits before them; approval in confirm approves only this gate and does not bypass those seatbelts. Only the built-in edit, write, and bash tools are gated; custom tools pass through. The extension is scoped to Pi and does not touch the existing watcher or turn-end semantics or other adapters.
Behavioral tests (tests/fm-permission-modes.test.sh) cover default-off compatibility, read-only plan calls, blocked mutation, confirm allow and deny, noninteractive refusal, and malformed command arguments, plus session-persisted state round-trip and an end-to-end proof against the real Pi ExtensionRunner that the gate is additive and never weakens the existing seatbelts. The new TypeScript files are added to the strict no-emit typecheck (tests/fm-pi-primary-types.test.sh). bin/fm-lint.sh and bin/fm-doc-audience-check.sh must pass.
What Changed
/fm-permissions off|plan|confirmcontrols for Pi with session persistence, footer status, and default-off compatibility.Risk Assessment
✅ Low: Captain, the change is well-bounded and the prior classifier, session-state, and mandatory-test defects are now resolved without introducing new material source risks.
Testing
No baseline test output was supplied; targeted tests passed against Pi 0.84.2, including the real ExtensionRunner additive-seatbelt proof, and the interaction transcript directly demonstrates footer, persistence, prompt, allow, and refusal behavior. No screenshot was captured because this is a terminal Pi extension and the deterministic harness exercised its actual UI API without requiring a credentialed interactive model session; exact displayed status and prompt values are preserved in the JSON artifact. Per phase rules, linting, typechecking/static analysis, and the full suite were not run.
Evidence: Permission-mode interaction transcript
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
🔧 **Review** - 3 issues found → auto-fixed (2) ✅
.pi/extensions/lib/fm-permission-policy.ts:220- Required intent says the classifier must default unrecognized commands to non-read-only and plan must block every non-read-only bash command. This returns true when only the command prefix matches the allowlist, so mutations such asfind . -delete,git branch new, orls; python -c "open('x','w').write('x')"bypass plan and receive no confirm prompt. Validate the complete command and mutating flags atisReadOnlyShellCommand, rather than relying on a leading-command match plus an incomplete blocklist..pi/extensions/fm-permission-modes.ts:146- Required intent says fresh sessions start off. After plan/confirm is selected, a Pisession_startfor/newwith no persisted entry leavesrestoredundefined and never resetsmode, so the previous session's mode leaks into the fresh session. Reset toDEFAULT_PERMISSION_MODEbefore restoring entries, or assignmode = restored ?? DEFAULT_PERMISSION_MODE.tests/fm-permission-modes.test.sh:63- Required intent calls for behavioral coverage and an end-to-end proof against the real Pi ExtensionRunner. Every test usesnode_guard || return 0, so when the globally installed Pi package is absent the entire new suite prints skips and exits successfully; the repository CI lane installs tasks-axi but not Pi. Make the required suite fail when Pi is unavailable or provision a pinned Pi package in its CI lane so these assertions actually execute.🔧 Fix: Harden permission modes and require pinned Pi tests
1 error still open:
.pi/extensions/lib/fm-permission-policy.ts:142- Required criterion says “plan blocks ... any non-read-only bash command” and unrecognized commands must default to non-read-only. The revised allowlist still accepts unrestricted arguments:sed -n 'w out' inputwritesout,fd -x ./mutatorexecutes a program, and attached mutating flags such assort input -ooutputordate -s@0evade the blocklist. These calls bypass both plan blocking and confirm prompting. AtisReadOnlyShellCommand, validate each allowed command’s complete argv/options or remove commands with write/exec capabilities instead of relying on an incomplete mutation blocklist.🔧 Fix: Validate complete shell argv before read-only approval
✅ Re-checked - no issues remain.
✅ **Test** - passed
✅ No issues found.
Inspected the target diff and focused permission-mode test surface against the authoritative intent.Installed CI-pinned Pi 0.84.2 into a disposable worktree directory, then ranFM_PI_PACKAGE_DIR="$PWD/.tmp-no-mistakes/pi/node_modules/@earendil-works/pi-coding-agent" tests/fm-permission-modes.test.sh.Ran anode --input-type=moduleinteraction harness against the real extension, exercising fresh-session off behavior, plan read/mutation decisions, custom-tool passthrough, confirm allow/deny, headless refusal, footer state, persistence, and resume restoration.Validated the generated JSON evidence structure, removed the disposable Pi installation, and confirmed the worktree remained clean.✅ **Document** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.