Feat v0.2.9 builtin retirement (#13)
Some checks failed
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-frontend (push) Successful in 6s
CI/CD / test-go-pg (push) Failing after 2m15s
CI/CD / test-sqlite (push) Successful in 2m36s
CI/CD / build-and-deploy (push) Has been skipped

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:
2026-03-27 16:46:50 +00:00
committed by xcaliber
parent babfe66721
commit 8580e1d93e
21 changed files with 42 additions and 439 deletions

View 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;
}
}
});

View File

@@ -0,0 +1,30 @@
{
"id": "js-sandbox",
"name": "JavaScript Sandbox",
"version": "1.0.0",
"type": "extension",
"tier": "browser",
"author": "switchboard",
"description": "Executes JavaScript in a sandboxed iframe. Allows the LLM to run code, verify calculations, and transform data — no server compute required.",
"requires": ["chat"],
"permissions": [],
"tools": [
{
"name": "js_eval",
"display_name": "JavaScript Eval",
"description": "Execute JavaScript code in a secure browser sandbox. Use this to: verify calculations, run algorithms, transform data, test code snippets, parse/format strings, or any computation. Console output (log/warn/error) is captured. The last expression's value is returned as the result. Has no network access, no DOM access, no access to the parent page. Timeout: 10 seconds.",
"parameters": {
"type": "object",
"properties": {
"code": {
"type": "string",
"description": "JavaScript code to execute. Use console.log() for output. The value of the last expression is returned automatically."
}
},
"required": ["code"]
}
}
],
"surfaces": [],
"settings": {}
}