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 ───────────────────────────────────────────────────────────
|
// ── 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 {
|
interface ProjectTracker {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
lastActivityTs: number;
|
lastActivityTs: number;
|
||||||
lastToolName: string | null;
|
lastToolName: string | null;
|
||||||
// Fix #8 (Codex audit): Counter instead of boolean for concurrent tools
|
// Fix #8 (Codex audit): Counter instead of boolean for concurrent tools
|
||||||
toolsInFlight: number;
|
toolsInFlight: number;
|
||||||
|
// Feature 10: Per-tool tracking map
|
||||||
|
activeToolMap: Map<string, ToolEntry>;
|
||||||
costSnapshots: Array<[number, number]>; // [timestamp, costUsd]
|
costSnapshots: Array<[number, number]>; // [timestamp, costUsd]
|
||||||
totalTokens: number;
|
totalTokens: number;
|
||||||
totalCost: number;
|
totalCost: number;
|
||||||
status: 'inactive' | 'running' | 'idle' | 'done' | 'error';
|
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 trackers = $state<Map<string, ProjectTracker>>(new Map());
|
||||||
let tickTs = $state<number>(Date.now());
|
let tickTs = $state<number>(Date.now());
|
||||||
let tickInterval: ReturnType<typeof setInterval> | null = null;
|
let tickInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,12 @@ export interface PluginMeta {
|
||||||
description: string;
|
description: string;
|
||||||
main: string;
|
main: string;
|
||||||
permissions: 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 {
|
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')) {
|
if (permissions.includes('events')) {
|
||||||
api.events = {
|
api.events = {
|
||||||
on(event, callback) {
|
on(event, callback) {
|
||||||
|
|
@ -173,13 +200,16 @@ export async function loadPlugin(meta: PluginMeta): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate permissions
|
// 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) {
|
for (const p of meta.permissions) {
|
||||||
if (!validPerms.has(p)) {
|
if (!validPerms.has(p)) {
|
||||||
throw new Error(`Plugin '${meta.id}' requests unknown permission: ${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
|
// Read plugin code via RPC
|
||||||
let code: string;
|
let code: string;
|
||||||
try {
|
try {
|
||||||
|
|
@ -253,11 +283,22 @@ export async function loadPlugin(meta: PluginMeta): Promise<void> {
|
||||||
type: 'init',
|
type: 'init',
|
||||||
code,
|
code,
|
||||||
permissions: meta.permissions,
|
permissions: meta.permissions,
|
||||||
|
allowedOrigins: meta.allowedOrigins ?? [],
|
||||||
meta: { id: meta.id, name: meta.name, version: meta.version, description: meta.description },
|
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 = () => {
|
const cleanup = () => {
|
||||||
|
if (runtimeTimer) clearTimeout(runtimeTimer);
|
||||||
commandRemover?.(meta.id);
|
commandRemover?.(meta.id);
|
||||||
for (const sub of eventSubscriptions) eventBus?.off(sub.event, sub.handler);
|
for (const sub of eventSubscriptions) eventBus?.off(sub.event, sub.handler);
|
||||||
eventSubscriptions.length = 0;
|
eventSubscriptions.length = 0;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue