wolverine-ai 4.8.0 → 5.0.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 +17 -0
- package/package.json +3 -2
- package/src/brain/brain.js +6 -2
- package/src/brain/tool-router.js +211 -0
- package/src/core/wolverine.js +7 -0
- package/src/middleware/x402-fastify.js +290 -0
- package/src/skills/vault.js +26 -3
package/bin/wolverine.js
CHANGED
|
@@ -38,6 +38,7 @@ ${chalk.bold("Options:")}
|
|
|
38
38
|
--workers <n> Force specific worker count
|
|
39
39
|
--info Show system info and exit
|
|
40
40
|
--init Scan server/ and build context map (routes, DB, config, deps)
|
|
41
|
+
--x402-price Set x402 price: wolverine --x402-price "POST /api" "$0.01"
|
|
41
42
|
|
|
42
43
|
${chalk.bold("Configuration:")}
|
|
43
44
|
server/config/settings.json Models, telemetry, limits, health checks
|
|
@@ -66,6 +67,22 @@ if (args.includes("--info")) {
|
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
// --init: scan server/ and build context map
|
|
70
|
+
// --x402-price: set route pricing live
|
|
71
|
+
if (args.includes("--x402-price")) {
|
|
72
|
+
const idx = args.indexOf("--x402-price");
|
|
73
|
+
const route = args[idx + 1];
|
|
74
|
+
const price = args[idx + 2];
|
|
75
|
+
if (!route || !price) {
|
|
76
|
+
console.log(chalk.red(' Usage: wolverine --x402-price "POST /v1/chat/completions" "$0.001"'));
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
const { setPrice } = require("../src/middleware/x402-fastify");
|
|
80
|
+
const result = setPrice(route, price);
|
|
81
|
+
console.log(chalk.green(` ✅ x402 price updated: ${result.route} → ${result.price}`));
|
|
82
|
+
console.log(chalk.gray(" Change is live — no restart needed."));
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
85
|
+
|
|
69
86
|
if (args.includes("--init")) {
|
|
70
87
|
const { scan } = require("../src/core/server-context");
|
|
71
88
|
console.log(chalk.blue("\n 🔍 Scanning server/ directory...\n"));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wolverine-ai",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.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": {
|
|
@@ -66,7 +66,8 @@
|
|
|
66
66
|
"ethers": "^6.0.0",
|
|
67
67
|
"ioredis": "^5.0.0",
|
|
68
68
|
"pg": "^8.0.0",
|
|
69
|
-
"stripe": "^18.0.0"
|
|
69
|
+
"stripe": "^18.0.0",
|
|
70
|
+
"viem": "^2.0.0"
|
|
70
71
|
},
|
|
71
72
|
"engines": {
|
|
72
73
|
"node": ">=18.0.0"
|
package/src/brain/brain.js
CHANGED
|
@@ -145,6 +145,10 @@ const SEED_DOCS = [
|
|
|
145
145
|
text: "Server context scanner (wolverine --init): scans server/ directory on every startup to build .wolverine/server-context.json. Extracts routes (HTTP methods + paths from fastify/express), middleware stack, database type + tables, config structure, dependencies, env vars used (process.env.X patterns), and full file tree. Context summary auto-injected into agent's heal prompt so it knows the server's route map, DB schema, and dependencies without re-scanning. Manual scan: wolverine --init. Auto-scan: runs silently on every boot. The context is read-only — never modified by the agent.",
|
|
146
146
|
metadata: { topic: "server-context" },
|
|
147
147
|
},
|
|
148
|
+
{
|
|
149
|
+
text: "Tool router (src/brain/tool-router.js): maps error types to recommended tool chains. Injected into agent prompt automatically — agent sees 'TOOL ROUTE: diagnose with [X, Y], fix with [Z]. Strategy: ...' for every error it heals. 25+ error categories mapped: TypeError→read_file+edit_file, ENOENT→list_dir+write_file, ECONNREFUSED→check_network+check_port+inspect_cache, EMFILE→check_file_descriptors+disk_cleanup, ENOSPC→disk_cleanup, certificate→inspect_certificate, OOM→check_memory+check_event_loop, websocket���check_websocket, database→inspect_db+run_db_fix (with prereq: inspect_db FIRST), missing_module→audit_deps+verify_node_modules, env_missing→inspect_env+add_env_var. Lookup is O(1) by error type + regex fallback. No AI tokens used for routing.",
|
|
150
|
+
metadata: { topic: "tool-router" },
|
|
151
|
+
},
|
|
148
152
|
{
|
|
149
153
|
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.",
|
|
150
154
|
metadata: { topic: "telemetry-architecture" },
|
|
@@ -186,8 +190,8 @@ const SEED_DOCS = [
|
|
|
186
190
|
metadata: { topic: "notifications" },
|
|
187
191
|
},
|
|
188
192
|
{
|
|
189
|
-
text: "Vault: encrypted key storage in .wolverine/vault/. AES-256-GCM encryption. master.key
|
|
190
|
-
metadata: { topic: "vault-
|
|
193
|
+
text: "Vault + x402 payments: encrypted key storage in .wolverine/vault/. AES-256-GCM encryption. master.key encrypts eth.vault (Ethereum private key). Generated on first run. Private key NEVER as JS string — Buffer only, wiped after use. wallet-ops: getWalletAddress(), signTransaction(), signMessage(). x402 protocol: HTTP 402 Payment Required for API monetization. Fastify middleware (src/middleware/x402-fastify.js) auto-gates routes with USDC payments on Base network. Config in settings.json x402.routes: { 'POST /api': { price: '$0.01' } }. Live price updates: PUT /x402/price or wolverine --x402-price 'POST /api' '$0.01'. Vault wallet is the payTo address. Facilitator (x402.org or Coinbase CDP) verifies and settles payments. Buyer SDKs (@x402/fetch, @x402/axios) auto-handle the 402→sign→retry flow. Vault skill actions: x402_pricing, x402_set_price, x402_remove_price.",
|
|
194
|
+
metadata: { topic: "vault-x402" },
|
|
191
195
|
},
|
|
192
196
|
{
|
|
193
197
|
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.",
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Router — maps error types to recommended tool chains.
|
|
3
|
+
*
|
|
4
|
+
* Lightweight adjacency graph that tells the agent which tools to use
|
|
5
|
+
* for each error category. Injected into the agent prompt so it doesn't
|
|
6
|
+
* waste turns guessing.
|
|
7
|
+
*
|
|
8
|
+
* Structure per error type:
|
|
9
|
+
* diagnose: tools to understand the problem (read-only)
|
|
10
|
+
* fix: tools to apply the solution (write)
|
|
11
|
+
* hint: one-line strategy for the AI
|
|
12
|
+
* prereq: tools that must run BEFORE fix tools
|
|
13
|
+
*
|
|
14
|
+
* Lookup: O(1) by error classifier output or regex match on error message.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const TOOL_ROUTES = {
|
|
18
|
+
// ── Code Errors ──
|
|
19
|
+
"TypeError": {
|
|
20
|
+
diagnose: ["read_file", "grep_code"],
|
|
21
|
+
fix: ["edit_file"],
|
|
22
|
+
hint: "Read the file at the error line. Find the undefined/null variable. Add a guard or fix the assignment.",
|
|
23
|
+
},
|
|
24
|
+
"ReferenceError": {
|
|
25
|
+
diagnose: ["read_file", "grep_code"],
|
|
26
|
+
fix: ["edit_file"],
|
|
27
|
+
hint: "Variable is not defined. Check for typos, missing imports, or scope issues.",
|
|
28
|
+
},
|
|
29
|
+
"SyntaxError": {
|
|
30
|
+
diagnose: ["read_file"],
|
|
31
|
+
fix: ["edit_file"],
|
|
32
|
+
hint: "Parse error in source. Read the file and fix the syntax at the reported line.",
|
|
33
|
+
},
|
|
34
|
+
"RangeError": {
|
|
35
|
+
diagnose: ["read_file", "check_memory"],
|
|
36
|
+
fix: ["edit_file"],
|
|
37
|
+
hint: "Stack overflow or invalid array length. Check for infinite recursion or unbounded growth.",
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
// ── Module/Dependency Errors ──
|
|
41
|
+
"missing_module": {
|
|
42
|
+
diagnose: ["audit_deps", "verify_node_modules"],
|
|
43
|
+
fix: ["bash_exec"],
|
|
44
|
+
hint: "Run npm install <module>. If in package.json but missing from disk, run npm ci.",
|
|
45
|
+
},
|
|
46
|
+
"missing_file": {
|
|
47
|
+
diagnose: ["read_file", "list_dir", "inspect_env"],
|
|
48
|
+
fix: ["write_file", "bash_exec"],
|
|
49
|
+
hint: "Check if the path is wrong (edit require) or if the file should exist (create it with expected content).",
|
|
50
|
+
},
|
|
51
|
+
"version_conflict": {
|
|
52
|
+
diagnose: ["audit_deps", "check_migration", "verify_node_modules"],
|
|
53
|
+
fix: ["bash_exec"],
|
|
54
|
+
hint: "Check peer deps and version compatibility. May need npm install package@version or full reinstall.",
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
// ── File System Errors ──
|
|
58
|
+
"ENOENT": {
|
|
59
|
+
diagnose: ["list_dir", "inspect_env", "read_file"],
|
|
60
|
+
fix: ["write_file", "bash_exec"],
|
|
61
|
+
prereq: ["read_file"],
|
|
62
|
+
hint: "File or directory doesn't exist. Check if path is correct, create if missing.",
|
|
63
|
+
},
|
|
64
|
+
"EACCES": {
|
|
65
|
+
diagnose: ["bash_exec", "list_dir"],
|
|
66
|
+
fix: ["bash_exec"],
|
|
67
|
+
hint: "Permission denied. Run chmod to fix permissions on the target file/directory.",
|
|
68
|
+
},
|
|
69
|
+
"ENOSPC": {
|
|
70
|
+
diagnose: ["disk_cleanup", "check_memory"],
|
|
71
|
+
fix: ["disk_cleanup"],
|
|
72
|
+
hint: "Disk full. Run disk_cleanup with dry_run=false to clear old backups and caches.",
|
|
73
|
+
},
|
|
74
|
+
"EMFILE": {
|
|
75
|
+
diagnose: ["check_file_descriptors", "list_processes", "check_event_loop"],
|
|
76
|
+
fix: ["disk_cleanup", "restart_service"],
|
|
77
|
+
hint: "Too many open files. Check for FD leaks, clear caches, consider raising ulimit in system profile.",
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
// ── Network Errors ──
|
|
81
|
+
"ECONNREFUSED": {
|
|
82
|
+
diagnose: ["check_network", "check_port", "inspect_cache", "inspect_env"],
|
|
83
|
+
fix: ["edit_file", "restart_service"],
|
|
84
|
+
hint: "Target service is down or wrong host/port. Check config, verify service is running.",
|
|
85
|
+
},
|
|
86
|
+
"ECONNRESET": {
|
|
87
|
+
diagnose: ["check_network", "check_logs"],
|
|
88
|
+
fix: ["edit_file"],
|
|
89
|
+
hint: "Connection reset by remote. Check for timeout settings, proxy config, or unstable network.",
|
|
90
|
+
},
|
|
91
|
+
"ETIMEDOUT": {
|
|
92
|
+
diagnose: ["check_network", "check_logs", "inspect_certificate"],
|
|
93
|
+
fix: ["edit_file"],
|
|
94
|
+
hint: "Connection timed out. Increase timeout, check DNS, verify endpoint is reachable.",
|
|
95
|
+
},
|
|
96
|
+
"EADDRINUSE": {
|
|
97
|
+
diagnose: ["check_port", "list_processes"],
|
|
98
|
+
fix: ["bash_exec"],
|
|
99
|
+
hint: "Port already in use. Find and kill the stale process holding the port.",
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
// ── Database Errors ──
|
|
103
|
+
"database": {
|
|
104
|
+
diagnose: ["inspect_db", "inspect_cache", "read_file"],
|
|
105
|
+
fix: ["run_db_fix", "edit_file"],
|
|
106
|
+
prereq: ["inspect_db"],
|
|
107
|
+
hint: "ALWAYS inspect_db before run_db_fix. Check table schema, query the affected data, then fix.",
|
|
108
|
+
},
|
|
109
|
+
"pool_exhaustion": {
|
|
110
|
+
diagnose: ["inspect_cache", "check_network", "check_logs"],
|
|
111
|
+
fix: ["edit_file", "restart_service"],
|
|
112
|
+
hint: "DB connection pool exhausted. Check for connection leaks, increase pool size, or restart.",
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
// ── SSL/TLS Errors ──
|
|
116
|
+
"certificate": {
|
|
117
|
+
diagnose: ["inspect_certificate", "check_network", "inspect_env"],
|
|
118
|
+
fix: ["bash_exec", "edit_file", "add_env_var"],
|
|
119
|
+
hint: "Check cert expiry, SAN list, and chain. May need renewal, hostname fix, or CA bundle.",
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
// ── Memory/Performance Errors ──
|
|
123
|
+
"OOM": {
|
|
124
|
+
diagnose: ["check_memory", "check_event_loop", "list_processes"],
|
|
125
|
+
fix: ["edit_file", "restart_service"],
|
|
126
|
+
hint: "Out of memory. Check for memory leaks (growing arrays, unclosed streams), reduce batch sizes.",
|
|
127
|
+
},
|
|
128
|
+
"event_loop_blocked": {
|
|
129
|
+
diagnose: ["check_event_loop", "check_logs", "check_memory"],
|
|
130
|
+
fix: ["edit_file"],
|
|
131
|
+
hint: "Synchronous operation blocking event loop. Replace readFileSync→readFile, execSync→exec, etc.",
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
// ── WebSocket Errors ──
|
|
135
|
+
"websocket": {
|
|
136
|
+
diagnose: ["check_websocket", "check_network", "check_port"],
|
|
137
|
+
fix: ["edit_file"],
|
|
138
|
+
hint: "Test WS handshake. Check upgrade headers, proxy config, and connection timeout settings.",
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
// ── Config/Environment Errors ──
|
|
142
|
+
"config": {
|
|
143
|
+
diagnose: ["read_file", "inspect_env", "list_dir"],
|
|
144
|
+
fix: ["write_file", "edit_file", "add_env_var"],
|
|
145
|
+
hint: "Check if config file exists and has expected structure. Check if required env vars are set.",
|
|
146
|
+
},
|
|
147
|
+
"env_missing": {
|
|
148
|
+
diagnose: ["inspect_env", "read_file"],
|
|
149
|
+
fix: ["add_env_var"],
|
|
150
|
+
hint: "Required environment variable not set. Add it to .env.local with the correct value.",
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Find tool recommendations for an error.
|
|
156
|
+
* @param {string} errorMessage — the raw error message
|
|
157
|
+
* @param {string} errorType — from error-parser classifyError() (optional)
|
|
158
|
+
* @returns {{ diagnose: string[], fix: string[], prereq?: string[], hint: string } | null}
|
|
159
|
+
*/
|
|
160
|
+
function route(errorMessage, errorType) {
|
|
161
|
+
const msg = (errorMessage || "").toLowerCase();
|
|
162
|
+
|
|
163
|
+
// 1. Try exact error type match
|
|
164
|
+
if (errorType && TOOL_ROUTES[errorType]) return TOOL_ROUTES[errorType];
|
|
165
|
+
|
|
166
|
+
// 2. Try error class match
|
|
167
|
+
if (/typeerror/i.test(msg)) return TOOL_ROUTES.TypeError;
|
|
168
|
+
if (/referenceerror/i.test(msg)) return TOOL_ROUTES.ReferenceError;
|
|
169
|
+
if (/syntaxerror|unexpected token/i.test(msg)) return TOOL_ROUTES.SyntaxError;
|
|
170
|
+
if (/rangeerror/i.test(msg)) return TOOL_ROUTES.RangeError;
|
|
171
|
+
|
|
172
|
+
// 3. Try errno/code match
|
|
173
|
+
if (/enoent|no such file/i.test(msg)) return TOOL_ROUTES.ENOENT;
|
|
174
|
+
if (/eacces|eperm/i.test(msg)) return TOOL_ROUTES.EACCES;
|
|
175
|
+
if (/enospc/i.test(msg)) return TOOL_ROUTES.ENOSPC;
|
|
176
|
+
if (/emfile|enfile/i.test(msg)) return TOOL_ROUTES.EMFILE;
|
|
177
|
+
if (/econnrefused/i.test(msg)) return TOOL_ROUTES.ECONNREFUSED;
|
|
178
|
+
if (/econnreset/i.test(msg)) return TOOL_ROUTES.ECONNRESET;
|
|
179
|
+
if (/etimedout/i.test(msg)) return TOOL_ROUTES.ETIMEDOUT;
|
|
180
|
+
if (/eaddrinuse/i.test(msg)) return TOOL_ROUTES.EADDRINUSE;
|
|
181
|
+
|
|
182
|
+
// 4. Try pattern match
|
|
183
|
+
if (/cannot find module/i.test(msg)) return /['"][./\\]/.test(msg) ? TOOL_ROUTES.missing_file : TOOL_ROUTES.missing_module;
|
|
184
|
+
if (/cert|ssl|tls|self.signed/i.test(msg)) return TOOL_ROUTES.certificate;
|
|
185
|
+
if (/pool.*exhaust|pool.*timeout|acquire.*timeout/i.test(msg)) return TOOL_ROUTES.pool_exhaustion;
|
|
186
|
+
if (/websocket|ws.*close|transport.*close/i.test(msg)) return TOOL_ROUTES.websocket;
|
|
187
|
+
if (/out of memory|heap.*limit|allocation.*failed/i.test(msg)) return TOOL_ROUTES.OOM;
|
|
188
|
+
if (/not.set|undefined.*env|missing.*env/i.test(msg)) return TOOL_ROUTES.env_missing;
|
|
189
|
+
if (/missing.*config|invalid.*json|invalid.*config/i.test(msg)) return TOOL_ROUTES.config;
|
|
190
|
+
if (/sqlite|postgres|mysql|mongo|sequelize|prisma|knex/i.test(msg)) return TOOL_ROUTES.database;
|
|
191
|
+
if (/peer dep|eresolve|version.*mismatch/i.test(msg)) return TOOL_ROUTES.version_conflict;
|
|
192
|
+
|
|
193
|
+
return null; // unknown — let the AI figure it out
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get a compact prompt injection for the agent.
|
|
198
|
+
* Returns a string like "TOOL ROUTE: diagnose with [check_port, list_processes], fix with [bash_exec]. Hint: ..."
|
|
199
|
+
*/
|
|
200
|
+
function getRoutePrompt(errorMessage, errorType) {
|
|
201
|
+
const r = route(errorMessage, errorType);
|
|
202
|
+
if (!r) return "";
|
|
203
|
+
const parts = [`TOOL ROUTE for this error:`];
|
|
204
|
+
parts.push(` Diagnose: ${r.diagnose.join(", ")}`);
|
|
205
|
+
if (r.prereq) parts.push(` Prerequisites: ${r.prereq.join(", ")} (run these FIRST)`);
|
|
206
|
+
parts.push(` Fix: ${r.fix.join(", ")}`);
|
|
207
|
+
parts.push(` Strategy: ${r.hint}`);
|
|
208
|
+
return parts.join("\n");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
module.exports = { route, getRoutePrompt, TOOL_ROUTES };
|
package/src/core/wolverine.js
CHANGED
|
@@ -13,6 +13,7 @@ const { RateLimiter } = require("../security/rate-limiter");
|
|
|
13
13
|
const { detectInjection } = require("../security/injection-detector");
|
|
14
14
|
const { redact, hasSecrets } = require("../security/secret-redactor");
|
|
15
15
|
const { BackupManager } = require("../backup/backup-manager");
|
|
16
|
+
const { getRoutePrompt } = require("../brain/tool-router");
|
|
16
17
|
const { AgentEngine } = require("../agent/agent-engine");
|
|
17
18
|
const { ResearchAgent } = require("../agent/research-agent");
|
|
18
19
|
const { GoalLoop } = require("../agent/goal-loop");
|
|
@@ -215,6 +216,12 @@ async function _healImpl({ stderr, cwd, sandbox, notifier, rateLimiter, backupMa
|
|
|
215
216
|
const serverCtx = getServerContextSummary(cwd);
|
|
216
217
|
if (serverCtx) brainContext += serverCtx + "\n\n";
|
|
217
218
|
} catch {}
|
|
219
|
+
// Inject tool routing — tells agent exactly which tools to use for this error type
|
|
220
|
+
const toolRoute = getRoutePrompt(parsed.errorMessage, parsed.errorType);
|
|
221
|
+
if (toolRoute) {
|
|
222
|
+
brainContext += toolRoute + "\n\n";
|
|
223
|
+
console.log(chalk.gray(` 🗺️ Tool route: ${toolRoute.split("\n")[1]?.trim() || "matched"}`));
|
|
224
|
+
}
|
|
218
225
|
// Inject relevant skill context (claw-code: pre-enrich prompt with matched tools)
|
|
219
226
|
if (skills) {
|
|
220
227
|
const skillCtx = skills.buildContext(parsed.errorMessage);
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* x402 Payment Middleware for Fastify — monetize any API route with USDC on Base.
|
|
6
|
+
*
|
|
7
|
+
* Implements the x402 protocol (HTTP 402 Payment Required) for Fastify.
|
|
8
|
+
* No official @x402/fastify exists, so we build directly on @x402/core.
|
|
9
|
+
*
|
|
10
|
+
* Usage in server/index.js:
|
|
11
|
+
* fastify.register(require('wolverine-ai/src/middleware/x402-fastify'), {
|
|
12
|
+
* payTo: '0xYourAddress', // or auto from vault
|
|
13
|
+
* network: 'eip155:8453', // Base mainnet
|
|
14
|
+
* });
|
|
15
|
+
*
|
|
16
|
+
* Route pricing in settings.json:
|
|
17
|
+
* "x402": {
|
|
18
|
+
* "enabled": true,
|
|
19
|
+
* "routes": {
|
|
20
|
+
* "POST /v1/chat/completions": { "price": "$0.001" },
|
|
21
|
+
* "GET /api/premium": { "price": "$0.01" }
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
*
|
|
25
|
+
* Live price updates:
|
|
26
|
+
* PUT /x402/price { route: "POST /v1/chat/completions", price: "$0.002" }
|
|
27
|
+
* wolverine --x402-price "POST /v1/chat/completions" "$0.002"
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
// In-memory route pricing — survives hot updates without restart
|
|
31
|
+
let _routePricing = {};
|
|
32
|
+
let _payTo = null;
|
|
33
|
+
let _network = "eip155:8453"; // Base mainnet default
|
|
34
|
+
let _facilitatorUrl = "https://x402.org/facilitator";
|
|
35
|
+
let _initialized = false;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get current route pricing (for external access).
|
|
39
|
+
*/
|
|
40
|
+
function getPricing() { return { ..._routePricing }; }
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Update a single route's price live — no restart needed.
|
|
44
|
+
*/
|
|
45
|
+
function setPrice(routeKey, price) {
|
|
46
|
+
if (!price.startsWith("$")) price = "$" + price;
|
|
47
|
+
_routePricing[routeKey] = { price };
|
|
48
|
+
// Persist to settings.json
|
|
49
|
+
_persistPricing();
|
|
50
|
+
return { route: routeKey, price };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Remove pricing from a route (make it free).
|
|
55
|
+
*/
|
|
56
|
+
function removePrice(routeKey) {
|
|
57
|
+
delete _routePricing[routeKey];
|
|
58
|
+
_persistPricing();
|
|
59
|
+
return { route: routeKey, price: "free" };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function _persistPricing() {
|
|
63
|
+
try {
|
|
64
|
+
const settingsPath = path.join(process.cwd(), "server", "config", "settings.json");
|
|
65
|
+
if (fs.existsSync(settingsPath)) {
|
|
66
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
67
|
+
if (!settings.x402) settings.x402 = {};
|
|
68
|
+
settings.x402.routes = {};
|
|
69
|
+
for (const [route, cfg] of Object.entries(_routePricing)) {
|
|
70
|
+
settings.x402.routes[route] = { price: cfg.price };
|
|
71
|
+
}
|
|
72
|
+
const tmp = settingsPath + ".tmp";
|
|
73
|
+
fs.writeFileSync(tmp, JSON.stringify(settings, null, 2), "utf-8");
|
|
74
|
+
fs.renameSync(tmp, settingsPath);
|
|
75
|
+
}
|
|
76
|
+
} catch {}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Fastify plugin — registers x402 payment middleware.
|
|
81
|
+
*/
|
|
82
|
+
async function x402Plugin(fastify, opts) {
|
|
83
|
+
// Load config
|
|
84
|
+
_payTo = opts.payTo || null;
|
|
85
|
+
_network = opts.network || "eip155:8453";
|
|
86
|
+
_facilitatorUrl = opts.facilitator || "https://x402.org/facilitator";
|
|
87
|
+
|
|
88
|
+
// Auto-detect payTo from vault if not provided
|
|
89
|
+
if (!_payTo) {
|
|
90
|
+
try {
|
|
91
|
+
const { getWalletAddress } = require("../vault/wallet-ops");
|
|
92
|
+
_payTo = await getWalletAddress();
|
|
93
|
+
} catch {}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Load route pricing from settings.json
|
|
97
|
+
try {
|
|
98
|
+
const settingsPath = path.join(process.cwd(), "server", "config", "settings.json");
|
|
99
|
+
if (fs.existsSync(settingsPath)) {
|
|
100
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
101
|
+
if (settings.x402?.routes) {
|
|
102
|
+
for (const [route, cfg] of Object.entries(settings.x402.routes)) {
|
|
103
|
+
_routePricing[route] = { price: cfg.price };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (settings.x402?.network) _network = settings.x402.network;
|
|
107
|
+
if (settings.x402?.facilitator) _facilitatorUrl = settings.x402.facilitator;
|
|
108
|
+
}
|
|
109
|
+
} catch {}
|
|
110
|
+
|
|
111
|
+
if (!_payTo) {
|
|
112
|
+
console.log(" ⚠️ x402: no payTo address (set in settings.json or init vault)");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
_initialized = true;
|
|
117
|
+
const routeCount = Object.keys(_routePricing).length;
|
|
118
|
+
if (routeCount > 0) {
|
|
119
|
+
console.log(` 💰 x402: ${routeCount} paid route(s), receiving at ${_payTo.slice(0, 6)}...${_payTo.slice(-4)}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── Main payment gate — onRequest hook ──
|
|
123
|
+
fastify.addHook("onRequest", async (request, reply) => {
|
|
124
|
+
if (!_initialized || Object.keys(_routePricing).length === 0) return;
|
|
125
|
+
|
|
126
|
+
const routeKey = `${request.method} ${request.url.split("?")[0]}`;
|
|
127
|
+
const routeConfig = _routePricing[routeKey];
|
|
128
|
+
if (!routeConfig) return; // Free route
|
|
129
|
+
|
|
130
|
+
const paymentSig = request.headers["payment-signature"];
|
|
131
|
+
|
|
132
|
+
// No payment — return 402 with payment instructions
|
|
133
|
+
if (!paymentSig) {
|
|
134
|
+
const paymentRequired = {
|
|
135
|
+
accepts: [{
|
|
136
|
+
scheme: "exact",
|
|
137
|
+
price: routeConfig.price,
|
|
138
|
+
network: _network,
|
|
139
|
+
payTo: _payTo,
|
|
140
|
+
}],
|
|
141
|
+
description: `Payment required for ${routeKey}`,
|
|
142
|
+
mimeType: "application/json",
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
reply
|
|
146
|
+
.code(402)
|
|
147
|
+
.header("Payment-Required", JSON.stringify(paymentRequired))
|
|
148
|
+
.header("X-402-Version", "1.0")
|
|
149
|
+
.send({
|
|
150
|
+
error: "Payment Required",
|
|
151
|
+
price: routeConfig.price,
|
|
152
|
+
network: _network,
|
|
153
|
+
payTo: _payTo,
|
|
154
|
+
protocol: "x402",
|
|
155
|
+
});
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Payment present — verify via facilitator
|
|
160
|
+
try {
|
|
161
|
+
const verified = await _verifyPayment(paymentSig, routeConfig);
|
|
162
|
+
if (verified.valid) {
|
|
163
|
+
// Payment good — add receipt header and continue
|
|
164
|
+
reply.header("Payment-Response", JSON.stringify(verified.receipt || {}));
|
|
165
|
+
request.x402 = { paid: true, amount: routeConfig.price, txHash: verified.txHash };
|
|
166
|
+
return; // continue to route handler
|
|
167
|
+
}
|
|
168
|
+
} catch {}
|
|
169
|
+
|
|
170
|
+
// Verification failed
|
|
171
|
+
reply.code(402).send({
|
|
172
|
+
error: "Payment verification failed",
|
|
173
|
+
price: routeConfig.price,
|
|
174
|
+
network: _network,
|
|
175
|
+
payTo: _payTo,
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// ── Live price management API ──
|
|
180
|
+
fastify.put("/x402/price", async (request, reply) => {
|
|
181
|
+
// Admin only
|
|
182
|
+
const token = request.headers.authorization?.replace("Bearer ", "");
|
|
183
|
+
let settings = {};
|
|
184
|
+
try { settings = JSON.parse(fs.readFileSync(path.join(process.cwd(), "server", "config", "settings.json"), "utf-8")); } catch {}
|
|
185
|
+
if (token !== settings.platform?.apiKey) {
|
|
186
|
+
return reply.code(403).send({ error: "Admin only" });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const { route, price } = request.body || {};
|
|
190
|
+
if (!route || !price) return reply.code(400).send({ error: "route and price required" });
|
|
191
|
+
const result = setPrice(route, price);
|
|
192
|
+
return { updated: true, ...result };
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
fastify.delete("/x402/price", async (request, reply) => {
|
|
196
|
+
const token = request.headers.authorization?.replace("Bearer ", "");
|
|
197
|
+
let settings = {};
|
|
198
|
+
try { settings = JSON.parse(fs.readFileSync(path.join(process.cwd(), "server", "config", "settings.json"), "utf-8")); } catch {}
|
|
199
|
+
if (token !== settings.platform?.apiKey) {
|
|
200
|
+
return reply.code(403).send({ error: "Admin only" });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const { route } = request.body || {};
|
|
204
|
+
if (!route) return reply.code(400).send({ error: "route required" });
|
|
205
|
+
return removePrice(route);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
fastify.get("/x402/pricing", async () => {
|
|
209
|
+
return {
|
|
210
|
+
payTo: _payTo,
|
|
211
|
+
network: _network,
|
|
212
|
+
routes: _routePricing,
|
|
213
|
+
};
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Verify a payment signature via the facilitator.
|
|
219
|
+
*/
|
|
220
|
+
async function _verifyPayment(paymentSig, routeConfig) {
|
|
221
|
+
try {
|
|
222
|
+
// Try @x402/core facilitator client if available
|
|
223
|
+
const { HTTPFacilitatorClient } = require("@x402/core/server");
|
|
224
|
+
const { ExactEvmScheme } = require("@x402/evm/exact/server");
|
|
225
|
+
|
|
226
|
+
const facilitator = new HTTPFacilitatorClient({ url: _facilitatorUrl });
|
|
227
|
+
const result = await facilitator.verify({
|
|
228
|
+
paymentSignature: paymentSig,
|
|
229
|
+
routeConfig: {
|
|
230
|
+
accepts: [{
|
|
231
|
+
scheme: "exact",
|
|
232
|
+
price: routeConfig.price,
|
|
233
|
+
network: _network,
|
|
234
|
+
payTo: _payTo,
|
|
235
|
+
}],
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
return { valid: result.valid, receipt: result.receipt, txHash: result.txHash };
|
|
239
|
+
} catch {
|
|
240
|
+
// Fallback: manual HTTP verification to facilitator
|
|
241
|
+
try {
|
|
242
|
+
const https = require("https");
|
|
243
|
+
const http = require("http");
|
|
244
|
+
const url = new (require("url").URL)(_facilitatorUrl + "/verify");
|
|
245
|
+
const body = JSON.stringify({
|
|
246
|
+
paymentSignature: paymentSig,
|
|
247
|
+
routeConfig: {
|
|
248
|
+
accepts: [{
|
|
249
|
+
scheme: "exact",
|
|
250
|
+
price: routeConfig.price,
|
|
251
|
+
network: _network,
|
|
252
|
+
payTo: _payTo,
|
|
253
|
+
}],
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
return new Promise((resolve) => {
|
|
258
|
+
const client = url.protocol === "https:" ? https : http;
|
|
259
|
+
const req = client.request({
|
|
260
|
+
hostname: url.hostname,
|
|
261
|
+
port: url.port,
|
|
262
|
+
path: url.pathname,
|
|
263
|
+
method: "POST",
|
|
264
|
+
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) },
|
|
265
|
+
timeout: 10000,
|
|
266
|
+
}, (res) => {
|
|
267
|
+
let data = "";
|
|
268
|
+
res.on("data", (c) => data += c);
|
|
269
|
+
res.on("end", () => {
|
|
270
|
+
try {
|
|
271
|
+
const parsed = JSON.parse(data);
|
|
272
|
+
resolve({ valid: parsed.valid || parsed.success, receipt: parsed, txHash: parsed.txHash });
|
|
273
|
+
} catch { resolve({ valid: false }); }
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
req.on("error", () => resolve({ valid: false }));
|
|
277
|
+
req.on("timeout", () => { req.destroy(); resolve({ valid: false }); });
|
|
278
|
+
req.write(body);
|
|
279
|
+
req.end();
|
|
280
|
+
});
|
|
281
|
+
} catch { return { valid: false }; }
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
x402Plugin[Symbol.for("skip-override")] = true; // Fastify plugin compat
|
|
286
|
+
|
|
287
|
+
module.exports = x402Plugin;
|
|
288
|
+
module.exports.getPricing = getPricing;
|
|
289
|
+
module.exports.setPrice = setPrice;
|
|
290
|
+
module.exports.removePrice = removePrice;
|
package/src/skills/vault.js
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
const SKILL_NAME = "vault";
|
|
13
|
-
const SKILL_DESCRIPTION = "Secure Ethereum wallet — sign transactions, get address,
|
|
14
|
-
const SKILL_KEYWORDS = ["wallet", "ethereum", "eth", "sign", "transaction", "vault", "private key", "address", "crypto", "blockchain", "send", "transfer"];
|
|
13
|
+
const SKILL_DESCRIPTION = "Secure Ethereum wallet + x402 payments — sign transactions, get address, manage paid API routes. Private key encrypted at rest via AES-256-GCM, never exposed in code or errors. x402 support: set prices on routes, receive USDC on Base network.";
|
|
14
|
+
const SKILL_KEYWORDS = ["wallet", "ethereum", "eth", "sign", "transaction", "vault", "private key", "address", "crypto", "blockchain", "send", "transfer", "x402", "payment", "usdc", "base", "price", "paid", "api"];
|
|
15
15
|
const SKILL_USAGE = `
|
|
16
16
|
vault.status() — check if vault is initialized, show address
|
|
17
17
|
vault.address() — get the wallet's Ethereum address
|
|
@@ -61,8 +61,31 @@ async function execute(action, params = {}) {
|
|
|
61
61
|
return { signedTransaction: signed };
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
// x402 payment actions
|
|
65
|
+
case "x402_pricing":
|
|
66
|
+
try {
|
|
67
|
+
const { getPricing } = require("../middleware/x402-fastify");
|
|
68
|
+
return getPricing();
|
|
69
|
+
} catch { return { error: "x402 middleware not loaded" }; }
|
|
70
|
+
|
|
71
|
+
case "x402_set_price": {
|
|
72
|
+
if (!params.route || !params.price) return { error: "route and price required" };
|
|
73
|
+
try {
|
|
74
|
+
const { setPrice } = require("../middleware/x402-fastify");
|
|
75
|
+
return setPrice(params.route, params.price);
|
|
76
|
+
} catch { return { error: "x402 middleware not loaded" }; }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
case "x402_remove_price": {
|
|
80
|
+
if (!params.route) return { error: "route required" };
|
|
81
|
+
try {
|
|
82
|
+
const { removePrice } = require("../middleware/x402-fastify");
|
|
83
|
+
return removePrice(params.route);
|
|
84
|
+
} catch { return { error: "x402 middleware not loaded" }; }
|
|
85
|
+
}
|
|
86
|
+
|
|
64
87
|
default:
|
|
65
|
-
return { error: `Unknown vault action: ${action}. Use: status, address, sign_tx, sign_message, public_key` };
|
|
88
|
+
return { error: `Unknown vault action: ${action}. Use: status, address, sign_tx, sign_message, public_key, x402_pricing, x402_set_price, x402_remove_price` };
|
|
66
89
|
}
|
|
67
90
|
}
|
|
68
91
|
|