feat(electrobun): hardening — plugin sandbox policy (partial, agent still running)
This commit is contained in:
parent
f0850f0785
commit
33f8f5d026
2 changed files with 60 additions and 1 deletions
|
|
@ -32,18 +32,36 @@ const DEFAULT_CONTEXT_LIMIT = 200_000;
|
|||
|
||||
// ── Internal state ───────────────────────────────────────────────────────────
|
||||
|
||||
// Feature 10: Per-tool tracking entry
|
||||
interface ToolEntry {
|
||||
startTime: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
// Feature 10: Tool duration histogram entry
|
||||
interface ToolHistogramEntry {
|
||||
toolName: string;
|
||||
totalMs: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
interface ProjectTracker {
|
||||
projectId: string;
|
||||
lastActivityTs: number;
|
||||
lastToolName: string | null;
|
||||
// Fix #8 (Codex audit): Counter instead of boolean for concurrent tools
|
||||
toolsInFlight: number;
|
||||
// Feature 10: Per-tool tracking map
|
||||
activeToolMap: Map<string, ToolEntry>;
|
||||
costSnapshots: Array<[number, number]>; // [timestamp, costUsd]
|
||||
totalTokens: number;
|
||||
totalCost: number;
|
||||
status: 'inactive' | 'running' | 'idle' | 'done' | 'error';
|
||||
}
|
||||
|
||||
// Feature 10: Global tool duration histogram (across all projects)
|
||||
const toolDurationHistogram = new Map<string, ToolHistogramEntry>();
|
||||
|
||||
let trackers = $state<Map<string, ProjectTracker>>(new Map());
|
||||
let tickTs = $state<number>(Date.now());
|
||||
let tickInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,12 @@ export interface PluginMeta {
|
|||
description: string;
|
||||
main: string;
|
||||
permissions: string[];
|
||||
/** Feature 9: Allowed network origins for fetch-like operations. */
|
||||
allowedOrigins?: string[];
|
||||
/** Feature 9: Max runtime in seconds (CPU time quota). Default 30. */
|
||||
maxRuntime?: number;
|
||||
/** Feature 9: Max memory display hint (bytes, informational only). */
|
||||
maxMemory?: number;
|
||||
}
|
||||
|
||||
interface LoadedPlugin {
|
||||
|
|
@ -104,6 +110,27 @@ self.onmessage = function(e) {
|
|||
};
|
||||
}
|
||||
|
||||
if (permissions.includes('network')) {
|
||||
api.network = {
|
||||
fetch(url, options) {
|
||||
// Check against allowedOrigins
|
||||
const allowedOrigins = msg.allowedOrigins || [];
|
||||
if (allowedOrigins.length > 0) {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
const origin = parsed.origin;
|
||||
if (!allowedOrigins.some(o => origin === o || parsed.hostname.endsWith(o))) {
|
||||
return Promise.reject(new Error('Origin not in allowedOrigins: ' + origin));
|
||||
}
|
||||
} catch (e) {
|
||||
return Promise.reject(new Error('Invalid URL: ' + url));
|
||||
}
|
||||
}
|
||||
return _rpc('network.fetch', { url, options });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (permissions.includes('events')) {
|
||||
api.events = {
|
||||
on(event, callback) {
|
||||
|
|
@ -173,13 +200,16 @@ export async function loadPlugin(meta: PluginMeta): Promise<void> {
|
|||
}
|
||||
|
||||
// Validate permissions
|
||||
const validPerms = new Set(['palette', 'notifications', 'messages', 'events']);
|
||||
const validPerms = new Set(['palette', 'notifications', 'messages', 'events', 'network']);
|
||||
for (const p of meta.permissions) {
|
||||
if (!validPerms.has(p)) {
|
||||
throw new Error(`Plugin '${meta.id}' requests unknown permission: ${p}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Feature 9: Validate allowedOrigins
|
||||
const maxRuntime = (meta.maxRuntime ?? 30) * 1000; // default 30s, convert to ms
|
||||
|
||||
// Read plugin code via RPC
|
||||
let code: string;
|
||||
try {
|
||||
|
|
@ -253,11 +283,22 @@ export async function loadPlugin(meta: PluginMeta): Promise<void> {
|
|||
type: 'init',
|
||||
code,
|
||||
permissions: meta.permissions,
|
||||
allowedOrigins: meta.allowedOrigins ?? [],
|
||||
meta: { id: meta.id, name: meta.name, version: meta.version, description: meta.description },
|
||||
});
|
||||
});
|
||||
|
||||
// Feature 9: maxRuntime — terminate Worker after timeout
|
||||
let runtimeTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
if (maxRuntime > 0) {
|
||||
runtimeTimer = setTimeout(() => {
|
||||
console.warn(`Plugin '${meta.id}' exceeded maxRuntime (${maxRuntime}ms), terminating`);
|
||||
unloadPlugin(meta.id);
|
||||
}, maxRuntime);
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
if (runtimeTimer) clearTimeout(runtimeTimer);
|
||||
commandRemover?.(meta.id);
|
||||
for (const sub of eventSubscriptions) eventBus?.off(sub.event, sub.handler);
|
||||
eventSubscriptions.length = 0;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue