@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
JavaScript
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