@wrongstack/plugins 0.277.2 → 0.280.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.
Files changed (72) hide show
  1. package/README.md +838 -0
  2. package/dist/auto-doc.d.ts +8 -0
  3. package/dist/auto-doc.js +175 -13
  4. package/dist/auto-escalate.d.ts +45 -0
  5. package/dist/auto-escalate.js +190 -0
  6. package/dist/branch-guard.d.ts +33 -0
  7. package/dist/branch-guard.js +228 -0
  8. package/dist/changelog-writer.d.ts +73 -0
  9. package/dist/changelog-writer.js +369 -0
  10. package/dist/checkpoint.d.ts +55 -0
  11. package/dist/checkpoint.js +305 -0
  12. package/dist/commit-validator.d.ts +33 -0
  13. package/dist/commit-validator.js +315 -0
  14. package/dist/config-validator.d.ts +48 -0
  15. package/dist/config-validator.js +347 -0
  16. package/dist/context-pins.d.ts +45 -0
  17. package/dist/context-pins.js +240 -0
  18. package/dist/cost-tracker.d.ts +40 -1
  19. package/dist/cost-tracker.js +105 -4
  20. package/dist/dep-guard.d.ts +65 -0
  21. package/dist/dep-guard.js +316 -0
  22. package/dist/diff-summary.d.ts +36 -0
  23. package/dist/diff-summary.js +235 -0
  24. package/dist/error-lens.d.ts +67 -0
  25. package/dist/error-lens.js +280 -0
  26. package/dist/format-on-save.d.ts +35 -0
  27. package/dist/format-on-save.js +219 -0
  28. package/dist/git-autocommit.js +186 -26
  29. package/dist/import-organizer.d.ts +52 -0
  30. package/dist/import-organizer.js +274 -0
  31. package/dist/index.d.ts +32 -6
  32. package/dist/index.js +10151 -1628
  33. package/dist/injection-shield.d.ts +49 -0
  34. package/dist/injection-shield.js +205 -0
  35. package/dist/lint-gate.d.ts +33 -0
  36. package/dist/lint-gate.js +394 -0
  37. package/dist/llm-cache.d.ts +56 -0
  38. package/dist/llm-cache.js +251 -0
  39. package/dist/loop-breaker.d.ts +43 -0
  40. package/dist/loop-breaker.js +241 -0
  41. package/dist/model-router.d.ts +69 -0
  42. package/dist/model-router.js +198 -0
  43. package/dist/notify-hub.d.ts +45 -0
  44. package/dist/notify-hub.js +304 -0
  45. package/dist/path-guard.d.ts +54 -0
  46. package/dist/path-guard.js +235 -0
  47. package/dist/prompt-firewall.d.ts +57 -0
  48. package/dist/prompt-firewall.js +290 -0
  49. package/dist/secret-scanner.d.ts +34 -0
  50. package/dist/secret-scanner.js +409 -0
  51. package/dist/semver-bump.js +45 -0
  52. package/dist/session-recap.d.ts +50 -0
  53. package/dist/session-recap.js +421 -0
  54. package/dist/shell-check.js +52 -4
  55. package/dist/spec-linker.d.ts +51 -0
  56. package/dist/spec-linker.js +541 -0
  57. package/dist/template-engine.js +19 -1
  58. package/dist/test-runner-gate.d.ts +37 -0
  59. package/dist/test-runner-gate.js +356 -0
  60. package/dist/todo-listener.d.ts +37 -0
  61. package/dist/todo-listener.js +216 -0
  62. package/dist/todo-tracker.d.ts +5 -0
  63. package/dist/todo-tracker.js +441 -0
  64. package/dist/token-budget.d.ts +40 -0
  65. package/dist/token-budget.js +254 -0
  66. package/dist/token-throttle.d.ts +54 -0
  67. package/dist/token-throttle.js +203 -0
  68. package/package.json +116 -12
  69. package/dist/json-path.d.ts +0 -18
  70. package/dist/json-path.js +0 -15
  71. package/dist/web-search.d.ts +0 -19
  72. package/dist/web-search.js +0 -15
