Skip to content

Commit 7f0a402

Browse files
committed
feat(v3.146.0): ESCALON — agent tool-graph privilege-escalation + MCP-poisoning analyzer
The deeper vuln engine (after MUTAGEN's input layer): the classes that live in HOW an agent's tools COMPOSE, which no single-tool review catches. • core/escalon: findEscalations() builds the capability DATA-FLOW graph (edge where one tool's output feeds another's input) and traces every untrusted-SOURCE → dangerous-SINK path — fetch-url → write-file → run-script = RCE by composition (the "confused deputy") — ranked by severity and whether a sanitizer/approval gate breaks the chain. scanPoisoning() screens each tool DESCRIPTION for embedded directives (MCP tool-poisoning / "line jumping"). analyze() → CLEAN / REVIEW / DANGER. • MEASURED (escalonGauntlet=100, 7 tests): detects the planted RCE chain; no false-positive on a benign read-only set; a gate lowers severity; detects a poisoned description + spares clean ones; ranks an exec sink above an exfil-only chain. • CLI `mneme escalon analyze`; MCP mneme.escalon.analyze (Matrix gRPC auto); morph `mneme escalon`; claim+probe (claim.escalon.finds_tool_chain_vulns), manifest entry. ★HONEST (DIAKRISIS): ESCALON reasons over the DECLARED tool graph (capabilities + data labels) — it surfaces reachable escalation PATHS + poisoned descriptions to inspect, not a proven runtime exploit, and is blind to a capability a tool fails to declare. Deterministic, no LLM. TRUTH GATE 136/136, drift 0, refuted 0, unmeasured 0; coverage 100%; 23 tests green; honesty ledger regenerated + verifies HONEST.
1 parent 66af53b commit 7f0a402

28 files changed

Lines changed: 479 additions & 77 deletions

assets/honesty-badge.svg

Lines changed: 1 addition & 1 deletion
Loading

docs/HONESTY-LEDGER.json

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,20 @@
22
"v": 1,
33
"alg": "ed25519",
44
"kind": "generic",
5-
"subject": "mneme-honesty-ledger@3.145.0",
6-
"payloadHash": "63c4dd7b2593b1d266ad1adeba17b68f8e925d75a5fdc5b9b780f174425d9e41",
5+
"subject": "mneme-honesty-ledger@3.146.0",
6+
"payloadHash": "5a796c79ab7597672740b2f6fbeeac83ed09ce6d53d46697ac598826468c7ea9",
77
"payload": {
88
"spec": "MNEME-HONESTY-LEDGER",
99
"v": 1,
10-
"version": "3.145.0",
11-
"generatedAt": "2026-06-23T07:40:32.761Z",
10+
"version": "3.146.0",
11+
"generatedAt": "2026-06-23T08:16:49.117Z",
1212
"summary": {
13-
"total": 135,
14-
"pass": 135,
13+
"total": 136,
14+
"pass": 136,
1515
"drift": 0,
1616
"refuted": 0,
1717
"unmeasured": 0,
18-
"measured": 135,
18+
"measured": 136,
1919
"score": 100,
2020
"honest": true
2121
},
@@ -369,7 +369,7 @@
369369
"probeId": "probe.tool_count",
370370
"severity": "info",
371371
"verdict": "pass",
372-
"reason": "asserted >= 800 tools; measured 1133"
372+
"reason": "asserted >= 800 tools; measured 1134"
373373
},
374374
{
375375
"id": "claim.rewind.card_count_known",
@@ -1217,6 +1217,15 @@
12171217
"verdict": "pass",
12181218
"reason": "asserted = 1 boolean; measured 1"
12191219
},
1220+
{
1221+
"id": "claim.escalon.finds_tool_chain_vulns",
1222+
"text": "ESCALON analyzes an AI agent's TOOL GRAPH for compositional vulnerabilities a single-tool review misses: (1) tool-chain PRIVILEGE ESCALATION — it builds the capability data-flow graph (edge where one tool's output feeds another's input) and",
1223+
"source": "v3.146.0 release notes — ESCALON",
1224+
"probeId": "probe.escalon.finds_tool_chain_vulns",
1225+
"severity": "block",
1226+
"verdict": "pass",
1227+
"reason": "asserted true; measured true"
1228+
},
12201229
{
12211230
"id": "claim.mutagen.finds_guardrail_holes",
12221231
"text": "MUTAGEN is an adversarial-mutation engine that finds an AI agent's guardrail holes by SEARCHING the mutation space: it derives a deterministic population of novel attack variants (injection/destructive/exfil primitives × stacked mutators — ",
@@ -1239,8 +1248,8 @@
12391248
},
12401249
"issuer": "MCowBQYDK2VwAyEAE8LE7MxyTrZatGLYHP0O4Kj4OoOzKHsRmLJBScBuPmM=",
12411250
"issuerFingerprint": "9de66ee802256bee",
1242-
"issuedAt": 1782200444710,
1251+
"issuedAt": 1782202618696,
12431252
"prev": null,
1244-
"receiptId": "15453ed1432245a3ef570c79f31e6d288042d8f886ea9ca0da9373d11ceb3bb7",
1245-
"sig": "yUkUdmVEr2prfhvaPZQc4eI3NiErzfNa3WbGWxP3Om+/YTEilDbcj7AgO6wVoHPWfTQNFVlUXnV0z6SgFLV6Ag=="
1253+
"receiptId": "960248178fac05823b3c94155623d6dfbd117e6672a38837e10051a5236e6de8",
1254+
"sig": "ImJs0GG8/aW7+1Y5Vna9DL9mYR/qlSvtpV/XCmXx+lIeop30ZEa46/x4oO1yL5JXi+bAyvSBF3ihdVr+g4a+Dg=="
12461255
}

