wolverine-ai 1.6.1 → 1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wolverine-ai",
3
- "version": "1.6.1",
3
+ "version": "1.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": {
@@ -96,7 +96,7 @@ const SEED_DOCS = [
96
96
  metadata: { topic: "skill-sql-patterns" },
97
97
  },
98
98
  {
99
- text: "Database best practices: SafeDB uses split connections — separate read connection (concurrent, never waits) and write connection (single writer, FIFO queue). Write queue drains synchronously in one microtask, zero delays. WAL mode means readers never block writers. Each write is microseconds. db.transaction(fn) queues as single atomic unit. No busy_timeout, no blocking, no IPC. Reads: db.get(), db.all() are instant. Writes: db.run(), db.exec() go through queue.",
99
+ text: "Database best practices: SafeDB uses split connections — separate read connection (concurrent, never waits) and write connection (single writer, FIFO queue). Write queue drains synchronously in one microtask, zero delays. WAL mode means readers never block writers. Each write is microseconds. db.transaction(fn) queues as single atomic unit. No busy_timeout, no blocking, no IPC. Reads: db.get(), db.all() are instant. Writes: db.run(), db.exec() go through queue. Idempotent writes: db.idempotent(key, fn, ttlSeconds) executes fn only once per key — prevents double-charge/double-insert when retries or cluster workers duplicate a request. Idempotency keys stored in _idempotency table (auto-created on connect), shared across all workers via WAL mode.",
100
100
  metadata: { topic: "skill-sql-best-practices" },
101
101
  },
102
102
  {
@@ -120,7 +120,7 @@ const SEED_DOCS = [
120
120
  metadata: { topic: "process-manager" },
121
121
  },
122
122
  {
123
- text: "Auto-clustering: wolverine detects machine capabilities (cores, RAM, disk, platform, Docker/K8s, cloud provider) and forks optimal workers. 2 cores = 2 workers, 3-4 = cores-1, 5-8 = cores-1 cap 6, 9+ = cores/2 cap 16. Workers auto-respawn on crash with exponential backoff. CLI: --single (no cluster), --workers N (fixed), --info (show system). Settings in server/config/settings.json cluster.mode.",
123
+ text: "Cluster mode: server handles its own clustering (not wolverine-level). WOLVERINE_CLUSTER=true enables it. Server forks N workers (WOLVERINE_RECOMMENDED_WORKERS set by system detection). Workers share port 3000 via reusePort. Wolverine kills entire process tree on restart (_killProcessTree: taskkill /T on Windows, kill -pgid + pgrep -P on Linux). Idempotency protection prevents double-fire: idempotencyGuard() middleware deduplicates write requests across workers using shared SQLite _idempotency table. Client sends X-Idempotency-Key header, or auto-generated from method+path+body hash. All workers see the same table via WAL mode. SafeDB.idempotent(key, fn) for database-level dedup.",
124
124
  metadata: { topic: "clustering" },
125
125
  },
126
126
  {
@@ -220,9 +220,13 @@ const SEED_DOCS = [
220
220
  metadata: { topic: "agent-tools-v2" },
221
221
  },
222
222
  {
223
- text: "Server problem categories the agent can fix: CODE BUGS (SyntaxError, TypeError, ReferenceError → edit_file), DEPENDENCIES (Cannot find module → npm install, corrupted node_modules → rm + reinstall), DATABASE (invalid entries → run_db_fix UPDATE, missing table → CREATE TABLE, schema mismatch → ALTER TABLE, constraint violation → fix data or schema), CONFIG (invalid JSON → edit_file, missing env vars → write .env, wrong port → edit config), FILESYSTEM (misplaced files → move_file, missing directories → bash_exec mkdir, wrong permissions → chmod), NETWORK (port conflict → check_port + kill, service down → restart, connection refused → check config), STATE (corrupted cache → delete + restart, stale locks → remove lock file, git conflicts → resolve markers). The agent investigates before fixing — reads files, checks directories, inspects databases, never guesses.",
223
+ text: "Server problem categories the agent can fix: CODE BUGS (SyntaxError, TypeError, ReferenceError → edit_file), DEPENDENCIES (Cannot find module → npm install, corrupted node_modules → rm + reinstall), DATABASE (invalid entries → run_db_fix UPDATE, missing table → CREATE TABLE, schema mismatch → ALTER TABLE, constraint violation → fix data or schema), CONFIG (invalid JSON → edit_file, missing env vars → write .env, wrong port → edit config), FILESYSTEM (misplaced files → move_file, missing directories → bash_exec mkdir, wrong permissions → chmod), NETWORK (port conflict → check_port + kill, service down → restart, connection refused → check config), STATE (corrupted cache → delete + restart, stale locks → remove lock file, git conflicts → resolve markers), IDEMPOTENCY (double-fire → add idempotencyGuard middleware, missing idempotency key → add X-Idempotency-Key header support, duplicate DB entries → add UNIQUE constraint or use db.idempotent()). The agent investigates before fixing — reads files, checks directories, inspects databases, never guesses.",
224
224
  metadata: { topic: "server-problems" },
225
225
  },
226
+ {
227
+ text: "Idempotency protection: two layers prevent double-fire in cluster mode. Layer 1: idempotencyGuard() Fastify middleware — intercepts POST/PUT/PATCH/DELETE, checks X-Idempotency-Key header (or auto-generates key from method+path+body hash), queries _idempotency table. If key exists and not expired → return cached response with X-Idempotency-Cached:true header, skip handler. If new → pass through, idempotencyAfterHook() stores response. Layer 2: SafeDB.idempotent(key, fn) — database-level dedup. Wraps fn in transaction, checks key, executes only if new. Returns {executed:true/false, result, cached}. Keys expire after TTL (default 24h). All workers share the SQLite _idempotency table via WAL mode — globally consistent. Auto-pruned on connect and via db.pruneIdempotency().",
228
+ metadata: { topic: "idempotency" },
229
+ },
226
230
  {
227
231
  text: "Heal pipeline no longer requires a file path. When no file is identified from the error (database errors, config problems, port conflicts), the pipeline skips fast path and goes straight to the agent, which uses investigation tools (glob_files, grep_code, list_dir, inspect_db, check_env, check_port) to find the root cause. Agent verification for no-file errors: if agent made changes or ran commands, trust the agent's assessment. For file-based errors, verification uses syntax check + boot probe as before.",
228
232
  metadata: { topic: "fileless-heal" },
package/src/index.js CHANGED
@@ -33,7 +33,7 @@ const { scanProject } = require("./brain/function-map");
33
33
  const { detect: detectSystem } = require("./core/system-info");
34
34
  const { ClusterManager } = require("./core/cluster-manager");
35
35
  const { loadConfig, getConfig } = require("./core/config");
36
- const { sqlGuard, SafeDB, scanForInjection } = require("./skills/sql");
36
+ const { sqlGuard, SafeDB, scanForInjection, idempotencyGuard, idempotencyAfterHook } = require("./skills/sql");
37
37
 
38
38
  module.exports = {
39
39
  // Core
@@ -93,4 +93,6 @@ module.exports = {
93
93
  sqlGuard,
94
94
  SafeDB,
95
95
  scanForInjection,
96
+ idempotencyGuard,
97
+ idempotencyAfterHook,
96
98
  };
package/src/skills/sql.js CHANGED
@@ -201,6 +201,18 @@ class SafeDB {
201
201
  this._writer.pragma("foreign_keys = ON");
202
202
  this._writer.pragma("synchronous = NORMAL");
203
203
 
204
+ // Idempotency table — prevents double-execution of writes in cluster mode
205
+ this._writer.exec(`
206
+ CREATE TABLE IF NOT EXISTS _idempotency (
207
+ key TEXT PRIMARY KEY,
208
+ result TEXT,
209
+ created_at INTEGER DEFAULT (strftime('%s','now')),
210
+ expires_at INTEGER
211
+ )
212
+ `);
213
+ // Clean expired keys on connect
214
+ this._writer.exec(`DELETE FROM _idempotency WHERE expires_at < strftime('%s','now')`);
215
+
204
216
  } catch (err) {
205
217
  if (err.code === "MODULE_NOT_FOUND") {
206
218
  throw new Error("Install better-sqlite3: npm install better-sqlite3");
@@ -216,6 +228,49 @@ class SafeDB {
216
228
  process.on("exit", () => this.close());
217
229
  }
218
230
 
231
+ /**
232
+ * Idempotent write — execute fn only if this key hasn't been seen before.
233
+ * In cluster mode, prevents the same request from double-firing across workers.
234
+ *
235
+ * @param {string} key — unique idempotency key (e.g. from X-Idempotency-Key header)
236
+ * @param {Function} fn — function that performs the write, receives writerProxy
237
+ * @param {number} ttlSeconds — how long to remember this key (default: 86400 = 24h)
238
+ * @returns {{ executed: boolean, result: any }} — executed=false if key was already seen
239
+ */
240
+ idempotent(key, fn, ttlSeconds = 86400) {
241
+ this._assertOpen();
242
+ return this._enqueueWrite(() => {
243
+ // Check if key already executed
244
+ const existing = this._writer.prepare("SELECT result FROM _idempotency WHERE key = ? AND expires_at > strftime('%s','now')").get(key);
245
+ if (existing) {
246
+ return { executed: false, result: JSON.parse(existing.result || "null"), cached: true };
247
+ }
248
+
249
+ // Execute the write
250
+ const txn = this._writer.transaction(() => {
251
+ const result = fn(this._writerProxy());
252
+ // Store the key so duplicates are rejected
253
+ this._writer.prepare(
254
+ "INSERT OR REPLACE INTO _idempotency (key, result, expires_at) VALUES (?, ?, strftime('%s','now') + ?)"
255
+ ).run(key, JSON.stringify(result ?? null), ttlSeconds);
256
+ return result;
257
+ });
258
+
259
+ const result = txn();
260
+ return { executed: true, result, cached: false };
261
+ });
262
+ }
263
+
264
+ /**
265
+ * Clean up expired idempotency keys. Call periodically (e.g., every hour).
266
+ */
267
+ pruneIdempotency() {
268
+ this._assertOpen();
269
+ return this._enqueueWrite(() => {
270
+ return this._writer.prepare("DELETE FROM _idempotency WHERE expires_at < strftime('%s','now')").run();
271
+ });
272
+ }
273
+
219
274
  /**
220
275
  * Write query (INSERT, UPDATE, DELETE, CREATE).
221
276
  * Queued and executed in order. Returns a promise that resolves with the result.
@@ -327,27 +382,137 @@ class SafeDB {
327
382
  }
328
383
  }
329
384
 
385
+ // ── Idempotency Middleware ──────────────────────────────────────
386
+
387
+ /**
388
+ * Request idempotency middleware — prevents double-fire in cluster mode.
389
+ *
390
+ * How it works:
391
+ * 1. Client sends write request (POST/PUT/PATCH/DELETE) with X-Idempotency-Key header
392
+ * 2. Middleware checks if this key was already processed
393
+ * 3. If yes: return cached response (no re-execution)
394
+ * 4. If no: execute handler, cache response, return result
395
+ *
396
+ * Without the header, mutating requests get an auto-generated key based on
397
+ * method + path + body hash. This means identical retries are deduplicated
398
+ * even without client cooperation.
399
+ *
400
+ * In cluster mode (reusePort), a retry can land on a different worker.
401
+ * Since all workers share the same SQLite database (WAL mode), the
402
+ * idempotency table is visible to all workers instantly.
403
+ *
404
+ * Safe methods (GET, HEAD, OPTIONS) are always passed through — they're
405
+ * inherently idempotent.
406
+ *
407
+ * @param {object} options
408
+ * @param {SafeDB} options.db — SafeDB instance (must be connected)
409
+ * @param {number} options.ttlSeconds — how long to cache responses (default: 86400)
410
+ * @param {object} options.logger — wolverine EventLogger (optional)
411
+ */
412
+ function idempotencyGuard(options = {}) {
413
+ const db = options.db;
414
+ const ttlSeconds = options.ttlSeconds || 86400;
415
+ const logger = options.logger || null;
416
+ const crypto = require("crypto");
417
+
418
+ return async (req, res, next) => {
419
+ // Safe methods are inherently idempotent — pass through
420
+ const method = (req.method || "GET").toUpperCase();
421
+ if (["GET", "HEAD", "OPTIONS"].includes(method)) return next();
422
+
423
+ // Get or generate idempotency key
424
+ let key = req.headers["x-idempotency-key"] || req.headers["idempotency-key"];
425
+ if (!key) {
426
+ // Auto-generate from method + path + body hash
427
+ const bodyStr = typeof req.body === "string" ? req.body : JSON.stringify(req.body || "");
428
+ key = crypto.createHash("sha256").update(`${method}:${req.url}:${bodyStr}`).digest("hex");
429
+ }
430
+
431
+ if (!db || !db._writer) return next(); // No DB — can't check, pass through
432
+
433
+ try {
434
+ // Check idempotency table directly (read from writer for consistency)
435
+ const existing = db._writer.prepare(
436
+ "SELECT result FROM _idempotency WHERE key = ? AND expires_at > strftime('%s','now')"
437
+ ).get(key);
438
+
439
+ if (existing) {
440
+ // Already processed — return cached response
441
+ const cached = JSON.parse(existing.result || "null");
442
+ if (logger) logger.debug("idempotency.hit", `Duplicate request blocked: ${method} ${req.url}`, { key: key.slice(0, 16) });
443
+
444
+ const status = cached?.statusCode || 200;
445
+ const body = cached?.body || cached;
446
+ if (typeof res.code === "function") {
447
+ // Fastify
448
+ res.code(status).header("X-Idempotency-Cached", "true").send(body);
449
+ } else {
450
+ // Express
451
+ res.status(status).set("X-Idempotency-Cached", "true").json(body);
452
+ }
453
+ return;
454
+ }
455
+
456
+ // Not seen — attach key to request for the route handler to use
457
+ req._idempotencyKey = key;
458
+ req._idempotencyTtl = ttlSeconds;
459
+ } catch {
460
+ // DB error — don't block the request, just pass through
461
+ }
462
+
463
+ next();
464
+ };
465
+ }
466
+
467
+ /**
468
+ * After-response hook — stores the response for future idempotency checks.
469
+ * For Fastify, add as onSend hook. For Express, monkey-patch res.json.
470
+ *
471
+ * @param {SafeDB} db — connected SafeDB instance
472
+ */
473
+ function idempotencyAfterHook(db) {
474
+ return (req, reply, payload, done) => {
475
+ if (req._idempotencyKey && db && db._writer) {
476
+ try {
477
+ const statusCode = reply.statusCode || 200;
478
+ const result = JSON.stringify({ statusCode, body: typeof payload === "string" ? JSON.parse(payload) : payload });
479
+ db._writer.prepare(
480
+ "INSERT OR IGNORE INTO _idempotency (key, result, expires_at) VALUES (?, ?, strftime('%s','now') + ?)"
481
+ ).run(req._idempotencyKey, result, req._idempotencyTtl || 86400);
482
+ } catch { /* non-fatal */ }
483
+ }
484
+ done();
485
+ };
486
+ }
487
+
330
488
  // ── Skill Metadata (for SkillRegistry discovery) ──
331
489
 
332
490
  const SKILL_NAME = "sql";
333
- const SKILL_DESCRIPTION = "SQL database interface with injection prevention. Provides sqlGuard() middleware to block SQL injection on all endpoints, and SafeDB class for parameterized-only database queries.";
334
- const SKILL_KEYWORDS = ["sql", "database", "db", "query", "injection", "sqlite", "postgres", "mysql", "select", "insert", "update", "delete", "table", "schema", "migration", "parameterized"];
335
- const SKILL_USAGE = `// Protect all routes from SQL injection
336
- const { sqlGuard } = require("../src/skills/sql");
337
- app.use(sqlGuard({ logger: wolverineLogger }));
338
-
339
- // Cluster-safe database (each worker gets its own connection)
340
- const { SafeDB } = require("../src/skills/sql");
491
+ const SKILL_DESCRIPTION = "SQL database interface with injection prevention + idempotency. Provides sqlGuard() middleware to block SQL injection, idempotencyGuard() middleware to prevent double-fire in cluster mode, and SafeDB class for parameterized-only database queries with built-in idempotency key support.";
492
+ const SKILL_KEYWORDS = ["sql", "database", "db", "query", "injection", "sqlite", "postgres", "mysql", "select", "insert", "update", "delete", "table", "schema", "migration", "parameterized", "idempotent", "idempotency", "duplicate", "double", "cluster", "transaction"];
493
+ const SKILL_USAGE = `// Protect routes from SQL injection + double-fire
494
+ const { sqlGuard, idempotencyGuard, idempotencyAfterHook, SafeDB } = require("../src/skills/sql");
341
495
  const db = new SafeDB({ type: "sqlite", path: "./server/data.db" });
342
- await db.connect(); // WAL mode, busy_timeout=5s, write serialization
496
+ await db.connect();
497
+
498
+ // Middleware: injection prevention + idempotency (cluster-safe)
499
+ fastify.addHook("preHandler", sqlGuard({ logger }));
500
+ fastify.addHook("preHandler", idempotencyGuard({ db, logger }));
501
+ fastify.addHook("onSend", idempotencyAfterHook(db));
343
502
 
344
- // Reads (concurrent across workers)
503
+ // Reads (concurrent across workers — never waits)
345
504
  const users = db.all("SELECT * FROM users WHERE role = ?", ["admin"]);
346
505
 
347
- // Writes (serialized — no corruption)
506
+ // Writes (serialized FIFO queue — no corruption)
348
507
  db.run("INSERT INTO users (name, role) VALUES (?, ?)", ["Alice", "admin"]);
349
508
 
350
- // Batch writes (atomic transaction, single lock)
509
+ // Idempotent write prevents double-charge/double-insert in cluster mode
510
+ const result = await db.idempotent("order-abc-123", (tx) => {
511
+ tx.run("INSERT INTO orders (id, total) VALUES (?, ?)", ["abc-123", 99.99]);
512
+ return { orderId: "abc-123" };
513
+ }); // result.executed=true first time, false on retry
514
+
515
+ // Atomic transaction (all-or-nothing)
351
516
  db.transaction((tx) => {
352
517
  tx.run("INSERT INTO orders (user_id, total) VALUES (?, ?)", [1, 99.99]);
353
518
  tx.run("UPDATE users SET order_count = order_count + 1 WHERE id = ?", [1]);
@@ -364,6 +529,8 @@ module.exports = {
364
529
 
365
530
  // Middleware
366
531
  sqlGuard,
532
+ idempotencyGuard,
533
+ idempotencyAfterHook,
367
534
  scanForInjection,
368
535
  deepScan,
369
536