@@ -0,0 +1,235 @@
1
+ // src/path-guard/index.ts
2
+ var state = {
3
+ invocations: 0,
4
+ blocks: 0,
5
+ warns: 0,
6
+ lastBlock: null,
7
+ hookUnregister: null
8
+ };
9
+ var DEFAULT_PROTECT = [
10
+ "pnpm-lock.yaml",
11
+ "package-lock.json",
12
+ "yarn.lock",
13
+ "bun.lockb",
14
+ "Cargo.lock",
15
+ "poetry.lock",
16
+ ".env",
17
+ ".env.*",
18
+ ".git/**",
19
+ "**/migrations/**"
20
+ ];
21
+ var DEFAULTS = {
22
+ enabled: true,
23
+ mode: "block",
24
+ protect: DEFAULT_PROTECT,
25
+ allow: []
26
+ };
27
+ function readConfig(raw) {
28
+ if (!raw || typeof raw !== "object") return { ...DEFAULTS, protect: [...DEFAULT_PROTECT] };
29
+ const r = raw;
30
+ return {
31
+ enabled: r["enabled"] !== false,
32
+ mode: r["mode"] === "warn" ? "warn" : "block",
33
+ protect: Array.isArray(r["protect"]) ? r["protect"].filter((p) => typeof p === "string" && p.length > 0) : [...DEFAULT_PROTECT],
34
+ allow: Array.isArray(r["allow"]) ? r["allow"].filter((p) => typeof p === "string" && p.length > 0) : []
35
+ };
36
+ }
37
+ function compilePathGlob(pattern) {
38
+ const normalized = pattern.replace(/\\/g, "/");
39
+ let source = "";
40
+ for (let i = 0; i < normalized.length; i++) {
41
+ const ch = normalized[i];
42
+ if (ch === "*") {
43
+ if (normalized[i + 1] === "*") {
44
+ source += "(?:.*)";
45
+ i += 1;
46
+ if (normalized[i + 1] === "/") i += 1;
47
+ } else {
48
+ source += "[^/]*";
49
+ }
50
+ } else if (ch === "?") {
51
+ source += "[^/]";
52
+ } else if (ch !== void 0 && /[.+^${}()|[\]\\]/.test(ch)) {
53
+ source += `\\${ch}`;
54
+ } else {
55
+ source += ch;
56
+ }
57
+ }
58
+ return new RegExp(`(?:^|/)${source}$`, "i");
59
+ }
60
+ function normalizePath(p) {
61
+ return p.replace(/\\/g, "/").replace(/^\.\//, "");
62
+ }
63
+ function matchesAny(path, patterns) {
64
+ const normalized = normalizePath(path);
65
+ return patterns.some((re) => re.test(normalized));
66
+ }
67
+ function destructiveTargets(command) {
68
+ const targets = [];
69
+ const destructive = /(?:^|[;&|]\s*)(?:sudo\s+)?(rm|rmdir|del|unlink|truncate|shred|mv)\s+([^;&|]+)/gi;
70
+ let m = destructive.exec(command);
71
+ while (m !== null) {
72
+ const args = (m[2] ?? "").split(/\s+/).filter((a) => a.length > 0 && !a.startsWith("-"));
73
+ targets.push(...args);
74
+ m = destructive.exec(command);
75
+ }
76
+ const redirect = />{1,2}\s*([^\s;&|>]+)/g;
77
+ let r = redirect.exec(command);
78
+ while (r !== null) {
79
+ const target = r[1];
80
+ if (target && target !== "/dev/null" && target !== "NUL" && target !== "nul") {
81
+ targets.push(target);
82
+ }
83
+ r = redirect.exec(command);
84
+ }
85
+ return targets.map((t) => t.replace(/^['"]|['"]$/g, ""));
86
+ }
87
+ var plugin = {
88
+ name: "path-guard",
89
+ version: "0.1.0",
90
+ description: "Blocks or warns about writes, edits, and destructive shell commands touching protected paths (lockfiles, .env, .git, migrations)",
91
+ apiVersion: "^0.1.10",
92
+ capabilities: { tools: true, hooks: true },
93
+ defaultConfig: { ...DEFAULTS },
94
+ configSchema: {
95
+ type: "object",
96
+ properties: {
97
+ enabled: { type: "boolean", default: true, description: "Master switch." },
98
+ mode: {
99
+ type: "string",
100
+ enum: ["block", "warn"],
101
+ default: "block",
102
+ description: "block = refuse the operation; warn = only inject context."
103
+ },
104
+ protect: {
105
+ type: "array",
106
+ items: { type: "string" },
107
+ description: "Glob patterns for protected paths. Replaces the default set when present."
108
+ },
109
+ allow: {
110
+ type: "array",
111
+ items: { type: "string" },
112
+ default: [],
113
+ description: "Glob patterns that override `protect` (exemptions)."
114
+ }
115
+ }
116
+ },
117
+ setup(api) {
118
+ state.invocations = 0;
119
+ state.blocks = 0;
120
+ state.warns = 0;
121
+ state.lastBlock = null;
122
+ if (state.hookUnregister) {
123
+ try {
124
+ state.hookUnregister();
125
+ } catch {
126
+ }
127
+ state.hookUnregister = null;
128
+ }
129
+ const cfg = readConfig(api.config.extensions?.["path-guard"]);
130
+ const protectRes = cfg.protect.map(compilePathGlob);
131
+ const allowRes = cfg.allow.map(compilePathGlob);
132
+ const verdict = (path, tool, operation) => {
133
+ if (cfg.mode === "block") {
134
+ state.blocks += 1;
135
+ state.lastBlock = { path, tool, when: (/* @__PURE__ */ new Date()).toISOString() };
136
+ api.metrics.counter("blocks");
137
+ return {
138
+ decision: "block",
139
+ reason: `path-guard: "${path}" is a protected path (matched by config.extensions["path-guard"].protect) \u2014 ${operation} refused. If this change is intentional, ask the user to do it, add an \`allow\` glob, or set mode: "warn".`
140
+ };
141
+ }
142
+ state.warns += 1;
143
+ api.metrics.counter("warns");
144
+ return {
145
+ decision: "allow",
146
+ additionalContext: `path-guard (warn mode): "${path}" is a protected path and this ${operation} would modify it. Double-check this is intentional.`
147
+ };
148
+ };
149
+ const hook = (input) => {
150
+ if (!cfg.enabled) return;
151
+ state.invocations += 1;
152
+ const toolName = input.toolName ?? "";
153
+ const ti = input.toolInput ?? {};
154
+ if (toolName === "write" || toolName === "edit") {
155
+ const raw = ti["path"] ?? ti["file_path"] ?? ti["filePath"];
156
+ if (typeof raw !== "string" || raw.length === 0) return;
157
+ if (matchesAny(raw, allowRes)) return;
158
+ if (matchesAny(raw, protectRes)) {
159
+ return verdict(raw, toolName, toolName === "write" ? "write" : "edit");
160
+ }
161
+ return;
162
+ }
163
+ if (toolName === "bash" || toolName === "exec") {
164
+ const command = typeof ti["command"] === "string" ? ti["command"] : "";
165
+ if (!command) return;
166
+ for (const target of destructiveTargets(command)) {
167
+ if (matchesAny(target, allowRes)) continue;
168
+ if (matchesAny(target, protectRes)) {
169
+ return verdict(target, toolName, "destructive shell command");
170
+ }
171
+ }
172
+ }
173
+ return;
174
+ };
175
+ state.hookUnregister = api.registerHook("PreToolUse", "write|edit|bash|exec", hook);
176
+ api.tools.register({
177
+ name: "path_guard_status",
178
+ description: "Reports path-guard state: protected globs, mode, and counters (invocations, blocks, warns).",
179
+ inputSchema: { type: "object", properties: {} },
180
+ permission: "auto",
181
+ category: "Diagnostics",
182
+ mutating: false,
183
+ async execute() {
184
+ return {
185
+ ok: true,
186
+ enabled: cfg.enabled,
187
+ mode: cfg.mode,
188
+ protect: cfg.protect,
189
+ allow: cfg.allow,
190
+ counters: {
191
+ invocations: state.invocations,
192
+ blocks: state.blocks,
193
+ warns: state.warns
194
+ },
195
+ lastBlock: state.lastBlock
196
+ };
197
+ }
198
+ });
199
+ api.log.info("path-guard plugin loaded", {
200
+ version: "0.1.0",
201
+ enabled: cfg.enabled,
202
+ mode: cfg.mode,
203
+ protectCount: cfg.protect.length
204
+ });
205
+ },
206
+ teardown(api) {
207
+ if (state.hookUnregister) {
208
+ try {
209
+ state.hookUnregister();
210
+ } catch {
211
+ }
212
+ state.hookUnregister = null;
213
+ }
214
+ const final = { invocations: state.invocations, blocks: state.blocks, warns: state.warns };
215
+ state.invocations = 0;
216
+ state.blocks = 0;
217
+ state.warns = 0;
218
+ state.lastBlock = null;
219
+ api.log.info("path-guard: teardown complete", { final });
220
+ },
221
+ async health() {
222
+ return {
223
+ ok: true,
224
+ message: state.lastBlock === null ? `path-guard: ${state.invocations} invocation(s), ${state.blocks} block(s), ${state.warns} warn(s)` : `path-guard: last block on "${state.lastBlock.path}" (${state.lastBlock.tool}) at ${state.lastBlock.when}`,
225
+ counters: {
226
+ invocations: state.invocations,
227
+ blocks: state.blocks,
228
+ warns: state.warns
229
+ }
230
+ };
231
+ }
232
+ };
233
+ var path_guard_default = plugin;
234
+
235
+ export { compilePathGlob, path_guard_default as default, destructiveTargets };
@@ -0,0 +1,57 @@
1
+ import { Plugin } from '@wrongstack/core';
2
+
3
+ /**
4
+ * prompt-firewall plugin — inspects and redacts secrets on the provider
5
+ * wire, before context leaves for the LLM API and as it returns.
6
+ *
7
+ * Distinct from `secret-scanner` (which guards the TOOL boundary): this
8
+ * sits on `AgentExtension.wrapProviderRunner`, so it sees the FULL
9
+ * request that is about to be sent to a third-party LLM provider —
10
+ * regardless of how a secret entered the conversation. It scans the
11
+ * outgoing request's system + message text for high-confidence
12
+ * credential patterns and, depending on `mode`:
13
+ *
14
+ * - `warn` (default) — logs + counts + emits a `prompt-firewall:leak`
15
+ * event; the request goes through unchanged
16
+ * - `redact` — replaces each match with `[REDACTED:<kind>]` in a CLONE
17
+ * of the request before sending, and also redacts secrets echoed back
18
+ * in the response
19
+ * - `block` — throws before the request is sent (the agent's error
20
+ * path surfaces it), so the secret never reaches the provider
21
+ *
22
+ * Safety posture: opt-in — loads inert until
23
+ * `config.extensions['prompt-firewall'].enabled = true`. `warn` is the
24
+ * default so it can never corrupt context until you deliberately choose
25
+ * `redact`/`block`.
26
+ *
27
+ * Config (`config.extensions['prompt-firewall']`):
28
+ *
29
+ * ```jsonc
30
+ * {
31
+ * "enabled": false,
32
+ * "mode": "warn", // "warn" | "redact" | "block"
33
+ * "scanResponse": true, // redact secrets echoed back (redact mode)
34
+ * "allow": [] // regex source strings to exempt (false positives)
35
+ * }
36
+ * ```
37
+ *
38
+ * Tools:
39
+ * - `prompt_firewall_status` — mode, pattern names, detection counters
40
+ *
41
+ * @public
42
+ */
43
+
44
+ interface Detection {
45
+ kind: string;
46
+ count: number;
47
+ }
48
+ /** Detect secret matches in text. Returns per-kind counts (no values). */
49
+ declare function detectSecrets(text: string, allow: RegExp[]): Detection[];
50
+ /** Redact secret matches in text, replacing each with `[REDACTED:<kind>]`. */
51
+ declare function redactSecrets(text: string, allow: RegExp[]): {
52
+ text: string;
53
+ redactions: number;
54
+ };
55
+ declare const plugin: Plugin;
56
+
57
+ export { type Detection, plugin as default, detectSecrets, redactSecrets };
@@ -0,0 +1,290 @@
1
+ // src/prompt-firewall/index.ts
2
+ var PATTERNS = [
3
+ { kind: "aws-access-key", re: /\bAKIA[0-9A-Z]{16}\b/g },
4
+ {
5
+ kind: "aws-secret-key",
6
+ re: /\b(?<![A-Za-z0-9/+])[A-Za-z0-9/+]{40}(?![A-Za-z0-9/+])\b(?=.*aws)/gi
7
+ },
8
+ { kind: "private-key-block", re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/g },
9
+ { kind: "github-token", re: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/g },
10
+ { kind: "openai-key", re: /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/g },
11
+ { kind: "anthropic-key", re: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/g },
12
+ { kind: "slack-token", re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
13
+ { kind: "google-api-key", re: /\bAIza[0-9A-Za-z_-]{35}\b/g },
14
+ { kind: "jwt", re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g },
15
+ { kind: "bearer-token", re: /\bBearer\s+[A-Za-z0-9._-]{20,}\b/g },
16
+ {
17
+ kind: "generic-secret-assignment",
18
+ re: /\b(?:api[_-]?key|secret|password|passwd|token)\s*[:=]\s*['"]?[A-Za-z0-9._/+-]{12,}['"]?/gi
19
+ }
20
+ ];
21
+ function detectSecrets(text, allow) {
22
+ const counts = /* @__PURE__ */ new Map();
23
+ for (const p of PATTERNS) {
24
+ p.re.lastIndex = 0;
25
+ let m = p.re.exec(text);
26
+ while (m !== null) {
27
+ const matched = m[0];
28
+ if (!allow.some((a) => a.test(matched))) {
29
+ counts.set(p.kind, (counts.get(p.kind) ?? 0) + 1);
30
+ }
31
+ m = p.re.exec(text);
32
+ }
33
+ }
34
+ return [...counts.entries()].map(([kind, count]) => ({ kind, count }));
35
+ }
36
+ function redactSecrets(text, allow) {
37
+ let out = text;
38
+ let redactions = 0;
39
+ for (const p of PATTERNS) {
40
+ out = out.replace(new RegExp(p.re.source, p.re.flags), (match) => {
41
+ if (allow.some((a) => a.test(match))) return match;
42
+ redactions += 1;
43
+ return `[REDACTED:${p.kind}]`;
44
+ });
45
+ }
46
+ return { text: out, redactions };
47
+ }
48
+ function collectText(request) {
49
+ const parts = [];
50
+ const walk = (v) => {
51
+ if (typeof v === "string") parts.push(v);
52
+ else if (Array.isArray(v)) for (const i of v) walk(i);
53
+ else if (v && typeof v === "object")
54
+ for (const val of Object.values(v)) walk(val);
55
+ };
56
+ walk(request["system"]);
57
+ walk(request["messages"]);
58
+ return parts.join("\n");
59
+ }
60
+ function redactDeep(value, allow, counter) {
61
+ if (typeof value === "string") {
62
+ const { text, redactions } = redactSecrets(value, allow);
63
+ counter.n += redactions;
64
+ return text;
65
+ }
66
+ if (Array.isArray(value)) return value.map((v) => redactDeep(v, allow, counter));
67
+ if (value && typeof value === "object") {
68
+ const out = {};
69
+ for (const [k, v] of Object.entries(value)) {
70
+ out[k] = redactDeep(v, allow, counter);
71
+ }
72
+ return out;
73
+ }
74
+ return value;
75
+ }
76
+ function readConfig(raw) {
77
+ const base = {
78
+ enabled: false,
79
+ mode: "warn",
80
+ scanResponse: true,
81
+ allow: []
82
+ };
83
+ if (!raw || typeof raw !== "object") return base;
84
+ const r = raw;
85
+ const allow = Array.isArray(r["allow"]) ? r["allow"].filter((s) => typeof s === "string" && s.length > 0).flatMap((s) => {
86
+ try {
87
+ return [new RegExp(s)];
88
+ } catch {
89
+ return [];
90
+ }
91
+ }) : [];
92
+ return {
93
+ enabled: r["enabled"] === true,
94
+ mode: r["mode"] === "redact" ? "redact" : r["mode"] === "block" ? "block" : "warn",
95
+ scanResponse: r["scanResponse"] !== false,
96
+ allow
97
+ };
98
+ }
99
+ var state = {
100
+ invocations: 0,
101
+ requestsWithSecrets: 0,
102
+ requestRedactions: 0,
103
+ responseRedactions: 0,
104
+ blocked: 0,
105
+ byKind: /* @__PURE__ */ new Map(),
106
+ lastDetection: null,
107
+ extensionUnregister: null
108
+ };
109
+ var plugin = {
110
+ name: "prompt-firewall",
111
+ version: "0.1.0",
112
+ description: "Scans the provider wire for credential leaks before context reaches the LLM API (wrapProviderRunner); warn/redact/block. Opt-in; warn by default.",
113
+ apiVersion: "^0.1.10",
114
+ capabilities: { tools: true },
115
+ defaultConfig: { enabled: false, mode: "warn", scanResponse: true, allow: [] },
116
+ configSchema: {
117
+ type: "object",
118
+ properties: {
119
+ enabled: {
120
+ type: "boolean",
121
+ default: false,
122
+ description: "Master switch. OFF by default; redact/block modes can alter or stop provider calls."
123
+ },
124
+ mode: {
125
+ type: "string",
126
+ enum: ["warn", "redact", "block"],
127
+ default: "warn",
128
+ description: "warn = detect only; redact = strip secrets from the request/response; block = refuse the request."
129
+ },
130
+ scanResponse: {
131
+ type: "boolean",
132
+ default: true,
133
+ description: "In redact mode, also redact secrets echoed back in the provider response."
134
+ },
135
+ allow: {
136
+ type: "array",
137
+ items: { type: "string" },
138
+ default: [],
139
+ description: "Regex source strings whose matches are exempt (to silence known false positives)."
140
+ }
141
+ }
142
+ },
143
+ setup(api) {
144
+ state.invocations = 0;
145
+ state.requestsWithSecrets = 0;
146
+ state.requestRedactions = 0;
147
+ state.responseRedactions = 0;
148
+ state.blocked = 0;
149
+ state.byKind.clear();
150
+ state.lastDetection = null;
151
+ if (state.extensionUnregister) {
152
+ try {
153
+ state.extensionUnregister();
154
+ } catch {
155
+ }
156
+ state.extensionUnregister = null;
157
+ }
158
+ const cfg = readConfig(api.config.extensions?.["prompt-firewall"]);
159
+ if (cfg.enabled) {
160
+ state.extensionUnregister = api.extensions.register({
161
+ name: "prompt-firewall",
162
+ owner: "prompt-firewall",
163
+ async wrapProviderRunner(_ctx, request, inner) {
164
+ const req = request ?? {};
165
+ state.invocations += 1;
166
+ const detections = detectSecrets(collectText(req), cfg.allow);
167
+ if (detections.length > 0) {
168
+ state.requestsWithSecrets += 1;
169
+ for (const d of detections) {
170
+ state.byKind.set(d.kind, (state.byKind.get(d.kind) ?? 0) + d.count);
171
+ }
172
+ const kinds = detections.map((d) => d.kind);
173
+ state.lastDetection = { where: "request", kinds, when: (/* @__PURE__ */ new Date()).toISOString() };
174
+ api.metrics.counter("request_leaks", 1);
175
+ api.log.warn("prompt-firewall: secrets detected in outgoing request", { kinds });
176
+ api.emitCustom("prompt-firewall:leak", { where: "request", kinds });
177
+ if (cfg.mode === "block") {
178
+ state.blocked += 1;
179
+ throw new Error(
180
+ `prompt-firewall blocked a provider call: outgoing context contains credential-shaped data (${kinds.join(", ")}). Remove the secret from context, add an \`allow\` pattern, or switch mode to "warn".`
181
+ );
182
+ }
183
+ if (cfg.mode === "redact") {
184
+ const counter = { n: 0 };
185
+ const redactedReq = redactDeep(req, cfg.allow, counter);
186
+ state.requestRedactions += counter.n;
187
+ api.metrics.counter("request_redactions", counter.n);
188
+ const response2 = await inner(_ctx, redactedReq);
189
+ return cfg.scanResponse ? redactResponse(response2, cfg.allow) : response2;
190
+ }
191
+ }
192
+ const response = await inner(_ctx, request);
193
+ if (cfg.mode === "redact" && cfg.scanResponse) {
194
+ return redactResponse(response, cfg.allow);
195
+ }
196
+ return response;
197
+ }
198
+ });
199
+ }
200
+ function redactResponse(response, allow) {
201
+ if (!response || typeof response !== "object") return response;
202
+ const counter = { n: 0 };
203
+ const content = response.content;
204
+ if (content === void 0) return response;
205
+ const redacted = redactDeep(content, allow, counter);
206
+ if (counter.n > 0) {
207
+ state.responseRedactions += counter.n;
208
+ api.metrics.counter("response_redactions", counter.n);
209
+ state.lastDetection = {
210
+ where: "response",
211
+ kinds: ["echoed-secret"],
212
+ when: (/* @__PURE__ */ new Date()).toISOString()
213
+ };
214
+ }
215
+ return { ...response, content: redacted };
216
+ }
217
+ api.tools.register({
218
+ name: "prompt_firewall_status",
219
+ description: "Reports prompt-firewall state: mode, pattern kinds, and detection/redaction/block counters.",
220
+ inputSchema: { type: "object", properties: {} },
221
+ permission: "auto",
222
+ category: "Diagnostics",
223
+ mutating: false,
224
+ async execute() {
225
+ return {
226
+ ok: true,
227
+ enabled: cfg.enabled,
228
+ mode: cfg.mode,
229
+ scanResponse: cfg.scanResponse,
230
+ patterns: PATTERNS.map((p) => p.kind),
231
+ counters: {
232
+ invocations: state.invocations,
233
+ requestsWithSecrets: state.requestsWithSecrets,
234
+ requestRedactions: state.requestRedactions,
235
+ responseRedactions: state.responseRedactions,
236
+ blocked: state.blocked
237
+ },
238
+ byKind: Object.fromEntries(state.byKind),
239
+ lastDetection: state.lastDetection
240
+ };
241
+ }
242
+ });
243
+ api.log.info("prompt-firewall plugin loaded", {
244
+ version: "0.1.0",
245
+ enabled: cfg.enabled,
246
+ mode: cfg.mode,
247
+ patterns: PATTERNS.length
248
+ });
249
+ },
250
+ teardown(api) {
251
+ if (state.extensionUnregister) {
252
+ try {
253
+ state.extensionUnregister();
254
+ } catch {
255
+ }
256
+ state.extensionUnregister = null;
257
+ }
258
+ const final = {
259
+ invocations: state.invocations,
260
+ requestsWithSecrets: state.requestsWithSecrets,
261
+ requestRedactions: state.requestRedactions,
262
+ responseRedactions: state.responseRedactions,
263
+ blocked: state.blocked
264
+ };
265
+ state.invocations = 0;
266
+ state.requestsWithSecrets = 0;
267
+ state.requestRedactions = 0;
268
+ state.responseRedactions = 0;
269
+ state.blocked = 0;
270
+ state.byKind.clear();
271
+ state.lastDetection = null;
272
+ api.log.info("prompt-firewall: teardown complete", { final });
273
+ },
274
+ async health() {
275
+ return {
276
+ ok: true,
277
+ message: `prompt-firewall: ${state.requestsWithSecrets} request(s) with secrets, ${state.requestRedactions} request redaction(s), ${state.blocked} blocked`,
278
+ counters: {
279
+ invocations: state.invocations,
280
+ requestsWithSecrets: state.requestsWithSecrets,
281
+ requestRedactions: state.requestRedactions,
282
+ responseRedactions: state.responseRedactions,
283
+ blocked: state.blocked
284
+ }
285
+ };
286
+ }
287
+ };
288
+ var prompt_firewall_default = plugin;
289
+
290
+ export { prompt_firewall_default as default, detectSecrets, redactSecrets };
@@ -0,0 +1,34 @@
1
+ import { Plugin } from '@wrongstack/core';
2
+
3
+ /**
4
+ * secret-scanner plugin — Pre-tool and post-tool hooks that block, redact,
5
+ * or warn about plaintext credentials flowing into or out of tools.
6
+ *
7
+ * Tools registered:
8
+ * - secret_scanner_status : Show which patterns are active, recent
9
+ * blocks/leaks, and current mode.
10
+ * - secret_scanner_test : Run the scanner against a user-supplied
11
+ * string and report which patterns matched.
12
+ *
13
+ * Hooks registered:
14
+ * - PreToolUse with matcher `bash|write|edit` (configurable). Default
15
+ * action is to BLOCK; the plugin can also auto-redact the offending
16
+ * fields via `HookOutcome.modifiedInput`.
17
+ * - PostToolUse with matcher `*` (configurable via `postToolUseMatcher`).
18
+ * Scans tool OUTPUT for secrets that leaked through. Since the tool
19
+ * has already run, the hook cannot block — instead it injects an
20
+ * `additionalContext` warning so the LLM knows the output contains
21
+ * a secret and should NOT echo it, store it, or commit it.
22
+ *
23
+ * Why a separate plugin from the built-in `DefaultSecretScrubber`?
24
+ * The scrubber is *output* sanitization (replace secrets with
25
+ * `[REDACTED:type]` before they leave the system). The scanner is
26
+ * *prevention* (stop the tool from running with a secret in the first
27
+ * place) + *detection* (flag secrets that leaked through the output).
28
+ * They share the same threat model but act at different points in the
29
+ * pipeline.
30
+ */
31
+
32
+ declare const plugin: Plugin;
33
+
34
+ export { plugin as default };