@dieulc/workflow 0.9.2 → 0.11.0
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 +104 -20
- package/commit-policy.ts +74 -10
- package/edit-policy.ts +111 -44
- package/folder-policy.ts +1159 -0
- package/index.ts +2868 -496
- package/package.json +16 -6
- package/provider-retarget.ts +258 -0
- package/read-policy-redact.ts +319 -0
- package/read-policy-tools.ts +310 -0
- package/read-policy.ts +654 -0
- package/review-handoff.ts +126 -0
- package/review-recovery.ts +67 -0
- package/review-runtime.ts +454 -94
- package/review-tools.ts +472 -7
- package/review.ts +451 -33
- package/shell-paths.ts +1045 -0
- package/todo-panel.ts +334 -0
- package/utils.ts +68 -0
- package/workflow-status.ts +22 -8
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow Extension — Read tool-call classifier
|
|
3
|
+
*
|
|
4
|
+
* Turns a tool name + input into the filesystem targets the read gate must
|
|
5
|
+
* classify. Covers Pi's built-in read tools, the pi-lens read tools, `explore`
|
|
6
|
+
* (the child's working root), local `fetch_content` / `browser_*` file URLs,
|
|
7
|
+
* and a conservative generic path-key scan for extension/MCP tools.
|
|
8
|
+
*
|
|
9
|
+
* Known writers are skipped (the write policy owns them). An unknown tool with
|
|
10
|
+
* a path-like input is guarded; an unknown tool without one is allowed. This
|
|
11
|
+
* is deliberately fail-safe without pretending every MCP tool is a file read.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
|
|
16
|
+
import type { ReadPolicyConfig } from "./read-policy.ts";
|
|
17
|
+
import type { FolderTarget } from "./folder-policy.ts";
|
|
18
|
+
import { normalizePolicyPath, toPosixPath } from "./folder-policy.ts";
|
|
19
|
+
import { analyzeShellPaths } from "./shell-paths.ts";
|
|
20
|
+
|
|
21
|
+
export interface ReadCallClassification {
|
|
22
|
+
tool: string;
|
|
23
|
+
targets: FolderTarget[];
|
|
24
|
+
uncertain: string[];
|
|
25
|
+
exempt?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const KNOWN_WRITE_TOOLS = new Set([
|
|
29
|
+
"edit",
|
|
30
|
+
"write",
|
|
31
|
+
"ast_grep_replace",
|
|
32
|
+
"review_submit_plan",
|
|
33
|
+
"review_plan_begin",
|
|
34
|
+
"review_plan_chunk",
|
|
35
|
+
"review_plan_finalize",
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
const EXEMPT_TOOLS = new Set([
|
|
39
|
+
"memory_search",
|
|
40
|
+
"memory_read",
|
|
41
|
+
"memory_write",
|
|
42
|
+
"memory_forget",
|
|
43
|
+
"memory_restore",
|
|
44
|
+
"memory_status",
|
|
45
|
+
"scratchpad",
|
|
46
|
+
"workflow_todo",
|
|
47
|
+
"web_search",
|
|
48
|
+
"source_check",
|
|
49
|
+
"get_search_content",
|
|
50
|
+
"pi_lens_activate_tools",
|
|
51
|
+
"lens_diagnostic_mark",
|
|
52
|
+
"questionnaire",
|
|
53
|
+
"browser_screenshot",
|
|
54
|
+
"browser_console_logs",
|
|
55
|
+
"browser_console_errors",
|
|
56
|
+
"browser_network_requests",
|
|
57
|
+
"browser_close",
|
|
58
|
+
"browser_attach",
|
|
59
|
+
"browser_detach",
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
const FILE_PATH_TOOLS = new Set([
|
|
63
|
+
"read",
|
|
64
|
+
"read_symbol",
|
|
65
|
+
"read_enclosing",
|
|
66
|
+
"module_report",
|
|
67
|
+
"effective_config",
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
const DIR_OR_FILE_PATH_TOOLS = new Set(["ls", "grep", "find", "lsp_navigation"]);
|
|
71
|
+
|
|
72
|
+
const PATHS_TOOLS = new Set([
|
|
73
|
+
"symbol_search",
|
|
74
|
+
"ast_grep_search",
|
|
75
|
+
"ast_grep_outline",
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
const GENERIC_PATH_KEYS = new Set([
|
|
79
|
+
"path",
|
|
80
|
+
"file",
|
|
81
|
+
"filePath",
|
|
82
|
+
"file_path",
|
|
83
|
+
"dir",
|
|
84
|
+
"dirs",
|
|
85
|
+
"directory",
|
|
86
|
+
"directories",
|
|
87
|
+
"cwd",
|
|
88
|
+
"root",
|
|
89
|
+
"roots",
|
|
90
|
+
"analysisRoot",
|
|
91
|
+
"paths",
|
|
92
|
+
"files",
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
function addTarget(
|
|
96
|
+
out: FolderTarget[],
|
|
97
|
+
value: unknown,
|
|
98
|
+
kind: FolderTarget["kind"],
|
|
99
|
+
cwd: string,
|
|
100
|
+
): void {
|
|
101
|
+
if (typeof value !== "string" || !value.trim()) return;
|
|
102
|
+
const path = normalizePolicyPath(value, cwd);
|
|
103
|
+
if (!path) return;
|
|
104
|
+
const target =
|
|
105
|
+
value.includes("*") || value.includes("?")
|
|
106
|
+
? { path: staticGlobPrefix(value, cwd), kind: "glob" as const }
|
|
107
|
+
: { path, kind };
|
|
108
|
+
if (!out.some((item) => item.path === target.path)) out.push(target);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Static, non-wildcard prefix of a glob, resolved to the narrowest directory
|
|
113
|
+
* that must be permitted. `../other/src/**\/*.ts` -> `../other/src`;
|
|
114
|
+
* `**\/*.ts` -> cwd.
|
|
115
|
+
*/
|
|
116
|
+
export function staticGlobPrefix(value: string, cwd: string): string {
|
|
117
|
+
const raw = value.trim();
|
|
118
|
+
const cut = raw.search(/[?*[]/);
|
|
119
|
+
const prefix = cut >= 0 ? raw.slice(0, cut) : raw;
|
|
120
|
+
const withoutTrailing = prefix.replace(/[\\/]+$/, "");
|
|
121
|
+
if (!withoutTrailing) return normalizePolicyPath(cwd, cwd);
|
|
122
|
+
const wildcardInLastSegment =
|
|
123
|
+
cut >= 0 && !/^[\\/]/.test(raw.slice(cut)) && !/[\\/]$/.test(prefix);
|
|
124
|
+
const base = wildcardInLastSegment
|
|
125
|
+
? withoutTrailing.replace(/[\\/][^\\/]*$/, "")
|
|
126
|
+
: withoutTrailing;
|
|
127
|
+
return normalizePolicyPath(base || cwd, cwd);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function collection(value: unknown): unknown[] {
|
|
131
|
+
return Array.isArray(value) ? value : [value];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function classifyFileUrls(
|
|
135
|
+
tool: string,
|
|
136
|
+
input: Record<string, unknown>,
|
|
137
|
+
cwd: string,
|
|
138
|
+
): ReadCallClassification {
|
|
139
|
+
const result: ReadCallClassification = { tool, targets: [], uncertain: [] };
|
|
140
|
+
const raw = input.url ?? input.urls;
|
|
141
|
+
for (const entry of collection(raw)) {
|
|
142
|
+
if (typeof entry !== "string" || !entry.trim()) continue;
|
|
143
|
+
const value = entry.trim();
|
|
144
|
+
if (/^https?:\/\//i.test(value) || /^(data|ftp|mailto):/i.test(value))
|
|
145
|
+
continue;
|
|
146
|
+
if (value.startsWith("file:")) {
|
|
147
|
+
try {
|
|
148
|
+
addTarget(result.targets, fileURLToPath(value), "file", cwd);
|
|
149
|
+
} catch {
|
|
150
|
+
result.uncertain.push(`${tool}: invalid file URL`);
|
|
151
|
+
}
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
// A bare local path is a local read (video/image/PDF fetchers). Windows
|
|
155
|
+
// drive paths are not URL schemes (`C:\...`).
|
|
156
|
+
if (
|
|
157
|
+
/^[A-Za-z]:[\\/]/.test(value) ||
|
|
158
|
+
value.startsWith("\\\\") ||
|
|
159
|
+
!/^[A-Za-z][A-Za-z0-9+.-]*:/.test(value)
|
|
160
|
+
) {
|
|
161
|
+
addTarget(result.targets, value, "file", cwd);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Classify a tool call. Total: malformed inputs never throw.
|
|
169
|
+
*/
|
|
170
|
+
export function classifyReadCall(
|
|
171
|
+
toolName: string,
|
|
172
|
+
input: unknown,
|
|
173
|
+
cwd: string,
|
|
174
|
+
config: ReadPolicyConfig,
|
|
175
|
+
): ReadCallClassification {
|
|
176
|
+
const tool = String(toolName ?? "");
|
|
177
|
+
const obj =
|
|
178
|
+
input && typeof input === "object" && !Array.isArray(input)
|
|
179
|
+
? (input as Record<string, unknown>)
|
|
180
|
+
: {};
|
|
181
|
+
const result: ReadCallClassification = { tool, targets: [], uncertain: [] };
|
|
182
|
+
|
|
183
|
+
if (tool === "bash" || tool === "powershell") {
|
|
184
|
+
const analysis = analyzeShellPaths(obj.command, cwd);
|
|
185
|
+
result.targets = analysis.reads;
|
|
186
|
+
result.uncertain = analysis.uncertain;
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (config.guardedTools.includes(tool)) {
|
|
191
|
+
collectGeneric(obj, cwd, result);
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (EXEMPT_TOOLS.has(tool)) {
|
|
196
|
+
result.exempt = "not a filesystem read tool";
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
if (KNOWN_WRITE_TOOLS.has(tool)) {
|
|
200
|
+
result.exempt = "write tool (write policy owns it)";
|
|
201
|
+
return result;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (tool === "explore") {
|
|
205
|
+
if (obj.cwd !== undefined) addTarget(result.targets, obj.cwd, "dir", cwd);
|
|
206
|
+
const tasks = Array.isArray(obj.tasks) ? obj.tasks : [];
|
|
207
|
+
for (const task of tasks) {
|
|
208
|
+
if (!task || typeof task !== "object") continue;
|
|
209
|
+
const taskCwd = (task as Record<string, unknown>).cwd;
|
|
210
|
+
if (taskCwd !== undefined) addTarget(result.targets, taskCwd, "dir", cwd);
|
|
211
|
+
}
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (tool === "fetch_content" || tool === "browser_launch" || tool === "browser_navigate") {
|
|
216
|
+
return classifyFileUrls(tool, obj, cwd);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (tool === "lens_diagnostics") {
|
|
220
|
+
for (const key of ["path", "file", "filePath", "analysisRoot"]) {
|
|
221
|
+
if (obj[key] !== undefined) addTarget(result.targets, obj[key], "dir", cwd);
|
|
222
|
+
}
|
|
223
|
+
for (const entry of collection(obj.paths)) {
|
|
224
|
+
addTarget(result.targets, entry, "file", cwd);
|
|
225
|
+
}
|
|
226
|
+
return result;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (PATHS_TOOLS.has(tool)) {
|
|
230
|
+
for (const entry of collection(obj.paths)) {
|
|
231
|
+
addTarget(result.targets, entry, "file", cwd);
|
|
232
|
+
}
|
|
233
|
+
if (obj.path !== undefined) addTarget(result.targets, obj.path, "dir", cwd);
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (FILE_PATH_TOOLS.has(tool)) {
|
|
238
|
+
const value =
|
|
239
|
+
obj.path ?? obj.file ?? obj.filePath ?? obj.file_path ?? undefined;
|
|
240
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
241
|
+
result.uncertain.push(`${tool}: missing path`);
|
|
242
|
+
return result;
|
|
243
|
+
}
|
|
244
|
+
addTarget(result.targets, value, "file", cwd);
|
|
245
|
+
return result;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (DIR_OR_FILE_PATH_TOOLS.has(tool)) {
|
|
249
|
+
const value = obj.path ?? obj.file ?? obj.filePath ?? obj.file_path;
|
|
250
|
+
addTarget(result.targets, value ?? ".", "dir", cwd);
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Unknown/MCP tool: guard path-like inputs, otherwise allow.
|
|
255
|
+
collectGeneric(obj, cwd, result);
|
|
256
|
+
return result;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function collectGeneric(
|
|
260
|
+
input: unknown,
|
|
261
|
+
cwd: string,
|
|
262
|
+
result: ReadCallClassification,
|
|
263
|
+
depth = 0,
|
|
264
|
+
): void {
|
|
265
|
+
if (!input || typeof input !== "object" || depth > 3) return;
|
|
266
|
+
if (Array.isArray(input)) {
|
|
267
|
+
for (const entry of input) {
|
|
268
|
+
if (typeof entry === "string") {
|
|
269
|
+
// Only reached for a direct array under a path key; handled below.
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
collectGeneric(entry, cwd, result, depth + 1);
|
|
273
|
+
}
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
for (const [key, value] of Object.entries(input as Record<string, unknown>)) {
|
|
277
|
+
if (GENERIC_PATH_KEYS.has(key)) {
|
|
278
|
+
if (Array.isArray(value)) {
|
|
279
|
+
for (const entry of value) {
|
|
280
|
+
if (typeof entry === "string") {
|
|
281
|
+
const kind = key === "paths" || key === "files" ? "file" : "dir";
|
|
282
|
+
addTarget(result.targets, entry, kind, cwd);
|
|
283
|
+
} else {
|
|
284
|
+
collectGeneric(entry, cwd, result, depth + 1);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
} else if (typeof value === "string") {
|
|
288
|
+
const kind =
|
|
289
|
+
key === "path" || key === "file" || key === "filePath" || key === "file_path"
|
|
290
|
+
? "file"
|
|
291
|
+
: "dir";
|
|
292
|
+
addTarget(result.targets, value, kind, cwd);
|
|
293
|
+
}
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
// Nested containers may hold `cwd`/`path` fields (explore tasks, MCP args).
|
|
297
|
+
if (value && typeof value === "object") {
|
|
298
|
+
collectGeneric(value, cwd, result, depth + 1);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** A short display path for diagnostics. */
|
|
304
|
+
export function describeReadTargets(
|
|
305
|
+
targets: readonly FolderTarget[],
|
|
306
|
+
): string {
|
|
307
|
+
return targets
|
|
308
|
+
.map((target) => toPosixPath(target.path))
|
|
309
|
+
.join(", ");
|
|
310
|
+
}
|