pi-mono-all 1.2.5 → 1.2.7
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 +12 -0
- package/node_modules/pi-mono-auto-fix/CHANGELOG.md +8 -0
- package/node_modules/pi-mono-auto-fix/index.ts +18 -6
- package/node_modules/pi-mono-auto-fix/package.json +1 -1
- package/node_modules/pi-mono-sentinel/CHANGELOG.md +10 -0
- package/node_modules/pi-mono-sentinel/__tests__/path-access.test.ts +21 -0
- package/node_modules/pi-mono-sentinel/__tests__/whitelist.test.ts +11 -0
- package/node_modules/pi-mono-sentinel/guards/path-access.ts +102 -21
- package/node_modules/pi-mono-sentinel/guards/permission-gate.ts +47 -8
- package/node_modules/pi-mono-sentinel/package.json +1 -1
- package/node_modules/pi-mono-sentinel/path-access.ts +18 -0
- package/node_modules/pi-mono-sentinel/session.ts +15 -3
- package/package.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# pi-mono-all
|
|
2
2
|
|
|
3
|
+
## 1.2.7
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Bundle `pi-mono-auto-fix@0.3.2` with Windows command quoting, local npm shim resolution, and OS temp-directory fallback fixes.
|
|
8
|
+
|
|
9
|
+
## 1.2.6
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Bundle `pi-mono-sentinel@1.13.0` with smarter path-access prompts, session-scoped grants, and exact multi-file grants.
|
|
14
|
+
|
|
3
15
|
## 1.2.5
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# pi-mono-auto-fix
|
|
2
2
|
|
|
3
|
+
## 0.3.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fix Windows auto-fix execution for paths containing spaces by using `cmd.exe`-compatible quoting, resolving local `.cmd`/`.exe`/`.bat` npm shims, and using the OS temp directory instead of hardcoded `/tmp` for npx fallbacks.
|
|
10
|
+
|
|
3
11
|
## 0.3.1
|
|
4
12
|
|
|
5
13
|
### Patch Changes
|
|
@@ -19,7 +19,7 @@ import type {
|
|
|
19
19
|
} from "@earendil-works/pi-coding-agent";
|
|
20
20
|
import { spawn } from "node:child_process";
|
|
21
21
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
22
|
-
import { homedir } from "node:os";
|
|
22
|
+
import { homedir, tmpdir } from "node:os";
|
|
23
23
|
import { dirname, extname, isAbsolute, relative, resolve } from "node:path";
|
|
24
24
|
|
|
25
25
|
// ---------------------------------------------------------------------------
|
|
@@ -101,6 +101,9 @@ function loadConfig(): Config {
|
|
|
101
101
|
// ---------------------------------------------------------------------------
|
|
102
102
|
|
|
103
103
|
function shellQuote(s: string): string {
|
|
104
|
+
if (process.platform === "win32") {
|
|
105
|
+
return `"${s.replace(/"/g, '\\"')}"`;
|
|
106
|
+
}
|
|
104
107
|
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
105
108
|
}
|
|
106
109
|
|
|
@@ -135,8 +138,17 @@ function findLocalBin(
|
|
|
135
138
|
): { binPath: string; projectRoot: string } | undefined {
|
|
136
139
|
let dir = startDir;
|
|
137
140
|
while (true) {
|
|
138
|
-
const
|
|
139
|
-
|
|
141
|
+
const candidates =
|
|
142
|
+
process.platform === "win32"
|
|
143
|
+
? [
|
|
144
|
+
`${dir}/node_modules/.bin/${tool}.cmd`,
|
|
145
|
+
`${dir}/node_modules/.bin/${tool}.exe`,
|
|
146
|
+
`${dir}/node_modules/.bin/${tool}.bat`,
|
|
147
|
+
`${dir}/node_modules/.bin/${tool}`,
|
|
148
|
+
]
|
|
149
|
+
: [`${dir}/node_modules/.bin/${tool}`];
|
|
150
|
+
const binPath = candidates.find((candidate) => existsSync(candidate));
|
|
151
|
+
if (binPath) return { binPath, projectRoot: dir };
|
|
140
152
|
const parent = dirname(dir);
|
|
141
153
|
if (parent === dir) return undefined;
|
|
142
154
|
dir = parent;
|
|
@@ -285,8 +297,8 @@ function resolveEslintCommand(
|
|
|
285
297
|
* If `command` starts with `npx <tool>`, attempt to resolve a project-local
|
|
286
298
|
* binary by walking up from the first file. On hit, rewrite the command to
|
|
287
299
|
* exec the local bin directly and return the project root as cwd. On miss,
|
|
288
|
-
* fall back to
|
|
289
|
-
* being delegated through pnpm.
|
|
300
|
+
* fall back to the OS temp directory so npx can fetch the latest from the
|
|
301
|
+
* registry without being delegated through pnpm.
|
|
290
302
|
*/
|
|
291
303
|
function resolveSpawnTarget(
|
|
292
304
|
command: string,
|
|
@@ -308,7 +320,7 @@ function resolveSpawnTarget(
|
|
|
308
320
|
}
|
|
309
321
|
}
|
|
310
322
|
// No local install — neutral cwd avoids pnpm delegating npx.
|
|
311
|
-
return { command, spawnCwd:
|
|
323
|
+
return { command, spawnCwd: tmpdir() };
|
|
312
324
|
}
|
|
313
325
|
|
|
314
326
|
async function runFixer(
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# pi-mono-sentinel
|
|
2
2
|
|
|
3
|
+
## 1.13.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
### Enhanced: path-access grants
|
|
8
|
+
|
|
9
|
+
- Added smarter path-access prompts that distinguish existing files, new files, directories, and multiple same-folder file targets.
|
|
10
|
+
- Added session-only permission grants for `write` / `edit`, including recursive directory grants that reset with the session.
|
|
11
|
+
- Added exact multi-file grants for bash commands that reference several outside-project files in the same directory.
|
|
12
|
+
|
|
3
13
|
## 1.12.0
|
|
4
14
|
|
|
5
15
|
### Changed
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
directoryGrantFor,
|
|
11
11
|
isInsideCwd,
|
|
12
12
|
isPathAllowed,
|
|
13
|
+
pathAccessGrantsForChoice,
|
|
13
14
|
pathAccessGrantForChoice,
|
|
14
15
|
toStoragePath,
|
|
15
16
|
} from "../path-access.ts";
|
|
@@ -78,4 +79,24 @@ describe("path-access helpers", () => {
|
|
|
78
79
|
rmSync(cwd, { recursive: true, force: true });
|
|
79
80
|
}
|
|
80
81
|
});
|
|
82
|
+
|
|
83
|
+
test("derives grants for multiple exact files", () => {
|
|
84
|
+
assert.deepEqual(
|
|
85
|
+
pathAccessGrantsForChoice("allow_files_session", ["/tmp/a/one.txt", "/tmp/a/two.txt"], CWD),
|
|
86
|
+
[
|
|
87
|
+
{
|
|
88
|
+
grant: "/tmp/a/one.txt",
|
|
89
|
+
broadCheckPath: "/tmp/a/one.txt",
|
|
90
|
+
scope: "memory",
|
|
91
|
+
directory: false,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
grant: "/tmp/a/two.txt",
|
|
95
|
+
broadCheckPath: "/tmp/a/two.txt",
|
|
96
|
+
scope: "memory",
|
|
97
|
+
directory: false,
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
);
|
|
101
|
+
});
|
|
81
102
|
});
|
|
@@ -50,6 +50,17 @@ describe("SentinelSession whitelist", () => {
|
|
|
50
50
|
assert.equal(session.isWhitelisted("/persisted/path"), true);
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
+
test("session directory grants are recursive and reset-scoped", () => {
|
|
54
|
+
const session = new SentinelSession();
|
|
55
|
+
session.addToSessionWhitelist("/tmp/sentinel-session-dir/");
|
|
56
|
+
assert.equal(session.isWhitelisted("/tmp/sentinel-session-dir/file.md"), true);
|
|
57
|
+
assert.equal(session.isWhitelisted("/tmp/sentinel-session-dir/nested/file.md"), true);
|
|
58
|
+
assert.equal(session.isWhitelisted("/tmp/sentinel-session-dir-sibling/file.md"), false);
|
|
59
|
+
|
|
60
|
+
session.reset();
|
|
61
|
+
assert.equal(session.isWhitelisted("/tmp/sentinel-session-dir/file.md"), false);
|
|
62
|
+
});
|
|
63
|
+
|
|
53
64
|
test("read whitelist is separate from permission whitelist", () => {
|
|
54
65
|
const session = new SentinelSession();
|
|
55
66
|
session.addToReadWhitelist("/safe/example-doc.md");
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { isToolCallEventType } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { existsSync, statSync } from "node:fs";
|
|
4
|
+
import { dirname } from "node:path";
|
|
3
5
|
|
|
4
6
|
import { configLoader, type ResolvedSentinelConfig } from "../config.js";
|
|
5
7
|
import { blockToolCall } from "../events.js";
|
|
6
8
|
import {
|
|
7
9
|
checkPathAccess,
|
|
8
10
|
isTooBroadGrant,
|
|
9
|
-
|
|
11
|
+
pathAccessGrantsForChoice,
|
|
10
12
|
} from "../path-access.js";
|
|
11
13
|
import { extractBashPathCandidates } from "../patterns/bash-paths.js";
|
|
12
14
|
import { resolveTargetPath } from "../patterns/permissions.js";
|
|
@@ -14,19 +16,63 @@ import { resolveTargetPath } from "../patterns/permissions.js";
|
|
|
14
16
|
type GrantChoice =
|
|
15
17
|
| "allow_once"
|
|
16
18
|
| "allow_file_session"
|
|
19
|
+
| "allow_files_session"
|
|
17
20
|
| "allow_directory_session"
|
|
18
21
|
| "allow_file_always"
|
|
22
|
+
| "allow_files_always"
|
|
19
23
|
| "allow_directory_always"
|
|
20
24
|
| "deny";
|
|
21
25
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
type PathPromptKind = "existing_file" | "new_file" | "directory" | "multiple_files";
|
|
27
|
+
|
|
28
|
+
function choicesForPathPrompt(kind: PathPromptKind): Array<{ value: GrantChoice; label: string }> {
|
|
29
|
+
switch (kind) {
|
|
30
|
+
case "new_file":
|
|
31
|
+
return [
|
|
32
|
+
{ value: "allow_once", label: "Allow once" },
|
|
33
|
+
{ value: "allow_directory_session", label: "Allow creating files in this folder for this session" },
|
|
34
|
+
{ value: "allow_directory_always", label: "Always allow creating files in this folder" },
|
|
35
|
+
{ value: "deny", label: "Deny" },
|
|
36
|
+
];
|
|
37
|
+
case "directory":
|
|
38
|
+
return [
|
|
39
|
+
{ value: "allow_once", label: "Allow once" },
|
|
40
|
+
{ value: "allow_directory_session", label: "Allow this folder for this session" },
|
|
41
|
+
{ value: "allow_directory_always", label: "Always allow this folder" },
|
|
42
|
+
{ value: "deny", label: "Deny" },
|
|
43
|
+
];
|
|
44
|
+
case "multiple_files":
|
|
45
|
+
return [
|
|
46
|
+
{ value: "allow_once", label: "Allow once" },
|
|
47
|
+
{ value: "allow_files_session", label: "Allow these files for this session" },
|
|
48
|
+
{ value: "allow_files_always", label: "Always allow these files" },
|
|
49
|
+
{ value: "deny", label: "Deny" },
|
|
50
|
+
];
|
|
51
|
+
case "existing_file":
|
|
52
|
+
default:
|
|
53
|
+
return [
|
|
54
|
+
{ value: "allow_once", label: "Allow once" },
|
|
55
|
+
{ value: "allow_file_session", label: "Allow this file for this session" },
|
|
56
|
+
{ value: "allow_file_always", label: "Always allow this file" },
|
|
57
|
+
{ value: "deny", label: "Deny" },
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function promptKindForPath(absolutePath: string, toolName: string): PathPromptKind {
|
|
63
|
+
try {
|
|
64
|
+
if (existsSync(absolutePath) && statSync(absolutePath).isDirectory()) return "directory";
|
|
65
|
+
} catch {
|
|
66
|
+
// Fall through to operation-based classification.
|
|
67
|
+
}
|
|
68
|
+
return toolName === "write" && !existsSync(absolutePath) ? "new_file" : "existing_file";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function allInSameDirectory(paths: readonly string[]): boolean {
|
|
72
|
+
if (paths.length < 2) return false;
|
|
73
|
+
const first = dirname(paths[0]);
|
|
74
|
+
return paths.every((path) => dirname(path) === first);
|
|
75
|
+
}
|
|
30
76
|
|
|
31
77
|
const MAX_BASH_PATH_CANDIDATES = 50;
|
|
32
78
|
const TOOL_PATH_NORMALIZERS = {
|
|
@@ -42,38 +88,63 @@ export function registerPathAccess(pi: ExtensionAPI): void {
|
|
|
42
88
|
toolName: string,
|
|
43
89
|
input: Record<string, unknown>,
|
|
44
90
|
ctx: { cwd: string; hasUI: boolean; ui: { select?: (title: string, options: string[]) => Promise<string | undefined> } },
|
|
91
|
+
): Promise<{ block: true; reason: string } | undefined> {
|
|
92
|
+
return guardPaths(config, [absolutePath], toolName, input, ctx);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function guardPaths(
|
|
96
|
+
config: ResolvedSentinelConfig,
|
|
97
|
+
absolutePaths: readonly string[],
|
|
98
|
+
toolName: string,
|
|
99
|
+
input: Record<string, unknown>,
|
|
100
|
+
ctx: { cwd: string; hasUI: boolean; ui: { select?: (title: string, options: string[]) => Promise<string | undefined> } },
|
|
45
101
|
): Promise<{ block: true; reason: string } | undefined> {
|
|
46
102
|
if (!config.features.pathAccess || config.pathAccess.mode === "allow") return;
|
|
47
103
|
|
|
48
|
-
const
|
|
49
|
-
|
|
104
|
+
const denied = absolutePaths
|
|
105
|
+
.map((absolutePath) => checkPathAccess(absolutePath, ctx.cwd, config.pathAccess.allowedPaths))
|
|
106
|
+
.filter((check) => !check.allowed) as Array<{ allowed: false; absolutePath: string; reason: string }>;
|
|
107
|
+
if (denied.length === 0) return;
|
|
50
108
|
|
|
51
|
-
const
|
|
109
|
+
const deniedPaths = denied.map((check) => check.absolutePath);
|
|
110
|
+
const reason = denied.length === 1
|
|
111
|
+
? denied[0].reason
|
|
112
|
+
: `Paths are outside the current working directory: ${deniedPaths.join(", ")}`;
|
|
52
113
|
if (config.pathAccess.mode === "block" || !ctx.hasUI) {
|
|
53
114
|
return blockToolCall(pi, { feature: "pathAccess", toolName, input, reason }, `[sentinel] ${reason}`);
|
|
54
115
|
}
|
|
55
116
|
|
|
117
|
+
const promptKind = deniedPaths.length > 1 && allInSameDirectory(deniedPaths)
|
|
118
|
+
? "multiple_files"
|
|
119
|
+
: promptKindForPath(deniedPaths[0], toolName);
|
|
120
|
+
const choices = choicesForPathPrompt(promptKind);
|
|
121
|
+
const pathLines = deniedPaths.length === 1
|
|
122
|
+
? [`Path: ${deniedPaths[0]}`]
|
|
123
|
+
: ["Paths:", ...deniedPaths.map((path) => ` - ${path}`)];
|
|
124
|
+
|
|
56
125
|
const selectedLabel = await ctx.ui.select?.(
|
|
57
126
|
[
|
|
58
127
|
"[sentinel] Path access outside current project",
|
|
59
128
|
`Tool: ${toolName}`,
|
|
60
|
-
|
|
129
|
+
...pathLines,
|
|
61
130
|
`Project: ${ctx.cwd}`,
|
|
62
131
|
"",
|
|
63
132
|
"Allow access?",
|
|
64
133
|
].join("\n"),
|
|
65
|
-
|
|
134
|
+
choices.map((choice) => choice.label),
|
|
66
135
|
);
|
|
67
|
-
const choice =
|
|
136
|
+
const choice = choices.find((item) => item.label === selectedLabel)?.value ?? "deny";
|
|
68
137
|
|
|
69
138
|
if (choice === "allow_once") return;
|
|
70
139
|
|
|
71
|
-
const
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
|
|
140
|
+
const grants = pathAccessGrantsForChoice(choice, deniedPaths, ctx.cwd);
|
|
141
|
+
if (grants.length > 0) {
|
|
142
|
+
for (const grant of grants) {
|
|
143
|
+
if (isTooBroadGrant(grant.broadCheckPath)) {
|
|
144
|
+
return { block: true, reason: `[sentinel] Refusing overly broad ${grant.directory ? "directory" : "path"} grant.` };
|
|
145
|
+
}
|
|
146
|
+
configLoader.addAllowedPath(grant.scope, grant.grant);
|
|
75
147
|
}
|
|
76
|
-
configLoader.addAllowedPath(grant.scope, grant.grant);
|
|
77
148
|
return;
|
|
78
149
|
}
|
|
79
150
|
|
|
@@ -94,8 +165,18 @@ export function registerPathAccess(pi: ExtensionAPI): void {
|
|
|
94
165
|
const command = event.input.command ?? "";
|
|
95
166
|
const config = configLoader.getConfig();
|
|
96
167
|
const candidates = extractBashPathCandidates(command, ctx.cwd).slice(0, MAX_BASH_PATH_CANDIDATES);
|
|
168
|
+
const pendingByDirectory = new Map<string, string[]>();
|
|
97
169
|
for (const absolutePath of candidates) {
|
|
98
|
-
const
|
|
170
|
+
const check = checkPathAccess(absolutePath, ctx.cwd, config.pathAccess.allowedPaths);
|
|
171
|
+
if (check.allowed) continue;
|
|
172
|
+
const paths = pendingByDirectory.get(dirname(absolutePath)) ?? [];
|
|
173
|
+
paths.push(absolutePath);
|
|
174
|
+
pendingByDirectory.set(dirname(absolutePath), paths);
|
|
175
|
+
}
|
|
176
|
+
for (const paths of pendingByDirectory.values()) {
|
|
177
|
+
const result = paths.length > 1
|
|
178
|
+
? await guardPaths(config, paths, "bash", event.input, ctx)
|
|
179
|
+
: await guardPath(config, paths[0], "bash", event.input, ctx);
|
|
99
180
|
if (result) return result;
|
|
100
181
|
}
|
|
101
182
|
});
|
|
@@ -18,9 +18,11 @@ import type {
|
|
|
18
18
|
ExtensionContext,
|
|
19
19
|
} from "@earendil-works/pi-coding-agent";
|
|
20
20
|
import { isToolCallEventType } from "@earendil-works/pi-coding-agent";
|
|
21
|
+
import { existsSync, statSync } from "node:fs";
|
|
21
22
|
|
|
22
23
|
import { configLoader } from "../config.js";
|
|
23
24
|
import { blockToolCall, emitDangerous } from "../events.js";
|
|
25
|
+
import { directoryGrantFor, toStoragePath } from "../path-access.js";
|
|
24
26
|
import type { SentinelSession } from "../session.js";
|
|
25
27
|
import {
|
|
26
28
|
BASH_RISK_DESCRIPTIONS,
|
|
@@ -100,6 +102,43 @@ function registerBashGate(pi: ExtensionAPI): void {
|
|
|
100
102
|
// Write / edit gating
|
|
101
103
|
// ---------------------------------------------------------------------------
|
|
102
104
|
|
|
105
|
+
type PathGateChoice = "allow_once" | "allow_session" | "allow_always" | "deny";
|
|
106
|
+
|
|
107
|
+
function pathGateChoices(absolutePath: string, toolName: "write" | "edit"): Array<{ value: PathGateChoice; label: string; directoryGrant: boolean }> {
|
|
108
|
+
let isDirectory = false;
|
|
109
|
+
try {
|
|
110
|
+
isDirectory = existsSync(absolutePath) && statSync(absolutePath).isDirectory();
|
|
111
|
+
} catch {
|
|
112
|
+
isDirectory = false;
|
|
113
|
+
}
|
|
114
|
+
const isNewFile = toolName === "write" && !existsSync(absolutePath);
|
|
115
|
+
|
|
116
|
+
if (isNewFile) {
|
|
117
|
+
return [
|
|
118
|
+
{ value: "allow_once", label: "Allow once", directoryGrant: false },
|
|
119
|
+
{ value: "allow_session", label: "Allow creating files in this folder for this session", directoryGrant: true },
|
|
120
|
+
{ value: "allow_always", label: "Always allow creating files in this folder", directoryGrant: true },
|
|
121
|
+
{ value: "deny", label: "Deny", directoryGrant: false },
|
|
122
|
+
];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (isDirectory) {
|
|
126
|
+
return [
|
|
127
|
+
{ value: "allow_once", label: "Allow once", directoryGrant: false },
|
|
128
|
+
{ value: "allow_session", label: "Allow this folder for this session", directoryGrant: true },
|
|
129
|
+
{ value: "allow_always", label: "Always allow this folder", directoryGrant: true },
|
|
130
|
+
{ value: "deny", label: "Deny", directoryGrant: false },
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return [
|
|
135
|
+
{ value: "allow_once", label: "Allow once", directoryGrant: false },
|
|
136
|
+
{ value: "allow_session", label: "Allow this file for this session", directoryGrant: false },
|
|
137
|
+
{ value: "allow_always", label: "Always allow this file", directoryGrant: false },
|
|
138
|
+
{ value: "deny", label: "Deny", directoryGrant: false },
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
|
|
103
142
|
function registerPathGate(pi: ExtensionAPI, session: SentinelSession): void {
|
|
104
143
|
const handler = async (
|
|
105
144
|
rawPath: string | undefined,
|
|
@@ -132,18 +171,18 @@ function registerPathGate(pi: ExtensionAPI, session: SentinelSession): void {
|
|
|
132
171
|
].join("\n");
|
|
133
172
|
|
|
134
173
|
if (ctx.hasUI) {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
"Deny",
|
|
139
|
-
]);
|
|
174
|
+
const choices = pathGateChoices(absolute, toolName);
|
|
175
|
+
const selectedLabel = await ctx.ui.select(title, choices.map((choice) => choice.label));
|
|
176
|
+
const choice = choices.find((item) => item.label === selectedLabel);
|
|
140
177
|
|
|
141
|
-
if (choice === "
|
|
178
|
+
if (choice?.value === "allow_once") {
|
|
142
179
|
return;
|
|
143
180
|
}
|
|
144
181
|
|
|
145
|
-
if (choice === "
|
|
146
|
-
|
|
182
|
+
if (choice?.value === "allow_session" || choice?.value === "allow_always") {
|
|
183
|
+
const grant = choice.directoryGrant ? directoryGrantFor(absolute) : toStoragePath(absolute);
|
|
184
|
+
if (choice.value === "allow_session") session.addToSessionWhitelist(grant);
|
|
185
|
+
else session.addToWhitelist(grant);
|
|
147
186
|
return;
|
|
148
187
|
}
|
|
149
188
|
|
|
@@ -33,6 +33,24 @@ export function pathAccessGrantForChoice(choice: string, absolutePath: string, c
|
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
export function pathAccessGrantsForChoice(choice: string, absolutePaths: readonly string[], cwd: string): Array<{ grant: string; broadCheckPath: string; scope: "memory" | "local"; directory: boolean }> {
|
|
37
|
+
if (absolutePaths.length === 0) return [];
|
|
38
|
+
const match = /^allow_(file|directory|files)_(session|always)$/.exec(choice);
|
|
39
|
+
if (!match) return [];
|
|
40
|
+
|
|
41
|
+
if (match[1] === "files") {
|
|
42
|
+
return absolutePaths.map((absolutePath) => ({
|
|
43
|
+
grant: toStoragePath(absolutePath),
|
|
44
|
+
broadCheckPath: absolutePath,
|
|
45
|
+
scope: match[2] === "always" ? "local" : "memory",
|
|
46
|
+
directory: false,
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const grant = pathAccessGrantForChoice(choice, absolutePaths[0], cwd);
|
|
51
|
+
return grant ? [grant] : [];
|
|
52
|
+
}
|
|
53
|
+
|
|
36
54
|
export function isTooBroadGrant(absolutePath: string): boolean {
|
|
37
55
|
const normalized = normalize(absolutePath).replace(/[\\/]+$/, "");
|
|
38
56
|
return normalized === "/" || normalized === homedir();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ScanResult, WriteEntry } from "./types.js";
|
|
2
2
|
import { configLoader } from "./config.js";
|
|
3
|
+
import { isPathAllowed } from "./path-access.js";
|
|
3
4
|
import {
|
|
4
5
|
loadReadWhitelist,
|
|
5
6
|
loadWhitelist,
|
|
@@ -27,6 +28,9 @@ export class SentinelSession {
|
|
|
27
28
|
/** Persistent whitelist of paths the user chose to remember. */
|
|
28
29
|
private whitelist = loadWhitelist();
|
|
29
30
|
|
|
31
|
+
/** Session-only whitelist of paths the user allowed until reset. */
|
|
32
|
+
private sessionWhitelist = new Set<string>();
|
|
33
|
+
|
|
30
34
|
/** Persistent whitelist of read paths that are safe despite secret matches. */
|
|
31
35
|
private readWhitelist = loadReadWhitelist();
|
|
32
36
|
|
|
@@ -34,6 +38,7 @@ export class SentinelSession {
|
|
|
34
38
|
reset(): void {
|
|
35
39
|
this.writeRegistry.clear();
|
|
36
40
|
this.scanCache.clear();
|
|
41
|
+
this.sessionWhitelist.clear();
|
|
37
42
|
// whitelist is intentionally NOT cleared here so it persists across sessions
|
|
38
43
|
}
|
|
39
44
|
|
|
@@ -77,11 +82,18 @@ export class SentinelSession {
|
|
|
77
82
|
// -- Whitelist (permission-gate persistence) -------------------------------
|
|
78
83
|
|
|
79
84
|
isWhitelisted(absolutePath: string): boolean {
|
|
80
|
-
return
|
|
85
|
+
return (
|
|
86
|
+
isPathAllowed(absolutePath, [...this.sessionWhitelist], process.cwd()) ||
|
|
87
|
+
isPathAllowed(absolutePath, [...this.whitelist], process.cwd())
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
addToSessionWhitelist(pathGrant: string): void {
|
|
92
|
+
this.sessionWhitelist.add(pathGrant);
|
|
81
93
|
}
|
|
82
94
|
|
|
83
|
-
addToWhitelist(
|
|
84
|
-
this.whitelist.add(
|
|
95
|
+
addToWhitelist(pathGrant: string): void {
|
|
96
|
+
this.whitelist.add(pathGrant);
|
|
85
97
|
saveWhitelist(this.whitelist);
|
|
86
98
|
}
|
|
87
99
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-mono-all",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.7",
|
|
4
4
|
"description": "All pi-mono extensions and bundled skills",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -9,24 +9,24 @@
|
|
|
9
9
|
"pi-skill"
|
|
10
10
|
],
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"pi-mono-auto-fix": "0.3.1",
|
|
13
12
|
"pi-mono-ask-user-question": "1.7.4",
|
|
13
|
+
"pi-mono-auto-fix": "0.3.2",
|
|
14
14
|
"pi-mono-btw": "1.7.4",
|
|
15
15
|
"pi-mono-clear": "1.7.3",
|
|
16
16
|
"pi-mono-context": "0.1.1",
|
|
17
17
|
"pi-mono-context-guard": "1.7.3",
|
|
18
|
-
"pi-mono-figma": "0.2.2",
|
|
19
|
-
"pi-common": "0.1.1",
|
|
20
18
|
"pi-mono-linear": "0.2.4",
|
|
21
19
|
"pi-mono-loop": "1.7.3",
|
|
20
|
+
"pi-mono-multi-edit": "1.7.3",
|
|
21
|
+
"pi-mono-review": "1.8.2",
|
|
22
|
+
"pi-mono-sentinel": "1.13.0",
|
|
23
|
+
"pi-mono-figma": "0.2.2",
|
|
22
24
|
"pi-mono-simplify": "1.7.3",
|
|
23
25
|
"pi-mono-status-line": "1.7.3",
|
|
24
|
-
"pi-mono-sentinel": "1.12.0",
|
|
25
|
-
"pi-mono-review": "1.8.2",
|
|
26
26
|
"pi-mono-team-mode": "2.3.2",
|
|
27
27
|
"pi-mono-usage": "0.1.1",
|
|
28
28
|
"pi-mono-web-search": "0.1.0",
|
|
29
|
-
"pi-
|
|
29
|
+
"pi-common": "0.1.1"
|
|
30
30
|
},
|
|
31
31
|
"bundledDependencies": [
|
|
32
32
|
"pi-mono-ask-user-question",
|