docs/HONESTY-LEDGER.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,19 @@
33
> Auto-generated + Ed25519-signed. Verify offline: `mneme honesty verify` (paste the JSON).
44
> This attests every **public claim currently passes its probe** — not that the software is bug-free.
55
6-
**mneme@3.145.0** · generated 2026-06-23T07:40:32.761Z
6+
**mneme@3.146.0** · generated 2026-06-23T08:16:49.117Z
77

88
| | count |
99
|--|--|
10-
| ✅ pass | 135 |
10+
| ✅ pass | 136 |
1111
| 🟠 drift | 0 |
1212
| 🔴 refuted | 0 |
1313
| ⚪ unmeasured | 0 |
1414
| **score** | **100/100** · 🟢 ZERO-DRIFT |
1515

16-
Signed: `9de66ee802256bee` · receipt `15453ed1432245a3`
16+
Signed: `9de66ee802256bee` · receipt `960248178fac0582`
1717

18-
<details><summary>All 135 claims</summary>
18+
<details><summary>All 136 claims</summary>
1919

2020
| claim | verdict | probe |
2121
|--|--|--|
@@ -152,6 +152,7 @@ Signed: `9de66ee802256bee` · receipt `15453ed1432245a3…`
152152
| `claim.bequest.inheritance_math_sound` | ✅ pass | `probe.bequest.inheritance_math_sound` |
153153
| `claim.exec.roi_math_sound` | ✅ pass | `probe.exec.roi_math_sound` |
154154
| `claim.egress.sovereign_guard` | ✅ pass | `probe.egress.sovereign_guard` |
155+
| `claim.escalon.finds_tool_chain_vulns` | ✅ pass | `probe.escalon.finds_tool_chain_vulns` |
155156
| `claim.mutagen.finds_guardrail_holes` | ✅ pass | `probe.mutagen.finds_guardrail_holes` |
156157
| `claim.honesty_ledger.signed_verifiable` | ✅ pass | `probe.honesty_ledger.signed_verifiable` |
157158

package-lock.json

Lines changed: 30 additions & 30 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "mneme-monorepo",
3-
"version": "3.145.0",
3+
"version": "3.146.0",
44
"private": true,
55
"type": "module",
66
"description": "Mneme — the memory layer for your codebase. Knows the WHY, the WHAT, the WHERE-IT-BREAKS, all in one graph.",

