@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/read-policy-redact.ts
CHANGED
|
@@ -1,319 +1,359 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Workflow Extension — Read-policy output redaction
|
|
3
|
-
*
|
|
4
|
-
* Defence in depth for channels the tool-call gate cannot fully see:
|
|
5
|
-
*
|
|
6
|
-
* - `grep` searches a directory with `rg --hidden`; a `.env` that is not
|
|
7
|
-
* gitignored would otherwise be returned in matched lines.
|
|
8
|
-
* - `find` / `ls` list sensitive names; listings omit them.
|
|
9
|
-
* - `bash` can print `git diff`/`git show` sections or `rg`-style lines.
|
|
10
|
-
* - Pi's truncated bash output writes the full text to a temp file and tells
|
|
11
|
-
* the model its path; when we redact, that escape hatch is removed.
|
|
12
|
-
* - a successful `read` of a sensitive path is replaced as a safety net.
|
|
13
|
-
*
|
|
14
|
-
* Textual redaction is best-effort (custom renderers/structured content could
|
|
15
|
-
* bypass); callers drop `structuredContent` whenever content changes.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import * as fs from "node:fs";
|
|
19
|
-
import * as path from "node:path";
|
|
20
|
-
|
|
21
|
-
import type {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
export interface
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
raw
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
):
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
text:
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Workflow Extension — Read-policy output redaction
|
|
3
|
+
*
|
|
4
|
+
* Defence in depth for channels the tool-call gate cannot fully see:
|
|
5
|
+
*
|
|
6
|
+
* - `grep` searches a directory with `rg --hidden`; a `.env` that is not
|
|
7
|
+
* gitignored would otherwise be returned in matched lines.
|
|
8
|
+
* - `find` / `ls` list sensitive names; listings omit them.
|
|
9
|
+
* - `bash` can print `git diff`/`git show` sections or `rg`-style lines.
|
|
10
|
+
* - Pi's truncated bash output writes the full text to a temp file and tells
|
|
11
|
+
* the model its path; when we redact, that escape hatch is removed.
|
|
12
|
+
* - a successful `read` of a sensitive path is replaced as a safety net.
|
|
13
|
+
*
|
|
14
|
+
* Textual redaction is best-effort (custom renderers/structured content could
|
|
15
|
+
* bypass); callers drop `structuredContent` whenever content changes.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import * as fs from "node:fs";
|
|
19
|
+
import * as path from "node:path";
|
|
20
|
+
|
|
21
|
+
import type {
|
|
22
|
+
ExclusionRescueRoots,
|
|
23
|
+
FolderPolicyConfig,
|
|
24
|
+
} from "./folder-policy.ts";
|
|
25
|
+
import {
|
|
26
|
+
matchSensitivePath,
|
|
27
|
+
matchUnrescuedExclusion,
|
|
28
|
+
toPosixPath,
|
|
29
|
+
} from "./folder-policy.ts";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Roots that can re-open an excluded path for this result. Without them an
|
|
33
|
+
* exclusion match is withheld, so a caller that forgets to pass roots fails
|
|
34
|
+
* closed rather than leaking the excluded tree.
|
|
35
|
+
*/
|
|
36
|
+
export type RedactRoots = ExclusionRescueRoots;
|
|
37
|
+
|
|
38
|
+
export interface RedactableBlock {
|
|
39
|
+
type: string;
|
|
40
|
+
text?: string;
|
|
41
|
+
[key: string]: unknown;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface RedactableToolResult {
|
|
45
|
+
toolName: string;
|
|
46
|
+
input: Record<string, unknown>;
|
|
47
|
+
content: RedactableBlock[];
|
|
48
|
+
details?: unknown;
|
|
49
|
+
structuredContent?: unknown;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface RedactedToolResult {
|
|
53
|
+
content?: RedactableBlock[];
|
|
54
|
+
details?: unknown;
|
|
55
|
+
structuredContent?: undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface RedactTextResult {
|
|
59
|
+
text: string;
|
|
60
|
+
redacted: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function searchBase(input: Record<string, unknown>, cwd: string): string {
|
|
64
|
+
const raw = input.path;
|
|
65
|
+
if (typeof raw !== "string" || !raw.trim()) return cwd;
|
|
66
|
+
const resolved = path.resolve(cwd, raw);
|
|
67
|
+
try {
|
|
68
|
+
return fs.statSync(resolved).isDirectory() ? resolved : path.dirname(resolved);
|
|
69
|
+
} catch {
|
|
70
|
+
return resolved;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isSensitive(
|
|
75
|
+
pathValue: string,
|
|
76
|
+
config: FolderPolicyConfig,
|
|
77
|
+
cwd: string,
|
|
78
|
+
): boolean {
|
|
79
|
+
return matchSensitivePath(pathValue, config, cwd).sensitive;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Sensitive always wins; an excluded path is withheld unless an approved root
|
|
84
|
+
* (see `matchUnrescuedExclusion`) re-opens it.
|
|
85
|
+
*/
|
|
86
|
+
function isProtected(
|
|
87
|
+
pathValue: string,
|
|
88
|
+
config: FolderPolicyConfig,
|
|
89
|
+
cwd: string,
|
|
90
|
+
roots: RedactRoots | undefined,
|
|
91
|
+
): boolean {
|
|
92
|
+
if (isSensitive(pathValue, config, cwd)) return true;
|
|
93
|
+
if (config.excludePaths.length === 0) return false;
|
|
94
|
+
return matchUnrescuedExclusion(pathValue, config, cwd, roots ?? {}).excluded;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function resolveOutputPath(
|
|
98
|
+
raw: string,
|
|
99
|
+
base: string,
|
|
100
|
+
cwd: string,
|
|
101
|
+
): string {
|
|
102
|
+
if (path.isAbsolute(raw)) return raw;
|
|
103
|
+
const joined = path.resolve(base, raw);
|
|
104
|
+
// `grep` relativises against the search root; if the joined path does not
|
|
105
|
+
// exist, the raw path may already be cwd-relative.
|
|
106
|
+
try {
|
|
107
|
+
if (fs.existsSync(joined)) return joined;
|
|
108
|
+
} catch {
|
|
109
|
+
/* ignore */
|
|
110
|
+
}
|
|
111
|
+
return path.resolve(cwd, raw);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function redactGrep(
|
|
115
|
+
text: string,
|
|
116
|
+
input: Record<string, unknown>,
|
|
117
|
+
cwd: string,
|
|
118
|
+
config: FolderPolicyConfig,
|
|
119
|
+
roots?: RedactRoots,
|
|
120
|
+
): RedactTextResult {
|
|
121
|
+
const base = searchBase(input, cwd);
|
|
122
|
+
const out: string[] = [];
|
|
123
|
+
let redacted = false;
|
|
124
|
+
for (const line of text.split("\n")) {
|
|
125
|
+
const match = /^(.+?)(?::(\d+):|-(\d+)-)(.*)$/.exec(line);
|
|
126
|
+
if (!match) {
|
|
127
|
+
out.push(line);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const rawPath = match[1];
|
|
131
|
+
if (rawPath.includes("://")) {
|
|
132
|
+
out.push(line);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const resolved = resolveOutputPath(rawPath, base, cwd);
|
|
136
|
+
if (isProtected(resolved, config, cwd, roots)) {
|
|
137
|
+
redacted = true;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
out.push(line);
|
|
141
|
+
}
|
|
142
|
+
if (redacted && out.every((line) => !line.trim())) {
|
|
143
|
+
return {
|
|
144
|
+
text:
|
|
145
|
+
"[matches in protected files were withheld by read policy]",
|
|
146
|
+
redacted: true,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
return { text: out.join("\n"), redacted };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function redactFind(
|
|
153
|
+
text: string,
|
|
154
|
+
input: Record<string, unknown>,
|
|
155
|
+
cwd: string,
|
|
156
|
+
config: FolderPolicyConfig,
|
|
157
|
+
roots?: RedactRoots,
|
|
158
|
+
): RedactTextResult {
|
|
159
|
+
const base = searchBase(input, cwd);
|
|
160
|
+
const out: string[] = [];
|
|
161
|
+
let redacted = false;
|
|
162
|
+
for (const line of text.split("\n")) {
|
|
163
|
+
const trimmed = line.trim();
|
|
164
|
+
if (!trimmed) {
|
|
165
|
+
out.push(line);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const resolved = resolveOutputPath(trimmed.replace(/\/$/, ""), base, cwd);
|
|
169
|
+
if (isProtected(resolved, config, cwd, roots)) {
|
|
170
|
+
redacted = true;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
out.push(line);
|
|
174
|
+
}
|
|
175
|
+
if (redacted && out.every((line) => !line.trim())) {
|
|
176
|
+
return { text: "[entries withheld by read policy]", redacted: true };
|
|
177
|
+
}
|
|
178
|
+
return { text: out.join("\n"), redacted };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function redactLs(
|
|
182
|
+
text: string,
|
|
183
|
+
input: Record<string, unknown>,
|
|
184
|
+
cwd: string,
|
|
185
|
+
config: FolderPolicyConfig,
|
|
186
|
+
roots?: RedactRoots,
|
|
187
|
+
): RedactTextResult {
|
|
188
|
+
const raw = input.path;
|
|
189
|
+
const base =
|
|
190
|
+
typeof raw === "string" && raw.trim()
|
|
191
|
+
? path.resolve(cwd, raw)
|
|
192
|
+
: cwd;
|
|
193
|
+
const out: string[] = [];
|
|
194
|
+
let redacted = false;
|
|
195
|
+
for (const line of text.split("\n")) {
|
|
196
|
+
const entry = line.trim().replace(/\/$/, "");
|
|
197
|
+
if (!entry) {
|
|
198
|
+
out.push(line);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (isProtected(path.join(base, entry), config, cwd, roots)) {
|
|
202
|
+
redacted = true;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
out.push(line);
|
|
206
|
+
}
|
|
207
|
+
if (redacted && out.every((line) => !line.trim())) {
|
|
208
|
+
return { text: "[entries withheld by read policy]", redacted: true };
|
|
209
|
+
}
|
|
210
|
+
return { text: out.join("\n"), redacted };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function redactBash(
|
|
214
|
+
text: string,
|
|
215
|
+
cwd: string,
|
|
216
|
+
config: FolderPolicyConfig,
|
|
217
|
+
roots?: RedactRoots,
|
|
218
|
+
): RedactTextResult {
|
|
219
|
+
const lines = text.split("\n");
|
|
220
|
+
const out: string[] = [];
|
|
221
|
+
let redacted = false;
|
|
222
|
+
let inSensitiveDiff = false;
|
|
223
|
+
for (const line of lines) {
|
|
224
|
+
const diffHeader = /^diff --git a\/(.+?) b\/(.+)$/.exec(line);
|
|
225
|
+
if (diffHeader) {
|
|
226
|
+
const targetA = resolveOutputPath(diffHeader[1], cwd, cwd);
|
|
227
|
+
const targetB = resolveOutputPath(diffHeader[2], cwd, cwd);
|
|
228
|
+
inSensitiveDiff =
|
|
229
|
+
isProtected(targetA, config, cwd, roots) ||
|
|
230
|
+
isProtected(targetB, config, cwd, roots);
|
|
231
|
+
if (inSensitiveDiff) {
|
|
232
|
+
redacted = true;
|
|
233
|
+
out.push("[diff section withheld by read policy]");
|
|
234
|
+
} else {
|
|
235
|
+
out.push(line);
|
|
236
|
+
}
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (inSensitiveDiff) {
|
|
240
|
+
redacted = true;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (/^Full output: /.test(line)) {
|
|
244
|
+
// Remove the temp-file escape hatch only when something was redacted;
|
|
245
|
+
// the caller also drops details.fullOutputPath.
|
|
246
|
+
if (redacted) continue;
|
|
247
|
+
}
|
|
248
|
+
const prefixed = /^(.+?)(?::(\d+):|-(\d+)-)(.*)$/.exec(line);
|
|
249
|
+
if (prefixed && !prefixed[1].includes("://")) {
|
|
250
|
+
const resolved = resolveOutputPath(prefixed[1], cwd, cwd);
|
|
251
|
+
if (isProtected(resolved, config, cwd, roots)) {
|
|
252
|
+
redacted = true;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
out.push(line);
|
|
257
|
+
}
|
|
258
|
+
if (redacted && out.every((line) => !line.trim())) {
|
|
259
|
+
return { text: "[output withheld by read policy]", redacted: true };
|
|
260
|
+
}
|
|
261
|
+
return { text: out.join("\n"), redacted };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Redact one text payload. `kind` is the tool name (grep/find/ls/bash/read).
|
|
266
|
+
*/
|
|
267
|
+
export function redactSensitiveText(
|
|
268
|
+
text: string,
|
|
269
|
+
kind: string,
|
|
270
|
+
input: Record<string, unknown>,
|
|
271
|
+
cwd: string,
|
|
272
|
+
config: FolderPolicyConfig,
|
|
273
|
+
roots?: RedactRoots,
|
|
274
|
+
): RedactTextResult {
|
|
275
|
+
if (!text) return { text, redacted: false };
|
|
276
|
+
if (kind === "grep") return redactGrep(text, input, cwd, config, roots);
|
|
277
|
+
if (kind === "find") return redactFind(text, input, cwd, config, roots);
|
|
278
|
+
if (kind === "ls") return redactLs(text, input, cwd, config, roots);
|
|
279
|
+
if (kind === "bash" || kind === "powershell")
|
|
280
|
+
return redactBash(text, cwd, config, roots);
|
|
281
|
+
return { text, redacted: false };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function inputPath(input: Record<string, unknown>): string {
|
|
285
|
+
const raw =
|
|
286
|
+
input.path ?? input.file ?? input.filePath ?? input.file_path ?? undefined;
|
|
287
|
+
return typeof raw === "string" ? raw.trim() : "";
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Redact a tool result. Returns `undefined` when nothing changed (the caller
|
|
292
|
+
* leaves the result untouched). Any change drops `structuredContent`, per the
|
|
293
|
+
* Pi tool_result contract.
|
|
294
|
+
*/
|
|
295
|
+
export function redactToolResult(
|
|
296
|
+
event: RedactableToolResult,
|
|
297
|
+
config: FolderPolicyConfig,
|
|
298
|
+
cwd: string,
|
|
299
|
+
roots?: RedactRoots,
|
|
300
|
+
): RedactedToolResult | undefined {
|
|
301
|
+
const tool = String(event.toolName ?? "");
|
|
302
|
+
const readTools = new Set([
|
|
303
|
+
"read",
|
|
304
|
+
"read_symbol",
|
|
305
|
+
"read_enclosing",
|
|
306
|
+
"module_report",
|
|
307
|
+
"effective_config",
|
|
308
|
+
]);
|
|
309
|
+
if (readTools.has(tool)) {
|
|
310
|
+
const target = inputPath(event.input);
|
|
311
|
+
if (target && isProtected(path.resolve(cwd, target), config, cwd, roots)) {
|
|
312
|
+
return {
|
|
313
|
+
content: [
|
|
314
|
+
{
|
|
315
|
+
type: "text",
|
|
316
|
+
text: "[blocked by read policy: protected file]",
|
|
317
|
+
},
|
|
318
|
+
],
|
|
319
|
+
structuredContent: undefined,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
return undefined;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (!["grep", "find", "ls", "bash", "powershell"].includes(tool))
|
|
326
|
+
return undefined;
|
|
327
|
+
let changed = false;
|
|
328
|
+
const content = (event.content ?? []).map((block) => {
|
|
329
|
+
if (block?.type !== "text" || typeof block.text !== "string") return block;
|
|
330
|
+
const result = redactSensitiveText(
|
|
331
|
+
block.text,
|
|
332
|
+
tool,
|
|
333
|
+
event.input ?? {},
|
|
334
|
+
cwd,
|
|
335
|
+
config,
|
|
336
|
+
roots,
|
|
337
|
+
);
|
|
338
|
+
if (!result.redacted) return block;
|
|
339
|
+
changed = true;
|
|
340
|
+
return { ...block, text: result.text };
|
|
341
|
+
});
|
|
342
|
+
if (!changed) return undefined;
|
|
343
|
+
|
|
344
|
+
const result: RedactedToolResult = { content, structuredContent: undefined };
|
|
345
|
+
if (event.details && typeof event.details === "object") {
|
|
346
|
+
const details = { ...(event.details as Record<string, unknown>) };
|
|
347
|
+
if (tool === "bash" || tool === "powershell") {
|
|
348
|
+
delete details.fullOutputPath;
|
|
349
|
+
delete details.full_output_path;
|
|
350
|
+
}
|
|
351
|
+
result.details = details;
|
|
352
|
+
}
|
|
353
|
+
return result;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** A short display path for refusals/diagnostics. */
|
|
357
|
+
export function displayRedactPath(value: string, cwd: string): string {
|
|
358
|
+
return toPosixPath(path.relative(cwd, value) || value);
|
|
359
|
+
}
|