Skip to content

Commit ae32a9a

Browse files
committed
move js evaluation to sandboxed iframe, remove debug logs
1 parent 75fd25e commit ae32a9a

File tree

4 files changed

+239
-59
lines changed

4 files changed

+239
-59
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<title>JS Sandbox</title>
5+
<script>
6+
// Capture console.log output within the iframe
7+
const iframeConsole = {
8+
_buffer: [],
9+
log: function (...args) {
10+
this._buffer.push(args.map(String).join(' '));
11+
},
12+
getOutput: function () {
13+
const output = this._buffer.join('\n');
14+
this._buffer = [];
15+
return output;
16+
},
17+
clear: function () {
18+
this._buffer = [];
19+
},
20+
};
21+
// Redirect the iframe's console.log
22+
const originalConsoleLog = console.log; // Keep a reference if needed
23+
console.log = iframeConsole.log.bind(iframeConsole);
24+
25+
window.addEventListener('message', (event) => {
26+
if (!event.data || !event.source || !event.source.postMessage) {
27+
return;
28+
}
29+
30+
if (event.data.command === 'executeCode') {
31+
const { code, call_id } = event.data;
32+
let result = '';
33+
let error = null;
34+
iframeConsole.clear();
35+
36+
try {
37+
result = eval(code);
38+
if (result !== undefined && result !== null) {
39+
try {
40+
result = JSON.stringify(result, null, 2);
41+
} catch (e) {
42+
result = String(result);
43+
}
44+
} else {
45+
result = '';
46+
}
47+
} catch (e) {
48+
error = e.message || String(e);
49+
}
50+
51+
const consoleOutput = iframeConsole.getOutput();
52+
const finalOutput = consoleOutput
53+
? consoleOutput + (result && consoleOutput ? '\n' : '') + result
54+
: result;
55+
56+
event.source.postMessage(
57+
{
58+
call_id: call_id,
59+
output: finalOutput,
60+
error: error,
61+
},
62+
event.origin === 'null' ? '*' : event.origin
63+
);
64+
}
65+
});
66+
67+
if (window.parent && window.parent !== window) {
68+
window.parent.postMessage(
69+
{ command: 'iframeReady', call_id: 'initial_ready' },
70+
'*'
71+
);
72+
}
73+
</script>
74+
</head>
75+
<body>
76+
<p>JavaScript Execution Sandbox</p>
77+
</body>
78+
</html>

