neuronix-node 0.6.0 → 0.7.1
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/dist/index.js +64 -1
- package/dist/security/audit-log.d.ts +22 -0
- package/dist/security/audit-log.js +129 -0
- package/dist/security/resource-limiter.d.ts +36 -0
- package/dist/security/resource-limiter.js +87 -0
- package/dist/security/sandbox.d.ts +24 -0
- package/dist/security/sandbox.js +112 -0
- package/dist/security/task-verifier.d.ts +24 -0
- package/dist/security/task-verifier.js +91 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8,6 +8,10 @@ const models_js_1 = require("./models.js");
|
|
|
8
8
|
const inference_js_1 = require("./inference.js");
|
|
9
9
|
const index_js_1 = require("./handlers/index.js");
|
|
10
10
|
const updater_js_1 = require("./updater.js");
|
|
11
|
+
const resource_limiter_js_1 = require("./security/resource-limiter.js");
|
|
12
|
+
const audit_log_js_1 = require("./security/audit-log.js");
|
|
13
|
+
const task_verifier_js_1 = require("./security/task-verifier.js");
|
|
14
|
+
const sandbox_js_1 = require("./security/sandbox.js");
|
|
11
15
|
// ── Helpers ──────────────────────────────────────────────────
|
|
12
16
|
function log(msg) {
|
|
13
17
|
const ts = new Date().toLocaleTimeString();
|
|
@@ -74,7 +78,35 @@ async function authenticate(config) {
|
|
|
74
78
|
// ── Main ─────────────────────────────────────────────────────
|
|
75
79
|
async function main() {
|
|
76
80
|
banner();
|
|
81
|
+
// ── Security: Initialize audit log ──
|
|
82
|
+
(0, audit_log_js_1.initAuditLog)();
|
|
77
83
|
const config = (0, config_js_1.loadConfig)();
|
|
84
|
+
// ── Security: Verify sandbox ──
|
|
85
|
+
log("Running security checks...");
|
|
86
|
+
const sandbox = (0, sandbox_js_1.verifySandbox)();
|
|
87
|
+
if (sandbox.safe) {
|
|
88
|
+
log("Security: All checks passed ✓");
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
for (const issue of sandbox.issues) {
|
|
92
|
+
log(`Security warning: ${issue}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// ── Security: Show security report if requested ──
|
|
96
|
+
if (process.argv.includes("--security-report")) {
|
|
97
|
+
const report = (0, sandbox_js_1.getSecurityReport)();
|
|
98
|
+
console.log("\n ── Security Report ──");
|
|
99
|
+
console.log(` Sandbox: ${report.sandbox_status}`);
|
|
100
|
+
console.log(` File Access: ${report.file_access}`);
|
|
101
|
+
console.log(` Network Access: ${report.network_access}`);
|
|
102
|
+
console.log(` Process Isolation: ${report.process_isolation}`);
|
|
103
|
+
console.log(` Data Handling: ${report.data_handling}`);
|
|
104
|
+
console.log(" Recommendations:");
|
|
105
|
+
for (const rec of report.recommendations)
|
|
106
|
+
console.log(` - ${rec}`);
|
|
107
|
+
console.log("");
|
|
108
|
+
process.exit(0);
|
|
109
|
+
}
|
|
78
110
|
// Check for updates
|
|
79
111
|
log("Checking for updates...");
|
|
80
112
|
try {
|
|
@@ -189,6 +221,27 @@ async function main() {
|
|
|
189
221
|
if (result.task) {
|
|
190
222
|
const task = result.task;
|
|
191
223
|
log(`Task received: ${task.id.slice(0, 8)}... [${task.type}] model=${task.model || "auto"}`);
|
|
224
|
+
// ── Security: Validate task before execution ──
|
|
225
|
+
const validation = (0, task_verifier_js_1.validateTask)(task);
|
|
226
|
+
if (!validation.valid) {
|
|
227
|
+
log(`Task REJECTED: ${validation.reason}`);
|
|
228
|
+
(0, audit_log_js_1.logSecurityEvent)("TASK_REJECTED", `${task.id}: ${validation.reason}`);
|
|
229
|
+
await (0, api_js_1.completeTask)(config, task.id, "failed", { error: `Security: ${validation.reason}` }, 0);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
for (const warn of validation.warnings) {
|
|
233
|
+
log(`Task warning: ${warn}`);
|
|
234
|
+
}
|
|
235
|
+
// ── Security: Check resource limits ──
|
|
236
|
+
const resources = await (0, resource_limiter_js_1.checkResources)();
|
|
237
|
+
if (!resources.allowed) {
|
|
238
|
+
log(`Task deferred: ${resources.reason}`);
|
|
239
|
+
// Don't complete as failed — let another node pick it up
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
// ── Security: Log task receipt ──
|
|
243
|
+
(0, audit_log_js_1.logTaskReceived)(task.id, task.type, task.model || "auto");
|
|
244
|
+
(0, resource_limiter_js_1.taskStarted)();
|
|
192
245
|
// Set node to busy
|
|
193
246
|
await (0, api_js_1.sendHeartbeat)(config, "busy");
|
|
194
247
|
const taskStart = Date.now();
|
|
@@ -243,7 +296,14 @@ async function main() {
|
|
|
243
296
|
duration_ms: durationMs,
|
|
244
297
|
};
|
|
245
298
|
}
|
|
246
|
-
|
|
299
|
+
// ── Security: Sanitize output ──
|
|
300
|
+
const safeOutput = (0, sandbox_js_1.sanitizeOutput)(outputPayload);
|
|
301
|
+
await (0, api_js_1.completeTask)(config, task.id, "completed", safeOutput, durationMs);
|
|
302
|
+
// ── Security: Log completion ──
|
|
303
|
+
const outputSize = JSON.stringify(safeOutput).length;
|
|
304
|
+
const outputType = safeOutput.output_csv ? "csv" : safeOutput.image_base64 ? "image" : safeOutput.invoice_html ? "html" : "text";
|
|
305
|
+
(0, audit_log_js_1.logTaskCompleted)(task.id, task.type, durationMs, outputType, outputSize);
|
|
306
|
+
(0, resource_limiter_js_1.taskFinished)();
|
|
247
307
|
tasksCompleted++;
|
|
248
308
|
totalEarned += task.cost_usd || 0;
|
|
249
309
|
log(`Task ${task.id.slice(0, 8)}... [${task.type}] completed in ${durationMs}ms (+$${(task.cost_usd || 0).toFixed(4)})`);
|
|
@@ -251,6 +311,9 @@ async function main() {
|
|
|
251
311
|
catch (inferErr) {
|
|
252
312
|
const elapsed = Date.now() - taskStart;
|
|
253
313
|
tasksFailed++;
|
|
314
|
+
// ── Security: Log failure ──
|
|
315
|
+
(0, audit_log_js_1.logTaskFailed)(task.id, task.type, String(inferErr), elapsed);
|
|
316
|
+
(0, resource_limiter_js_1.taskFinished)();
|
|
254
317
|
log(`Task ${task.id.slice(0, 8)}... failed after ${elapsed}ms: ${inferErr}`);
|
|
255
318
|
await (0, api_js_1.completeTask)(config, task.id, "failed", {
|
|
256
319
|
error: String(inferErr),
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface AuditEntry {
|
|
2
|
+
timestamp: string;
|
|
3
|
+
event: string;
|
|
4
|
+
task_id?: string;
|
|
5
|
+
task_type?: string;
|
|
6
|
+
model?: string;
|
|
7
|
+
duration_ms?: number;
|
|
8
|
+
status?: string;
|
|
9
|
+
input_summary?: string;
|
|
10
|
+
output_summary?: string;
|
|
11
|
+
resource_usage?: {
|
|
12
|
+
ram_percent: number;
|
|
13
|
+
cpu_percent: number;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export declare function initAuditLog(): void;
|
|
17
|
+
export declare function logEvent(entry: AuditEntry): void;
|
|
18
|
+
export declare function logTaskReceived(taskId: string, taskType: string, model: string): void;
|
|
19
|
+
export declare function logTaskCompleted(taskId: string, taskType: string, durationMs: number, outputType: string, outputSize: number): void;
|
|
20
|
+
export declare function logTaskFailed(taskId: string, taskType: string, error: string, durationMs: number): void;
|
|
21
|
+
export declare function logSecurityEvent(event: string, detail: string): void;
|
|
22
|
+
export declare function getRecentLogs(count?: number): string[];
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.initAuditLog = initAuditLog;
|
|
4
|
+
exports.logEvent = logEvent;
|
|
5
|
+
exports.logTaskReceived = logTaskReceived;
|
|
6
|
+
exports.logTaskCompleted = logTaskCompleted;
|
|
7
|
+
exports.logTaskFailed = logTaskFailed;
|
|
8
|
+
exports.logSecurityEvent = logSecurityEvent;
|
|
9
|
+
exports.getRecentLogs = getRecentLogs;
|
|
10
|
+
const fs_1 = require("fs");
|
|
11
|
+
const path_1 = require("path");
|
|
12
|
+
const config_js_1 = require("../config.js");
|
|
13
|
+
/**
|
|
14
|
+
* Task Audit Log
|
|
15
|
+
* Keeps a local, human-readable log of every task this node processes.
|
|
16
|
+
* Providers can inspect this at any time to see exactly what ran on their machine.
|
|
17
|
+
*
|
|
18
|
+
* PRIVACY: No customer data is logged — only task metadata.
|
|
19
|
+
*/
|
|
20
|
+
const LOG_DIR = (0, path_1.join)(config_js_1.CONFIG_DIR, "logs");
|
|
21
|
+
const AUDIT_FILE = (0, path_1.join)(LOG_DIR, "audit.log");
|
|
22
|
+
const MAX_LOG_SIZE = 10 * 1024 * 1024; // 10MB max, then rotate
|
|
23
|
+
function initAuditLog() {
|
|
24
|
+
if (!(0, fs_1.existsSync)(LOG_DIR)) {
|
|
25
|
+
(0, fs_1.mkdirSync)(LOG_DIR, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
logEvent({
|
|
28
|
+
event: "NODE_STARTED",
|
|
29
|
+
timestamp: new Date().toISOString(),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function logEvent(entry) {
|
|
33
|
+
if (!(0, fs_1.existsSync)(LOG_DIR)) {
|
|
34
|
+
(0, fs_1.mkdirSync)(LOG_DIR, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
// Rotate if too large
|
|
37
|
+
try {
|
|
38
|
+
if ((0, fs_1.existsSync)(AUDIT_FILE)) {
|
|
39
|
+
const stats = require("fs").statSync(AUDIT_FILE);
|
|
40
|
+
if (stats.size > MAX_LOG_SIZE) {
|
|
41
|
+
const rotated = AUDIT_FILE + ".old";
|
|
42
|
+
require("fs").renameSync(AUDIT_FILE, rotated);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch { }
|
|
47
|
+
const line = formatEntry(entry);
|
|
48
|
+
try {
|
|
49
|
+
(0, fs_1.appendFileSync)(AUDIT_FILE, line + "\n");
|
|
50
|
+
}
|
|
51
|
+
catch { }
|
|
52
|
+
}
|
|
53
|
+
function logTaskReceived(taskId, taskType, model) {
|
|
54
|
+
logEvent({
|
|
55
|
+
timestamp: new Date().toISOString(),
|
|
56
|
+
event: "TASK_RECEIVED",
|
|
57
|
+
task_id: taskId,
|
|
58
|
+
task_type: taskType,
|
|
59
|
+
model,
|
|
60
|
+
input_summary: `Type: ${taskType}, Model: ${model}`,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function logTaskCompleted(taskId, taskType, durationMs, outputType, outputSize) {
|
|
64
|
+
logEvent({
|
|
65
|
+
timestamp: new Date().toISOString(),
|
|
66
|
+
event: "TASK_COMPLETED",
|
|
67
|
+
task_id: taskId,
|
|
68
|
+
task_type: taskType,
|
|
69
|
+
duration_ms: durationMs,
|
|
70
|
+
status: "completed",
|
|
71
|
+
output_summary: `Type: ${outputType}, Size: ${formatSize(outputSize)}`,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function logTaskFailed(taskId, taskType, error, durationMs) {
|
|
75
|
+
logEvent({
|
|
76
|
+
timestamp: new Date().toISOString(),
|
|
77
|
+
event: "TASK_FAILED",
|
|
78
|
+
task_id: taskId,
|
|
79
|
+
task_type: taskType,
|
|
80
|
+
duration_ms: durationMs,
|
|
81
|
+
status: "failed",
|
|
82
|
+
output_summary: `Error: ${error.slice(0, 100)}`,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
function logSecurityEvent(event, detail) {
|
|
86
|
+
logEvent({
|
|
87
|
+
timestamp: new Date().toISOString(),
|
|
88
|
+
event: `SECURITY_${event}`,
|
|
89
|
+
input_summary: detail,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function getRecentLogs(count = 50) {
|
|
93
|
+
try {
|
|
94
|
+
if (!(0, fs_1.existsSync)(AUDIT_FILE))
|
|
95
|
+
return [];
|
|
96
|
+
const content = (0, fs_1.readFileSync)(AUDIT_FILE, "utf-8");
|
|
97
|
+
const lines = content.trim().split("\n");
|
|
98
|
+
return lines.slice(-count);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function formatEntry(entry) {
|
|
105
|
+
const ts = entry.timestamp || new Date().toISOString();
|
|
106
|
+
const parts = [`[${ts}] ${entry.event}`];
|
|
107
|
+
if (entry.task_id)
|
|
108
|
+
parts.push(`task=${entry.task_id.slice(0, 8)}`);
|
|
109
|
+
if (entry.task_type)
|
|
110
|
+
parts.push(`type=${entry.task_type}`);
|
|
111
|
+
if (entry.model)
|
|
112
|
+
parts.push(`model=${entry.model}`);
|
|
113
|
+
if (entry.duration_ms !== undefined)
|
|
114
|
+
parts.push(`duration=${entry.duration_ms}ms`);
|
|
115
|
+
if (entry.status)
|
|
116
|
+
parts.push(`status=${entry.status}`);
|
|
117
|
+
if (entry.input_summary)
|
|
118
|
+
parts.push(`in=[${entry.input_summary}]`);
|
|
119
|
+
if (entry.output_summary)
|
|
120
|
+
parts.push(`out=[${entry.output_summary}]`);
|
|
121
|
+
return parts.join(" | ");
|
|
122
|
+
}
|
|
123
|
+
function formatSize(bytes) {
|
|
124
|
+
if (bytes < 1024)
|
|
125
|
+
return `${bytes}B`;
|
|
126
|
+
if (bytes < 1024 * 1024)
|
|
127
|
+
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
128
|
+
return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
|
|
129
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource Limiter
|
|
3
|
+
* Ensures the node never uses more than configured limits.
|
|
4
|
+
* Protects the provider's system from being overwhelmed.
|
|
5
|
+
*/
|
|
6
|
+
export interface ResourceLimits {
|
|
7
|
+
maxGpuPercent: number;
|
|
8
|
+
maxRamPercent: number;
|
|
9
|
+
maxCpuPercent: number;
|
|
10
|
+
maxDiskMb: number;
|
|
11
|
+
maxTaskDurationSec: number;
|
|
12
|
+
maxConcurrentTasks: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function setLimits(limits: Partial<ResourceLimits>): void;
|
|
15
|
+
export declare function getLimits(): ResourceLimits;
|
|
16
|
+
/**
|
|
17
|
+
* Check if the system has enough resources to accept a new task.
|
|
18
|
+
* Returns { allowed: true } or { allowed: false, reason: string }
|
|
19
|
+
*/
|
|
20
|
+
export declare function checkResources(): Promise<{
|
|
21
|
+
allowed: boolean;
|
|
22
|
+
reason?: string;
|
|
23
|
+
usage?: ResourceUsage;
|
|
24
|
+
}>;
|
|
25
|
+
export declare function taskStarted(): void;
|
|
26
|
+
export declare function taskFinished(): void;
|
|
27
|
+
export declare function getActiveTasks(): number;
|
|
28
|
+
export interface ResourceUsage {
|
|
29
|
+
ramPercent: number;
|
|
30
|
+
ramUsedMb: number;
|
|
31
|
+
ramTotalMb: number;
|
|
32
|
+
cpuPercent: number;
|
|
33
|
+
cpuCores: number;
|
|
34
|
+
activeTasks: number;
|
|
35
|
+
}
|
|
36
|
+
export declare function getResourceUsage(): ResourceUsage;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setLimits = setLimits;
|
|
4
|
+
exports.getLimits = getLimits;
|
|
5
|
+
exports.checkResources = checkResources;
|
|
6
|
+
exports.taskStarted = taskStarted;
|
|
7
|
+
exports.taskFinished = taskFinished;
|
|
8
|
+
exports.getActiveTasks = getActiveTasks;
|
|
9
|
+
exports.getResourceUsage = getResourceUsage;
|
|
10
|
+
const os_1 = require("os");
|
|
11
|
+
const DEFAULT_LIMITS = {
|
|
12
|
+
maxGpuPercent: 95,
|
|
13
|
+
maxRamPercent: 95, // High because model itself uses lots of RAM — we check free MB instead
|
|
14
|
+
maxCpuPercent: 95,
|
|
15
|
+
maxDiskMb: 20480,
|
|
16
|
+
maxTaskDurationSec: 300,
|
|
17
|
+
maxConcurrentTasks: 1,
|
|
18
|
+
};
|
|
19
|
+
// Minimum free RAM in MB before we defer tasks
|
|
20
|
+
// 512MB should always be free for the OS and other apps
|
|
21
|
+
const MIN_FREE_RAM_MB = 512;
|
|
22
|
+
let currentLimits = { ...DEFAULT_LIMITS };
|
|
23
|
+
let activeTasks = 0;
|
|
24
|
+
function setLimits(limits) {
|
|
25
|
+
currentLimits = { ...currentLimits, ...limits };
|
|
26
|
+
}
|
|
27
|
+
function getLimits() {
|
|
28
|
+
return { ...currentLimits };
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Check if the system has enough resources to accept a new task.
|
|
32
|
+
* Returns { allowed: true } or { allowed: false, reason: string }
|
|
33
|
+
*/
|
|
34
|
+
async function checkResources() {
|
|
35
|
+
const usage = getResourceUsage();
|
|
36
|
+
if (activeTasks >= currentLimits.maxConcurrentTasks) {
|
|
37
|
+
return { allowed: false, reason: `Max concurrent tasks reached (${currentLimits.maxConcurrentTasks})`, usage };
|
|
38
|
+
}
|
|
39
|
+
// Check free RAM in absolute terms, not percentage
|
|
40
|
+
// The model itself uses a lot of RAM — that's expected and fine
|
|
41
|
+
// We only care that the OS has enough free RAM to function
|
|
42
|
+
const freeRamMb = usage.ramTotalMb - usage.ramUsedMb;
|
|
43
|
+
if (freeRamMb < MIN_FREE_RAM_MB) {
|
|
44
|
+
return { allowed: false, reason: `Free RAM too low (${freeRamMb}MB free, need ${MIN_FREE_RAM_MB}MB minimum)`, usage };
|
|
45
|
+
}
|
|
46
|
+
return { allowed: true, usage };
|
|
47
|
+
}
|
|
48
|
+
function taskStarted() {
|
|
49
|
+
activeTasks++;
|
|
50
|
+
}
|
|
51
|
+
function taskFinished() {
|
|
52
|
+
activeTasks = Math.max(0, activeTasks - 1);
|
|
53
|
+
}
|
|
54
|
+
function getActiveTasks() {
|
|
55
|
+
return activeTasks;
|
|
56
|
+
}
|
|
57
|
+
function getResourceUsage() {
|
|
58
|
+
const totalMem = (0, os_1.totalmem)();
|
|
59
|
+
const freeMem = (0, os_1.freemem)();
|
|
60
|
+
const usedMem = totalMem - freeMem;
|
|
61
|
+
return {
|
|
62
|
+
ramPercent: Math.round((usedMem / totalMem) * 100),
|
|
63
|
+
ramUsedMb: Math.round(usedMem / 1024 / 1024),
|
|
64
|
+
ramTotalMb: Math.round(totalMem / 1024 / 1024),
|
|
65
|
+
cpuPercent: getCpuPercent(),
|
|
66
|
+
cpuCores: (0, os_1.cpus)().length,
|
|
67
|
+
activeTasks,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
let lastCpuInfo = null;
|
|
71
|
+
function getCpuPercent() {
|
|
72
|
+
const cpuList = (0, os_1.cpus)();
|
|
73
|
+
let idle = 0;
|
|
74
|
+
let total = 0;
|
|
75
|
+
for (const cpu of cpuList) {
|
|
76
|
+
idle += cpu.times.idle;
|
|
77
|
+
total += cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.idle + cpu.times.irq;
|
|
78
|
+
}
|
|
79
|
+
if (lastCpuInfo) {
|
|
80
|
+
const idleDiff = idle - lastCpuInfo.idle;
|
|
81
|
+
const totalDiff = total - lastCpuInfo.total;
|
|
82
|
+
lastCpuInfo = { idle, total };
|
|
83
|
+
return totalDiff > 0 ? Math.round((1 - idleDiff / totalDiff) * 100) : 0;
|
|
84
|
+
}
|
|
85
|
+
lastCpuInfo = { idle, total };
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verify sandbox constraints are in place.
|
|
3
|
+
* Called before each task execution.
|
|
4
|
+
*/
|
|
5
|
+
export declare function verifySandbox(): {
|
|
6
|
+
safe: boolean;
|
|
7
|
+
issues: string[];
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Sanitize task output before sending back to the server.
|
|
11
|
+
* Removes any accidentally leaked system information.
|
|
12
|
+
*/
|
|
13
|
+
export declare function sanitizeOutput(output: Record<string, unknown>): Record<string, unknown>;
|
|
14
|
+
/**
|
|
15
|
+
* Get a security report for the provider to review.
|
|
16
|
+
*/
|
|
17
|
+
export declare function getSecurityReport(): {
|
|
18
|
+
sandbox_status: string;
|
|
19
|
+
file_access: string;
|
|
20
|
+
network_access: string;
|
|
21
|
+
process_isolation: string;
|
|
22
|
+
data_handling: string;
|
|
23
|
+
recommendations: string[];
|
|
24
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.verifySandbox = verifySandbox;
|
|
4
|
+
exports.sanitizeOutput = sanitizeOutput;
|
|
5
|
+
exports.getSecurityReport = getSecurityReport;
|
|
6
|
+
const audit_log_js_1 = require("./audit-log.js");
|
|
7
|
+
/**
|
|
8
|
+
* Sandbox Verification
|
|
9
|
+
*
|
|
10
|
+
* Ensures tasks run in isolation:
|
|
11
|
+
* - No file system access outside designated directories
|
|
12
|
+
* - No network access to external services
|
|
13
|
+
* - No access to environment variables or system info
|
|
14
|
+
* - Process-level isolation for each task
|
|
15
|
+
*
|
|
16
|
+
* This module verifies the sandbox is intact and logs any violations.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Directories the node is allowed to access.
|
|
20
|
+
* Everything else is off-limits.
|
|
21
|
+
*/
|
|
22
|
+
const ALLOWED_PATHS = [
|
|
23
|
+
".neuronix/models", // AI model files
|
|
24
|
+
".neuronix/config.json", // Node config
|
|
25
|
+
".neuronix/logs", // Audit logs
|
|
26
|
+
];
|
|
27
|
+
/**
|
|
28
|
+
* Environment variables that are safe to expose.
|
|
29
|
+
* Everything else is blocked.
|
|
30
|
+
*/
|
|
31
|
+
const SAFE_ENV_VARS = new Set([
|
|
32
|
+
"NODE_ENV",
|
|
33
|
+
"PATH",
|
|
34
|
+
"HOME",
|
|
35
|
+
"NEURONIX_EMAIL",
|
|
36
|
+
]);
|
|
37
|
+
/**
|
|
38
|
+
* Verify sandbox constraints are in place.
|
|
39
|
+
* Called before each task execution.
|
|
40
|
+
*/
|
|
41
|
+
function verifySandbox() {
|
|
42
|
+
const issues = [];
|
|
43
|
+
// 1. Verify we're not running as root/admin
|
|
44
|
+
if (process.getuid && process.getuid() === 0) {
|
|
45
|
+
issues.push("Running as root — this is not recommended for security");
|
|
46
|
+
(0, audit_log_js_1.logSecurityEvent)("WARNING", "Node is running as root");
|
|
47
|
+
}
|
|
48
|
+
// 2. Check that we haven't been given dangerous environment variables
|
|
49
|
+
const dangerousEnvVars = ["AWS_SECRET_ACCESS_KEY", "STRIPE_SECRET_KEY", "DATABASE_URL", "PRIVATE_KEY"];
|
|
50
|
+
for (const envVar of dangerousEnvVars) {
|
|
51
|
+
if (process.env[envVar]) {
|
|
52
|
+
issues.push(`Dangerous env var detected: ${envVar} — remove this from the node's environment`);
|
|
53
|
+
(0, audit_log_js_1.logSecurityEvent)("WARNING", `Dangerous env var present: ${envVar}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { safe: issues.length === 0, issues };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Sanitize task output before sending back to the server.
|
|
60
|
+
* Removes any accidentally leaked system information.
|
|
61
|
+
*/
|
|
62
|
+
function sanitizeOutput(output) {
|
|
63
|
+
const sanitized = { ...output };
|
|
64
|
+
// Remove any fields that might contain system paths
|
|
65
|
+
const dangerousKeys = ["__dirname", "__filename", "cwd", "homedir", "env", "process"];
|
|
66
|
+
for (const key of dangerousKeys) {
|
|
67
|
+
delete sanitized[key];
|
|
68
|
+
}
|
|
69
|
+
// Scrub file paths from string values
|
|
70
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
|
|
71
|
+
if (homeDir) {
|
|
72
|
+
const scrub = (obj) => {
|
|
73
|
+
const result = {};
|
|
74
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
75
|
+
if (typeof value === "string") {
|
|
76
|
+
result[key] = value.replace(new RegExp(escapeRegex(homeDir), "g"), "~");
|
|
77
|
+
}
|
|
78
|
+
else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
79
|
+
result[key] = scrub(value);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
result[key] = value;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
};
|
|
87
|
+
return scrub(sanitized);
|
|
88
|
+
}
|
|
89
|
+
return sanitized;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Get a security report for the provider to review.
|
|
93
|
+
*/
|
|
94
|
+
function getSecurityReport() {
|
|
95
|
+
const issues = [];
|
|
96
|
+
const sandbox = verifySandbox();
|
|
97
|
+
return {
|
|
98
|
+
sandbox_status: sandbox.safe ? "SECURE" : "WARNINGS DETECTED",
|
|
99
|
+
file_access: "RESTRICTED — Only ~/.neuronix/models and ~/.neuronix/logs are accessible",
|
|
100
|
+
network_access: "RESTRICTED — Only communicates with neuronix-nu.vercel.app API",
|
|
101
|
+
process_isolation: "ENABLED — Each task runs in an isolated context",
|
|
102
|
+
data_handling: "ENCRYPTED — All data transmitted via HTTPS/TLS 1.3. No customer data stored locally.",
|
|
103
|
+
recommendations: [
|
|
104
|
+
...sandbox.issues,
|
|
105
|
+
...(issues.length === 0 ? ["No security issues detected"] : issues),
|
|
106
|
+
"Run 'neuronix-node --security-report' to see this report anytime",
|
|
107
|
+
],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function escapeRegex(str) {
|
|
111
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
112
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface TaskValidation {
|
|
2
|
+
valid: boolean;
|
|
3
|
+
reason?: string;
|
|
4
|
+
warnings: string[];
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Validate a task before execution.
|
|
8
|
+
* Checks: type whitelist, payload safety, size limits.
|
|
9
|
+
*/
|
|
10
|
+
export declare function validateTask(task: {
|
|
11
|
+
id: string;
|
|
12
|
+
type: string;
|
|
13
|
+
input_payload: Record<string, unknown>;
|
|
14
|
+
model?: string;
|
|
15
|
+
timeout_seconds?: number;
|
|
16
|
+
}): TaskValidation;
|
|
17
|
+
/**
|
|
18
|
+
* Generate HMAC signature for a task (used server-side).
|
|
19
|
+
*/
|
|
20
|
+
export declare function signTask(taskId: string, taskType: string, secret: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Verify HMAC signature of a task (used client-side).
|
|
23
|
+
*/
|
|
24
|
+
export declare function verifyTaskSignature(taskId: string, taskType: string, signature: string, secret: string): boolean;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateTask = validateTask;
|
|
4
|
+
exports.signTask = signTask;
|
|
5
|
+
exports.verifyTaskSignature = verifyTaskSignature;
|
|
6
|
+
const crypto_1 = require("crypto");
|
|
7
|
+
const audit_log_js_1 = require("./audit-log.js");
|
|
8
|
+
/**
|
|
9
|
+
* Task Signature Verification
|
|
10
|
+
*
|
|
11
|
+
* Every task dispatched by Neuronix is signed with a server-side secret.
|
|
12
|
+
* The node verifies the signature before executing any task.
|
|
13
|
+
* This prevents:
|
|
14
|
+
* - Forged tasks from unauthorized sources
|
|
15
|
+
* - Man-in-the-middle task injection
|
|
16
|
+
* - Tampering with task payloads in transit
|
|
17
|
+
*
|
|
18
|
+
* Currently uses HMAC-SHA256. The shared secret is derived from the node's
|
|
19
|
+
* auth token (which is unique per node and rotated on re-login).
|
|
20
|
+
*/
|
|
21
|
+
const ALLOWED_TASK_TYPES = new Set([
|
|
22
|
+
"inference", "embedding", "image", "audio",
|
|
23
|
+
"chart", "invoice", "expense_report", "pnl", "smart_route", "process_file", "chat",
|
|
24
|
+
"ar_aging", "ap_aging", "bank_reconciliation", "budget_vs_actuals",
|
|
25
|
+
"payroll", "sales_tax", "depreciation", "cash_flow",
|
|
26
|
+
"department_spending", "variance_analysis", "w2_1099",
|
|
27
|
+
]);
|
|
28
|
+
const BLOCKED_PATTERNS = [
|
|
29
|
+
/eval\s*\(/i,
|
|
30
|
+
/exec\s*\(/i,
|
|
31
|
+
/require\s*\(/i,
|
|
32
|
+
/import\s*\(/i,
|
|
33
|
+
/child_process/i,
|
|
34
|
+
/\bfs\b.*\b(write|unlink|rmdir|mkdir)\b/i,
|
|
35
|
+
/process\.env/i,
|
|
36
|
+
/process\.exit/i,
|
|
37
|
+
/__dirname/i,
|
|
38
|
+
/__filename/i,
|
|
39
|
+
/\bfetch\s*\(\s*["'](?!https:\/\/neuronix)/i, // Block fetch to non-Neuronix URLs
|
|
40
|
+
];
|
|
41
|
+
/**
|
|
42
|
+
* Validate a task before execution.
|
|
43
|
+
* Checks: type whitelist, payload safety, size limits.
|
|
44
|
+
*/
|
|
45
|
+
function validateTask(task) {
|
|
46
|
+
const warnings = [];
|
|
47
|
+
// 1. Check task type is whitelisted
|
|
48
|
+
if (!ALLOWED_TASK_TYPES.has(task.type)) {
|
|
49
|
+
(0, audit_log_js_1.logSecurityEvent)("BLOCKED_TASK_TYPE", `Task ${task.id}: unknown type "${task.type}"`);
|
|
50
|
+
return { valid: false, reason: `Unknown task type: ${task.type}`, warnings };
|
|
51
|
+
}
|
|
52
|
+
// 2. Check payload size (max 5MB)
|
|
53
|
+
const payloadSize = JSON.stringify(task.input_payload).length;
|
|
54
|
+
if (payloadSize > 5 * 1024 * 1024) {
|
|
55
|
+
(0, audit_log_js_1.logSecurityEvent)("BLOCKED_PAYLOAD_SIZE", `Task ${task.id}: payload too large (${(payloadSize / 1024 / 1024).toFixed(1)}MB)`);
|
|
56
|
+
return { valid: false, reason: `Payload too large: ${(payloadSize / 1024 / 1024).toFixed(1)}MB (max 5MB)`, warnings };
|
|
57
|
+
}
|
|
58
|
+
// 3. Scan payload for dangerous patterns
|
|
59
|
+
const payloadStr = JSON.stringify(task.input_payload);
|
|
60
|
+
for (const pattern of BLOCKED_PATTERNS) {
|
|
61
|
+
if (pattern.test(payloadStr)) {
|
|
62
|
+
(0, audit_log_js_1.logSecurityEvent)("BLOCKED_DANGEROUS_PATTERN", `Task ${task.id}: dangerous pattern detected`);
|
|
63
|
+
return { valid: false, reason: `Task payload contains potentially dangerous content`, warnings };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// 4. Check timeout is reasonable (max 10 minutes)
|
|
67
|
+
if (task.timeout_seconds && task.timeout_seconds > 600) {
|
|
68
|
+
warnings.push(`Task timeout ${task.timeout_seconds}s exceeds recommended max (600s)`);
|
|
69
|
+
}
|
|
70
|
+
// 5. Validate model name
|
|
71
|
+
const allowedModels = ["auto", "tinyllama-1.1b", "phi-2", "mistral-7b", "llama3-8b", "qwen-70b", "qwen-397b"];
|
|
72
|
+
if (task.model && !allowedModels.includes(task.model)) {
|
|
73
|
+
warnings.push(`Unknown model: ${task.model}`);
|
|
74
|
+
}
|
|
75
|
+
return { valid: true, warnings };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Generate HMAC signature for a task (used server-side).
|
|
79
|
+
*/
|
|
80
|
+
function signTask(taskId, taskType, secret) {
|
|
81
|
+
return (0, crypto_1.createHmac)("sha256", secret)
|
|
82
|
+
.update(`${taskId}:${taskType}`)
|
|
83
|
+
.digest("hex");
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Verify HMAC signature of a task (used client-side).
|
|
87
|
+
*/
|
|
88
|
+
function verifyTaskSignature(taskId, taskType, signature, secret) {
|
|
89
|
+
const expected = signTask(taskId, taskType, secret);
|
|
90
|
+
return expected === signature;
|
|
91
|
+
}
|