@aefree/pi-plastic 0.5.1 → 0.5.2

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/CHANGELOG.md CHANGED
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project follows semantic versioning for public package releases.
7
7
 
8
+ ## [0.5.2] - 2026-08-05
9
+
10
+ ### Fixed
11
+
12
+ - Hardened the Bash GUI-diff guard for Plastic's `differences` alias, quoted subcommands, common CMD and PowerShell wrappers, `call`/`start`, `Start-Process`, CMD caret escaping, and direct `!`/`!!` shell commands; the internal process guard now rejects the alias too.
13
+ - Added runtime-handler coverage and documented that user-scope installation is required to protect delegated Pi processes whose working directory does not load the originating project's package settings.
14
+
8
15
  ## [0.5.1] - 2026-08-05
9
16
 
10
17
  ### Changed
package/README.md CHANGED
@@ -70,6 +70,8 @@ Pi 0.82 and newer use canonical `sourceInfo` provenance to identify this package
70
70
 
71
71
  ## Safety behavior
72
72
 
73
+ The Bash guards run only in Pi processes that actually load this package. A project-local install does not protect a delegated child process whose working directory resolves different project settings. Install `pi-plastic` at user scope (the default `pi install`, without `-l`) when subagents and sessions in arbitrary workspaces must inherit the guards. Restart existing Pi processes after installation or source changes.
74
+
73
75
  - `plastic_branchCreate` supports an explicit parent branch independent of the loaded workspace branch, defaults relative names to the current branch when no parent is supplied, and rejects top-level paths unless `allowRootBranch=true` is explicit.
74
76
  - `plastic_diff` remains a disabled alias by design. Use `plastic_status` for changed-path listing; do not diff as routine post-edit validation or checkin preflight. When change-boundary evidence is needed, use `plastic_diffFile` for one exact file, `plastic_workspaceDiff` with selected paths or explicit `allPending=true`, or `plastic_diffRevisions` for an explicit historical pair. Diff responses use small defaults and caller-controlled `maxChars` bounds.
75
77
  - Preflight is not routine confirmation. Use it for ambiguous or broad mutation scope, moved/deleted path rewriting, compound operations, or explicit preview requests; otherwise rely on exact targets and the tools' runtime guards.
@@ -96,16 +98,22 @@ If `output` is omitted, Plastic prints patch content to stdout. If `output` is p
96
98
 
97
99
  ## Install
98
100
 
99
- Install the stable GitHub release over HTTPS:
101
+ Install the latest stable npm release at user scope so the safety guards also load for delegated child processes in other workspaces:
102
+
103
+ ```bash
104
+ pi install npm:@aefree/pi-plastic
105
+ ```
106
+
107
+ Install a pinned GitHub release over HTTPS:
100
108
 
101
109
  ```bash
102
- pi install git:github.com/aefreedman/pi-plastic@v0.4.0
110
+ pi install git:github.com/aefreedman/pi-plastic@v0.5.1
103
111
  ```
104
112
 
105
113
  Equivalent SSH install:
106
114
 
107
115
  ```bash
108
- pi install git:git@github.com:aefreedman/pi-plastic@v0.4.0
116
+ pi install git:git@github.com:aefreedman/pi-plastic@v0.5.1
109
117
  ```
110
118
 
111
119
  To intentionally track the moving default branch instead of a release tag:
@@ -126,6 +134,8 @@ Project-local install:
126
134
  pi install -l <path-to-pi-plastic>
127
135
  ```
128
136
 
137
+ Project-local installation protects only Pi processes that load those project settings. It is insufficient as a global Bash safety rail when a subagent launches with another working directory.
138
+
129
139
  ## Requirements
130
140
 
131
141
  - Node.js 22.19.0 or newer
@@ -8,15 +8,24 @@ import {
8
8
  const BLOCK_MESSAGE =
9
9
  "`cm diff` is blocked in Pi because it can launch a GUI window and hang CLI automation. Use `plastic_status` when only changed paths are needed. Do not diff routinely; when change-boundary evidence is necessary, use a focused `plastic_diffFile`, explicitly scoped `plastic_workspaceDiff`, or `plastic_diffRevisions`.";
10
10
 
11
- const DIRECT_CM_DIFF = /^(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)*(?:(?:"[^"]*[\\/]cm(?:\.exe)?"|'[^']*[\\/]cm(?:\.exe)?')|(?:[^"'\s]*[\\/])?cm(?:\.exe)?)\s+diff(?:\s|$)/i;
11
+ const CM_EXECUTABLE = String.raw`(?:"(?:[^"]*[\\/])?cm(?:\.exe)?"|'(?:[^']*[\\/])?cm(?:\.exe)?'|(?:[^"'\s]*[\\/])?cm(?:\.exe)?)`;
12
+ const DIFF_SUBCOMMAND = String.raw`(?:diff|differences)`;
13
+ const DIRECT_CM_DIFF = new RegExp(String.raw`^(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)*${CM_EXECUTABLE}\s+["']?${DIFF_SUBCOMMAND}["']?(?:\s|$)`, "i");
14
+ const POWERSHELL_START_PROCESS_CM_DIFF = new RegExp(String.raw`^(?:start-process|saps|start)\s+(?:-filepath\s+)?${CM_EXECUTABLE}(?=[\s;]|$)[\s\S]*?-argumentlist\s+(?:["'][^"']*${DIFF_SUBCOMMAND}[^"']*["']|${DIFF_SUBCOMMAND}(?:\s|,|$))`, "i");
15
+ const POWERSHELL_INVOKE_EXPRESSION_CM_DIFF = new RegExp(String.raw`^(?:invoke-expression|iex)\s+["'][^"']*${CM_EXECUTABLE}\s+${DIFF_SUBCOMMAND}(?:\s|["'])`, "i");
12
16
 
