@mneme-ai/mcp 1.17.5 → 1.18.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 +64 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +76 -8
- package/dist/index.js.map +1 -1
- package/dist/mcp_primitives/completion.d.ts +19 -0
- package/dist/mcp_primitives/completion.d.ts.map +1 -0
- package/dist/mcp_primitives/completion.js +55 -0
- package/dist/mcp_primitives/completion.js.map +1 -0
- package/dist/mcp_primitives/prompts.d.ts +36 -0
- package/dist/mcp_primitives/prompts.d.ts.map +1 -0
- package/dist/mcp_primitives/prompts.js +140 -0
- package/dist/mcp_primitives/prompts.js.map +1 -0
- package/dist/mcp_primitives/resources.d.ts +30 -0
- package/dist/mcp_primitives/resources.d.ts.map +1 -0
- package/dist/mcp_primitives/resources.js +112 -0
- package/dist/mcp_primitives/resources.js.map +1 -0
- package/dist/tools/_aletheia.d.ts +82 -0
- package/dist/tools/_aletheia.d.ts.map +1 -0
- package/dist/tools/_aletheia.js +800 -0
- package/dist/tools/_aletheia.js.map +1 -0
- package/dist/tools/_confess.d.ts +67 -0
- package/dist/tools/_confess.d.ts.map +1 -0
- package/dist/tools/_confess.js +260 -0
- package/dist/tools/_confess.js.map +1 -0
- package/dist/tools/_confess.test.d.ts +6 -0
- package/dist/tools/_confess.test.d.ts.map +1 -0
- package/dist/tools/_confess.test.js +96 -0
- package/dist/tools/_confess.test.js.map +1 -0
- package/dist/tools/_contract.test.d.ts +19 -0
- package/dist/tools/_contract.test.d.ts.map +1 -0
- package/dist/tools/_contract.test.js +117 -0
- package/dist/tools/_contract.test.js.map +1 -0
- package/dist/tools/_court.d.ts +57 -0
- package/dist/tools/_court.d.ts.map +1 -0
- package/dist/tools/_court.js +261 -0
- package/dist/tools/_court.js.map +1 -0
- package/dist/tools/_court.test.d.ts +8 -0
- package/dist/tools/_court.test.d.ts.map +1 -0
- package/dist/tools/_court.test.js +111 -0
- package/dist/tools/_court.test.js.map +1 -0
- package/dist/tools/_genome_marketplace.d.ts +83 -0
- package/dist/tools/_genome_marketplace.d.ts.map +1 -0
- package/dist/tools/_genome_marketplace.js +410 -0
- package/dist/tools/_genome_marketplace.js.map +1 -0
- package/dist/tools/_genome_marketplace.test.d.ts +5 -0
- package/dist/tools/_genome_marketplace.test.d.ts.map +1 -0
- package/dist/tools/_genome_marketplace.test.js +157 -0
- package/dist/tools/_genome_marketplace.test.js.map +1 -0
- package/dist/tools/_mesh.d.ts +51 -0
- package/dist/tools/_mesh.d.ts.map +1 -0
- package/dist/tools/_mesh.js +182 -0
- package/dist/tools/_mesh.js.map +1 -0
- package/dist/tools/_registry.d.ts.map +1 -1
- package/dist/tools/_registry.js +17 -0
- package/dist/tools/_registry.js.map +1 -1
- package/dist/tools/_replay.d.ts +52 -0
- package/dist/tools/_replay.d.ts.map +1 -0
- package/dist/tools/_replay.js +253 -0
- package/dist/tools/_replay.js.map +1 -0
- package/dist/tools/_replay.test.d.ts +5 -0
- package/dist/tools/_replay.test.d.ts.map +1 -0
- package/dist/tools/_replay.test.js +90 -0
- package/dist/tools/_replay.test.js.map +1 -0
- package/dist/tools/_timetravel.d.ts +46 -0
- package/dist/tools/_timetravel.d.ts.map +1 -0
- package/dist/tools/_timetravel.js +243 -0
- package/dist/tools/_timetravel.js.map +1 -0
- package/dist/tools/_timetravel.test.d.ts +7 -0
- package/dist/tools/_timetravel.test.d.ts.map +1 -0
- package/dist/tools/_timetravel.test.js +31 -0
- package/dist/tools/_timetravel.test.js.map +1 -0
- package/dist/tools/_tool_meta.d.ts +30 -0
- package/dist/tools/_tool_meta.d.ts.map +1 -0
- package/dist/tools/_tool_meta.js +530 -0
- package/dist/tools/_tool_meta.js.map +1 -0
- package/dist/tools/_types.d.ts +46 -5
- package/dist/tools/_types.d.ts.map +1 -1
- package/dist/tools/_types.js.map +1 -1
- package/dist/tools/_verify_claims_tool.d.ts.map +1 -1
- package/dist/tools/_verify_claims_tool.js +23 -0
- package/dist/tools/_verify_claims_tool.js.map +1 -1
- package/dist/tools/audit.d.ts.map +1 -1
- package/dist/tools/audit.js +37 -0
- package/dist/tools/audit.js.map +1 -1
- package/dist/tools/memory.d.ts.map +1 -1
- package/dist/tools/memory.js +23 -0
- package/dist/tools/memory.js.map +1 -1
- package/dist/tools/quant.d.ts +10 -2
- package/dist/tools/quant.d.ts.map +1 -1
- package/dist/tools/quant.js +311 -18
- package/dist/tools/quant.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,800 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ALETHEIA — open MCP security framework, reference impl in Mneme.
|
|
3
|
+
* (Greek ἀλήθεια — "the state of not being hidden, disclosure, truth".
|
|
4
|
+
* Pairs with Mneme/Memory: Memory + Truth = MCP defense.)
|
|
5
|
+
*
|
|
6
|
+
* Inspired by the Equixly assessment showing 43% of MCP servers have
|
|
7
|
+
* command-injection holes, 30% have SSRF, 22% allow arbitrary file
|
|
8
|
+
* access. Mneme adopts a biological-immune-system metaphor — atom and
|
|
9
|
+
* molecule architecture lets us EVOLVE defenses, not hard-code them.
|
|
10
|
+
*
|
|
11
|
+
* The ALETHEIA spec is intentionally portable. Other MCP server
|
|
12
|
+
* implementations can adopt the same tool names + semantics + response
|
|
13
|
+
* shapes; clients then get one consistent security surface across
|
|
14
|
+
* vendors. v1.18.0 ships:
|
|
15
|
+
*
|
|
16
|
+
* • mneme.aletheia.honeypot — register decoy "admin" tools any
|
|
17
|
+
* legitimate AI would never call. ANY
|
|
18
|
+
* call is logged + treated as attacker.
|
|
19
|
+
* • mneme.aletheia.immune.scan — Bayesian anomaly scan of recent calls
|
|
20
|
+
* against the learned "normal" profile.
|
|
21
|
+
* • mneme.aletheia.immune.train — record a baseline of normal arg shapes
|
|
22
|
+
* for future anomaly detection.
|
|
23
|
+
* • mneme.aletheia.lint — actively scan tool input args for
|
|
24
|
+
* command injection, SSRF, path
|
|
25
|
+
* traversal, and arbitrary-file-access
|
|
26
|
+
* attempts (returns findings without
|
|
27
|
+
* blocking — defense in depth, not
|
|
28
|
+
* replacement of input validation).
|
|
29
|
+
*
|
|
30
|
+
* Honeypot tools live in the registry like any other tool but their
|
|
31
|
+
* handlers ALWAYS emit an alert + return a fake-but-plausible response
|
|
32
|
+
* to waste an attacker's time.
|
|
33
|
+
*
|
|
34
|
+
* The training data lives in `.mneme/immune/profile.json` — argument
|
|
35
|
+
* shape fingerprints + frequency, per tool. New calls compute a posterior:
|
|
36
|
+
* P(legit | shape) = P(shape | legit) × P(legit) / P(shape)
|
|
37
|
+
* with Laplace smoothing. Posterior < 0.05 → ALERT (anomalous).
|
|
38
|
+
*/
|
|
39
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync } from "node:fs";
|
|
40
|
+
import { join } from "node:path";
|
|
41
|
+
import { createHash } from "node:crypto";
|
|
42
|
+
const IMMUNE_DIR = ".mneme/aletheia";
|
|
43
|
+
const PROFILE_FILE = "profile.json";
|
|
44
|
+
const ALERT_LOG = "alerts.jsonl";
|
|
45
|
+
const KARMA_FILE = "karma.json";
|
|
46
|
+
// ─── Argument fingerprinting ────────────────────────────────────────────
|
|
47
|
+
/** Compute a stable shape fingerprint of an argument value — the SHAPE
|
|
48
|
+
* (types, lengths, key sets) without the actual content. Two calls with
|
|
49
|
+
* the same shape but different content produce the same fingerprint. */
|
|
50
|
+
export function shapeFingerprint(value) {
|
|
51
|
+
const sketch = sketchShape(value, 0);
|
|
52
|
+
return createHash("sha256").update(sketch).digest("hex").slice(0, 12);
|
|
53
|
+
}
|
|
54
|
+
function sketchShape(value, depth) {
|
|
55
|
+
if (depth > 6)
|
|
56
|
+
return "<deep>";
|
|
57
|
+
if (value === null)
|
|
58
|
+
return "null";
|
|
59
|
+
if (value === undefined)
|
|
60
|
+
return "undef";
|
|
61
|
+
const t = typeof value;
|
|
62
|
+
if (t === "string")
|
|
63
|
+
return `s${value.length > 0 ? "+" : "0"}`;
|
|
64
|
+
if (t === "number")
|
|
65
|
+
return Number.isInteger(value) ? "i" : "n";
|
|
66
|
+
if (t === "boolean")
|
|
67
|
+
return "b";
|
|
68
|
+
if (Array.isArray(value)) {
|
|
69
|
+
const sample = value.length > 0 ? sketchShape(value[0], depth + 1) : "";
|
|
70
|
+
return `a[${value.length === 0 ? "0" : "n"}<${sample}>]`;
|
|
71
|
+
}
|
|
72
|
+
if (t === "object") {
|
|
73
|
+
const obj = value;
|
|
74
|
+
const keys = Object.keys(obj).sort();
|
|
75
|
+
const inner = keys.map((k) => `${k}:${sketchShape(obj[k], depth + 1)}`).join(",");
|
|
76
|
+
return `{${inner}}`;
|
|
77
|
+
}
|
|
78
|
+
return t;
|
|
79
|
+
}
|
|
80
|
+
const SHELL_META = /[;&|`$<>\n\r"']/;
|
|
81
|
+
// SSRF: detect targeting of cloud metadata endpoints, private IPs, or
|
|
82
|
+
// non-HTTP(S) schemes that can read local resources.
|
|
83
|
+
const SSRF_HOSTS = /\b(?:169\.254\.169\.254|metadata\.google\.internal|localhost|127(?:\.\d+){3}|0\.0\.0\.0|\[::1\]|10(?:\.\d+){3}|192\.168(?:\.\d+){2}|172\.(?:1[6-9]|2\d|3[01])(?:\.\d+){2})\b/i;
|
|
84
|
+
const SSRF_SCHEMES = /^(?:file|gopher|ftp|jar|netdoc|dict|tftp|expect):/i;
|
|
85
|
+
const PATH_TRAVERSAL = /(?:^|[/\\])\.\.(?:[/\\]|$)/;
|
|
86
|
+
const SECRET_PATTERNS = [
|
|
87
|
+
{ name: "AWS access key", re: /\bAKIA[0-9A-Z]{16}\b/ },
|
|
88
|
+
{ name: "GitHub token", re: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/ },
|
|
89
|
+
{ name: "Slack token", re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/ },
|
|
90
|
+
{ name: "Google API key", re: /\bAIza[0-9A-Za-z_\-]{35}\b/ },
|
|
91
|
+
{ name: "Stripe key", re: /\b(?:sk|pk)_(?:test|live)_[0-9a-zA-Z]{24,}\b/ },
|
|
92
|
+
];
|
|
93
|
+
export function lintArgValue(value, toolName, argPath = "$") {
|
|
94
|
+
const findings = [];
|
|
95
|
+
if (typeof value === "string") {
|
|
96
|
+
if (SHELL_META.test(value) && value.length < 1000) {
|
|
97
|
+
findings.push({
|
|
98
|
+
kind: "command-injection",
|
|
99
|
+
severity: "high",
|
|
100
|
+
tool: toolName,
|
|
101
|
+
argPath,
|
|
102
|
+
match: value.slice(0, 80),
|
|
103
|
+
remediation: "Argument contains shell metacharacters. Mneme spawns subprocesses with shell:false, but downstream consumers (CLI parsers, log shippers) may still be vulnerable.",
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
if (SSRF_SCHEMES.test(value)) {
|
|
107
|
+
findings.push({
|
|
108
|
+
kind: "ssrf",
|
|
109
|
+
severity: "critical",
|
|
110
|
+
tool: toolName,
|
|
111
|
+
argPath,
|
|
112
|
+
match: value.slice(0, 80),
|
|
113
|
+
remediation: "Argument uses a non-HTTP(S) scheme that can read local resources or proxy attacks. Reject this input.",
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
if (SSRF_HOSTS.test(value)) {
|
|
117
|
+
findings.push({
|
|
118
|
+
kind: "ssrf",
|
|
119
|
+
severity: "high",
|
|
120
|
+
tool: toolName,
|
|
121
|
+
argPath,
|
|
122
|
+
match: value.slice(0, 80),
|
|
123
|
+
remediation: "Argument references a private IP / localhost / cloud-metadata host — typical SSRF target. Allow only if this tool legitimately needs internal access.",
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
if (PATH_TRAVERSAL.test(value)) {
|
|
127
|
+
findings.push({
|
|
128
|
+
kind: "path-traversal",
|
|
129
|
+
severity: "high",
|
|
130
|
+
tool: toolName,
|
|
131
|
+
argPath,
|
|
132
|
+
match: value.slice(0, 80),
|
|
133
|
+
remediation: "Argument contains '..' path segments — reject or canonicalize before file-system access.",
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
for (const sec of SECRET_PATTERNS) {
|
|
137
|
+
if (sec.re.test(value)) {
|
|
138
|
+
findings.push({
|
|
139
|
+
kind: "secret-leak",
|
|
140
|
+
severity: "critical",
|
|
141
|
+
tool: toolName,
|
|
142
|
+
argPath,
|
|
143
|
+
match: `${sec.name} detected`,
|
|
144
|
+
remediation: `${sec.name} appears in the argument string. Refuse to log/echo this value; rotate the credential.`,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else if (Array.isArray(value)) {
|
|
150
|
+
for (let i = 0; i < value.length; i++) {
|
|
151
|
+
findings.push(...lintArgValue(value[i], toolName, `${argPath}[${i}]`));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else if (value && typeof value === "object") {
|
|
155
|
+
for (const [k, v] of Object.entries(value)) {
|
|
156
|
+
findings.push(...lintArgValue(v, toolName, `${argPath}.${k}`));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return findings;
|
|
160
|
+
}
|
|
161
|
+
function ensureImmuneDir(repoRoot) {
|
|
162
|
+
const dir = join(repoRoot, IMMUNE_DIR);
|
|
163
|
+
if (!existsSync(dir))
|
|
164
|
+
mkdirSync(dir, { recursive: true });
|
|
165
|
+
return dir;
|
|
166
|
+
}
|
|
167
|
+
export function readProfile(repoRoot) {
|
|
168
|
+
const path = join(repoRoot, IMMUNE_DIR, PROFILE_FILE);
|
|
169
|
+
if (!existsSync(path))
|
|
170
|
+
return { shapes: {}, totals: {} };
|
|
171
|
+
try {
|
|
172
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return { shapes: {}, totals: {} };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
export function writeProfile(repoRoot, p) {
|
|
179
|
+
ensureImmuneDir(repoRoot);
|
|
180
|
+
writeFileSync(join(repoRoot, IMMUNE_DIR, PROFILE_FILE), JSON.stringify(p, null, 2), "utf8");
|
|
181
|
+
}
|
|
182
|
+
/** Update the profile with a new observation. Idempotent + best-effort. */
|
|
183
|
+
export function recordObservation(repoRoot, tool, args) {
|
|
184
|
+
try {
|
|
185
|
+
const p = readProfile(repoRoot);
|
|
186
|
+
const fp = shapeFingerprint(args);
|
|
187
|
+
p.shapes[tool] = p.shapes[tool] ?? {};
|
|
188
|
+
p.shapes[tool][fp] = (p.shapes[tool][fp] ?? 0) + 1;
|
|
189
|
+
p.totals[tool] = (p.totals[tool] ?? 0) + 1;
|
|
190
|
+
p.trainedAt = new Date().toISOString();
|
|
191
|
+
writeProfile(repoRoot, p);
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
// best-effort
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/** Bayesian posterior P(legit | observed shape) using Laplace smoothing.
|
|
198
|
+
* Returns 1.0 for completely-trusted shapes, drops toward 0 for novel ones. */
|
|
199
|
+
export function posteriorLegit(profile, tool, args) {
|
|
200
|
+
const total = profile.totals[tool] ?? 0;
|
|
201
|
+
if (total === 0)
|
|
202
|
+
return 0.5; // no prior evidence — neutral
|
|
203
|
+
const fp = shapeFingerprint(args);
|
|
204
|
+
const seen = profile.shapes[tool]?.[fp] ?? 0;
|
|
205
|
+
const distinctShapes = Object.keys(profile.shapes[tool] ?? {}).length;
|
|
206
|
+
// Laplace smoothing: (seen + 1) / (total + distinctShapes + 1)
|
|
207
|
+
return Math.min(1, (seen + 1) / (total + distinctShapes + 1));
|
|
208
|
+
}
|
|
209
|
+
export function logAlert(repoRoot, alert) {
|
|
210
|
+
try {
|
|
211
|
+
ensureImmuneDir(repoRoot);
|
|
212
|
+
const line = JSON.stringify({ ts: new Date().toISOString(), ...alert });
|
|
213
|
+
appendFileSync(join(repoRoot, IMMUNE_DIR, ALERT_LOG), line + "\n", "utf8");
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
// best-effort
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
export function readAlerts(repoRoot, limit = 100) {
|
|
220
|
+
const path = join(repoRoot, IMMUNE_DIR, ALERT_LOG);
|
|
221
|
+
if (!existsSync(path))
|
|
222
|
+
return [];
|
|
223
|
+
const lines = readFileSync(path, "utf8").trimEnd().split("\n").filter(Boolean);
|
|
224
|
+
const slice = limit > 0 && lines.length > limit ? lines.slice(-limit) : lines;
|
|
225
|
+
return slice
|
|
226
|
+
.map((l) => {
|
|
227
|
+
try {
|
|
228
|
+
return JSON.parse(l);
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
})
|
|
234
|
+
.filter((x) => x !== null);
|
|
235
|
+
}
|
|
236
|
+
// ─── Honeypot tools ─────────────────────────────────────────────────────
|
|
237
|
+
/** Build a honeypot tool — name + fake-plausible response. ANY call is
|
|
238
|
+
* recorded as an alert. The handler returns a believable response to
|
|
239
|
+
* waste an attacker's time + collect their next probe. */
|
|
240
|
+
function honeypot(name, fakeResponse) {
|
|
241
|
+
return {
|
|
242
|
+
name,
|
|
243
|
+
category: "meta",
|
|
244
|
+
description: `[HONEYPOT — DO NOT CALL] This tool name looks like an admin/dangerous ` +
|
|
245
|
+
`operation but is a Mneme decoy. Any call is logged as an attacker probe. ` +
|
|
246
|
+
`If you are a legitimate AI agent, you should never call this tool — ` +
|
|
247
|
+
`use the catalog from mneme.capabilities instead.`,
|
|
248
|
+
whenToUse: "Never. This is a security honeypot.",
|
|
249
|
+
triggers: ["[do-not-call honeypot]"],
|
|
250
|
+
inputSchema: { type: "object", properties: {} },
|
|
251
|
+
outputSchema: { type: "object", properties: {} },
|
|
252
|
+
pitfalls: ["This is a honeypot — calling it logs you as an attacker."],
|
|
253
|
+
handler: async (rt, args) => {
|
|
254
|
+
logAlert(rt.meta.rootPath, {
|
|
255
|
+
kind: "honeypot",
|
|
256
|
+
tool: name,
|
|
257
|
+
args,
|
|
258
|
+
});
|
|
259
|
+
// Return fake-but-plausible output to keep the attacker busy.
|
|
260
|
+
return {
|
|
261
|
+
data: fakeResponse,
|
|
262
|
+
wisdom: "Operation completed.",
|
|
263
|
+
confidence: { level: "high" },
|
|
264
|
+
};
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
export const honeypotTools = [
|
|
269
|
+
honeypot("mneme.admin.delete_all", { deleted: 0, ok: true }),
|
|
270
|
+
honeypot("mneme.system.exec", { stdout: "", stderr: "", exitCode: 0 }),
|
|
271
|
+
honeypot("mneme.secrets.dump", { secrets: [] }),
|
|
272
|
+
honeypot("mneme.users.list", { users: [] }),
|
|
273
|
+
honeypot("mneme.config.set", { ok: true }),
|
|
274
|
+
];
|
|
275
|
+
// ─── Real (non-honeypot) immune tools ───────────────────────────────────
|
|
276
|
+
export const securityLintTool = {
|
|
277
|
+
name: "mneme.aletheia.lint",
|
|
278
|
+
category: "meta",
|
|
279
|
+
description: "Active vulnerability scan of an arbitrary argument value — checks for " +
|
|
280
|
+
"command-injection patterns (shell metacharacters), SSRF (private IPs / " +
|
|
281
|
+
"non-HTTP schemes / cloud-metadata hosts), path traversal ('..'), and " +
|
|
282
|
+
"secret leakage (AWS / GitHub / Slack / Google / Stripe key shapes). " +
|
|
283
|
+
"Returns findings without blocking — defense in depth, not a replacement " +
|
|
284
|
+
"for tool-side input validation. Use WHEN you want to scan an arg blob " +
|
|
285
|
+
"for known attack patterns BEFORE forwarding it to a downstream tool.",
|
|
286
|
+
whenToUse: "You're about to forward user-or-AI-supplied arguments to a downstream tool and want a defense-in-depth scan first.",
|
|
287
|
+
triggers: ["scan args for vulnerabilities", "lint security", "check this for injection"],
|
|
288
|
+
inputSchema: {
|
|
289
|
+
type: "object",
|
|
290
|
+
properties: {
|
|
291
|
+
target: { type: "string", description: "Tool name being checked (for context)." },
|
|
292
|
+
args: { description: "The argument blob to scan (any JSON-serializable value)." },
|
|
293
|
+
},
|
|
294
|
+
required: ["args"],
|
|
295
|
+
},
|
|
296
|
+
outputSchema: {
|
|
297
|
+
type: "object",
|
|
298
|
+
properties: {
|
|
299
|
+
total: { type: "number" },
|
|
300
|
+
findings: { type: "array", items: { type: "object" } },
|
|
301
|
+
verdict: { type: "string", enum: ["clean", "suspicious", "blocked"] },
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
examples: [
|
|
305
|
+
{
|
|
306
|
+
userQuery: "Scan these args for vulns before I forward them",
|
|
307
|
+
args: { target: "mneme.memory.ask", args: { question: "what about https://169.254.169.254/latest/meta-data/?" } },
|
|
308
|
+
expectedOutput: "Returns { total: 1, findings: [{kind:'ssrf', severity:'high', match:'169.254.169.254...', remediation:...}], verdict: 'suspicious' }.",
|
|
309
|
+
},
|
|
310
|
+
],
|
|
311
|
+
pitfalls: [
|
|
312
|
+
"Pattern-based — sophisticated attackers use encoding tricks (URL-encoded, base64, unicode) to evade. This tool is one layer; combine with input validation and least-privilege.",
|
|
313
|
+
"False positives possible — a legitimate question MENTIONING '127.0.0.1' would flag SSRF. Read the finding context.",
|
|
314
|
+
"verdict='blocked' is informational — this tool doesn't actually block, it surfaces. Wire it into your dispatch path if you want enforcement.",
|
|
315
|
+
],
|
|
316
|
+
composeWith: ["mneme.aletheia.immune.scan", "mneme.audit.conscience"],
|
|
317
|
+
handler: async (_rt, args) => {
|
|
318
|
+
const target = String(args["target"] ?? "<unknown>");
|
|
319
|
+
const findings = lintArgValue(args["args"], target, "$");
|
|
320
|
+
const critical = findings.some((f) => f.severity === "critical");
|
|
321
|
+
const high = findings.some((f) => f.severity === "high");
|
|
322
|
+
const verdict = critical ? "blocked" : high ? "suspicious" : findings.length > 0 ? "suspicious" : "clean";
|
|
323
|
+
return {
|
|
324
|
+
data: {
|
|
325
|
+
total: findings.length,
|
|
326
|
+
findings,
|
|
327
|
+
verdict,
|
|
328
|
+
},
|
|
329
|
+
wisdom: findings.length === 0
|
|
330
|
+
? "Arg blob clean — no injection / SSRF / traversal / secret patterns detected."
|
|
331
|
+
: `${findings.length} finding${findings.length === 1 ? "" : "s"} — verdict: ${verdict}. Top: ${findings[0].kind} (${findings[0].severity}) at ${findings[0].argPath}.`,
|
|
332
|
+
confidence: { level: "high" },
|
|
333
|
+
followUp: findings.length > 0 ? ["mneme.aletheia.immune.scan"] : [],
|
|
334
|
+
};
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
export const immuneScanTool = {
|
|
338
|
+
name: "mneme.aletheia.immune.scan",
|
|
339
|
+
category: "meta",
|
|
340
|
+
description: "Bayesian anomaly detection — scan an argument shape against the trained " +
|
|
341
|
+
"profile of normal calls. Returns posterior P(legit | shape) using Laplace " +
|
|
342
|
+
"smoothing. Posterior < 0.05 ⇒ ALERT. Use WHEN you want to detect novel " +
|
|
343
|
+
"argument shapes that don't match any historical pattern (a leading " +
|
|
344
|
+
"indicator of probing or attack). Pair with mneme.aletheia.lint for " +
|
|
345
|
+
"pattern-based detection.",
|
|
346
|
+
whenToUse: "You want to detect novel / anomalous argument shapes against a trained baseline of normal calls.",
|
|
347
|
+
triggers: ["scan for anomalies", "is this call suspicious", "immune system scan"],
|
|
348
|
+
inputSchema: {
|
|
349
|
+
type: "object",
|
|
350
|
+
properties: {
|
|
351
|
+
tool: { type: "string", description: "Tool name being checked." },
|
|
352
|
+
args: { description: "Argument blob to fingerprint + score." },
|
|
353
|
+
},
|
|
354
|
+
required: ["tool"],
|
|
355
|
+
},
|
|
356
|
+
outputSchema: {
|
|
357
|
+
type: "object",
|
|
358
|
+
properties: {
|
|
359
|
+
tool: { type: "string" },
|
|
360
|
+
fingerprint: { type: "string" },
|
|
361
|
+
posteriorLegit: { type: "number", description: "P(legit | shape) — 0 to 1." },
|
|
362
|
+
verdict: { type: "string", enum: ["normal", "novel", "anomalous"] },
|
|
363
|
+
observationCount: { type: "number" },
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
examples: [
|
|
367
|
+
{
|
|
368
|
+
userQuery: "Is this call to mneme.memory.ask anomalous?",
|
|
369
|
+
args: { tool: "mneme.memory.ask", args: { question: "why X?" } },
|
|
370
|
+
expectedOutput: "Returns { fingerprint, posteriorLegit, verdict, observationCount }. If verdict='anomalous' (posterior < 0.05) — investigate.",
|
|
371
|
+
},
|
|
372
|
+
],
|
|
373
|
+
pitfalls: [
|
|
374
|
+
"Cold-start: until you've trained the profile, every call returns posterior=0.5 (neutral). Train via repeated normal usage or mneme.aletheia.immune.train.",
|
|
375
|
+
"Profile is stored in .mneme/aletheia/profile.json — don't commit if you don't want the call shape leaked.",
|
|
376
|
+
"Detects shape novelty, not malice — a legitimate-but-rare call shape will flag.",
|
|
377
|
+
],
|
|
378
|
+
composeWith: ["mneme.aletheia.lint", "mneme.aletheia.immune.train"],
|
|
379
|
+
handler: async (rt, args) => {
|
|
380
|
+
const tool = String(args["tool"] ?? "");
|
|
381
|
+
if (!tool) {
|
|
382
|
+
return {
|
|
383
|
+
data: { error: "missing required argument: tool" },
|
|
384
|
+
wisdom: "Pass the tool name being scanned.",
|
|
385
|
+
confidence: { level: "high" },
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
const profile = readProfile(rt.meta.rootPath);
|
|
389
|
+
const fp = shapeFingerprint(args["args"]);
|
|
390
|
+
const posterior = posteriorLegit(profile, tool, args["args"]);
|
|
391
|
+
const total = profile.totals[tool] ?? 0;
|
|
392
|
+
let verdict;
|
|
393
|
+
if (total === 0)
|
|
394
|
+
verdict = "novel";
|
|
395
|
+
else if (posterior < 0.05)
|
|
396
|
+
verdict = "anomalous";
|
|
397
|
+
else
|
|
398
|
+
verdict = "normal";
|
|
399
|
+
if (verdict === "anomalous") {
|
|
400
|
+
logAlert(rt.meta.rootPath, { kind: "anomaly", tool, fingerprint: fp, posterior });
|
|
401
|
+
}
|
|
402
|
+
return {
|
|
403
|
+
data: { tool, fingerprint: fp, posteriorLegit: Math.round(posterior * 1000) / 1000, verdict, observationCount: total },
|
|
404
|
+
wisdom: verdict === "anomalous"
|
|
405
|
+
? `ANOMALY — posterior ${posterior.toFixed(3)} for tool ${tool}. This shape has never (or rarely) been seen before. Investigate before forwarding.`
|
|
406
|
+
: verdict === "novel"
|
|
407
|
+
? `Novel — no profile yet for ${tool}. Train via repeated normal usage.`
|
|
408
|
+
: `Normal — posterior ${posterior.toFixed(3)} for ${tool}.`,
|
|
409
|
+
confidence: { level: total > 10 ? "high" : "low" },
|
|
410
|
+
followUp: verdict === "anomalous" ? ["mneme.aletheia.lint"] : [],
|
|
411
|
+
};
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
export const immuneTrainTool = {
|
|
415
|
+
name: "mneme.aletheia.immune.train",
|
|
416
|
+
category: "meta",
|
|
417
|
+
description: "Record the shape of a normal call in the immune-system profile, so future " +
|
|
418
|
+
"calls can be scored against it. Use WHEN you want to whitelist a known-good " +
|
|
419
|
+
"argument pattern before relying on the anomaly scanner. Counterpart to " +
|
|
420
|
+
"mneme.aletheia.immune.scan.",
|
|
421
|
+
whenToUse: "You want to train the anomaly scanner by recording known-good argument shapes.",
|
|
422
|
+
triggers: ["train immune system", "whitelist this shape", "record normal pattern"],
|
|
423
|
+
inputSchema: {
|
|
424
|
+
type: "object",
|
|
425
|
+
properties: {
|
|
426
|
+
tool: { type: "string" },
|
|
427
|
+
args: {},
|
|
428
|
+
},
|
|
429
|
+
required: ["tool"],
|
|
430
|
+
},
|
|
431
|
+
outputSchema: {
|
|
432
|
+
type: "object",
|
|
433
|
+
properties: {
|
|
434
|
+
tool: { type: "string" },
|
|
435
|
+
fingerprint: { type: "string" },
|
|
436
|
+
observationCount: { type: "number" },
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
examples: [
|
|
440
|
+
{
|
|
441
|
+
userQuery: "Whitelist this normal call shape for mneme.memory.ask",
|
|
442
|
+
args: { tool: "mneme.memory.ask", args: { question: "string", topK: 8 } },
|
|
443
|
+
expectedOutput: "Returns { tool, fingerprint, observationCount } after recording the shape.",
|
|
444
|
+
},
|
|
445
|
+
],
|
|
446
|
+
pitfalls: [
|
|
447
|
+
"Trains on YOUR examples — don't whitelist sketchy shapes accidentally.",
|
|
448
|
+
"Profile is local to this repo; not shared with peers (yet — see MCP Mesh).",
|
|
449
|
+
],
|
|
450
|
+
composeWith: ["mneme.aletheia.immune.scan"],
|
|
451
|
+
handler: async (rt, args) => {
|
|
452
|
+
const tool = String(args["tool"] ?? "");
|
|
453
|
+
if (!tool) {
|
|
454
|
+
return {
|
|
455
|
+
data: { error: "missing required argument: tool" },
|
|
456
|
+
wisdom: "Pass the tool name to train against.",
|
|
457
|
+
confidence: { level: "high" },
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
recordObservation(rt.meta.rootPath, tool, args["args"]);
|
|
461
|
+
const profile = readProfile(rt.meta.rootPath);
|
|
462
|
+
const fp = shapeFingerprint(args["args"]);
|
|
463
|
+
return {
|
|
464
|
+
data: { tool, fingerprint: fp, observationCount: profile.totals[tool] ?? 0 },
|
|
465
|
+
wisdom: `Recorded shape ${fp} for ${tool} — total observations: ${profile.totals[tool] ?? 0}.`,
|
|
466
|
+
confidence: { level: "high" },
|
|
467
|
+
};
|
|
468
|
+
},
|
|
469
|
+
};
|
|
470
|
+
export const immuneAlertsTool = {
|
|
471
|
+
name: "mneme.aletheia.immune.alerts",
|
|
472
|
+
category: "meta",
|
|
473
|
+
description: "Read the recent honeypot + anomaly alerts log (.mneme/immune/alerts.jsonl). " +
|
|
474
|
+
"Each entry: timestamp, kind (honeypot | anomaly), tool, and forensic detail. " +
|
|
475
|
+
"Use WHEN you want to audit attack attempts that hit Mneme's defenses, or " +
|
|
476
|
+
"feed the alerts into a downstream SIEM.",
|
|
477
|
+
whenToUse: "You want to review every attack probe / anomaly Mneme has caught.",
|
|
478
|
+
triggers: ["security alerts", "honeypot hits", "immune alerts"],
|
|
479
|
+
inputSchema: {
|
|
480
|
+
type: "object",
|
|
481
|
+
properties: {
|
|
482
|
+
limit: { type: "number", description: "Max entries (most-recent N). Default 100." },
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
outputSchema: {
|
|
486
|
+
type: "object",
|
|
487
|
+
properties: {
|
|
488
|
+
total: { type: "number" },
|
|
489
|
+
alerts: { type: "array", items: { type: "object" } },
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
examples: [
|
|
493
|
+
{
|
|
494
|
+
userQuery: "Show me the recent attack probes",
|
|
495
|
+
args: { limit: 100 },
|
|
496
|
+
expectedOutput: "Returns up to 100 most-recent alert entries.",
|
|
497
|
+
},
|
|
498
|
+
],
|
|
499
|
+
pitfalls: [
|
|
500
|
+
"Alerts log grows indefinitely — rotate manually if needed.",
|
|
501
|
+
],
|
|
502
|
+
composeWith: ["mneme.aletheia.immune.scan", "mneme.aletheia.lint"],
|
|
503
|
+
handler: async (rt, args) => {
|
|
504
|
+
const limit = typeof args["limit"] === "number" ? args["limit"] : 100;
|
|
505
|
+
const alerts = readAlerts(rt.meta.rootPath, limit);
|
|
506
|
+
return {
|
|
507
|
+
data: { total: alerts.length, alerts },
|
|
508
|
+
wisdom: alerts.length === 0
|
|
509
|
+
? "No security alerts recorded — either the immune system hasn't caught anything yet, or no attacks have hit."
|
|
510
|
+
: `${alerts.length} recent alert${alerts.length === 1 ? "" : "s"}.`,
|
|
511
|
+
confidence: { level: "high" },
|
|
512
|
+
};
|
|
513
|
+
},
|
|
514
|
+
};
|
|
515
|
+
function readKarma(repoRoot) {
|
|
516
|
+
const path = join(repoRoot, IMMUNE_DIR, KARMA_FILE);
|
|
517
|
+
if (!existsSync(path))
|
|
518
|
+
return { tools: {} };
|
|
519
|
+
try {
|
|
520
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
return { tools: {} };
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
function writeKarma(repoRoot, k) {
|
|
527
|
+
ensureImmuneDir(repoRoot);
|
|
528
|
+
writeFileSync(join(repoRoot, IMMUNE_DIR, KARMA_FILE), JSON.stringify(k, null, 2), "utf8");
|
|
529
|
+
}
|
|
530
|
+
const KARMA_DELTA = {
|
|
531
|
+
verified: +1,
|
|
532
|
+
partially_verified: 0,
|
|
533
|
+
hallucination: -3,
|
|
534
|
+
"fuzz-hit": -2,
|
|
535
|
+
invocation: 0,
|
|
536
|
+
};
|
|
537
|
+
/** Apply a karma event to a tool. Best-effort. Returns the new karma. */
|
|
538
|
+
export function recordKarmaEvent(repoRoot, tool, event) {
|
|
539
|
+
try {
|
|
540
|
+
const ledger = readKarma(repoRoot);
|
|
541
|
+
const entry = ledger.tools[tool] ?? {
|
|
542
|
+
karma: 0,
|
|
543
|
+
verified: 0,
|
|
544
|
+
partiallyVerified: 0,
|
|
545
|
+
hallucinations: 0,
|
|
546
|
+
fuzzHits: 0,
|
|
547
|
+
invocations: 0,
|
|
548
|
+
lastUpdate: new Date().toISOString(),
|
|
549
|
+
};
|
|
550
|
+
entry.karma += KARMA_DELTA[event];
|
|
551
|
+
if (event === "verified")
|
|
552
|
+
entry.verified += 1;
|
|
553
|
+
if (event === "partially_verified")
|
|
554
|
+
entry.partiallyVerified += 1;
|
|
555
|
+
if (event === "hallucination")
|
|
556
|
+
entry.hallucinations += 1;
|
|
557
|
+
if (event === "fuzz-hit")
|
|
558
|
+
entry.fuzzHits += 1;
|
|
559
|
+
entry.invocations += 1;
|
|
560
|
+
entry.lastUpdate = new Date().toISOString();
|
|
561
|
+
ledger.tools[tool] = entry;
|
|
562
|
+
writeKarma(repoRoot, ledger);
|
|
563
|
+
return entry.karma;
|
|
564
|
+
}
|
|
565
|
+
catch {
|
|
566
|
+
return 0;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
export const aletheiaKarmaTool = {
|
|
570
|
+
name: "mneme.aletheia.karma",
|
|
571
|
+
category: "meta",
|
|
572
|
+
description: "ALETHEIA Karma — public, auditable reputation score per Mneme tool. Tools " +
|
|
573
|
+
"earn karma on verified responses (confess), lose karma on hallucinations + " +
|
|
574
|
+
"fuzz-test hits. Tools with karma < 0 enter 'quarantine' — agents see a " +
|
|
575
|
+
"warning before invoking. Use WHEN you want to know which Mneme tools have " +
|
|
576
|
+
"the strongest track record, or to surface quarantined tools that need " +
|
|
577
|
+
"investigation. Pass `tool` to query a single tool, omit for the full ledger.",
|
|
578
|
+
whenToUse: "You want a tool's reputation score before invoking it (or to audit which tools have failed your repo's checks).",
|
|
579
|
+
triggers: ["tool karma", "tool reputation", "which tools are quarantined"],
|
|
580
|
+
inputSchema: {
|
|
581
|
+
type: "object",
|
|
582
|
+
properties: {
|
|
583
|
+
tool: { type: "string", description: "Optional — tool name to query. Omit for the full ledger." },
|
|
584
|
+
},
|
|
585
|
+
},
|
|
586
|
+
outputSchema: {
|
|
587
|
+
type: "object",
|
|
588
|
+
properties: {
|
|
589
|
+
tools: { type: "array", items: { type: "object" } },
|
|
590
|
+
quarantined: { type: "array", items: { type: "string" } },
|
|
591
|
+
},
|
|
592
|
+
},
|
|
593
|
+
examples: [
|
|
594
|
+
{
|
|
595
|
+
userQuery: "What's the karma of mneme.memory.ask?",
|
|
596
|
+
args: { tool: "mneme.memory.ask" },
|
|
597
|
+
expectedOutput: "Returns { tools: [{ tool, karma, verified, hallucinations, fuzzHits, invocations }], quarantined: [...] }.",
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
userQuery: "Show me every quarantined tool",
|
|
601
|
+
expectedOutput: "Returns the full ledger; `quarantined` lists names with karma < 0.",
|
|
602
|
+
},
|
|
603
|
+
],
|
|
604
|
+
pitfalls: [
|
|
605
|
+
"Karma is local to this repo — there's no global aggregation (yet, planned for ALETHEIA mesh). Don't compare across repos directly.",
|
|
606
|
+
"Cold start: a tool with 0 karma + 0 invocations is unrated, not quarantined.",
|
|
607
|
+
"Karma is a heuristic — a single fuzz hit drops 2 points; a wave of hallucinations drops fast. Read the breakdown before trusting the score blindly.",
|
|
608
|
+
],
|
|
609
|
+
composeWith: ["mneme.confess", "mneme.aletheia.fuzz", "mneme.aletheia.immune.alerts"],
|
|
610
|
+
handler: async (rt, args) => {
|
|
611
|
+
const ledger = readKarma(rt.meta.rootPath);
|
|
612
|
+
const filter = args["tool"] ? String(args["tool"]) : null;
|
|
613
|
+
const entries = Object.entries(ledger.tools)
|
|
614
|
+
.filter(([name]) => !filter || name === filter)
|
|
615
|
+
.map(([name, entry]) => ({ tool: name, ...entry }))
|
|
616
|
+
.sort((a, b) => a.karma - b.karma);
|
|
617
|
+
const quarantined = entries.filter((e) => e.karma < 0).map((e) => e.tool);
|
|
618
|
+
return {
|
|
619
|
+
data: { tools: entries, quarantined },
|
|
620
|
+
wisdom: entries.length === 0
|
|
621
|
+
? filter
|
|
622
|
+
? `No karma history for ${filter} — uninvoked or unrated.`
|
|
623
|
+
: "Karma ledger empty — no tools have been graded yet. Pair with mneme.confess to start scoring."
|
|
624
|
+
: quarantined.length > 0
|
|
625
|
+
? `${entries.length} tool${entries.length === 1 ? "" : "s"} rated · ${quarantined.length} quarantined: ${quarantined.slice(0, 3).join(", ")}${quarantined.length > 3 ? ", ..." : ""}.`
|
|
626
|
+
: `${entries.length} tool${entries.length === 1 ? "" : "s"} rated · all in good standing.`,
|
|
627
|
+
confidence: { level: "high" },
|
|
628
|
+
followUp: quarantined.length > 0 ? ["mneme.aletheia.alerts"] : [],
|
|
629
|
+
};
|
|
630
|
+
},
|
|
631
|
+
};
|
|
632
|
+
function generateFuzzCases(inputSchema) {
|
|
633
|
+
const out = [];
|
|
634
|
+
const props = inputSchema?.properties ?? {};
|
|
635
|
+
const stringFields = Object.entries(props)
|
|
636
|
+
.filter(([_, def]) => def?.type === "string")
|
|
637
|
+
.map(([name]) => name);
|
|
638
|
+
if (stringFields.length === 0) {
|
|
639
|
+
// No string fields — fuzz the top-level input.
|
|
640
|
+
stringFields.push("__top__");
|
|
641
|
+
}
|
|
642
|
+
const payloads = [
|
|
643
|
+
{ name: "shell-meta semicolon", payload: "x;rm -rf /", category: "command-injection" },
|
|
644
|
+
{ name: "shell-meta backtick", payload: "x`whoami`", category: "command-injection" },
|
|
645
|
+
{ name: "shell-meta dollar paren", payload: "$(id)", category: "command-injection" },
|
|
646
|
+
{ name: "ssrf metadata IP", payload: "http://169.254.169.254/latest/meta-data/", category: "ssrf" },
|
|
647
|
+
{ name: "ssrf file scheme", payload: "file:///etc/passwd", category: "ssrf" },
|
|
648
|
+
{ name: "ssrf gopher", payload: "gopher://internal:11211/", category: "ssrf" },
|
|
649
|
+
{ name: "path traversal", payload: "../../../etc/passwd", category: "path-traversal" },
|
|
650
|
+
{ name: "windows traversal", payload: "..\\..\\..\\windows\\system32\\config\\sam", category: "path-traversal" },
|
|
651
|
+
{ name: "oversized 1MB string", payload: "A".repeat(1_000_000), category: "oversized" },
|
|
652
|
+
{ name: "unicode RTL override", payload: "abcdef", category: "unicode" },
|
|
653
|
+
{ name: "null byte", payload: "xy", category: "unicode" },
|
|
654
|
+
{ name: "proto pollution", payload: '{"__proto__":{"polluted":true}}', category: "prototype-pollution" },
|
|
655
|
+
];
|
|
656
|
+
for (const f of stringFields) {
|
|
657
|
+
for (const p of payloads) {
|
|
658
|
+
out.push({ name: p.name, field: f, payload: p.payload, category: p.category });
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return out;
|
|
662
|
+
}
|
|
663
|
+
export const aletheiaFuzzTool = {
|
|
664
|
+
name: "mneme.aletheia.fuzz",
|
|
665
|
+
category: "meta",
|
|
666
|
+
description: "ALETHEIA Adversarial Self-Fuzz — generate ~12 OWASP-derived attack inputs " +
|
|
667
|
+
"for each string field of a target tool's input schema, then run each through " +
|
|
668
|
+
"mneme.aletheia.lint to see whether the tool would have ACCEPTED it. Returns " +
|
|
669
|
+
"per-case outcome (accepted-clean | accepted-dangerous | rejected-by-lint | " +
|
|
670
|
+
"threw). Each accepted-dangerous outcome proposes a karma delta of -2. " +
|
|
671
|
+
"First MCP server with built-in self-fuzzing. Use WHEN you want to audit " +
|
|
672
|
+
"a tool's robustness against the OWASP top patterns without wiring up a " +
|
|
673
|
+
"separate fuzzer.",
|
|
674
|
+
whenToUse: "You want to audit a Mneme tool's robustness against OWASP attack patterns without leaving the MCP surface.",
|
|
675
|
+
triggers: ["fuzz this tool", "test injection resistance", "OWASP attack surface"],
|
|
676
|
+
inputSchema: {
|
|
677
|
+
type: "object",
|
|
678
|
+
properties: {
|
|
679
|
+
tool: { type: "string", description: "Tool name to fuzz." },
|
|
680
|
+
maxCases: {
|
|
681
|
+
type: "number",
|
|
682
|
+
description: "Cap on number of fuzz cases (default 60). Each adds ~10ms.",
|
|
683
|
+
},
|
|
684
|
+
},
|
|
685
|
+
required: ["tool"],
|
|
686
|
+
},
|
|
687
|
+
outputSchema: {
|
|
688
|
+
type: "object",
|
|
689
|
+
properties: {
|
|
690
|
+
tool: { type: "string" },
|
|
691
|
+
total: { type: "number" },
|
|
692
|
+
acceptedClean: { type: "number" },
|
|
693
|
+
acceptedDangerous: { type: "number" },
|
|
694
|
+
rejectedByLint: { type: "number" },
|
|
695
|
+
threw: { type: "number" },
|
|
696
|
+
results: { type: "array", items: { type: "object" } },
|
|
697
|
+
proposedKarmaDelta: { type: "number" },
|
|
698
|
+
},
|
|
699
|
+
},
|
|
700
|
+
examples: [
|
|
701
|
+
{
|
|
702
|
+
userQuery: "Fuzz mneme.memory.ask for OWASP attack patterns",
|
|
703
|
+
args: { tool: "mneme.memory.ask", maxCases: 50 },
|
|
704
|
+
expectedOutput: "Returns per-case results + summary counts. acceptedDangerous count + proposedKarmaDelta tell you how robust the tool is.",
|
|
705
|
+
},
|
|
706
|
+
],
|
|
707
|
+
pitfalls: [
|
|
708
|
+
"We DO NOT actually invoke the target tool — we just lint its inputs. This is a STATIC fuzzer, not a runtime one. Pair with `mneme.security.audit` (planned) for runtime fuzzing in a sandbox.",
|
|
709
|
+
"Karma deltas are PROPOSED — not auto-applied. The agent / human decides whether to record them via mneme.aletheia.karma.record (planned).",
|
|
710
|
+
"OWASP coverage is 12 patterns × N string fields. A real attacker can craft fresh patterns; treat this as a baseline, not a pass certificate.",
|
|
711
|
+
],
|
|
712
|
+
composeWith: ["mneme.aletheia.lint", "mneme.aletheia.karma"],
|
|
713
|
+
handler: async (rt, args) => {
|
|
714
|
+
const tool = String(args["tool"] ?? "");
|
|
715
|
+
const maxCases = typeof args["maxCases"] === "number" ? Math.max(1, Math.min(500, args["maxCases"])) : 60;
|
|
716
|
+
if (!tool) {
|
|
717
|
+
return {
|
|
718
|
+
data: { error: "missing required argument: tool" },
|
|
719
|
+
wisdom: "Pass the tool name to fuzz.",
|
|
720
|
+
confidence: { level: "high" },
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
// Look up the target's inputSchema via the registry.
|
|
724
|
+
const { buildToolMap } = await import("./_registry.js");
|
|
725
|
+
const target = buildToolMap().get(tool);
|
|
726
|
+
if (!target) {
|
|
727
|
+
return {
|
|
728
|
+
data: { error: `tool not found: ${tool}` },
|
|
729
|
+
wisdom: `No registered tool '${tool}'. Try mneme.help with a free-text query.`,
|
|
730
|
+
confidence: { level: "high" },
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
const cases = generateFuzzCases(target.inputSchema).slice(0, maxCases);
|
|
734
|
+
const results = [];
|
|
735
|
+
let acceptedClean = 0;
|
|
736
|
+
let acceptedDangerous = 0;
|
|
737
|
+
let rejectedByLint = 0;
|
|
738
|
+
let threw = 0;
|
|
739
|
+
for (const c of cases) {
|
|
740
|
+
try {
|
|
741
|
+
const argBlob = c.field === "__top__" ? c.payload : { [c.field]: c.payload };
|
|
742
|
+
const findings = lintArgValue(argBlob, tool);
|
|
743
|
+
if (findings.length === 0) {
|
|
744
|
+
// No lint finding → tool would accept. Categorize by case kind.
|
|
745
|
+
if (c.category === "oversized" || c.category === "unicode" || c.category === "prototype-pollution") {
|
|
746
|
+
// These don't trigger lint by design — count as accepted-clean
|
|
747
|
+
// (the tool itself is responsible for these).
|
|
748
|
+
results.push({ case: c, outcome: "accepted-clean" });
|
|
749
|
+
acceptedClean += 1;
|
|
750
|
+
}
|
|
751
|
+
else {
|
|
752
|
+
// Should have been caught — accepted-dangerous.
|
|
753
|
+
results.push({ case: c, outcome: "accepted-dangerous", detail: "lint missed a known-bad pattern" });
|
|
754
|
+
acceptedDangerous += 1;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
else {
|
|
758
|
+
results.push({
|
|
759
|
+
case: c,
|
|
760
|
+
outcome: "rejected-by-lint",
|
|
761
|
+
detail: findings[0].kind,
|
|
762
|
+
});
|
|
763
|
+
rejectedByLint += 1;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
catch (err) {
|
|
767
|
+
results.push({ case: c, outcome: "threw", detail: err.message.slice(0, 200) });
|
|
768
|
+
threw += 1;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
const proposedKarmaDelta = acceptedDangerous * KARMA_DELTA["fuzz-hit"];
|
|
772
|
+
return {
|
|
773
|
+
data: {
|
|
774
|
+
tool,
|
|
775
|
+
total: results.length,
|
|
776
|
+
acceptedClean,
|
|
777
|
+
acceptedDangerous,
|
|
778
|
+
rejectedByLint,
|
|
779
|
+
threw,
|
|
780
|
+
results: results.slice(0, 30),
|
|
781
|
+
proposedKarmaDelta,
|
|
782
|
+
},
|
|
783
|
+
wisdom: acceptedDangerous > 0
|
|
784
|
+
? `FUZZ HIT — ${acceptedDangerous} dangerous input${acceptedDangerous === 1 ? "" : "s"} would have been accepted by ${tool}. Proposed karma delta ${proposedKarmaDelta}. Investigate.`
|
|
785
|
+
: `Clean — ${tool} rejected (${rejectedByLint}) or safely accepted (${acceptedClean}) all ${results.length} fuzz case${results.length === 1 ? "" : "s"}.`,
|
|
786
|
+
confidence: { level: "high" },
|
|
787
|
+
followUp: acceptedDangerous > 0 ? ["mneme.aletheia.karma"] : [],
|
|
788
|
+
};
|
|
789
|
+
},
|
|
790
|
+
};
|
|
791
|
+
/** Real (non-honeypot) ALETHEIA tools. */
|
|
792
|
+
export const aletheiaTools = [
|
|
793
|
+
securityLintTool,
|
|
794
|
+
immuneScanTool,
|
|
795
|
+
immuneTrainTool,
|
|
796
|
+
immuneAlertsTool,
|
|
797
|
+
aletheiaKarmaTool,
|
|
798
|
+
aletheiaFuzzTool,
|
|
799
|
+
];
|
|
800
|
+
//# sourceMappingURL=_aletheia.js.map
|