thinknagent 0.1.10 → 0.1.12

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/Readme.md CHANGED
@@ -181,6 +181,17 @@ Role is assigned by the room Owner at approval time and can be changed anytime f
181
181
 
182
182
  ---
183
183
 
184
+ ## Security & Privacy
185
+
186
+ The agent is designed with a "zero-trust" approach to protect your server's credentials, logs, and shell sessions.
187
+
188
+ - 🔒 **Double-Gate Authorization**: New agents connect in `PENDING` mode. They cannot stream metrics or logs until explicitly approved by the room Owner. Upon approval, they receive a signed HMAC `agentToken` for secure reconnection.
189
+ - 🛡️ **Remote Shell Environment Insulation**: Spawning a shell strips all server environment variables, passing only a safe allowlist (`PATH`, `HOME`, `SHELL`, `TERM`, `LANG`). Your database credentials, cloud API keys, and environment secrets remain completely safe.
190
+ - 🚫 **Path Traversal & Sensitive File Blocks**: Log files are verified using absolute path checking. They must belong to allowed directories (`/var/log`, `/home`, `/root`, `/tmp`) and are explicitly blocked if they contain sensitive directories or files like `.ssh/id_rsa`, `.env`, `/etc/passwd`, `/etc/shadow`, etc.
191
+ - 💬 **SSL Transport**: All WebSocket and HTTP traffic between the agent and the server is encrypted using HTTPS/WSS.
192
+
193
+ ---
194
+
184
195
  ## Auth Flow
185
196
  thinknagent init
186
197
  → generates agentId
@@ -241,7 +252,7 @@ pm2 startup
241
252
 
242
253
  **Using systemd:**
243
254
  ```bash
244
- sudo nano /etc/systemd/system/app.service
255
+ sudo nano /etc/systemd/system/thinknagent.service
245
256
  ```
246
257
 
