thinknagent 0.1.27 → 0.1.30
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/thinknagent.js +42 -20
- package/lib/alerts.js +14 -2
- package/lib/connect.js +197 -60
- package/lib/e2ee.js +38 -9
- package/lib/metrics.js +28 -6
- package/package.json +1 -1
package/bin/thinknagent.js
CHANGED
|
@@ -27,6 +27,7 @@ program
|
|
|
27
27
|
.option('--retention <days>', 'Disk history retention in days (default: 15)', '15')
|
|
28
28
|
.option('--logs <paths>', 'Comma-separated log file paths to stream')
|
|
29
29
|
.option('--app-path <path>', 'Path to the deployed application folder (to track version)')
|
|
30
|
+
.option('--pin-cert <sha256>', 'Pin server SHA-256 certificate fingerprint for zero-trust MITM defense')
|
|
30
31
|
.option('-f, --force', 'Force overwrite existing registration')
|
|
31
32
|
.option('-d, --daemon', 'Start background auto-restart daemon immediately after init')
|
|
32
33
|
.action(async (opts) => {
|
|
@@ -55,14 +56,15 @@ program
|
|
|
55
56
|
...existing,
|
|
56
57
|
agentId,
|
|
57
58
|
serverUrl,
|
|
58
|
-
name:
|
|
59
|
-
interval:
|
|
60
|
-
retentionDays:
|
|
61
|
-
gpu:
|
|
62
|
-
logs:
|
|
63
|
-
roomId:
|
|
64
|
-
appPath:
|
|
65
|
-
|
|
59
|
+
name: nodeName,
|
|
60
|
+
interval: intervalMs,
|
|
61
|
+
retentionDays: retentionDays,
|
|
62
|
+
gpu: opts.gpu !== undefined ? !!opts.gpu : (existing.gpu || false),
|
|
63
|
+
logs: opts.logs ? opts.logs.split(',').map(s => s.trim()) : (existing.logs || []),
|
|
64
|
+
roomId: opts.room,
|
|
65
|
+
appPath: opts.appPath || existing.appPath || null,
|
|
66
|
+
certFingerprint: opts.pinCert || existing.certFingerprint || null,
|
|
67
|
+
alerts: existing.alerts || [
|
|
66
68
|
{ id: 'cpu-high', metric: 'cpu.usage', op: 'gt', value: 85, for: 60, severity: 'warning' },
|
|
67
69
|
{ id: 'cpu-crit', metric: 'cpu.usage', op: 'gt', value: 95, for: 30, severity: 'critical' },
|
|
68
70
|
{ id: 'mem-high', metric: 'memory.usedPct', op: 'gt', value: 85, for: 60, severity: 'warning' },
|
|
@@ -158,9 +160,9 @@ program
|
|
|
158
160
|
program
|
|
159
161
|
.command('status')
|
|
160
162
|
.description('Show current agent config, approval, and daemon status')
|
|
161
|
-
.action(() => {
|
|
163
|
+
.action(async () => {
|
|
162
164
|
const os = require('os');
|
|
163
|
-
|
|
165
|
+
let cfg = store.read();
|
|
164
166
|
const DaemonManager = require('../lib/daemon');
|
|
165
167
|
const daemon = new DaemonManager();
|
|
166
168
|
const isRunning = daemon.isRunning();
|
|
@@ -171,18 +173,38 @@ program
|
|
|
171
173
|
return;
|
|
172
174
|
}
|
|
173
175
|
|
|
176
|
+
// Dynamic auto-sync: If local store doesn't have token yet, check server for live approval
|
|
177
|
+
if (!cfg.agentToken && cfg.serverUrl && cfg.agentId) {
|
|
178
|
+
try {
|
|
179
|
+
const url = `${cfg.serverUrl}/devops/api/agent/status/${cfg.agentId}`;
|
|
180
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(3000) });
|
|
181
|
+
if (res.ok) {
|
|
182
|
+
const data = await res.json();
|
|
183
|
+
if (data && data.success && data.status === 'approved' && data.token) {
|
|
184
|
+
store.set('agentToken', data.token);
|
|
185
|
+
if (data.role) store.set('role', data.role);
|
|
186
|
+
cfg = store.read();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
} catch (e) {}
|
|
190
|
+
}
|
|
191
|
+
|
|
174
192
|
console.log(chalk.cyan('\n thinknagent status') + chalk.gray(` v${require('../package.json').version}`));
|
|
175
193
|
console.log(chalk.gray(' ─────────────────────────────────────────────'));
|
|
176
|
-
console.log(` Name
|
|
177
|
-
console.log(` Server
|
|
178
|
-
console.log(` Room ID
|
|
179
|
-
console.log(` Agent ID
|
|
180
|
-
console.log(` Role
|
|
181
|
-
console.log(` Auth State
|
|
182
|
-
console.log(`
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
194
|
+
console.log(` Name : ${chalk.white(cfg.name || os.hostname())}`);
|
|
195
|
+
console.log(` Server : ${chalk.white(cfg.serverUrl || 'https://thinkncollab.com')}`);
|
|
196
|
+
console.log(` Room ID : ${chalk.white(cfg.roomId || '—')}`);
|
|
197
|
+
console.log(` Agent ID : ${chalk.white(cfg.agentId || '—')}`);
|
|
198
|
+
console.log(` Role : ${chalk.white(cfg.role || 'monitor')}`);
|
|
199
|
+
console.log(` Auth State : ${cfg.agentToken ? chalk.green('APPROVED (Zero-Knowledge HMAC)') : chalk.yellow('PENDING (Waiting for Owner approval)')}`);
|
|
200
|
+
console.log(` MITM Defense : ${chalk.green('ACTIVE (Anti-Replay Nonce + Strict TLS)')}`);
|
|
201
|
+
if (cfg.certFingerprint) {
|
|
202
|
+
console.log(` Cert Pinning : ${chalk.green('ACTIVE (' + cfg.certFingerprint.slice(0, 16) + '...)')}`);
|
|
203
|
+
}
|
|
204
|
+
console.log(` Daemon : ${isRunning ? chalk.green(`RUNNING (PID ${daemon.getPid()})`) : chalk.gray('STOPPED')}`);
|
|
205
|
+
console.log(` GPU : ${cfg.gpu ? chalk.green('enabled') : chalk.gray('disabled')}`);
|
|
206
|
+
console.log(` Logs : ${(cfg.logs||[]).length ? cfg.logs.join(', ') : chalk.gray('none')}`);
|
|
207
|
+
console.log(` App Path : ${cfg.appPath ? chalk.white(cfg.appPath) : chalk.gray('none')}`);
|
|
186
208
|
console.log(chalk.gray(' ─────────────────────────────────────────────\n'));
|
|
187
209
|
});
|
|
188
210
|
|
package/lib/alerts.js
CHANGED
|
@@ -59,6 +59,17 @@ class AlertEngine {
|
|
|
59
59
|
|
|
60
60
|
_fire(rule, currentValue, metrics = null) {
|
|
61
61
|
console.warn(`[alerts] FIRING: ${rule.id} — ${rule.metric} ${rule.op} ${rule.value} (current: ${currentValue})`);
|
|
62
|
+
const store = require('./store');
|
|
63
|
+
const { encryptE2EE } = require('./e2ee');
|
|
64
|
+
const roomId = this.conn?.roomId || store.get('roomId');
|
|
65
|
+
|
|
66
|
+
const diagObj = {
|
|
67
|
+
diagnostics: metrics?.diagnostics || null,
|
|
68
|
+
topProcesses: metrics?.processes?.top || [],
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const encryptedDiag = roomId ? encryptE2EE(diagObj, roomId) : null;
|
|
72
|
+
|
|
62
73
|
this.conn.emit('agent:alert', {
|
|
63
74
|
id: rule.id,
|
|
64
75
|
metric: rule.metric,
|
|
@@ -68,8 +79,9 @@ class AlertEngine {
|
|
|
68
79
|
severity: rule.severity || 'warning',
|
|
69
80
|
status: 'active',
|
|
70
81
|
firedAt: Date.now(),
|
|
71
|
-
diagnostics: metrics?.diagnostics || null,
|
|
72
|
-
topProcesses: metrics?.processes?.top || [],
|
|
82
|
+
diagnostics: encryptedDiag || metrics?.diagnostics || null,
|
|
83
|
+
topProcesses: encryptedDiag ? [] : (metrics?.processes?.top || []),
|
|
84
|
+
e2ee: !!encryptedDiag
|
|
73
85
|
});
|
|
74
86
|
}
|
|
75
87
|
|
package/lib/connect.js
CHANGED
|
@@ -3,12 +3,21 @@
|
|
|
3
3
|
const { io } = require('socket.io-client');
|
|
4
4
|
const store = require('./store');
|
|
5
5
|
const { execSync } = require('child_process');
|
|
6
|
-
const fs
|
|
7
|
-
const path
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const crypto = require('crypto');
|
|
10
|
+
|
|
11
|
+
let cachedAppVersion = null;
|
|
12
|
+
let lastAppPathChecked = null;
|
|
8
13
|
|
|
9
14
|
function getDeployedAppVersion(appPath) {
|
|
15
|
+
if (cachedAppVersion && lastAppPathChecked === appPath) {
|
|
16
|
+
return cachedAppVersion;
|
|
17
|
+
}
|
|
10
18
|
try {
|
|
11
19
|
const resolvedPath = path.resolve(appPath || process.cwd());
|
|
20
|
+
lastAppPathChecked = appPath;
|
|
12
21
|
let pkgVersion = '';
|
|
13
22
|
let gitCommit = '';
|
|
14
23
|
|
|
@@ -24,26 +33,36 @@ function getDeployedAppVersion(appPath) {
|
|
|
24
33
|
gitCommit = execSync('git log -n 1 --format="%h - %s" --no-color', {
|
|
25
34
|
cwd: resolvedPath,
|
|
26
35
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
27
|
-
timeout:
|
|
36
|
+
timeout: 1500,
|
|
28
37
|
encoding: 'utf8'
|
|
29
38
|
}).trim();
|
|
30
39
|
} catch (e) {}
|
|
31
40
|
|
|
32
41
|
if (pkgVersion && gitCommit) {
|
|
33
|
-
|
|
42
|
+
cachedAppVersion = `v${pkgVersion} (${gitCommit})`;
|
|
34
43
|
} else if (pkgVersion) {
|
|
35
|
-
|
|
44
|
+
cachedAppVersion = `v${pkgVersion}`;
|
|
36
45
|
} else if (gitCommit) {
|
|
37
|
-
|
|
46
|
+
cachedAppVersion = gitCommit;
|
|
47
|
+
} else {
|
|
48
|
+
cachedAppVersion = 'unknown';
|
|
38
49
|
}
|
|
39
|
-
return
|
|
50
|
+
return cachedAppVersion;
|
|
40
51
|
} catch (err) {
|
|
41
|
-
|
|
52
|
+
cachedAppVersion = 'unknown';
|
|
53
|
+
return cachedAppVersion;
|
|
42
54
|
}
|
|
43
55
|
}
|
|
44
56
|
|
|
45
57
|
const NAMESPACE = '/devops';
|
|
46
|
-
const RECONNECT_DELAY =
|
|
58
|
+
const RECONNECT_DELAY = 3000;
|
|
59
|
+
const cachedHostname = os.hostname();
|
|
60
|
+
let cachedPkgVersion = null;
|
|
61
|
+
try {
|
|
62
|
+
cachedPkgVersion = require('../package.json').version;
|
|
63
|
+
} catch (e) {
|
|
64
|
+
cachedPkgVersion = '0.1.28';
|
|
65
|
+
}
|
|
47
66
|
|
|
48
67
|
/**
|
|
49
68
|
* Option B auth flow:
|
|
@@ -51,80 +70,195 @@ const RECONNECT_DELAY = 5000;
|
|
|
51
70
|
* 2. Server creates a PENDING agent entry, notifies Owner in room
|
|
52
71
|
* 3. Owner approves in browser → server sends back a signed agentToken
|
|
53
72
|
* 4. Agent stores token in ~/.thinknagent/config.json (mode 600)
|
|
54
|
-
* 5. All future connections: agent
|
|
73
|
+
* 5. All future connections: agent computes HMAC(agentId:ts) in ~0.05ms → server verifies → ACTIVE
|
|
55
74
|
*/
|
|
56
75
|
|
|
57
76
|
class Connection {
|
|
58
77
|
constructor({ serverUrl, onReady, onDisconnect, onRoleUpdate }) {
|
|
59
|
-
this.serverUrl
|
|
60
|
-
this.onReady
|
|
78
|
+
this.serverUrl = serverUrl;
|
|
79
|
+
this.onReady = onReady; // called when agent is ACTIVE and authed
|
|
61
80
|
this.onDisconnect = onDisconnect;
|
|
62
81
|
this.onRoleUpdate = onRoleUpdate; // called if Owner changes agent permissions
|
|
63
|
-
this.socket
|
|
64
|
-
this.agentId
|
|
65
|
-
this.agentToken
|
|
66
|
-
this.role
|
|
82
|
+
this.socket = null;
|
|
83
|
+
this.agentId = store.get('agentId');
|
|
84
|
+
this.agentToken = store.get('agentToken');
|
|
85
|
+
this.role = store.get('role') || 'monitor'; // monitor | shell | admin
|
|
86
|
+
this._pollTimer = null;
|
|
67
87
|
}
|
|
68
88
|
|
|
69
89
|
connect() {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
auth: (cb) => {
|
|
77
|
-
const freshCfg = store.read();
|
|
78
|
-
console.log('[debug] Reconnecting with token:', !!freshCfg.agentToken, 'agentId:', freshCfg.agentId);
|
|
79
|
-
cb({
|
|
80
|
-
agentId: freshCfg.agentId,
|
|
81
|
-
agentToken: freshCfg.agentToken || null,
|
|
82
|
-
name: freshCfg.name,
|
|
83
|
-
hostname: require('os').hostname(),
|
|
84
|
-
version: require('../package.json').version,
|
|
85
|
-
roomId: freshCfg.roomId || null,
|
|
86
|
-
appVersion: getDeployedAppVersion(freshCfg.appPath),
|
|
87
|
-
});
|
|
90
|
+
const cfg = store.read();
|
|
91
|
+
const appVer = getDeployedAppVersion(cfg.appPath);
|
|
92
|
+
|
|
93
|
+
let normalizedServer = (this.serverUrl || cfg.serverUrl || 'https://thinkncollab.com').trim().replace(/\/$/, '');
|
|
94
|
+
if (!normalizedServer.startsWith('https://') && !normalizedServer.startsWith('http://')) {
|
|
95
|
+
normalizedServer = 'https://' + normalizedServer;
|
|
88
96
|
}
|
|
89
|
-
});
|
|
90
97
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
98
|
+
const isLocal = normalizedServer.includes('localhost') || normalizedServer.includes('127.0.0.1');
|
|
99
|
+
|
|
100
|
+
// MITM Transport Security: Automatically upgrade HTTP to HTTPS for remote servers
|
|
101
|
+
if (!isLocal && normalizedServer.startsWith('http://')) {
|
|
102
|
+
console.warn('[thinknagent:security] Upgrading insecure HTTP to encrypted HTTPS/WSS to prevent MITM interception.');
|
|
103
|
+
normalizedServer = normalizedServer.replace(/^http:\/\//, 'https://');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
this.serverUrl = normalizedServer;
|
|
107
|
+
|
|
108
|
+
this.socket = io(`${this.serverUrl}${NAMESPACE}`, {
|
|
109
|
+
reconnection: true,
|
|
110
|
+
reconnectionDelay: RECONNECT_DELAY,
|
|
111
|
+
reconnectionAttempts: Infinity,
|
|
112
|
+
transports: ['websocket', 'polling'],
|
|
113
|
+
rejectUnauthorized: !cfg.allowSelfSignedCert, // Strict TLS Certificate Authority validation against MITM
|
|
114
|
+
auth: (cb) => {
|
|
115
|
+
const freshCfg = store.read();
|
|
116
|
+
const token = freshCfg.agentToken || this.agentToken;
|
|
117
|
+
const ts = Date.now();
|
|
118
|
+
const nonce = crypto.randomBytes(16).toString('hex'); // 128-bit cryptographic anti-replay nonce
|
|
119
|
+
let signature = null;
|
|
120
|
+
|
|
121
|
+
// Zero-Knowledge HMAC calculation (<0.05ms) with Anti-Replay Nonce
|
|
122
|
+
if (token && freshCfg.agentId) {
|
|
123
|
+
const raw = `${freshCfg.agentId}:${ts}:${nonce}`;
|
|
124
|
+
signature = crypto.createHmac('sha256', token).update(raw).digest('hex');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
cb({
|
|
128
|
+
agentId: freshCfg.agentId,
|
|
129
|
+
ts,
|
|
130
|
+
nonce,
|
|
131
|
+
signature,
|
|
132
|
+
agentToken: null, // Zero-Knowledge: Secret token NEVER sent in plaintext over the wire
|
|
133
|
+
name: freshCfg.name,
|
|
134
|
+
hostname: cachedHostname,
|
|
135
|
+
version: cachedPkgVersion,
|
|
136
|
+
roomId: freshCfg.roomId || null,
|
|
137
|
+
appVersion: appVer,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
this._bind();
|
|
143
|
+
|
|
144
|
+
// If agent is waiting for approval, start background poll to auto-detect web approval
|
|
145
|
+
if (!cfg.agentToken) {
|
|
146
|
+
this._startApprovalPolling();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return this.socket;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
_verifyCertPinning(expectedPin) {
|
|
153
|
+
if (!expectedPin) return;
|
|
154
|
+
try {
|
|
155
|
+
const rawSocket = this.socket?.io?.engine?.transport?.ws?._socket || this.socket?.io?.engine?.transport?.socket;
|
|
156
|
+
if (rawSocket && typeof rawSocket.getPeerCertificate === 'function') {
|
|
157
|
+
const cert = rawSocket.getPeerCertificate();
|
|
158
|
+
if (cert && cert.fingerprint256) {
|
|
159
|
+
const actualPin = cert.fingerprint256.replace(/:/g, '').toLowerCase();
|
|
160
|
+
const cleanExpected = expectedPin.replace(/:/g, '').toLowerCase();
|
|
161
|
+
if (actualPin !== cleanExpected) {
|
|
162
|
+
console.error(`\x1b[31m[thinknagent:security] CRITICAL: MITM ATTACK DETECTED!\x1b[0m`);
|
|
163
|
+
console.error(` Server certificate SHA-256 fingerprint mismatch!`);
|
|
164
|
+
console.error(` Expected: ${cleanExpected}`);
|
|
165
|
+
console.error(` Received: ${actualPin}`);
|
|
166
|
+
this.socket.disconnect();
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} catch (pinErr) {
|
|
172
|
+
console.warn('[thinknagent:security] Fingerprint check skipped:', pinErr.message);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
_startApprovalPolling() {
|
|
177
|
+
if (this._pollTimer) return;
|
|
178
|
+
this._pollTimer = setInterval(async () => {
|
|
179
|
+
const cfg = store.read();
|
|
180
|
+
if (cfg.agentToken || !cfg.agentId) {
|
|
181
|
+
this._stopApprovalPolling();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
const url = `${this.serverUrl}/devops/api/agent/status/${cfg.agentId}`;
|
|
186
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(3000) });
|
|
187
|
+
if (res.ok) {
|
|
188
|
+
const data = await res.json();
|
|
189
|
+
if (data && data.success && data.status === 'approved' && data.token) {
|
|
190
|
+
console.log(`[thinknagent] Approval detected via API! Saving token...`);
|
|
191
|
+
this._handleApproved({
|
|
192
|
+
agentToken: data.token,
|
|
193
|
+
role: data.role || 'monitor',
|
|
194
|
+
roomId: cfg.roomId
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} catch (err) {}
|
|
199
|
+
}, 4000);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
_stopApprovalPolling() {
|
|
203
|
+
if (this._pollTimer) {
|
|
204
|
+
clearInterval(this._pollTimer);
|
|
205
|
+
this._pollTimer = null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
_handleApproved({ agentToken, role, roomId }) {
|
|
210
|
+
this._stopApprovalPolling();
|
|
211
|
+
store.set('agentToken', agentToken);
|
|
212
|
+
store.set('role', role);
|
|
213
|
+
if (roomId) store.set('roomId', roomId);
|
|
214
|
+
|
|
215
|
+
this.agentToken = agentToken;
|
|
216
|
+
this.role = role;
|
|
217
|
+
|
|
218
|
+
console.log(`[thinknagent] Approved! Active session ready. Role: ${role} | Room: ${roomId}`);
|
|
219
|
+
this.onReady?.({ role, roomId });
|
|
220
|
+
}
|
|
94
221
|
|
|
95
222
|
_bind() {
|
|
96
223
|
const s = this.socket;
|
|
97
224
|
|
|
98
|
-
|
|
225
|
+
s.on('connect', () => {
|
|
226
|
+
const cfg = store.read();
|
|
227
|
+
if (cfg.certFingerprint) {
|
|
228
|
+
this._verifyCertPinning(cfg.certFingerprint);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
99
231
|
|
|
100
232
|
// Server says: "I got your registration, waiting for Owner approval"
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
this.role = role;
|
|
113
|
-
|
|
114
|
-
console.log(`[thinknagent] Approved! Active session ready. Role: ${role} | Room: ${roomId}`);
|
|
115
|
-
this.onReady?.({ role, roomId });
|
|
116
|
-
});
|
|
117
|
-
// Server says: "Owner rejected this agent"
|
|
233
|
+
s.on('agent:pending', ({ agentId }) => {
|
|
234
|
+
console.log(`[thinknagent] Registered — waiting for Owner approval...`);
|
|
235
|
+
this._startApprovalPolling();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Owner approved agent via socket broadcast
|
|
239
|
+
s.on('agent:approved', (payload) => {
|
|
240
|
+
this._handleApproved(payload);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Owner explicitly rejected this agent
|
|
118
244
|
s.on('agent:rejected', ({ reason }) => {
|
|
119
|
-
console.error(`[thinknagent] Registration rejected: ${reason}`);
|
|
245
|
+
console.error(`[thinknagent] Registration rejected by owner: ${reason}`);
|
|
246
|
+
this._stopApprovalPolling();
|
|
120
247
|
store.clear();
|
|
121
248
|
process.exit(1);
|
|
122
249
|
});
|
|
123
250
|
|
|
251
|
+
// Auth verification failed (do NOT wipe store, could be temporary drift or network error)
|
|
252
|
+
s.on('agent:auth_failed', ({ reason }) => {
|
|
253
|
+
console.warn(`[thinknagent] Auth challenge warning: ${reason}. Retrying...`);
|
|
254
|
+
this._startApprovalPolling();
|
|
255
|
+
});
|
|
256
|
+
|
|
124
257
|
// Already approved on previous run — server confirms active session
|
|
125
258
|
s.on('agent:active', ({ role, roomId }) => {
|
|
259
|
+
this._stopApprovalPolling();
|
|
126
260
|
this.role = role;
|
|
127
|
-
console.log(`[thinknagent]
|
|
261
|
+
console.log(`[thinknagent] Active session confirmed. Role: ${role} | Room: ${roomId}`);
|
|
128
262
|
this.onReady?.({ role, roomId });
|
|
129
263
|
});
|
|
130
264
|
|
|
@@ -139,6 +273,7 @@ s.on('agent:approved', ({ agentToken, role, roomId }) => {
|
|
|
139
273
|
// Owner revoked this agent
|
|
140
274
|
s.on('agent:revoked', () => {
|
|
141
275
|
console.warn('[thinknagent] Agent revoked by Owner. Clearing credentials.');
|
|
276
|
+
this._stopApprovalPolling();
|
|
142
277
|
store.clear();
|
|
143
278
|
process.exit(0);
|
|
144
279
|
});
|
|
@@ -147,15 +282,16 @@ s.on('agent:approved', ({ agentToken, role, roomId }) => {
|
|
|
147
282
|
s.on('agent:auth_challenge', ({ challenge, ts }) => {
|
|
148
283
|
const cfg = store.read();
|
|
149
284
|
const token = cfg.agentToken || this.agentToken;
|
|
150
|
-
const crypto = require('crypto');
|
|
151
285
|
if (!token) return;
|
|
152
286
|
|
|
153
|
-
const
|
|
287
|
+
const clientNonce = crypto.randomBytes(16).toString('hex');
|
|
288
|
+
const raw = `${challenge}:${ts}:${cfg.agentId}:${clientNonce}`;
|
|
154
289
|
const signature = crypto.createHmac('sha256', token).update(raw).digest('hex');
|
|
155
290
|
|
|
156
291
|
s.emit('agent:auth_challenge_response', {
|
|
157
292
|
challenge,
|
|
158
293
|
ts,
|
|
294
|
+
clientNonce,
|
|
159
295
|
signature,
|
|
160
296
|
agentId: cfg.agentId
|
|
161
297
|
});
|
|
@@ -190,3 +326,4 @@ s.on('agent:approved', ({ agentToken, role, roomId }) => {
|
|
|
190
326
|
|
|
191
327
|
Connection.getDeployedAppVersion = getDeployedAppVersion;
|
|
192
328
|
module.exports = Connection;
|
|
329
|
+
|
package/lib/e2ee.js
CHANGED
|
@@ -12,18 +12,17 @@ const crypto = require("crypto");
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
function deriveKeySync(roomId, secretSeed) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const salt = Buffer.from('thinkncollab-e2ee-agent-salt-v2', 'utf8');
|
|
19
|
-
// 100,000 iterations — OWASP recommended minimum for PBKDF2-SHA256
|
|
20
|
-
return crypto.pbkdf2Sync(password, salt, 100000, 32, 'sha256');
|
|
15
|
+
const seed = secretSeed || roomId;
|
|
16
|
+
if (!seed) return null;
|
|
17
|
+
return crypto.createHash('sha256').update(seed, 'utf8').digest();
|
|
21
18
|
}
|
|
22
19
|
|
|
23
|
-
function encryptE2EE(
|
|
24
|
-
if (!
|
|
20
|
+
function encryptE2EE(payload, roomId, secretSeed) {
|
|
21
|
+
if (!payload) return payload;
|
|
22
|
+
const plaintext = typeof payload === 'object' ? JSON.stringify(payload) : String(payload);
|
|
25
23
|
try {
|
|
26
24
|
const key = deriveKeySync(roomId, secretSeed);
|
|
25
|
+
if (!key) return plaintext;
|
|
27
26
|
const iv = crypto.randomBytes(12); // 12-byte random IV
|
|
28
27
|
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
|
29
28
|
|
|
@@ -41,5 +40,35 @@ function encryptE2EE(plaintext, roomId, secretSeed) {
|
|
|
41
40
|
}
|
|
42
41
|
}
|
|
43
42
|
|
|
44
|
-
|
|
43
|
+
function decryptE2EE(ciphertextEnvelope, roomId, secretSeed) {
|
|
44
|
+
if (!ciphertextEnvelope || typeof ciphertextEnvelope !== 'string') return ciphertextEnvelope;
|
|
45
|
+
if (!ciphertextEnvelope.startsWith('e2ee:')) return ciphertextEnvelope;
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const key = deriveKeySync(roomId, secretSeed);
|
|
49
|
+
if (!key) return ciphertextEnvelope;
|
|
50
|
+
|
|
51
|
+
const b64 = ciphertextEnvelope.slice(5);
|
|
52
|
+
const bytes = Buffer.from(b64, 'base64');
|
|
53
|
+
if (bytes.length < 28) return ciphertextEnvelope;
|
|
54
|
+
|
|
55
|
+
const iv = bytes.slice(0, 12);
|
|
56
|
+
const tag = bytes.slice(12, 28);
|
|
57
|
+
const ciphertext = bytes.slice(28);
|
|
58
|
+
|
|
59
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
|
60
|
+
decipher.setAuthTag(tag);
|
|
61
|
+
|
|
62
|
+
const decrypted = Buffer.concat([
|
|
63
|
+
decipher.update(ciphertext),
|
|
64
|
+
decipher.final()
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
return decrypted.toString('utf8');
|
|
68
|
+
} catch (err) {
|
|
69
|
+
return ciphertextEnvelope;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = { encryptE2EE, decryptE2EE, deriveKeySync };
|
|
45
74
|
|
package/lib/metrics.js
CHANGED
|
@@ -97,10 +97,16 @@ class MetricsPoller {
|
|
|
97
97
|
processes: {
|
|
98
98
|
total: procs.all,
|
|
99
99
|
running: procs.running,
|
|
100
|
-
top: (
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
.
|
|
100
|
+
top: (() => {
|
|
101
|
+
const list = procs.list || [];
|
|
102
|
+
// Filter out idle Linux kernel threads (PID 2, kthreadd, rcu, pool_workqueue, etc. which consume 0 user memory and 0 CPU)
|
|
103
|
+
const nonKernel = list.filter(p => p && p.pid > 1 && (p.pmem > 0 || p.pcpu > 0) && p.name !== 'kthreadd' && !p.name.startsWith('R-') && !p.name.startsWith('kworker') && p.name !== 'pool_workqueue_release');
|
|
104
|
+
const candidates = nonKernel.length > 0 ? nonKernel : list.filter(p => p && p.pid > 0);
|
|
105
|
+
return candidates
|
|
106
|
+
.sort((a, b) => ((b.pcpu || 0) - (a.pcpu || 0)) || ((b.pmem || 0) - (a.pmem || 0)))
|
|
107
|
+
.slice(0, 5)
|
|
108
|
+
.map(p => ({ pid: p.pid, name: p.name, cpu: p.pcpu || 0, mem: p.pmem || 0 }));
|
|
109
|
+
})(),
|
|
104
110
|
},
|
|
105
111
|
apm: {
|
|
106
112
|
nodeName: require('os').hostname(),
|
|
@@ -227,10 +233,26 @@ class MetricsPoller {
|
|
|
227
233
|
if (this._history.length > 60) this._history.shift();
|
|
228
234
|
payload.history = this._history;
|
|
229
235
|
|
|
230
|
-
// pehle
|
|
231
|
-
this.conn.emit('agent:metrics', payload);
|
|
236
|
+
// pehle alert engine aur history ko raw metrics pass karo — 24/7 Edge evaluation
|
|
232
237
|
this.onMetricsEmit?.(payload);
|
|
233
238
|
|
|
239
|
+
// MITM / Zero-Knowledge Defense: Encrypt metrics before sending over socket
|
|
240
|
+
const store = require('./store');
|
|
241
|
+
const { encryptE2EE } = require('./e2ee');
|
|
242
|
+
const roomId = this.conn?.roomId || store.get('roomId');
|
|
243
|
+
|
|
244
|
+
if (roomId) {
|
|
245
|
+
const encrypted = encryptE2EE(payload, roomId);
|
|
246
|
+
this.conn.emit('agent:metrics', {
|
|
247
|
+
agentId: this.conn.agentId || store.get('agentId'),
|
|
248
|
+
ts: payload.ts,
|
|
249
|
+
encrypted,
|
|
250
|
+
e2ee: true
|
|
251
|
+
});
|
|
252
|
+
} else {
|
|
253
|
+
this.conn.emit('agent:metrics', payload);
|
|
254
|
+
}
|
|
255
|
+
|
|
234
256
|
} catch (err) {
|
|
235
257
|
console.error('[metrics] Poll error:', err.message);
|
|
236
258
|
} finally {
|
package/package.json
CHANGED