osai-agent 4.2.22 → 4.2.24

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osai-agent",
3
- "version": "4.2.22",
3
+ "version": "4.2.24",
4
4
  "type": "module",
5
5
  "description": "OS AI Agent - YOUR AI AGENT",
6
6
  "main": "src/index.js",
@@ -5,6 +5,7 @@ import { mcpClientManager } from '../../tools/mcp-client.js';
5
5
  import { memory } from '../../memory/store.js';
6
6
  import { DEFAULTS, COMPLETION_SIGNALS } from '../../utils/constants.js';
7
7
  import { logger } from '../../utils/logger.js';
8
+ import { stripSystemPrompt } from '../../utils/helpers.js';
8
9
 
9
10
  export default {
10
11
  async _callWorkerLocal() {
@@ -181,7 +182,7 @@ const systemPrompt = buildSystemPrompt(this._userOS, this.mode, {
181
182
  return null;
182
183
  }
183
184
  if (attempt >= maxAttempts - 1) {
184
- this.onError(error.message);
185
+ this.onError(stripSystemPrompt(error.message));
185
186
  return null;
186
187
  }
187
188
  retrying = true;
@@ -22,7 +22,7 @@ import fs from 'fs';
22
22
  import { DEFAULTS, TOOLS, COMPLETION_SIGNALS, SAFETY_TIERS, MODES, EXECUTION_MODES, READ_ONLY_TOOLS, CODING_TOOLS, CRITICAL_ENV_FILENAMES, CRITICAL_ENV_FILENAME_PATTERNS, CRITICAL_ENV_PATH_SEGMENTS, CRITICAL_ENV_EXCLUDE, MAX_ITERATIONS, AUTO_CONTINUE_LIMIT, READ_FRESHNESS_INTERACTIONS, TOOL_SEQUENCE_WINDOW_SIZE, CONTEXT_SUMMARY_TAG, SUBAGENT_MAX_ITERATIONS } from '../utils/constants.js';
23
23
  import { discoverSkills } from '../skills/loader.js';
24
24
  import { logger } from '../utils/logger.js';
25
- import { sleep, cancellableSleep } from '../utils/helpers.js';
25
+ import { sleep, cancellableSleep, stripSystemPrompt } from '../utils/helpers.js';
26
26
 
27
27
  import contextSummaryMethods from './loop/context-summary.js';
28
28
  import loopDetectionMethods from './loop/loop-detection.js';
@@ -336,13 +336,13 @@ export class AgentLoop {
336
336
  if (this.iteration <= 1 && !this._shouldCancel()) {
337
337
  logger.info('Retrying iteration after error...');
338
338
  iterationRetrying = true;
339
- this.onRetryStatus(`Retrying after error: ${error.message}`);
339
+ this.onRetryStatus(`Retrying after error: ${stripSystemPrompt(error.message)}`);
340
340
  this.iteration--;
341
341
  const slept = await this._cancellableSleep(DEFAULTS.API_RETRY_DELAY);
342
342
  if (!slept || this._shouldCancel()) break;
343
343
  continue;
344
344
  }
345
- this.onError(error.message);
345
+ this.onError(stripSystemPrompt(error.message));
346
346
  break;
347
347
  }
348
348
  }
@@ -616,7 +616,7 @@ export class AgentLoop {
616
616
  } catch {}
617
617
 
618
618
  if (!isRetryableStatus(response.status) || attempt >= maxAttempts - 1) {
619
- this.onError(errorMsg);
619
+ this.onError(stripSystemPrompt(errorMsg));
620
620
  return null;
621
621
  }
622
622
 
@@ -646,7 +646,7 @@ export class AgentLoop {
646
646
  }
647
647
  if (json.error) {
648
648
  if (attempt >= maxAttempts - 1) {
649
- this.onError(json.error);
649
+ this.onError(stripSystemPrompt(json.error));
650
650
  return null;
651
651
  }
652
652
  retrying = true;
@@ -667,7 +667,7 @@ export class AgentLoop {
667
667
  return null;
668
668
  }
669
669
  if (attempt >= maxAttempts - 1) {
670
- this.onError(error.message);
670
+ this.onError(stripSystemPrompt(error.message));
671
671
  return null;
672
672
  }
673
673
  retrying = true;
package/src/llm/direct.js CHANGED
@@ -268,13 +268,16 @@ export function streamLocalCompletion({
268
268
  } else if (provider.type === 'gemini') {
269
269
  const genAI = new GoogleGenerativeAI(provider.apiKey);
270
270
  const geminiModel = genAI.getGenerativeModel({ model: effectiveModel });
271
- const geminiHistory = messages
271
+ let geminiHistory = messages
272
272
  .filter(m => m.role !== 'system')
273
273
  .slice(0, -1)
274
274
  .map(m => ({
275
275
  role: m.role === 'assistant' ? 'model' : 'user',
276
276
  parts: [{ text: m.content }],
277
277
  }));
278
+ while (geminiHistory.length > 0 && geminiHistory[0].role === 'model') {
279
+ geminiHistory.shift();
280
+ }
278
281
  const chat = geminiModel.startChat({
279
282
  history: geminiHistory,
280
283
  systemInstruction: { parts: [{ text: systemPrompt }] },
@@ -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: { ...process.env, PYTHONIOENCODING: 'utf-8', LANG: 'en_US.UTF-8' },
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
 
package/src/ui/App.js CHANGED
@@ -81,6 +81,14 @@ const INTERNAL_SSE_LINE_RE = /^\s*(data|event|id|retry):\s.*$/gim;
81
81
  const INTERNAL_TOOL_JSON_LINE_RE = /^\s*\{(?:\\")?tool(?:\\")?\s*:\s*.*$/gim;
82
82
  const INTERNAL_TOOL_XML_LINE_RE = /<tool\b[^>]*>[\s\S]*?<\/tool\s*>|<tool\b[^>]*\/>/gim;
83
83
 
84
+ const UI_SYSTEM_PROMPT_MARKERS = [
85
+ 'You are OS AI Agent',
86
+ '## TOOLS \u2014',
87
+ '## EXECUTION MODE',
88
+ '## CUSTOM INSTRUCTIONS',
89
+ '### System Commands',
90
+ ];
91
+
84
92
  function sanitizeUiText(input) {
85
93
  let text = String(input || '');
86
94
  if (!text) return '';
@@ -93,6 +101,15 @@ function sanitizeUiText(input) {
93
101
  .replace(/(?:^|\n)\s*```json\s*(?=\n|$)/gi, '\n')
94
102
  .replace(/\n{3,}/g, '\n\n');
95
103
 
104
+ for (const marker of UI_SYSTEM_PROMPT_MARKERS) {
105
+ const idx = text.indexOf(marker);
106
+ if (idx !== -1) {
107
+ text = text.slice(0, idx).trim();
108
+ if (text.length > 300) text = text.slice(0, 300) + '...';
109
+ break;
110
+ }
111
+ }
112
+
96
113
  return text;
97
114
  }
98
115
 
@@ -1025,7 +1042,7 @@ export function App({ createAgentLoop, agentConfig, initialSession = null, onExi
1025
1042
  }
1026
1043
  agentLoopActiveRef.current = false;
1027
1044
  cancelRequestedRef.current = false;
1028
- finishTaskUi({ success: false, output: String(err || 'Error') });
1045
+ finishTaskUi({ success: false, output: sanitizeUiText(String(err || 'Error')) });
1029
1046
  setState('error');
1030
1047
  badgeInfoRef.current = { signal: 'ERROR', message: err };
1031
1048
  addEvent({ type: 'badge', signal: 'ERROR', elapsed: null });
@@ -87,6 +87,28 @@ export const categorizeError = (error) => {
87
87
  return { category: 'recoverable', recoverable: true, userMessage: error.message };
88
88
  };
89
89
 
90
+ const SYSTEM_PROMPT_MARKERS = [
91
+ 'You are OS AI Agent',
92
+ '## TOOLS \u2014',
93
+ '## EXECUTION MODE',
94
+ '## CUSTOM INSTRUCTIONS',
95
+ '### System Commands',
96
+ ];
97
+
98
+ export const stripSystemPrompt = (str) => {
99
+ if (!str) return str;
100
+ const s = String(str);
101
+ for (const marker of SYSTEM_PROMPT_MARKERS) {
102
+ const idx = s.indexOf(marker);
103
+ if (idx !== -1) {
104
+ let trimmed = s.slice(0, idx).trim();
105
+ if (trimmed.length > 300) trimmed = trimmed.slice(0, 300) + '...';
106
+ return trimmed;
107
+ }
108
+ }
109
+ return s;
110
+ };
111
+
90
112
  /**
91
113
  * Sanitize a string for safe shell usage (basic escaping)
92
114
  */