mirror of
https://github.com/HeyPuter/puter.git
synced 2026-08-25 23:46:51 +00:00
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:
@@ -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 ) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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 () {
|
||||
|
||||
@@ -1953,6 +1953,10 @@ label {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.publish-worker-error-msg{
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.error {
|
||||
display: none;
|
||||
color: red;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user