nyxora 1.6.2 → 1.6.4

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +22 -12
  3. package/SECURITY.md +25 -21
  4. package/assets/raw-diagram.png +0 -0
  5. package/assets/security-flow.png +0 -0
  6. package/bin/nyxora.mjs +236 -0
  7. package/launcher.js +8 -3
  8. package/launcher.ts +28 -1
  9. package/package.json +11 -8
  10. package/packages/core/package.json +4 -4
  11. package/packages/core/src/agent/reasoning.ts +10 -8
  12. package/packages/core/src/config/parser.ts +2 -1
  13. package/packages/core/src/gateway/cli.ts +2 -64
  14. package/packages/core/src/gateway/server.ts +89 -8
  15. package/packages/core/src/gateway/setup-cli.ts +7 -0
  16. package/packages/core/src/gateway/setup.ts +52 -28
  17. package/packages/core/src/gateway/telegram.ts +147 -89
  18. package/packages/core/src/memory/logger.ts +83 -20
  19. package/packages/core/src/system/pluginManager.ts +48 -34
  20. package/packages/core/src/utils/state.ts +15 -2
  21. package/packages/core/src/web3/config.ts +18 -3
  22. package/packages/core/src/web3/skills/marketAnalysis.ts +43 -17
  23. package/packages/core/src/web3/skills/swapToken.ts +9 -1
  24. package/packages/dashboard/dist/assets/index-CfIids2e.js +170 -0
  25. package/packages/dashboard/dist/assets/index-POJM-7Fd.css +1 -0
  26. package/packages/dashboard/dist/favicon.svg +1 -1
  27. package/packages/dashboard/dist/index.html +2 -2
  28. package/packages/dashboard/package.json +7 -7
  29. package/packages/dashboard/public/favicon.svg +1 -1
  30. package/packages/dashboard/src/App.tsx +224 -167
  31. package/packages/dashboard/src/Settings.tsx +55 -0
  32. package/packages/dashboard/src/Skills.tsx +8 -1
  33. package/packages/dashboard/src/index.css +146 -35
  34. package/packages/policy/package.json +1 -1
  35. package/packages/policy/src/server.ts +21 -28
  36. package/packages/signer/package.json +1 -1
  37. package/packages/signer/src/server.ts +40 -13
  38. package/test-db.ts +3 -0
  39. package/bin/nyxora.js +0 -13
  40. package/packages/dashboard/dist/assets/index-BK4qmIy6.js +0 -200
  41. package/packages/dashboard/dist/assets/index-C1m4ohce.css +0 -1
  42. package/packages/dashboard/package-lock.json +0 -2748
  43. package/packages/dashboard/src/Memory.tsx +0 -110
@@ -1,6 +1,6 @@
1
- import TelegramBot from 'node-telegram-bot-api';
1
+ import { Telegraf, Markup } from 'telegraf';
2
2
  import { processUserInput, logger } from '../agent/reasoning';
3
- import { loadConfig } from '../config/parser';
3
+ import { loadConfig, saveConfig } from '../config/parser';
4
4
  import { txManager } from '../agent/transactionManager';
5
5
  import { executeTransfer } from '../web3/skills/transfer';
6
6
  import { executeSwap } from '../web3/skills/swapToken';
