mindforge-cc 10.7.0 → 11.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/.mindforge/MINDFORGE-V2-SCHEMA.json +43 -10
- package/.mindforge/config.json +6 -1
- package/CHANGELOG.md +64 -0
- package/MINDFORGE.md +3 -3
- package/README.md +49 -4
- package/RELEASENOTES.md +80 -0
- package/SECURITY.md +20 -8
- package/bin/autonomous/audit-writer.js +13 -0
- package/bin/autonomous/auto-runner.js +74 -16
- package/bin/autonomous/context-refactorer.js +26 -11
- package/bin/autonomous/state-manager.js +62 -6
- package/bin/autonomous/stuck-monitor.js +46 -7
- package/bin/autonomous/wave-executor.js +66 -25
- package/bin/dashboard/api-router.js +43 -0
- package/bin/dashboard/metrics-aggregator.js +28 -1
- package/bin/dashboard/server.js +67 -4
- package/bin/dashboard/sse-bridge.js +4 -4
- package/bin/engine/feedback-loop.js +8 -0
- package/bin/engine/intelligence-interlock.js +32 -15
- package/bin/engine/logic-drift-detector.js +2 -1
- package/bin/engine/nexus-tracer.js +3 -2
- package/bin/engine/remediation-engine.js +155 -32
- package/bin/engine/self-corrective-synthesizer.js +84 -10
- package/bin/engine/sre-manager.js +12 -4
- package/bin/engine/temporal-hub.js +131 -34
- package/bin/governance/approve.js +41 -5
- package/bin/governance/impact-analyzer.js +28 -0
- package/bin/governance/policy-engine.js +10 -3
- package/bin/governance/quantum-crypto.js +32 -19
- package/bin/governance/rbac-manager.js +74 -2
- package/bin/governance/ztai-manager.js +49 -7
- package/bin/hindsight-injector.js +3 -3
- package/bin/memory/eis-client.js +71 -34
- package/bin/memory/embedding-engine.js +61 -0
- package/bin/memory/knowledge-graph.js +58 -5
- package/bin/memory/knowledge-indexer.js +53 -6
- package/bin/memory/knowledge-store.js +22 -0
- package/bin/migrations/10.7.0-to-11.0.0.js +110 -0
- package/bin/migrations/schema-versions.js +13 -0
- package/bin/models/anthropic-provider.js +45 -0
- package/bin/models/cloud-broker.js +68 -20
- package/bin/models/gemini-provider.js +51 -0
- package/bin/models/model-client.js +20 -0
- package/bin/models/model-router.js +28 -8
- package/bin/models/openai-provider.js +44 -0
- package/bin/utils/file-io.js +63 -1
- package/bin/utils/index.js +58 -0
- package/docs/getting-started.md +1 -1
- package/docs/user-guide.md +2 -2
- package/package.json +2 -2
|
@@ -7,8 +7,59 @@
|
|
|
7
7
|
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
const path = require('path');
|
|
10
|
+
const { atomicWriteJSON } = require('../utils/file-io');
|
|
10
11
|
|
|
11
12
|
const VALID_PHASES = ['idle', 'planning', 'executing', 'verifying', 'complete', 'running', 'paused', 'completed'];
|
|
13
|
+
const VALID_STATUSES = ['idle', 'running', 'paused', 'completed', 'escalated', 'timeout'];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Validates HANDOFF.json structure without blocking on failure (fail-open).
|
|
17
|
+
* Warns on malformed fields but always returns the data for processing.
|
|
18
|
+
* @param {*} data — Parsed HANDOFF.json content
|
|
19
|
+
* @returns {{ valid: boolean, warnings: string[] }}
|
|
20
|
+
*/
|
|
21
|
+
function validateHandoff(data) {
|
|
22
|
+
const warnings = [];
|
|
23
|
+
|
|
24
|
+
if (!data || typeof data !== 'object') {
|
|
25
|
+
warnings.push('HANDOFF.json is not a valid object');
|
|
26
|
+
return { valid: false, warnings };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!data.schema_version) {
|
|
30
|
+
warnings.push('Missing schema_version field');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (data.handoffs && !Array.isArray(data.handoffs)) {
|
|
34
|
+
warnings.push('handoffs field must be an array');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (data.handoffs && Array.isArray(data.handoffs)) {
|
|
38
|
+
for (let i = 0; i < data.handoffs.length; i++) {
|
|
39
|
+
const task = data.handoffs[i];
|
|
40
|
+
if (!task.id) warnings.push(`handoffs[${i}] missing required field: id`);
|
|
41
|
+
if (!task.name) warnings.push(`handoffs[${i}] missing required field: name`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (data.status && !VALID_STATUSES.includes(data.status)) {
|
|
46
|
+
warnings.push(`Invalid status: "${data.status}". Expected one of: ${VALID_STATUSES.join(', ')}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (data.wave_current !== undefined && typeof data.wave_current !== 'number') {
|
|
50
|
+
warnings.push('wave_current must be a number');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (data.tasks_completed !== undefined && typeof data.tasks_completed !== 'number') {
|
|
54
|
+
warnings.push('tasks_completed must be a number');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (data.timestamps && typeof data.timestamps !== 'object') {
|
|
58
|
+
warnings.push('timestamps must be an object');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { valid: warnings.length === 0, warnings };
|
|
62
|
+
}
|
|
12
63
|
|
|
13
64
|
/**
|
|
14
65
|
* Creates a state manager for the given planning directory.
|
|
@@ -45,7 +96,7 @@ function createStateManager(planningDir) {
|
|
|
45
96
|
function updateState(patch) {
|
|
46
97
|
const current = getState();
|
|
47
98
|
const merged = Object.assign(Object.create(null), current, patch);
|
|
48
|
-
|
|
99
|
+
atomicWriteJSON(statePath, merged);
|
|
49
100
|
return merged;
|
|
50
101
|
}
|
|
51
102
|
|
|
@@ -64,7 +115,8 @@ function createStateManager(planningDir) {
|
|
|
64
115
|
}
|
|
65
116
|
|
|
66
117
|
/**
|
|
67
|
-
* Reads and parses HANDOFF.json
|
|
118
|
+
* Reads and parses HANDOFF.json with schema validation (fail-open).
|
|
119
|
+
* Logs warnings for structural issues but always returns data if parseable.
|
|
68
120
|
* @returns {object} Parsed handoff data (fresh object)
|
|
69
121
|
*/
|
|
70
122
|
function readHandoff() {
|
|
@@ -79,8 +131,12 @@ function createStateManager(planningDir) {
|
|
|
79
131
|
throw new Error(`HANDOFF.json is malformed: ${e.message}`);
|
|
80
132
|
}
|
|
81
133
|
|
|
82
|
-
|
|
83
|
-
|
|
134
|
+
// Fail-open schema validation — warn but never block
|
|
135
|
+
const { valid, warnings } = validateHandoff(handoff);
|
|
136
|
+
if (!valid) {
|
|
137
|
+
for (const warning of warnings) {
|
|
138
|
+
console.warn('[STATE] HANDOFF validation:', warning);
|
|
139
|
+
}
|
|
84
140
|
}
|
|
85
141
|
|
|
86
142
|
return handoff;
|
|
@@ -94,7 +150,7 @@ function createStateManager(planningDir) {
|
|
|
94
150
|
const timestamped = Object.assign(Object.create(null), data, {
|
|
95
151
|
last_updated: new Date().toISOString(),
|
|
96
152
|
});
|
|
97
|
-
|
|
153
|
+
atomicWriteJSON(handoffPath, timestamped);
|
|
98
154
|
return timestamped;
|
|
99
155
|
}
|
|
100
156
|
|
|
@@ -113,4 +169,4 @@ function sanitizeState(parsed) {
|
|
|
113
169
|
return clean;
|
|
114
170
|
}
|
|
115
171
|
|
|
116
|
-
module.exports = { createStateManager };
|
|
172
|
+
module.exports = { createStateManager, validateHandoff };
|
|
@@ -96,12 +96,49 @@ class StuckMonitor {
|
|
|
96
96
|
return identical.length >= 3;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
isContentSimilar(a, b) {
|
|
99
|
+
isContentSimilar(a, b, threshold = 10) {
|
|
100
100
|
if (!a || !b) return false;
|
|
101
101
|
if (a === b) return true;
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
102
|
+
|
|
103
|
+
const hashA = this._quickHash(a);
|
|
104
|
+
const hashB = this._quickHash(b);
|
|
105
|
+
if (hashA === hashB) return true;
|
|
106
|
+
|
|
107
|
+
const lenDiff = Math.abs(a.length - b.length);
|
|
108
|
+
if (lenDiff > Math.max(a.length, b.length) * 0.2) return false;
|
|
109
|
+
|
|
110
|
+
const cached = this._getCachedSimilarity(hashA, hashB);
|
|
111
|
+
if (cached !== undefined) return cached;
|
|
112
|
+
|
|
113
|
+
const truncA = a.substring(0, 100);
|
|
114
|
+
const truncB = b.substring(0, 100);
|
|
115
|
+
const result = this.levenshtein(truncA, truncB) <= threshold;
|
|
116
|
+
|
|
117
|
+
this._setCachedSimilarity(hashA, hashB, result);
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
_quickHash(str) {
|
|
122
|
+
let hash = 0;
|
|
123
|
+
for (let i = 0; i < str.length; i++) {
|
|
124
|
+
hash = ((hash << 5) - hash) + str.charCodeAt(i);
|
|
125
|
+
hash = hash & hash;
|
|
126
|
+
}
|
|
127
|
+
return hash;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
_getCachedSimilarity(keyA, keyB) {
|
|
131
|
+
const key = keyA < keyB ? `${keyA}|${keyB}` : `${keyB}|${keyA}`;
|
|
132
|
+
return StuckMonitor._similarityCache.get(key);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
_setCachedSimilarity(keyA, keyB, result) {
|
|
136
|
+
const key = keyA < keyB ? `${keyA}|${keyB}` : `${keyB}|${keyA}`;
|
|
137
|
+
if (StuckMonitor._similarityCache.size >= 200) {
|
|
138
|
+
const firstKey = StuckMonitor._similarityCache.keys().next().value;
|
|
139
|
+
StuckMonitor._similarityCache.delete(firstKey);
|
|
140
|
+
}
|
|
141
|
+
StuckMonitor._similarityCache.set(key, result);
|
|
105
142
|
}
|
|
106
143
|
|
|
107
144
|
levenshtein(a, b) {
|
|
@@ -109,12 +146,14 @@ class StuckMonitor {
|
|
|
109
146
|
for (let i = 0; i <= a.length; i++) { tmp[i] = [i]; }
|
|
110
147
|
for (let j = 0; j <= b.length; j++) { tmp[0][j] = j; }
|
|
111
148
|
for (let i = 1; i <= a.length; i++) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
149
|
+
for (let j = 1; j <= b.length; j++) {
|
|
150
|
+
tmp[i][j] = Math.min(tmp[i - 1][j] + 1, tmp[i][j - 1] + 1, tmp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
151
|
+
}
|
|
115
152
|
}
|
|
116
153
|
return tmp[a.length][b.length];
|
|
117
154
|
}
|
|
118
155
|
}
|
|
119
156
|
|
|
157
|
+
StuckMonitor._similarityCache = new Map();
|
|
158
|
+
|
|
120
159
|
module.exports = StuckMonitor;
|
|
@@ -7,6 +7,35 @@
|
|
|
7
7
|
|
|
8
8
|
const crypto = require('crypto');
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Semaphore for bounding concurrency within a wave.
|
|
12
|
+
* Tasks within a wave are independent — this limits how many run simultaneously.
|
|
13
|
+
*/
|
|
14
|
+
class Semaphore {
|
|
15
|
+
constructor(max) {
|
|
16
|
+
this.max = max;
|
|
17
|
+
this.current = 0;
|
|
18
|
+
this.queue = [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async acquire() {
|
|
22
|
+
if (this.current < this.max) {
|
|
23
|
+
this.current++;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
await new Promise(resolve => this.queue.push(resolve));
|
|
27
|
+
this.current++;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
release() {
|
|
31
|
+
this.current--;
|
|
32
|
+
if (this.queue.length > 0) {
|
|
33
|
+
const next = this.queue.shift();
|
|
34
|
+
next();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
10
39
|
/**
|
|
11
40
|
* Creates a wave executor with the given configuration.
|
|
12
41
|
* @param {object} config
|
|
@@ -73,42 +102,54 @@ function createWaveExecutor(config = {}) {
|
|
|
73
102
|
}
|
|
74
103
|
|
|
75
104
|
/**
|
|
76
|
-
* Executes a single wave — runs tasks
|
|
105
|
+
* Executes a single wave — runs tasks in parallel (bounded by maxConcurrency),
|
|
106
|
+
* skipping already-completed ones.
|
|
77
107
|
* @param {object} wave — A wave object from planWaves
|
|
78
108
|
* @param {object} context — Execution context passed to callbacks
|
|
79
|
-
* @param {
|
|
109
|
+
* @param {function} context.executor — async function(task) => result (performs actual work)
|
|
110
|
+
* @param {number} [context.maxConcurrency=3] — Max parallel tasks within this wave
|
|
80
111
|
* @returns {Promise<{ completed: string[], failed: string[], skipped: string[] }>}
|
|
81
112
|
*/
|
|
82
113
|
async function executeWave(wave, context = {}) {
|
|
83
|
-
const { executor = async () => {} } = context;
|
|
114
|
+
const { executor = async () => {}, maxConcurrency = 3 } = context;
|
|
84
115
|
status = 'running';
|
|
85
116
|
|
|
86
117
|
const pending = wave.tasks.filter(t => !completedTasks.has(t.id));
|
|
87
118
|
const result = { completed: [], failed: [], skipped: [] };
|
|
119
|
+
const semaphore = new Semaphore(maxConcurrency);
|
|
88
120
|
|
|
89
121
|
onWaveStart({ wave: wave.wave, taskCount: pending.length });
|
|
90
122
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
123
|
+
const settled = await Promise.allSettled(
|
|
124
|
+
pending.map(async (task) => {
|
|
125
|
+
await semaphore.acquire();
|
|
126
|
+
const taskStart = Date.now();
|
|
127
|
+
try {
|
|
128
|
+
onTaskStart({ task, wave: wave.wave });
|
|
129
|
+
await executor(task);
|
|
130
|
+
|
|
131
|
+
const duration = Date.now() - taskStart;
|
|
132
|
+
completedTasks = new Set([...completedTasks, task.id]);
|
|
133
|
+
result.completed.push(task.id);
|
|
134
|
+
|
|
135
|
+
onTaskComplete({ task, wave: wave.wave, duration_ms: duration });
|
|
136
|
+
return { task, status: 'fulfilled' };
|
|
137
|
+
} catch (err) {
|
|
138
|
+
const duration = Date.now() - taskStart;
|
|
139
|
+
result.failed.push(task.id);
|
|
140
|
+
|
|
141
|
+
onTaskFail({ task, wave: wave.wave, error: err, duration_ms: duration });
|
|
142
|
+
throw err;
|
|
143
|
+
} finally {
|
|
144
|
+
semaphore.release();
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const failures = settled.filter(r => r.status === 'rejected');
|
|
150
|
+
if (failures.length > 0) {
|
|
151
|
+
const failMsg = failures.map(f => f.reason?.message || 'unknown').join(', ');
|
|
152
|
+
throw new Error(`${failures.length} task(s) failed in wave: ${failMsg}`);
|
|
112
153
|
}
|
|
113
154
|
|
|
114
155
|
currentWaveIndex++;
|
|
@@ -166,4 +207,4 @@ function normalizeTask(h) {
|
|
|
166
207
|
});
|
|
167
208
|
}
|
|
168
209
|
|
|
169
|
-
module.exports = { createWaveExecutor };
|
|
210
|
+
module.exports = { createWaveExecutor, Semaphore };
|
|
@@ -192,6 +192,49 @@ function register(app) {
|
|
|
192
192
|
app.get('/api/connections', (req, res) => {
|
|
193
193
|
res.json({ clients: SSE.getClientCount() });
|
|
194
194
|
});
|
|
195
|
+
|
|
196
|
+
// ── System observability ────────────────────────────────────────────────────
|
|
197
|
+
app.get('/api/v1/system', (req, res) => {
|
|
198
|
+
try {
|
|
199
|
+
const heapUsed = process.memoryUsage().heapUsed;
|
|
200
|
+
const heapTotal = process.memoryUsage().heapTotal;
|
|
201
|
+
const uptime = process.uptime();
|
|
202
|
+
|
|
203
|
+
let auditLines = 0;
|
|
204
|
+
try {
|
|
205
|
+
const auditPath = path.join(process.cwd(), '.planning', 'AUDIT.jsonl');
|
|
206
|
+
if (fs.existsSync(auditPath)) {
|
|
207
|
+
const content = fs.readFileSync(auditPath, 'utf8');
|
|
208
|
+
auditLines = content.split('\n').filter(l => l.trim()).length;
|
|
209
|
+
}
|
|
210
|
+
} catch { /* non-critical */ }
|
|
211
|
+
|
|
212
|
+
let snapshotCount = 0;
|
|
213
|
+
try {
|
|
214
|
+
const historyDir = path.join(process.cwd(), '.planning', 'history');
|
|
215
|
+
if (fs.existsSync(historyDir)) {
|
|
216
|
+
snapshotCount = fs.readdirSync(historyDir).length;
|
|
217
|
+
}
|
|
218
|
+
} catch { /* non-critical */ }
|
|
219
|
+
|
|
220
|
+
const heapHealth = Metrics.checkHeapHealth();
|
|
221
|
+
|
|
222
|
+
res.json({
|
|
223
|
+
heap_used_mb: Math.round(heapUsed / 1024 / 1024 * 100) / 100,
|
|
224
|
+
heap_total_mb: Math.round(heapTotal / 1024 / 1024 * 100) / 100,
|
|
225
|
+
heap_usage_pct: Math.round(heapUsed / heapTotal * 100),
|
|
226
|
+
heap_alert: heapHealth,
|
|
227
|
+
uptime_seconds: Math.round(uptime),
|
|
228
|
+
audit_lines: auditLines,
|
|
229
|
+
snapshot_count: snapshotCount,
|
|
230
|
+
sse_clients: SSE.getClientCount(),
|
|
231
|
+
node_version: process.version,
|
|
232
|
+
timestamp: new Date().toISOString()
|
|
233
|
+
});
|
|
234
|
+
} catch (err) {
|
|
235
|
+
res.status(500).json({ error: err.message });
|
|
236
|
+
}
|
|
237
|
+
});
|
|
195
238
|
}
|
|
196
239
|
|
|
197
240
|
module.exports = { register };
|
|
@@ -326,6 +326,32 @@ function getCosts(windowDays = 7) {
|
|
|
326
326
|
return stats;
|
|
327
327
|
}
|
|
328
328
|
|
|
329
|
+
// ── Heap Health ──────────────────────────────────────────────────────────────
|
|
330
|
+
function checkHeapHealth() {
|
|
331
|
+
const heapUsed = process.memoryUsage().heapUsed;
|
|
332
|
+
const maxHeap = getMaxOldSpaceSize();
|
|
333
|
+
const usagePct = Math.round(heapUsed / maxHeap * 100);
|
|
334
|
+
|
|
335
|
+
let status = 'healthy';
|
|
336
|
+
if (usagePct > 85) {
|
|
337
|
+
status = 'critical';
|
|
338
|
+
} else if (usagePct > 70) {
|
|
339
|
+
status = 'warning';
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return { status, usage_pct: usagePct };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function getMaxOldSpaceSize() {
|
|
346
|
+
// Parse --max-old-space-size from process args, default 1.4GB
|
|
347
|
+
const flag = process.execArgv.find(a => a.startsWith('--max-old-space-size'));
|
|
348
|
+
if (flag) {
|
|
349
|
+
const mb = parseInt(flag.split('=')[1], 10);
|
|
350
|
+
if (mb > 0) return mb * 1024 * 1024;
|
|
351
|
+
}
|
|
352
|
+
return 1.4 * 1024 * 1024 * 1024;
|
|
353
|
+
}
|
|
354
|
+
|
|
329
355
|
module.exports = {
|
|
330
356
|
getStatus,
|
|
331
357
|
getAuditEntries,
|
|
@@ -333,5 +359,6 @@ module.exports = {
|
|
|
333
359
|
getApprovals,
|
|
334
360
|
getTeamActivity,
|
|
335
361
|
getMemory,
|
|
336
|
-
getCosts
|
|
362
|
+
getCosts,
|
|
363
|
+
checkHeapHealth
|
|
337
364
|
};
|
package/bin/dashboard/server.js
CHANGED
|
@@ -42,12 +42,18 @@ const RevOpsAPI = require('./revops-api');
|
|
|
42
42
|
const app = express();
|
|
43
43
|
|
|
44
44
|
// ── Bearer token authentication ──────────────────────────────────────────────
|
|
45
|
-
|
|
45
|
+
let currentToken = crypto.randomBytes(32).toString('hex');
|
|
46
46
|
const TOKEN_FILE = path.join(process.cwd(), '.mindforge', '.dashboard-token');
|
|
47
|
+
const TOKEN_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
48
|
+
let tokenCreatedAt = Date.now();
|
|
49
|
+
|
|
50
|
+
function isTokenExpired() {
|
|
51
|
+
return (Date.now() - tokenCreatedAt) > TOKEN_EXPIRY_MS;
|
|
52
|
+
}
|
|
47
53
|
|
|
48
54
|
// Write token to file with restrictive permissions (owner-only read/write)
|
|
49
55
|
fs.mkdirSync(path.dirname(TOKEN_FILE), { recursive: true });
|
|
50
|
-
fs.writeFileSync(TOKEN_FILE,
|
|
56
|
+
fs.writeFileSync(TOKEN_FILE, currentToken, { mode: 0o600 });
|
|
51
57
|
|
|
52
58
|
/**
|
|
53
59
|
* requireAuth — Validates Bearer token on mutating requests (POST/PUT/DELETE).
|
|
@@ -56,6 +62,11 @@ fs.writeFileSync(TOKEN_FILE, DASHBOARD_TOKEN, { mode: 0o600 });
|
|
|
56
62
|
function requireAuth(req, res, next) {
|
|
57
63
|
if (req.method === 'GET' || req.method === 'OPTIONS') return next();
|
|
58
64
|
|
|
65
|
+
// Check token expiration first
|
|
66
|
+
if (isTokenExpired()) {
|
|
67
|
+
return res.status(401).json({ error: 'token_expired' });
|
|
68
|
+
}
|
|
69
|
+
|
|
59
70
|
const authHeader = req.headers.authorization;
|
|
60
71
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
61
72
|
return res.status(401).json({
|
|
@@ -65,7 +76,9 @@ function requireAuth(req, res, next) {
|
|
|
65
76
|
|
|
66
77
|
const provided = authHeader.slice(7);
|
|
67
78
|
// Constant-time comparison to prevent timing attacks
|
|
68
|
-
|
|
79
|
+
const tokenBuf = Buffer.from(currentToken);
|
|
80
|
+
const providedBuf = Buffer.from(provided);
|
|
81
|
+
if (tokenBuf.length !== providedBuf.length || !crypto.timingSafeEqual(providedBuf, tokenBuf)) {
|
|
69
82
|
return res.status(401).json({
|
|
70
83
|
error: 'Authentication required. Use the token printed at dashboard startup.'
|
|
71
84
|
});
|
|
@@ -74,6 +87,44 @@ function requireAuth(req, res, next) {
|
|
|
74
87
|
next();
|
|
75
88
|
}
|
|
76
89
|
|
|
90
|
+
// ── Rate limiting (100 req/min/IP) ───────────────────────────────────────────
|
|
91
|
+
const rateLimitMap = new Map(); // ip -> { count, resetAt }
|
|
92
|
+
const RATE_LIMIT = 100;
|
|
93
|
+
const RATE_WINDOW_MS = 60000;
|
|
94
|
+
|
|
95
|
+
function rateLimitMiddleware(req, res, next) {
|
|
96
|
+
const ip = req.ip || req.connection.remoteAddress;
|
|
97
|
+
const now = Date.now();
|
|
98
|
+
let entry = rateLimitMap.get(ip);
|
|
99
|
+
|
|
100
|
+
if (!entry || now > entry.resetAt) {
|
|
101
|
+
entry = { count: 0, resetAt: now + RATE_WINDOW_MS };
|
|
102
|
+
rateLimitMap.set(ip, entry);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
entry.count++;
|
|
106
|
+
|
|
107
|
+
if (entry.count > RATE_LIMIT) {
|
|
108
|
+
return res.status(429).json({
|
|
109
|
+
error: 'rate_limit_exceeded',
|
|
110
|
+
retry_after_ms: entry.resetAt - now
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
next();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Periodically clean stale rate-limit entries to prevent memory growth
|
|
118
|
+
const rateLimitCleanupInterval = setInterval(() => {
|
|
119
|
+
const now = Date.now();
|
|
120
|
+
for (const [ip, entry] of rateLimitMap.entries()) {
|
|
121
|
+
if (now > entry.resetAt) {
|
|
122
|
+
rateLimitMap.delete(ip);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}, 60000);
|
|
126
|
+
if (rateLimitCleanupInterval.unref) rateLimitCleanupInterval.unref();
|
|
127
|
+
|
|
77
128
|
// Security middleware
|
|
78
129
|
app.use((req, res, next) => {
|
|
79
130
|
const addr = req.socket.remoteAddress;
|
|
@@ -84,6 +135,9 @@ app.use((req, res, next) => {
|
|
|
84
135
|
next();
|
|
85
136
|
});
|
|
86
137
|
|
|
138
|
+
// ── Rate limiting — applied after localhost check, before auth ────────────────
|
|
139
|
+
app.use(rateLimitMiddleware);
|
|
140
|
+
|
|
87
141
|
// CORS — restrict to dashboard's own origin only (prevent cross-origin attacks)
|
|
88
142
|
const DASHBOARD_ORIGIN = `http://127.0.0.1:${PORT}`;
|
|
89
143
|
app.use((req, res, next) => {
|
|
@@ -124,6 +178,15 @@ app.get('/', (req, res) => {
|
|
|
124
178
|
res.sendFile(FRONTEND);
|
|
125
179
|
});
|
|
126
180
|
|
|
181
|
+
// ── Token refresh endpoint (requires valid existing token) ───────────────────
|
|
182
|
+
app.post('/api/v1/token/refresh', requireAuth, (req, res) => {
|
|
183
|
+
const newToken = crypto.randomBytes(32).toString('hex');
|
|
184
|
+
fs.writeFileSync(TOKEN_FILE, newToken, { mode: 0o600 });
|
|
185
|
+
tokenCreatedAt = Date.now();
|
|
186
|
+
currentToken = newToken;
|
|
187
|
+
res.json({ success: true, token: newToken, expires_in_ms: TOKEN_EXPIRY_MS });
|
|
188
|
+
});
|
|
189
|
+
|
|
127
190
|
// ── Register API routes ───────────────────────────────────────────────────────
|
|
128
191
|
API.register(app);
|
|
129
192
|
app.use('/api/temporal', TemporalAPI);
|
|
@@ -143,7 +206,7 @@ server.listen(PORT, '127.0.0.1', () => {
|
|
|
143
206
|
console.log(` Status: http://localhost:${PORT}/api/status`);
|
|
144
207
|
console.log(` Events: http://localhost:${PORT}/events`);
|
|
145
208
|
console.log(` PID: ${process.pid}`);
|
|
146
|
-
console.log(
|
|
209
|
+
console.log('[Dashboard] Auth token written to token file (not logged for security).');
|
|
147
210
|
console.log(` Token file: ${TOKEN_FILE}`);
|
|
148
211
|
console.log('\n Press CTRL+C to stop\n');
|
|
149
212
|
|
|
@@ -80,14 +80,14 @@ function pollAuditLog() {
|
|
|
80
80
|
const newSize = stat.size;
|
|
81
81
|
const newIno = stat.ino;
|
|
82
82
|
|
|
83
|
-
// File rotation detected: inode changed or
|
|
84
|
-
if (newIno !== _auditInode && _auditInode !== 0) {
|
|
85
|
-
process.stderr.write(`[sse-bridge] AUDIT.jsonl rotation detected (
|
|
83
|
+
// File rotation detected: inode changed or file shrunk (truncated after archival)
|
|
84
|
+
if ((newIno !== _auditInode && _auditInode !== 0) || (newSize < _lastAuditSize)) {
|
|
85
|
+
process.stderr.write(`[sse-bridge] AUDIT.jsonl rotation detected (size: ${_lastAuditSize} -> ${newSize}, ino: ${_auditInode} -> ${newIno})\n`);
|
|
86
86
|
_lastAuditSize = 0;
|
|
87
87
|
}
|
|
88
88
|
_auditInode = newIno;
|
|
89
89
|
|
|
90
|
-
if (newSize <= _lastAuditSize) return;
|
|
90
|
+
if (newSize <= _lastAuditSize) return;
|
|
91
91
|
|
|
92
92
|
// Read only the new bytes appended since last poll
|
|
93
93
|
const fd = fs.openSync(AUDIT_PATH, 'r');
|
|
@@ -98,6 +98,14 @@ class WaveFeedbackLoop {
|
|
|
98
98
|
return { shouldPause: false };
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
recordRemediationOutcome(remediationId, outcome) {
|
|
102
|
+
this.recordPerformance(
|
|
103
|
+
outcome.strategy,
|
|
104
|
+
'remediation',
|
|
105
|
+
outcome.effective
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
101
109
|
reset() {
|
|
102
110
|
this.waveState = { completed: 0, failed: 0, skipped: 0, total: 0 };
|
|
103
111
|
}
|
|
@@ -15,24 +15,41 @@ class IntelligenceInterlock {
|
|
|
15
15
|
this.UPGRADE_THRESHOLD = 0.50; // v6.3.0 Threshold (Recalibrated for IDC readiness)
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
/**
|
|
19
|
-
* Evaluates if a model upgrade is required based on reasoning drift.
|
|
20
|
-
* @param {string} spanId
|
|
21
|
-
* @param {string} thought
|
|
22
|
-
*/
|
|
23
18
|
evaluate(spanId, thought) {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
19
|
+
const driftReport = driftDetector.analyze(spanId, thought);
|
|
20
|
+
const driftScore = driftReport.drift_score;
|
|
21
|
+
|
|
22
|
+
if (driftScore <= this.UPGRADE_THRESHOLD) {
|
|
23
|
+
return { action: 'CONTINUE', drift_score: driftScore };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let tierIncrease;
|
|
27
|
+
if (driftScore > 0.80) {
|
|
28
|
+
tierIncrease = 'MAX';
|
|
29
|
+
} else if (driftScore > 0.65) {
|
|
30
|
+
tierIncrease = 2;
|
|
31
|
+
} else {
|
|
32
|
+
tierIncrease = 1;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
let costWarning = false;
|
|
36
|
+
try {
|
|
37
|
+
const CostTracker = require('../models/cost-tracker');
|
|
38
|
+
const dailySpend = CostTracker.getDailySpend ? CostTracker.getDailySpend() : 0;
|
|
39
|
+
const hardLimit = CostTracker.getHardLimit ? CostTracker.getHardLimit() : Infinity;
|
|
40
|
+
if (dailySpend / hardLimit > 0.8) {
|
|
41
|
+
costWarning = true;
|
|
42
|
+
tierIncrease = Math.min(tierIncrease === 'MAX' ? 3 : tierIncrease, 1);
|
|
43
|
+
}
|
|
44
|
+
} catch { /* cost tracker unavailable */ }
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
action: 'UPGRADE_MIR',
|
|
48
|
+
tier_increase: tierIncrease,
|
|
49
|
+
drift_score: driftScore,
|
|
50
|
+
cost_constrained: costWarning,
|
|
51
|
+
reason: driftReport.markers
|
|
52
|
+
};
|
|
36
53
|
}
|
|
37
54
|
}
|
|
38
55
|
|
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
'use strict';
|
|
9
9
|
|
|
10
10
|
const configManager = require('../governance/config-manager');
|
|
11
|
+
const { LRUMap } = require('../utils/index');
|
|
11
12
|
|
|
12
13
|
class LogicDriftDetector {
|
|
13
14
|
constructor() {
|
|
14
|
-
this.sessionDriftHistory = new
|
|
15
|
+
this.sessionDriftHistory = new LRUMap(500);
|
|
15
16
|
this.DRIFT_THRESHOLD = configManager.get('governance.drift_threshold', 0.75);
|
|
16
17
|
this.CRITICAL_DRIFT_THRESHOLD = configManager.get('governance.critical_drift_threshold', 0.50);
|
|
17
18
|
}
|
|
@@ -16,6 +16,7 @@ const remediationEngine = require('./remediation-engine'); // v6.1 Pillar X
|
|
|
16
16
|
const logicValidator = require('./logic-validator'); // v7 Pillar X
|
|
17
17
|
const vectorHub = require('../memory/vector-hub'); // v8 Pillar XV
|
|
18
18
|
const { AuditWriter } = require('../utils/file-io');
|
|
19
|
+
const { LRUMap } = require('../utils/index');
|
|
19
20
|
|
|
20
21
|
class NexusTracer {
|
|
21
22
|
constructor(config = {}) {
|
|
@@ -29,8 +30,8 @@ class NexusTracer {
|
|
|
29
30
|
this.vhInitialized = false;
|
|
30
31
|
|
|
31
32
|
// v7: Centralized Thresholds
|
|
32
|
-
this.RES_THRESHOLD = configManager.get('governance.res_threshold', 0.8);
|
|
33
|
-
this.entropyCache = new
|
|
33
|
+
this.RES_THRESHOLD = configManager.get('governance.res_threshold', 0.8);
|
|
34
|
+
this.entropyCache = new LRUMap(1000);
|
|
34
35
|
|
|
35
36
|
// v9: Async Audit Writer (replaces sync appendFileSync)
|
|
36
37
|
this._auditWriter = new AuditWriter(this.auditPath);
|