@wrongstack/plugins 0.277.2 → 0.280.0
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/README.md +838 -0
- package/dist/auto-doc.d.ts +8 -0
- package/dist/auto-doc.js +175 -13
- package/dist/auto-escalate.d.ts +45 -0
- package/dist/auto-escalate.js +190 -0
- package/dist/branch-guard.d.ts +33 -0
- package/dist/branch-guard.js +228 -0
- package/dist/changelog-writer.d.ts +73 -0
- package/dist/changelog-writer.js +369 -0
- package/dist/checkpoint.d.ts +55 -0
- package/dist/checkpoint.js +305 -0
- package/dist/commit-validator.d.ts +33 -0
- package/dist/commit-validator.js +315 -0
- package/dist/config-validator.d.ts +48 -0
- package/dist/config-validator.js +347 -0
- package/dist/context-pins.d.ts +45 -0
- package/dist/context-pins.js +240 -0
- package/dist/cost-tracker.d.ts +40 -1
- package/dist/cost-tracker.js +105 -4
- package/dist/dep-guard.d.ts +65 -0
- package/dist/dep-guard.js +316 -0
- package/dist/diff-summary.d.ts +36 -0
- package/dist/diff-summary.js +235 -0
- package/dist/error-lens.d.ts +67 -0
- package/dist/error-lens.js +280 -0
- package/dist/format-on-save.d.ts +35 -0
- package/dist/format-on-save.js +219 -0
- package/dist/git-autocommit.js +186 -26
- package/dist/import-organizer.d.ts +52 -0
- package/dist/import-organizer.js +274 -0
- package/dist/index.d.ts +32 -6
- package/dist/index.js +10151 -1628
- package/dist/injection-shield.d.ts +49 -0
- package/dist/injection-shield.js +205 -0
- package/dist/lint-gate.d.ts +33 -0
- package/dist/lint-gate.js +394 -0
- package/dist/llm-cache.d.ts +56 -0
- package/dist/llm-cache.js +251 -0
- package/dist/loop-breaker.d.ts +43 -0
- package/dist/loop-breaker.js +241 -0
- package/dist/model-router.d.ts +69 -0
- package/dist/model-router.js +198 -0
- package/dist/notify-hub.d.ts +45 -0
- package/dist/notify-hub.js +304 -0
- package/dist/path-guard.d.ts +54 -0
- package/dist/path-guard.js +235 -0
- package/dist/prompt-firewall.d.ts +57 -0
- package/dist/prompt-firewall.js +290 -0
- package/dist/secret-scanner.d.ts +34 -0
- package/dist/secret-scanner.js +409 -0
- package/dist/semver-bump.js +45 -0
- package/dist/session-recap.d.ts +50 -0
- package/dist/session-recap.js +421 -0
- package/dist/shell-check.js +52 -4
- package/dist/spec-linker.d.ts +51 -0
- package/dist/spec-linker.js +541 -0
- package/dist/template-engine.js +19 -1
- package/dist/test-runner-gate.d.ts +37 -0
- package/dist/test-runner-gate.js +356 -0
- package/dist/todo-listener.d.ts +37 -0
- package/dist/todo-listener.js +216 -0
- package/dist/todo-tracker.d.ts +5 -0
- package/dist/todo-tracker.js +441 -0
- package/dist/token-budget.d.ts +40 -0
- package/dist/token-budget.js +254 -0
- package/dist/token-throttle.d.ts +54 -0
- package/dist/token-throttle.js +203 -0
- package/package.json +116 -12
- package/dist/json-path.d.ts +0 -18
- package/dist/json-path.js +0 -15
- package/dist/web-search.d.ts +0 -19
- 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 };
|