wolverine-ai 1.1.0 → 1.2.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 +13 -8
- package/package.json +1 -1
- package/src/agent/research-agent.js +4 -4
- package/src/brain/brain.js +4 -0
- package/src/core/runner.js +2 -2
- package/src/core/wolverine.js +10 -10
- package/src/dashboard/server.js +3 -2
- package/src/index.js +1 -1
- package/src/logger/event-logger.js +6 -9
- package/src/logger/repair-history.js +3 -2
- package/src/platform/telemetry.js +7 -5
- package/src/security/secret-redactor.js +47 -1
package/README.md
CHANGED
|
@@ -10,24 +10,29 @@ Built on patterns from [claw-code](https://github.com/instructkr/claw-code) —
|
|
|
10
10
|
|
|
11
11
|
## Quick Start
|
|
12
12
|
|
|
13
|
+
### Install from npm
|
|
14
|
+
|
|
13
15
|
```bash
|
|
14
|
-
# Install from npm
|
|
15
16
|
npm i wolverine-ai
|
|
17
|
+
cp node_modules/wolverine-ai/.env.example .env.local
|
|
18
|
+
# Edit .env.local — add your OPENAI_API_KEY
|
|
19
|
+
npx wolverine server/index.js
|
|
20
|
+
```
|
|
16
21
|
|
|
17
|
-
|
|
22
|
+
### Or clone from GitHub
|
|
23
|
+
|
|
24
|
+
```bash
|
|
18
25
|
git clone https://github.com/bobbyswhip/Wolverine.git
|
|
19
26
|
cd Wolverine
|
|
20
27
|
npm install
|
|
21
|
-
|
|
22
|
-
# Configure
|
|
23
28
|
cp .env.example .env.local
|
|
24
|
-
# Edit .env.local — add your OPENAI_API_KEY
|
|
25
|
-
|
|
26
|
-
# Run
|
|
29
|
+
# Edit .env.local — add your OPENAI_API_KEY
|
|
27
30
|
npm start
|
|
28
|
-
# or: npx wolverine server/index.js
|
|
29
31
|
```
|
|
30
32
|
|
|
33
|
+
[](https://www.npmjs.com/package/wolverine-ai)
|
|
34
|
+
[](https://github.com/bobbyswhip/Wolverine)
|
|
35
|
+
|
|
31
36
|
Dashboard opens at `http://localhost:PORT+1`. Server runs on `PORT`.
|
|
32
37
|
|
|
33
38
|
### Try a Demo
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wolverine-ai",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.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": {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const chalk = require("chalk");
|
|
2
2
|
const { aiCall } = require("../core/ai-client");
|
|
3
3
|
const { getModel } = require("../core/models");
|
|
4
|
+
const { redact } = require("../security/secret-redactor");
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Research Agent — deep research + learning from experience.
|
|
@@ -18,7 +19,6 @@ class ResearchAgent {
|
|
|
18
19
|
constructor(options = {}) {
|
|
19
20
|
this.brain = options.brain;
|
|
20
21
|
this.logger = options.logger;
|
|
21
|
-
this.redactor = options.redactor;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/**
|
|
@@ -50,8 +50,8 @@ class ResearchAgent {
|
|
|
50
50
|
async recordAttempt({ errorMessage, filePath, fix, success, explanation }) {
|
|
51
51
|
if (!this.brain || !this.brain._initialized) return;
|
|
52
52
|
|
|
53
|
-
const safeError =
|
|
54
|
-
const safeExplanation =
|
|
53
|
+
const safeError = redact(errorMessage);
|
|
54
|
+
const safeExplanation = redact(explanation || fix || "");
|
|
55
55
|
|
|
56
56
|
const namespace = success ? "fixes" : "errors";
|
|
57
57
|
const prefix = success ? "FIXED" : "FAILED";
|
|
@@ -66,7 +66,7 @@ class ResearchAgent {
|
|
|
66
66
|
* Stores findings in brain for future reference.
|
|
67
67
|
*/
|
|
68
68
|
async research(errorMessage, context) {
|
|
69
|
-
const safeError =
|
|
69
|
+
const safeError = redact(errorMessage);
|
|
70
70
|
|
|
71
71
|
console.log(chalk.magenta(` 🔬 Deep research (${getModel("research")})...`));
|
|
72
72
|
|
package/src/brain/brain.js
CHANGED
|
@@ -191,6 +191,10 @@ const SEED_DOCS = [
|
|
|
191
191
|
text: "Port best practices: development uses port 3000 (standard, no admin, firewall-friendly). Production uses 443 (HTTPS) or 80 (HTTP) behind a reverse proxy (nginx/caddy). Never use random high ports in production — they bypass firewalls and confuse load balancers. Always use HTTPS in production — terminate TLS at the proxy, not in Node. Dashboard auto-runs on port+1. Wolverine warns on startup if the port is non-standard.",
|
|
192
192
|
metadata: { topic: "port-security" },
|
|
193
193
|
},
|
|
194
|
+
{
|
|
195
|
+
text: "Secret redaction is a singleton: initRedactor(projectRoot) called once on startup, then redact(text), redactObj(obj), hasSecrets(text) available everywhere via require('../security/secret-redactor'). No need to pass redactor instances. Every outbound path auto-redacts: event logger, repair history, telemetry heartbeats, brain memories, AI calls, dashboard output. Env variable values replaced with process.env.KEY_NAME.",
|
|
196
|
+
metadata: { topic: "redaction-singleton" },
|
|
197
|
+
},
|
|
194
198
|
];
|
|
195
199
|
|
|
196
200
|
class Brain {
|
package/src/core/runner.js
CHANGED
|
@@ -10,7 +10,7 @@ const { EventLogger, EVENT_TYPES } = require("../logger/event-logger");
|
|
|
10
10
|
const { DashboardServer } = require("../dashboard/server");
|
|
11
11
|
const { PerfMonitor } = require("../monitor/perf-monitor");
|
|
12
12
|
const { Brain } = require("../brain/brain");
|
|
13
|
-
const {
|
|
13
|
+
const { initRedactor, getRedactor } = require("../security/secret-redactor");
|
|
14
14
|
const { McpRegistry } = require("../mcp/mcp-registry");
|
|
15
15
|
const { TokenTracker } = require("../logger/token-tracker");
|
|
16
16
|
const { RepairHistory } = require("../logger/repair-history");
|
|
@@ -43,7 +43,7 @@ class WolverineRunner {
|
|
|
43
43
|
|
|
44
44
|
// Core subsystems
|
|
45
45
|
this.sandbox = new Sandbox(this.cwd);
|
|
46
|
-
this.redactor =
|
|
46
|
+
this.redactor = initRedactor(this.cwd);
|
|
47
47
|
this.rateLimiter = new RateLimiter({
|
|
48
48
|
maxCallsPerWindow: parseInt(process.env.WOLVERINE_RATE_MAX_CALLS, 10) || 10,
|
|
49
49
|
windowMs: parseInt(process.env.WOLVERINE_RATE_WINDOW_MS, 10) || 600000,
|
package/src/core/wolverine.js
CHANGED
|
@@ -23,10 +23,12 @@ const { EVENT_TYPES } = require("../logger/event-logger");
|
|
|
23
23
|
*
|
|
24
24
|
* The engine tries fast path first. If that fails verification, it escalates to the agent.
|
|
25
25
|
*/
|
|
26
|
-
async function heal({ stderr, cwd, sandbox,
|
|
26
|
+
async function heal({ stderr, cwd, sandbox, notifier, rateLimiter, backupManager, logger, brain, mcp, skills, repairHistory }) {
|
|
27
27
|
const healStartTime = Date.now();
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
const { redact, hasSecrets } = require("../security/secret-redactor");
|
|
29
|
+
|
|
30
|
+
// Redact secrets BEFORE any processing, logging, or AI calls
|
|
31
|
+
const safeStderr = redact(stderr);
|
|
30
32
|
|
|
31
33
|
if (logger) logger.info(EVENT_TYPES.HEAL_START, "Wolverine detected a crash", { stderr: safeStderr.slice(0, 500) });
|
|
32
34
|
console.log(chalk.yellow("\n🐺 Wolverine detected a crash. Analyzing...\n"));
|
|
@@ -35,13 +37,11 @@ async function heal({ stderr, cwd, sandbox, redactor, notifier, rateLimiter, bac
|
|
|
35
37
|
const parsed = parseError(stderr);
|
|
36
38
|
const errorSignature = RateLimiter.signature(parsed.errorMessage, parsed.filePath);
|
|
37
39
|
|
|
38
|
-
// Redact
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
parsed.stackTrace = redactor.redact(parsed.stackTrace);
|
|
42
|
-
}
|
|
40
|
+
// Redact parsed fields — these go to AI, brain, and logs
|
|
41
|
+
parsed.errorMessage = redact(parsed.errorMessage);
|
|
42
|
+
parsed.stackTrace = redact(parsed.stackTrace);
|
|
43
43
|
|
|
44
|
-
if (
|
|
44
|
+
if (hasSecrets(stderr)) {
|
|
45
45
|
console.log(chalk.yellow(" 🔐 Secrets detected in error output — redacted before AI/brain/logs"));
|
|
46
46
|
}
|
|
47
47
|
|
|
@@ -136,7 +136,7 @@ async function heal({ stderr, cwd, sandbox, redactor, notifier, rateLimiter, bac
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
// 6. Research — check past attempts to avoid loops
|
|
139
|
-
const researcher = new ResearchAgent({ brain, logger
|
|
139
|
+
const researcher = new ResearchAgent({ brain, logger });
|
|
140
140
|
let researchContext = "";
|
|
141
141
|
try {
|
|
142
142
|
researchContext = await researcher.buildFixContext(parsed.errorMessage);
|
package/src/dashboard/server.js
CHANGED
|
@@ -154,7 +154,8 @@ class DashboardServer {
|
|
|
154
154
|
return;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
const
|
|
157
|
+
const { redact } = require("../security/secret-redactor");
|
|
158
|
+
const safeCommand = redact(command);
|
|
158
159
|
|
|
159
160
|
if (this._isSecretExtractionRequest(command)) {
|
|
160
161
|
const refusal = this._buildSecretRefusal();
|
|
@@ -763,7 +764,7 @@ ${context ? "\nBrain:\n" + context : ""}`,
|
|
|
763
764
|
req.on("error", (e) => resolve(`Error: ${e.message}`));
|
|
764
765
|
req.on("timeout", () => { req.destroy(); resolve("Timeout"); });
|
|
765
766
|
});
|
|
766
|
-
|
|
767
|
+
const { redact: _r } = require("../security/secret-redactor"); toolResult = _r(toolResult);
|
|
767
768
|
console.log(chalk.green(` 🌐 → ${toolResult.slice(0, 80)}`));
|
|
768
769
|
} catch (e) { toolResult = `Error: ${e.message}`; }
|
|
769
770
|
} else if (tc.function.name === "read_file") {
|
package/src/index.js
CHANGED
|
@@ -11,7 +11,7 @@ const { HealthMonitor } = require("./core/health-monitor");
|
|
|
11
11
|
const { Sandbox, SandboxViolationError } = require("./security/sandbox");
|
|
12
12
|
const { RateLimiter } = require("./security/rate-limiter");
|
|
13
13
|
const { detectInjection } = require("./security/injection-detector");
|
|
14
|
-
const { SecretRedactor } = require("./security/secret-redactor");
|
|
14
|
+
const { SecretRedactor, initRedactor, redact, redactObj, hasSecrets } = require("./security/secret-redactor");
|
|
15
15
|
const { AdminAuth } = require("./security/admin-auth");
|
|
16
16
|
const { BackupManager } = require("./backup/backup-manager");
|
|
17
17
|
const { EventLogger, EVENT_TYPES, SEVERITY } = require("./logger/event-logger");
|
|
@@ -91,9 +91,6 @@ class EventLogger extends EventEmitter {
|
|
|
91
91
|
this._recentEvents = [];
|
|
92
92
|
this._maxRecent = 1000;
|
|
93
93
|
|
|
94
|
-
// Secret redactor — if set, all events get redacted before persist/emit
|
|
95
|
-
this.redactor = null;
|
|
96
|
-
|
|
97
94
|
// Session tracking
|
|
98
95
|
this.sessionId = Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 6);
|
|
99
96
|
this.sessionStart = Date.now();
|
|
@@ -101,11 +98,9 @@ class EventLogger extends EventEmitter {
|
|
|
101
98
|
}
|
|
102
99
|
|
|
103
100
|
/**
|
|
104
|
-
*
|
|
101
|
+
* @deprecated Use singleton redact() — kept for backwards compat
|
|
105
102
|
*/
|
|
106
|
-
setRedactor(
|
|
107
|
-
this.redactor = redactor;
|
|
108
|
-
}
|
|
103
|
+
setRedactor() {}
|
|
109
104
|
|
|
110
105
|
/**
|
|
111
106
|
* Log an event. This is the primary API.
|
|
@@ -117,8 +112,10 @@ class EventLogger extends EventEmitter {
|
|
|
117
112
|
*/
|
|
118
113
|
log(type, severity, message, data = {}) {
|
|
119
114
|
// Redact secrets before they hit storage or the wire
|
|
120
|
-
|
|
121
|
-
const
|
|
115
|
+
// Universal redaction via singleton — no instance needed
|
|
116
|
+
const { redact, redactObj } = require("../security/secret-redactor");
|
|
117
|
+
const safeMessage = redact(message);
|
|
118
|
+
const safeData = redactObj(data);
|
|
122
119
|
|
|
123
120
|
const event = {
|
|
124
121
|
id: `${this.sessionId}-${(++this._eventCount).toString(36)}`,
|
|
@@ -37,14 +37,15 @@ class RepairHistory {
|
|
|
37
37
|
duration, // ms from crash to fix
|
|
38
38
|
filesModified, // files changed
|
|
39
39
|
}) {
|
|
40
|
+
const { redact } = require("../security/secret-redactor");
|
|
40
41
|
const entry = {
|
|
41
42
|
id: Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 4),
|
|
42
43
|
timestamp: Date.now(),
|
|
43
44
|
iso: new Date().toISOString(),
|
|
44
|
-
error: (error || "").slice(0, 200),
|
|
45
|
+
error: redact((error || "").slice(0, 200)),
|
|
45
46
|
file,
|
|
46
47
|
line,
|
|
47
|
-
resolution: (resolution || "").slice(0, 300),
|
|
48
|
+
resolution: redact((resolution || "").slice(0, 300)),
|
|
48
49
|
success,
|
|
49
50
|
mode,
|
|
50
51
|
model,
|
|
@@ -13,7 +13,7 @@ let _v = null;
|
|
|
13
13
|
function collectHeartbeat(subsystems) {
|
|
14
14
|
if (!_v) { try { _v = require("../../package.json").version; } catch { _v = "0.0.0"; } }
|
|
15
15
|
|
|
16
|
-
const { processMonitor, routeProber, tokenTracker, repairHistory, backupManager, brain
|
|
16
|
+
const { processMonitor, routeProber, tokenTracker, repairHistory, backupManager, brain } = subsystems;
|
|
17
17
|
const proc = processMonitor?.getMetrics();
|
|
18
18
|
const usage = tokenTracker?.getAnalytics();
|
|
19
19
|
const repairs = repairHistory?.getStats();
|
|
@@ -60,10 +60,10 @@ function collectHeartbeat(subsystems) {
|
|
|
60
60
|
backups: backupManager?.getStats() || { total: 0, stable: 0 },
|
|
61
61
|
};
|
|
62
62
|
|
|
63
|
-
if (
|
|
63
|
+
if (repairs?.lastRepair) {
|
|
64
64
|
payload.repairs.lastRepair = {
|
|
65
|
-
error:
|
|
66
|
-
resolution:
|
|
65
|
+
error: (repairs.lastRepair?.error || "").slice(0, 150),
|
|
66
|
+
resolution: (repairs.lastRepair?.resolution || "").slice(0, 150),
|
|
67
67
|
tokens: repairs.lastRepair?.tokens || 0,
|
|
68
68
|
cost: repairs.lastRepair?.cost || 0,
|
|
69
69
|
mode: repairs.lastRepair?.mode || "",
|
|
@@ -72,7 +72,9 @@ function collectHeartbeat(subsystems) {
|
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
// Pre-flight security: redact entire payload before it leaves the process
|
|
76
|
+
const { redactObj } = require("../security/secret-redactor");
|
|
77
|
+
return redactObj(payload);
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
module.exports = { collectHeartbeat, INSTANCE_ID };
|
|
@@ -214,4 +214,50 @@ class SecretRedactor {
|
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
|
|
217
|
+
// ── Singleton ──
|
|
218
|
+
// One instance, initialized once, used everywhere.
|
|
219
|
+
// Import { redact, redactObj } from anywhere — no need to pass the redactor around.
|
|
220
|
+
|
|
221
|
+
let _instance = null;
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Initialize the singleton. Call once on startup from runner.
|
|
225
|
+
*/
|
|
226
|
+
function initRedactor(projectRoot) {
|
|
227
|
+
_instance = new SecretRedactor(projectRoot);
|
|
228
|
+
return _instance;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Get the singleton instance.
|
|
233
|
+
*/
|
|
234
|
+
function getRedactor() {
|
|
235
|
+
return _instance;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Redact a string. Safe to call even if not initialized (returns input unchanged).
|
|
240
|
+
* This is the universal function — use it everywhere instead of this.redactor?.redact().
|
|
241
|
+
*/
|
|
242
|
+
function redact(text) {
|
|
243
|
+
if (!_instance || !text || typeof text !== "string") return text;
|
|
244
|
+
return _instance.redact(text);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Redact all string values in an object (deep). Safe to call if not initialized.
|
|
249
|
+
*/
|
|
250
|
+
function redactObj(obj) {
|
|
251
|
+
if (!_instance) return obj;
|
|
252
|
+
return _instance.redactObject(obj);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Check if a string contains secrets.
|
|
257
|
+
*/
|
|
258
|
+
function hasSecrets(text) {
|
|
259
|
+
if (!_instance || !text) return false;
|
|
260
|
+
return _instance.containsSecrets(text);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
module.exports = { SecretRedactor, initRedactor, getRedactor, redact, redactObj, hasSecrets };
|