13
17
  function isBashToolCall(event: { toolName?: string }): event is { toolName: "bash"; input: { command?: string } } {
14
18
  return event.toolName === "bash";
15
19
  }
16
20
 
17
21
  function segmentRunsCmDiff(segment: string): boolean {
18
- const stripped = stripLeadingWrappers(stripAssignments(segment));
19
- return DIRECT_CM_DIFF.test(stripped);
22
+ // cmd.exe removes caret escaping before dispatching the command.
23
+ const normalized = segment.replace(/\^(?=.)/g, "");
24
+ const stripped = stripLeadingWrappers(stripAssignments(normalized));
25
+ return DIRECT_CM_DIFF.test(stripped)
26
+ || POWERSHELL_START_PROCESS_CM_DIFF.test(normalized)
27
+ || POWERSHELL_START_PROCESS_CM_DIFF.test(stripped)
28
+ || POWERSHELL_INVOKE_EXPRESSION_CM_DIFF.test(stripped);
20
29
  }
21
30
 
22
31
  function commandRunsCmDiff(command: string): boolean {
@@ -47,4 +56,21 @@ export default function bashCmDiffGuard(pi: ExtensionAPI) {
47
56
  reason: BLOCK_MESSAGE,
48
57
  };
49
58
  });
59
+
60
+ pi.on("user_bash", async (event, ctx) => {
61
+ if (!event.command || !commandRunsCmDiff(event.command)) return;
62
+
63
+ if (ctx.hasUI) {
64
+ ctx.ui.notify("Blocked user shell command invoking cm diff", "warning");
65
+ }
66
+
67
+ return {
68
+ result: {
69
+ output: BLOCK_MESSAGE,
70
+ exitCode: 1,
71
+ cancelled: false,
72
+ truncated: false,
73
+ },
74
+ };
75
+ });
50
76
  }
@@ -1,10 +1,10 @@
1
1
  export const WRAPPED_COMMAND_PATTERNS = [
2
2
  /^(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)*(?:["']?[^"'\s]*[\\/])?(?:bash|sh|zsh)(?:\.exe)?\s+-lc\s+(["'])([\s\S]*)\1$/i,
3
3
  /^(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)*(?:["']?[^"'\s]*[\\/])?(?:bash|sh|zsh)(?:\.exe)?\s+-c\s+(["'])([\s\S]*)\1$/i,
4
- /^(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)*(?:["']?[^"'\s]*[\\/])?cmd(?:\.exe)?\s+\/[ck]\s+(["'])([\s\S]*)\1$/i,
5
- /^(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)*(?:["']?[^"'\s]*[\\/])?cmd(?:\.exe)?\s+\/[ck]\s+()([\s\S]+)$/i,
6
- /^(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)*(?:["']?[^"'\s]*[\\/])?(?:pwsh|powershell)(?:\.exe)?\s+-command\s+(["'])([\s\S]*)\1$/i,
7
- /^(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)*(?:["']?[^"'\s]*[\\/])?(?:pwsh|powershell)(?:\.exe)?\s+-command\s+()([\s\S]+)$/i,
4
+ /^(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)*(?:["']?[^"'\s]*[\\/])?cmd(?:\.exe)?(?:\s+\/[dqsvu](?::\S+)*)*\s+\/[ck]\s+(["'])([\s\S]*)\1$/i,
5
+ /^(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)*(?:["']?[^"'\s]*[\\/])?cmd(?:\.exe)?(?:\s+\/[dqsvu](?::\S+)*)*\s+\/[ck]\s+()([\s\S]+)$/i,
6
+ /^(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)*(?:["']?[^"'\s]*[\\/])?(?:pwsh|powershell)(?:\.exe)?(?:(?:\s+-(?:noprofile|noninteractive|nologo|sta|mta))|(?:\s+-executionpolicy\s+\S+))*\s+-(?:command|c)\s+(["'])([\s\S]*)\1$/i,
7
+ /^(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)*(?:["']?[^"'\s]*[\\/])?(?:pwsh|powershell)(?:\.exe)?(?:(?:\s+-(?:noprofile|noninteractive|nologo|sta|mta))|(?:\s+-executionpolicy\s+\S+))*\s+-(?:command|c)\s+()([\s\S]+)$/i,
8
8
  ];
9
9
 
10
10
  export function splitSegments(command: string): string[] {
@@ -25,6 +25,8 @@ export function stripLeadingWrappers(segment: string): string {
25
25
  const next = value
26
26
  .replace(/^sudo\s+/i, "")
27
27
  .replace(/^command\s+/i, "")
28
+ .replace(/^call\s+/i, "")
29
+ .replace(/^start\s+(?:["'][^"']*["']\s+)?(?:(?:\/wait|\/b|\/min|\/max|\/i|\/low|\/normal|\/high|\/realtime|\/abovenormal|\/belownormal)\s+)*/i, "")
28
30
  .replace(/^time\s+/i, "")
29
31
  .replace(/^nice(?:\s+(?:-n\s+)?-?\d+)?\s+/i, "");
30
32
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aefree/pi-plastic",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22.19.0"
@@ -250,7 +250,7 @@ const BLOCKED_CM_DIFF_MESSAGE = "`cm diff` is blocked in Pi because it may launc
250
250
  const ensureCmCommandAllowed = (args: string[]): void =>
251
251
  {
252
252
  const command = (args[0] ?? "").trim().toLowerCase();
253
- if (command === "diff")
253
+ if (command === "diff" || command === "differences")
254
254
  {
255
255
  throw new Error(BLOCKED_CM_DIFF_MESSAGE);
256
256
  }