Feat v0.2.9 builtin retirement (#13)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #13.
This commit is contained in:
202
packages/js-sandbox/js/script.js
Normal file
202
packages/js-sandbox/js/script.js
Normal file
@@ -0,0 +1,202 @@
|
||||
// ==========================================
|
||||
// JavaScript Sandbox — Browser Extension
|
||||
// ==========================================
|
||||
// Executes JavaScript in a sandboxed iframe.
|
||||
// Registers the js_eval tool so the LLM can run code,
|
||||
// verify calculations, and transform data.
|
||||
//
|
||||
// Security: The iframe has sandbox="allow-scripts" only.
|
||||
// No allow-same-origin, no network, no DOM access.
|
||||
// ==========================================
|
||||
|
||||
Extensions.register({
|
||||
id: 'js-sandbox',
|
||||
|
||||
_iframe: null,
|
||||
_pendingCallbacks: new Map(), // messageId → { resolve, reject, timer }
|
||||
_messageCounter: 0,
|
||||
|
||||
async init(ctx) {
|
||||
const self = this;
|
||||
|
||||
// Create the sandboxed iframe on first use (lazy)
|
||||
ctx.tools.handle('js_eval', async (args) => {
|
||||
return self._execute(args.code || '');
|
||||
});
|
||||
},
|
||||
|
||||
async _execute(code) {
|
||||
if (!code.trim()) {
|
||||
return { error: 'No code provided' };
|
||||
}
|
||||
|
||||
// Ensure iframe is ready
|
||||
await this._ensureIframe();
|
||||
|
||||
const messageId = 'eval-' + (++this._messageCounter);
|
||||
const TIMEOUT_MS = 10000;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
// Set up timeout
|
||||
const timer = setTimeout(() => {
|
||||
this._pendingCallbacks.delete(messageId);
|
||||
resolve({
|
||||
success: false,
|
||||
error: 'Execution timed out (10s)',
|
||||
output: [],
|
||||
result: null,
|
||||
});
|
||||
}, TIMEOUT_MS);
|
||||
|
||||
this._pendingCallbacks.set(messageId, { resolve, timer });
|
||||
|
||||
// Send code to sandbox
|
||||
this._iframe.contentWindow.postMessage({
|
||||
type: 'eval',
|
||||
id: messageId,
|
||||
code: code,
|
||||
}, '*');
|
||||
});
|
||||
},
|
||||
|
||||
_ensureIframe() {
|
||||
if (this._iframe) return Promise.resolve();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
// Build sandbox HTML as a blob URL
|
||||
const sandboxHTML = this._buildSandboxHTML();
|
||||
const blob = new Blob([sandboxHTML], { type: 'text/html' });
|
||||
const blobURL = URL.createObjectURL(blob);
|
||||
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.sandbox = 'allow-scripts';
|
||||
iframe.style.cssText = 'display:none;width:0;height:0;border:none;position:absolute;';
|
||||
iframe.src = blobURL;
|
||||
|
||||
iframe.onload = () => {
|
||||
this._iframe = iframe;
|
||||
URL.revokeObjectURL(blobURL);
|
||||
resolve();
|
||||
};
|
||||
|
||||
// Listen for messages from sandbox
|
||||
window.addEventListener('message', (event) => {
|
||||
// Only accept messages from our sandbox iframe
|
||||
if (!this._iframe || event.source !== this._iframe.contentWindow) return;
|
||||
|
||||
const data = event.data;
|
||||
if (data?.type !== 'eval-result') return;
|
||||
|
||||
const pending = this._pendingCallbacks.get(data.id);
|
||||
if (!pending) return;
|
||||
|
||||
clearTimeout(pending.timer);
|
||||
this._pendingCallbacks.delete(data.id);
|
||||
|
||||
pending.resolve({
|
||||
success: !data.error,
|
||||
result: data.result !== undefined ? data.result : null,
|
||||
output: data.output || [],
|
||||
error: data.error || null,
|
||||
});
|
||||
});
|
||||
|
||||
document.body.appendChild(iframe);
|
||||
});
|
||||
},
|
||||
|
||||
_buildSandboxHTML() {
|
||||
// This HTML runs INSIDE the sandboxed iframe.
|
||||
// It has no access to the parent page, no network, no cookies.
|
||||
return `<!DOCTYPE html>
|
||||
<html><head><title>Sandbox</title></head>
|
||||
<body><script>
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Listen for eval messages from parent
|
||||
window.addEventListener('message', function(event) {
|
||||
var msg = event.data;
|
||||
if (!msg || msg.type !== 'eval') return;
|
||||
|
||||
var output = [];
|
||||
var result = undefined;
|
||||
var error = null;
|
||||
|
||||
// Override console methods to capture output
|
||||
var origLog = console.log;
|
||||
var origWarn = console.warn;
|
||||
var origError = console.error;
|
||||
|
||||
console.log = function() {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
output.push({ level: 'log', text: args.map(stringify).join(' ') });
|
||||
};
|
||||
console.warn = function() {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
output.push({ level: 'warn', text: args.map(stringify).join(' ') });
|
||||
};
|
||||
console.error = function() {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
output.push({ level: 'error', text: args.map(stringify).join(' ') });
|
||||
};
|
||||
|
||||
try {
|
||||
// Use indirect eval for global scope
|
||||
result = (0, eval)(msg.code);
|
||||
} catch (e) {
|
||||
error = (e && e.message) ? e.message : String(e);
|
||||
}
|
||||
|
||||
// Restore console
|
||||
console.log = origLog;
|
||||
console.warn = origWarn;
|
||||
console.error = origError;
|
||||
|
||||
// Stringify result for safe transfer
|
||||
var resultStr;
|
||||
try {
|
||||
resultStr = stringify(result);
|
||||
} catch (e2) {
|
||||
resultStr = '[unserializable]';
|
||||
}
|
||||
|
||||
// Post result back to parent
|
||||
event.source.postMessage({
|
||||
type: 'eval-result',
|
||||
id: msg.id,
|
||||
result: resultStr,
|
||||
output: output,
|
||||
error: error,
|
||||
}, '*');
|
||||
});
|
||||
|
||||
function stringify(val) {
|
||||
if (val === undefined) return 'undefined';
|
||||
if (val === null) return 'null';
|
||||
if (typeof val === 'function') return val.toString();
|
||||
if (typeof val === 'object') {
|
||||
try { return JSON.stringify(val, null, 2); }
|
||||
catch(e) { return String(val); }
|
||||
}
|
||||
return String(val);
|
||||
}
|
||||
})();
|
||||
</` + `script></body></html>`;
|
||||
},
|
||||
|
||||
destroy() {
|
||||
// Clean up pending callbacks
|
||||
for (const [id, pending] of this._pendingCallbacks) {
|
||||
clearTimeout(pending.timer);
|
||||
pending.resolve({ success: false, error: 'Extension destroyed', output: [], result: null });
|
||||
}
|
||||
this._pendingCallbacks.clear();
|
||||
|
||||
// Remove iframe
|
||||
if (this._iframe) {
|
||||
this._iframe.remove();
|
||||
this._iframe = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user