@wrongstack/plugins 0.308.0 → 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/audit/index.d.ts +6 -6
- package/dist/audit.js +7 -7
- package/dist/factories/index.d.ts +1 -1
- package/dist/factories.js +1 -1
- package/dist/import-organizer.js +4 -0
- package/dist/index.js +217 -35
- package/dist/llm-cache.js +6 -0
- package/dist/manifest.js +1 -1
- package/dist/plugin-audit-catalog.js +7 -7
- package/dist/prompt-firewall/index.d.ts +43 -41
- package/dist/prompt-firewall.js +217 -34
- package/dist/spec-linker.js +1 -1
- package/package.json +7 -7
package/dist/audit/index.d.ts
CHANGED
|
@@ -208,6 +208,12 @@ export declare const OFFICIAL_PLUGIN_AUDIT_ENTRIES: readonly [{
|
|
|
208
208
|
readonly summary: 'Scans tool output (fetched pages, files) for prompt-injection patterns and warns the model that content is data, not instructions';
|
|
209
209
|
readonly defaultState: 'active';
|
|
210
210
|
readonly canDisable: true;
|
|
211
|
+
}, {
|
|
212
|
+
readonly name: 'prompt-firewall';
|
|
213
|
+
readonly risk: 'high';
|
|
214
|
+
readonly summary: 'Scans the provider wire for credential leaks before context reaches the LLM API (wrapProviderRunner); redact/warn/block. Opt-in; redact by default.';
|
|
215
|
+
readonly defaultState: 'inactive';
|
|
216
|
+
readonly canDisable: true;
|
|
211
217
|
}, {
|
|
212
218
|
readonly name: 'llm-cache';
|
|
213
219
|
readonly risk: 'medium';
|
|
@@ -226,12 +232,6 @@ export declare const OFFICIAL_PLUGIN_AUDIT_ENTRIES: readonly [{
|
|
|
226
232
|
readonly summary: 'Collects session work (commits, edited files, diff) and drafts a pull-request description';
|
|
227
233
|
readonly defaultState: 'inactive';
|
|
228
234
|
readonly canDisable: true;
|
|
229
|
-
}, {
|
|
230
|
-
readonly name: 'prompt-firewall';
|
|
231
|
-
readonly risk: 'high';
|
|
232
|
-
readonly summary: 'Scans the provider wire for credential leaks before context reaches the LLM API (wrapProviderRunner); redact/warn/block. Opt-in; redact by default.';
|
|
233
|
-
readonly defaultState: 'inactive';
|
|
234
|
-
readonly canDisable: true;
|
|
235
235
|
}, {
|
|
236
236
|
readonly name: 'auto-escalate';
|
|
237
237
|
readonly risk: 'medium';
|
package/dist/audit.js
CHANGED
|
@@ -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,4 +1,4 @@
|
|
|
1
1
|
import type { Plugin } from '@wrongstack/core/types';
|
|
2
|
-
export declare const OFFICIAL_PLUGIN_SPECIFIERS: readonly ['@wrongstack/plugins/agent-handoff', '@wrongstack/plugins/cost-tracker', '@wrongstack/plugins/file-watcher', '@wrongstack/plugins/git-autocommit', '@wrongstack/plugins/auto-doc', '@wrongstack/plugins/shell-check', '@wrongstack/plugins/cron', '@wrongstack/plugins/template-engine', '@wrongstack/plugins/semver-bump', '@wrongstack/plugins/secret-scanner', '@wrongstack/plugins/todo-tracker', '@wrongstack/plugins/token-budget', '@wrongstack/plugins/lint-gate', '@wrongstack/plugins/branch-guard', '@wrongstack/plugins/diff-summary', '@wrongstack/plugins/commit-validator', '@wrongstack/plugins/format-on-save', '@wrongstack/plugins/test-runner-gate', '@wrongstack/plugins/import-organizer', '@wrongstack/plugins/knowledge-graph', '@wrongstack/plugins/todo-listener', '@wrongstack/plugins/session-recap', '@wrongstack/plugins/spec-linker', '@wrongstack/plugins/loop-breaker', '@wrongstack/plugins/gitignore-guard', '@wrongstack/plugins/path-guard', '@wrongstack/plugins/process-guard', '@wrongstack/plugins/context-pins', '@wrongstack/plugins/checkpoint', '@wrongstack/plugins/error-lens', '@wrongstack/plugins/dep-guard', '@wrongstack/plugins/config-validator', '@wrongstack/plugins/notify-hub', '@wrongstack/plugins/changelog-writer', '@wrongstack/plugins/injection-shield', '@wrongstack/plugins/
|
|
2
|
+
export declare const OFFICIAL_PLUGIN_SPECIFIERS: readonly ['@wrongstack/plugins/agent-handoff', '@wrongstack/plugins/cost-tracker', '@wrongstack/plugins/file-watcher', '@wrongstack/plugins/git-autocommit', '@wrongstack/plugins/auto-doc', '@wrongstack/plugins/shell-check', '@wrongstack/plugins/cron', '@wrongstack/plugins/template-engine', '@wrongstack/plugins/semver-bump', '@wrongstack/plugins/secret-scanner', '@wrongstack/plugins/todo-tracker', '@wrongstack/plugins/token-budget', '@wrongstack/plugins/lint-gate', '@wrongstack/plugins/branch-guard', '@wrongstack/plugins/diff-summary', '@wrongstack/plugins/commit-validator', '@wrongstack/plugins/format-on-save', '@wrongstack/plugins/test-runner-gate', '@wrongstack/plugins/import-organizer', '@wrongstack/plugins/knowledge-graph', '@wrongstack/plugins/todo-listener', '@wrongstack/plugins/session-recap', '@wrongstack/plugins/spec-linker', '@wrongstack/plugins/loop-breaker', '@wrongstack/plugins/gitignore-guard', '@wrongstack/plugins/path-guard', '@wrongstack/plugins/process-guard', '@wrongstack/plugins/context-pins', '@wrongstack/plugins/checkpoint', '@wrongstack/plugins/error-lens', '@wrongstack/plugins/dep-guard', '@wrongstack/plugins/config-validator', '@wrongstack/plugins/notify-hub', '@wrongstack/plugins/changelog-writer', '@wrongstack/plugins/injection-shield', '@wrongstack/plugins/prompt-firewall', '@wrongstack/plugins/llm-cache', '@wrongstack/plugins/model-router', '@wrongstack/plugins/pr-drafter', '@wrongstack/plugins/auto-escalate', '@wrongstack/plugins/test-coverage-gate', '@wrongstack/plugins/type-gate', '@wrongstack/plugins/token-throttle', '@wrongstack/plugins/plugin-stack-observer', '@wrongstack/plugins/dependency-vulnerability-gate', '@wrongstack/plugins/migration-planner', '@wrongstack/plugins/semantic-search-indexer', '@wrongstack/plugins/auto-i18n-extractor', '@wrongstack/plugins/doc-sync-guard', '@wrongstack/plugins/api-compatibility-gate', '@wrongstack/plugins/performance-regression-gate', '@wrongstack/plugins/test-flake-detector', '@wrongstack/plugins/schema-evolution-guard', '@wrongstack/plugins/license-audit-gate', '@wrongstack/plugins/accessibility-auditor', '@wrongstack/plugins/security-hotspot-scanner', '@wrongstack/plugins/dead-code-detector', '@wrongstack/plugins/duplicate-code-detector', '@wrongstack/plugins/code-metrics', '@wrongstack/plugins/refactor-suggester', '@wrongstack/plugins/test-generator', '@wrongstack/plugins/release-notes-generator', '@wrongstack/plugins/smart-rename', '@wrongstack/plugins/feature-flag-tracker', '@wrongstack/plugins/interface-contract-guard'];
|
|
3
3
|
export declare const OFFICIAL_PLUGIN_FACTORIES: readonly (() => Promise<Plugin>)[];
|
|
4
4
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/factories.js
CHANGED
|
@@ -35,10 +35,10 @@ var OFFICIAL_PLUGIN_SPECIFIERS = [
|
|
|
35
35
|
"@wrongstack/plugins/notify-hub",
|
|
36
36
|
"@wrongstack/plugins/changelog-writer",
|
|
37
37
|
"@wrongstack/plugins/injection-shield",
|
|
38
|
+
"@wrongstack/plugins/prompt-firewall",
|
|
38
39
|
"@wrongstack/plugins/llm-cache",
|
|
39
40
|
"@wrongstack/plugins/model-router",
|
|
40
41
|
"@wrongstack/plugins/pr-drafter",
|
|
41
|
-
"@wrongstack/plugins/prompt-firewall",
|
|
42
42
|
"@wrongstack/plugins/auto-escalate",
|
|
43
43
|
"@wrongstack/plugins/test-coverage-gate",
|
|
44
44
|
"@wrongstack/plugins/type-gate",
|
package/dist/import-organizer.js
CHANGED
|
@@ -37,6 +37,9 @@ function cachePut(key, value) {
|
|
|
37
37
|
binCache.set(key, { value, cachedAt: Date.now() });
|
|
38
38
|
return value;
|
|
39
39
|
}
|
|
40
|
+
function clearLocalBinCache() {
|
|
41
|
+
binCache.clear();
|
|
42
|
+
}
|
|
40
43
|
function resolveNodeBin(packageName, binName, cwd, extraArgs = []) {
|
|
41
44
|
const key = `${packageName}|${binName}|${cwd}`;
|
|
42
45
|
const cached = binCache.get(key);
|
|
@@ -450,6 +453,7 @@ ${result.stderr.trim()}`
|
|
|
450
453
|
});
|
|
451
454
|
},
|
|
452
455
|
teardown(api) {
|
|
456
|
+
clearLocalBinCache();
|
|
453
457
|
if (state.hookUnregister) {
|
|
454
458
|
try {
|
|
455
459
|
state.hookUnregister();
|
package/dist/index.js
CHANGED
|
@@ -10298,6 +10298,7 @@ ${result.stderr.trim()}`
|
|
|
10298
10298
|
});
|
|
10299
10299
|
},
|
|
10300
10300
|
teardown(api) {
|
|
10301
|
+
clearLocalBinCache();
|
|
10301
10302
|
if (state25.hookUnregister) {
|
|
10302
10303
|
try {
|
|
10303
10304
|
state25.hookUnregister();
|
|
@@ -11951,6 +11952,12 @@ var plugin34 = {
|
|
|
11951
11952
|
description: "Caches identical provider requests and short-circuits the provider call on a hit (wrapProviderRunner). Opt-in; deterministic-only by default.",
|
|
11952
11953
|
apiVersion: "^0.1.10",
|
|
11953
11954
|
capabilities: { tools: true },
|
|
11955
|
+
// Wrap-stack contract (issue #362): ExtensionRegistry composes wrappers
|
|
11956
|
+
// first-registered = outermost, and both dependsOn and optionalDeps load
|
|
11957
|
+
// dependencies first. Declaring prompt-firewall here guarantees the
|
|
11958
|
+
// firewall wraps OUTSIDE this cache even if the manifest order changes —
|
|
11959
|
+
// otherwise a cache hit short-circuits `inner` and bypasses redaction.
|
|
11960
|
+
optionalDeps: ["prompt-firewall"],
|
|
11954
11961
|
defaultConfig: { ...DEFAULTS28 },
|
|
11955
11962
|
configSchema: {
|
|
11956
11963
|
type: "object",
|
|
@@ -16226,6 +16233,9 @@ var plugin42 = {
|
|
|
16226
16233
|
};
|
|
16227
16234
|
var process_guard_default = plugin42;
|
|
16228
16235
|
|
|
16236
|
+
// src/prompt-firewall/index.ts
|
|
16237
|
+
import { performance as performance2 } from "node:perf_hooks";
|
|
16238
|
+
|
|
16229
16239
|
// src/runtime/credential-patterns.ts
|
|
16230
16240
|
var CREDENTIAL_PATTERNS = [
|
|
16231
16241
|
// LLM provider keys
|
|
@@ -16397,32 +16407,156 @@ var PATTERNS2 = [
|
|
|
16397
16407
|
})),
|
|
16398
16408
|
...EXTRA_PATTERNS
|
|
16399
16409
|
];
|
|
16400
|
-
|
|
16401
|
-
|
|
16402
|
-
|
|
16403
|
-
|
|
16404
|
-
|
|
16410
|
+
var PATTERN_BUDGET_MS = 250;
|
|
16411
|
+
var GUARD_PROBE_LENGTH = 1e5;
|
|
16412
|
+
var GUARD_PROBE_OVERLAP = 4096;
|
|
16413
|
+
var SCAN_PASS_BUDGET_MS = 250;
|
|
16414
|
+
function createScanDeadline() {
|
|
16415
|
+
return { deadline: performance2.now() + SCAN_PASS_BUDGET_MS, tripped: /* @__PURE__ */ new Set() };
|
|
16416
|
+
}
|
|
16417
|
+
var SCAN_WINDOW_STRIDE = GUARD_PROBE_LENGTH - GUARD_PROBE_OVERLAP;
|
|
16418
|
+
var SCAN_WINDOW_TAIL = 65536;
|
|
16419
|
+
var SCAN_WINDOW_LIMIT = GUARD_PROBE_LENGTH + SCAN_WINDOW_TAIL;
|
|
16420
|
+
var SCAN_MAX_GROWTH = 16;
|
|
16421
|
+
function growMatch(re, p, text, absStart, deadline) {
|
|
16422
|
+
let size = GUARD_PROBE_LENGTH;
|
|
16423
|
+
for (let grown = 0; grown < SCAN_MAX_GROWTH; grown++) {
|
|
16424
|
+
if (deadline && performance2.now() > deadline.deadline) {
|
|
16425
|
+
deadline.tripped.add(p.kind);
|
|
16426
|
+
return null;
|
|
16427
|
+
}
|
|
16428
|
+
const extEnd = Math.min(text.length, absStart + size);
|
|
16429
|
+
const ext = text.slice(absStart, extEnd);
|
|
16430
|
+
re.lastIndex = 0;
|
|
16431
|
+
const em = re.exec(ext);
|
|
16432
|
+
if (!em || em.index !== 0 || em[0].length === 0) return null;
|
|
16433
|
+
const end = absStart + em[0].length;
|
|
16434
|
+
if (end < extEnd || extEnd === text.length) {
|
|
16435
|
+
return { start: absStart, end, matched: em[0] };
|
|
16436
|
+
}
|
|
16437
|
+
size *= 2;
|
|
16438
|
+
}
|
|
16439
|
+
return null;
|
|
16440
|
+
}
|
|
16441
|
+
function* execWindowed(p, text, deadline) {
|
|
16442
|
+
const re = new RegExp(p.re.source, p.re.flags);
|
|
16443
|
+
let acceptLo = 0;
|
|
16444
|
+
let highWater = 0;
|
|
16445
|
+
for (let window = 0; acceptLo < text.length; window++) {
|
|
16446
|
+
const sliceStart = window === 0 ? 0 : acceptLo - GUARD_PROBE_OVERLAP;
|
|
16447
|
+
const sliceEnd = Math.min(text.length, sliceStart + SCAN_WINDOW_LIMIT);
|
|
16448
|
+
const slice = text.slice(sliceStart, sliceEnd);
|
|
16449
|
+
const acceptHi = Math.min(acceptLo + SCAN_WINDOW_STRIDE, text.length);
|
|
16450
|
+
re.lastIndex = 0;
|
|
16451
|
+
let m = re.exec(slice);
|
|
16405
16452
|
while (m !== null) {
|
|
16453
|
+
if (deadline && performance2.now() > deadline.deadline) {
|
|
16454
|
+
deadline.tripped.add(p.kind);
|
|
16455
|
+
return;
|
|
16456
|
+
}
|
|
16406
16457
|
const matched = m[0];
|
|
16407
|
-
if (
|
|
16408
|
-
|
|
16458
|
+
if (matched.length === 0) {
|
|
16459
|
+
re.lastIndex += 1;
|
|
16460
|
+
m = re.exec(slice);
|
|
16461
|
+
continue;
|
|
16462
|
+
}
|
|
16463
|
+
const absStart = sliceStart + m.index;
|
|
16464
|
+
const absEnd = absStart + matched.length;
|
|
16465
|
+
if (absStart >= acceptLo && absStart < acceptHi) {
|
|
16466
|
+
let final = { start: absStart, end: absEnd, matched };
|
|
16467
|
+
if (absEnd === sliceEnd && sliceEnd < text.length) {
|
|
16468
|
+
final = growMatch(re, p, text, absStart, deadline) ?? final;
|
|
16469
|
+
re.lastIndex = m.index + final.matched.length;
|
|
16470
|
+
}
|
|
16471
|
+
if (final.start >= highWater) {
|
|
16472
|
+
highWater = Math.max(highWater, final.end);
|
|
16473
|
+
yield final;
|
|
16474
|
+
}
|
|
16409
16475
|
}
|
|
16410
|
-
m =
|
|
16476
|
+
m = re.exec(slice);
|
|
16477
|
+
}
|
|
16478
|
+
acceptLo += SCAN_WINDOW_STRIDE;
|
|
16479
|
+
}
|
|
16480
|
+
}
|
|
16481
|
+
function countMatches(p, text, allow, counts, deadline) {
|
|
16482
|
+
if (deadline && performance2.now() > deadline.deadline) {
|
|
16483
|
+
deadline.tripped.add(p.kind);
|
|
16484
|
+
return;
|
|
16485
|
+
}
|
|
16486
|
+
for (const m of execWindowed(p, text, deadline)) {
|
|
16487
|
+
if (!allow.some((a) => a.test(m.matched))) {
|
|
16488
|
+
counts.set(p.kind, (counts.get(p.kind) ?? 0) + 1);
|
|
16411
16489
|
}
|
|
16412
16490
|
}
|
|
16413
|
-
return [...counts.entries()].map(([kind, count]) => ({ kind, count }));
|
|
16414
16491
|
}
|
|
16415
|
-
function
|
|
16492
|
+
function replacePattern(p, text, allow, redactions, deadline) {
|
|
16493
|
+
if (deadline && performance2.now() > deadline.deadline) {
|
|
16494
|
+
deadline.tripped.add(p.kind);
|
|
16495
|
+
return text;
|
|
16496
|
+
}
|
|
16497
|
+
let out = "";
|
|
16498
|
+
let copied = 0;
|
|
16499
|
+
for (const m of execWindowed(p, text, deadline)) {
|
|
16500
|
+
out += text.slice(copied, m.start);
|
|
16501
|
+
if (allow.some((a) => a.test(m.matched))) {
|
|
16502
|
+
out += m.matched;
|
|
16503
|
+
} else {
|
|
16504
|
+
redactions.n += 1;
|
|
16505
|
+
out += `[REDACTED:${p.kind}]`;
|
|
16506
|
+
}
|
|
16507
|
+
copied = m.end;
|
|
16508
|
+
}
|
|
16509
|
+
return out + text.slice(copied);
|
|
16510
|
+
}
|
|
16511
|
+
function redactSecrets(text, allow, deadline) {
|
|
16512
|
+
const redactions = { n: 0 };
|
|
16416
16513
|
let out = text;
|
|
16417
|
-
|
|
16418
|
-
|
|
16419
|
-
|
|
16420
|
-
|
|
16421
|
-
|
|
16422
|
-
|
|
16514
|
+
for (const p of PATTERNS2) out = replacePattern(p, out, allow, redactions, deadline);
|
|
16515
|
+
return { text: out, redactions: redactions.n };
|
|
16516
|
+
}
|
|
16517
|
+
var DISTINCT_PATTERN_KINDS = new Set(PATTERNS2.map((p) => p.kind)).size;
|
|
16518
|
+
async function probeTimedOutPatterns(text) {
|
|
16519
|
+
const timedOut = /* @__PURE__ */ new Set();
|
|
16520
|
+
if (text.length === 0) return timedOut;
|
|
16521
|
+
const probeWindow = (offset) => {
|
|
16522
|
+
const window = text.slice(offset, offset + GUARD_PROBE_LENGTH);
|
|
16523
|
+
const combined = new RegExp(PATTERNS2.map((p) => `(${p.re.source})`).join("|"), "gi");
|
|
16524
|
+
return withReDoSGuard(combined, window, PATTERN_BUDGET_MS).then(async (combinedResult) => {
|
|
16525
|
+
if (!combinedResult.timedOut) return;
|
|
16526
|
+
for (const p of PATTERNS2) {
|
|
16527
|
+
if (timedOut.has(p.kind)) continue;
|
|
16528
|
+
const result = await withReDoSGuard(p.re, window, PATTERN_BUDGET_MS);
|
|
16529
|
+
if (result.timedOut) timedOut.add(p.kind);
|
|
16530
|
+
}
|
|
16423
16531
|
});
|
|
16532
|
+
};
|
|
16533
|
+
const stride = GUARD_PROBE_LENGTH - GUARD_PROBE_OVERLAP;
|
|
16534
|
+
for (let offset = 0; offset < text.length; offset += stride) {
|
|
16535
|
+
if (timedOut.size >= DISTINCT_PATTERN_KINDS) break;
|
|
16536
|
+
await probeWindow(offset);
|
|
16537
|
+
}
|
|
16538
|
+
return timedOut;
|
|
16539
|
+
}
|
|
16540
|
+
async function detectSecretsGuarded(text, allow) {
|
|
16541
|
+
const timedOut = await probeTimedOutPatterns(text);
|
|
16542
|
+
const deadline = createScanDeadline();
|
|
16543
|
+
const counts = /* @__PURE__ */ new Map();
|
|
16544
|
+
for (const p of PATTERNS2) {
|
|
16545
|
+
if (!timedOut.has(p.kind)) countMatches(p, text, allow, counts, deadline);
|
|
16424
16546
|
}
|
|
16425
|
-
|
|
16547
|
+
const allSkipped = /* @__PURE__ */ new Set([...timedOut, ...deadline.tripped]);
|
|
16548
|
+
return {
|
|
16549
|
+
detections: [...counts.entries()].map(([kind, count]) => ({ kind, count })),
|
|
16550
|
+
skipped: [...allSkipped].sort().map((kind) => ({ kind, reason: "redos-timeout" }))
|
|
16551
|
+
};
|
|
16552
|
+
}
|
|
16553
|
+
function redactSecretsGuarded(text, allow, skip, deadline) {
|
|
16554
|
+
const redactions = { n: 0 };
|
|
16555
|
+
let out = text;
|
|
16556
|
+
for (const p of PATTERNS2) {
|
|
16557
|
+
if (!skip.has(p.kind)) out = replacePattern(p, out, allow, redactions, deadline);
|
|
16558
|
+
}
|
|
16559
|
+
return { text: out, redactions: redactions.n };
|
|
16426
16560
|
}
|
|
16427
16561
|
function collectText(request) {
|
|
16428
16562
|
const parts = [];
|
|
@@ -16436,17 +16570,27 @@ function collectText(request) {
|
|
|
16436
16570
|
walk(request["messages"]);
|
|
16437
16571
|
return parts.join("\n");
|
|
16438
16572
|
}
|
|
16439
|
-
|
|
16573
|
+
var RESPONSE_SCAN_BUDGET = 1e6;
|
|
16574
|
+
function redactDeep(value, allow, counter, skip, budget, deadline) {
|
|
16440
16575
|
if (typeof value === "string") {
|
|
16441
|
-
|
|
16576
|
+
if (budget) {
|
|
16577
|
+
if (value.length > budget.remaining) {
|
|
16578
|
+
budget.remaining = 0;
|
|
16579
|
+
budget.truncated = true;
|
|
16580
|
+
return value;
|
|
16581
|
+
}
|
|
16582
|
+
budget.remaining -= value.length;
|
|
16583
|
+
}
|
|
16584
|
+
const { text, redactions } = skip ? redactSecretsGuarded(value, allow, skip, deadline) : redactSecrets(value, allow, deadline);
|
|
16442
16585
|
counter.n += redactions;
|
|
16443
16586
|
return text;
|
|
16444
16587
|
}
|
|
16445
|
-
if (Array.isArray(value))
|
|
16588
|
+
if (Array.isArray(value))
|
|
16589
|
+
return value.map((v) => redactDeep(v, allow, counter, skip, budget, deadline));
|
|
16446
16590
|
if (value && typeof value === "object") {
|
|
16447
16591
|
const out = {};
|
|
16448
16592
|
for (const [k, v] of Object.entries(value)) {
|
|
16449
|
-
out[k] = redactDeep(v, allow, counter);
|
|
16593
|
+
out[k] = redactDeep(v, allow, counter, skip, budget, deadline);
|
|
16450
16594
|
}
|
|
16451
16595
|
return out;
|
|
16452
16596
|
}
|
|
@@ -16482,16 +16626,33 @@ var state40 = {
|
|
|
16482
16626
|
responseRedactions: 0,
|
|
16483
16627
|
blocked: 0,
|
|
16484
16628
|
timeoutCount: 0,
|
|
16629
|
+
skippedPatterns: [],
|
|
16630
|
+
responseTruncated: false,
|
|
16485
16631
|
byKind: /* @__PURE__ */ new Map(),
|
|
16486
16632
|
lastDetection: null,
|
|
16487
16633
|
extensionUnregister: null
|
|
16488
16634
|
};
|
|
16635
|
+
function surfaceScanTrips(api, tripped) {
|
|
16636
|
+
for (const kind of tripped) {
|
|
16637
|
+
if (!state40.skippedPatterns.includes(kind)) state40.skippedPatterns.push(kind);
|
|
16638
|
+
}
|
|
16639
|
+
state40.timeoutCount += 1;
|
|
16640
|
+
api.metrics.counter("redos_skips", 1);
|
|
16641
|
+
api.log.warn("prompt-firewall: scan-pass budget exceeded \u2014 patterns skipped mid-pass (issue #370)", {
|
|
16642
|
+
skipped: [...tripped]
|
|
16643
|
+
});
|
|
16644
|
+
}
|
|
16489
16645
|
var plugin43 = {
|
|
16490
16646
|
name: "prompt-firewall",
|
|
16491
16647
|
version: "0.1.0",
|
|
16492
16648
|
description: "Scans the provider wire for credential leaks before context reaches the LLM API (wrapProviderRunner); redact/warn/block. Opt-in; redact by default.",
|
|
16493
16649
|
apiVersion: "^0.1.10",
|
|
16494
16650
|
capabilities: { tools: true },
|
|
16651
|
+
// Wrap-stack contract (issue #362): ExtensionRegistry composes wrappers
|
|
16652
|
+
// first-registered = outermost. The manifest lists this plugin before
|
|
16653
|
+
// llm-cache, and llm-cache declares this plugin in optionalDeps, so the
|
|
16654
|
+
// firewall is the outer wrap: every request is scanned/redacted before
|
|
16655
|
+
// llm-cache can fingerprint or cache it.
|
|
16495
16656
|
defaultConfig: { enabled: false, mode: "redact", scanResponse: true, allow: [] },
|
|
16496
16657
|
configSchema: {
|
|
16497
16658
|
type: "object",
|
|
@@ -16527,6 +16688,8 @@ var plugin43 = {
|
|
|
16527
16688
|
state40.responseRedactions = 0;
|
|
16528
16689
|
state40.blocked = 0;
|
|
16529
16690
|
state40.timeoutCount = 0;
|
|
16691
|
+
state40.skippedPatterns = [];
|
|
16692
|
+
state40.responseTruncated = false;
|
|
16530
16693
|
state40.byKind.clear();
|
|
16531
16694
|
state40.lastDetection = null;
|
|
16532
16695
|
if (state40.extensionUnregister) {
|
|
@@ -16550,15 +16713,16 @@ var plugin43 = {
|
|
|
16550
16713
|
const req = request ?? {};
|
|
16551
16714
|
state40.invocations += 1;
|
|
16552
16715
|
const requestText = collectText(req);
|
|
16553
|
-
|
|
16554
|
-
|
|
16555
|
-
|
|
16556
|
-
|
|
16557
|
-
|
|
16716
|
+
const { detections, skipped } = await detectSecretsGuarded(requestText, cfg.allow);
|
|
16717
|
+
state40.skippedPatterns = skipped.map((s) => s.kind);
|
|
16718
|
+
if (skipped.length > 0) {
|
|
16719
|
+
state40.timeoutCount += 1;
|
|
16720
|
+
api.log.warn("prompt-firewall: ReDoS budget exceeded, patterns skipped", {
|
|
16721
|
+
skipped: state40.skippedPatterns
|
|
16558
16722
|
});
|
|
16559
|
-
|
|
16723
|
+
api.metrics.counter("redos_skips", 1);
|
|
16560
16724
|
}
|
|
16561
|
-
const
|
|
16725
|
+
const skipSet = new Set(state40.skippedPatterns);
|
|
16562
16726
|
if (detections.length > 0) {
|
|
16563
16727
|
state40.requestsWithSecrets += 1;
|
|
16564
16728
|
for (const d of detections) {
|
|
@@ -16577,27 +16741,39 @@ var plugin43 = {
|
|
|
16577
16741
|
}
|
|
16578
16742
|
if (cfg.mode === "redact") {
|
|
16579
16743
|
const counter = { n: 0 };
|
|
16580
|
-
const
|
|
16744
|
+
const deadline = createScanDeadline();
|
|
16745
|
+
const redactedReq = redactDeep(req, cfg.allow, counter, skipSet, void 0, deadline);
|
|
16581
16746
|
state40.requestRedactions += counter.n;
|
|
16582
16747
|
api.metrics.counter("request_redactions", counter.n);
|
|
16748
|
+
if (deadline.tripped.size > 0) surfaceScanTrips(api, deadline.tripped);
|
|
16583
16749
|
const response2 = await inner(_ctx, redactedReq);
|
|
16584
|
-
return cfg.scanResponse ? redactResponse(response2, cfg.allow) : response2;
|
|
16750
|
+
return cfg.scanResponse ? redactResponse(response2, cfg.allow, skipSet) : response2;
|
|
16585
16751
|
}
|
|
16586
16752
|
}
|
|
16587
16753
|
const response = await inner(_ctx, request);
|
|
16588
16754
|
if (cfg.mode === "redact" && cfg.scanResponse) {
|
|
16589
|
-
return redactResponse(response, cfg.allow);
|
|
16755
|
+
return redactResponse(response, cfg.allow, skipSet);
|
|
16590
16756
|
}
|
|
16591
16757
|
return response;
|
|
16592
16758
|
}
|
|
16593
16759
|
});
|
|
16594
16760
|
}
|
|
16595
|
-
function redactResponse(response, allow) {
|
|
16761
|
+
function redactResponse(response, allow, skip) {
|
|
16596
16762
|
if (!response || typeof response !== "object") return response;
|
|
16597
16763
|
const counter = { n: 0 };
|
|
16598
16764
|
const content = response.content;
|
|
16599
16765
|
if (content === void 0) return response;
|
|
16600
|
-
const
|
|
16766
|
+
const budget = { remaining: RESPONSE_SCAN_BUDGET, truncated: false };
|
|
16767
|
+
const deadline = createScanDeadline();
|
|
16768
|
+
const redacted = redactDeep(content, allow, counter, skip, budget, deadline);
|
|
16769
|
+
if (deadline.tripped.size > 0) surfaceScanTrips(api, deadline.tripped);
|
|
16770
|
+
if (budget.truncated) {
|
|
16771
|
+
state40.responseTruncated = true;
|
|
16772
|
+
api.log.warn(
|
|
16773
|
+
"prompt-firewall: response scan budget exhausted \u2014 part of the response was returned unredacted"
|
|
16774
|
+
);
|
|
16775
|
+
api.metrics.counter("response_scan_truncated", 1);
|
|
16776
|
+
}
|
|
16601
16777
|
if (counter.n > 0) {
|
|
16602
16778
|
state40.responseRedactions += counter.n;
|
|
16603
16779
|
api.metrics.counter("response_redactions", counter.n);
|
|
@@ -16623,6 +16799,8 @@ var plugin43 = {
|
|
|
16623
16799
|
mode: cfg.mode,
|
|
16624
16800
|
scanResponse: cfg.scanResponse,
|
|
16625
16801
|
patterns: PATTERNS2.map((p) => p.kind),
|
|
16802
|
+
skippedPatterns: state40.skippedPatterns,
|
|
16803
|
+
responseTruncated: state40.responseTruncated,
|
|
16626
16804
|
counters: {
|
|
16627
16805
|
invocations: state40.invocations,
|
|
16628
16806
|
requestsWithSecrets: state40.requestsWithSecrets,
|
|
@@ -16656,13 +16834,17 @@ var plugin43 = {
|
|
|
16656
16834
|
requestsWithSecrets: state40.requestsWithSecrets,
|
|
16657
16835
|
requestRedactions: state40.requestRedactions,
|
|
16658
16836
|
responseRedactions: state40.responseRedactions,
|
|
16659
|
-
blocked: state40.blocked
|
|
16837
|
+
blocked: state40.blocked,
|
|
16838
|
+
timeoutCount: state40.timeoutCount
|
|
16660
16839
|
};
|
|
16661
16840
|
state40.invocations = 0;
|
|
16662
16841
|
state40.requestsWithSecrets = 0;
|
|
16663
16842
|
state40.requestRedactions = 0;
|
|
16664
16843
|
state40.responseRedactions = 0;
|
|
16665
16844
|
state40.blocked = 0;
|
|
16845
|
+
state40.timeoutCount = 0;
|
|
16846
|
+
state40.skippedPatterns = [];
|
|
16847
|
+
state40.responseTruncated = false;
|
|
16666
16848
|
state40.byKind.clear();
|
|
16667
16849
|
state40.lastDetection = null;
|
|
16668
16850
|
api.log.info("prompt-firewall: teardown complete", { final });
|
|
@@ -20629,10 +20811,10 @@ var OFFICIAL_PLUGIN_NAMES = [
|
|
|
20629
20811
|
"notify-hub",
|
|
20630
20812
|
"changelog-writer",
|
|
20631
20813
|
"injection-shield",
|
|
20814
|
+
"prompt-firewall",
|
|
20632
20815
|
"llm-cache",
|
|
20633
20816
|
"model-router",
|
|
20634
20817
|
"pr-drafter",
|
|
20635
|
-
"prompt-firewall",
|
|
20636
20818
|
"auto-escalate",
|
|
20637
20819
|
"test-coverage-gate",
|
|
20638
20820
|
"type-gate",
|
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",
|
|
@@ -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,43 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* prompt-firewall plugin — inspects and redacts secrets on the provider
|
|
3
|
-
* wire, before context leaves for the LLM API and as it returns.
|
|
4
|
-
*
|
|
5
|
-
* Distinct from `secret-scanner` (which guards the TOOL boundary): this
|
|
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
|
|
40
|
-
*/
|
|
41
1
|
import type { Plugin } from '@wrongstack/core/types';
|
|
42
2
|
/**
|
|
43
3
|
* Legacy `kind` names for the shapes this plugin already reported, so the
|
|
@@ -50,10 +10,52 @@ export interface Detection {
|
|
|
50
10
|
kind: string;
|
|
51
11
|
count: number;
|
|
52
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;
|
|
53
38
|
/** Detect secret matches in text. Returns per-kind counts (no values). */
|
|
54
39
|
export declare function detectSecrets(text: string, allow: RegExp[]): Detection[];
|
|
55
40
|
/** Redact secret matches in text, replacing each with `[REDACTED:<kind>]`. */
|
|
56
|
-
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): {
|
|
57
59
|
text: string;
|
|
58
60
|
redactions: number;
|
|
59
61
|
};
|
package/dist/prompt-firewall.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// src/prompt-firewall/index.ts
|
|
2
|
+
import { performance } from "node:perf_hooks";
|
|
3
|
+
|
|
1
4
|
// src/runtime/credential-patterns.ts
|
|
2
5
|
var CREDENTIAL_PATTERNS = [
|
|
3
6
|
// LLM provider keys
|
|
@@ -246,32 +249,161 @@ var PATTERNS = [
|
|
|
246
249
|
})),
|
|
247
250
|
...EXTRA_PATTERNS
|
|
248
251
|
];
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
252
|
+
var PATTERN_BUDGET_MS = 250;
|
|
253
|
+
var GUARD_PROBE_LENGTH = 1e5;
|
|
254
|
+
var GUARD_PROBE_OVERLAP = 4096;
|
|
255
|
+
var SCAN_PASS_BUDGET_MS = 250;
|
|
256
|
+
function createScanDeadline() {
|
|
257
|
+
return { deadline: performance.now() + SCAN_PASS_BUDGET_MS, tripped: /* @__PURE__ */ new Set() };
|
|
258
|
+
}
|
|
259
|
+
var SCAN_WINDOW_STRIDE = GUARD_PROBE_LENGTH - GUARD_PROBE_OVERLAP;
|
|
260
|
+
var SCAN_WINDOW_TAIL = 65536;
|
|
261
|
+
var SCAN_WINDOW_LIMIT = GUARD_PROBE_LENGTH + SCAN_WINDOW_TAIL;
|
|
262
|
+
var SCAN_MAX_GROWTH = 16;
|
|
263
|
+
function growMatch(re, p, text, absStart, deadline) {
|
|
264
|
+
let size = GUARD_PROBE_LENGTH;
|
|
265
|
+
for (let grown = 0; grown < SCAN_MAX_GROWTH; grown++) {
|
|
266
|
+
if (deadline && performance.now() > deadline.deadline) {
|
|
267
|
+
deadline.tripped.add(p.kind);
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
const extEnd = Math.min(text.length, absStart + size);
|
|
271
|
+
const ext = text.slice(absStart, extEnd);
|
|
272
|
+
re.lastIndex = 0;
|
|
273
|
+
const em = re.exec(ext);
|
|
274
|
+
if (!em || em.index !== 0 || em[0].length === 0) return null;
|
|
275
|
+
const end = absStart + em[0].length;
|
|
276
|
+
if (end < extEnd || extEnd === text.length) {
|
|
277
|
+
return { start: absStart, end, matched: em[0] };
|
|
278
|
+
}
|
|
279
|
+
size *= 2;
|
|
280
|
+
}
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
function* execWindowed(p, text, deadline) {
|
|
284
|
+
const re = new RegExp(p.re.source, p.re.flags);
|
|
285
|
+
let acceptLo = 0;
|
|
286
|
+
let highWater = 0;
|
|
287
|
+
for (let window = 0; acceptLo < text.length; window++) {
|
|
288
|
+
const sliceStart = window === 0 ? 0 : acceptLo - GUARD_PROBE_OVERLAP;
|
|
289
|
+
const sliceEnd = Math.min(text.length, sliceStart + SCAN_WINDOW_LIMIT);
|
|
290
|
+
const slice = text.slice(sliceStart, sliceEnd);
|
|
291
|
+
const acceptHi = Math.min(acceptLo + SCAN_WINDOW_STRIDE, text.length);
|
|
292
|
+
re.lastIndex = 0;
|
|
293
|
+
let m = re.exec(slice);
|
|
254
294
|
while (m !== null) {
|
|
295
|
+
if (deadline && performance.now() > deadline.deadline) {
|
|
296
|
+
deadline.tripped.add(p.kind);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
255
299
|
const matched = m[0];
|
|
256
|
-
if (
|
|
257
|
-
|
|
300
|
+
if (matched.length === 0) {
|
|
301
|
+
re.lastIndex += 1;
|
|
302
|
+
m = re.exec(slice);
|
|
303
|
+
continue;
|
|
258
304
|
}
|
|
259
|
-
|
|
305
|
+
const absStart = sliceStart + m.index;
|
|
306
|
+
const absEnd = absStart + matched.length;
|
|
307
|
+
if (absStart >= acceptLo && absStart < acceptHi) {
|
|
308
|
+
let final = { start: absStart, end: absEnd, matched };
|
|
309
|
+
if (absEnd === sliceEnd && sliceEnd < text.length) {
|
|
310
|
+
final = growMatch(re, p, text, absStart, deadline) ?? final;
|
|
311
|
+
re.lastIndex = m.index + final.matched.length;
|
|
312
|
+
}
|
|
313
|
+
if (final.start >= highWater) {
|
|
314
|
+
highWater = Math.max(highWater, final.end);
|
|
315
|
+
yield final;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
m = re.exec(slice);
|
|
319
|
+
}
|
|
320
|
+
acceptLo += SCAN_WINDOW_STRIDE;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function countMatches(p, text, allow, counts, deadline) {
|
|
324
|
+
if (deadline && performance.now() > deadline.deadline) {
|
|
325
|
+
deadline.tripped.add(p.kind);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
for (const m of execWindowed(p, text, deadline)) {
|
|
329
|
+
if (!allow.some((a) => a.test(m.matched))) {
|
|
330
|
+
counts.set(p.kind, (counts.get(p.kind) ?? 0) + 1);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
function replacePattern(p, text, allow, redactions, deadline) {
|
|
335
|
+
if (deadline && performance.now() > deadline.deadline) {
|
|
336
|
+
deadline.tripped.add(p.kind);
|
|
337
|
+
return text;
|
|
338
|
+
}
|
|
339
|
+
let out = "";
|
|
340
|
+
let copied = 0;
|
|
341
|
+
for (const m of execWindowed(p, text, deadline)) {
|
|
342
|
+
out += text.slice(copied, m.start);
|
|
343
|
+
if (allow.some((a) => a.test(m.matched))) {
|
|
344
|
+
out += m.matched;
|
|
345
|
+
} else {
|
|
346
|
+
redactions.n += 1;
|
|
347
|
+
out += `[REDACTED:${p.kind}]`;
|
|
260
348
|
}
|
|
349
|
+
copied = m.end;
|
|
261
350
|
}
|
|
351
|
+
return out + text.slice(copied);
|
|
352
|
+
}
|
|
353
|
+
function detectSecrets(text, allow) {
|
|
354
|
+
const counts = /* @__PURE__ */ new Map();
|
|
355
|
+
for (const p of PATTERNS) countMatches(p, text, allow, counts);
|
|
262
356
|
return [...counts.entries()].map(([kind, count]) => ({ kind, count }));
|
|
263
357
|
}
|
|
264
|
-
function redactSecrets(text, allow) {
|
|
358
|
+
function redactSecrets(text, allow, deadline) {
|
|
359
|
+
const redactions = { n: 0 };
|
|
265
360
|
let out = text;
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
361
|
+
for (const p of PATTERNS) out = replacePattern(p, out, allow, redactions, deadline);
|
|
362
|
+
return { text: out, redactions: redactions.n };
|
|
363
|
+
}
|
|
364
|
+
var DISTINCT_PATTERN_KINDS = new Set(PATTERNS.map((p) => p.kind)).size;
|
|
365
|
+
async function probeTimedOutPatterns(text) {
|
|
366
|
+
const timedOut = /* @__PURE__ */ new Set();
|
|
367
|
+
if (text.length === 0) return timedOut;
|
|
368
|
+
const probeWindow = (offset) => {
|
|
369
|
+
const window = text.slice(offset, offset + GUARD_PROBE_LENGTH);
|
|
370
|
+
const combined = new RegExp(PATTERNS.map((p) => `(${p.re.source})`).join("|"), "gi");
|
|
371
|
+
return withReDoSGuard(combined, window, PATTERN_BUDGET_MS).then(async (combinedResult) => {
|
|
372
|
+
if (!combinedResult.timedOut) return;
|
|
373
|
+
for (const p of PATTERNS) {
|
|
374
|
+
if (timedOut.has(p.kind)) continue;
|
|
375
|
+
const result = await withReDoSGuard(p.re, window, PATTERN_BUDGET_MS);
|
|
376
|
+
if (result.timedOut) timedOut.add(p.kind);
|
|
377
|
+
}
|
|
272
378
|
});
|
|
379
|
+
};
|
|
380
|
+
const stride = GUARD_PROBE_LENGTH - GUARD_PROBE_OVERLAP;
|
|
381
|
+
for (let offset = 0; offset < text.length; offset += stride) {
|
|
382
|
+
if (timedOut.size >= DISTINCT_PATTERN_KINDS) break;
|
|
383
|
+
await probeWindow(offset);
|
|
384
|
+
}
|
|
385
|
+
return timedOut;
|
|
386
|
+
}
|
|
387
|
+
async function detectSecretsGuarded(text, allow) {
|
|
388
|
+
const timedOut = await probeTimedOutPatterns(text);
|
|
389
|
+
const deadline = createScanDeadline();
|
|
390
|
+
const counts = /* @__PURE__ */ new Map();
|
|
391
|
+
for (const p of PATTERNS) {
|
|
392
|
+
if (!timedOut.has(p.kind)) countMatches(p, text, allow, counts, deadline);
|
|
393
|
+
}
|
|
394
|
+
const allSkipped = /* @__PURE__ */ new Set([...timedOut, ...deadline.tripped]);
|
|
395
|
+
return {
|
|
396
|
+
detections: [...counts.entries()].map(([kind, count]) => ({ kind, count })),
|
|
397
|
+
skipped: [...allSkipped].sort().map((kind) => ({ kind, reason: "redos-timeout" }))
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function redactSecretsGuarded(text, allow, skip, deadline) {
|
|
401
|
+
const redactions = { n: 0 };
|
|
402
|
+
let out = text;
|
|
403
|
+
for (const p of PATTERNS) {
|
|
404
|
+
if (!skip.has(p.kind)) out = replacePattern(p, out, allow, redactions, deadline);
|
|
273
405
|
}
|
|
274
|
-
return { text: out, redactions };
|
|
406
|
+
return { text: out, redactions: redactions.n };
|
|
275
407
|
}
|
|
276
408
|
function collectText(request) {
|
|
277
409
|
const parts = [];
|
|
@@ -285,17 +417,27 @@ function collectText(request) {
|
|
|
285
417
|
walk(request["messages"]);
|
|
286
418
|
return parts.join("\n");
|
|
287
419
|
}
|
|
288
|
-
|
|
420
|
+
var RESPONSE_SCAN_BUDGET = 1e6;
|
|
421
|
+
function redactDeep(value, allow, counter, skip, budget, deadline) {
|
|
289
422
|
if (typeof value === "string") {
|
|
290
|
-
|
|
423
|
+
if (budget) {
|
|
424
|
+
if (value.length > budget.remaining) {
|
|
425
|
+
budget.remaining = 0;
|
|
426
|
+
budget.truncated = true;
|
|
427
|
+
return value;
|
|
428
|
+
}
|
|
429
|
+
budget.remaining -= value.length;
|
|
430
|
+
}
|
|
431
|
+
const { text, redactions } = skip ? redactSecretsGuarded(value, allow, skip, deadline) : redactSecrets(value, allow, deadline);
|
|
291
432
|
counter.n += redactions;
|
|
292
433
|
return text;
|
|
293
434
|
}
|
|
294
|
-
if (Array.isArray(value))
|
|
435
|
+
if (Array.isArray(value))
|
|
436
|
+
return value.map((v) => redactDeep(v, allow, counter, skip, budget, deadline));
|
|
295
437
|
if (value && typeof value === "object") {
|
|
296
438
|
const out = {};
|
|
297
439
|
for (const [k, v] of Object.entries(value)) {
|
|
298
|
-
out[k] = redactDeep(v, allow, counter);
|
|
440
|
+
out[k] = redactDeep(v, allow, counter, skip, budget, deadline);
|
|
299
441
|
}
|
|
300
442
|
return out;
|
|
301
443
|
}
|
|
@@ -331,16 +473,33 @@ var state = {
|
|
|
331
473
|
responseRedactions: 0,
|
|
332
474
|
blocked: 0,
|
|
333
475
|
timeoutCount: 0,
|
|
476
|
+
skippedPatterns: [],
|
|
477
|
+
responseTruncated: false,
|
|
334
478
|
byKind: /* @__PURE__ */ new Map(),
|
|
335
479
|
lastDetection: null,
|
|
336
480
|
extensionUnregister: null
|
|
337
481
|
};
|
|
482
|
+
function surfaceScanTrips(api, tripped) {
|
|
483
|
+
for (const kind of tripped) {
|
|
484
|
+
if (!state.skippedPatterns.includes(kind)) state.skippedPatterns.push(kind);
|
|
485
|
+
}
|
|
486
|
+
state.timeoutCount += 1;
|
|
487
|
+
api.metrics.counter("redos_skips", 1);
|
|
488
|
+
api.log.warn("prompt-firewall: scan-pass budget exceeded \u2014 patterns skipped mid-pass (issue #370)", {
|
|
489
|
+
skipped: [...tripped]
|
|
490
|
+
});
|
|
491
|
+
}
|
|
338
492
|
var plugin = {
|
|
339
493
|
name: "prompt-firewall",
|
|
340
494
|
version: "0.1.0",
|
|
341
495
|
description: "Scans the provider wire for credential leaks before context reaches the LLM API (wrapProviderRunner); redact/warn/block. Opt-in; redact by default.",
|
|
342
496
|
apiVersion: "^0.1.10",
|
|
343
497
|
capabilities: { tools: true },
|
|
498
|
+
// Wrap-stack contract (issue #362): ExtensionRegistry composes wrappers
|
|
499
|
+
// first-registered = outermost. The manifest lists this plugin before
|
|
500
|
+
// llm-cache, and llm-cache declares this plugin in optionalDeps, so the
|
|
501
|
+
// firewall is the outer wrap: every request is scanned/redacted before
|
|
502
|
+
// llm-cache can fingerprint or cache it.
|
|
344
503
|
defaultConfig: { enabled: false, mode: "redact", scanResponse: true, allow: [] },
|
|
345
504
|
configSchema: {
|
|
346
505
|
type: "object",
|
|
@@ -376,6 +535,8 @@ var plugin = {
|
|
|
376
535
|
state.responseRedactions = 0;
|
|
377
536
|
state.blocked = 0;
|
|
378
537
|
state.timeoutCount = 0;
|
|
538
|
+
state.skippedPatterns = [];
|
|
539
|
+
state.responseTruncated = false;
|
|
379
540
|
state.byKind.clear();
|
|
380
541
|
state.lastDetection = null;
|
|
381
542
|
if (state.extensionUnregister) {
|
|
@@ -399,15 +560,16 @@ var plugin = {
|
|
|
399
560
|
const req = request ?? {};
|
|
400
561
|
state.invocations += 1;
|
|
401
562
|
const requestText = collectText(req);
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
563
|
+
const { detections, skipped } = await detectSecretsGuarded(requestText, cfg.allow);
|
|
564
|
+
state.skippedPatterns = skipped.map((s) => s.kind);
|
|
565
|
+
if (skipped.length > 0) {
|
|
566
|
+
state.timeoutCount += 1;
|
|
567
|
+
api.log.warn("prompt-firewall: ReDoS budget exceeded, patterns skipped", {
|
|
568
|
+
skipped: state.skippedPatterns
|
|
407
569
|
});
|
|
408
|
-
|
|
570
|
+
api.metrics.counter("redos_skips", 1);
|
|
409
571
|
}
|
|
410
|
-
const
|
|
572
|
+
const skipSet = new Set(state.skippedPatterns);
|
|
411
573
|
if (detections.length > 0) {
|
|
412
574
|
state.requestsWithSecrets += 1;
|
|
413
575
|
for (const d of detections) {
|
|
@@ -426,27 +588,39 @@ var plugin = {
|
|
|
426
588
|
}
|
|
427
589
|
if (cfg.mode === "redact") {
|
|
428
590
|
const counter = { n: 0 };
|
|
429
|
-
const
|
|
591
|
+
const deadline = createScanDeadline();
|
|
592
|
+
const redactedReq = redactDeep(req, cfg.allow, counter, skipSet, void 0, deadline);
|
|
430
593
|
state.requestRedactions += counter.n;
|
|
431
594
|
api.metrics.counter("request_redactions", counter.n);
|
|
595
|
+
if (deadline.tripped.size > 0) surfaceScanTrips(api, deadline.tripped);
|
|
432
596
|
const response2 = await inner(_ctx, redactedReq);
|
|
433
|
-
return cfg.scanResponse ? redactResponse(response2, cfg.allow) : response2;
|
|
597
|
+
return cfg.scanResponse ? redactResponse(response2, cfg.allow, skipSet) : response2;
|
|
434
598
|
}
|
|
435
599
|
}
|
|
436
600
|
const response = await inner(_ctx, request);
|
|
437
601
|
if (cfg.mode === "redact" && cfg.scanResponse) {
|
|
438
|
-
return redactResponse(response, cfg.allow);
|
|
602
|
+
return redactResponse(response, cfg.allow, skipSet);
|
|
439
603
|
}
|
|
440
604
|
return response;
|
|
441
605
|
}
|
|
442
606
|
});
|
|
443
607
|
}
|
|
444
|
-
function redactResponse(response, allow) {
|
|
608
|
+
function redactResponse(response, allow, skip) {
|
|
445
609
|
if (!response || typeof response !== "object") return response;
|
|
446
610
|
const counter = { n: 0 };
|
|
447
611
|
const content = response.content;
|
|
448
612
|
if (content === void 0) return response;
|
|
449
|
-
const
|
|
613
|
+
const budget = { remaining: RESPONSE_SCAN_BUDGET, truncated: false };
|
|
614
|
+
const deadline = createScanDeadline();
|
|
615
|
+
const redacted = redactDeep(content, allow, counter, skip, budget, deadline);
|
|
616
|
+
if (deadline.tripped.size > 0) surfaceScanTrips(api, deadline.tripped);
|
|
617
|
+
if (budget.truncated) {
|
|
618
|
+
state.responseTruncated = true;
|
|
619
|
+
api.log.warn(
|
|
620
|
+
"prompt-firewall: response scan budget exhausted \u2014 part of the response was returned unredacted"
|
|
621
|
+
);
|
|
622
|
+
api.metrics.counter("response_scan_truncated", 1);
|
|
623
|
+
}
|
|
450
624
|
if (counter.n > 0) {
|
|
451
625
|
state.responseRedactions += counter.n;
|
|
452
626
|
api.metrics.counter("response_redactions", counter.n);
|
|
@@ -472,6 +646,8 @@ var plugin = {
|
|
|
472
646
|
mode: cfg.mode,
|
|
473
647
|
scanResponse: cfg.scanResponse,
|
|
474
648
|
patterns: PATTERNS.map((p) => p.kind),
|
|
649
|
+
skippedPatterns: state.skippedPatterns,
|
|
650
|
+
responseTruncated: state.responseTruncated,
|
|
475
651
|
counters: {
|
|
476
652
|
invocations: state.invocations,
|
|
477
653
|
requestsWithSecrets: state.requestsWithSecrets,
|
|
@@ -505,13 +681,17 @@ var plugin = {
|
|
|
505
681
|
requestsWithSecrets: state.requestsWithSecrets,
|
|
506
682
|
requestRedactions: state.requestRedactions,
|
|
507
683
|
responseRedactions: state.responseRedactions,
|
|
508
|
-
blocked: state.blocked
|
|
684
|
+
blocked: state.blocked,
|
|
685
|
+
timeoutCount: state.timeoutCount
|
|
509
686
|
};
|
|
510
687
|
state.invocations = 0;
|
|
511
688
|
state.requestsWithSecrets = 0;
|
|
512
689
|
state.requestRedactions = 0;
|
|
513
690
|
state.responseRedactions = 0;
|
|
514
691
|
state.blocked = 0;
|
|
692
|
+
state.timeoutCount = 0;
|
|
693
|
+
state.skippedPatterns = [];
|
|
694
|
+
state.responseTruncated = false;
|
|
515
695
|
state.byKind.clear();
|
|
516
696
|
state.lastDetection = null;
|
|
517
697
|
api.log.info("prompt-firewall: teardown complete", { final });
|
|
@@ -533,8 +713,11 @@ var plugin = {
|
|
|
533
713
|
var prompt_firewall_default = plugin;
|
|
534
714
|
export {
|
|
535
715
|
KIND_ALIASES,
|
|
716
|
+
SCAN_WINDOW_LIMIT,
|
|
536
717
|
prompt_firewall_default as default,
|
|
537
718
|
detectSecrets,
|
|
719
|
+
detectSecretsGuarded,
|
|
538
720
|
readConfig,
|
|
539
|
-
redactSecrets
|
|
721
|
+
redactSecrets,
|
|
722
|
+
redactSecretsGuarded
|
|
540
723
|
};
|
package/dist/spec-linker.js
CHANGED
|
@@ -55,10 +55,10 @@ var OFFICIAL_PLUGIN_NAMES = [
|
|
|
55
55
|
"notify-hub",
|
|
56
56
|
"changelog-writer",
|
|
57
57
|
"injection-shield",
|
|
58
|
+
"prompt-firewall",
|
|
58
59
|
"llm-cache",
|
|
59
60
|
"model-router",
|
|
60
61
|
"pr-drafter",
|
|
61
|
-
"prompt-firewall",
|
|
62
62
|
"auto-escalate",
|
|
63
63
|
"test-coverage-gate",
|
|
64
64
|
"type-gate",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wrongstack/plugins",
|
|
3
|
-
"version": "0.308.
|
|
3
|
+
"version": "0.308.1",
|
|
4
4
|
"description": "Official WrongStack collection of focused plugins for code quality, security, observability, planning, and agent coordination",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ECOSTACK TECHNOLOGY OÜ",
|
|
@@ -168,6 +168,10 @@
|
|
|
168
168
|
"types": "./dist/injection-shield.d.ts",
|
|
169
169
|
"import": "./dist/injection-shield.js"
|
|
170
170
|
},
|
|
171
|
+
"./prompt-firewall": {
|
|
172
|
+
"types": "./dist/prompt-firewall.d.ts",
|
|
173
|
+
"import": "./dist/prompt-firewall.js"
|
|
174
|
+
},
|
|
171
175
|
"./llm-cache": {
|
|
172
176
|
"types": "./dist/llm-cache.d.ts",
|
|
173
177
|
"import": "./dist/llm-cache.js"
|
|
@@ -180,10 +184,6 @@
|
|
|
180
184
|
"types": "./dist/pr-drafter.d.ts",
|
|
181
185
|
"import": "./dist/pr-drafter.js"
|
|
182
186
|
},
|
|
183
|
-
"./prompt-firewall": {
|
|
184
|
-
"types": "./dist/prompt-firewall.d.ts",
|
|
185
|
-
"import": "./dist/prompt-firewall.js"
|
|
186
|
-
},
|
|
187
187
|
"./auto-escalate": {
|
|
188
188
|
"types": "./dist/auto-escalate.d.ts",
|
|
189
189
|
"import": "./dist/auto-escalate.js"
|
|
@@ -303,8 +303,8 @@
|
|
|
303
303
|
"vitest": "^4.1.10"
|
|
304
304
|
},
|
|
305
305
|
"dependencies": {
|
|
306
|
-
"@wrongstack/
|
|
307
|
-
"@wrongstack/
|
|
306
|
+
"@wrongstack/core": "0.308.1",
|
|
307
|
+
"@wrongstack/tools": "0.308.1"
|
|
308
308
|
},
|
|
309
309
|
"scripts": {
|
|
310
310
|
"build": "node ../../scripts/build-package.mjs",
|