wolverine-ai 1.0.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/PLATFORM.md +8 -0
- package/README.md +17 -2
- package/SERVER_BEST_PRACTICES.md +11 -3
- package/package.json +1 -1
- package/server/config/settings.json +1 -1
- package/src/agent/research-agent.js +4 -4
- package/src/brain/brain.js +56 -0
- package/src/core/runner.js +16 -2
- package/src/core/wolverine.js +10 -10
- package/src/dashboard/server.js +83 -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 +9 -5
- package/src/security/secret-redactor.js +47 -1
package/PLATFORM.md
CHANGED
|
@@ -103,6 +103,14 @@ Content-Type: application/json
|
|
|
103
103
|
"chat": { "tokens": 25000, "cost": 0.05, "calls": 60 },
|
|
104
104
|
"classify": { "tokens": 3000, "cost": 0.001, "calls": 15 },
|
|
105
105
|
"develop": { "tokens": 5000, "cost": 0.03, "calls": 5 }
|
|
106
|
+
},
|
|
107
|
+
"byModel": {
|
|
108
|
+
"gpt-5.4-mini": { "tokens": 30000, "cost": 0.06, "calls": 40 },
|
|
109
|
+
"gpt-4o-mini": { "tokens": 15000, "cost": 0.02, "calls": 45 }
|
|
110
|
+
},
|
|
111
|
+
"byTool": {
|
|
112
|
+
"call_endpoint": { "tokens": 5000, "cost": 0.01, "calls": 20 },
|
|
113
|
+
"search_brain": { "tokens": 2000, "cost": 0.005, "calls": 10 }
|
|
106
114
|
}
|
|
107
115
|
},
|
|
108
116
|
|
package/README.md
CHANGED
|
@@ -10,15 +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
|
+
|
|
15
|
+
```bash
|
|
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
|
+
```
|
|
21
|
+
|
|
22
|
+
### Or clone from GitHub
|
|
23
|
+
|
|
13
24
|
```bash
|
|
14
25
|
git clone https://github.com/bobbyswhip/Wolverine.git
|
|
15
26
|
cd Wolverine
|
|
16
27
|
npm install
|
|
17
28
|
cp .env.example .env.local
|
|
18
|
-
# Edit .env.local — add your OPENAI_API_KEY
|
|
29
|
+
# Edit .env.local — add your OPENAI_API_KEY
|
|
19
30
|
npm start
|
|
20
31
|
```
|
|
21
32
|
|
|
33
|
+
[](https://www.npmjs.com/package/wolverine-ai)
|
|
34
|
+
[](https://github.com/bobbyswhip/Wolverine)
|
|
35
|
+
|
|
22
36
|
Dashboard opens at `http://localhost:PORT+1`. Server runs on `PORT`.
|
|
23
37
|
|
|
24
38
|
### Try a Demo
|
|
@@ -359,7 +373,8 @@ Startup:
|
|
|
359
373
|
- Auto-registers on first run, retries every 60s until platform responds
|
|
360
374
|
- Saves key to `.wolverine/platform-key` (survives restarts)
|
|
361
375
|
- Sends one ~2KB JSON POST every 60 seconds (5s timeout, non-blocking)
|
|
362
|
-
- Payload matches [PLATFORM.md](PLATFORM.md) spec: `instanceId`, `server`, `process`, `routes`, `repairs`, `usage
|
|
376
|
+
- Payload matches [PLATFORM.md](PLATFORM.md) spec: `instanceId`, `server`, `process`, `routes`, `repairs`, `usage` (tokens/cost/calls + `byCategory` + `byModel` + `byTool`), `brain`, `backups`
|
|
377
|
+
- Platform analytics aggregates across all servers: total tokens/cost, breakdown by category (heal/chat/develop/security/classify/research/brain), by model, by tool
|
|
363
378
|
- Secrets redacted before sending
|
|
364
379
|
- Offline-resilient: queues up to 1440 heartbeats locally, drains on reconnect
|
|
365
380
|
|
package/SERVER_BEST_PRACTICES.md
CHANGED
|
@@ -19,13 +19,21 @@ server/
|
|
|
19
19
|
|
|
20
20
|
## Rules
|
|
21
21
|
|
|
22
|
+
### Ports
|
|
23
|
+
- **Development**: use port 3000 (standard, no admin required, firewall-friendly)
|
|
24
|
+
- **Production**: use port 443 (HTTPS) or 80 (HTTP) behind a reverse proxy (nginx/caddy)
|
|
25
|
+
- **Never** use random high ports in production — they bypass firewalls and confuse load balancers
|
|
26
|
+
- **Always** use HTTPS in production — terminate TLS at the reverse proxy, not in Node
|
|
27
|
+
- Dashboard runs on port+1 automatically (3001 in dev, not exposed in prod)
|
|
28
|
+
|
|
22
29
|
### Security
|
|
23
30
|
- Never expose secrets in responses — use env vars, never hardcode
|
|
24
|
-
- Validate ALL input —
|
|
25
|
-
- Use
|
|
31
|
+
- Validate ALL input — Fastify has built-in JSON schema validation
|
|
32
|
+
- Use HTTPS in production — reverse proxy (nginx/caddy) handles TLS
|
|
26
33
|
- Rate limit public endpoints
|
|
27
|
-
- Sanitize user input before database queries
|
|
34
|
+
- Sanitize user input before database queries — use the SQL skill
|
|
28
35
|
- Never return stack traces in production error responses
|
|
36
|
+
- Use the sqlGuard() middleware on all routes that accept user input
|
|
29
37
|
|
|
30
38
|
### Scalability
|
|
31
39
|
- Keep routes thin — business logic goes in services/
|
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
|
@@ -139,6 +139,62 @@ const SEED_DOCS = [
|
|
|
139
139
|
text: "Telemetry architecture: 4 files, ~250 lines total. heartbeat.js sends one HTTP POST every 60s (5s timeout, non-blocking). register.js auto-registers and caches key in memory + disk. queue.js appends to JSONL file only on failure, trims lazily. telemetry.js collects from subsystems using optional chaining (no crashes if subsystem missing). All secrets redacted before sending. Response bodies drained immediately (res.resume). No blocking, no delays, no busy waits.",
|
|
140
140
|
metadata: { topic: "telemetry-architecture" },
|
|
141
141
|
},
|
|
142
|
+
{
|
|
143
|
+
text: "Server uses Fastify (migrated from Express). 5.6x faster routing: ~114k req/s vs Express ~20k req/s. Routes are async plugin functions: async function routes(fastify) { fastify.get('/', async () => ({...})); } module.exports = routes. Registered in index.js with fastify.register(require('./routes/X'), {prefix:'/X'}). JSON parsing is built-in, no middleware needed.",
|
|
144
|
+
metadata: { topic: "fastify" },
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
text: "npm package: wolverine-ai on npmjs.com. Install: npm i wolverine-ai. CLI: npx wolverine server/index.js. v1.0.0, 79 files, 125KB compressed. Includes src/, bin/, server/, examples/. GitHub: https://github.com/bobbyswhip/Wolverine",
|
|
148
|
+
metadata: { topic: "npm-package" },
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
text: "Dashboard has 9 panels: Overview (stats cards + recent events), Events (live SSE stream), Performance (endpoint metrics), Analytics (memory/CPU charts, route health, response times), Command (admin chat with 3-route classifier), Backups (server/ snapshots with status badges), Brain (vector store stats + function map), Repairs (error/resolution audit trail with tokens and cost), Tools (agent tool harness listing), Usage (token analytics by model/category/tool with USD costs).",
|
|
152
|
+
metadata: { topic: "dashboard-panels" },
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
text: "Command interface routing: AI classifier (CLASSIFIER_MODEL) returns SIMPLE/TOOLS/AGENT. SIMPLE = brain knowledge only (CHAT_MODEL, no tools). TOOLS = live data with function calling (TOOL_MODEL, call_endpoint/read_file/search_brain). AGENT SMALL = smart edit (CODING_MODEL, 1 AI call, structured JSON file operations). AGENT MEDIUM = single agent (REASONING_MODEL, 8 turns). AGENT LARGE = sub-agents (explore→plan→fix).",
|
|
156
|
+
metadata: { topic: "command-routing" },
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
text: "Smart edit: for SMALL tier tasks, one AI call returns JSON with file operations: [{action:'create',path:'server/routes/X.js',content:'...'},{action:'edit',path:'server/index.js',find:'...',replace:'...'}]. Creates backup before changes, restarts server after, tests endpoint, rescans brain with new routes. Skills auto-injected into prompt when relevant.",
|
|
160
|
+
metadata: { topic: "smart-edit" },
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
text: "Token tracking: every AI call tracked with input/output tokens + USD cost. Categories: heal, develop, chat, security, classify, research, brain. Tracked by model, by category, by tool. Persisted to .wolverine/usage.json (aggregates) and .wolverine/usage-history.jsonl (full timeline). Auto-saves on every call. Dashboard shows charts + cost breakdowns. Pricing from src/logger/pricing.js, customizable via .wolverine/pricing.json.",
|
|
164
|
+
metadata: { topic: "token-tracking" },
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
text: "Repair history: dedicated audit trail at .wolverine/repair-history.json. Each entry: error, file, line, resolution, success, mode (fast/agent/sub-agents), model, tokens, cost, iteration, duration, filesModified. Dashboard Repairs panel shows stats (total, success rate, total cost, avg tokens) + scrollable history with per-repair details.",
|
|
168
|
+
metadata: { topic: "repair-history" },
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
text: "Skill registry: auto-discovers skills from src/skills/ on startup. Each skill exports SKILL_NAME, SKILL_DESCRIPTION, SKILL_KEYWORDS, SKILL_USAGE. Registry matches skills to commands using token scoring (claw-code pattern). Matched skills get injected into agent prompts before AI calls. SQL skill auto-injects when building database features.",
|
|
172
|
+
metadata: { topic: "skill-registry" },
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
text: "Notifications: detects human-required errors (expired keys, billing, service down, certs, permissions, disk). Classifies errors as AI-fixable vs human-required using pattern matching. Generates AI summary (CHAT_MODEL). Fires before wasting tokens on repair. Console alert + dashboard event + optional webhook. Categories: auth, billing, service, cert, permission, disk.",
|
|
176
|
+
metadata: { topic: "notifications" },
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
text: "MCP integration: connect external tools via Model Context Protocol. Configure in .wolverine/mcp.json with per-server tool allowlists. Security: arg sanitization (secrets redacted before sending to MCP servers), result injection scanning, rate limiting per server, audit logging. Tools appear as mcp__server__tool in the agent. Supports stdio and HTTP transports.",
|
|
180
|
+
metadata: { topic: "mcp" },
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
text: "Demos: 7 demo servers in examples/demos/. Demo runner (examples/run-demo.js) copies demo into server/, runs wolverine, restores on exit. npm run demo:list shows all demos. Each demo is a proper Fastify server with routes/ that mirrors the real server/ structure. Tests: basic typo, multi-file, syntax error, secret leak, expired key, JSON config, null crash.",
|
|
184
|
+
metadata: { topic: "demos" },
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
text: "10 configurable models: REASONING_MODEL (multi-file agent), CODING_MODEL (code repair, Responses API for codex), CHAT_MODEL (simple text), TOOL_MODEL (function calling), CLASSIFIER_MODEL (routing), AUDIT_MODEL (injection detection), COMPACTING_MODEL (brain text compression), RESEARCH_MODEL (deep research), TEXT_EMBEDDING_MODEL (vectors). All in server/config/settings.json. Reasoning models auto-get 4x token limits for chain-of-thought.",
|
|
188
|
+
metadata: { topic: "model-slots" },
|
|
189
|
+
},
|
|
190
|
+
{
|
|
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
|
+
metadata: { topic: "port-security" },
|
|
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
|
+
},
|
|
142
198
|
];
|
|
143
199
|
|
|
144
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,
|
|
@@ -140,6 +140,20 @@ class WolverineRunner {
|
|
|
140
140
|
maxRetries: this.maxRetries,
|
|
141
141
|
});
|
|
142
142
|
|
|
143
|
+
// Port safety check
|
|
144
|
+
const port = parseInt(process.env.PORT, 10) || 3000;
|
|
145
|
+
const safeDevPorts = [3000, 3001, 8080, 8443];
|
|
146
|
+
const safeProdPorts = [80, 443, 8080, 8443];
|
|
147
|
+
const env = process.env.NODE_ENV || "development";
|
|
148
|
+
|
|
149
|
+
if (env === "production" && port !== 443 && port !== 80 && port !== 8443 && port !== 8080) {
|
|
150
|
+
console.log(chalk.yellow(` ⚠️ Port ${port} in production — recommend 443 (HTTPS) or 80 (HTTP) behind a reverse proxy`));
|
|
151
|
+
} else if (env !== "production" && !safeDevPorts.includes(port) && port < 1024) {
|
|
152
|
+
console.log(chalk.yellow(` ⚠️ Port ${port} requires root/admin — use 3000 for local development`));
|
|
153
|
+
} else if (port > 9999) {
|
|
154
|
+
console.log(chalk.yellow(` ⚠️ Port ${port} is non-standard — use 3000 (dev) or 443 (prod) for best compatibility`));
|
|
155
|
+
}
|
|
156
|
+
|
|
143
157
|
// Initialize brain (scan project, seed docs, embed function map)
|
|
144
158
|
try {
|
|
145
159
|
await this.brain.init();
|
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") {
|
|
@@ -1029,6 +1030,7 @@ main{overflow-y:auto;padding:24px}
|
|
|
1029
1030
|
<a data-panel="events">📋 Events</a>
|
|
1030
1031
|
<a data-panel="perf">⚡ Performance</a>
|
|
1031
1032
|
<a data-panel="analytics">📊 Analytics</a>
|
|
1033
|
+
<a data-panel="processes">⚙️ Processes</a>
|
|
1032
1034
|
<div class="sep"></div>
|
|
1033
1035
|
<div class="label">Agent</div>
|
|
1034
1036
|
<a data-panel="command">💬 Command</a>
|
|
@@ -1059,6 +1061,32 @@ main{overflow-y:auto;padding:24px}
|
|
|
1059
1061
|
</div>
|
|
1060
1062
|
<div class="panel" id="p-events"><div class="card"><h3>Live Event Stream</h3><div class="ev-list" id="ev-all"></div></div></div>
|
|
1061
1063
|
<div class="panel" id="p-perf"><div class="card"><h3>Endpoint Metrics</h3><div id="perf-list"><div class="empty">No traffic yet</div></div></div></div>
|
|
1064
|
+
<div class="panel" id="p-processes">
|
|
1065
|
+
<div class="stats" style="grid-template-columns:repeat(4,1fr)">
|
|
1066
|
+
<div class="stat-card heal"><div class="stat-val" id="proc-pid">-</div><div class="stat-lbl">Server PID</div></div>
|
|
1067
|
+
<div class="stat-card up"><div class="stat-val" id="proc-up">-</div><div class="stat-lbl">Uptime</div></div>
|
|
1068
|
+
<div class="stat-card err"><div class="stat-val" id="proc-mem">-</div><div class="stat-lbl">Memory (RSS)</div></div>
|
|
1069
|
+
<div class="stat-card brain"><div class="stat-val" id="proc-cpu">-</div><div class="stat-lbl">CPU %</div></div>
|
|
1070
|
+
</div>
|
|
1071
|
+
<div class="row2">
|
|
1072
|
+
<div class="card">
|
|
1073
|
+
<h3>System</h3>
|
|
1074
|
+
<div id="proc-sys"><div class="empty">Loading...</div></div>
|
|
1075
|
+
</div>
|
|
1076
|
+
<div class="card">
|
|
1077
|
+
<h3>Process Health</h3>
|
|
1078
|
+
<div id="proc-health"><div class="empty">Loading...</div></div>
|
|
1079
|
+
</div>
|
|
1080
|
+
</div>
|
|
1081
|
+
<div class="card">
|
|
1082
|
+
<h3>Memory Timeline</h3>
|
|
1083
|
+
<div id="proc-mem-chart" style="height:160px"></div>
|
|
1084
|
+
</div>
|
|
1085
|
+
<div class="card">
|
|
1086
|
+
<h3>Active Connections & Listeners</h3>
|
|
1087
|
+
<div id="proc-listen"><div class="empty">Loading...</div></div>
|
|
1088
|
+
</div>
|
|
1089
|
+
</div>
|
|
1062
1090
|
<div class="panel" id="p-analytics">
|
|
1063
1091
|
<div class="stats" style="grid-template-columns:repeat(4,1fr)">
|
|
1064
1092
|
<div class="stat-card heal"><div class="stat-val" id="a-mem">-</div><div class="stat-lbl">Memory (RSS)</div></div>
|
|
@@ -1280,6 +1308,59 @@ async function refresh(){
|
|
|
1280
1308
|
return '<div class="mrow" style="flex-wrap:wrap"><div style="flex:1"><span style="margin-right:6px">'+icon+'</span><span class="ep">'+esc(r.error).slice(0,60)+'</span></div><span class="vals">'+r.mode+' · '+r.tokens.toLocaleString()+' tokens · '+cost+' · iter '+r.iteration+' · '+(r.duration/1000).toFixed(1)+'s</span><div style="width:100%;font-size:.75rem;color:var(--text2);margin-top:4px">'+date+' — '+esc(r.resolution).slice(0,100)+'</div></div>';
|
|
1281
1309
|
}).join('');
|
|
1282
1310
|
}
|
|
1311
|
+
// Processes panel
|
|
1312
|
+
if(proc){
|
|
1313
|
+
$('proc-pid').textContent=proc.pid||'-';
|
|
1314
|
+
if(proc.current){
|
|
1315
|
+
$('proc-mem').textContent=proc.current.rss+'MB';
|
|
1316
|
+
$('proc-cpu').textContent=proc.current.cpu+'%';
|
|
1317
|
+
}
|
|
1318
|
+
// Uptime from server stats
|
|
1319
|
+
if(sr.session){
|
|
1320
|
+
const u=Math.round((sr.session.uptime||0)/1000);
|
|
1321
|
+
const h=Math.floor(u/3600),m=Math.floor((u%3600)/60),s=u%60;
|
|
1322
|
+
$('proc-up').textContent=(h>0?h+'h ':'')+(m>0?m+'m ':'')+s+'s';
|
|
1323
|
+
}
|
|
1324
|
+
// Leak detection
|
|
1325
|
+
if(proc.leakDetection){
|
|
1326
|
+
const ld=proc.leakDetection;
|
|
1327
|
+
$('proc-health').innerHTML='<div class="mrow"><span>Leak Detection</span><span class="vals">'+(ld.warning?'<span style="color:var(--yellow)">⚠️ Growing</span>':'<span style="color:var(--green)">✅ Stable</span>')+' ('+ld.consecutiveGrowth+'/'+ld.threshold+' samples)</span></div>'
|
|
1328
|
+
+'<div class="mrow"><span>Peak Memory</span><span class="vals"><b>'+(proc.peak?.memory||0)+'MB</b></span></div>'
|
|
1329
|
+
+'<div class="mrow"><span>Avg Memory</span><span class="vals"><b>'+(proc.average?.memory||0)+'MB</b></span></div>'
|
|
1330
|
+
+'<div class="mrow"><span>Avg CPU</span><span class="vals"><b>'+(proc.average?.cpu||0)+'%</b></span></div>'
|
|
1331
|
+
+'<div class="mrow"><span>Process Alive</span><span class="vals">'+(proc.alive?'<span style="color:var(--green)">✅ Yes</span>':'<span style="color:var(--red)">❌ No</span>')+'</span></div>';
|
|
1332
|
+
}
|
|
1333
|
+
// Memory timeline chart
|
|
1334
|
+
if(proc.samples&&proc.samples.length>1){
|
|
1335
|
+
const s=proc.samples,max=Math.max(...s.map(e=>e.rss))||1,w=$('proc-mem-chart').offsetWidth||500,h=150;
|
|
1336
|
+
const bw=Math.max(3,Math.floor(w/s.length)-1);
|
|
1337
|
+
let svg='<svg width="'+w+'" height="'+h+'">';
|
|
1338
|
+
s.forEach((e,i)=>{
|
|
1339
|
+
const bh=Math.max(2,Math.round((e.rss/max)*h*0.85));
|
|
1340
|
+
const c=e.rss>400?'var(--red)':e.rss>200?'var(--yellow)':'var(--blue)';
|
|
1341
|
+
svg+='<rect x="'+(i*(bw+1))+'" y="'+(h-bh)+'" width="'+bw+'" height="'+bh+'" fill="'+c+'" rx="1"><title>'+e.rss+'MB</title></rect>';
|
|
1342
|
+
});
|
|
1343
|
+
svg+='</svg>';
|
|
1344
|
+
$('proc-mem-chart').innerHTML=svg;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
// System info
|
|
1348
|
+
const sysInfo=await fetch(B+'/api/system').then(r=>r.json()).catch(()=>({}));
|
|
1349
|
+
if(sysInfo&&sysInfo.cpu){
|
|
1350
|
+
$('proc-sys').innerHTML=[
|
|
1351
|
+
'<div class="mrow"><span>Platform</span><span class="vals">'+esc(sysInfo.platform)+'/'+esc(sysInfo.arch)+'</span></div>',
|
|
1352
|
+
'<div class="mrow"><span>CPU</span><span class="vals">'+esc((sysInfo.cpu.model||'').slice(0,40))+' ('+sysInfo.cpu.cores+' cores)</span></div>',
|
|
1353
|
+
'<div class="mrow"><span>RAM</span><span class="vals">'+sysInfo.memory.totalGB+'GB total, '+sysInfo.memory.freeGB+'GB free ('+sysInfo.memory.usedPercent+'%)</span></div>',
|
|
1354
|
+
'<div class="mrow"><span>Disk</span><span class="vals">'+sysInfo.disk.totalGB+'GB total, '+sysInfo.disk.freeGB+'GB free ('+sysInfo.disk.usedPercent+'%)</span></div>',
|
|
1355
|
+
'<div class="mrow"><span>Node</span><span class="vals">'+esc(sysInfo.nodeVersion)+'</span></div>',
|
|
1356
|
+
'<div class="mrow"><span>Hostname</span><span class="vals">'+esc(sysInfo.hostname)+'</span></div>',
|
|
1357
|
+
'<div class="mrow"><span>Environment</span><span class="vals">'+esc(sysInfo.environment?.type||'unknown')+(sysInfo.environment?.cloud?' ('+sysInfo.environment.cloud+')':'')+'</span></div>',
|
|
1358
|
+
].join('');
|
|
1359
|
+
// Listening ports
|
|
1360
|
+
$('proc-listen').innerHTML='<div class="mrow"><span>Server</span><span class="vals">:'+(sysInfo.hostname?'running':'?')+'</span></div>'
|
|
1361
|
+
+'<div class="mrow"><span>Dashboard</span><span class="vals">:'+P+'</span></div>'
|
|
1362
|
+
+'<div class="mrow"><span>Workers Recommended</span><span class="vals">'+sysInfo.recommended?.workers+'</span></div>';
|
|
1363
|
+
}
|
|
1283
1364
|
// Analytics: process + routes
|
|
1284
1365
|
if(proc&&proc.current){
|
|
1285
1366
|
$('a-mem').textContent=proc.current.rss+'MB';
|
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();
|
|
@@ -52,16 +52,18 @@ function collectHeartbeat(subsystems) {
|
|
|
52
52
|
totalCost: usage?.session?.totalCostUsd || 0,
|
|
53
53
|
totalCalls: usage?.session?.totalCalls || 0,
|
|
54
54
|
byCategory: usage?.byCategory || {},
|
|
55
|
+
byModel: usage?.byModel || {},
|
|
56
|
+
byTool: usage?.byTool || {},
|
|
55
57
|
},
|
|
56
58
|
|
|
57
59
|
brain: { totalMemories: brain?.getStats()?.totalEntries || 0 },
|
|
58
60
|
backups: backupManager?.getStats() || { total: 0, stable: 0 },
|
|
59
61
|
};
|
|
60
62
|
|
|
61
|
-
if (
|
|
63
|
+
if (repairs?.lastRepair) {
|
|
62
64
|
payload.repairs.lastRepair = {
|
|
63
|
-
error:
|
|
64
|
-
resolution:
|
|
65
|
+
error: (repairs.lastRepair?.error || "").slice(0, 150),
|
|
66
|
+
resolution: (repairs.lastRepair?.resolution || "").slice(0, 150),
|
|
65
67
|
tokens: repairs.lastRepair?.tokens || 0,
|
|
66
68
|
cost: repairs.lastRepair?.cost || 0,
|
|
67
69
|
mode: repairs.lastRepair?.mode || "",
|
|
@@ -70,7 +72,9 @@ function collectHeartbeat(subsystems) {
|
|
|
70
72
|
};
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
|
|
75
|
+
// Pre-flight security: redact entire payload before it leaves the process
|
|
76
|
+
const { redactObj } = require("../security/secret-redactor");
|
|
77
|
+
return redactObj(payload);
|
|
74
78
|
}
|
|
75
79
|
|
|
76
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 };
|