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 +1 -1
- package/src/agent/loop/local.js +2 -1
- package/src/agent/react-loop.js +6 -6
- package/src/llm/direct.js +4 -1
- package/src/tools/local.js +36 -1
- package/src/ui/App.js +18 -1
- package/src/utils/helpers.js +22 -0
package/package.json
CHANGED
package/src/agent/loop/local.js
CHANGED
|
@@ -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;
|
package/src/agent/react-loop.js
CHANGED
|
@@ -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
|
-
|
|
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 }] },
|
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
|
|
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 });
|
package/src/utils/helpers.js
CHANGED
|
@@ -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
|
*/
|