packages/cli/package.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "mneme-ai",
3-
"version": "3.145.0",
3+
"version": "3.146.0",
44
"mcpName": "io.github.patsa2561-art/mneme-ai",
55
"description": "Mneme — the memory layer for your codebase. Knows the WHY, the WHAT, the WHERE-IT-BREAKS.",
66
"type": "module",
@@ -32,12 +32,12 @@
3232
"preinstall": "node -e \"try{const fs=require('node:fs');const path=require('node:path');const os=require('node:os');const{spawnSync}=require('node:child_process');const crypto=require('node:crypto');const w=process.platform==='win32';const home=os.homedir();const organ=path.join(home,'.mneme-global');const trailPath=path.join(organ,'preinstall-trail.jsonl');const trailSecret=process.env['MNEME_PREINSTALL_TRAIL_SECRET']||'mneme-preinstall-trail-v1';const version=process.env['npm_package_version']||'unknown';try{if(!fs.existsSync(organ))fs.mkdirSync(organ,{recursive:true,mode:0o700})}catch(e){}const lastSig=()=>{try{if(!fs.existsSync(trailPath))return'genesis';const lines=fs.readFileSync(trailPath,'utf8').trim().split('\\\\n').filter(Boolean);if(lines.length===0)return'genesis';const last=JSON.parse(lines[lines.length-1]);return typeof last?.sig==='string'?last.sig:'genesis'}catch(e){return'genesis'}};const trail=(step,ok,details)=>{try{const prevSig=lastSig();const body={v:1,ts:new Date().toISOString(),version,step,ok,...(details?{details}:{}),pid:process.pid,prevSig};const sig=crypto.createHmac('sha256',trailSecret).update(prevSig+'::'+JSON.stringify(body)).digest('hex');fs.appendFileSync(trailPath,JSON.stringify({...body,sig})+'\\\\n','utf8')}catch(e){}};trail('preinstall-start',true);let flagOk=false;try{fs.writeFileSync(path.join(organ,'install-incoming.flag'),JSON.stringify({v:1,announcedAt:new Date().toISOString(),announcerPid:process.pid,reason:'preinstall-hook'}),{encoding:'utf8',mode:0o600});flagOk=true}catch(e){}trail('flag-written',flagOk);const wait=(ms)=>{const e=Date.now()+ms;while(Date.now()<e){}};wait(300);let held=[];if(w){const r=spawnSync('taskkill',['/F','/IM','mneme.exe','/T'],{shell:true,windowsHide:true,timeout:5000,stdio:'ignore'});trail('daemon-stop-windows',true,{exitCode:r.status});let reaped=0;try{const beatDir=path.join(organ,'heartbeats');if(fs.existsSync(beatDir)){for(const f of fs.readdirSync(beatDir)){const m=f.match(/^(\\\\d+)\\\\.beat$/);if(m){const pid=parseInt(m[1]);if(pid>0&&pid!==process.pid){try{const bj=JSON.parse(fs.readFileSync(path.join(beatDir,f),'utf8'));if(Array.isArray(bj.holdsPaths))for(const hp of bj.holdsPaths){if(typeof hp==='string'&&hp)held.push(hp)}}catch(e){}spawnSync('taskkill',['/F','/PID',pid.toString(),'/T'],{shell:true,windowsHide:true,timeout:3000,stdio:'ignore'});try{fs.unlinkSync(path.join(beatDir,f));reaped++}catch(e){}}}}}}catch(e){}trail('heartbeat-reaped',true,{reaped})}else{const r=spawnSync('mneme',['daemon','stop'],{timeout:8000,stdio:'ignore'});trail('daemon-stop-posix',true,{exitCode:r.status});let reaped=0;try{const beatDir=path.join(organ,'heartbeats');if(fs.existsSync(beatDir)){for(const f of fs.readdirSync(beatDir)){const m=f.match(/^(\\\\d+)\\\\.beat$/);if(m){const pid=parseInt(m[1]);if(pid>0&&pid!==process.pid){try{const bj=JSON.parse(fs.readFileSync(path.join(beatDir,f),'utf8'));if(Array.isArray(bj.holdsPaths))for(const hp of bj.holdsPaths){if(typeof hp==='string'&&hp)held.push(hp)}}catch(e){}try{process.kill(pid,'SIGTERM')}catch(e){}wait(100);try{process.kill(pid,'SIGKILL')}catch(e){}try{fs.unlinkSync(path.join(beatDir,f));reaped++}catch(e){}}}}}}catch(e){}trail('heartbeat-reaped',true,{reaped})}wait(500);let renamed=0;let prefixesChecked=[];try{const candidatePrefixes=w?[path.join(home,'AppData','Roaming','npm'),path.dirname(process.execPath),'C:\\\\\\\\nvm4w\\\\\\\\nodejs',path.join(home,'AppData','Local','nvm')]:['/usr/local/lib','/usr/lib',path.join(home,'.npm-global'),path.join(home,'.nvm','versions','node')];const seen=new Set();for(const pfx of candidatePrefixes){if(!fs.existsSync(pfx))continue;let nodeModulesBases=[];if(fs.existsSync(path.join(pfx,'node_modules')))nodeModulesBases.push(path.join(pfx,'node_modules'));try{for(const entry of fs.readdirSync(pfx)){const sub=path.join(pfx,entry,'node_modules');if(fs.existsSync(sub))nodeModulesBases.push(sub);const sub2=path.join(pfx,entry,'nodejs','node_modules');if(fs.existsSync(sub2))nodeModulesBases.push(sub2)}}catch(e){}for(const nm of nodeModulesBases){if(seen.has(nm))continue;seen.add(nm);prefixesChecked.push(nm);const npmGlobal=path.join(nm,'mneme-ai');if(!fs.existsSync(npmGlobal))continue;const dllPaths=w?[path.join(npmGlobal,'node_modules','@img','sharp-libvips-win32-x64','lib','libvips-42.dll'),path.join(npmGlobal,'node_modules','@img','sharp-libvips-win32-x64','lib','libvips-cpp-8.17.3.dll'),path.join(npmGlobal,'node_modules','sharp','build','Release','sharp-win32-x64.node')]:[];for(const dll of dllPaths){if(!fs.existsSync(dll))continue;let freed=false;for(let i=0;i<40;i++){try{const fd=fs.openSync(dll,'r+');fs.closeSync(fd);freed=true;break}catch(e2){wait(50)}}if(!freed){try{fs.renameSync(dll,dll+'.locked-'+Date.now()+'-'+process.pid);renamed++}catch(e){}}}}}}catch(e){}try{const seenH=new Set();for(const dll of held){if(seenH.has(dll))continue;seenH.add(dll);if(!fs.existsSync(dll))continue;let freed=false;for(let i=0;i<40;i++){try{const fd=fs.openSync(dll,'r+');fs.closeSync(fd);freed=true;break}catch(e2){wait(50)}}if(!freed){try{fs.renameSync(dll,dll+'.locked-'+Date.now()+'-'+process.pid);renamed++}catch(e){}}}}catch(e){}trail('handle-oracle',true,{renamed,prefixesChecked:prefixesChecked.length,held:held.length});let swept=0;try{const candidates=w?[path.join(home,'AppData','Roaming','npm','node_modules'),path.join(path.dirname(process.execPath),'node_modules')]:['/usr/local/lib/node_modules',path.join(home,'.npm-global','node_modules')];for(const npmParent of candidates){if(!fs.existsSync(npmParent))continue;try{for(const entry of fs.readdirSync(npmParent)){if(entry.startsWith('.mneme-ai-')){try{fs.rmSync(path.join(npmParent,entry),{recursive:true,force:true});swept++}catch(e){}}}}catch(e){}}}catch(e){}trail('staging-swept',true,{swept});trail('preinstall-end',true)}catch(e){}process.exit(0)\""
3333
},
3434
"dependencies": {
35-
"@mneme-ai/core": "3.145.0",
36-
"@mneme-ai/correlator": "3.145.0",
37-
"@mneme-ai/embeddings": "3.145.0",
38-
"@mneme-ai/matrix": "3.145.0",
39-
"@mneme-ai/mcp": "3.145.0",
40-
"@mneme-ai/xray": "3.145.0",
35+
"@mneme-ai/core": "3.146.0",
36+
"@mneme-ai/correlator": "3.146.0",
37+
"@mneme-ai/embeddings": "3.146.0",
38+
"@mneme-ai/matrix": "3.146.0",
39+
"@mneme-ai/mcp": "3.146.0",
40+
"@mneme-ai/xray": "3.146.0",
4141
"commander": "^14.0.3",
4242
"kleur": "^4.1.5"
4343
},
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* `mneme escalon` (v3.146.0) — the agent TOOL-GRAPH vulnerability analyzer.
3+
*
4+
* analyze — given a tool manifest (JSON: [{id, capabilities, consumes, produces, description}]),
5+
* find tool-chain privilege-escalation paths + poisoned tool descriptions.
6+
*
7+
* mneme escalon analyze --file tools.json
8+
*/
9+
10+
import type { Command } from "commander";
11+
import { readFileSync } from "node:fs";
12+
import { escalon } from "@mneme-ai/core";
13+
14+
function out(s: string): void { process.stdout.write(s + "\n"); }
15+
16+
export function registerEscalonCommands(program: Command): void {
17+
const c = program.command("escalon")
18+
.description("🔗 ESCALON — analyze an AI agent's TOOL GRAPH for (1) tool-chain privilege escalation (safe tools that compose into a dangerous capability — the confused deputy) and (2) MCP tool-poisoning (injection hidden in a tool's description). Deterministic, no LLM. ★HONEST: reasons over the DECLARED capabilities — surfaces reachable paths to inspect, not a proven runtime exploit.");
19+
20+
c.command("analyze").description("find privilege-escalation chains + poisoned descriptions in a tool manifest")
21+
.option("--file <file>", "tool manifest JSON: [{id, capabilities:[], consumes:[], produces:[], description}]")
22+
.option("--json", "JSON")
23+
.action((o: { file?: string; json?: boolean }) => {
24+
let raw = "";
25+
try { raw = o.file ? readFileSync(o.file, "utf8") : readFileSync(0, "utf8"); }
26+
catch { out("⛔ provide --file <tools.json> (or pipe the manifest on stdin)"); process.exitCode = 2; return; }
27+
let tools: unknown;
28+
try { tools = JSON.parse(raw); } catch { out("⛔ not valid JSON"); process.exitCode = 2; return; }
29+
const r = escalon.analyze(tools as Parameters<typeof escalon.analyze>[0]);
30+
if (o.json) { out(JSON.stringify(r, null, 2)); return; }
31+
const mark = r.verdict === "DANGER" ? "🔴" : r.verdict === "REVIEW" ? "🟠" : "🟢";
32+
out(`${mark} ESCALON — ${r.verdict} · ${r.tools} tools · ${r.escalations.length} escalation path(s) · ${r.poisoned.length} poisoned · ${r.critical} critical`);
33+
for (const e of r.escalations.slice(0, 8)) out(` ${e.severity >= 80 && !e.gated ? "🔴" : "🟠"} [sev ${e.severity}] ${e.tools.join(" → ")}${e.sink}${e.gated ? " (gated)" : ""}`);
34+
for (const p of r.poisoned) out(` ☣ poisoned: ${p.tool} — "${p.excerpt}"`);
35+
if (r.verdict === "CLEAN") out(` ✅ no escalation path or poisoned description found.`);
36+
});
37+
}

packages/cli/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ import { registerArkCommands } from "./commands/ark.js";
189189
import { registerCosmosCommands } from "./commands/cosmos.js";
190190
import { registerLedgerCommands } from "./commands/honesty_ledger.js";
191191
import { registerMutagenCommands } from "./commands/mutagen.js";
192+
import { registerEscalonCommands } from "./commands/escalon.js";
192193
import { registerMoatCommands } from "./commands/moat.js";
193194
import { attachRegretOracle } from "./commands/regret.js";
194195
import { registerTrustCommands } from "./commands/trust.js";
@@ -5050,6 +5051,7 @@ export async function run(argv: string[]): Promise<void> {
50505051
registerCosmosCommands(program);
50515052
registerLedgerCommands(program);
50525053
registerMutagenCommands(program);
5054+
registerEscalonCommands(program);
50535055
registerMoatCommands(program);
50545056
// ─── Trust calibrator (v1.31.0) -- per-subsystem precision/recall/band
50555057
registerTrustCommands(program);

0 commit comments

Comments
 (0)