add sandboxing by default in god mode created workers (#2481)

* add sandboxing by default in god mode workers

* closes #2481

* Update UIWindowPublishWorker.js

* Add sandbox option to worker publish UI

Add a collapsible 'Advanced' section with a 'Sandboxed' checkbox (checked by default) to UIWindowPublishWorker. On publish the code reads the checkbox state and builds createOptions ({ sandbox: true } or { sandbox: false }) and passes it to puter.workers.create as an argument so workers can be created in sandboxed or non-sandboxed mode. Small UI styling and markup for the details/summary block included.

---------

Co-authored-by: jelveh <nj@puter.com>
This commit is contained in:
Neal Shah
2026-02-12 16:06:40 -08:00
committed by GitHub
co-authored by jelveh
parent 354ac314a9
commit a23f272fd2
5 changed files with 257 additions and 13 deletions
+2
View File
@@ -98,6 +98,7 @@ extension.get('/whoami', { subdomain: 'api' }, async (req, res, next) => {
human_readable_age: timeago.format(new Date(req.user.timestamp)),
hasDevAccountAccess: !!req.actor.type.user.metadata?.hasDevAccountAccess,
...(req.new_token ? { token: req.token } : {}),
is_user_token: true, // gets deleted if not a user token
};
// TODO: redundant? GetUserService already puts these values on 'user'
@@ -128,6 +129,7 @@ extension.get('/whoami', { subdomain: 'api' }, async (req, res, next) => {
delete details.taskbar_items;
delete details.token;
delete details.human_readable_age;
delete details.is_user_token;
}
if ( actor.type instanceof AppUnderUserActorType ) {
+201
View File
@@ -0,0 +1,201 @@
const page = `
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Puter Worker Sandbox Playground</title>
<style>
:root {
color-scheme: light;
--bg: #0f172a;
--panel: #111827;
--panel-2: #1f2937;
--text: #e5e7eb;
--muted: #94a3b8;
--accent: #22d3ee;
--danger: #fb7185;
--ok: #34d399;
--border: #334155;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
background: radial-gradient(circle at top, #1e293b, var(--bg) 55%);
color: var(--text);
min-height: 100vh;
padding: 20px;
}
.wrap {
max-width: 980px;
margin: 0 auto;
}
h1 {
margin: 0 0 8px;
font-size: 24px;
}
p {
margin: 0 0 14px;
color: var(--muted);
}
.toolbar {
display: flex;
gap: 10px;
margin-bottom: 10px;
flex-wrap: wrap;
}
button {
border: 1px solid var(--border);
background: var(--panel-2);
color: var(--text);
padding: 8px 12px;
border-radius: 8px;
cursor: pointer;
}
button:hover {
border-color: var(--accent);
}
.layout {
display: grid;
gap: 12px;
grid-template-columns: 1fr;
}
@media (min-width: 900px) {
.layout {
grid-template-columns: 1fr 1fr;
}
}
.card {
border: 1px solid var(--border);
border-radius: 10px;
background: color-mix(in srgb, var(--panel) 88%, black 12%);
overflow: hidden;
}
.card h2 {
margin: 0;
padding: 10px 12px;
font-size: 14px;
border-bottom: 1px solid var(--border);
background: color-mix(in srgb, var(--panel-2) 90%, black 10%);
}
textarea {
display: block;
width: 100%;
min-height: 420px;
border: 0;
resize: vertical;
background: transparent;
color: var(--text);
padding: 12px;
outline: none;
font-size: 13px;
line-height: 1.5;
}
#logs {
margin: 0;
padding: 12px;
min-height: 420px;
max-height: 70vh;
overflow: auto;
white-space: pre-wrap;
word-break: break-word;
font-size: 13px;
line-height: 1.45;
}
.line { margin: 0 0 8px; }
.info { color: var(--muted); }
.ok { color: var(--ok); }
.warn { color: #fbbf24; }
.error { color: var(--danger); }
code {
color: var(--accent);
}
</style>
</head>
<body>
<main class="wrap">
<h1>Puter Worker Sandbox Playground</h1>
<p>Use this page to interact with the puter APIs in the same sandbox as your worker.</p>
<div class="toolbar">
<button id="run">Run</button>
<button id="clear">Clear Logs</button>
</div>
<section class="layout">
<article class="card">
<h2>Code</h2>
<textarea id="code" spellcheck="false">console.log(JSON.stringify(await puter.kv.list({limit: 100})))</textarea>
</article>
<article class="card">
<h2>Logs</h2>
<pre id="logs"></pre>
</article>
</section>
</main>
<script src="https://js.puter.com/v2/"></script>
<script>
(() => {
const codeEl = document.getElementById('code');
const logsEl = document.getElementById('logs');
const runBtn = document.getElementById('run');
const clearBtn = document.getElementById('clear');
const originalConsole = {
log: console.log.bind(console),
info: console.info.bind(console),
warn: console.warn.bind(console),
error: console.error.bind(console),
};
const safeStringify = (value) => {
if (typeof value === 'string') return value;
if (value instanceof Error) return value.stack || value.message || String(value);
try { return JSON.stringify(value, null, 2); }
catch { return String(value); }
};
const appendLog = (level, parts) => {
const line = document.createElement('div');
line.className = 'line ' + level;
line.textContent = '[' + new Date().toLocaleTimeString() + '] ' + parts.map(safeStringify).join(' ');
logsEl.appendChild(line);
logsEl.scrollTop = logsEl.scrollHeight;
};
['log', 'info', 'warn', 'error'].forEach((level) => {
console[level] = (...args) => {
appendLog(level === 'log' ? 'ok' : level, args);
originalConsole[level](...args);
};
});
window.addEventListener('error', (event) => {
appendLog('error', [event.error || event.message]);
});
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
runBtn.addEventListener('click', async () => {
const source = codeEl.value;
appendLog('info', ['Running...']);
try {
const runInSandbox = new AsyncFunction(source);
const result = await runInSandbox.call(window);
appendLog('ok', ['Result:', result]);
} catch (err) {
appendLog('error', ['Execution failed:', err]);
}
});
clearBtn.addEventListener('click', () => {
logsEl.textContent = '';
});
})();
</script>
</body>
</html>
`;
extension.get('/', { noauth: true, subdomain: 'worker-sandbox' }, (req, res) => {
res.type('html').send(page);
});
+31 -10
View File
@@ -41,6 +41,15 @@ async function UIWindowPublishWorker (target_dir_uid, target_dir_name, target_di
h += '</div>';
// uid
h += `<input class="publishWebsiteTargetDirUID" type="hidden" value="${html_encode(target_dir_uid)}"/>`;
// Advanced (collapsed by default)
h += '<details class="publish-worker-advanced" style="margin: 16px 0;">';
h += '<summary style="cursor: pointer; font-size: 13px; color: #5c6b7a; user-select: none;">Advanced</summary>';
h += '<div style="margin-top: 12px; padding-left: 2px;">';
h += '<label style="display: flex; align-items: center; gap: 8px; cursor: pointer; font-size: 13px;">';
h += '<input type="checkbox" class="publish-worker-sandboxed" checked>';
h += 'Sandboxed</label>';
h += '</div>';
h += '</details>';
// Publish
h += `<button class="publish-btn button button-action button-block button-normal">${i18n('publish')}</button>`;
h += '</form>';
@@ -91,9 +100,13 @@ async function UIWindowPublishWorker (target_dir_uid, target_dir_name, target_di
<div style="display: inline-block; margin-top: 10px; width: 16px; height: 16px; border: 2px solid #ffffff; border-radius: 50%; border-top: 2px solid transparent; animation: spin 1s linear infinite;"></div>
`);
const sandboxed = $(el_window).find('.publish-worker-sandboxed').is(':checked');
const createOptions = sandboxed ? { sandbox: true } : { sandbox: false };
puter.workers.create(worker_name,
target_dir_path).then((res) => {
let url = `https://${ worker_name }.puter.work`;
target_dir_path,
createOptions).then((res) => {
let url = `https://${ worker_name }.puter.work`;
$(el_window).find('.window-publishWorker-form').hide(100, function () {
$(el_window).find('.publishWorker-published-link').attr('href', url);
$(el_window).find('.publishWorker-published-link').text(url);
@@ -108,17 +121,25 @@ async function UIWindowPublishWorker (target_dir_uid, target_dir_name, target_di
// update item's website_url attribute
$(this).attr('data-website_url', url + $(this).attr('data-path').substring(target_dir_path.length));
});
}).catch((err) => {
err = err.error;
$(el_window).find('.publish-worker-error-msg').html(
err.message + (
err.code === 'subdomain_limit_reached' ?
` <span class="manage-your-websites-link">${ i18n('manage_your_subdomains') }</span>` : ''
));
}).catch((err) => {
let errorHtml;
// Handle worker service errors (result.success === false)
if ( ! (err instanceof Error) ) {
// Handle regular API errors
const error = err.error || err;
errorHtml = error.message + (
error.code === 'subdomain_limit_reached' ?
` <span class="manage-your-websites-link">${ i18n('manage_your_subdomains') }</span>` : ''
);
} else {
errorHtml = `<pre style="white-space: pre-wrap; font-family: monospace; font-size: 12px; margin: 0; text-align: left; font-family: monospace;">${html_encode(err)}</pre>`;
}
$(el_window).find('.publish-worker-error-msg').html(errorHtml);
$(el_window).find('.publish-worker-error-msg').fadeIn();
// re-enable 'Publish' button and restore original text
$(el_window).find('.publish-btn').prop('disabled', false).text(originalText);
});
});
});
$(el_window).find('.publish-window-ok-btn').on('click', function () {
+4
View File
@@ -1953,6 +1953,10 @@ label {
font-size: 13px;
}
.publish-worker-error-msg{
text-align: left;
}
.error {
display: none;
color: red;
+19 -3
View File
@@ -18,6 +18,22 @@ export class WorkersHandler {
}
let appId;
if ( typeof (appName) === 'object' || typeof (appName) === 'undefined' ) {
const user = (puter.whoami || await puter.getUser());
if ( user.is_user_token && (appName === undefined || appName?.sandbox !== false) ) {
let sandboxApp;
try {
sandboxApp = await puter.apps.get(`sandbox-${ workerName }`);
} catch ( e ) {
sandboxApp = await puter.apps.create(`sandbox-${ workerName }`, 'https://worker-sandbox.puter.com/');
}
if ( sandboxApp.owner.uuid !== user.uuid ) {
throw new Error(`Sandbox context is not owned by you! This worker's sandbox is currently owned by: ${ sandboxApp.owner.username }`);
}
appId = sandboxApp.uid;
}
}
if ( typeof (appName) === 'string' ) {
appId = ((await puter.apps.list()).find(el => el.name === appName)).uid;
}
@@ -121,7 +137,7 @@ export class WorkersHandler {
const loggingEndpoint = await utils.make_driver_method([], 'workers', 'worker-service', 'getLoggingUrl')(puter.authToken, workerName);
const socket = new WebSocket(`${loggingEndpoint}/${puter.authToken}/${workerName}`);
const logStreamObject = new EventTarget();
logStreamObject.onLog = (data) => {
logStreamObject.onLog = (_data) => {
};
@@ -132,11 +148,11 @@ export class WorkersHandler {
socket.addEventListener('message', (event) => {
controller.enqueue(JSON.parse(event.data));
});
socket.addEventListener('close', (event) => {
socket.addEventListener('close', () => {
try {
controller.close();
} catch (e) {
// no-op
}
});
},