@@ -14,143 +14,201 @@ export function startTelegramBot() {
14
14
  const config = loadConfig();
15
15
  const token = config.integrations?.telegram?.bot_token;
16
16
 
17
-
18
17
  if (!token) {
19
18
  console.log('[Telegram] No TELEGRAM_BOT_TOKEN found in config.yaml. Bot is disabled.');
20
19
  return;
21
20
  }
22
21
 
23
22
  try {
24
- const bot = new TelegramBot(token, { polling: true });
25
-
26
- bot.on('message', async (msg) => {
27
- const chatId = msg.chat.id;
28
- const text = msg.text;
29
-
30
- if (!text) return;
23
+ const bot = new Telegraf(token);
24
+
25
+ // Pairing state variables
26
+ const isPaired = !!config.integrations?.telegram?.authorized_chat_id;
27
+ let generatedPin = '';
28
+ let pinExpiry = 0;
29
+
30
+ if (!isPaired) {
31
+ generatedPin = Math.floor(100000 + Math.random() * 900000).toString();
32
+ pinExpiry = Date.now() + 5 * 60 * 1000; // 5 minutes TTL
33
+
34
+ console.log(pc.yellow('\n==================================================='));
35
+ console.log(pc.yellow('🔐 OTORISASI BOT TELEGRAM DIBUTUHKAN'));
36
+ console.log(pc.yellow('==================================================='));
37
+ console.log('Bot Telegram Anda saat ini terkunci demi keamanan.');
38
+ console.log('Buka Telegram Anda, dan kirimkan perintah berikut ke bot Anda:\n');
39
+ console.log(pc.cyan(` /auth ${generatedPin}\n`));
40
+ console.log(pc.gray('(Kode OTP ini akan kedaluwarsa dalam 5 menit)\n'));
41
+ console.log('⏳ Menunggu pesan masuk...');
42
+ }
43
+
44
+ // Security Middleware (OTP & Authorization)
45
+ bot.use(async (ctx, next) => {
46
+ const currentConfig = loadConfig();
47
+ const authId = currentConfig.integrations?.telegram?.authorized_chat_id;
48
+
49
+ if (authId) {
50
+ // Paired mode: Reject unauthorized users silently
51
+ if (ctx.chat?.id !== authId) {
52
+ return;
53
+ }
54
+ return next();
55
+ }
31
56
 
32
- if (text === '/clear') {
33
- logger.clear();
34
- bot.sendMessage(chatId, '✅ Memori AI telah dihapus. Mari kita mulai obrolan baru!');
35
- return;
57
+ // Pairing mode: Listen for /auth command
58
+ if (ctx.message && 'text' in ctx.message) {
59
+ const text = ctx.message.text || '';
60
+ if (text.startsWith('/auth ')) {
61
+ const pin = text.split(' ')[1];
62
+ if (Date.now() > pinExpiry) {
63
+ await ctx.reply('❌ The pairing PIN has expired. Please restart the CLI to generate a new one.');
64
+ return;
65
+ }
66
+ if (pin === generatedPin) {
67
+ // Success
68
+ if (!currentConfig.integrations) currentConfig.integrations = {};
69
+ if (!currentConfig.integrations.telegram) currentConfig.integrations.telegram = { enabled: true, bot_token: token };
70
+
71
+ currentConfig.integrations.telegram.authorized_chat_id = ctx.chat?.id;
72
+ saveConfig(currentConfig);
73
+
74
+ await ctx.reply('✅ Otorisasi Berhasil! Agen Nyxora kini hanya akan mematuhi perintah Anda. Koneksi diamankan.');
75
+ console.log(pc.green(`\n[Telegram] Successfully paired with Chat ID: ${ctx.chat?.id}`));
76
+ return; // Done parsing auth, ignore this specific message for further logic
77
+ } else {
78
+ await ctx.reply('❌ PIN salah.');
79
+ return;
80
+ }
81
+ }
36
82
  }
83
+
84
+ // If not paired and not an auth command, silently drop
85
+ return;
86
+ });
37
87
 
38
- // Log incoming message
39
- console.log(`[Telegram] Received from ${msg.from?.first_name}: ${text}`);
88
+ bot.command('clear', async (ctx) => {
89
+ logger.clear();
90
+ await ctx.reply('✅ Memori AI telah dihapus. Mari kita mulai obrolan baru!');
91
+ });
92
+
93
+ bot.on('text', async (ctx) => {
94
+ const text = ctx.message.text;
95
+ if (text.startsWith('/')) return; // Ignore other commands
40
96
 
41
- // Send typing action to Telegram
42
- bot.sendChatAction(chatId, 'typing');
97
+ console.log(`[Telegram] Received from ${ctx.from?.first_name || 'User'}: ${text}`);
98
+
99
+ // Send typing action
100
+ await ctx.sendChatAction('typing');
43
101
 
44
102
  try {
45
103
  let progressMsgId: number | null = null;
46
104
  const onProgress = async (progressText: string) => {
47
105
  try {
48
106
  if (!progressMsgId) {
49
- const sent = await bot.sendMessage(chatId, progressText, { parse_mode: 'Markdown' });
107
+ const sent = await ctx.reply(progressText, { parse_mode: 'Markdown' });
50
108
  progressMsgId = sent.message_id;
51
109
  } else {
52
- await bot.editMessageText(progressText, { chat_id: chatId, message_id: progressMsgId, parse_mode: 'Markdown' });
110
+ await ctx.telegram.editMessageText(ctx.chat.id, progressMsgId, undefined, progressText, { parse_mode: 'Markdown' });
53
111
  }
54
112
  } catch (e) {}
55
113
  };
56
114
 
57
- // Feed the message to the AI agent
58
115
  const response = await processUserInput(text, 'user', onProgress);
59
-
116
+
60
117
  if (progressMsgId) {
61
- // Clean up the progress message
62
- bot.deleteMessage(chatId, progressMsgId).catch(() => {});
118
+ await ctx.telegram.deleteMessage(ctx.chat.id, progressMsgId).catch(() => {});
63
119
  }
64
-
65
- // Send the AI's response back to Telegram
66
-
67
- // Check for newly created pending transactions
120
+
68
121
  const pendingTxs = txManager.getPending();
69
122
  if (pendingTxs.length > 0) {
70
123
  const latestTx = pendingTxs[pendingTxs.length - 1];
71
- // If the transaction was created within the last 2 minutes, show the inline keyboard
72
124
  if (Date.now() - latestTx.createdAt < 120000) {
73
- bot.sendMessage(chatId, response, {
74
- reply_markup: {
75
- inline_keyboard: [[
76
- { text: ' Approve', callback_data: `approve_${latestTx.id}` },
77
- { text: '❌ Reject', callback_data: `reject_${latestTx.id}` }
78
- ]]
79
- }
80
- });
125
+ await ctx.reply(response, Markup.inlineKeyboard([
126
+ [
127
+ Markup.button.callback('✅ Approve', `approve_${latestTx.id}`),
128
+ Markup.button.callback(' Reject', `reject_${latestTx.id}`)
129
+ ]
130
+ ]));
81
131
  return;
82
132
  }
83
133
  }
84
-
85
- bot.sendMessage(chatId, response);
134
+
135
+ await ctx.reply(response);
86
136
  } catch (error: any) {
87
137
  console.error('[Telegram] Error processing message:', error);
88
- bot.sendMessage(chatId, '❌ Sorry, I encountered an error while processing your message.');
138
+ await ctx.reply('❌ Sorry, I encountered an error while processing your message.');
89
139
  }
90
140
  });
91
141
 
92
- bot.on('callback_query', async (query) => {
93
- const chatId = query.message?.chat.id;
94
- if (!chatId || !query.data) return;
95
-
96
- const [action, txId] = query.data.split('_');
142
+ // Handle callbacks
143
+ bot.action(/^approve_(.+)$/, async (ctx) => {
144
+ const txId = ctx.match[1];
97
145
  const tx = txManager.getTransaction(txId);
98
146
 
99
147
  if (!tx || tx.status !== 'pending') {
100
- bot.answerCallbackQuery(query.id, { text: 'Transaction not found or already processed.', show_alert: true });
148
+ await ctx.answerCbQuery('Transaction not found or already processed.', { show_alert: true });
101
149
  return;
102
150
  }
103
151
 
104
- if (action === 'approve') {
105
- bot.answerCallbackQuery(query.id, { text: 'Processing transaction...' });
106
- bot.sendMessage(chatId, `⏳ Processing transaction ${txId}...`);
107
- try {
108
- let result = '';
109
- if (tx.type === 'transfer') {
110
- result = await executeTransfer(tx.chainName as any, tx.details);
111
- } else if (tx.type === 'swap') {
112
- result = await executeSwap(tx.chainName as any, tx.details);
113
- } else if (tx.type === 'bridge') {
114
- result = await executeBridge(tx.chainName as any, tx.details);
115
- } else if (tx.type === 'mint') {
116
- result = await executeMintNft(tx.chainName as any, tx.details);
117
- } else if (tx.type === 'custom') {
118
- result = await executeCustomTx(tx.chainName as any, tx.details);
119
- }
120
- txManager.updateStatus(txId, 'executed', result);
121
- const prettyMsg = formatTransactionSuccess(tx, result);
122
- bot.sendMessage(chatId, `✅ Transaction processed:\n\n${prettyMsg}`);
123
-
124
- // Sync with dashboard
125
- logger.addEntry({ role: 'assistant', content: `✅ Transaction processed:\n\n${prettyMsg}` });
126
- logger.addEntry({ role: 'tool', name: tx.type === 'swap' ? 'swap_token' : 'transfer_native', content: result });
127
-
128
- // Background update to LLM
129
- processUserInput(`Transaction ${txId} was APPROVED via Telegram. Result: ${result}`, 'system').catch(() => {});
130
- } catch (err: any) {
131
- txManager.updateStatus(txId, 'failed', err.message);
132
- const prettyError = formatTransactionError(tx, err.message);
133
- bot.sendMessage(chatId, prettyError);
134
-
135
- // Background update to LLM
136
- processUserInput(`Transaction ${txId} FAILED via Telegram. Error: ${err.message}`, 'system').catch(() => {});
152
+ await ctx.answerCbQuery('Processing transaction...');
153
+ await ctx.reply(`⏳ Processing transaction ${txId}...`);
154
+
155
+ // Remove inline keyboard immediately
156
+ await ctx.editMessageReplyMarkup(undefined).catch(() => {});
157
+
158
+ try {
159
+ let result = '';
160
+ if (tx.type === 'transfer') {
161
+ result = await executeTransfer(tx.chainName as any, tx.details);
162
+ } else if (tx.type === 'swap') {
163
+ result = await executeSwap(tx.chainName as any, tx.details);
164
+ } else if (tx.type === 'bridge') {
165
+ result = await executeBridge(tx.chainName as any, tx.details);
166
+ } else if (tx.type === 'mint') {
167
+ result = await executeMintNft(tx.chainName as any, tx.details);
168
+ } else if (tx.type === 'custom') {
169
+ result = await executeCustomTx(tx.chainName as any, tx.details);
137
170
  }
138
- } else if (action === 'reject') {
139
- txManager.updateStatus(txId, 'rejected');
140
- processUserInput(`Transaction ${txId} was REJECTED via Telegram. Acknowledge this briefly.`, 'system').catch(() => {});
141
- bot.answerCallbackQuery(query.id, { text: 'Transaction cancelled.' });
142
- bot.sendMessage(chatId, `❌ Transaction cancelled.`);
171
+
172
+ txManager.updateStatus(txId, 'executed', result);
173
+ const prettyMsg = formatTransactionSuccess(tx, result);
174
+ await ctx.reply(`✅ Transaction processed:\n\n${prettyMsg}`);
175
+
176
+ logger.addEntry({ role: 'assistant', content: `✅ Transaction processed:\n\n${prettyMsg}` });
177
+ logger.addEntry({ role: 'tool', name: tx.type === 'swap' ? 'swap_token' : 'transfer_native', content: result });
178
+
179
+ processUserInput(`Transaction ${txId} was APPROVED via Telegram. Result: ${result}`, 'system').catch(() => {});
180
+ } catch (err: any) {
181
+ txManager.updateStatus(txId, 'failed', err.message);
182
+ const prettyError = formatTransactionError(tx, err.message);
183
+ await ctx.reply(prettyError);
184
+ processUserInput(`Transaction ${txId} FAILED via Telegram. Error: ${err.message}`, 'system').catch(() => {});
143
185
  }
186
+ });
187
+
188
+ bot.action(/^reject_(.+)$/, async (ctx) => {
189
+ const txId = ctx.match[1];
190
+ txManager.updateStatus(txId, 'rejected');
191
+ processUserInput(`Transaction ${txId} was REJECTED via Telegram. Acknowledge this briefly.`, 'system').catch(() => {});
144
192
 
145
- // Remove inline keyboard
146
- bot.editMessageReplyMarkup({ inline_keyboard: [] }, { chat_id: chatId, message_id: query.message?.message_id });
193
+ await ctx.answerCbQuery('Transaction cancelled.');
194
+ await ctx.reply(`❌ Transaction cancelled.`);
195
+ await ctx.editMessageReplyMarkup(undefined).catch(() => {});
147
196
  });
148
197
 
149
- bot.on('polling_error', (error) => {
150
- console.error('[Telegram] Polling error:', error);
198
+ bot.catch((err) => {
199
+ console.error('[Telegram] Telegraf error:', err);
151
200
  });
152
201
 
153
- console.log('🤖 Telegram Bot is running and listening for messages...');
202
+ bot.launch();
203
+
204
+ if (isPaired) {
205
+ console.log('🤖 Telegram Bot is running and securely listening for your messages...');
206
+ }
207
+
208
+ // Enable graceful stop
209
+ process.once('SIGINT', () => bot.stop('SIGINT'));
210
+ process.once('SIGTERM', () => bot.stop('SIGTERM'));
211
+
154
212
  } catch (error) {
155
213
  console.error('[Telegram] Failed to initialize bot:', error);
156
214
  }
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import Database from 'better-sqlite3';
3
+ import crypto from 'crypto';
4
+ import { DatabaseSync } from 'node:sqlite';
4
5
  import { loadConfig } from '../config/parser';
5
6
  import { getPath } from '../config/paths';
6
7
 
@@ -10,10 +11,17 @@ export interface MemoryEntry {
10
11
  name?: string;
11
12
  tool_call_id?: string;
12
13
  tool_calls?: any[];
14
+ session_id?: string;
15
+ }
16
+
17
+ export interface ChatSession {
18
+ id: string;
19
+ title: string;
20
+ timestamp: string;
13
21
  }
14
22
 
15
23
  export class Logger {
16
- private db: Database.Database;
24
+ private db: DatabaseSync;
17
25
 
18
26
  constructor() {
19
27
  const config = loadConfig() || {};
@@ -29,14 +37,23 @@ export class Logger {
29
37
  fs.mkdirSync(dir, { recursive: true });
30
38
  }
31
39
 
32
- this.db = new Database(fullPath);
40
+ this.db = new DatabaseSync(fullPath);
33
41
  this.initDb();
34
42
  }
35
43
 
36
44
  private initDb() {
45
+ this.db.exec(`
46
+ CREATE TABLE IF NOT EXISTS sessions (
47
+ id TEXT PRIMARY KEY,
48
+ title TEXT NOT NULL,
49
+ timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
50
+ )
51
+ `);
52
+
37
53
  this.db.exec(`
38
54
  CREATE TABLE IF NOT EXISTS messages (
39
55
  id INTEGER PRIMARY KEY AUTOINCREMENT,
56
+ session_id TEXT,
40
57
  role TEXT NOT NULL,
41
58
  content TEXT NOT NULL,
42
59
  name TEXT,
@@ -46,6 +63,13 @@ export class Logger {
46
63
  )
47
64
  `);
48
65
 
66
+ // Ensure session_id exists for older DBs
67
+ try {
68
+ this.db.prepare('ALTER TABLE messages ADD COLUMN session_id TEXT').run();
69
+ } catch (e) {
70
+ // Column probably already exists
71
+ }
72
+
49
73
  // Migration logic from old memory.json to SQLite
50
74
  const config = loadConfig() || {};
51
75
  const oldJsonPath = getPath((config && (config as any).memory && (config as any).memory.path) ? (config as any).memory.path : 'memory.json');
@@ -62,17 +86,24 @@ export class Logger {
62
86
  VALUES (@role, @content, @name, @tool_call_id, @tool_calls)
63
87
  `);
64
88
 
65
- const insertMany = this.db.transaction((entries: any[]) => {
66
- for (const entry of entries) {
67
- insert.run({
68
- role: entry.role,
69
- content: entry.content || '',
70
- name: entry.name || null,
71
- tool_call_id: entry.tool_call_id || null,
72
- tool_calls: entry.tool_calls ? JSON.stringify(entry.tool_calls) : null
73
- });
89
+ const insertMany = (entries: any[]) => {
90
+ this.db.exec('BEGIN TRANSACTION');
91
+ try {
92
+ for (const entry of entries) {
93
+ insert.run({
94
+ role: entry.role,
95
+ content: entry.content || '',
96
+ name: entry.name || null,
97
+ tool_call_id: entry.tool_call_id || null,
98
+ tool_calls: entry.tool_calls ? JSON.stringify(entry.tool_calls) : null
99
+ });
100
+ }
101
+ this.db.exec('COMMIT');
102
+ } catch (e) {
103
+ this.db.exec('ROLLBACK');
104
+ throw e;
74
105
  }
75
- });
106
+ };
76
107
 
77
108
  insertMany(oldMemory);
78
109
  console.log('[Nyxora Memory] Successfully migrated memory.json to SQLite database (Atomic Storage).');
@@ -86,8 +117,34 @@ export class Logger {
86
117
  }
87
118
  }
88
119
 
89
- public getHistory(): MemoryEntry[] {
90
- const rows = this.db.prepare('SELECT role, content, name, tool_call_id, tool_calls FROM messages ORDER BY id ASC').all();
120
+ public getSessions(): ChatSession[] {
121
+ const rows = this.db.prepare('SELECT id, title, timestamp FROM sessions ORDER BY timestamp DESC').all();
122
+ return rows as unknown as ChatSession[];
123
+ }
124
+
125
+ public createSession(title: string): string {
126
+ const id = crypto.randomUUID();
127
+ this.db.prepare('INSERT INTO sessions (id, title) VALUES (?, ?)').run(id, title);
128
+ return id;
129
+ }
130
+
131
+ public deleteSession(sessionId: string) {
132
+ this.db.prepare('DELETE FROM messages WHERE session_id = ?').run(sessionId);
133
+ this.db.prepare('DELETE FROM sessions WHERE id = ?').run(sessionId);
134
+ }
135
+
136
+ public renameSession(sessionId: string, newTitle: string) {
137
+ this.db.prepare('UPDATE sessions SET title = ? WHERE id = ?').run(newTitle, sessionId);
138
+ }
139
+
140
+ public getHistory(sessionId?: string): MemoryEntry[] {
141
+ let rows;
142
+ if (sessionId) {
143
+ rows = this.db.prepare('SELECT role, content, name, tool_call_id, tool_calls, session_id FROM messages WHERE session_id = ? ORDER BY id ASC').all(sessionId);
144
+ } else {
145
+ rows = this.db.prepare('SELECT role, content, name, tool_call_id, tool_calls, session_id FROM messages WHERE session_id IS NULL ORDER BY id ASC').all();
146
+ }
147
+
91
148
  return rows.map((row: any) => {
92
149
  const entry: MemoryEntry = {
93
150
  role: row.role,
@@ -96,17 +153,19 @@ export class Logger {
96
153
  if (row.name) entry.name = row.name;
97
154
  if (row.tool_call_id) entry.tool_call_id = row.tool_call_id;
98
155
  if (row.tool_calls) entry.tool_calls = JSON.parse(row.tool_calls);
156
+ if (row.session_id) entry.session_id = row.session_id;
99
157
  return entry;
100
158
  });
101
159
  }
102
160
 
103
- public addEntry(entry: MemoryEntry) {
161
+ public addEntry(entry: MemoryEntry, sessionId?: string) {
104
162
  const insert = this.db.prepare(`
105
- INSERT INTO messages (role, content, name, tool_call_id, tool_calls)
106
- VALUES (@role, @content, @name, @tool_call_id, @tool_calls)
163
+ INSERT INTO messages (session_id, role, content, name, tool_call_id, tool_calls)
164
+ VALUES (@session_id, @role, @content, @name, @tool_call_id, @tool_calls)
107
165
  `);
108
166
 
109
167
  insert.run({
168
+ session_id: sessionId || null,
110
169
  role: entry.role,
111
170
  content: entry.content || '',
112
171
  name: entry.name || null,
@@ -115,7 +174,11 @@ export class Logger {
115
174
  });
116
175
  }
117
176
 
118
- public clear() {
119
- this.db.prepare('DELETE FROM messages').run();
177
+ public clear(sessionId?: string) {
178
+ if (sessionId) {
179
+ this.db.prepare('DELETE FROM messages WHERE session_id = ?').run(sessionId);
180
+ } else {
181
+ this.db.prepare('DELETE FROM messages WHERE session_id IS NULL').run();
182
+ }
120
183
  }
121
184
  }
@@ -1,6 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import vm from 'vm';
3
+ import ivm from 'isolated-vm';
4
4
 
5
5
  // Define how an external skill should look like
6
6
  export interface ExternalSkill {
@@ -26,44 +26,58 @@ export class PluginManager {
26
26
  try {
27
27
  const absolutePath = path.resolve(pluginsDir, file);
28
28
  const code = fs.readFileSync(absolutePath, 'utf8');
29
-
30
- // Construct a restricted require function for the sandbox
31
- const restrictedRequire = (moduleName: string) => {
32
- const blockedModules = ['fs', 'child_process', 'os', 'net', 'tls', 'cluster', 'worker_threads'];
33
- if (blockedModules.includes(moduleName)) {
34
- throw new Error(`Sandboxing error: Access to the '${moduleName}' module is blocked for security reasons.`);
35
- }
36
- // Allow fetch and other safe modules by delegating to actual require
37
- return require(moduleName);
38
- };
39
-
40
- // Create the sandbox environment
41
- const sandbox = {
42
- require: restrictedRequire,
43
- console: console,
44
- module: { exports: {} as any },
45
- exports: {},
46
- process: { env: {} }, // Hide actual environment variables
47
- Buffer: Buffer,
48
- setTimeout: setTimeout,
49
- clearTimeout: clearTimeout,
50
- setInterval: setInterval,
51
- clearInterval: clearInterval,
52
- };
53
29
 
54
- const context = vm.createContext(sandbox);
55
- const script = new vm.Script(code, { filename: file });
30
+ const isolate = new ivm.Isolate({ memoryLimit: 128 });
31
+ const context = isolate.createContextSync();
32
+ const jail = context.global;
33
+ jail.setSync('global', jail.derefInto());
56
34
 
57
- // Execute the plugin code inside the VM
58
- script.runInContext(context);
35
+ // Inject a safe fetch
36
+ const safeFetch = new ivm.Reference(async (url: string, options: any) => {
37
+ if (url.includes('127.0.0.1') || url.includes('localhost') || url.includes('::1')) {
38
+ throw new Error("SSRF Protection: Access to localhost is blocked.");
39
+ }
40
+ const res = await fetch(url, options);
41
+ const text = await res.text();
42
+ return text; // Only return text to avoid passing complex Response objects
43
+ });
44
+ jail.setSync('fetchText', safeFetch);
59
45
 
60
- const moduleExports = sandbox.module.exports;
46
+ // Inject console
47
+ const logCallback = new ivm.Reference((...args: any[]) => console.log('[Plugin]', ...args));
48
+ jail.setSync('log', logCallback);
49
+
50
+ const scriptCode = `
51
+ const console = { log: (...args) => log(...args) };
52
+ const fetch = async (url, options) => {
53
+ const text = await fetchText.apply(undefined, [url, options], { arguments: { copy: true }, result: { promise: true } });
54
+ return { text: async () => text, json: async () => JSON.parse(text) };
55
+ };
56
+ const module = { exports: {} };
57
+ const exports = module.exports;
58
+ ${code}
59
+ module.exports;
60
+ `;
61
+
62
+ const script = isolate.compileScriptSync(scriptCode, { filename: file });
63
+ const moduleExportsRef = script.runSync(context);
61
64
 
62
- if (moduleExports.toolDefinition && moduleExports.execute) {
63
- const toolName = moduleExports.toolDefinition.function.name;
64
- this.skills.set(toolName, moduleExports as ExternalSkill);
65
- console.log(`[PluginManager] Loaded sandboxed external skill: ${toolName}`);
65
+ const toolDefinition = moduleExportsRef.getSync('toolDefinition');
66
+ const executeRef = moduleExportsRef.getSync('execute');
67
+
68
+ if (toolDefinition && executeRef && typeof executeRef === 'object') {
69
+ const toolName = toolDefinition.function.name;
70
+
71
+ const safeExecute = async (args: any) => {
72
+ const result = await executeRef.apply(undefined, [args], { arguments: { copy: true }, result: { promise: true, copy: true } });
73
+ return result;
74
+ };
75
+
76
+ this.skills.set(toolName, { toolDefinition, execute: safeExecute });
77
+ console.log(`[PluginManager] Loaded sandboxed external skill: ${toolName}`);
66
78
  }
79
+
80
+ moduleExportsRef.release();
67
81
  } catch (error: any) {
68
82
  console.error(`[PluginManager] Failed to load sandboxed plugin ${file}:`, error.message);
69
83
  }
@@ -1,10 +1,23 @@
1
- let sessionToken: string | null = null;
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
2
4
 
5
+ let sessionToken: string | null = null;
3
6
  import crypto from 'crypto';
4
7
 
5
8
  export function getSessionToken(): string {
6
9
  if (!sessionToken) {
7
- sessionToken = crypto.randomBytes(32).toString('hex');
10
+ const tokenFile = path.join(os.homedir(), '.nyxora', 'auth.token');
11
+ try {
12
+ if (fs.existsSync(tokenFile)) {
13
+ sessionToken = fs.readFileSync(tokenFile, 'utf8').trim();
14
+ } else {
15
+ sessionToken = crypto.randomBytes(32).toString('hex');
16
+ fs.writeFileSync(tokenFile, sessionToken, { mode: 0o600 });
17
+ }
18
+ } catch(e) {
19
+ if (!sessionToken) sessionToken = crypto.randomBytes(32).toString('hex');
20
+ }
8
21
  }
9
22
  return sessionToken;
10
23
  }
@@ -1,4 +1,4 @@
1
- import { createPublicClient, http, PublicClient } from 'viem';
1
+ import { createPublicClient, http, fallback, PublicClient, Transport } from 'viem';
2
2
  import { mainnet, base, bsc, arbitrum, optimism, sepolia } from 'viem/chains';
3
3
  import { loadConfig } from '../config/parser';
4
4
 
@@ -18,12 +18,27 @@ export function getPublicClient(chainName: ChainName): PublicClient {
18
18
  if (!chain) throw new Error(`Unsupported chain: ${chainName}`);
19
19
 
20
20
  const config = loadConfig();
21
- const rpcUrl = config.web3?.rpc_urls?.[chainName];
21
+ const customRpcRaw = config.web3?.rpc_urls?.[chainName];
22
+
23
+ const transports: Transport[] = [];
24
+
25
+ if (customRpcRaw) {
26
+ if (Array.isArray(customRpcRaw)) {
27
+ customRpcRaw.forEach(url => {
28
+ if (url.trim()) transports.push(http(url.trim()));
29
+ });
30
+ } else if (typeof customRpcRaw === 'string' && customRpcRaw.trim()) {
31
+ transports.push(http(customRpcRaw.trim()));
32
+ }
33
+ }
34
+
35
+ // Always append the default public RPC as the last resort
36
+ transports.push(http());
22
37
 
23
38
  // @ts-ignore
24
39
  return createPublicClient({
25
40
  chain,
26
- transport: http(rpcUrl),
41
+ transport: fallback(transports, { rank: false }),
27
42
  });
28
43
  }
29
44