@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 +7 -0
- package/README.md +13 -3
- package/extensions/bash-cm-diff-guard.ts +29 -3
- package/extensions/shared/bash-command-guards.ts +6 -4
- package/package.json +1 -1
- package/src/plastic-core.ts +1 -1
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
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
19
|
-
|
|
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)
|
|
5
|
-
/^(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)*(?:["']?[^"'\s]*[\\/])?cmd(?:\.exe)
|
|
6
|
-
/^(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)*(?:["']?[^"'\s]*[\\/])?(?:pwsh|powershell)(?:\.exe)
|
|
7
|
-
/^(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)*(?:["']?[^"'\s]*[\\/])?(?:pwsh|powershell)(?:\.exe)
|
|
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
package/src/plastic-core.ts
CHANGED
|
@@ -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
|
}
|