247
258
  ```ini
package/install/setup.sh CHANGED
@@ -1,35 +1,35 @@
1
1
  #!/usr/bin/env bash
2
2
  # ThinkNCollab Agent — installer
3
- # Usage: curl -fsSL https://thinkncollab.com/install-agent.sh | bash -s -- --server https://thinkncollab.com --name my-server
4
-
3
+ # Usage: curl -fsSL https://thinkncollab.com/install-agent.sh | bash -s -- --server https://thinkncollab.com --name my-server --room <roomId>
5
4
  set -euo pipefail
6
5
 
7
6
  SERVER=""
8
7
  NAME=""
8
+ ROOM=""
9
9
  GPU=false
10
10
  LOGS=""
11
11
  SYSTEMD=false
12
12
 
13
- # Parse args
14
13
  while [[ $# -gt 0 ]]; do
15
14
  case $1 in
16
- --server) SERVER="$2"; shift 2 ;;
17
- --name) NAME="$2"; shift 2 ;;
18
- --gpu) GPU=true; shift ;;
19
- --logs) LOGS="$2"; shift 2 ;;
20
- --systemd) SYSTEMD=true; shift ;;
15
+ --server) SERVER="$2"; shift 2 ;;
16
+ --name) NAME="$2"; shift 2 ;;
17
+ --room) ROOM="$2"; shift 2 ;;
18
+ --gpu) GPU=true; shift ;;
19
+ --logs) LOGS="$2"; shift 2 ;;
20
+ --systemd) SYSTEMD=true; shift ;;
21
21
  *) echo "Unknown option: $1"; exit 1 ;;
22
22
  esac
23
23
  done
24
24
 
25
25
  [[ -z "$SERVER" ]] && echo "Error: --server required" && exit 1
26
26
  [[ -z "$NAME" ]] && echo "Error: --name required" && exit 1
27
+ [[ -z "$ROOM" ]] && echo "Error: --room required" && exit 1
27
28
 
28
29
  echo ""
29
30
  echo " ThinkNCollab Agent Installer"
30
31
  echo " ──────────────────────────────"
31
32
 
32
- # Check Node.js >= 18
33
33
  NODE_VER=$(node --version 2>/dev/null | cut -d. -f1 | tr -d 'v' || echo "0")
34
34
  if [[ "$NODE_VER" -lt 18 ]]; then
35
35
  echo " Error: Node.js 18+ required (found: $(node --version 2>/dev/null || echo 'not found'))"
@@ -37,34 +37,45 @@ if [[ "$NODE_VER" -lt 18 ]]; then
37
37
  fi
38
38
  echo " Node.js : $(node --version) ✓"
39
39
 
40
- # Install package
41
40
  echo " Installing thinknagent..."
42
41
  npm install -g thinknagent --silent
43
42
 
44
- # Init
45
43
  GPU_FLAG=""
46
44
  $GPU && GPU_FLAG="--gpu"
45
+
47
46
  LOGS_FLAG=""
48
47
  [[ -n "$LOGS" ]] && LOGS_FLAG="--logs $LOGS"
49
48
 
50
- thinknagent init --server "$SERVER" --name "$NAME" $GPU_FLAG $LOGS_FLAG
49
+ thinknagent init --server "$SERVER" --name "$NAME" --room "$ROOM" $GPU_FLAG $LOGS_FLAG
51
50
 
52
- # Optional systemd setup
53
51
  if $SYSTEMD; then
54
52
  echo ""
55
53
  echo " Setting up systemd service..."
56
-
57
- # Create system user
58
54
  id thinknagent &>/dev/null || useradd --system --no-create-home thinknagent
55
+ AGENT_BIN="$(which thinknagent)"
56
+ cat > /etc/systemd/system/thinknagent.service << SVCEOF
57
+ [Unit]
58
+ Description=ThinkNCollab Agent
59
+ After=network-online.target
60
+ Wants=network-online.target
59
61
 
60
- # Copy service file
61
- AGENT_SERVICE_SRC="$(npm root -g)/thinknagent/install/thinknagent.service"
62
- cp "$AGENT_SERVICE_SRC" /etc/systemd/system/thinknagent.service
62
+ [Service]
63
+ User=thinknagent
64
+ Group=thinknagent
65
+ ExecStart=${AGENT_BIN} start
66
+ Restart=on-failure
67
+ RestartSec=10
68
+ Environment=NODE_ENV=production
69
+ StandardOutput=journal
70
+ StandardError=journal
71
+ SyslogIdentifier=thinknagent
63
72
 
73
+ [Install]
74
+ WantedBy=multi-user.target
75
+ SVCEOF
64
76
  systemctl daemon-reload
65
77
  systemctl enable thinknagent
66
78
  systemctl start thinknagent
67
-
68
79
  echo " systemd service: enabled + started ✓"
69
80
  echo " Logs: journalctl -u thinknagent -f"
70
81
  else
@@ -76,4 +87,4 @@ fi
76
87
 
77
88
  echo ""
78
89
  echo " ✓ Done. Open ThinkNCollab and approve this agent in your room's DevOps Wall."
79
- echo ""
90
+ echo ""
@@ -5,31 +5,21 @@ After=network-online.target
5
5
  Wants=network-online.target
6
6
 
7
7
  [Service]
8
- # Run as a dedicated low-privilege user for security
9
- # Create with: sudo useradd --system --no-create-home thinknagent
10
8
  User=thinknagent
11
9
  Group=thinknagent
12
-
13
- # Point to wherever thinknagent was installed globally
14
- ExecStart=/usr/bin/node /usr/local/bin/thinknagent start
15
-
16
- # Restart policy
10
+ ExecStart=/usr/bin/env thinknagent start
17
11
  Restart=on-failure
18
12
  RestartSec=10
19
13
  StartLimitInterval=60
20
14
  StartLimitBurst=3
21
15
 
22
- # Hardening — limit what the process can do
23
16
  NoNewPrivileges=true
24
17
  ProtectSystem=strict
25
- ProtectHome=read-only # needed to read ~/.thinknagent/config.json
26
- ReadWritePaths=/root/.thinknagent /home/%u/.thinknagent
27
- PrivateTmp=true
18
+ ProtectHome=false
19
+ ReadWritePaths=%h/.thinknagent
28
20
 
29
- # Environment
21
+ PrivateTmp=true
30
22
  Environment=NODE_ENV=production
31
-
32
- # Logging — journald will capture stdout/stderr
33
23
  StandardOutput=journal
34
24
  StandardError=journal
35
25
  SyslogIdentifier=thinknagent
package/lib/agent.js CHANGED
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ const path = require('path');
3
4
  const Connection = require('./connect');
4
5
  const MetricsPoller = require('./metrics');
5
6
  const LogWatcher = require('./logwatcher');
@@ -7,102 +8,140 @@ const AlertEngine = require('./alerts');
7
8
  const ShellBridge = require('./shell');
8
9
  const store = require('./store');
9
10
 
11
+ // allowed base dirs for log streaming
12
+ // room owner agent:logs_updated se bahar ke paths reject ho jayenge
13
+ const LOG_PATH_ALLOWLIST = [
14
+ '/var/log',
15
+ '/home',
16
+ '/root',
17
+ '/tmp',
18
+ ];
19
+
20
+ function isSafeLogPath(logPath) {
21
+ const resolved = path.resolve(logPath);
22
+
23
+ // path traversal check — resolved path allowlist mein hona chahiye
24
+ const allowed = LOG_PATH_ALLOWLIST.some(base => resolved.startsWith(base + path.sep) || resolved === base);
25
+ if (!allowed) {
26
+ console.warn(`[agent] Rejected log path (not in allowlist): ${resolved}`);
27
+ return false;
28
+ }
29
+
30
+ // sensitive files blocklist
31
+ const BLOCKED = [
32
+ '/etc/passwd', '/etc/shadow', '/etc/sudoers',
33
+ '.ssh', '.gnupg', '.aws', '.env',
34
+ 'id_rsa', 'id_ed25519', 'authorized_keys',
35
+ ];
36
+ const blocked = BLOCKED.some(b => resolved.includes(b));
37
+ if (blocked) {
38
+ console.warn(`[agent] Rejected sensitive log path: ${resolved}`);
39
+ return false;
40
+ }
41
+
42
+ return true;
43
+ }
44
+
45
+ function validateRules(rules) {
46
+ if (!Array.isArray(rules)) return [];
47
+ return rules.filter(r =>
48
+ r && typeof r.id === 'string' &&
49
+ typeof r.metric === 'string' &&
50
+ typeof r.op === 'string' &&
51
+ typeof r.value === 'number'
52
+ );
53
+ }
54
+
10
55
  class Agent {
11
56
  constructor() {
12
57
  const cfg = store.read();
13
58
 
14
- this.conn = new Connection({
59
+ this.alerts = new AlertEngine({
60
+ connection: null,
61
+ rules: cfg.alerts || [],
62
+ });
63
+
64
+ this.conn = new Connection({
15
65
  serverUrl: cfg.serverUrl,
16
66
  onReady: ({ role, roomId }) => this._onReady(role, roomId),
17
67
  onDisconnect: ({ reason }) => this._onDisconnect(reason),
18
68
  onRoleUpdate: ({ role }) => this._onRoleUpdate(role),
19
69
  });
20
70
 
21
- this.metrics = new MetricsPoller({
22
- connection: this.conn,
23
- gpu: cfg.gpu || false,
24
- });
71
+ this.alerts.conn = this.conn;
25
72
 
26
- this.logs = new LogWatcher({
27
- connection: this.conn,
28
- logPaths: cfg.logs || [],
73
+ this.metrics = new MetricsPoller({
74
+ connection: this.conn,
75
+ gpu: cfg.gpu || false,
76
+ onMetricsEmit: (payload) => this.alerts.evaluate(payload),
29
77
  });
30
78
 
31
- this.alerts = new AlertEngine({
79
+ this.logs = new LogWatcher({
32
80
  connection: this.conn,
33
- rules: cfg.alerts || [],
81
+ logPaths: (cfg.logs || []).filter(isSafeLogPath),
34
82
  });
35
83
 
36
84
  this.shell = new ShellBridge({ connection: this.conn });
37
-
38
85
  this._active = false;
39
86
  }
40
87
 
41
- start() {
42
- const cfg = store.read();
43
- if (!cfg.serverUrl) {
44
- console.error('[agent] Not initialized.');
45
- process.exit(1);
46
- }
88
+ start() {
89
+ const cfg = store.read();
90
+ if (!cfg.serverUrl) {
91
+ console.error('[agent] Not initialized.');
92
+ process.exit(1);
93
+ }
47
94
 
48
- console.log(`[agent] Connecting to ${cfg.serverUrl}...`);
49
-
50
- // ✅ Pehle connect karo
51
- this.conn.connect();
52
-
53
- // ✅ Socket ready hone ke baad events bind karo
54
- // conn.socket ab available hai kyunki connect() sync mein socket banata hai
55
- this.conn.socket.on('agent:rules_updated', ({ rules }) => {
56
- store.set('alerts', rules);
57
- this.alerts.reloadRules(rules);
58
- });
59
-
60
- this.conn.socket.on('agent:send_metrics_now', () => {
61
- console.log('[agent] Metrics poll requested');
62
- this.metrics.pollNow();
63
- });
64
-
65
- this.conn.socket.on('agent:logs_updated', ({ logs }) => {
66
- store.set('logs', logs);
67
- this.logs.stop();
68
- this.logs = new LogWatcher({ connection: this.conn, logPaths: logs });
69
- if (this._active) this.logs.start(); // ✅ sirf tab start karo agar active ho
70
- });
95
+ console.log(`[agent] Connecting to ${cfg.serverUrl}...`);
96
+ this.conn.connect();
71
97
 
72
- process.on('SIGTERM', () => this._shutdown('SIGTERM'));
73
- process.on('SIGINT', () => this._shutdown('SIGINT'));
74
- }
98
+ this.conn.socket.on('agent:rules_updated', ({ rules }) => {
99
+ const safe = validateRules(rules);
100
+ store.set('alerts', safe);
101
+ this.alerts.reloadRules(safe);
102
+ });
75
103
 
76
- _onReady(role, roomId) {
77
- if (this._active) {
78
- // Reconnect case — metrics/logs already chal rahe hain
79
- console.log(`[agent] Reconnected. Role: ${role}`);
80
- return;
81
- }
104
+ this.conn.socket.on('agent:send_metrics_now', () => {
105
+ console.log('[agent] Metrics poll requested');
106
+ this.metrics.pollNow();
107
+ });
82
108
 
83
- this._active = true;
84
- console.log(`[agent] Active! Role: ${role} | Room: ${roomId}`);
109
+ this.conn.socket.on('agent:logs_updated', ({ logs }) => {
110
+ if (!Array.isArray(logs)) return;
85
111
 
86
- // Metrics emit ko alert engine se connect karo
87
- const origEmit = this.conn.emit.bind(this.conn);
88
- this.conn.emit = (event, data) => {
89
- if (event === 'agent:metrics') this.alerts.evaluate(data);
90
- origEmit(event, data);
91
- };
112
+ // har path validate karo koi bhi unsafe ho toh poori update reject
113
+ const safeLogs = logs.filter(isSafeLogPath);
114
+ if (safeLogs.length !== logs.length) {
115
+ console.warn(`[agent] Dropped ${logs.length - safeLogs.length} unsafe log path(s) from server update`);
116
+ }
92
117
 
93
- this.metrics.start();
94
- this.logs.start();
95
- this.shell.start();
96
- }
97
-
118
+ store.set('logs', safeLogs);
119
+ this.logs.stop();
120
+ this.logs = new LogWatcher({ connection: this.conn, logPaths: safeLogs });
121
+ if (this._active) this.logs.start();
122
+ });
123
+
124
+ process.on('SIGTERM', () => this._shutdown('SIGTERM'));
125
+ process.on('SIGINT', () => this._shutdown('SIGINT'));
126
+ }
127
+
128
+ _onReady(role, roomId) {
129
+ if (this._active) {
130
+ console.log(`[agent] Reconnected. Role: ${role}`);
131
+ return;
132
+ }
133
+ this._active = true;
134
+ console.log(`[agent] Active! Role: ${role} | Room: ${roomId}`);
135
+ this.metrics.start();
136
+ this.logs.start();
137
+ this.shell.start();
138
+ }
98
139
 
99
140
  _onDisconnect(reason) {
100
141
  this.shell.killAll();
101
- // metrics + logs keep their intervals; socket.io will reconnect automatically
102
142
  }
103
143
 
104
144
  _onRoleUpdate({ role }) {
105
- // If role was downgraded to 'monitor', kill open shell sessions
106
145
  if (!this.conn.hasRole('shell')) {
107
146
  this.shell.killAll();
108
147
  }
@@ -118,4 +157,4 @@ _onReady(role, roomId) {
118
157
  }
119
158
  }
120
159
 
121
- module.exports = Agent;
160
+ module.exports = Agent;
package/lib/logwatcher.js CHANGED
@@ -2,17 +2,18 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
+ const chokidar = require('chokidar');
5
6
  const { EventEmitter } = require('events');
6
7
 
7
- const BUFFER_LINES = 100; // lines to send on initial connect
8
- const CHUNK_DELAY = 50; // ms debounce before flushing new lines
8
+ const BUFFER_LINES = 100;
9
+ const CHUNK_DELAY = 50;
9
10
 
10
11
  class LogWatcher extends EventEmitter {
11
12
  constructor({ connection, logPaths = [] }) {
12
13
  super();
13
14
  this.conn = connection;
14
15
  this.logPaths = logPaths;
15
- this._watchers = new Map(); // path → { fd, size, timer }
16
+ this._watchers = new Map(); // path → { watcher, size, timer, lines }
16
17
  }
17
18
 
18
19
  start() {
@@ -27,8 +28,8 @@ class LogWatcher extends EventEmitter {
27
28
  }
28
29
 
29
30
  stop() {
30
- for (const [p, w] of this._watchers) {
31
- try { fs.unwatchFile(p); } catch {}
31
+ for (const [, w] of this._watchers) {
32
+ w.watcher.close();
32
33
  if (w.timer) clearTimeout(w.timer);
33
34
  }
34
35
  this._watchers.clear();
@@ -37,43 +38,51 @@ class LogWatcher extends EventEmitter {
37
38
  _watch(filePath) {
38
39
  const absPath = path.resolve(filePath);
39
40
 
40
- if (!fs.existsSync(absPath)) {
41
- console.warn(`[logs] File not found: ${absPath} — will retry when it appears`);
42
- // retry every 10s in case log file is created later
43
- setTimeout(() => this._watch(filePath), 10000);
44
- return;
45
- }
46
-
47
- const stat = fs.statSync(absPath);
48
41
  const state = {
49
- size: stat.size,
50
- timer: null,
51
- lines: [],
42
+ watcher: null,
43
+ size: fs.existsSync(absPath) ? fs.statSync(absPath).size : 0,
44
+ timer: null,
45
+ lines: [],
52
46
  };
53
47
 
54
- // Send last BUFFER_LINES lines on startup
55
- this._sendTail(absPath);
48
+ // send tail on startup if file exists
49
+ if (fs.existsSync(absPath)) {
50
+ this._sendTail(absPath);
51
+ }
52
+
53
+ const watcher = chokidar.watch(absPath, {
54
+ persistent: true,
55
+ usePolling: false, // inotify on Linux — no polling overhead
56
+ ignoreInitial: true,
57
+ awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 50 },
58
+ });
56
59
 
57
- fs.watchFile(absPath, { interval: 500 }, (curr, prev) => {
58
- if (curr.size < prev.size) {
59
- // Log rotated — reset position
60
+ watcher.on('add', () => {
61
+ // file created after watch started (e.g. log rotation)
62
+ state.size = 0;
63
+ this._sendTail(absPath);
64
+ });
65
+
66
+ watcher.on('change', (fpath, stats) => {
67
+ const newSize = stats ? stats.size : fs.statSync(absPath).size;
68
+
69
+ if (newSize < state.size) {
70
+ // log rotated — reset
60
71
  state.size = 0;
61
72
  }
62
- if (curr.size === prev.size) return;
63
73
 
64
- const newBytes = curr.size - state.size;
74
+ const newBytes = newSize - state.size;
65
75
  if (newBytes <= 0) return;
66
76
 
67
77
  const buf = Buffer.alloc(newBytes);
68
78
  const fd = fs.openSync(absPath, 'r');
69
79
  fs.readSync(fd, buf, 0, newBytes, state.size);
70
80
  fs.closeSync(fd);
71
- state.size = curr.size;
81
+ state.size = newSize;
72
82
 
73
83
  const newLines = buf.toString('utf8').split('\n').filter(Boolean);
74
84
  state.lines.push(...newLines);
75
85
 
76
- // debounce — batch lines before emit
77
86
  clearTimeout(state.timer);
78
87
  state.timer = setTimeout(() => {
79
88
  const toSend = state.lines.splice(0);
@@ -86,6 +95,11 @@ class LogWatcher extends EventEmitter {
86
95
  }, CHUNK_DELAY);
87
96
  });
88
97
 
98
+ watcher.on('error', (err) => {
99
+ console.error(`[logs] Watch error on ${absPath}:`, err.message);
100
+ });
101
+
102
+ state.watcher = watcher;
89
103
  this._watchers.set(absPath, state);
90
104
  }
91
105
 
@@ -96,7 +110,7 @@ class LogWatcher extends EventEmitter {
96
110
  if (lines.length) {
97
111
  this.conn.emit('agent:logs:tail', {
98
112
  file: filePath,
99
- lines: lines.map(l => ({ ts: null, text: l })), // historical = no ts
113
+ lines: lines.map(l => ({ ts: null, text: l })),
100
114
  });
101
115
  }
102
116
  } catch (err) {
@@ -105,4 +119,4 @@ class LogWatcher extends EventEmitter {
105
119
  }
106
120
  }
107
121
 
108
- module.exports = LogWatcher;
122
+ module.exports = LogWatcher;
package/lib/metrics.js CHANGED
@@ -2,18 +2,19 @@
2
2
 
3
3
  const si = require('systeminformation');
4
4
 
5
- const POLL_INTERVAL = 5000; // ms
5
+ const POLL_INTERVAL = 5000;
6
6
 
7
7
  class MetricsPoller {
8
- constructor({ connection, gpu = false }) {
9
- this.conn = connection;
10
- this.gpu = gpu;
11
- this._timer = null;
12
- this._history = []; // last 60 data points (5 min rolling)
8
+ constructor({ connection, gpu = false, onMetricsEmit = null }) {
9
+ this.conn = connection;
10
+ this.gpu = gpu;
11
+ this.onMetricsEmit = onMetricsEmit; // alert engine callback — no monkey-patch needed
12
+ this._timer = null;
13
+ this._history = [];
13
14
  }
14
15
 
15
16
  start() {
16
- this._poll(); // immediate first poll
17
+ this._poll();
17
18
  this._timer = setInterval(() => this._poll(), POLL_INTERVAL);
18
19
  console.log('[metrics] Poller started');
19
20
  }
@@ -22,50 +23,49 @@ class MetricsPoller {
22
23
  if (this._timer) clearInterval(this._timer);
23
24
  console.log('[metrics] Poller stopped');
24
25
  }
26
+
25
27
  pollNow() {
26
- return this._poll();
27
- }
28
+ return this._poll();
29
+ }
28
30
 
29
31
  async _poll() {
30
32
  try {
31
- const [cpu, mem, disk, net, load, procs] = await Promise.all([
33
+ const [cpu, mem, disk, net, procs] = await Promise.all([
32
34
  si.currentLoad(),
33
35
  si.mem(),
34
36
  si.fsSize(),
35
37
  si.networkStats(),
36
- si.currentLoad(), // for load average
37
38
  si.processes(),
38
39
  ]);
39
40
 
40
41
  const payload = {
41
42
  ts: Date.now(),
42
43
  cpu: {
43
- usage: parseFloat(cpu.currentLoad.toFixed(1)),
44
- cores: cpu.cpus?.length ?? 0,
45
- loadAvg: load.avgLoad ?? 0,
44
+ usage: parseFloat(cpu.currentLoad.toFixed(1)),
45
+ cores: cpu.cpus?.length ?? 0,
46
+ loadAvg: cpu.avgLoad ?? 0,
46
47
  },
47
48
  memory: {
48
- total: mem.total,
49
- used: mem.used,
50
- free: mem.free,
51
- usedPct: parseFloat(((mem.used / mem.total) * 100).toFixed(1)),
49
+ total: mem.total,
50
+ used: mem.used,
51
+ free: mem.free,
52
+ usedPct: parseFloat(((mem.used / mem.total) * 100).toFixed(1)),
52
53
  },
53
54
  disk: disk.map(d => ({
54
- fs: d.fs,
55
- mount: d.mount,
56
- size: d.size,
57
- used: d.used,
58
- usedPct: parseFloat(d.use?.toFixed(1) ?? 0),
55
+ fs: d.fs,
56
+ mount: d.mount,
57
+ size: d.size,
58
+ used: d.used,
59
+ usedPct: parseFloat(d.use?.toFixed(1) ?? 0),
59
60
  })),
60
61
  network: net.map(n => ({
61
- iface: n.iface,
62
- rxSec: n.rx_sec ?? 0,
63
- txSec: n.tx_sec ?? 0,
62
+ iface: n.iface,
63
+ rxSec: n.rx_sec ?? 0,
64
+ txSec: n.tx_sec ?? 0,
64
65
  })),
65
66
  processes: {
66
- total: procs.all,
67
+ total: procs.all,
67
68
  running: procs.running,
68
- // top 5 by CPU
69
69
  top: (procs.list || [])
70
70
  .sort((a, b) => b.pcpu - a.pcpu)
71
71
  .slice(0, 5)
@@ -73,33 +73,33 @@ class MetricsPoller {
73
73
  },
74
74
  };
75
75
 
76
- // optional GPU
77
76
  if (this.gpu) {
78
77
  try {
79
78
  const gpuData = await si.graphics();
80
79
  payload.gpu = gpuData.controllers?.map(g => ({
81
- model: g.model,
80
+ model: g.model,
82
81
  utilizationGpu: g.utilizationGpu ?? null,
83
- memUsed: g.memoryUsed ?? null,
84
- memTotal: g.memoryTotal ?? null,
85
- tempC: g.temperatureGpu ?? null,
82
+ memUsed: g.memoryUsed ?? null,
83
+ memTotal: g.memoryTotal ?? null,
84
+ tempC: g.temperatureGpu ?? null,
86
85
  }));
87
86
  } catch {
88
87
  // nvidia-smi not available — silently skip
89
88
  }
90
89
  }
91
90
 
92
- // rolling history (keep last 60 points)
93
91
  this._history.push({ ts: payload.ts, cpu: payload.cpu.usage, mem: payload.memory.usedPct });
94
92
  if (this._history.length > 60) this._history.shift();
95
-
96
93
  payload.history = this._history;
97
94
 
95
+ // pehle emit karo, phir alert engine ko directly pass karo — no interception
98
96
  this.conn.emit('agent:metrics', payload);
97
+ this.onMetricsEmit?.(payload);
98
+
99
99
  } catch (err) {
100
100
  console.error('[metrics] Poll error:', err.message);
101
101
  }
102
102
  }
103
103
  }
104
104
 
105
- module.exports = MetricsPoller;
105
+ module.exports = MetricsPoller;
package/lib/shell.js CHANGED
@@ -4,54 +4,65 @@ let pty;
4
4
  try {
5
5
  pty = require('node-pty');
6
6
  } catch {
7
- pty = null; // node-pty is optional — shell feature disabled if not installed
7
+ pty = null;
8
8
  }
9
9
 
10
- /**
11
- * Shell bridge — opens a PTY session on this server and relays I/O
12
- * bidirectionally over Socket.IO to the DevOps Wall xterm.js terminal.
13
- *
14
- * Role gate: connection.hasRole('shell') must be true.
15
- * If the agent role is 'monitor' only, all shell:open events are rejected.
16
- */
10
+ // explicit allowlist — agent process ke secrets PTY mein nahi jayenge
11
+ const PTY_ENV_ALLOWLIST = new Set([
12
+ 'PATH', 'HOME', 'SHELL', 'TERM', 'LANG', 'LC_ALL', 'LC_CTYPE',
13
+ 'USER', 'LOGNAME', 'HOSTNAME', 'TZ', 'COLORTERM', 'DISPLAY',
14
+ ]);
15
+
16
+ function buildSafeEnv() {
17
+ const safe = {};
18
+ for (const key of PTY_ENV_ALLOWLIST) {
19
+ if (process.env[key] !== undefined) safe[key] = process.env[key];
20
+ }
21
+ safe.TERM = 'xterm-256color';
22
+ safe.THINKNCOLLAB_AGENT = '1';
23
+ return safe;
24
+ }
17
25
 
18
26
  class ShellBridge {
19
27
  constructor({ connection }) {
20
- this.conn = connection;
21
- this._sessions = new Map(); // sessionId → ptyProcess
28
+ this.conn = connection;
29
+ this._sessions = new Map();
22
30
  }
23
31
 
24
32
  start() {
33
+ const s = this.conn.socket;
34
+
25
35
  if (!pty) {
26
36
  console.warn('[shell] node-pty not installed — shell feature disabled');
37
+ s.on('shell:open', ({ sessionId }) => {
38
+ s.emit('shell:error', {
39
+ sessionId,
40
+ reason: 'node-pty is not installed on this agent. Run: npm install -g node-pty',
41
+ });
42
+ });
27
43
  return;
28
44
  }
29
45
 
30
- const s = this.conn.socket;
31
-
32
- // Browser requests a new shell session
33
46
  s.on('shell:open', ({ sessionId, cols = 80, rows = 24 }) => {
34
47
  if (!this.conn.hasRole('shell')) {
35
- s.emit('shell:error', { sessionId, reason: 'Insufficient role — shell access not granted for this agent' });
48
+ s.emit('shell:error', {
49
+ sessionId,
50
+ reason: 'Insufficient role — shell access not granted for this agent',
51
+ });
36
52
  return;
37
53
  }
38
- if (this._sessions.has(sessionId)) return; // already open
54
+
55
+ if (this._sessions.has(sessionId)) return;
39
56
 
40
57
  const proc = pty.spawn(process.env.SHELL || '/bin/bash', [], {
41
58
  name: 'xterm-256color',
42
59
  cols,
43
60
  rows,
44
- cwd: process.env.HOME || '/',
45
- env: {
46
- ...process.env,
47
- TERM: 'xterm-256color',
48
- THINKNCOLLAB_AGENT: '1', // agent can inject this env so shell scripts know
49
- },
61
+ cwd: process.env.HOME || '/',
62
+ env: buildSafeEnv(), // only allowlisted vars — no secrets
50
63
  });
51
64
 
52
- proc.onData(data => {
53
- s.emit('shell:data', { sessionId, data });
54
- });
65
+ proc.onData(data => s.emit('shell:data', { sessionId, data }));
55
66
 
56
67
  proc.onExit(({ exitCode }) => {
57
68
  s.emit('shell:exit', { sessionId, exitCode });
@@ -64,24 +75,19 @@ class ShellBridge {
64
75
  console.log(`[shell] Session ${sessionId} opened (${cols}x${rows})`);
65
76
  });
66
77
 
67
- // Browser → server → agent: user typed something
68
78
  s.on('shell:input', ({ sessionId, data }) => {
69
79
  const proc = this._sessions.get(sessionId);
70
80
  if (!proc) return;
71
81
  proc.write(data);
72
82
  });
73
83
 
74
- // Browser resized terminal
75
84
  s.on('shell:resize', ({ sessionId, cols, rows }) => {
76
85
  const proc = this._sessions.get(sessionId);
77
86
  if (!proc) return;
78
87
  proc.resize(cols, rows);
79
88
  });
80
89
 
81
- // Browser closed terminal tab
82
- s.on('shell:close', ({ sessionId }) => {
83
- this._killSession(sessionId);
84
- });
90
+ s.on('shell:close', ({ sessionId }) => this._killSession(sessionId));
85
91
 
86
92
  console.log('[shell] Bridge ready');
87
93
  }
@@ -95,10 +101,9 @@ class ShellBridge {
95
101
  }
96
102
  }
97
103
 
98
- // Kill all sessions on disconnect
99
104
  killAll() {
100
105
  for (const [id] of this._sessions) this._killSession(id);
101
106
  }
102
107
  }
103
108
 
104
- module.exports = ShellBridge;
109
+ module.exports = ShellBridge;
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "thinknagent",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "ThinkNCollab server agent — metrics, logs, alerts, shell bridge",
5
5
  "main": "lib/agent.js",
6
+ "author": "ThinkNCollab Team <team@thinkncollab.com>",
6
7
  "bin": {
7
8
  "thinknagent": "./bin/thinknagent.js"
8
9
  },