UNPKG

@stackmemoryai/stackmemory

Version:

Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.

665 lines (664 loc) 19.7 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { z } from "zod"; import { EventEmitter } from "events"; import { ValidationError, ErrorCode, SystemError } from "../errors/index.js"; import { logger } from "../monitoring/logger.js"; const PermissionManifestSchema = z.object({ name: z.string().min(1).max(100), version: z.string().regex(/^\d+\.\d+\.\d+$/), permissions: z.array( z.discriminatedUnion("type", [ z.object({ type: z.literal("network"), domains: z.array(z.string().min(1)) }), z.object({ type: z.literal("storage:local"), quotaBytes: z.number().positive().optional() }), z.object({ type: z.literal("storage:indexed"), quotaBytes: z.number().positive().optional() }), z.object({ type: z.literal("frames:read"), scope: z.enum(["own", "all"]).optional() }), z.object({ type: z.literal("frames:write"), scope: z.enum(["own", "all"]).optional() }), z.object({ type: z.literal("events:emit"), patterns: z.array(z.string()).optional() }), z.object({ type: z.literal("events:subscribe"), patterns: z.array(z.string()).optional() }), z.object({ type: z.literal("dom:read") }), z.object({ type: z.literal("dom:write") }) ]) ), description: z.string().max(1e3).optional(), author: z.string().max(200).optional(), homepage: z.string().url().optional() }); class SandboxRuntime { extensions = /* @__PURE__ */ new Map(); eventBus = new EventEmitter(); config; frameManager; stateSerializer; constructor(config = {}) { this.config = { mode: config.mode ?? "node", timeout: config.timeout ?? 3e4, memoryLimit: config.memoryLimit, networkAllowlist: config.networkAllowlist ?? [], enableDevtools: config.enableDevtools ?? false }; logger.info("SandboxRuntime initialized", { mode: this.config.mode, timeout: this.config.timeout }); } /** * Set the frame manager API for extensions */ setFrameManager(frameManager) { this.frameManager = frameManager; } /** * Set the state serializer API for extensions */ setStateSerializer(stateSerializer) { this.stateSerializer = stateSerializer; } /** * Load an extension from a source */ async loadExtension(source) { logger.info("Loading extension", { source }); const extension = await this.resolveExtension(source); const manifest = extension.manifest; this.validateManifest(manifest); if (this.extensions.has(manifest.name)) { throw new ValidationError( `Extension "${manifest.name}" is already loaded`, ErrorCode.VALIDATION_FAILED, { extensionName: manifest.name } ); } const context = this.createExtensionContext(manifest); const state = { extension, context, status: "loading", loadedAt: Date.now(), source }; this.extensions.set(manifest.name, state); try { await this.withTimeout( extension.init(context), this.config.timeout, `Extension "${manifest.name}" initialization timed out` ); state.status = "active"; logger.info("Extension loaded successfully", { name: manifest.name, version: manifest.version }); return manifest.name; } catch (error) { state.status = "error"; state.error = error instanceof Error ? error : new Error(String(error)); logger.error("Extension initialization failed", { name: manifest.name, error: state.error.message }); throw error; } } /** * Unload an extension */ async unloadExtension(extensionName) { const state = this.extensions.get(extensionName); if (!state) { throw new ValidationError( `Extension "${extensionName}" not found`, ErrorCode.RESOURCE_NOT_FOUND, { extensionName } ); } try { await this.withTimeout( state.extension.destroy(), this.config.timeout, `Extension "${extensionName}" destruction timed out` ); } catch (error) { logger.warn("Extension destruction failed", { name: extensionName, error: error instanceof Error ? error.message : String(error) }); } state.status = "unloaded"; this.extensions.delete(extensionName); logger.info("Extension unloaded", { name: extensionName }); } /** * Hot-reload an extension (unload and reload) */ async reloadExtension(extensionName) { const state = this.extensions.get(extensionName); if (!state) { throw new ValidationError( `Extension "${extensionName}" not found`, ErrorCode.RESOURCE_NOT_FOUND, { extensionName } ); } const source = state.source; await this.unloadExtension(extensionName); await this.loadExtension(source); logger.info("Extension reloaded", { name: extensionName }); } /** * Get extension by name */ getExtension(extensionName) { return this.extensions.get(extensionName)?.extension; } /** * Get all loaded extensions */ listExtensions() { return Array.from(this.extensions.entries()).map(([name, state]) => ({ name, version: state.extension.version, status: state.status, loadedAt: state.loadedAt })); } /** * Get tools from all active extensions */ getTools() { const tools = []; for (const state of Array.from(this.extensions.values())) { if (state.status === "active" && state.extension.tools) { tools.push(...state.extension.tools); } } return tools; } /** * Get providers from all active extensions */ getProviders() { const providers = []; for (const state of Array.from(this.extensions.values())) { if (state.status === "active" && state.extension.providers) { providers.push(...state.extension.providers); } } return providers; } /** * Emit event to all extensions */ emit(event, data) { this.eventBus.emit(event, data); } /** * Shutdown the runtime */ async shutdown() { logger.info("Shutting down SandboxRuntime"); const names = Array.from(this.extensions.keys()); for (const name of names) { try { await this.unloadExtension(name); } catch (error) { logger.warn("Failed to unload extension during shutdown", { name, error: error instanceof Error ? error.message : String(error) }); } } this.eventBus.removeAllListeners(); logger.info("SandboxRuntime shutdown complete"); } // ============================================================================ // Private Methods // ============================================================================ /** * Resolve extension from source */ async resolveExtension(source) { switch (source.type) { case "inline": return this.loadInlineExtension(source.code, source.manifest); case "file": return this.loadFileExtension(source.path); case "url": return this.loadUrlExtension(source.url); case "npm": return this.loadNpmExtension(source.package, source.version); default: throw new ValidationError( "Invalid extension source type", ErrorCode.VALIDATION_FAILED, { source } ); } } /** * Load inline extension */ async loadInlineExtension(code, manifest) { const AsyncFunction = Object.getPrototypeOf( async function() { } ).constructor; try { const factory = new AsyncFunction( "manifest", ` ${code} return { ...exports, manifest }; ` ); const extension = await factory(manifest); return extension; } catch (error) { throw new SystemError( `Failed to load inline extension: ${error instanceof Error ? error.message : String(error)}`, ErrorCode.INITIALIZATION_ERROR, { manifest: manifest.name } ); } } /** * Load extension from file */ async loadFileExtension(filePath) { try { const module = await import(filePath); const extension = module.default || module; if (!extension.name || !extension.version || !extension.manifest) { throw new Error("Invalid extension export: missing required fields"); } return extension; } catch (error) { throw new SystemError( `Failed to load extension from file: ${error instanceof Error ? error.message : String(error)}`, ErrorCode.FILE_NOT_FOUND, { filePath } ); } } /** * Load extension from URL */ async loadUrlExtension(url) { try { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const code = await response.text(); const manifestMatch = code.match( /export\s+const\s+manifest\s*=\s*({[\s\S]*?});/ ); if (!manifestMatch) { throw new Error("No manifest found in extension code"); } const manifest = JSON.parse( manifestMatch[1].replace(/'/g, '"') ); return this.loadInlineExtension(code, manifest); } catch (error) { throw new SystemError( `Failed to load extension from URL: ${error instanceof Error ? error.message : String(error)}`, ErrorCode.NETWORK_ERROR, { url } ); } } /** * Load extension from npm package */ async loadNpmExtension(packageName, version) { try { const packagePath = version ? `${packageName}@${version}` : packageName; const module = await import(packagePath); const extension = module.default || module; if (!extension.name || !extension.version || !extension.manifest) { throw new Error("Invalid extension export: missing required fields"); } return extension; } catch (error) { throw new SystemError( `Failed to load extension from npm: ${error instanceof Error ? error.message : String(error)}`, ErrorCode.INITIALIZATION_ERROR, { packageName, version } ); } } /** * Validate permission manifest */ validateManifest(manifest) { const result = PermissionManifestSchema.safeParse(manifest); if (!result.success) { throw new ValidationError( `Invalid extension manifest: ${result.error.errors.map((e) => e.message).join(", ")}`, ErrorCode.VALIDATION_FAILED, { errors: result.error.errors } ); } const networkPerms = manifest.permissions.filter( (p) => p.type === "network" ); for (const perm of networkPerms) { for (const domain of perm.domains) { if (!this.isDomainAllowed(domain)) { throw new ValidationError( `Domain "${domain}" is not in the global allowlist`, ErrorCode.PERMISSION_VIOLATION, { domain, allowlist: this.config.networkAllowlist } ); } } } } /** * Check if a domain is allowed */ isDomainAllowed(domain) { if (!this.config.networkAllowlist || this.config.networkAllowlist.length === 0) { return true; } return this.config.networkAllowlist.some((allowed) => { if (allowed === domain) return true; if (allowed.startsWith("*.")) { const suffix = allowed.slice(1); return domain.endsWith(suffix) || domain === allowed.slice(2); } return false; }); } /** * Create sandboxed extension context */ createExtensionContext(manifest) { const extensionId = `${manifest.name}@${manifest.version}`; const sandboxedFetch = this.createSandboxedFetch(manifest); const sandboxedStorage = this.createSandboxedStorage(manifest); const framesAPI = this.createFramesAPI(manifest); const stateAPI = this.createStateAPI(manifest); const handlers = /* @__PURE__ */ new Map(); const context = { extensionId, extensionName: manifest.name, permissions: manifest.permissions, frames: framesAPI, state: stateAPI, fetch: sandboxedFetch, storage: sandboxedStorage, emit: (event, data) => { if (!this.hasPermission(manifest, "events:emit", event)) { logger.warn("Extension attempted to emit without permission", { extension: manifest.name, event }); return; } this.eventBus.emit(event, data); }, on: (event, handler) => { if (!this.hasPermission(manifest, "events:subscribe", event)) { logger.warn("Extension attempted to subscribe without permission", { extension: manifest.name, event }); return () => { }; } if (!handlers.has(event)) { handlers.set(event, /* @__PURE__ */ new Set()); } handlers.get(event).add(handler); this.eventBus.on(event, handler); return () => { handlers.get(event)?.delete(handler); this.eventBus.off(event, handler); }; }, off: (event, handler) => { handlers.get(event)?.delete(handler); this.eventBus.off(event, handler); } }; return context; } /** * Create sandboxed fetch function */ createSandboxedFetch(manifest) { const networkPerms = manifest.permissions.filter( (p) => p.type === "network" ); const allowedDomains = networkPerms.flatMap((p) => p.domains); return async (input, init) => { const url = typeof input === "string" ? new URL(input) : input instanceof URL ? input : new URL(input.url); const isAllowed = allowedDomains.some((domain) => { if (domain === url.hostname) return true; if (domain.startsWith("*.")) { const suffix = domain.slice(1); return url.hostname.endsWith(suffix); } return false; }); if (!isAllowed) { throw new ValidationError( `Network access to "${url.hostname}" is not permitted`, ErrorCode.PERMISSION_VIOLATION, { hostname: url.hostname, allowed: allowedDomains } ); } return globalThis.fetch(input, init); }; } /** * Create sandboxed storage */ createSandboxedStorage(manifest) { const hasLocalStorage = manifest.permissions.some( (p) => p.type === "storage:local" ); if (!hasLocalStorage) { return { getItem: () => null, setItem: () => { }, removeItem: () => { }, clear: () => { }, key: () => null, length: 0 }; } const storageKey = `ext:${manifest.name}:`; const storage = /* @__PURE__ */ new Map(); return { getItem: (key) => storage.get(storageKey + key) ?? null, setItem: (key, value) => { storage.set(storageKey + key, value); }, removeItem: (key) => { storage.delete(storageKey + key); }, clear: () => { for (const key of Array.from(storage.keys())) { if (key.startsWith(storageKey)) { storage.delete(key); } } }, key: (index) => { const keys = Array.from(storage.keys()).filter( (k) => k.startsWith(storageKey) ); return keys[index]?.slice(storageKey.length) ?? null; }, get length() { return Array.from(storage.keys()).filter( (k) => k.startsWith(storageKey) ).length; } }; } /** * Create frames API with permission checks */ createFramesAPI(manifest) { const canRead = manifest.permissions.some((p) => p.type === "frames:read"); const canWrite = manifest.permissions.some( (p) => p.type === "frames:write" ); const noOpFrameManager = { getCurrentFrame: async () => null, getFrame: async () => null, listFrames: async () => [] }; if (!canRead && !this.frameManager) { return noOpFrameManager; } const fm = this.frameManager; const api = { getCurrentFrame: async () => { if (!canRead || !fm) return null; return fm.getCurrentFrame(); }, getFrame: async (frameId) => { if (!canRead || !fm) return null; return fm.getFrame(frameId); }, listFrames: async (filter) => { if (!canRead || !fm) return []; return fm.listFrames(filter); } }; if (canWrite && fm?.createFrame) { api.createFrame = async (options) => { return fm.createFrame(options); }; } if (canWrite && fm?.closeFrame) { api.closeFrame = async (frameId) => { return fm.closeFrame(frameId); }; } return api; } /** * Create state API with permission checks */ createStateAPI(manifest) { const canRead = manifest.permissions.some((p) => p.type === "frames:read"); const canWrite = manifest.permissions.some( (p) => p.type === "frames:write" ); const prefix = `ext:${manifest.name}:`; const ss = this.stateSerializer; return { get: async (key) => { if (!canRead || !ss) return null; return ss.get(prefix + key); }, set: async (key, value) => { if (!canWrite || !ss) return; await ss.set(prefix + key, value); }, delete: async (key) => { if (!canWrite || !ss) return false; return ss.delete(prefix + key); }, getDocument: async (key) => { if (!canRead || !ss) return null; return ss.getDocument(prefix + key); }, setDocument: async (key, content) => { if (!canWrite || !ss) return; await ss.setDocument(prefix + key, content); }, deleteDocument: async (key) => { if (!canWrite || !ss) return false; return ss.deleteDocument(prefix + key); } }; } /** * Check if extension has specific permission */ hasPermission(manifest, type, target) { const perm = manifest.permissions.find((p) => p.type === type); if (!perm) return false; if ((type === "events:emit" || type === "events:subscribe") && target) { const eventPerm = perm; if (!eventPerm.patterns || eventPerm.patterns.length === 0) { return true; } return eventPerm.patterns.some( (pattern) => this.matchPattern(pattern, target) ); } return true; } /** * Match glob-style pattern */ matchPattern(pattern, value) { const regex = new RegExp( "^" + pattern.replace(/\*/g, ".*").replace(/\?/g, ".") + "$" ); return regex.test(value); } /** * Execute with timeout */ async withTimeout(promise, timeoutMs, message) { const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error(message)), timeoutMs); }); return Promise.race([promise, timeoutPromise]); } } function createSandboxRuntime(config) { return new SandboxRuntime(config); } function createManifest(name, version, permissions, options) { return { name, version, permissions, ...options }; } var sandbox_runtime_default = SandboxRuntime; export { SandboxRuntime, createManifest, createSandboxRuntime, sandbox_runtime_default as default }; //# sourceMappingURL=sandbox-runtime.js.map