osai-agent 4.2.23 → 4.2.25
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/package.json +1 -1
- package/src/commands/admin.js +13 -2
- package/src/index.js +6 -1
- package/src/tools/local.js +36 -1
package/package.json
CHANGED
package/src/commands/admin.js
CHANGED
|
@@ -12,7 +12,7 @@ const promptSecret = () => new Promise((resolve) => {
|
|
|
12
12
|
});
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
-
const adminFetch = async (serverArg, path, method = 'GET', body = null) => {
|
|
15
|
+
const adminFetch = async (serverArg, path, method = 'GET', body = null, pathPrefix = '/auth') => {
|
|
16
16
|
const config = new Conf({ projectName: 'osai-agent' });
|
|
17
17
|
const serverUrl = serverArg || config.get('server') || DEFAULT_SERVER_URL;
|
|
18
18
|
const httpUrl = toHttpUrl(serverUrl);
|
|
@@ -32,7 +32,7 @@ const adminFetch = async (serverArg, path, method = 'GET', body = null) => {
|
|
|
32
32
|
options.body = JSON.stringify(body);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
const response = await fetch(`${httpUrl}
|
|
35
|
+
const response = await fetch(`${httpUrl}${pathPrefix}${path}`, options);
|
|
36
36
|
const data = await response.json().catch(() => null);
|
|
37
37
|
|
|
38
38
|
if (!response.ok) {
|
|
@@ -80,3 +80,14 @@ export const enableUser = async ({ server, email }) => {
|
|
|
80
80
|
const data = await adminFetch(server, '/admin/enable-user', 'POST', { email });
|
|
81
81
|
const spinner = ora().succeed(chalk.green(`User enabled: ${data.email}`));
|
|
82
82
|
};
|
|
83
|
+
|
|
84
|
+
export const generateProKeys = async ({ server, count, type }) => {
|
|
85
|
+
const spinner = ora('Generating Pro keys...').start();
|
|
86
|
+
const data = await adminFetch(server, '/generate', 'POST', { type, count }, '/pro');
|
|
87
|
+
spinner.stop();
|
|
88
|
+
console.log(chalk.green(`\n ${data.count} Pro keys generated (${data.type}, ${data.duration_days} days):\n`));
|
|
89
|
+
for (const key of data.keys) {
|
|
90
|
+
console.log(` ${chalk.hex('#ff9e64')(key)}`);
|
|
91
|
+
}
|
|
92
|
+
console.log();
|
|
93
|
+
};
|
package/src/index.js
CHANGED
|
@@ -26,7 +26,7 @@ import { handleMcpCommand } from './commands/mcp.js';
|
|
|
26
26
|
import { stopSubagent } from './commands/stop-subagent.js';
|
|
27
27
|
import { claimProKey } from './commands/pro.js';
|
|
28
28
|
import { deleteAccount } from './commands/account.js';
|
|
29
|
-
import { listUsers, disableUser, enableUser } from './commands/admin.js';
|
|
29
|
+
import { listUsers, disableUser, enableUser, generateProKeys } from './commands/admin.js';
|
|
30
30
|
import minimist from 'minimist';
|
|
31
31
|
import updateNotifier from 'update-notifier';
|
|
32
32
|
import { createRequire } from 'module';
|
|
@@ -153,6 +153,10 @@ switch (cmd) {
|
|
|
153
153
|
} else if (args._[1] === 'enable') {
|
|
154
154
|
if (!args._[2]) { console.error('Usage: osai-agent admin enable <email>'); process.exit(1); }
|
|
155
155
|
await enableUser({ server: args.server, email: args._[2] });
|
|
156
|
+
} else if (args._[1] === 'pro-gen') {
|
|
157
|
+
const count = Math.min(Math.max(1, parseInt(args._[2]) || 1), 50);
|
|
158
|
+
const type = args.type || 'monthly';
|
|
159
|
+
await generateProKeys({ server: args.server, count, type });
|
|
156
160
|
} else {
|
|
157
161
|
console.log();
|
|
158
162
|
console.log('Usage: osai-agent admin <subcommand> [args]');
|
|
@@ -161,6 +165,7 @@ switch (cmd) {
|
|
|
161
165
|
console.log(' users List all users');
|
|
162
166
|
console.log(' disable <email> Disable a user account');
|
|
163
167
|
console.log(' enable <email> Re-enable a user account');
|
|
168
|
+
console.log(' pro-gen <count> Generate Pro keys (--type monthly|yearly)');
|
|
164
169
|
console.log();
|
|
165
170
|
process.exit(1);
|
|
166
171
|
}
|
package/src/tools/local.js
CHANGED
|
@@ -15,6 +15,13 @@ const COMMAND_TIMEOUT = parseInt(process.env.OSAI_COMMAND_TIMEOUT) || DEFAULTS.C
|
|
|
15
15
|
const FETCH_TIMEOUT = parseInt(process.env.OSAI_FETCH_TIMEOUT) || DEFAULTS.FETCH_TIMEOUT;
|
|
16
16
|
const MAX_COMMAND_OUTPUT_CHARS = parseInt(process.env.OSAI_MAX_COMMAND_OUTPUT_CHARS || '40000', 10);
|
|
17
17
|
|
|
18
|
+
const SAFE_ENV_KEYS = new Set([
|
|
19
|
+
'PATH', 'HOME', 'USER', 'USERNAME', 'HOSTNAME', 'HOST',
|
|
20
|
+
'LANG', 'LC_ALL', 'LC_CTYPE', 'TERM', 'SHELL',
|
|
21
|
+
'TMPDIR', 'TMP', 'TEMP', 'XDG_*', 'DISPLAY', 'WAYLAND_DISPLAY',
|
|
22
|
+
'NODE_ENV', 'PYTHONIOENCODING', 'LANG',
|
|
23
|
+
]);
|
|
24
|
+
|
|
18
25
|
/** Commands that spawn interactive shells — must be blocked */
|
|
19
26
|
const INTERACTIVE_COMMANDS = [
|
|
20
27
|
/^cmd(\s+\/k)?$/i, /^powershell(\s+-noexit)?$/i, /^bash$/i, /^sh$/i,
|
|
@@ -211,6 +218,12 @@ export const executeLocal = async (command, sudoPassword = null) => {
|
|
|
211
218
|
}
|
|
212
219
|
}
|
|
213
220
|
|
|
221
|
+
// Block dangerous patterns
|
|
222
|
+
const safety = validateLocalCommand(trimmedCmd);
|
|
223
|
+
if (!safety.safe) {
|
|
224
|
+
return { success: false, output: '', error: safety.reason };
|
|
225
|
+
}
|
|
226
|
+
|
|
214
227
|
logger.debug('Executing local command', { cmd: trimmedCmd, timeout: COMMAND_TIMEOUT });
|
|
215
228
|
|
|
216
229
|
const isWindows = detectOS() === 'windows';
|
|
@@ -220,6 +233,12 @@ export const executeLocal = async (command, sudoPassword = null) => {
|
|
|
220
233
|
finalCommand = finalCommand.replace(/^sudo\b/, 'sudo -S');
|
|
221
234
|
}
|
|
222
235
|
|
|
236
|
+
// Apply resource limits via shell (non-Windows)
|
|
237
|
+
if (!isWindows) {
|
|
238
|
+
const cpuSecs = Math.ceil(COMMAND_TIMEOUT / 1000);
|
|
239
|
+
finalCommand = `ulimit -t ${cpuSecs} -v 524288 2>/dev/null; ${finalCommand}`;
|
|
240
|
+
}
|
|
241
|
+
|
|
223
242
|
return await new Promise((resolve) => {
|
|
224
243
|
let output = '';
|
|
225
244
|
let truncatedChars = 0;
|
|
@@ -233,10 +252,20 @@ export const executeLocal = async (command, sudoPassword = null) => {
|
|
|
233
252
|
if (text.length > remaining) truncatedChars += text.length - remaining;
|
|
234
253
|
};
|
|
235
254
|
|
|
255
|
+
const filteredEnv = Object.fromEntries(
|
|
256
|
+
Object.entries(process.env).filter(([key]) => {
|
|
257
|
+
if (SAFE_ENV_KEYS.has(key)) return true;
|
|
258
|
+
if (key.startsWith('XDG_')) return true;
|
|
259
|
+
return false;
|
|
260
|
+
})
|
|
261
|
+
);
|
|
262
|
+
filteredEnv.PYTHONIOENCODING = 'utf-8';
|
|
263
|
+
filteredEnv.LANG = 'en_US.UTF-8';
|
|
264
|
+
|
|
236
265
|
const child = spawn(finalCommand, {
|
|
237
266
|
shell: isWindows ? 'cmd.exe' : '/bin/sh',
|
|
238
267
|
windowsHide: true,
|
|
239
|
-
env:
|
|
268
|
+
env: filteredEnv,
|
|
240
269
|
});
|
|
241
270
|
|
|
242
271
|
if (sudoPassword) {
|
|
@@ -761,12 +790,18 @@ export const getFileInfo = async (filePath) => {
|
|
|
761
790
|
* Fetch content from a URL (for documentation, API data, etc.)
|
|
762
791
|
* {"tool":"FETCH_URL","url":"<url>","description":"<why>"}
|
|
763
792
|
*/
|
|
793
|
+
const PRIVATE_IP_RE = /^(https?:\/\/)(127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|169\.254\.|0\.|::1|fc00:|fe80:)/i;
|
|
794
|
+
|
|
764
795
|
export const fetchUrl = async (url, description = '') => {
|
|
765
796
|
try {
|
|
766
797
|
if (!url || !url.startsWith('http')) {
|
|
767
798
|
return { success: false, output: '', error: 'Invalid URL. Must start with http:// or https://' };
|
|
768
799
|
}
|
|
769
800
|
|
|
801
|
+
if (PRIVATE_IP_RE.test(url)) {
|
|
802
|
+
return { success: false, output: '', error: 'Blocked: URL points to a private/internal network address' };
|
|
803
|
+
}
|
|
804
|
+
|
|
770
805
|
const controller = new AbortController();
|
|
771
806
|
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
|
|
772
807
|
|