@dieulc/workflow 0.11.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -7
- package/commit-policy.ts +8 -0
- package/edit-policy.ts +12 -3
- package/folder-policy.ts +2165 -1159
- package/index.ts +644 -121
- package/package.json +2 -2
- package/read-policy-redact.ts +359 -319
- package/read-policy-tools.ts +3 -1
- package/read-policy.ts +26 -8
- package/review-runtime.ts +10 -0
- package/review-tools.ts +70 -11
- package/shell-paths.ts +29 -8
- package/utils.ts +98 -0
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ Manage it with `pi list`, `pi update --extensions`, `pi remove npm:@dieulc/workf
|
|
|
45
45
|
| `/commit` | Commit staged changes with an AI-summarized message (`-a`: stage tracked changes first). Confirmation-gated; the confirmation arms the commit exemption for that run |
|
|
46
46
|
| `/commit-policy [never\|ask\|allow] [global\|project]` | View or set the commit policy (default `never`: the agent cannot `git add`/`commit`/`push` — you commit, or use `/commit`) |
|
|
47
47
|
| `/write-policy [allow\|ask\|never] [global\|project]` | View or set the write permission for Default mode (default `allow`: the agent writes freely; `ask` prompts per write; `never` blocks writes — see [Write permission](#write-permission)) |
|
|
48
|
-
| `/folder-policy [repo\|cwd\|parent\|any] [global\|project]` | Shared folder scope for read, write **and** commit (default `repo`: nearest `.git` root, fallback cwd). `outside ask\|block`, `grant project\|global`, `templates allow\|block`, `sensitive` |
|
|
48
|
+
| `/folder-policy [repo\|cwd\|parent\|any] [global\|project]` | Shared folder scope for read, write **and** commit (default `repo`: nearest `.git` root, fallback cwd). `outside ask\|block`, `grant project\|global`, `templates allow\|block`, `sensitive`, `exclude [add\|revoke\|list] <glob> [--global\|--project]` |
|
|
49
49
|
| `/read-policy [status\|allow <path>\|revoke <path>\|shell paths\|strict] [global\|project]` | Read grants, shell analysis mode and the shared folder scope. Sensitive files are always blocked |
|
|
50
50
|
| `/permissions [explain] <command>` | Explain a shell command's verdict without running it: each segment's class, the Plan/Review/Explorer answer and whether the write policy would treat it as a write |
|
|
51
51
|
|
|
@@ -361,15 +361,62 @@ Settings live under `workflow.reviewMode` in `~/.pi/agent/settings.json` (global
|
|
|
361
361
|
|
|
362
362
|
## Folder and read permission
|
|
363
363
|
|
|
364
|
-
**One shared floor.** `workflow.folderPolicy` decides which folders read, write and commit may touch. `mode: "repo"` (default) is the nearest `.git` ancestor (fallback: the session cwd); `cwd` is the exact working directory; `parent` is the parent of that base (siblings); `any` disables containment. `outside: "ask"` prompts once per turn (headless fails closed); `outside: "block"` refuses silently, and an unresolvable write/commit target is refused without a dialog (the read gate keeps its own `readPolicy.shell` opt-in). A policy can only be **stricter** than this floor — `writePolicy: "allow"` cannot write outside it. Both Default and Build respect the floor. For writes it asks only for a *proven* outside target, a sensitive file, or an explicit path that cannot be resolved; a command whose writes merely cannot be located is not treated as outside (see “Not a sandbox”). Windows Git-Bash/Cygwin/WSL spellings (`/c/...`, `/cygdrive/c/...`, `/mnt/c/...`) and `file://` paths are translated to native paths before matching
|
|
364
|
+
**One shared floor.** `workflow.folderPolicy` decides which folders read, write and commit may touch. `mode: "repo"` (default) is the nearest `.git` ancestor (fallback: the session cwd); `cwd` is the exact working directory; `parent` is the parent of that base (siblings); `any` disables containment. `outside: "ask"` prompts once per turn (headless fails closed); `outside: "block"` refuses silently, and an unresolvable write/commit target is refused without a dialog (the read gate keeps its own `readPolicy.shell` opt-in). A policy can only be **stricter** than this floor — `writePolicy: "allow"` cannot write outside it. Both Default and Build respect the floor. For writes it asks only for a *proven* outside target, a sensitive file, or an explicit path that cannot be resolved; a command whose writes merely cannot be located is not treated as outside (see “Not a sandbox”). Windows Git-Bash/Cygwin/WSL spellings (`/c/...`, `/cygdrive/c/...`, `/mnt/c/...`) and `file://` paths are translated to native paths before matching. For **shell commands** on Windows, Git-Bash's `/tmp` is also resolved to the real Git-Bash temp directory (`%LOCALAPPDATA%\Temp`, or a native `TMP`/`TEMP`; detected via Git for Windows or `MSYSTEM`, overridable with `PI_MSYS_TMP`), while direct tool paths keep Node's meaning (`/tmp` is `C:/tmp` there). A folder grant covers the first folder outside the base (the outside project), while a file sitting directly in the common parent is granted as that file only. Read, write and commit dialogs list the base/current folder and every requested target path; a prompt caused by the floor is titled **Folder policy 'ask'**, not “Write policy”.
|
|
365
365
|
|
|
366
366
|
**Sensitive files are always blocked.** The default deny list covers `.env*` (templates such as `.env.example` are exempt by default), `.npmrc`/`.netrc`/`.git-credentials`, `auth.json`, `credentials*`, `secrets.*`, SSH/GPG/cloud stores, key/cert stores, `.git/config`, `.tfstate`/`.tfvars` and keychains. A folder grant never re-enables one; add a manual `folderPolicy.sensitiveExemptions` entry only when a template/fixture is genuinely needed. `/folder-policy sensitive` lists the effective rules.
|
|
367
367
|
|
|
368
|
+
### Excluded folders
|
|
369
|
+
|
|
370
|
+
`folderPolicy.excludePaths` protects a folder you do **not** want the agent to hold standing permission over — typically an OS root or your home directory:
|
|
371
|
+
|
|
372
|
+
```jsonc
|
|
373
|
+
// ~/.pi/agent/settings.json — global is the usual scope here
|
|
374
|
+
{ "workflow": { "folderPolicy": { "excludePaths": ["C:/", "C:/Users/Lenovo"] } } }
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
An exclusion is **scope subtraction, not a secret**, so it differs from `sensitivePatterns` in one decisive way: a more specific approval can re-open a sub-folder.
|
|
378
|
+
|
|
379
|
+
| Target | No grant | Grant on the excluded folder | Grant inside it | Grant above it |
|
|
380
|
+
| --- | --- | --- | --- | --- |
|
|
381
|
+
| the excluded folder itself | **blocked** (no dialog) | allowed | blocked | blocked |
|
|
382
|
+
| a file/folder below it | outside → ask / block | allowed | allowed | outside |
|
|
383
|
+
| the session folder, strictly inside | allowed | allowed | allowed | allowed |
|
|
384
|
+
| the session folder **is** the excluded folder | **blocked** | allowed | blocked | blocked |
|
|
385
|
+
|
|
386
|
+
- The excluded folder itself never prompts and never offers a grant; the refusal names the rule and `exclude revoke`.
|
|
387
|
+
- Below it, normal `outside` rules apply: `outside: "ask"` lets the agent request one specific sub-folder (that approval is a grant inside the exclusion, so it sticks), `outside: "block"` refuses without a dialog.
|
|
388
|
+
- The session folder (and a spawned subagent's locked roots) only rescue automatically when **strictly inside** an exclusion, so opening your home directory as the cwd does not silently hand over the whole home.
|
|
389
|
+
- A grant at or above the excluded root never rescues; dialogs also refuse a one-click grant of a filesystem/drive root, the home directory, a parent of home, or a protected system/profile umbrella (`C:/Windows`, `Program Files`, `ProgramData`, the `AppData` umbrella, `/etc`, `/usr`, `/var`, `/System`, …), so “Always allow” cannot undo the protection by accident. A concrete folder *inside* one of those is still grantable, and a deliberate `/read-policy allow C:/` does re-open the root — that is an explicit user decision.
|
|
390
|
+
- Sensitive-file rules still win inside an approved subtree.
|
|
391
|
+
|
|
392
|
+
Commands (global by default — `--project` opts into `<cwd>/.pi/settings.json`):
|
|
393
|
+
|
|
394
|
+
```
|
|
395
|
+
/folder-policy exclude C:/ --global # add (the bare form adds)
|
|
396
|
+
/folder-policy exclude add ~ --project # add to the project file
|
|
397
|
+
/folder-policy exclude list # effective rules + which files supply them
|
|
398
|
+
/folder-policy exclude revoke C:/ # remove exactly one entry
|
|
399
|
+
/folder-policy exclude "C:/My Documents" # quote paths that contain spaces
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
Scope is **flags only** (`--global` / `--project`). A bare `global`/`project` word is treated as an ordinary argument, so a folder literally named `project` can be excluded and a trailing word can never silently retarget the write; any leftover argument is rejected with the usage line instead of being dropped.
|
|
403
|
+
|
|
404
|
+
Input handling worth knowing:
|
|
405
|
+
|
|
406
|
+
- **Paths with spaces** are supported with quotes (`"…"` or `'…'`); backslashes stay literal, so Windows paths survive. An unbalanced quote is refused, not half-applied.
|
|
407
|
+
- **`C:Personal`** (drive-relative, no separator) is repaired to **`C:/Personal`** on Windows and the message says `(normalised from C:Personal)`. Without that repair a slash-less pattern would be a basename glob that never matches an absolute path — a listed rule that does nothing. Revoking works with either spelling.
|
|
408
|
+
- **Message levels:** a successful write is `info` (and prints the settings file plus the resulting `Active exclusions (N)` list, so a stale list can never look like a contradiction); a refused/ambiguous input or a non-blocking note is `warning`; `error` means the new rule blocks a capability at your session folder.
|
|
409
|
+
- **“Broad root” means an OS/drive/share root or your home directory** (`/`, `C:/`, `//server/share`, `~`). Excluding an ordinary folder such as `C:/Personal` is a precise request and gets no such note. Catch-all patterns (`*`, `**`, `**/*`) are refused; roots are allowed with the note.
|
|
410
|
+
|
|
411
|
+
Global and project `excludePaths` are **unioned**: a project can add an exclusion, never remove a global one. Project settings are only read when the project is trusted (`exclude list` says when it ignored them). When a rule covers the session folder, the warning is **per capability and grant-aware**: read, write and commit are each evaluated with that capability's own `allowedPaths`, so it names only the capabilities that are actually refused (e.g. `Refused at the session folder: write, commit` + `Still covered by a grant: read`). `/folder-policy status` uses the same check.
|
|
412
|
+
|
|
413
|
+
**Workflow settings are user-owned.** Tool writes (`edit`, `write`, shell redirects/`sed -i`) that target the global `~/.pi/agent/settings.json` or the project `.pi/settings.json` are refused in every mode, including Build — otherwise the agent could delete its own exclusions. The slash commands write those files through the filesystem and still work; ask the user to run one, or to edit the file themselves.
|
|
414
|
+
|
|
368
415
|
**Read gate.** `read`, `ls`, `grep`, `find`, pi-lens read tools, `explore` cwd/tasks, local `fetch_content`/`browser_*` file URLs and balanced shell reads (`cat`, `rg`, `git show/-C`, redirections, `cp`/`tar` sources) are path-gated. `grep`/`find`/`ls` output, sensitive git diff sections and truncated `bash` full-output paths are redacted. `readPolicy.shell: "strict"` also asks for inline/interpreter commands that cannot be statically verified.
|
|
369
416
|
|
|
370
|
-
**Grants.** “Always allow” writes to the capability's `allowedPaths` in project settings by default (`folderPolicy.grantScope: "project"`); the project must be trusted. Existing global write grants remain honoured. “Always allow”
|
|
417
|
+
**Grants.** “Always allow” writes to the capability's `allowedPaths` in project settings by default (`folderPolicy.grantScope: "project"`); the project must be trusted. Existing global write grants remain honoured. A dialog may remember a concrete folder — including a drive-top folder such as `C:/tmp` and the real Git-Bash temp dir — but not a filesystem/drive root, the home directory, a parent of home, or a protected system/profile umbrella (`C:/Windows`, `Program Files`, `ProgramData`, the `AppData` umbrella, `/etc`, `/usr`, `/var`, `/System`, …); a concrete folder *inside* one of those is grantable. When a grant cannot be persisted (broad root, or an untrusted project with project scope) the dialog **omits** the “Always allow” row, explains why, and names the deliberate path: `/read-policy allow <target> [global|project]` for reads; write and commit grants go in `workflow.writePolicy.allowedPaths` / `workflow.commitPolicy.allowedPaths` (see `/write-policy status` and `/commit-policy status`). If an approval still arrives, the operation runs **once**, nothing is saved, and a warning says so. “Broad” for an **exclusion** is deliberately different and narrower (only an OS/drive/share root or home) because an exclusion is a narrowing action. Use `/folder-policy any` deliberately to disable the floor for read, write and commit.
|
|
371
418
|
|
|
372
|
-
**Not a sandbox.** Arbitrary code (`node -e`, `python -c`, package scripts), renames of sensitive files and git history are documented residuals. A shell command whose writes cannot be located — `npm run build`, `node script`, `npx`, a `sed`/`awk` program, `powershell`, or a path-less tool in `mutatingTools` — is **not** treated as an outside write: under `writePolicy: "allow"` (and in Build) it runs without a dialog. That is deliberate: the analyzer cannot see inside project code, and asking made ordinary in-repo builds and tests look like a permission violation. Use `writePolicy: "ask"` for a prompt on every mutating call, or an OS container/VM for a hard boundary. The Review Mode reviewer gets guarded built-in `read`/`grep`/`find`/`ls` tools even though its session has no ambient extensions; `review_bash` uses the same
|
|
419
|
+
**Not a sandbox.** Arbitrary code (`node -e`, `python -c`, package scripts), renames of sensitive files and git history are documented residuals. A shell command whose writes cannot be located — `npm run build`, `node script`, `npx`, a `sed`/`awk` program, `powershell`, or a path-less tool in `mutatingTools` — is **not** treated as an outside write: under `writePolicy: "allow"` (and in Build) it runs without a dialog. That is deliberate: the analyzer cannot see inside project code, and asking made ordinary in-repo builds and tests look like a permission violation. Use `writePolicy: "ask"` for a prompt on every mutating call, or an OS container/VM for a hard boundary. The same honesty applies to `excludePaths`: `grep -r` still traverses excluded files before the output is redacted, project-wide tools (`symbol_search`, `project_report`, a path-less `lens_diagnostics`) can surface names or symbols from an excluded folder, and opaque code is not path-analysed — the exclusions stop the agent's *tools* from reading or writing there, not an arbitrary subprocess. The Review Mode reviewer gets guarded built-in `read`/`grep`/`find`/`ls` tools even though its session has no ambient extensions, and inherits the parent's exclusions and read grants; `review_bash` uses the same guard. Spawned subagents receive locked roots plus the parent's deny layer (`PI_FOLDER_POLICY_DENY`) and cannot expand via a sibling project's settings.
|
|
373
420
|
|
|
374
421
|
**Migration.** The default `folderPolicy.mode: "repo"` + `outside: "ask"` also constrains writes/commits. Set `folderPolicy.mode: "any"` to restore the old “write anywhere” behaviour (sensitive files remain blocked); this now also disables the unresolvable-target asks. Use `cwd`/`parent` to keep a narrower floor.
|
|
375
422
|
|
|
@@ -388,6 +435,8 @@ The default exists because prompt-only rules are soft: models habitually commit
|
|
|
388
435
|
|
|
389
436
|
### Modes
|
|
390
437
|
|
|
438
|
+
Exclusions and sensitive files apply to commits too: `git add <sensitive|excluded>`, `git add .`/`-A` when such a path would be included, and a staged sensitive/excluded path before `git commit` are refused. `/commit` bypasses the commit *mode* (with your confirmation), never the sensitive-file or exclusion checks. Per-step auto-commit skips with a notice when the repository — or any dirty path it would stage — is excluded.
|
|
439
|
+
|
|
391
440
|
| Mode | Meaning |
|
|
392
441
|
| --- | --- |
|
|
393
442
|
| `never` (default) | `git add` / `git commit` / `git push` are blocked with a policy reason that tells the model to leave changes uncommitted for review |
|
|
@@ -427,7 +476,7 @@ The extension has three modes — **Plan → Build → Default**. Plan mode is r
|
|
|
427
476
|
| Mode | Meaning |
|
|
428
477
|
| --- | --- |
|
|
429
478
|
| `allow` (default) | The agent writes files freely — today's behaviour, and the shipped default so existing installs are unaffected |
|
|
430
|
-
| `ask` | Writes pop a dialog: **Allow once (this turn)** / **Always allow …** (*this path*, *these paths*, *this folder*, *these folders*, *this command*, *these commands*) / **Deny** / **Deny with instructions…**. One dialog covers a whole turn and lists the writes it covers; a permanent grant is remembered, so the same target stops asking. A prompt caused by the folder floor is titled **Folder policy 'ask'** and shows the base folder; a write-mode prompt is **Write policy 'ask'
|
|
479
|
+
| `ask` | Writes pop a dialog: **Allow once (this turn)** / **Always allow …** (*this path*, *these paths*, *this folder*, *these folders*, *this command*, *these commands*) / **Deny** / **Deny with instructions…**. One dialog covers a whole turn and lists the writes it covers; a permanent grant is remembered, so the same target stops asking. A prompt caused by the folder floor is titled **Folder policy 'ask'** and shows the base folder; a write-mode prompt is **Write policy 'ask'**. If a persistent grant is impossible (a root/home/protected umbrella, or an untrusted project with project scope), the Always row is omitted and an approval applies once with a warning |
|
|
431
480
|
| `never` | The agent may not modify your files (`.pi/plans/` excepted). You apply the changes, or the user switches to `/build` for the approved-execution path |
|
|
432
481
|
|
|
433
482
|
The status line always shows the active permission (`◦ commits:never · writes:ask`), so you can see at a glance what the agent may touch.
|
|
@@ -443,7 +492,8 @@ The status line always shows the active permission (`◦ commits:never · writes
|
|
|
443
492
|
|
|
444
493
|
### Scope and carve-outs
|
|
445
494
|
|
|
446
|
-
- **Default mode only for the write mode.** Plan mode keeps its stricter read-only rules (writes only to `.pi/plans/`), and Build stays the execution escape hatch: it never inherits `writePolicy: ask/never`. The shared folder floor still applies in Build and Default (outside targets and
|
|
495
|
+
- **Default mode only for the write mode.** Plan mode keeps its stricter read-only rules (writes only to `.pi/plans/`), and Build stays the execution escape hatch: it never inherits `writePolicy: ask/never`. The shared folder floor still applies in Build and Default (outside targets, sensitive files and [excluded folders](#excluded-folders)).
|
|
496
|
+
- **Settings files are user-owned.** A write whose target is the global `~/.pi/agent/settings.json` or the project `.pi/settings.json` is refused in every mode, so the agent cannot delete its own exclusions; the slash commands still write them.
|
|
447
497
|
- **Subagents are unaffected.** A spawned child carries `PI_WORKFLOW_ROLE` and is already read-only through the role gate — and it has no UI to answer a prompt with.
|
|
448
498
|
- **`.pi/plans/` writes stay allowed** in every mode, so a session that leaves Plan mode mid-flow is not stranded.
|
|
449
499
|
- **Third-party write tools need opting in.** `ast_grep_replace` (pi-lens) rewrites files, and the `office_*` tools (pi-office-protocol) write documents; neither is covered by the built-in scope. List them in `mutatingTools` to gate them (see below). A path-less mutating tool follows the write mode: no prompt under `allow`/Build, prompted under `ask`.
|
|
@@ -614,6 +664,32 @@ Write Permission settings live under `workflow.writePolicy` (same global/project
|
|
|
614
664
|
|
|
615
665
|
`"writePolicy": "ask"` (a bare mode string) is accepted as shorthand for `{ "mode": "ask" }`. Booleans, numbers and unknown modes fall back to the default. `/write-policy` writes this key read-modify-write, preserving every other settings key; the three list fields are **additive** — a write never drops the entries already stored (remove one by editing the JSON directly).
|
|
616
666
|
|
|
667
|
+
Folder Policy settings live under `workflow.folderPolicy` (scalars project-over-global; `sensitivePatterns`, `sensitiveExemptions` and `excludePaths` are **unioned**, so a trusted project can add a rule but never remove a global one):
|
|
668
|
+
|
|
669
|
+
| Key | Default | Meaning |
|
|
670
|
+
| --- | --- | --- |
|
|
671
|
+
| `mode` | `"repo"` | Base folder: `repo` (nearest `.git`, fallback cwd), `cwd`, `parent` (siblings), `any` (containment off; sensitive files and exclusions still apply) |
|
|
672
|
+
| `outside` | `"ask"` | `ask` = prompt once per turn for a target outside the floor (headless fails closed); `block` = refuse without a dialog |
|
|
673
|
+
| `grantScope` | `"project"` | Where “Always allow” persists grants (`project` \| `global`); a project grant needs a trusted project |
|
|
674
|
+
| `sensitiveTemplates` | `"allow"` | `block` treats `.env.example`-style templates as sensitive |
|
|
675
|
+
| `sensitivePatterns` | `[]` | Extra always-blocked globs (additive on top of the built-in list) |
|
|
676
|
+
| `sensitiveExemptions` | `[]` | Manual exemptions; never written by a dialog |
|
|
677
|
+
| `excludePaths` | `[]` | Folders the agent has no standing permission over. See [Excluded folders](#excluded-folders) for the rescue rule |
|
|
678
|
+
|
|
679
|
+
```jsonc
|
|
680
|
+
{
|
|
681
|
+
"workflow": {
|
|
682
|
+
"folderPolicy": {
|
|
683
|
+
"mode": "repo",
|
|
684
|
+
"outside": "ask",
|
|
685
|
+
"excludePaths": ["C:/", "~"]
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
`"folderPolicy": "ask"` is **not** shorthand — a bare string is read as a `mode` (`"repo"`, `"cwd"`, `"parent"`, `"any"`), matching the earlier release. Project settings (all four `workflow.*` policies) are ignored unless the project is trusted; `/folder-policy status` says when it ignored them. Dialog grants are limited to concrete folders: the filesystem/drive root, home and its parents, and protected system/profile umbrellas are refused as one-click grants (see [Grants](#folder-and-read-permission)).
|
|
692
|
+
|
|
617
693
|
## How it works
|
|
618
694
|
|
|
619
695
|
See `index.ts`, `checkpoint.ts` (shadow bare-repo), `commit-policy.ts` (commit policy config, coercion, the `/commit` prompt and write — no runtime imports, unit-testable), `questionnaire-layout.ts` (pure questionnaire viewport/scroll geometry), `questionnaire-view.ts` (the bounded bottom-sheet questionnaire view), `edit-policy.ts` (write-policy config, coercion, path/command matching, the decision table, and the guided-denial helpers — no runtime imports, unit-testable), `permissions.ts` (shell-command mutation classification), `shell-paths.ts` (path extraction for the shared floor), `folder-policy.ts` (the shared containment floor and sensitive-file rules), `read-policy.ts` / `read-policy-tools.ts` / `read-policy-redact.ts` (the read gate, tool classification and output redaction), `utils.ts` (safe-command gating, todo/plan extraction, plan hashing), `todo-panel.ts` (pure todo-panel state matrix, list identity, summary and row rendering), `roles.ts` (role registry, one model per role), `profiles.ts` (profile registry, the model overlay and the `workflow.defaultProfile` setting — no Pi runtime needed, unit-testable), `provider-retarget.ts` (guarded, backed-up `provider` ref migration for profiles/roles — pure document rewrite plus a two-phase file apply), `picker.ts` (the fuzzy-filterable picker primitives), and the Review Mode modules: `review.ts` (config, prompt, state, transcript, child session — no runtime imports, so it is unit-testable), `review-providers.ts` (parent→reviewer provider seeding and the provider-agnostic auth preflight), `review-tools.ts` (the reviewer's tool set), `review-runtime.ts` (round orchestration), `review-ui.ts` (the main-transcript entries: line buffer, formatters, renderers), `review-handoff.ts` (pure pending-gate correlation, supersede and reload-recovery decisions). Hot-reload via `/reload` when placed in `~/.pi/agent/extensions/workflow/` or loaded via `pi.extensions` manifest.
|
|
@@ -670,7 +746,7 @@ What to do:
|
|
|
670
746
|
cd agent/extensions/workflow
|
|
671
747
|
npm install
|
|
672
748
|
npm run typecheck # tsc --noEmit (covers every source file, including the review modules)
|
|
673
|
-
npm test # node --test utils.todo.test.ts permissions.test.ts shell-text.test.ts roles.test.ts profiles.test.ts provider-retarget.test.ts thinking-levels.test.ts workflow-status.test.ts picker.test.ts questionnaire-layout.test.ts questionnaire-view.test.ts model-default.test.ts review.test.ts review-ui.test.ts extension.test.ts commit-policy.test.ts edit-policy.test.ts tab-policy.test.ts tab-editor.test.ts
|
|
749
|
+
npm test # node --test utils.todo.test.ts permissions.test.ts shell-text.test.ts roles.test.ts profiles.test.ts provider-retarget.test.ts thinking-levels.test.ts workflow-status.test.ts picker.test.ts questionnaire-layout.test.ts questionnaire-view.test.ts model-default.test.ts review.test.ts review-ui.test.ts extension.test.ts commit-policy.test.ts edit-policy.test.ts tab-policy.test.ts tab-editor.test.ts folder-policy.test.ts folder-exclude.test.ts
|
|
674
750
|
```
|
|
675
751
|
|
|
676
752
|
The write policy also has an end-to-end check that drives the real `tool_call` handler against the real `settings.json` (it temporarily writes your global settings and restores them afterwards):
|
package/commit-policy.ts
CHANGED
|
@@ -523,6 +523,8 @@ export interface CommitGateInput {
|
|
|
523
523
|
folder?: "allow" | "ask" | "block";
|
|
524
524
|
/** A sensitive path would be added/committed. */
|
|
525
525
|
sensitive?: boolean;
|
|
526
|
+
/** The repository or a staged path is an excluded folder. */
|
|
527
|
+
excluded?: boolean;
|
|
526
528
|
}
|
|
527
529
|
|
|
528
530
|
/**
|
|
@@ -534,6 +536,9 @@ export interface CommitGateInput {
|
|
|
534
536
|
export function decideCommitGate(input: CommitGateInput): CommitGateDecision {
|
|
535
537
|
const { policy, hit, hasTodos } = input;
|
|
536
538
|
if (input.sensitive) return "block";
|
|
539
|
+
// An excluded path is refused before the authorization exemption is
|
|
540
|
+
// consulted, so a blocked attempt never burns a `/commit` authorization.
|
|
541
|
+
if (input.excluded) return "block";
|
|
537
542
|
if (input.folder === "block") return "block";
|
|
538
543
|
if (
|
|
539
544
|
hit === "commit" &&
|
|
@@ -565,6 +570,9 @@ export function writeCommitPolicyConfig(
|
|
|
565
570
|
): { path: string; ok: boolean; error?: string } {
|
|
566
571
|
const paths = commitConfigPaths(cwd, agentDir);
|
|
567
572
|
const fp = scope === "project" ? paths.project : paths.global;
|
|
573
|
+
if (process.env.PI_FOLDER_POLICY_LOCKED === "1") {
|
|
574
|
+
return { path: fp, ok: false, error: "commit policy is locked for this subagent" };
|
|
575
|
+
}
|
|
568
576
|
try {
|
|
569
577
|
const existing = readJsonSafe(fp);
|
|
570
578
|
const root: JsonObject = existing ? { ...existing } : {};
|
package/edit-policy.ts
CHANGED
|
@@ -310,6 +310,9 @@ export function writeWritePolicyConfig(
|
|
|
310
310
|
): { path: string; ok: boolean; error?: string } {
|
|
311
311
|
const paths = writePolicyConfigPaths(cwd, agentDir);
|
|
312
312
|
const fp = scope === "project" ? paths.project : paths.global;
|
|
313
|
+
if (process.env.PI_FOLDER_POLICY_LOCKED === "1") {
|
|
314
|
+
return { path: fp, ok: false, error: "write policy is locked for this subagent" };
|
|
315
|
+
}
|
|
313
316
|
try {
|
|
314
317
|
const existing = readJsonSafe(fp);
|
|
315
318
|
const root: JsonObject = existing ? { ...existing } : {};
|
|
@@ -716,10 +719,11 @@ export function sanitizeWriteInstruction(raw: unknown): string {
|
|
|
716
719
|
*/
|
|
717
720
|
export function writeGateOptions(
|
|
718
721
|
kind: WriteGateOptionKind,
|
|
722
|
+
opts: { allowAlways?: boolean } = {},
|
|
719
723
|
): readonly string[] {
|
|
720
724
|
return [
|
|
721
725
|
"Allow once (this turn)",
|
|
722
|
-
writeGateAlwaysLabel(kind),
|
|
726
|
+
...(opts.allowAlways !== false ? [writeGateAlwaysLabel(kind)] : []),
|
|
723
727
|
"Deny",
|
|
724
728
|
WRITE_DENY_WITH_INSTRUCTIONS_LABEL,
|
|
725
729
|
];
|
|
@@ -1014,9 +1018,13 @@ export function writeGateTitle(
|
|
|
1014
1018
|
items: readonly WriteGateItem[],
|
|
1015
1019
|
source: WriteGateSource = "write",
|
|
1016
1020
|
base?: string,
|
|
1021
|
+
opts: { grantBlockedReason?: string } = {},
|
|
1017
1022
|
): string {
|
|
1018
1023
|
const outside =
|
|
1019
1024
|
source === "folder" && base ? `\nOutside: ${base}` : "";
|
|
1025
|
+
const blocked = opts.grantBlockedReason
|
|
1026
|
+
? `\nGrant not available: ${opts.grantBlockedReason}`
|
|
1027
|
+
: "";
|
|
1020
1028
|
const header =
|
|
1021
1029
|
source === "folder"
|
|
1022
1030
|
? "Folder policy 'ask': allow this write outside the permitted folders?"
|
|
@@ -1031,7 +1039,8 @@ export function writeGateTitle(
|
|
|
1031
1039
|
header +
|
|
1032
1040
|
outside +
|
|
1033
1041
|
(target ? `\nTarget: ${target}` : "") +
|
|
1034
|
-
(reason && reason !== target ? `\nReason: ${reason}` : "")
|
|
1042
|
+
(reason && reason !== target ? `\nReason: ${reason}` : "") +
|
|
1043
|
+
blocked
|
|
1035
1044
|
);
|
|
1036
1045
|
}
|
|
1037
1046
|
const lines = items
|
|
@@ -1045,7 +1054,7 @@ export function writeGateTitle(
|
|
|
1045
1054
|
: source === "mixed"
|
|
1046
1055
|
? `Permission required: allow these ${items.length} writes? (write policy and folder policy are 'ask')`
|
|
1047
1056
|
: `Write policy 'ask': allow these ${items.length} writes?`;
|
|
1048
|
-
return `${many}${outside}\n${lines.join("\n")}`;
|
|
1057
|
+
return `${many}${outside}\n${lines.join("\n")}${blocked}`;
|
|
1049
1058
|
}
|
|
1050
1059
|
|
|
1051
1060
|
// ── Argument completions (/write-policy) ────────────────────────────
|