wolverine-ai 4.6.0 → 4.7.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/bin/wolverine.js +24 -1
- package/package.json +4 -4
- package/src/agent/agent-engine.js +14 -0
- package/src/core/runner.js +8 -4
- package/src/core/server-context.js +50 -2
- package/src/core/wolverine.js +35 -25
package/bin/wolverine.js
CHANGED
|
@@ -4,6 +4,16 @@ const path = require("path");
|
|
|
4
4
|
const dotenv = require("dotenv");
|
|
5
5
|
const chalk = require("chalk");
|
|
6
6
|
|
|
7
|
+
// Global error handlers — prevent parent process death from unhandled errors
|
|
8
|
+
process.on("uncaughtException", (err) => {
|
|
9
|
+
console.error(chalk.red(`\n ⚠️ Uncaught exception (wolverine survived): ${err.message}`));
|
|
10
|
+
console.error(chalk.gray(` ${err.stack?.split("\n")[1]?.trim() || ""}`));
|
|
11
|
+
});
|
|
12
|
+
process.on("unhandledRejection", (reason) => {
|
|
13
|
+
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
14
|
+
console.error(chalk.red(`\n ⚠️ Unhandled rejection (wolverine survived): ${msg}`));
|
|
15
|
+
});
|
|
16
|
+
|
|
7
17
|
// Load secrets
|
|
8
18
|
dotenv.config({ path: path.resolve(process.cwd(), ".env.local") });
|
|
9
19
|
dotenv.config({ path: path.resolve(process.cwd(), ".env") });
|
|
@@ -70,7 +80,20 @@ if (args.includes("--init")) {
|
|
|
70
80
|
console.log(chalk.gray(` Database: ${ctx.database.type || "none"}${ctx.database.tables.length > 0 ? ` (${ctx.database.tables.length} tables)` : ""}`));
|
|
71
81
|
console.log(chalk.gray(` Env vars: ${ctx.envVars.length}`));
|
|
72
82
|
console.log(chalk.gray(` Files: ${ctx.structure.length}`));
|
|
73
|
-
console.log(chalk.gray(` Saved to: .wolverine/server-context.json
|
|
83
|
+
console.log(chalk.gray(` Saved to: .wolverine/server-context.json`));
|
|
84
|
+
if (ctx.warnings && ctx.warnings.length > 0) {
|
|
85
|
+
console.log(chalk.yellow(`\n ⚠️ Security warnings (${ctx.warnings.length}):`));
|
|
86
|
+
const seen = new Set();
|
|
87
|
+
for (const w of ctx.warnings) {
|
|
88
|
+
const key = `${w.file}:${w.type}`;
|
|
89
|
+
if (seen.has(key)) continue;
|
|
90
|
+
seen.add(key);
|
|
91
|
+
console.log(chalk.yellow(` ${w.file}: ${w.label}`));
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
console.log(chalk.green(` No security warnings`));
|
|
95
|
+
}
|
|
96
|
+
console.log("");
|
|
74
97
|
process.exit(0);
|
|
75
98
|
}
|
|
76
99
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wolverine-ai",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.7.0",
|
|
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": {
|
|
@@ -58,14 +58,14 @@
|
|
|
58
58
|
"diff": "^7.0.0",
|
|
59
59
|
"dotenv": "^16.4.7",
|
|
60
60
|
"fastify": "^5.8.4",
|
|
61
|
-
"
|
|
62
|
-
"openai": "^4.73.0",
|
|
63
|
-
"pg": "^8.0.0"
|
|
61
|
+
"openai": "^4.73.0"
|
|
64
62
|
},
|
|
65
63
|
"optionalDependencies": {
|
|
66
64
|
"@privy-io/server-auth": "1.14.0",
|
|
67
65
|
"better-sqlite3": "^11.0.0",
|
|
68
66
|
"ethers": "^6.0.0",
|
|
67
|
+
"ioredis": "^5.0.0",
|
|
68
|
+
"pg": "^8.0.0",
|
|
69
69
|
"stripe": "^18.0.0"
|
|
70
70
|
},
|
|
71
71
|
"engines": {
|
|
@@ -544,6 +544,16 @@ function _detectSandboxEscape(cmd, cwd) {
|
|
|
544
544
|
return "curl uploading local file (potential data exfiltration)";
|
|
545
545
|
}
|
|
546
546
|
|
|
547
|
+
// 6. Environment variable dumping to file/network (staging attack via /tmp)
|
|
548
|
+
if (/\b(env|printenv|set)\s*>/.test(c) || /\b(env|printenv)\s*\|/.test(c)) {
|
|
549
|
+
return "Environment variable dump detected (exfiltration risk)";
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// 7. Reading secrets and piping to network
|
|
553
|
+
if (/cat\s+.*\.(env|key|pem|crt)\s*\|/.test(c) || /cat\s+.*secret.*\|/.test(c)) {
|
|
554
|
+
return "Reading sensitive file and piping to output";
|
|
555
|
+
}
|
|
556
|
+
|
|
547
557
|
return null; // safe
|
|
548
558
|
}
|
|
549
559
|
|
|
@@ -1075,6 +1085,10 @@ class AgentEngine {
|
|
|
1075
1085
|
/^192\.168\./,
|
|
1076
1086
|
/^fd[0-9a-f]{2}:/i,
|
|
1077
1087
|
/^::1$/,
|
|
1088
|
+
/^0\.0\.0\.0$/,
|
|
1089
|
+
/^0\./,
|
|
1090
|
+
/^\[::1\]$/,
|
|
1091
|
+
/^metadata\.google\.internal$/i,
|
|
1078
1092
|
];
|
|
1079
1093
|
if (privatePatterns.some(p => p.test(hostname))) {
|
|
1080
1094
|
resolve({ content: `BLOCKED: Cannot fetch private/internal address "${hostname}"` });
|
package/src/core/runner.js
CHANGED
|
@@ -213,11 +213,13 @@ class WolverineRunner {
|
|
|
213
213
|
|
|
214
214
|
// Scan server context (routes, DB, config, deps) for agent knowledge
|
|
215
215
|
try {
|
|
216
|
-
const { scan
|
|
216
|
+
const { scan } = require("./server-context");
|
|
217
217
|
const ctx = scan(this.cwd);
|
|
218
218
|
if (ctx) {
|
|
219
219
|
const routes = ctx.routes.reduce((s, r) => s + r.endpoints.length, 0);
|
|
220
|
+
const warns = (ctx.warnings || []).length;
|
|
220
221
|
console.log(chalk.gray(` 🗺️ Server context: ${routes} routes, ${ctx.structure.length} files, ${ctx.envVars.length} env vars`));
|
|
222
|
+
if (warns > 0) console.log(chalk.yellow(` ⚠️ ${warns} security warning(s) — run wolverine --init for details`));
|
|
221
223
|
}
|
|
222
224
|
} catch {}
|
|
223
225
|
|
|
@@ -708,14 +710,16 @@ class WolverineRunner {
|
|
|
708
710
|
}
|
|
709
711
|
this._healInProgress = true;
|
|
710
712
|
|
|
711
|
-
//
|
|
713
|
+
// Safety timeout — must be strictly greater than heal()'s 5-min timeout to avoid concurrent heals
|
|
714
|
+
const HEAL_TIMEOUT_MS = parseInt(process.env.WOLVERINE_HEAL_TIMEOUT_MS, 10) || 300000;
|
|
715
|
+
const safetyMs = HEAL_TIMEOUT_MS + 30000; // heal timeout + 30s grace
|
|
712
716
|
const healTimeout = setTimeout(() => {
|
|
713
717
|
if (this._healInProgress) {
|
|
714
|
-
console.log(chalk.red(` ⚠️ _healFromError safety timeout (
|
|
718
|
+
console.log(chalk.red(` ⚠️ _healFromError safety timeout (${Math.round(safetyMs / 60000)}min) — releasing heal lock`));
|
|
715
719
|
this._healInProgress = false;
|
|
716
720
|
this._healStatus = null;
|
|
717
721
|
}
|
|
718
|
-
},
|
|
722
|
+
}, safetyMs);
|
|
719
723
|
|
|
720
724
|
console.log(chalk.yellow(`\n🐺 Wolverine healing caught error on ${routePath}...`));
|
|
721
725
|
this._healStatus = { active: true, route: routePath, error: errorDetails?.message?.slice(0, 200), phase: "diagnosing", startedAt: Date.now() };
|
|
@@ -168,12 +168,60 @@ function scan(cwd) {
|
|
|
168
168
|
scanForEnv(serverDir);
|
|
169
169
|
context.envVars = [...envVars].sort();
|
|
170
170
|
|
|
171
|
+
// 8. Security scan — detect dangerous patterns in server code
|
|
172
|
+
context.warnings = [];
|
|
173
|
+
const scanForDangers = (dir) => {
|
|
174
|
+
if (!fs.existsSync(dir)) return;
|
|
175
|
+
for (const file of _listFiles(dir, ".js")) {
|
|
176
|
+
try {
|
|
177
|
+
const code = fs.readFileSync(file, "utf-8");
|
|
178
|
+
const rel = path.relative(cwd, file);
|
|
179
|
+
// Hardcoded secrets
|
|
180
|
+
if (/['"][a-zA-Z0-9_-]{20,}['"]/.test(code)) {
|
|
181
|
+
const secretPatterns = [
|
|
182
|
+
{ regex: /sk-[a-zA-Z0-9]{20,}/g, label: "OpenAI API key" },
|
|
183
|
+
{ regex: /sk-ant-[a-zA-Z0-9_-]{20,}/g, label: "Anthropic API key" },
|
|
184
|
+
{ regex: /ghp_[a-zA-Z0-9]{36,}/g, label: "GitHub token" },
|
|
185
|
+
{ regex: /AKIA[0-9A-Z]{16}/g, label: "AWS access key" },
|
|
186
|
+
{ regex: /['"](?:password|secret|token|api_key|apikey|auth)\s*['"]?\s*[:=]\s*['"][^'"]{8,}['"]/gi, label: "Hardcoded credential" },
|
|
187
|
+
{ regex: /mongodb\+srv:\/\/[^\s'"]+/g, label: "MongoDB connection string" },
|
|
188
|
+
{ regex: /postgres:\/\/[^\s'"]+/g, label: "PostgreSQL connection string" },
|
|
189
|
+
{ regex: /redis:\/\/[^\s'"]+/g, label: "Redis connection string" },
|
|
190
|
+
];
|
|
191
|
+
for (const p of secretPatterns) {
|
|
192
|
+
if (p.regex.test(code)) {
|
|
193
|
+
context.warnings.push({ file: rel, type: "hardcoded_secret", label: p.label });
|
|
194
|
+
p.regex.lastIndex = 0;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// eval / Function constructor
|
|
199
|
+
if (/\beval\s*\(/.test(code)) context.warnings.push({ file: rel, type: "eval_usage", label: "eval() call" });
|
|
200
|
+
if (/new\s+Function\s*\(/.test(code)) context.warnings.push({ file: rel, type: "function_constructor", label: "new Function() call" });
|
|
201
|
+
// SQL injection risk (string concat in queries)
|
|
202
|
+
if (/\.(query|exec|prepare)\s*\(\s*['"`].*\$\{/.test(code) || /\.(query|exec)\s*\(\s*.*\+\s*(?:req|args|params|body)/i.test(code)) {
|
|
203
|
+
context.warnings.push({ file: rel, type: "sql_injection_risk", label: "String concatenation in SQL query" });
|
|
204
|
+
}
|
|
205
|
+
// Unvalidated redirect
|
|
206
|
+
if (/res\.redirect\s*\(\s*(?:req\.|args\.|params\.)/.test(code)) {
|
|
207
|
+
context.warnings.push({ file: rel, type: "open_redirect", label: "Unvalidated redirect from user input" });
|
|
208
|
+
}
|
|
209
|
+
} catch {}
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
scanForDangers(serverDir);
|
|
213
|
+
|
|
214
|
+
// Sanitize: strip any actual secret values that might have leaked into context
|
|
215
|
+
const contextStr = JSON.stringify(context);
|
|
216
|
+
const { redact } = require("../security/secret-redactor");
|
|
217
|
+
const sanitized = JSON.parse(redact(contextStr));
|
|
218
|
+
|
|
171
219
|
// Save
|
|
172
220
|
const outPath = path.join(cwd, CONTEXT_PATH);
|
|
173
221
|
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
174
|
-
fs.writeFileSync(outPath, JSON.stringify(
|
|
222
|
+
fs.writeFileSync(outPath, JSON.stringify(sanitized, null, 2), "utf-8");
|
|
175
223
|
|
|
176
|
-
return
|
|
224
|
+
return sanitized;
|
|
177
225
|
}
|
|
178
226
|
|
|
179
227
|
/**
|
package/src/core/wolverine.js
CHANGED
|
@@ -249,27 +249,35 @@ async function _healImpl({ stderr, cwd, sandbox, notifier, rateLimiter, backupMa
|
|
|
249
249
|
} catch {}
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
-
// 7. Classify error complexity —
|
|
252
|
+
// 7. Classify error complexity — regex first (fast, free), AI only when uncertain
|
|
253
253
|
let errorComplexity = "moderate";
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
254
|
+
const isObviouslySimple = /TypeError|ReferenceError|SyntaxError|Cannot find module|Cannot read prop/.test(parsed.errorMessage);
|
|
255
|
+
const isObviouslyModerate = /ECONNREFUSED|timeout|ENOENT|EACCES|EADDRINUSE|ENOSPC|EMFILE/.test(parsed.errorMessage);
|
|
256
|
+
if (isObviouslySimple) {
|
|
257
|
+
errorComplexity = "simple";
|
|
258
|
+
console.log(chalk.gray(` 🏷️ Classifier (fast): simple`));
|
|
259
|
+
} else if (isObviouslyModerate) {
|
|
260
|
+
errorComplexity = "moderate";
|
|
261
|
+
console.log(chalk.gray(` 🏷️ Classifier (fast): moderate`));
|
|
262
|
+
} else {
|
|
263
|
+
// Uncertain — use AI classifier
|
|
264
|
+
try {
|
|
265
|
+
const classifyResult = await aiCall({
|
|
266
|
+
model: getModel("classifier"),
|
|
267
|
+
systemPrompt: "You classify Node.js errors. Respond with ONLY one word: SIMPLE, MODERATE, or COMPLEX.",
|
|
268
|
+
userPrompt: `Classify this error:\n${parsed.errorMessage}\n\nFile: ${parsed.filePath || "unknown"}\nType: ${parsed.errorType || "unknown"}`,
|
|
269
|
+
maxTokens: 10,
|
|
270
|
+
category: "classifier",
|
|
271
|
+
});
|
|
272
|
+
const word = (classifyResult.content || "").trim().toUpperCase();
|
|
273
|
+
if (word.includes("SIMPLE")) errorComplexity = "simple";
|
|
274
|
+
else if (word.includes("COMPLEX")) errorComplexity = "complex";
|
|
275
|
+
else errorComplexity = "moderate";
|
|
276
|
+
console.log(chalk.gray(` 🏷️ Classifier (AI): ${errorComplexity}`));
|
|
277
|
+
} catch {
|
|
278
|
+
errorComplexity = "complex"; // unknown = treat as complex to be safe
|
|
279
|
+
console.log(chalk.gray(` 🏷️ Classifier (fallback): complex`));
|
|
280
|
+
}
|
|
273
281
|
}
|
|
274
282
|
|
|
275
283
|
// 7b. Research — look up past fixes AND search for solutions
|
|
@@ -687,13 +695,15 @@ async function tryOperationalFix(parsed, cwd, logger, sandbox) {
|
|
|
687
695
|
} catch {}
|
|
688
696
|
}
|
|
689
697
|
|
|
690
|
-
// Pattern 6: EMFILE — too many open files
|
|
698
|
+
// Pattern 6: EMFILE — too many open files
|
|
691
699
|
if (/EMFILE|ENFILE/.test(msg)) {
|
|
700
|
+
// Close stale file handles by clearing node_modules/.cache and restarting
|
|
692
701
|
try {
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
702
|
+
const cachePath = path.join(cwd, "node_modules", ".cache");
|
|
703
|
+
if (fs.existsSync(cachePath)) {
|
|
704
|
+
execSync(`rm -rf "${cachePath}"`, { timeout: 10000 });
|
|
705
|
+
console.log(chalk.blue(" 📂 Cleared node_modules/.cache to reduce open FDs"));
|
|
706
|
+
return { fixed: true, action: "EMFILE — cleared build cache to reduce open file descriptors. Consider increasing ulimit -n in your system profile." };
|
|
697
707
|
}
|
|
698
708
|
} catch {}
|
|
699
709
|
}
|