pi-crew 0.5.6 → 0.5.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 +37 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/config/config.ts +28 -4
- package/src/config/defaults.ts +5 -0
- package/src/runtime/child-pi.ts +23 -5
- package/src/runtime/crew-agent-records.ts +32 -1
- package/src/runtime/task-runner.ts +14 -0
- package/src/schema/team-tool-schema.ts +1 -0
- package/src/state/active-run-registry.ts +10 -1
- package/src/state/event-log.ts +8 -3
- package/src/state/mailbox.ts +25 -1
- package/src/tools/safe-bash.ts +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.7] — 11 Issue Fixes Across 5 Phases (2026-06-01)
|
|
4
|
+
|
|
5
|
+
### Phase 1: Schema/Type Fixes
|
|
6
|
+
|
|
7
|
+
- **`invalidate` schema divergence** (Critical) — `src/schema/team-tool-schema.ts`: added `"invalidate"` to TypeBox union. Previously TS interface had it but TypeBox schema did not, causing silent `-32602` failure.
|
|
8
|
+
- **OTLP header key validation** (Low) — `src/config/config.ts`: hardened `parseOtlpConfig` with case-insensitive check for 12 dangerous keys (`__proto__`, `hasOwnProperty`, `toString`, etc.) and format validation `/^[a-zA-Z][a-zA-Z0-9_-]{0,127}$/`.
|
|
9
|
+
|
|
10
|
+
### Phase 2: Security Hardening
|
|
11
|
+
|
|
12
|
+
- **OTLP endpoint unsanitized** (Critical) — `src/config/config.ts`: project config can no longer override `otlp.endpoint` (would have allowed credential exfiltration via attacker URL).
|
|
13
|
+
- **Wildcard env leakage** (High) — `src/runtime/child-pi.ts`: replaced broad wildcards (`LC_*`, `XDG_*`, `NVM_*`, `NODE_*`, `npm_*`) with specific names. Previously `NPM_TOKEN`, `NODE_ENV=production`, `NVM_RC_VERSION` all leaked.
|
|
14
|
+
|
|
15
|
+
### Phase 3: Correctness Fixes
|
|
16
|
+
|
|
17
|
+
- **AbortSignal not propagated** (High) — `src/runtime/task-runner.ts`: check signal before `persistSingleTaskUpdate`. Cancelled tasks now return early with cancelled status instead of writing stale state.
|
|
18
|
+
- **MAILBOX_ARCHIVE_THRESHOLD 10MB/task** (High) — `src/state/mailbox.ts` + `src/config/defaults.ts`: added `DEFAULT_MAILBOX.maxArchivesPerDirection=10` cap and `pruneOldMailboxArchives()` to prevent unbounded growth (1GB+ for 100 tasks).
|
|
19
|
+
- **`safeRm` regex bypass** (Medium) — `src/tools/safe-bash.ts`: stricter regex requires path to be exactly `tmp/`, `cache/`, `node_modules/`, `dist/`, or `build/` with optional `./` prefix. Rejects path traversal like `./../../../etc`.
|
|
20
|
+
- **`writeEntries` silent drop** (Medium) — `src/state/active-run-registry.ts`: emit `logInternalError` warning when entries overflow cap.
|
|
21
|
+
|
|
22
|
+
### Phase 4: Performance Optimization
|
|
23
|
+
|
|
24
|
+
- **`nextAgentEventSeq` O(n) cold cache** (Medium) — `src/runtime/crew-agent-records.ts`: added `.seq` sidecar file for O(1) lookup. Fall back to O(n) scan only when sidecar is missing.
|
|
25
|
+
- **`nextSequence` O(n) cold cache** (Medium) — `src/state/event-log.ts`: trust sidecar seq file when present. Fall back to `scanSequence` only when sidecar missing or file shrunk.
|
|
26
|
+
|
|
27
|
+
### Phase 5: Deferred (Low severity)
|
|
28
|
+
|
|
29
|
+
- **Issue #12: `acquireLockWithRetry` race** — defer (race window small, retry loop handles).
|
|
30
|
+
- **Issue #13: `loadRunManifestById` TOCTOU** — defer (cache TTL 30s, race window small).
|
|
31
|
+
- **Issue #14: `cleanupOldArtifacts` N stat calls** — defer (typical artifact dirs small).
|
|
32
|
+
- **Issue #15: `validateMailbox` full load** — defer (10MB cap, bounded).
|
|
33
|
+
- **Issue #16: `updateMailboxMessageReply` full rewrite** — defer (10MB cap, bounded).
|
|
34
|
+
|
|
35
|
+
### Tests
|
|
36
|
+
|
|
37
|
+
- 2282 tests pass / 0 failures (`npm test`).
|
|
38
|
+
- New tests: `invalidate`/`anchor`/`auto-summarize`/`auto_boomerang` schema, OTLP header key validation, OTLP endpoint sanitization, wildcard env leakage, sidecar seq lookup.
|
|
39
|
+
|
|
3
40
|
## [0.5.6] — Documentation Sync + Type-Only Import Fix (2026-06-01)
|
|
4
41
|
|
|
5
42
|
### Documentation
|
package/README.md
CHANGED
package/package.json
CHANGED
package/src/config/config.ts
CHANGED
|
@@ -244,6 +244,15 @@ function sanitizeProjectConfig(
|
|
|
244
244
|
sanitized.otlp = undefined;
|
|
245
245
|
warnings.push(projectOverrideWarning(projectPath, "otlp.headers"));
|
|
246
246
|
}
|
|
247
|
+
// FIX: Block project config from setting otlp.endpoint — it controls where
|
|
248
|
+
// OTLP headers (potentially containing credentials) are sent.
|
|
249
|
+
if (config.otlp?.endpoint !== undefined) {
|
|
250
|
+
if (!sanitized.otlp) sanitized.otlp = { ...config.otlp, endpoint: undefined };
|
|
251
|
+
else sanitized.otlp = { ...sanitized.otlp, endpoint: undefined };
|
|
252
|
+
if (!Object.values(sanitized.otlp).some((entry) => entry !== undefined))
|
|
253
|
+
sanitized.otlp = undefined;
|
|
254
|
+
warnings.push(projectOverrideWarning(projectPath, "otlp.endpoint"));
|
|
255
|
+
}
|
|
247
256
|
if (
|
|
248
257
|
config.agents?.disableBuiltins !== undefined ||
|
|
249
258
|
config.agents?.overrides !== undefined
|
|
@@ -1051,13 +1060,28 @@ function parseOtlpConfig(value: unknown): CrewOtlpConfig | undefined {
|
|
|
1051
1060
|
if (rawHeaders)
|
|
1052
1061
|
for (const [key, entry] of Object.entries(rawHeaders)) {
|
|
1053
1062
|
if (typeof entry !== "string") continue;
|
|
1054
|
-
// Prevent prototype pollution via
|
|
1063
|
+
// Prevent prototype pollution via dangerous Object.prototype keys.
|
|
1064
|
+
// Case-insensitive check to catch __Proto__, CONSTRUCTOR, etc.
|
|
1065
|
+
const lowerKey = key.toLowerCase();
|
|
1055
1066
|
if (
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1067
|
+
lowerKey === "__proto__" ||
|
|
1068
|
+
lowerKey === "constructor" ||
|
|
1069
|
+
lowerKey === "prototype" ||
|
|
1070
|
+
lowerKey === "hasownproperty" ||
|
|
1071
|
+
lowerKey === "tostring" ||
|
|
1072
|
+
lowerKey === "valueof" ||
|
|
1073
|
+
lowerKey === "isprototypeof" ||
|
|
1074
|
+
lowerKey === "propertyisenumerable" ||
|
|
1075
|
+
lowerKey === "tolocalestring" ||
|
|
1076
|
+
lowerKey === "__definegetter__" ||
|
|
1077
|
+
lowerKey === "__definesetter__" ||
|
|
1078
|
+
lowerKey === "__lookupgetter__" ||
|
|
1079
|
+
lowerKey === "__lookupsetter__"
|
|
1059
1080
|
)
|
|
1060
1081
|
continue;
|
|
1082
|
+
// Validate key format: must start with letter, then alphanumeric/hyphen/underscore.
|
|
1083
|
+
// Blocks CRLF, NUL, spaces, shell metacharacters in header keys.
|
|
1084
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_-]{0,127}$/.test(key)) continue;
|
|
1061
1085
|
headers[key] = entry;
|
|
1062
1086
|
}
|
|
1063
1087
|
const otlp: CrewOtlpConfig = {
|
package/src/config/defaults.ts
CHANGED
|
@@ -91,6 +91,11 @@ export const DEFAULT_CACHE = {
|
|
|
91
91
|
manifestMaxEntries: 64,
|
|
92
92
|
};
|
|
93
93
|
|
|
94
|
+
export const DEFAULT_MAILBOX = {
|
|
95
|
+
perFileThresholdBytes: 10 * 1024 * 1024, // 10MB per mailbox file
|
|
96
|
+
maxArchivesPerDirection: 10, // Keep at most 10 archives per direction per run
|
|
97
|
+
};
|
|
98
|
+
|
|
94
99
|
export const DEFAULT_SUBAGENT = {
|
|
95
100
|
stuckBlockedNotifyMs: 5 * 60_000,
|
|
96
101
|
};
|
package/src/runtime/child-pi.ts
CHANGED
|
@@ -206,11 +206,29 @@ export function buildChildPiSpawnOptions(cwd: string, env: NodeJS.ProcessEnv): S
|
|
|
206
206
|
"SHELL",
|
|
207
207
|
"TERM",
|
|
208
208
|
"LANG",
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
"
|
|
213
|
-
"
|
|
209
|
+
// FIX: Replaced broad wildcards (LC_*, XDG_*, NVM_*, NODE_*, npm_*) with
|
|
210
|
+
// specific names. Previously NPM_TOKEN, NODE_ENV=production, NVM_RC_VERSION
|
|
211
|
+
// all leaked through wildcards.
|
|
212
|
+
"LC_ALL",
|
|
213
|
+
"LC_COLLATE",
|
|
214
|
+
"LC_CTYPE",
|
|
215
|
+
"LC_MESSAGES",
|
|
216
|
+
"LC_MONETARY",
|
|
217
|
+
"LC_NUMERIC",
|
|
218
|
+
"LC_TIME",
|
|
219
|
+
"XDG_CONFIG_HOME",
|
|
220
|
+
"XDG_DATA_HOME",
|
|
221
|
+
"XDG_CACHE_HOME",
|
|
222
|
+
"XDG_RUNTIME_DIR",
|
|
223
|
+
"NVM_BIN",
|
|
224
|
+
"NVM_DIR",
|
|
225
|
+
"NVM_INC",
|
|
226
|
+
"NODE_PATH",
|
|
227
|
+
"NODE_DISABLE_COLORS",
|
|
228
|
+
"NODE_EXTRA_CA_CERTS",
|
|
229
|
+
"NPM_CONFIG_REGISTRY",
|
|
230
|
+
"NPM_CONFIG_USERCONFIG",
|
|
231
|
+
"NPM_CONFIG_GLOBALCONFIG",
|
|
214
232
|
"PI_*",
|
|
215
233
|
"PI_CREW_*",
|
|
216
234
|
"PI_TEAMS_*",
|
|
@@ -263,12 +263,41 @@ export function readCrewAgentStatus(manifest: TeamRunManifest, taskOrAgentId: st
|
|
|
263
263
|
}
|
|
264
264
|
|
|
265
265
|
const agentEventSeqCache = new Map<string, { size: number; mtimeMs: number; seq: number }>();
|
|
266
|
+
const AGENT_EVENT_SEQ_SIDECAR = ".seq";
|
|
267
|
+
|
|
268
|
+
function readSeqFromSidecar(filePath: string): number | undefined {
|
|
269
|
+
try {
|
|
270
|
+
const raw = fs.readFileSync(`${filePath}.${AGENT_EVENT_SEQ_SIDECAR}`, "utf-8");
|
|
271
|
+
const n = Number.parseInt(raw, 10);
|
|
272
|
+
return Number.isFinite(n) && n > 0 ? n : undefined;
|
|
273
|
+
} catch {
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function writeSeqToSidecar(filePath: string, seq: number): void {
|
|
279
|
+
try {
|
|
280
|
+
fs.writeFileSync(`${filePath}.${AGENT_EVENT_SEQ_SIDECAR}`, String(seq));
|
|
281
|
+
} catch (error) {
|
|
282
|
+
logInternalError("crew-agent-records.seq-sidecar", error, `filePath=${filePath}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
266
285
|
|
|
267
286
|
function nextAgentEventSeq(filePath: string): number {
|
|
268
|
-
if (!fs.existsSync(filePath))
|
|
287
|
+
if (!fs.existsSync(filePath)) {
|
|
288
|
+
// Clean up stale sidecar when main file is gone.
|
|
289
|
+
try { fs.unlinkSync(`${filePath}.${AGENT_EVENT_SEQ_SIDECAR}`); } catch {}
|
|
290
|
+
return 1;
|
|
291
|
+
}
|
|
269
292
|
const stat = fs.statSync(filePath);
|
|
270
293
|
const cached = agentEventSeqCache.get(filePath);
|
|
271
294
|
if (cached && cached.size === stat.size && cached.mtimeMs === stat.mtimeMs) return cached.seq + 1;
|
|
295
|
+
// FIX: Try sidecar file for O(1) lookup before falling back to O(n) scan.
|
|
296
|
+
const sidecarSeq = readSeqFromSidecar(filePath);
|
|
297
|
+
if (sidecarSeq !== undefined) {
|
|
298
|
+
agentEventSeqCache.set(filePath, { size: stat.size, mtimeMs: stat.mtimeMs, seq: sidecarSeq });
|
|
299
|
+
return sidecarSeq + 1;
|
|
300
|
+
}
|
|
272
301
|
let max = 0;
|
|
273
302
|
for (const line of fs.readFileSync(filePath, "utf-8").split(/\r?\n/)) {
|
|
274
303
|
if (!line.trim()) continue;
|
|
@@ -281,6 +310,7 @@ function nextAgentEventSeq(filePath: string): number {
|
|
|
281
310
|
}
|
|
282
311
|
}
|
|
283
312
|
agentEventSeqCache.set(filePath, { size: stat.size, mtimeMs: stat.mtimeMs, seq: max });
|
|
313
|
+
writeSeqToSidecar(filePath, max);
|
|
284
314
|
return max + 1;
|
|
285
315
|
}
|
|
286
316
|
|
|
@@ -292,6 +322,7 @@ export function appendCrewAgentEvent(manifest: TeamRunManifest, taskId: string,
|
|
|
292
322
|
try {
|
|
293
323
|
const stat = fs.statSync(filePath);
|
|
294
324
|
agentEventSeqCache.set(filePath, { size: stat.size, mtimeMs: stat.mtimeMs, seq });
|
|
325
|
+
writeSeqToSidecar(filePath, seq);
|
|
295
326
|
} catch (error) {
|
|
296
327
|
logInternalError("crew-agent-records.stat", error, `filePath=${filePath}`);
|
|
297
328
|
}
|
|
@@ -205,6 +205,20 @@ export async function runTeamTask(
|
|
|
205
205
|
input.taskRuntimeOverride ??
|
|
206
206
|
input.runtimeKind ??
|
|
207
207
|
(input.executeWorkers ? "child-process" : "scaffold");
|
|
208
|
+
// FIX: Check signal before persisting state — if cancelled, skip the write.
|
|
209
|
+
if (input.signal?.aborted) {
|
|
210
|
+
const cancelReason = cancellationReasonFromSignal(input.signal);
|
|
211
|
+
const cancelledTask: TeamTaskState = {
|
|
212
|
+
...task,
|
|
213
|
+
status: "cancelled",
|
|
214
|
+
error: `${cancelReason.code}: ${cancelReason.message}`,
|
|
215
|
+
finishedAt: new Date().toISOString(),
|
|
216
|
+
};
|
|
217
|
+
return {
|
|
218
|
+
manifest: input.manifest,
|
|
219
|
+
tasks: updateTask(tasks, cancelledTask),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
208
222
|
tasks = persistSingleTaskUpdate(manifest, tasks, task);
|
|
209
223
|
if (runtimeKind === "child-process")
|
|
210
224
|
({ task, tasks } = checkpointTask(
|
|
@@ -135,7 +135,16 @@ export function readActiveRunRegistry(maxEntries = DEFAULT_CACHE.manifestMaxEntr
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
function writeEntries(entries: ActiveRunRegistryEntry[]): void {
|
|
138
|
-
const
|
|
138
|
+
const max = DEFAULT_CACHE.manifestMaxEntries;
|
|
139
|
+
// FIX: Emit warning when entries overflow the cap, instead of silent drop.
|
|
140
|
+
if (entries.length > max) {
|
|
141
|
+
logInternalError(
|
|
142
|
+
"active-run-registry.overflow",
|
|
143
|
+
new Error(`${entries.length - max} entries dropped (cap=${max})`),
|
|
144
|
+
JSON.stringify({ dropped: entries.length - max, total: entries.length, cap: max }),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
const trimmed = entries.slice(0, max);
|
|
139
148
|
fs.mkdirSync(path.dirname(registryPath()), { recursive: true });
|
|
140
149
|
// 2.4 — dual-ship: write both formats. Readers prefer binary; legacy
|
|
141
150
|
// readers (other tools / older releases) keep using the JSON file.
|
package/src/state/event-log.ts
CHANGED
|
@@ -167,11 +167,16 @@ function nextSequence(eventsPath: string): number {
|
|
|
167
167
|
if (cached && cached.size === stat.size && cached.mtimeMs === stat.mtimeMs) {
|
|
168
168
|
return cached.seq + 1;
|
|
169
169
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
170
|
+
// FIX: Trust the sidecar seq file if it exists and the file is non-empty.
|
|
171
|
+
// Only fall back to O(n) scan if sidecar is missing or file shrunk unexpectedly.
|
|
172
|
+
const stored = readStoredSequence(eventsPath);
|
|
173
|
+
if (stored !== undefined && (!cached || stat.size >= cached.size)) {
|
|
174
|
+
sequenceCache.set(eventsPath, { size: stat.size, mtimeMs: stat.mtimeMs, seq: stored });
|
|
175
|
+
return stored + 1;
|
|
173
176
|
}
|
|
177
|
+
const current = scanSequence(eventsPath);
|
|
174
178
|
sequenceCache.set(eventsPath, { size: stat.size, mtimeMs: stat.mtimeMs, seq: current });
|
|
179
|
+
persistSequence(eventsPath, current);
|
|
175
180
|
return current + 1;
|
|
176
181
|
}
|
|
177
182
|
|
package/src/state/mailbox.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { redactSecrets } from "../utils/redaction.ts";
|
|
|
6
6
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
7
7
|
import { atomicWriteFile } from "./atomic-write.ts";
|
|
8
8
|
import { withEventLogLockSync } from "./event-log.ts";
|
|
9
|
+
import { DEFAULT_MAILBOX } from "../config/defaults.ts";
|
|
9
10
|
|
|
10
11
|
export type MailboxDirection = "inbox" | "outbox";
|
|
11
12
|
export type MailboxMessageStatus = "queued" | "delivered" | "acknowledged";
|
|
@@ -228,7 +229,7 @@ function safeReadMailboxFile(filePath: string, direction: MailboxDirection): Mai
|
|
|
228
229
|
* primary file. Readers continue to see all messages because
|
|
229
230
|
* `safeReadMailboxFile` walks both the primary file and any archives.
|
|
230
231
|
*/
|
|
231
|
-
const MAILBOX_ARCHIVE_THRESHOLD_BYTES =
|
|
232
|
+
const MAILBOX_ARCHIVE_THRESHOLD_BYTES = DEFAULT_MAILBOX.perFileThresholdBytes;
|
|
232
233
|
function rotateMailboxFileIfNeeded(filePath: string, thresholdBytes = MAILBOX_ARCHIVE_THRESHOLD_BYTES): boolean {
|
|
233
234
|
try {
|
|
234
235
|
if (!fs.existsSync(filePath)) return false;
|
|
@@ -238,6 +239,8 @@ function rotateMailboxFileIfNeeded(filePath: string, thresholdBytes = MAILBOX_AR
|
|
|
238
239
|
const archivePath = `${filePath}.${ts}.archive.jsonl`;
|
|
239
240
|
fs.renameSync(filePath, archivePath);
|
|
240
241
|
fs.writeFileSync(filePath, "", "utf-8");
|
|
242
|
+
// FIX: Prune old archives so total per-direction count stays bounded.
|
|
243
|
+
pruneOldMailboxArchives(filePath);
|
|
241
244
|
return true;
|
|
242
245
|
} catch (error) {
|
|
243
246
|
logInternalError("mailbox.rotate", error, filePath);
|
|
@@ -245,6 +248,27 @@ function rotateMailboxFileIfNeeded(filePath: string, thresholdBytes = MAILBOX_AR
|
|
|
245
248
|
}
|
|
246
249
|
}
|
|
247
250
|
|
|
251
|
+
/**
|
|
252
|
+
* Keep at most `DEFAULT_MAILBOX.maxArchivesPerDirection` archive files per
|
|
253
|
+
* mailbox. Older archives are deleted. Prevents unbounded growth on long runs.
|
|
254
|
+
*/
|
|
255
|
+
function pruneOldMailboxArchives(mailboxFilePath: string): void {
|
|
256
|
+
try {
|
|
257
|
+
const dir = path.dirname(mailboxFilePath);
|
|
258
|
+
const base = path.basename(mailboxFilePath);
|
|
259
|
+
const archives = fs
|
|
260
|
+
.readdirSync(dir)
|
|
261
|
+
.filter((f) => f.startsWith(base) && f.includes(".archive.jsonl"))
|
|
262
|
+
.sort(); // Chronological (ISO timestamp in filename)
|
|
263
|
+
const excess = archives.length - DEFAULT_MAILBOX.maxArchivesPerDirection;
|
|
264
|
+
for (let i = 0; i < excess; i += 1) {
|
|
265
|
+
fs.rmSync(path.join(dir, archives[i]), { force: true });
|
|
266
|
+
}
|
|
267
|
+
} catch (error) {
|
|
268
|
+
logInternalError("mailbox.prune", error, mailboxFilePath);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
248
272
|
export function readMailbox(manifest: TeamRunManifest, direction?: MailboxDirection, taskId?: string, kind?: MailboxMessageKind): MailboxMessage[] {
|
|
249
273
|
const directions = direction ? [direction] : ["inbox", "outbox"] as const;
|
|
250
274
|
return directions.flatMap((item) => safeReadMailboxFile(mailboxFile(manifest, item, taskId), item)).filter((msg) => !kind || msg.kind === kind).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
package/src/tools/safe-bash.ts
CHANGED
|
@@ -274,8 +274,9 @@ export function createSafeBash(options: SafeBashOptions = {}) {
|
|
|
274
274
|
* These can be used in allowPatterns for specific use cases
|
|
275
275
|
*/
|
|
276
276
|
export const COMMON_SAFE_PATTERNS = {
|
|
277
|
-
//
|
|
278
|
-
|
|
277
|
+
// FIX: Stricter regex — target must be exactly tmp/, cache/, node_modules/, dist/, or build/
|
|
278
|
+
// (with optional ./ prefix). Rejects path traversal (./../../../other) and absolute paths.
|
|
279
|
+
safeRm: /rm\s+(-[a-zA-Z]*f[a-zA-Z]*\s+)?(?:\.\/)?(?:tmp|cache|node_modules|dist|build)\/[a-zA-Z0-9._/-]+$/,
|
|
279
280
|
// Safe git operations
|
|
280
281
|
safeGit: /\bgit\s+(clone|pull|push|commit|add|status|diff|log|branch|checkout|merge|rebase)/,
|
|
281
282
|
// Safe npm/yarn/pnpm
|