@wrongstack/plugins 0.307.1 → 0.308.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/dist/accessibility-auditor/index.d.ts +5 -0
- package/dist/accessibility-auditor.js +67 -11
- package/dist/audit/index.d.ts +6 -6
- package/dist/audit.js +7 -7
- package/dist/code-metrics/index.d.ts +9 -0
- package/dist/code-metrics.js +42 -11
- package/dist/dep-guard.js +4 -2
- package/dist/dependency-vulnerability-gate/index.d.ts +4 -0
- package/dist/dependency-vulnerability-gate.js +435 -62
- package/dist/factories/index.d.ts +1 -1
- package/dist/factories.js +1 -1
- package/dist/file-watcher.js +25 -10
- package/dist/format-on-save.js +10 -4
- package/dist/import-organizer/index.d.ts +4 -0
- package/dist/import-organizer.js +26 -1
- package/dist/index.js +719 -234
- package/dist/license-audit-gate/index.d.ts +1 -0
- package/dist/license-audit-gate.js +445 -83
- package/dist/lint-gate/index.d.ts +13 -0
- package/dist/lint-gate.js +13 -10
- package/dist/llm-cache.js +6 -0
- package/dist/manifest.js +1 -1
- package/dist/path-guard/glob.d.ts +23 -0
- package/dist/path-guard/index.d.ts +2 -0
- package/dist/path-guard.js +214 -32
- package/dist/plugin-audit-catalog.js +7 -7
- package/dist/prompt-firewall/index.d.ts +49 -40
- package/dist/prompt-firewall.js +302 -29
- package/dist/runtime/h1-state.d.ts +62 -0
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/redos-guard.d.ts +69 -0
- package/dist/runtime/sandbox.d.ts +59 -0
- package/dist/runtime.js +210 -19
- package/dist/secret-scanner.js +16 -2
- package/dist/spec-linker.js +1 -1
- package/dist/type-gate.js +33 -3
- package/package.json +7 -7
package/dist/lint-gate.js
CHANGED
|
@@ -221,9 +221,10 @@ async function detectLinter(requested, cwd) {
|
|
|
221
221
|
}
|
|
222
222
|
async function lintContent(content, filePath, linter, timeoutMs, cwd, signal) {
|
|
223
223
|
const ext = filePath.includes(".") ? filePath.slice(filePath.lastIndexOf(".")) : ".ts";
|
|
224
|
-
|
|
225
|
-
const tmpFile = join(tmpDir, `input${ext}`);
|
|
224
|
+
let tmpDir;
|
|
226
225
|
try {
|
|
226
|
+
tmpDir = await mkdtemp(join(tmpdir(), "lint-gate-"));
|
|
227
|
+
const tmpFile = join(tmpDir, `input${ext}`);
|
|
227
228
|
await writeFile(tmpFile, content, "utf-8");
|
|
228
229
|
const fullArgs = [...linter.args, tmpFile];
|
|
229
230
|
const result = await runCommand(linter.cmd, fullArgs, timeoutMs, cwd, signal);
|
|
@@ -234,14 +235,15 @@ async function lintContent(content, filePath, linter, timeoutMs, cwd, signal) {
|
|
|
234
235
|
if (signal.aborted) throw signal.reason;
|
|
235
236
|
return null;
|
|
236
237
|
} finally {
|
|
237
|
-
await rm(tmpDir, { recursive: true, force: true }).catch(() => void 0);
|
|
238
|
+
if (tmpDir) await rm(tmpDir, { recursive: true, force: true }).catch(() => void 0);
|
|
238
239
|
}
|
|
239
240
|
}
|
|
240
241
|
async function lintAndFix(content, filePath, linter, timeoutMs, cwd, signal) {
|
|
241
242
|
const ext = filePath.includes(".") ? filePath.slice(filePath.lastIndexOf(".")) : ".ts";
|
|
242
|
-
|
|
243
|
-
const tmpFile = join(tmpDir, `input${ext}`);
|
|
243
|
+
let tmpDir;
|
|
244
244
|
try {
|
|
245
|
+
tmpDir = await mkdtemp(join(tmpdir(), "lint-gate-fix-"));
|
|
246
|
+
const tmpFile = join(tmpDir, `input${ext}`);
|
|
245
247
|
await writeFile(tmpFile, content, "utf-8");
|
|
246
248
|
const fixArgs = linter.name === "biome" ? [linter.args[0], "check", "--write", tmpFile] : [linter.args[0], "--fix", tmpFile];
|
|
247
249
|
await runCommand(linter.cmd, fixArgs, timeoutMs, cwd, signal);
|
|
@@ -251,7 +253,7 @@ async function lintAndFix(content, filePath, linter, timeoutMs, cwd, signal) {
|
|
|
251
253
|
if (signal.aborted) throw signal.reason;
|
|
252
254
|
return content;
|
|
253
255
|
} finally {
|
|
254
|
-
await rm(tmpDir, { recursive: true, force: true }).catch(() => void 0);
|
|
256
|
+
if (tmpDir) await rm(tmpDir, { recursive: true, force: true }).catch(() => void 0);
|
|
255
257
|
}
|
|
256
258
|
}
|
|
257
259
|
function parseLinterOutput(stdout, linterName) {
|
|
@@ -505,9 +507,9 @@ ${summary}${truncated}`
|
|
|
505
507
|
name: "lint-gate",
|
|
506
508
|
stage: "mutate",
|
|
507
509
|
timeoutMs: Math.max(1e3, cfg.timeoutMs + 1e3),
|
|
508
|
-
//
|
|
509
|
-
//
|
|
510
|
-
failurePolicy: "
|
|
510
|
+
// Fail closed: a linter crash or timeout must not let unlinted
|
|
511
|
+
// content through in block mode (issue #363).
|
|
512
|
+
failurePolicy: "closed"
|
|
511
513
|
});
|
|
512
514
|
api.tools.register({
|
|
513
515
|
name: "lint_gate_status",
|
|
@@ -579,5 +581,6 @@ ${summary}${truncated}`
|
|
|
579
581
|
};
|
|
580
582
|
var lint_gate_default = plugin;
|
|
581
583
|
export {
|
|
582
|
-
lint_gate_default as default
|
|
584
|
+
lint_gate_default as default,
|
|
585
|
+
resolveLocalLinter
|
|
583
586
|
};
|
package/dist/llm-cache.js
CHANGED
|
@@ -82,6 +82,12 @@ var plugin = {
|
|
|
82
82
|
description: "Caches identical provider requests and short-circuits the provider call on a hit (wrapProviderRunner). Opt-in; deterministic-only by default.",
|
|
83
83
|
apiVersion: "^0.1.10",
|
|
84
84
|
capabilities: { tools: true },
|
|
85
|
+
// Wrap-stack contract (issue #362): ExtensionRegistry composes wrappers
|
|
86
|
+
// first-registered = outermost, and both dependsOn and optionalDeps load
|
|
87
|
+
// dependencies first. Declaring prompt-firewall here guarantees the
|
|
88
|
+
// firewall wraps OUTSIDE this cache even if the manifest order changes —
|
|
89
|
+
// otherwise a cache hit short-circuits `inner` and bypasses redaction.
|
|
90
|
+
optionalDeps: ["prompt-firewall"],
|
|
85
91
|
defaultConfig: { ...DEFAULTS },
|
|
86
92
|
configSchema: {
|
|
87
93
|
type: "object",
|
package/dist/manifest.js
CHANGED
|
@@ -35,10 +35,10 @@ var OFFICIAL_PLUGIN_NAMES = [
|
|
|
35
35
|
"notify-hub",
|
|
36
36
|
"changelog-writer",
|
|
37
37
|
"injection-shield",
|
|
38
|
+
"prompt-firewall",
|
|
38
39
|
"llm-cache",
|
|
39
40
|
"model-router",
|
|
40
41
|
"pr-drafter",
|
|
41
|
-
"prompt-firewall",
|
|
42
42
|
"auto-escalate",
|
|
43
43
|
"test-coverage-gate",
|
|
44
44
|
"type-gate",
|
|
@@ -2,6 +2,20 @@ export interface WriteTarget {
|
|
|
2
2
|
path: string;
|
|
3
3
|
kind: 'file' | 'scope' | 'deletion-scope';
|
|
4
4
|
}
|
|
5
|
+
/** Default wall-clock budget per glob match (ms). Must clear worker spin-up (~20ms). */
|
|
6
|
+
export declare const GLOB_REDOS_BUDGET_MS = 250;
|
|
7
|
+
export interface GuardedMatchOptions {
|
|
8
|
+
/** Invoked when a match times out — wired to the plugin's redosTimeouts counter. */
|
|
9
|
+
onTimeout?: (info: {
|
|
10
|
+
regex: RegExp;
|
|
11
|
+
input: string;
|
|
12
|
+
budgetMs: number;
|
|
13
|
+
elapsedMs: number;
|
|
14
|
+
}) => void;
|
|
15
|
+
/** Per-regex wall-clock budget. Default GLOB_REDOS_BUDGET_MS. */
|
|
16
|
+
budgetMs?: number;
|
|
17
|
+
}
|
|
18
|
+
export declare function matchesAnyGuarded(path: string, patterns: RegExp[], options?: GuardedMatchOptions): Promise<boolean>;
|
|
5
19
|
/**
|
|
6
20
|
* Compile a glob pattern to a RegExp. Supports `**` (any depth),
|
|
7
21
|
* `*` (within one segment), and `?` (single char). Matching is done
|
|
@@ -27,6 +41,15 @@ export declare function staticPrefix(pattern: string): string;
|
|
|
27
41
|
export declare function hasPartialSegmentWildcard(pattern: string): boolean;
|
|
28
42
|
export declare function globWitness(pattern: string): string;
|
|
29
43
|
export declare function scopesMayOverlap(left: string, right: string): boolean;
|
|
44
|
+
export declare function targetIntersectsScope(target: WriteTarget, patternTexts: string[]): boolean;
|
|
30
45
|
export declare function targetIntersectsPatterns(target: WriteTarget, patternTexts: string[], patterns: RegExp[]): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* ReDoS-guarded variant of `targetIntersectsPatterns` (issue #365 / audit
|
|
48
|
+
* T-02): the direct path match runs through `matchesAnyGuarded` (worker
|
|
49
|
+
* watchdog, fail-closed on timeout); scope-overlap logic stays synchronous
|
|
50
|
+
* because it matches against constructed candidate paths, not the raw
|
|
51
|
+
* hostile input.
|
|
52
|
+
*/
|
|
53
|
+
export declare function targetIntersectsPatternsGuarded(target: WriteTarget, patternTexts: string[], patterns: RegExp[], options?: GuardedMatchOptions): Promise<boolean>;
|
|
31
54
|
export declare function targetFullyAllowed(target: WriteTarget, allowTexts: string[], allowRes: RegExp[]): boolean;
|
|
32
55
|
//# sourceMappingURL=glob.d.ts.map
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
import type { Plugin } from '@wrongstack/core/types';
|
|
8
8
|
export { compilePathGlob } from './glob.js';
|
|
9
9
|
export { destructiveTargets } from './shell-targets.js';
|
|
10
|
+
/** True when `path` is a project-local symlink whose real target escaped. */
|
|
11
|
+
export declare function isSymlinkEscape(path: string, cwd?: string): boolean;
|
|
10
12
|
declare const plugin: Plugin;
|
|
11
13
|
export default plugin;
|
|
12
14
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/path-guard.js
CHANGED
|
@@ -1,4 +1,104 @@
|
|
|
1
|
+
// src/runtime/redos-guard.ts
|
|
2
|
+
import { Worker } from "node:worker_threads";
|
|
3
|
+
function withReDoSGuard(re, input, budgetMs = 50, options = {}) {
|
|
4
|
+
const opts = { budgetMs, ...options };
|
|
5
|
+
const start = Date.now();
|
|
6
|
+
const workerSource = buildWorkerSource(re.source, input, re.flags);
|
|
7
|
+
const worker = new Worker(workerSource, {
|
|
8
|
+
eval: true,
|
|
9
|
+
name: `redos-guard:${re.source.slice(0, 32)}`
|
|
10
|
+
});
|
|
11
|
+
return new Promise((resolve3) => {
|
|
12
|
+
let settled = false;
|
|
13
|
+
const onMessage = (msg) => {
|
|
14
|
+
if (settled) return;
|
|
15
|
+
settled = true;
|
|
16
|
+
clearTimeout(timer);
|
|
17
|
+
worker.terminate().catch(() => {
|
|
18
|
+
});
|
|
19
|
+
if (!msg.ok) {
|
|
20
|
+
resolve3({ timedOut: true, match: null });
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
resolve3({ timedOut: false, match: msg.match });
|
|
24
|
+
};
|
|
25
|
+
const onError = () => {
|
|
26
|
+
if (settled) return;
|
|
27
|
+
settled = true;
|
|
28
|
+
clearTimeout(timer);
|
|
29
|
+
worker.terminate().catch(() => {
|
|
30
|
+
});
|
|
31
|
+
resolve3({ timedOut: true, match: null });
|
|
32
|
+
};
|
|
33
|
+
const timer = setTimeout(() => {
|
|
34
|
+
if (settled) return;
|
|
35
|
+
settled = true;
|
|
36
|
+
const elapsedMs = Date.now() - start;
|
|
37
|
+
worker.terminate().catch(() => {
|
|
38
|
+
});
|
|
39
|
+
try {
|
|
40
|
+
opts.onTimeout?.({
|
|
41
|
+
regex: re,
|
|
42
|
+
input,
|
|
43
|
+
budgetMs: opts.budgetMs,
|
|
44
|
+
elapsedMs
|
|
45
|
+
});
|
|
46
|
+
} catch {
|
|
47
|
+
}
|
|
48
|
+
resolve3({ timedOut: true, match: null });
|
|
49
|
+
}, opts.budgetMs);
|
|
50
|
+
timer.unref?.();
|
|
51
|
+
worker.on("message", onMessage);
|
|
52
|
+
worker.on("error", onError);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function buildWorkerSource(source, input, flags) {
|
|
56
|
+
const S = JSON.stringify(source);
|
|
57
|
+
const I = JSON.stringify(input);
|
|
58
|
+
const F = JSON.stringify(flags);
|
|
59
|
+
return `
|
|
60
|
+
const { parentPort } = require('node:worker_threads');
|
|
61
|
+
const source = ${S};
|
|
62
|
+
const input = ${I};
|
|
63
|
+
const flags = ${F};
|
|
64
|
+
try {
|
|
65
|
+
const re = new RegExp(source, flags);
|
|
66
|
+
const match = re.exec(input);
|
|
67
|
+
// parentPort.postMessage, NOT bare postMessage: with eval:true
|
|
68
|
+
// workers this Node version does not expose the bare postMessage
|
|
69
|
+
// global \u2014 the worker throws ReferenceError at startup and the
|
|
70
|
+
// host misreads it as a timeout (positive-path regression).
|
|
71
|
+
parentPort.postMessage({ ok: true, match });
|
|
72
|
+
} catch (err) {
|
|
73
|
+
parentPort.postMessage({ ok: false, error: err && err.message ? err.message : String(err) });
|
|
74
|
+
}
|
|
75
|
+
`;
|
|
76
|
+
}
|
|
77
|
+
|
|
1
78
|
// src/path-guard/glob.ts
|
|
79
|
+
var GLOB_REDOS_BUDGET_MS = 250;
|
|
80
|
+
function mergeGuardedRegex(patterns) {
|
|
81
|
+
const flags = /* @__PURE__ */ new Set();
|
|
82
|
+
for (const p of patterns) {
|
|
83
|
+
for (const f of p.flags) {
|
|
84
|
+
if (f !== "g" && f !== "y") flags.add(f);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const uniqueFlags = [...flags].join("");
|
|
88
|
+
return new RegExp(patterns.map((p) => p.source).join("|"), uniqueFlags);
|
|
89
|
+
}
|
|
90
|
+
async function matchesAnyGuarded(path, patterns, options = {}) {
|
|
91
|
+
const normalized = normalizePath(path);
|
|
92
|
+
if (patterns.length === 0) return false;
|
|
93
|
+
const result = await withReDoSGuard(
|
|
94
|
+
mergeGuardedRegex(patterns),
|
|
95
|
+
normalized,
|
|
96
|
+
options.budgetMs ?? GLOB_REDOS_BUDGET_MS,
|
|
97
|
+
options.onTimeout ? { onTimeout: options.onTimeout } : {}
|
|
98
|
+
);
|
|
99
|
+
if (result.timedOut) return true;
|
|
100
|
+
return result.match !== null;
|
|
101
|
+
}
|
|
2
102
|
function compilePathGlob(pattern) {
|
|
3
103
|
const normalized = pattern.replace(/\\/g, "/");
|
|
4
104
|
let source = "";
|
|
@@ -89,8 +189,8 @@ function isRootPathScope(path) {
|
|
|
89
189
|
function isDirectoryAmbiguousPath(path) {
|
|
90
190
|
const normalized = normalizePath(path).replace(/\/$/, "");
|
|
91
191
|
if (isRootPathScope(normalized)) return true;
|
|
92
|
-
const
|
|
93
|
-
return path.endsWith("/") ||
|
|
192
|
+
const basename2 = normalized.slice(normalized.lastIndexOf("/") + 1);
|
|
193
|
+
return path.endsWith("/") || basename2.length > 0 && !basename2.includes(".");
|
|
94
194
|
}
|
|
95
195
|
function hasConfiguredProtectedDescendant(path, patterns) {
|
|
96
196
|
const normalized = normalizePath(path).replace(/\/$/, "").toLowerCase();
|
|
@@ -133,8 +233,7 @@ function scopesMayOverlap(left, right) {
|
|
|
133
233
|
}
|
|
134
234
|
return hasPartialSegmentWildcard(left) && rightPrefix.startsWith(leftPrefix) || hasPartialSegmentWildcard(right) && leftPrefix.startsWith(rightPrefix);
|
|
135
235
|
}
|
|
136
|
-
function
|
|
137
|
-
if (matchesAny(target.path, patterns)) return true;
|
|
236
|
+
function targetIntersectsScope(target, patternTexts) {
|
|
138
237
|
if (target.kind === "file") return false;
|
|
139
238
|
const normalized = normalizePath(target.path).replace(/\/$/, "");
|
|
140
239
|
if (!isUnresolvedPathScope(normalized)) {
|
|
@@ -146,6 +245,14 @@ function targetIntersectsPatterns(target, patternTexts, patterns) {
|
|
|
146
245
|
}
|
|
147
246
|
return patternTexts.some((pattern) => scopesMayOverlap(normalized, normalizePath(pattern)));
|
|
148
247
|
}
|
|
248
|
+
function targetIntersectsPatterns(target, patternTexts, patterns) {
|
|
249
|
+
if (matchesAny(target.path, patterns)) return true;
|
|
250
|
+
return targetIntersectsScope(target, patternTexts);
|
|
251
|
+
}
|
|
252
|
+
async function targetIntersectsPatternsGuarded(target, patternTexts, patterns, options = {}) {
|
|
253
|
+
if (await matchesAnyGuarded(target.path, patterns, options)) return true;
|
|
254
|
+
return targetIntersectsScope(target, patternTexts);
|
|
255
|
+
}
|
|
149
256
|
function targetFullyAllowed(target, allowTexts, allowRes) {
|
|
150
257
|
if (target.kind === "file") return matchesAny(target.path, allowRes);
|
|
151
258
|
const normalized = normalizePath(target.path).replace(/\/$/, "");
|
|
@@ -462,7 +569,7 @@ function commandRecursivelyDeletes(command) {
|
|
|
462
569
|
)) {
|
|
463
570
|
return true;
|
|
464
571
|
}
|
|
465
|
-
const destructive = /(?:^|[;&|\r\n]\s*|\{\s*|\(\s*|\bxargs(?:\s+-[^\s]+)*\s+)(rm|rmdir|del)\s+((?:"[^"]*"|'[^']*'|\\.|\{[^}]*\}|\([^()]*\)|[^;&|\r\n}])+)/gi;
|
|
572
|
+
const destructive = /(?:^|[;&|\r\n]\s*|\{\s*|\(\s*|\bxargs(?:\s+-[^\s]+)*\s+)(rm|rmdir|del|rd|Remove-Item)\s+((?:"[^"]*"|'[^']*'|\\.|\{[^}]*\}|\([^()]*\)|[^;&|\r\n}])+)/gi;
|
|
466
573
|
let match = destructive.exec(stripped);
|
|
467
574
|
while (match !== null) {
|
|
468
575
|
const tool = match[1]?.toLowerCase();
|
|
@@ -475,6 +582,8 @@ function commandRecursivelyDeletes(command) {
|
|
|
475
582
|
if (token === "--") break;
|
|
476
583
|
if (tool === "rm") {
|
|
477
584
|
if (token === "--recursive" || /^-[^-]*[rR]/.test(token)) recursive = true;
|
|
585
|
+
} else if (tool === "remove-item") {
|
|
586
|
+
if (/^-Recurse$/i.test(token) || /^-r$/i.test(token)) recursive = true;
|
|
478
587
|
} else if (/^\/[a-z]*s[a-z]*$/i.test(token)) {
|
|
479
588
|
recursive = true;
|
|
480
589
|
}
|
|
@@ -723,7 +832,7 @@ function destructiveTargetsAtDepth(command, depth) {
|
|
|
723
832
|
}
|
|
724
833
|
targets.push(...destructiveTargetsAtDepth(executableBody, depth + 1));
|
|
725
834
|
}
|
|
726
|
-
const destructive = /(?:^|[;&|\r\n]\s*|\{\s*|(?<![$(])\(\s*|\bxargs(?:\s+-[^\s]+)*\s+)(?:sudo\s+)?(rm|rmdir|del|unlink|truncate|shred|mv)\s+((?:"[^"]*"|'[^']*'|\\.|\{[^}]*\}|\([^()]*\)|[^;&|\r\n}])+)/gi;
|
|
835
|
+
const destructive = /(?:^|[;&|\r\n]\s*|\{\s*|(?<![$(])\(\s*|\bxargs(?:\s+-[^\s]+)*\s+)(?:sudo\s+)?(rm|rmdir|del|rd|Remove-Item|unlink|truncate|shred|mv)\s+((?:"[^"]*"|'[^']*'|\\.|\{[^}]*\}|\([^()]*\)|[^;&|\r\n}])+)/gi;
|
|
727
836
|
let m = destructive.exec(normalizedCommand);
|
|
728
837
|
while (m !== null) {
|
|
729
838
|
if (!tokenIsQuoted(m, m[1] ?? "")) targets.push(...shellArgs(m[2] ?? ""));
|
|
@@ -1211,8 +1320,52 @@ function operationLabel(toolName) {
|
|
|
1211
1320
|
}
|
|
1212
1321
|
|
|
1213
1322
|
// src/path-guard/index.ts
|
|
1323
|
+
import { realpathSync } from "node:fs";
|
|
1324
|
+
import { resolve as resolve2 } from "node:path";
|
|
1325
|
+
|
|
1326
|
+
// src/runtime/index.ts
|
|
1327
|
+
import { basename, extname, isAbsolute, relative, resolve } from "node:path";
|
|
1328
|
+
|
|
1329
|
+
// src/runtime/local-bin.ts
|
|
1330
|
+
import { buildWin32CmdShimInvocation, resolveWin32Command } from "@wrongstack/tools/win32";
|
|
1331
|
+
|
|
1332
|
+
// src/runtime/index.ts
|
|
1333
|
+
var MAX_BUFFER_BYTES = 16 * 1024 * 1024;
|
|
1334
|
+
function hasLeadingDash(arg) {
|
|
1335
|
+
return arg.length > 0 && arg.startsWith("-");
|
|
1336
|
+
}
|
|
1337
|
+
function withinProjectPath(projectRoot, candidate) {
|
|
1338
|
+
if (candidate.length === 0 || candidate.length > 4096) return false;
|
|
1339
|
+
if (hasLeadingDash(candidate)) return false;
|
|
1340
|
+
const resolved = isAbsolute(candidate) ? resolve(candidate) : resolve(projectRoot, candidate);
|
|
1341
|
+
const rel = relative(projectRoot, resolved);
|
|
1342
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
1343
|
+
}
|
|
1344
|
+
function withinProject(p) {
|
|
1345
|
+
const cwd = process.cwd();
|
|
1346
|
+
return withinProjectPath(cwd, p) || relative(cwd, p) === ".";
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// src/path-guard/index.ts
|
|
1350
|
+
function isSymlinkEscape(path, cwd) {
|
|
1351
|
+
if (!withinProject(path)) return false;
|
|
1352
|
+
try {
|
|
1353
|
+
const abs = resolve2(cwd ?? process.cwd(), path);
|
|
1354
|
+
const real = realpathSync(abs);
|
|
1355
|
+
return !withinProject(real);
|
|
1356
|
+
} catch {
|
|
1357
|
+
return false;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1214
1360
|
function createState() {
|
|
1215
|
-
return {
|
|
1361
|
+
return {
|
|
1362
|
+
invocations: 0,
|
|
1363
|
+
blocks: 0,
|
|
1364
|
+
warns: 0,
|
|
1365
|
+
redosTimeouts: 0,
|
|
1366
|
+
lastBlock: null,
|
|
1367
|
+
hookUnregister: null
|
|
1368
|
+
};
|
|
1216
1369
|
}
|
|
1217
1370
|
var states = /* @__PURE__ */ new WeakMap();
|
|
1218
1371
|
var latestState = createState();
|
|
@@ -1307,7 +1460,11 @@ var plugin = {
|
|
|
1307
1460
|
additionalContext: `path-guard (warn mode): ${subject} and this ${operation} would modify it. Double-check this is intentional.`
|
|
1308
1461
|
};
|
|
1309
1462
|
};
|
|
1310
|
-
const
|
|
1463
|
+
const bumpRedos = () => {
|
|
1464
|
+
state.redosTimeouts += 1;
|
|
1465
|
+
api.metrics.counter("redos_timeouts");
|
|
1466
|
+
};
|
|
1467
|
+
const hook = async (input) => {
|
|
1311
1468
|
if (!cfg.enabled) return;
|
|
1312
1469
|
state.invocations += 1;
|
|
1313
1470
|
const toolName = input.toolName ?? "";
|
|
@@ -1328,21 +1485,27 @@ var plugin = {
|
|
|
1328
1485
|
const effectiveCwd = effectiveToolCwd(ti["cwd"], input.cwd);
|
|
1329
1486
|
const recursivelyDeletes = commandRecursivelyDeletes(commandForInspection);
|
|
1330
1487
|
const deletesImplicitScope = commandDeletesImplicitScope(commandForInspection);
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1488
|
+
const shellHits = await Promise.all(
|
|
1489
|
+
shellTargets.map(async (path) => {
|
|
1490
|
+
const target = {
|
|
1491
|
+
path: relativeToInvocationCwd(resolveTargetPath(path, effectiveCwd), input.cwd),
|
|
1492
|
+
kind: isRootPathScope(path) && deletesImplicitScope || isUnresolvedPathScope(path) || recursivelyDeletes && (isDirectoryAmbiguousPath(path) || hasConfiguredProtectedDescendant(path, cfg.protect)) ? "deletion-scope" : "file"
|
|
1493
|
+
};
|
|
1494
|
+
if (targetFullyAllowed(target, cfg.allow, allowRes)) return null;
|
|
1495
|
+
const protectedShellTarget = await targetIntersectsPatternsGuarded(target, cfg.protect, protectRes, { onTimeout: bumpRedos }) || await matchesAnyGuarded(`${target.path.replace(/\/$/, "")}/.path-guard-probe`, protectRes, {
|
|
1496
|
+
onTimeout: bumpRedos
|
|
1497
|
+
});
|
|
1498
|
+
return protectedShellTarget ? target : null;
|
|
1499
|
+
})
|
|
1500
|
+
);
|
|
1501
|
+
const firstProtected = shellHits.find((t) => t !== null);
|
|
1502
|
+
if (firstProtected) {
|
|
1503
|
+
return verdict(
|
|
1504
|
+
firstProtected.path,
|
|
1505
|
+
toolName,
|
|
1506
|
+
"destructive shell command",
|
|
1507
|
+
firstProtected.kind !== "file"
|
|
1508
|
+
);
|
|
1346
1509
|
}
|
|
1347
1510
|
const writes = writesToDisk({ ...input, toolInput: ti });
|
|
1348
1511
|
const hasStructuredTarget = [...PATH_FIELDS, ...PATH_LIST_FIELDS].some((field) => ti[field] !== void 0) || typeof ti["patch"] === "string";
|
|
@@ -1353,11 +1516,21 @@ var plugin = {
|
|
|
1353
1516
|
}
|
|
1354
1517
|
if (!writesToDisk({ ...input, toolInput: ti }) || isReadOnlyInvocation(toolName, ti)) return;
|
|
1355
1518
|
const targets = pathsFromToolInput(ti, toolName, input.cwd);
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1519
|
+
const writeHits = await Promise.all(
|
|
1520
|
+
targets.map(async (target) => {
|
|
1521
|
+
if (target.kind === "file" && isSymlinkEscape(target.path, input.cwd)) return target;
|
|
1522
|
+
if (targetFullyAllowed(target, cfg.allow, allowRes)) return null;
|
|
1523
|
+
return targetIntersectsPatterns(target, cfg.protect, protectRes) ? target : null;
|
|
1524
|
+
})
|
|
1525
|
+
);
|
|
1526
|
+
const firstProtectedWrite = writeHits.find((t) => t !== null);
|
|
1527
|
+
if (firstProtectedWrite) {
|
|
1528
|
+
return verdict(
|
|
1529
|
+
firstProtectedWrite.path,
|
|
1530
|
+
toolName,
|
|
1531
|
+
operationLabel(toolName),
|
|
1532
|
+
firstProtectedWrite.kind !== "file"
|
|
1533
|
+
);
|
|
1361
1534
|
}
|
|
1362
1535
|
return;
|
|
1363
1536
|
};
|
|
@@ -1384,7 +1557,8 @@ var plugin = {
|
|
|
1384
1557
|
counters: {
|
|
1385
1558
|
invocations: state.invocations,
|
|
1386
1559
|
blocks: state.blocks,
|
|
1387
|
-
warns: state.warns
|
|
1560
|
+
warns: state.warns,
|
|
1561
|
+
redosTimeouts: state.redosTimeouts
|
|
1388
1562
|
},
|
|
1389
1563
|
lastBlock: state.lastBlock
|
|
1390
1564
|
};
|
|
@@ -1407,10 +1581,16 @@ var plugin = {
|
|
|
1407
1581
|
}
|
|
1408
1582
|
state.hookUnregister = null;
|
|
1409
1583
|
}
|
|
1410
|
-
const final = {
|
|
1584
|
+
const final = {
|
|
1585
|
+
invocations: state.invocations,
|
|
1586
|
+
blocks: state.blocks,
|
|
1587
|
+
warns: state.warns,
|
|
1588
|
+
redosTimeouts: state.redosTimeouts
|
|
1589
|
+
};
|
|
1411
1590
|
state.invocations = 0;
|
|
1412
1591
|
state.blocks = 0;
|
|
1413
1592
|
state.warns = 0;
|
|
1593
|
+
state.redosTimeouts = 0;
|
|
1414
1594
|
state.lastBlock = null;
|
|
1415
1595
|
states.delete(api);
|
|
1416
1596
|
api.log.info("path-guard: teardown complete", { final });
|
|
@@ -1423,7 +1603,8 @@ var plugin = {
|
|
|
1423
1603
|
counters: {
|
|
1424
1604
|
invocations: state.invocations,
|
|
1425
1605
|
blocks: state.blocks,
|
|
1426
|
-
warns: state.warns
|
|
1606
|
+
warns: state.warns,
|
|
1607
|
+
redosTimeouts: state.redosTimeouts
|
|
1427
1608
|
}
|
|
1428
1609
|
};
|
|
1429
1610
|
}
|
|
@@ -1432,5 +1613,6 @@ var path_guard_default = plugin;
|
|
|
1432
1613
|
export {
|
|
1433
1614
|
compilePathGlob,
|
|
1434
1615
|
path_guard_default as default,
|
|
1435
|
-
destructiveTargets
|
|
1616
|
+
destructiveTargets,
|
|
1617
|
+
isSymlinkEscape
|
|
1436
1618
|
};
|
|
@@ -245,6 +245,13 @@ var OFFICIAL_PLUGIN_AUDIT_ENTRIES = [
|
|
|
245
245
|
defaultState: "active",
|
|
246
246
|
canDisable: true
|
|
247
247
|
},
|
|
248
|
+
{
|
|
249
|
+
name: "prompt-firewall",
|
|
250
|
+
risk: "high",
|
|
251
|
+
summary: "Scans the provider wire for credential leaks before context reaches the LLM API (wrapProviderRunner); redact/warn/block. Opt-in; redact by default.",
|
|
252
|
+
defaultState: "inactive",
|
|
253
|
+
canDisable: true
|
|
254
|
+
},
|
|
248
255
|
{
|
|
249
256
|
name: "llm-cache",
|
|
250
257
|
risk: "medium",
|
|
@@ -266,13 +273,6 @@ var OFFICIAL_PLUGIN_AUDIT_ENTRIES = [
|
|
|
266
273
|
defaultState: "inactive",
|
|
267
274
|
canDisable: true
|
|
268
275
|
},
|
|
269
|
-
{
|
|
270
|
-
name: "prompt-firewall",
|
|
271
|
-
risk: "high",
|
|
272
|
-
summary: "Scans the provider wire for credential leaks before context reaches the LLM API (wrapProviderRunner); redact/warn/block. Opt-in; redact by default.",
|
|
273
|
-
defaultState: "inactive",
|
|
274
|
-
canDisable: true
|
|
275
|
-
},
|
|
276
276
|
{
|
|
277
277
|
name: "auto-escalate",
|
|
278
278
|
risk: "medium",
|
|
@@ -1,52 +1,61 @@
|
|
|
1
|
+
import type { Plugin } from '@wrongstack/core/types';
|
|
1
2
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* sits on `AgentExtension.wrapProviderRunner`, so it sees the FULL
|
|
7
|
-
* request that is about to be sent to a third-party LLM provider —
|
|
8
|
-
* regardless of how a secret entered the conversation. It scans the
|
|
9
|
-
* outgoing request's system + message text for high-confidence
|
|
10
|
-
* credential patterns and, depending on `mode`:
|
|
11
|
-
*
|
|
12
|
-
* - `warn` — logs + counts + emits a `prompt-firewall:leak`
|
|
13
|
-
* event; the request goes through unchanged
|
|
14
|
-
* - `redact` (default) — replaces each match with `[REDACTED:<kind>]` in a CLONE
|
|
15
|
-
* of the request before sending, and also redacts secrets echoed back
|
|
16
|
-
* in the response
|
|
17
|
-
* - `block` — throws before the request is sent (the agent's error
|
|
18
|
-
* path surfaces it), so the secret never reaches the provider
|
|
19
|
-
*
|
|
20
|
-
* Safety posture: opt-in — loads inert until
|
|
21
|
-
* `config.extensions['prompt-firewall'].enabled = true`. `redact` is the
|
|
22
|
-
* default so secrets are automatically stripped; switch to `warn` only
|
|
23
|
-
* for detection-mode diagnostics without data loss.
|
|
24
|
-
*
|
|
25
|
-
* Config (`config.extensions['prompt-firewall']`):
|
|
26
|
-
*
|
|
27
|
-
* ```jsonc
|
|
28
|
-
* {
|
|
29
|
-
* "enabled": false,
|
|
30
|
-
* "mode": "redact", // "warn" | "redact" | "block"
|
|
31
|
-
* "scanResponse": true, // redact secrets echoed back (redact mode)
|
|
32
|
-
* "allow": [] // regex source strings to exempt (false positives)
|
|
33
|
-
* }
|
|
34
|
-
* ```
|
|
35
|
-
*
|
|
36
|
-
* Tools:
|
|
37
|
-
* - `prompt_firewall_status` — mode, pattern names, detection counters
|
|
38
|
-
*
|
|
39
|
-
* @public
|
|
3
|
+
* Legacy `kind` names for the shapes this plugin already reported, so the
|
|
4
|
+
* canonical `type` ids from the shared table keep their existing spelling
|
|
5
|
+
* in logs, metrics and the status tool. Anything not listed here is
|
|
6
|
+
* reported under its canonical id.
|
|
40
7
|
*/
|
|
41
|
-
|
|
8
|
+
export declare const KIND_ALIASES: Readonly<Record<string, string>>;
|
|
42
9
|
export interface Detection {
|
|
43
10
|
kind: string;
|
|
44
11
|
count: number;
|
|
45
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* A pattern excluded from a scan pass. `redos-timeout` = the pattern blew
|
|
15
|
+
* its wall-clock budget on the guarded probe and was skipped (fail-open
|
|
16
|
+
* with a visible counter — this surface trades blocking for availability;
|
|
17
|
+
* `secret-scanner` is the fail-closed tool-side gate).
|
|
18
|
+
*/
|
|
19
|
+
export interface ScanSkip {
|
|
20
|
+
kind: string;
|
|
21
|
+
reason: 'redos-timeout';
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Mutable deadline shared across one scan pass. `tripped` collects the
|
|
25
|
+
* kinds that blew the budget mid-pass; the caller merges them into the
|
|
26
|
+
* visible skip surface (status + counters + warn log).
|
|
27
|
+
*/
|
|
28
|
+
interface ScanDeadline {
|
|
29
|
+
deadline: number;
|
|
30
|
+
tripped: Set<string>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Hard bound on any single synchronous regex input in a scan pass. The
|
|
34
|
+
* structural guarantee of #371: even a growth-loop re-measure never hands
|
|
35
|
+
* a regex the full unbounded leaf.
|
|
36
|
+
*/
|
|
37
|
+
export declare const SCAN_WINDOW_LIMIT: number;
|
|
46
38
|
/** Detect secret matches in text. Returns per-kind counts (no values). */
|
|
47
39
|
export declare function detectSecrets(text: string, allow: RegExp[]): Detection[];
|
|
48
40
|
/** Redact secret matches in text, replacing each with `[REDACTED:<kind>]`. */
|
|
49
|
-
export declare function redactSecrets(text: string, allow: RegExp[]): {
|
|
41
|
+
export declare function redactSecrets(text: string, allow: RegExp[], deadline?: ScanDeadline): {
|
|
42
|
+
text: string;
|
|
43
|
+
redactions: number;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Detect secret matches, skipping patterns that blow the ReDoS budget on
|
|
47
|
+
* the guarded probe. The skip set is returned so callers can surface it
|
|
48
|
+
* (status counters / logs) instead of silently trusting the result.
|
|
49
|
+
*/
|
|
50
|
+
export declare function detectSecretsGuarded(text: string, allow: RegExp[]): Promise<{
|
|
51
|
+
detections: Detection[];
|
|
52
|
+
skipped: ScanSkip[];
|
|
53
|
+
}>;
|
|
54
|
+
/**
|
|
55
|
+
* Redact secret matches, skipping the given timed-out patterns (reuse the
|
|
56
|
+
* set from `detectSecretsGuarded` so a request pays the probe once).
|
|
57
|
+
*/
|
|
58
|
+
export declare function redactSecretsGuarded(text: string, allow: RegExp[], skip: ReadonlySet<string>, deadline?: ScanDeadline): {
|
|
50
59
|
text: string;
|
|
51
60
|
redactions: number;
|
|
52
61
|
};
|