@wrongstack/plugins 0.308.0 → 0.308.2

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.
@@ -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/llm-cache', '@wrongstack/plugins/model-router', '@wrongstack/plugins/pr-drafter', '@wrongstack/plugins/prompt-firewall', '@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'];
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",
@@ -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",
@@ -14263,14 +14270,19 @@ function stripLauncherAtBoundary(command, launcher, valueTaking) {
14263
14270
  }
14264
14271
  return `${command.slice(0, match.index)}${boundary}${after.slice(consumed).replace(/^\s+/, "")}`;
14265
14272
  }
14273
+ var XARGS_OPTIONS = String.raw`(?:\s+(?:(?:-[InLsPjeE]|--(?:arg-file|replace|max-args|max-lines|max-chars|max-procs))\s+[^\s]+|-[^\s]+))*`;
14274
+ var COMMAND_BOUNDARY = String.raw`(?:^|[;&|\r\n]\s*|\{\s*|(?<![$(])\(\s*|\bxargs${XARGS_OPTIONS}\s+)`;
14266
14275
  function commandRecursivelyDeletes(command) {
14267
14276
  const stripped = stripTransparentLaunchers(maskNonExecutingHeredocBodies(command));
14268
- if (/(?:^|[;&|\r\n]\s*|\$\(\s*|\(\s*|`\s*)find\b[^;&|)`]*(?:-delete\b|-exec(?:dir)?\s+(?:[^\s;&|]+[\\/])?(?:rm|rmdir)\b)/i.test(
14277
+ if (/(?:^|[;&|\r\n]\s*|\{\s*|\$\(\s*|\(\s*|`\s*)find\b[^;&|)`]*(?:-delete\b|-exec(?:dir)?\s+(?:[^\s;&|]+[\\/])?(?:rm|rmdir)\b)/i.test(
14269
14278
  stripped
14270
14279
  )) {
14271
14280
  return true;
14272
14281
  }
14273
- const destructive = /(?:^|[;&|\r\n]\s*|\{\s*|\(\s*|\bxargs(?:\s+-[^\s]+)*\s+)(rm|rmdir|del|rd|Remove-Item)\s+((?:"[^"]*"|'[^']*'|\\.|\{[^}]*\}|\([^()]*\)|[^;&|\r\n}])+)/gi;
14282
+ const destructive = new RegExp(
14283
+ String.raw`${COMMAND_BOUNDARY}(rm|rmdir|del|rd|Remove-Item)\s+((?:"[^"]*"|'[^']*'|\\.|\{[^}]*\}|\([^()]*\)|[^;&|\r\n}])+)`,
14284
+ "gi"
14285
+ );
14274
14286
  let match = destructive.exec(stripped);
14275
14287
  while (match !== null) {
14276
14288
  const tool = match[1]?.toLowerCase();
@@ -14487,6 +14499,27 @@ ${remainingCommand}`;
14487
14499
  if (heredoc !== null) maskBody(heredoc.bodyStart, lines.length, heredoc.quoted);
14488
14500
  return lines.join("");
14489
14501
  }
14502
+ function stripSubshellClosers(raw) {
14503
+ let result = raw.trimEnd();
14504
+ while (result.endsWith(")")) {
14505
+ if (quoteIsEscaped(result, result.length - 1)) break;
14506
+ let quote = null;
14507
+ let depth = 0;
14508
+ for (let index = 0; index < result.length - 1; index += 1) {
14509
+ const char = result[index];
14510
+ if (isQuoteBoundary(result, index, quote)) {
14511
+ quote = quote === char ? null : char === "'" ? "'" : '"';
14512
+ continue;
14513
+ }
14514
+ if (quote !== null) continue;
14515
+ if (char === "(" && !quoteIsEscaped(result, index)) depth += 1;
14516
+ else if (char === ")" && !quoteIsEscaped(result, index)) depth -= 1;
14517
+ }
14518
+ if (depth > 0) break;
14519
+ result = result.slice(0, -1).trimEnd();
14520
+ }
14521
+ return result;
14522
+ }
14490
14523
  function destructiveTargets(command) {
14491
14524
  return destructiveTargetsAtDepth(command, 0);
14492
14525
  }
@@ -14533,20 +14566,26 @@ function destructiveTargetsAtDepth(command, depth) {
14533
14566
  }
14534
14567
  targets.push(...destructiveTargetsAtDepth(executableBody, depth + 1));
14535
14568
  }
14536
- 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;
14569
+ const destructive = new RegExp(
14570
+ String.raw`${COMMAND_BOUNDARY}(?:sudo\s+)?(rm|rmdir|del|rd|Remove-Item|unlink|truncate|shred|mv)\s+((?:"[^"]*"|'[^']*'|\\.|\{[^}]*\}|\([^()]*\)|[^;&|\r\n}])+)`,
14571
+ "gi"
14572
+ );
14537
14573
  let m = destructive.exec(normalizedCommand);
14538
14574
  while (m !== null) {
14539
14575
  if (!tokenIsQuoted(m, m[1] ?? "")) targets.push(...shellArgs(m[2] ?? ""));
14540
14576
  m = destructive.exec(normalizedCommand);
14541
14577
  }
14542
- const copy = /(?:^|[;&|\r\n]\s*)(?:sudo\s+)?(cp|install)\s+([^;&|\r\n]+)/gi;
14578
+ const copy = new RegExp(
14579
+ String.raw`${COMMAND_BOUNDARY}(?:sudo\s+)?(cp|install)\s+([^;&|\r\n]+)`,
14580
+ "gi"
14581
+ );
14543
14582
  let c = copy.exec(normalizedCommand);
14544
14583
  while (c !== null) {
14545
14584
  if (tokenIsQuoted(c, c[1] ?? "")) {
14546
14585
  c = copy.exec(normalizedCommand);
14547
14586
  continue;
14548
14587
  }
14549
- const tokens = shellTokens(c[2] ?? "");
14588
+ const tokens = shellTokens(stripSubshellClosers(c[2] ?? ""));
14550
14589
  let destination;
14551
14590
  for (let index = 0; index < tokens.length; index += 1) {
14552
14591
  const token = tokens[index];
@@ -14563,23 +14602,32 @@ function destructiveTargetsAtDepth(command, depth) {
14563
14602
  if (destination) targets.push(destination);
14564
14603
  c = copy.exec(normalizedCommand);
14565
14604
  }
14566
- const tee = /(?:^|[;&|\r\n]\s*)(?:sudo\s+)?(tee)\s+([^;&|\r\n]+)/gi;
14605
+ const tee = new RegExp(
14606
+ String.raw`${COMMAND_BOUNDARY}(?:sudo\s+)?(tee)\s+([^;&|\r\n]+)`,
14607
+ "gi"
14608
+ );
14567
14609
  let t = tee.exec(normalizedCommand);
14568
14610
  while (t !== null) {
14569
14611
  if (!tokenIsQuoted(t, t[1] ?? "")) targets.push(...shellArgs(t[2] ?? ""));
14570
14612
  t = tee.exec(normalizedCommand);
14571
14613
  }
14572
- const dd = /(?:^|[;&|\r\n]\s*)(?:sudo\s+)?(dd)\s+([^;&|\r\n]+)/gi;
14614
+ const dd = new RegExp(
14615
+ String.raw`${COMMAND_BOUNDARY}(?:sudo\s+)?(dd)\s+([^;&|\r\n]+)`,
14616
+ "gi"
14617
+ );
14573
14618
  let d = dd.exec(normalizedCommand);
14574
14619
  while (d !== null) {
14575
14620
  if (!tokenIsQuoted(d, d[1] ?? "")) {
14576
14621
  const outputMatch = /(?:^|\s)of=("[^"]*"|'[^']*'|[^\s]+)/i.exec(d[2] ?? "");
14577
- const output = outputMatch?.[1]?.replace(/^['"]|['"]$/g, "");
14622
+ const output = outputMatch?.[1] ? stripSubshellClosers(outputMatch[1]).replace(/^['"]|['"]$/g, "") : void 0;
14578
14623
  if (output) targets.push(output);
14579
14624
  }
14580
14625
  d = dd.exec(normalizedCommand);
14581
14626
  }
14582
- const overwrite = /(?:^|[;&|\r\n]\s*)(?:sudo\s+)?(sed|ln)\s+([^;&|\r\n]+)/gi;
14627
+ const overwrite = new RegExp(
14628
+ String.raw`${COMMAND_BOUNDARY}(?:sudo\s+)?(sed|ln)\s+([^;&|\r\n]+)`,
14629
+ "gi"
14630
+ );
14583
14631
  let o = overwrite.exec(normalizedCommand);
14584
14632
  while (o !== null) {
14585
14633
  const rawArgs = o[2] ?? "";
@@ -14597,7 +14645,10 @@ function destructiveTargetsAtDepth(command, depth) {
14597
14645
  }
14598
14646
  o = overwrite.exec(normalizedCommand);
14599
14647
  }
14600
- const xargsPipeline = /\b(?:echo|printf)\s+([^|]+)\|\s*xargs(?:\s+-[^\s]+)*\s+(?:sudo\s+)?(?:rm|rmdir|del|unlink|truncate|shred)\b/gi;
14648
+ const xargsPipeline = new RegExp(
14649
+ String.raw`\b(?:echo|printf)\s+([^|]+)\|\s*xargs${XARGS_OPTIONS}\s+(?:sudo\s+)?(?:rm|rmdir|del|unlink|truncate|shred)\b`,
14650
+ "gi"
14651
+ );
14601
14652
  let x = xargsPipeline.exec(normalizedCommand);
14602
14653
  while (x !== null) {
14603
14654
  if (!tokenIsQuoted(x, "xargs")) targets.push(...shellArgs(x[1] ?? ""));
@@ -14714,7 +14765,7 @@ function destructiveTargetsAtDepth(command, depth) {
14714
14765
  targets.push(gitTarget("."));
14715
14766
  }
14716
14767
  }
14717
- const findDelete = /(?:^|[;&|\r\n]\s*|\$\(\s*|\(\s*|`\s*)find\b([^;&|)`]*(?:\s-delete(?:\s|$)|\s-exec(?:dir)?\s+(?:[^\s;&|]+[\\/])?(?:rm|rmdir)\b)[^;&|)`]*)/gi;
14768
+ const findDelete = /(?:^|[;&|\r\n]\s*|\{\s*|\$\(\s*|\(\s*|`\s*)find\b([^;&|)`]*(?:\s-delete(?:[\s;})]|$)|\s-exec(?:dir)?\s+(?:[^\s;&|]+[\\/])?(?:rm|rmdir)\b)[^;&|)`]*)/gi;
14718
14769
  let f = findDelete.exec(normalizedCommand);
14719
14770
  while (f !== null) {
14720
14771
  const tokens = shellTokens(f[1] ?? "");
@@ -15147,10 +15198,19 @@ var plugin39 = {
15147
15198
  const ti = input.toolInput ?? {};
15148
15199
  const command = typeof ti["command"] === "string" ? ti["command"] : "";
15149
15200
  const commandArgs = Array.isArray(ti["args"]) ? ti["args"].filter((arg) => typeof arg === "string") : [];
15150
- const commandForInspection = [
15151
- command,
15152
- ...commandArgs.map((arg) => /^[\w./:@%+=,-]+$/.test(arg) ? arg : JSON.stringify(arg))
15153
- ].join(" ");
15201
+ const serializeArgForInspection = (arg) => {
15202
+ if (/^[\w./:@%+=,-]+$/.test(arg)) return arg;
15203
+ const assignment = /^([A-Za-z_]\w*)=(.+)$/.exec(arg);
15204
+ const key = assignment?.[1];
15205
+ const value = assignment?.[2];
15206
+ if (key !== void 0 && value !== void 0) {
15207
+ return `${key}="${value.replaceAll('"', "")}"`;
15208
+ }
15209
+ return JSON.stringify(arg);
15210
+ };
15211
+ const commandForInspection = [command, ...commandArgs.map(serializeArgForInspection)].join(
15212
+ " "
15213
+ );
15154
15214
  if (command && executesShell({
15155
15215
  toolName: input.toolName,
15156
15216
  toolInput: ti,
@@ -16226,6 +16286,9 @@ var plugin42 = {
16226
16286
  };
16227
16287
  var process_guard_default = plugin42;
16228
16288
 
16289
+ // src/prompt-firewall/index.ts
16290
+ import { performance as performance2 } from "node:perf_hooks";
16291
+
16229
16292
  // src/runtime/credential-patterns.ts
16230
16293
  var CREDENTIAL_PATTERNS = [
16231
16294
  // LLM provider keys
@@ -16397,32 +16460,156 @@ var PATTERNS2 = [
16397
16460
  })),
16398
16461
  ...EXTRA_PATTERNS
16399
16462
  ];
16400
- function detectSecrets(text, allow) {
16401
- const counts = /* @__PURE__ */ new Map();
16402
- for (const p of PATTERNS2) {
16403
- p.re.lastIndex = 0;
16404
- let m = p.re.exec(text);
16463
+ var PATTERN_BUDGET_MS = 250;
16464
+ var GUARD_PROBE_LENGTH = 1e5;
16465
+ var GUARD_PROBE_OVERLAP = 4096;
16466
+ var SCAN_PASS_BUDGET_MS = 250;
16467
+ function createScanDeadline() {
16468
+ return { deadline: performance2.now() + SCAN_PASS_BUDGET_MS, tripped: /* @__PURE__ */ new Set() };
16469
+ }
16470
+ var SCAN_WINDOW_STRIDE = GUARD_PROBE_LENGTH - GUARD_PROBE_OVERLAP;
16471
+ var SCAN_WINDOW_TAIL = 65536;
16472
+ var SCAN_WINDOW_LIMIT = GUARD_PROBE_LENGTH + SCAN_WINDOW_TAIL;
16473
+ var SCAN_MAX_GROWTH = 16;
16474
+ function growMatch(re, p, text, absStart, deadline) {
16475
+ let size = GUARD_PROBE_LENGTH;
16476
+ for (let grown = 0; grown < SCAN_MAX_GROWTH; grown++) {
16477
+ if (deadline && performance2.now() > deadline.deadline) {
16478
+ deadline.tripped.add(p.kind);
16479
+ return null;
16480
+ }
16481
+ const extEnd = Math.min(text.length, absStart + size);
16482
+ const ext = text.slice(absStart, extEnd);
16483
+ re.lastIndex = 0;
16484
+ const em = re.exec(ext);
16485
+ if (!em || em.index !== 0 || em[0].length === 0) return null;
16486
+ const end = absStart + em[0].length;
16487
+ if (end < extEnd || extEnd === text.length) {
16488
+ return { start: absStart, end, matched: em[0] };
16489
+ }
16490
+ size *= 2;
16491
+ }
16492
+ return null;
16493
+ }
16494
+ function* execWindowed(p, text, deadline) {
16495
+ const re = new RegExp(p.re.source, p.re.flags);
16496
+ let acceptLo = 0;
16497
+ let highWater = 0;
16498
+ for (let window = 0; acceptLo < text.length; window++) {
16499
+ const sliceStart = window === 0 ? 0 : acceptLo - GUARD_PROBE_OVERLAP;
16500
+ const sliceEnd = Math.min(text.length, sliceStart + SCAN_WINDOW_LIMIT);
16501
+ const slice = text.slice(sliceStart, sliceEnd);
16502
+ const acceptHi = Math.min(acceptLo + SCAN_WINDOW_STRIDE, text.length);
16503
+ re.lastIndex = 0;
16504
+ let m = re.exec(slice);
16405
16505
  while (m !== null) {
16506
+ if (deadline && performance2.now() > deadline.deadline) {
16507
+ deadline.tripped.add(p.kind);
16508
+ return;
16509
+ }
16406
16510
  const matched = m[0];
16407
- if (!allow.some((a) => a.test(matched))) {
16408
- counts.set(p.kind, (counts.get(p.kind) ?? 0) + 1);
16511
+ if (matched.length === 0) {
16512
+ re.lastIndex += 1;
16513
+ m = re.exec(slice);
16514
+ continue;
16409
16515
  }
16410
- m = p.re.exec(text);
16516
+ const absStart = sliceStart + m.index;
16517
+ const absEnd = absStart + matched.length;
16518
+ if (absStart >= acceptLo && absStart < acceptHi) {
16519
+ let final = { start: absStart, end: absEnd, matched };
16520
+ if (absEnd === sliceEnd && sliceEnd < text.length) {
16521
+ final = growMatch(re, p, text, absStart, deadline) ?? final;
16522
+ re.lastIndex = m.index + final.matched.length;
16523
+ }
16524
+ if (final.start >= highWater) {
16525
+ highWater = Math.max(highWater, final.end);
16526
+ yield final;
16527
+ }
16528
+ }
16529
+ m = re.exec(slice);
16411
16530
  }
16531
+ acceptLo += SCAN_WINDOW_STRIDE;
16412
16532
  }
16413
- return [...counts.entries()].map(([kind, count]) => ({ kind, count }));
16414
16533
  }
16415
- function redactSecrets(text, allow) {
16534
+ function countMatches(p, text, allow, counts, deadline) {
16535
+ if (deadline && performance2.now() > deadline.deadline) {
16536
+ deadline.tripped.add(p.kind);
16537
+ return;
16538
+ }
16539
+ for (const m of execWindowed(p, text, deadline)) {
16540
+ if (!allow.some((a) => a.test(m.matched))) {
16541
+ counts.set(p.kind, (counts.get(p.kind) ?? 0) + 1);
16542
+ }
16543
+ }
16544
+ }
16545
+ function replacePattern(p, text, allow, redactions, deadline) {
16546
+ if (deadline && performance2.now() > deadline.deadline) {
16547
+ deadline.tripped.add(p.kind);
16548
+ return text;
16549
+ }
16550
+ let out = "";
16551
+ let copied = 0;
16552
+ for (const m of execWindowed(p, text, deadline)) {
16553
+ out += text.slice(copied, m.start);
16554
+ if (allow.some((a) => a.test(m.matched))) {
16555
+ out += m.matched;
16556
+ } else {
16557
+ redactions.n += 1;
16558
+ out += `[REDACTED:${p.kind}]`;
16559
+ }
16560
+ copied = m.end;
16561
+ }
16562
+ return out + text.slice(copied);
16563
+ }
16564
+ function redactSecrets(text, allow, deadline) {
16565
+ const redactions = { n: 0 };
16416
16566
  let out = text;
16417
- let redactions = 0;
16418
- for (const p of PATTERNS2) {
16419
- out = out.replace(new RegExp(p.re.source, p.re.flags), (match) => {
16420
- if (allow.some((a) => a.test(match))) return match;
16421
- redactions += 1;
16422
- return `[REDACTED:${p.kind}]`;
16567
+ for (const p of PATTERNS2) out = replacePattern(p, out, allow, redactions, deadline);
16568
+ return { text: out, redactions: redactions.n };
16569
+ }
16570
+ var DISTINCT_PATTERN_KINDS = new Set(PATTERNS2.map((p) => p.kind)).size;
16571
+ async function probeTimedOutPatterns(text) {
16572
+ const timedOut = /* @__PURE__ */ new Set();
16573
+ if (text.length === 0) return timedOut;
16574
+ const probeWindow = (offset) => {
16575
+ const window = text.slice(offset, offset + GUARD_PROBE_LENGTH);
16576
+ const combined = new RegExp(PATTERNS2.map((p) => `(${p.re.source})`).join("|"), "gi");
16577
+ return withReDoSGuard(combined, window, PATTERN_BUDGET_MS).then(async (combinedResult) => {
16578
+ if (!combinedResult.timedOut) return;
16579
+ for (const p of PATTERNS2) {
16580
+ if (timedOut.has(p.kind)) continue;
16581
+ const result = await withReDoSGuard(p.re, window, PATTERN_BUDGET_MS);
16582
+ if (result.timedOut) timedOut.add(p.kind);
16583
+ }
16423
16584
  });
16585
+ };
16586
+ const stride = GUARD_PROBE_LENGTH - GUARD_PROBE_OVERLAP;
16587
+ for (let offset = 0; offset < text.length; offset += stride) {
16588
+ if (timedOut.size >= DISTINCT_PATTERN_KINDS) break;
16589
+ await probeWindow(offset);
16590
+ }
16591
+ return timedOut;
16592
+ }
16593
+ async function detectSecretsGuarded(text, allow) {
16594
+ const timedOut = await probeTimedOutPatterns(text);
16595
+ const deadline = createScanDeadline();
16596
+ const counts = /* @__PURE__ */ new Map();
16597
+ for (const p of PATTERNS2) {
16598
+ if (!timedOut.has(p.kind)) countMatches(p, text, allow, counts, deadline);
16424
16599
  }
16425
- return { text: out, redactions };
16600
+ const allSkipped = /* @__PURE__ */ new Set([...timedOut, ...deadline.tripped]);
16601
+ return {
16602
+ detections: [...counts.entries()].map(([kind, count]) => ({ kind, count })),
16603
+ skipped: [...allSkipped].sort().map((kind) => ({ kind, reason: "redos-timeout" }))
16604
+ };
16605
+ }
16606
+ function redactSecretsGuarded(text, allow, skip, deadline) {
16607
+ const redactions = { n: 0 };
16608
+ let out = text;
16609
+ for (const p of PATTERNS2) {
16610
+ if (!skip.has(p.kind)) out = replacePattern(p, out, allow, redactions, deadline);
16611
+ }
16612
+ return { text: out, redactions: redactions.n };
16426
16613
  }
16427
16614
  function collectText(request) {
16428
16615
  const parts = [];
@@ -16436,17 +16623,27 @@ function collectText(request) {
16436
16623
  walk(request["messages"]);
16437
16624
  return parts.join("\n");
16438
16625
  }
16439
- function redactDeep(value, allow, counter) {
16626
+ var RESPONSE_SCAN_BUDGET = 1e6;
16627
+ function redactDeep(value, allow, counter, skip, budget, deadline) {
16440
16628
  if (typeof value === "string") {
16441
- const { text, redactions } = redactSecrets(value, allow);
16629
+ if (budget) {
16630
+ if (value.length > budget.remaining) {
16631
+ budget.remaining = 0;
16632
+ budget.truncated = true;
16633
+ return value;
16634
+ }
16635
+ budget.remaining -= value.length;
16636
+ }
16637
+ const { text, redactions } = skip ? redactSecretsGuarded(value, allow, skip, deadline) : redactSecrets(value, allow, deadline);
16442
16638
  counter.n += redactions;
16443
16639
  return text;
16444
16640
  }
16445
- if (Array.isArray(value)) return value.map((v) => redactDeep(v, allow, counter));
16641
+ if (Array.isArray(value))
16642
+ return value.map((v) => redactDeep(v, allow, counter, skip, budget, deadline));
16446
16643
  if (value && typeof value === "object") {
16447
16644
  const out = {};
16448
16645
  for (const [k, v] of Object.entries(value)) {
16449
- out[k] = redactDeep(v, allow, counter);
16646
+ out[k] = redactDeep(v, allow, counter, skip, budget, deadline);
16450
16647
  }
16451
16648
  return out;
16452
16649
  }
@@ -16482,16 +16679,33 @@ var state40 = {
16482
16679
  responseRedactions: 0,
16483
16680
  blocked: 0,
16484
16681
  timeoutCount: 0,
16682
+ skippedPatterns: [],
16683
+ responseTruncated: false,
16485
16684
  byKind: /* @__PURE__ */ new Map(),
16486
16685
  lastDetection: null,
16487
16686
  extensionUnregister: null
16488
16687
  };
16688
+ function surfaceScanTrips(api, tripped) {
16689
+ for (const kind of tripped) {
16690
+ if (!state40.skippedPatterns.includes(kind)) state40.skippedPatterns.push(kind);
16691
+ }
16692
+ state40.timeoutCount += 1;
16693
+ api.metrics.counter("redos_skips", 1);
16694
+ api.log.warn("prompt-firewall: scan-pass budget exceeded \u2014 patterns skipped mid-pass (issue #370)", {
16695
+ skipped: [...tripped]
16696
+ });
16697
+ }
16489
16698
  var plugin43 = {
16490
16699
  name: "prompt-firewall",
16491
16700
  version: "0.1.0",
16492
16701
  description: "Scans the provider wire for credential leaks before context reaches the LLM API (wrapProviderRunner); redact/warn/block. Opt-in; redact by default.",
16493
16702
  apiVersion: "^0.1.10",
16494
16703
  capabilities: { tools: true },
16704
+ // Wrap-stack contract (issue #362): ExtensionRegistry composes wrappers
16705
+ // first-registered = outermost. The manifest lists this plugin before
16706
+ // llm-cache, and llm-cache declares this plugin in optionalDeps, so the
16707
+ // firewall is the outer wrap: every request is scanned/redacted before
16708
+ // llm-cache can fingerprint or cache it.
16495
16709
  defaultConfig: { enabled: false, mode: "redact", scanResponse: true, allow: [] },
16496
16710
  configSchema: {
16497
16711
  type: "object",
@@ -16527,6 +16741,8 @@ var plugin43 = {
16527
16741
  state40.responseRedactions = 0;
16528
16742
  state40.blocked = 0;
16529
16743
  state40.timeoutCount = 0;
16744
+ state40.skippedPatterns = [];
16745
+ state40.responseTruncated = false;
16530
16746
  state40.byKind.clear();
16531
16747
  state40.lastDetection = null;
16532
16748
  if (state40.extensionUnregister) {
@@ -16550,15 +16766,16 @@ var plugin43 = {
16550
16766
  const req = request ?? {};
16551
16767
  state40.invocations += 1;
16552
16768
  const requestText = collectText(req);
16553
- for (const extra of EXTRA_PATTERNS) {
16554
- const guarded = await withReDoSGuard(extra.re, requestText, 250, {
16555
- onTimeout: () => {
16556
- state40.timeoutCount += 1;
16557
- }
16769
+ const { detections, skipped } = await detectSecretsGuarded(requestText, cfg.allow);
16770
+ state40.skippedPatterns = skipped.map((s) => s.kind);
16771
+ if (skipped.length > 0) {
16772
+ state40.timeoutCount += 1;
16773
+ api.log.warn("prompt-firewall: ReDoS budget exceeded, patterns skipped", {
16774
+ skipped: state40.skippedPatterns
16558
16775
  });
16559
- if (guarded.timedOut) break;
16776
+ api.metrics.counter("redos_skips", 1);
16560
16777
  }
16561
- const detections = detectSecrets(requestText, cfg.allow);
16778
+ const skipSet = new Set(state40.skippedPatterns);
16562
16779
  if (detections.length > 0) {
16563
16780
  state40.requestsWithSecrets += 1;
16564
16781
  for (const d of detections) {
@@ -16577,27 +16794,39 @@ var plugin43 = {
16577
16794
  }
16578
16795
  if (cfg.mode === "redact") {
16579
16796
  const counter = { n: 0 };
16580
- const redactedReq = redactDeep(req, cfg.allow, counter);
16797
+ const deadline = createScanDeadline();
16798
+ const redactedReq = redactDeep(req, cfg.allow, counter, skipSet, void 0, deadline);
16581
16799
  state40.requestRedactions += counter.n;
16582
16800
  api.metrics.counter("request_redactions", counter.n);
16801
+ if (deadline.tripped.size > 0) surfaceScanTrips(api, deadline.tripped);
16583
16802
  const response2 = await inner(_ctx, redactedReq);
16584
- return cfg.scanResponse ? redactResponse(response2, cfg.allow) : response2;
16803
+ return cfg.scanResponse ? redactResponse(response2, cfg.allow, skipSet) : response2;
16585
16804
  }
16586
16805
  }
16587
16806
  const response = await inner(_ctx, request);
16588
16807
  if (cfg.mode === "redact" && cfg.scanResponse) {
16589
- return redactResponse(response, cfg.allow);
16808
+ return redactResponse(response, cfg.allow, skipSet);
16590
16809
  }
16591
16810
  return response;
16592
16811
  }
16593
16812
  });
16594
16813
  }
16595
- function redactResponse(response, allow) {
16814
+ function redactResponse(response, allow, skip) {
16596
16815
  if (!response || typeof response !== "object") return response;
16597
16816
  const counter = { n: 0 };
16598
16817
  const content = response.content;
16599
16818
  if (content === void 0) return response;
16600
- const redacted = redactDeep(content, allow, counter);
16819
+ const budget = { remaining: RESPONSE_SCAN_BUDGET, truncated: false };
16820
+ const deadline = createScanDeadline();
16821
+ const redacted = redactDeep(content, allow, counter, skip, budget, deadline);
16822
+ if (deadline.tripped.size > 0) surfaceScanTrips(api, deadline.tripped);
16823
+ if (budget.truncated) {
16824
+ state40.responseTruncated = true;
16825
+ api.log.warn(
16826
+ "prompt-firewall: response scan budget exhausted \u2014 part of the response was returned unredacted"
16827
+ );
16828
+ api.metrics.counter("response_scan_truncated", 1);
16829
+ }
16601
16830
  if (counter.n > 0) {
16602
16831
  state40.responseRedactions += counter.n;
16603
16832
  api.metrics.counter("response_redactions", counter.n);
@@ -16623,6 +16852,8 @@ var plugin43 = {
16623
16852
  mode: cfg.mode,
16624
16853
  scanResponse: cfg.scanResponse,
16625
16854
  patterns: PATTERNS2.map((p) => p.kind),
16855
+ skippedPatterns: state40.skippedPatterns,
16856
+ responseTruncated: state40.responseTruncated,
16626
16857
  counters: {
16627
16858
  invocations: state40.invocations,
16628
16859
  requestsWithSecrets: state40.requestsWithSecrets,
@@ -16656,13 +16887,17 @@ var plugin43 = {
16656
16887
  requestsWithSecrets: state40.requestsWithSecrets,
16657
16888
  requestRedactions: state40.requestRedactions,
16658
16889
  responseRedactions: state40.responseRedactions,
16659
- blocked: state40.blocked
16890
+ blocked: state40.blocked,
16891
+ timeoutCount: state40.timeoutCount
16660
16892
  };
16661
16893
  state40.invocations = 0;
16662
16894
  state40.requestsWithSecrets = 0;
16663
16895
  state40.requestRedactions = 0;
16664
16896
  state40.responseRedactions = 0;
16665
16897
  state40.blocked = 0;
16898
+ state40.timeoutCount = 0;
16899
+ state40.skippedPatterns = [];
16900
+ state40.responseTruncated = false;
16666
16901
  state40.byKind.clear();
16667
16902
  state40.lastDetection = null;
16668
16903
  api.log.info("prompt-firewall: teardown complete", { final });
@@ -20629,10 +20864,10 @@ var OFFICIAL_PLUGIN_NAMES = [
20629
20864
  "notify-hub",
20630
20865
  "changelog-writer",
20631
20866
  "injection-shield",
20867
+ "prompt-firewall",
20632
20868
  "llm-cache",
20633
20869
  "model-router",
20634
20870
  "pr-drafter",
20635
- "prompt-firewall",
20636
20871
  "auto-escalate",
20637
20872
  "test-coverage-gate",
20638
20873
  "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",