tools/server/webui/src/utils/app.context.tsx

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
Conversation,
66
Message,
77
PendingMessage,
8-
ToolCall,
8+
ToolCallRequest,
99
ViewingChat,
1010
} from './types';
1111
import StorageUtils from './storage';
@@ -276,14 +276,12 @@ export const AppContextProvider = ({
276276
}
277277
} else {
278278
const responseData = await fetchResponse.json();
279-
if (isDev) console.log({ responseData });
280279
if (responseData.error) {
281280
throw new Error(responseData.error?.message || 'Unknown error');
282281
}
283282

284283
const choice = responseData.choices[0];
285284
const messageFromAPI = choice.message;
286-
console.log({ messageFromAPI });
287285
let newContent = '';
288286

289287
if (messageFromAPI.content) {
@@ -296,21 +294,21 @@ export const AppContextProvider = ({
296294
// Store the raw tool calls in the pendingMsg
297295
pendingMsg = {
298296
...pendingMsg,
299-
tool_calls: messageFromAPI.tool_calls as ToolCall[],
297+
tool_calls: messageFromAPI.tool_calls as ToolCallRequest[],
300298
};
301299

302300
for (let i = 0; i < messageFromAPI.tool_calls.length; i++) {
303-
const tc = messageFromAPI.tool_calls[i] as ToolCall;
304-
if (tc) {
301+
const toolCall = messageFromAPI.tool_calls[i] as ToolCallRequest;
302+
if (toolCall) {
305303
// Set up call id
306-
tc.call_id ??= `call_${i}`;
304+
toolCall.call_id ??= `call_${i}`;
307305

308-
if (isDev) console.log({ tc });
306+
if (isDev) console.log({ tc: toolCall });
309307

310308
// Process tool call
311-
const toolResult = AVAILABLE_TOOLS.get(
312-
tc.function.name
313-
)?.processCall(tc);
309+
const toolResult = await AVAILABLE_TOOLS.get(
310+
toolCall.function.name
311+
)?.processCall(toolCall);
314312

315313
const toolMsg: PendingMessage = {
316314
id: lastMsgId + 1,
@@ -368,13 +366,7 @@ export const AppContextProvider = ({
368366
// if message ended due to "finish_reason": "tool_calls"
369367
// resend it to assistant to process the result.
370368
if (shouldContinueChain) {
371-
console.log('Generating followup message!');
372369
lastMsgId = await generateMessage(convId, lastMsgId, onChunk);
373-
console.log('Generating - done!');
374-
375-
// Fetch messages from DB for debug
376-
const savedMsgs = await StorageUtils.getMessages(convId);
377-
console.log({ savedMsgs });
378370
}
379371

380372
setPending(convId, null);

tools/server/webui/src/utils/tool_calling/agent_tool.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,21 @@ export abstract class AgentTool {
1818
* @param call The tool call object from the API response.
1919
* @returns The tool call output or undefined if the tool is not enabled.
2020
*/
21-
public processCall(call: ToolCallRequest): ToolCallOutput | undefined {
21+
public async processCall(
22+
call: ToolCallRequest
23+
): Promise<ToolCallOutput | undefined> {
2224
if (this.enabled) {
23-
return this._process(call);
25+
try {
26+
return await this._process(call);
27+
} catch (error) {
28+
console.error(`Error processing tool call for ${this.id}:`, error);
29+
return {
30+
type: 'function_call_output',
31+
call_id: call.call_id,
32+
output: `Error during tool execution: ${(error as Error).message}`,
33+
} as ToolCallOutput;
34+
}
2435
}
25-
2636
return undefined;
2737
}
2838

@@ -55,5 +65,5 @@ export abstract class AgentTool {
5565
* The actual tool call processing logic.
5666
* @param call: The tool call object from the API response.
5767
*/
58-
protected abstract _process(call: ToolCallRequest): ToolCallOutput;
68+
protected abstract _process(call: ToolCallRequest): Promise<ToolCallOutput>;
5969
}

tools/server/webui/src/utils/tool_calling/js_repl_tool.ts

Lines changed: 138 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,34 @@ import StorageUtils from '../storage';
22
import { ToolCallRequest, ToolCallOutput, ToolCallParameters } from '../types';
33
import { AgentTool } from './agent_tool';
44

5+
// Import the HTML content as a raw string
6+
import iframeHTMLContent from '../../assets/iframe_sandbox.html?raw';
7+
8+
interface IframeMessage {
9+
call_id: string;
10+
output?: string;
11+
error?: string;
12+
command?: 'executeCode' | 'iframeReady';
13+
code?: string;
14+
}
15+
516
export class JSReplAgentTool extends AgentTool {
617
private static readonly ID = 'javascript_interpreter';
7-
private fakeLogger: FakeConsoleLog;
18+
private iframe: HTMLIFrameElement | null = null;
19+
private iframeReadyPromise: Promise<void> | null = null;
20+
private resolveIframeReady: (() => void) | null = null;
21+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
22+
private rejectIframeReady: ((reason?: any) => void) | null = null;
23+
private pendingCalls = new Map<string, (output: ToolCallOutput) => void>();
24+
private messageHandler:
25+
| ((event: MessageEvent<IframeMessage>) => void)
26+
| null = null;
827

928
constructor() {
1029
super(
1130
JSReplAgentTool.ID,
1231
() => StorageUtils.getConfig().jsInterpreterToolUse,
13-
'Executes JavaScript code in the browser console. The code should be self-contained valid javascript. You can use console.log(variable) to print out intermediate values.',
32+
'Executes JavaScript code in a sandboxed iframe. The code should be self-contained valid javascript. You can use console.log(variable) to print out intermediate values, which will be captured.',
1433
{
1534
type: 'object',
1635
properties: {
@@ -22,53 +41,134 @@ export class JSReplAgentTool extends AgentTool {
2241
required: ['code'],
2342
} as ToolCallParameters
2443
);
25-
this.fakeLogger = new FakeConsoleLog();
44+
this.initIframe();
2645
}
2746

28-
_process(tc: ToolCallRequest): ToolCallOutput {
29-
const args = JSON.parse(tc.function.arguments);
47+
private initIframe(): void {
48+
if (typeof window === 'undefined' || typeof document === 'undefined') {
49+
console.warn(
50+
'JSReplAgentTool: Not in a browser environment, iframe will not be created.'
51+
);
52+
return;
53+
}
3054

31-
// Redirect console.log which agent will use to
32-
// the fake logger so that later we can get the content
33-
const originalConsoleLog = console.log;
34-
console.log = this.fakeLogger.log;
55+
this.iframeReadyPromise = new Promise<void>((resolve, reject) => {
56+
this.resolveIframeReady = resolve;
57+
this.rejectIframeReady = reject;
58+
});
3559

36-
let result = '';
37-
try {
38-
// Evaluate the provided agent code
39-
result = eval(args.code);
40-
if (result) {
41-
result = JSON.stringify(result, null, 2);
42-
} else {
43-
result = '';
60+
this.messageHandler = (event: MessageEvent<IframeMessage>) => {
61+
if (
62+
!event.data ||
63+
!this.iframe ||
64+
!this.iframe.contentWindow ||
65+
event.source !== this.iframe.contentWindow
66+
) {
67+
return;
4468
}
45-
} catch (err) {
46-
result = String(err);
47-
}
4869

49-
console.log = originalConsoleLog;
50-
result = this.fakeLogger.content + result;
70+
const { command, call_id, output, error } = event.data;
71+
if (command === 'iframeReady' && call_id === 'initial_ready') {
72+
if (this.resolveIframeReady) {
73+
this.resolveIframeReady();
74+
this.resolveIframeReady = null;
75+
this.rejectIframeReady = null;
76+
}
77+
return;
78+
}
79+
if (typeof call_id !== 'string') {
80+
return;
81+
}
82+
if (this.pendingCalls.has(call_id)) {
83+
const callback = this.pendingCalls.get(call_id)!;
84+
callback({
85+
type: 'function_call_output',
86+
call_id: call_id,
87+
output: error ? `Error: ${error}` : (output ?? ''),
88+
} as ToolCallOutput);
89+
this.pendingCalls.delete(call_id);
90+
}
91+
};
92+
window.addEventListener('message', this.messageHandler);
5193

52-
this.fakeLogger.clear();
94+
this.iframe = document.createElement('iframe');
95+
this.iframe.style.display = 'none';
96+
this.iframe.sandbox.add('allow-scripts');
5397

54-
return { call_id: tc.call_id, output: result } as ToolCallOutput;
55-
}
56-
}
98+
// Use srcdoc with the imported HTML content
99+
this.iframe.srcdoc = iframeHTMLContent;
57100

58-
class FakeConsoleLog {
59-
private _content: string = '';
101+
document.body.appendChild(this.iframe);
60102

61-
public get content(): string {
62-
return this._content;
103+
setTimeout(() => {
104+
if (this.rejectIframeReady) {
105+
this.rejectIframeReady(new Error('Iframe readiness timeout'));
106+
this.resolveIframeReady = null;
107+
this.rejectIframeReady = null;
108+
}
109+
}, 5000);
63110
}
64111

65-
// Use an arrow function for log to correctly bind 'this'
66-
public log = (...args: any[]): void => {
67-
// Convert arguments to strings and join them.
68-
this._content += args.map((arg) => String(arg)).join(' ') + '\n';
69-
};
112+
async _process(tc: ToolCallRequest): Promise<ToolCallOutput> {
113+
let error = null;
114+
if (
115+
typeof window === 'undefined' ||
116+
!this.iframe ||
117+
!this.iframe.contentWindow ||
118+
!this.iframeReadyPromise
119+
) {
120+
error =
121+
'Error: JavaScript interpreter is not available or iframe not ready.';
122+
}
123+
124+
try {
125+
await this.iframeReadyPromise;
126+
} catch (e) {
127+
error = `Error: Iframe for JavaScript interpreter failed to initialize. ${(e as Error).message}`;
128+
}
129+
130+
let args;
131+
try {
132+
args = JSON.parse(tc.function.arguments);
133+
} catch (e) {
134+
error = `Error: Could not parse arguments for tool call. ${(e as Error).message}`;
135+
}
136+
137+
const codeToExecute = args.code;
138+
if (typeof codeToExecute !== 'string') {
139+
error = 'Error: "code" argument must be a string.';
140+
}
141+
142+
if (error) {
143+
return {
144+
type: 'function_call_output',
145+
call_id: tc.call_id,
146+
output: error,
147+
} as ToolCallOutput;
148+
}
149+
150+
return new Promise<ToolCallOutput>((resolve) => {
151+
this.pendingCalls.set(tc.call_id, resolve);
152+
const message: IframeMessage = {
153+
command: 'executeCode',
154+
code: codeToExecute,
155+
call_id: tc.call_id,
156+
};
157+
this.iframe!.contentWindow!.postMessage(message, '*');
158+
});
159+
}
70160

71-
public clear = (): void => {
72-
this._content = '';
73-
};
161+
// public dispose(): void {
162+
// if (this.iframe) {
163+
// document.body.removeChild(this.iframe);
164+
// this.iframe = null;
165+
// }
166+
// if (this.messageHandler) {
167+
// window.removeEventListener('message', this.messageHandler);
168+
// this.messageHandler = null;
169+
// }
170+
// this.pendingCalls.clear();
171+
// this.resolveIframeReady = null;
172+
// this.rejectIframeReady = null;
173+
// }
74174
}

0 commit comments

Comments
 (0)