UNPKG

trellis

Version:

Agentic State Engine — event-sourced causal graph with branching, decision traces, and realtime sync for AI-native applications

1,623 lines (1,607 loc) 76.2 kB
import { McpServer, StreamableMcpGateway, corsEnabledForConfig, corsPreflightResponse, discoveryMcpGateway, mcpServiceDocument, oauthAuthorizationServerMetadata, oauthProtectedResourceMetadata, withCors } from "./chunk-WGKXHUAD.js"; import { McpRateLimitError, UsageMeter, assertMcpBudget, collectionSlugFromMetaId, resolveCollectionId, resolveMcpTenantId, resolveUsageTenantId, verifyAdminKey, writeContext } from "./chunk-2NJRCGWJ.js"; import { DEFAULT_TENANT } from "./chunk-GA6RZXIK.js"; import { roomMcpPathForUrl } from "./chunk-YC5I32PS.js"; import { entityRecordToPlain, hydrateBindings, resolveRelations } from "./chunk-JUEMDWLU.js"; import { FORM_MODES, listFormableTypes, readFormOverrides, resolveFormDescriptor } from "./chunk-VKZC4ZIY.js"; import { rel } from "./chunk-E4T2QHKA.js"; import { DAGScheduler, WorkerPool } from "./chunk-ZJ3NSF63.js"; import { parseSimple } from "./chunk-LTBGCNC4.js"; import { RealtimeFieldError } from "./chunk-LEGH72HW.js"; import { PROVENANCE, init_canonical_op } from "./chunk-RUMOVKR4.js"; // src/server/server.ts import { existsSync, readFileSync } from "fs"; import { dirname, join } from "path"; import { fileURLToPath } from "url"; init_canonical_op(); // src/server/auth.ts var ANONYMOUS = { userId: null, tenantId: null, roles: [], claims: {}, authenticated: false }; function base64UrlEncode(input) { return btoa(String.fromCharCode(...input)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); } function base64UrlDecode(input) { const padded = input.replace(/-/g, "+").replace(/_/g, "/"); const padLen = (4 - padded.length % 4) % 4; const base64 = padded + "=".repeat(padLen); const binary = atob(base64); return Uint8Array.from(binary, (c) => c.charCodeAt(0)); } async function hmacKey(secret) { return crypto.subtle.importKey( "raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign", "verify"] ); } async function signJwt(payload, secret, expiresInSeconds = 86400) { const header = { alg: "HS256", typ: "JWT" }; const now = Math.floor(Date.now() / 1e3); const claims = { iat: now, exp: now + expiresInSeconds, ...payload }; const enc = new TextEncoder(); const headerB64 = base64UrlEncode(enc.encode(JSON.stringify(header))); const payloadB64 = base64UrlEncode(enc.encode(JSON.stringify(claims))); const signing = `${headerB64}.${payloadB64}`; const key = await hmacKey(secret); const sig = await crypto.subtle.sign("HMAC", key, enc.encode(signing)); const sigB64 = base64UrlEncode(new Uint8Array(sig)); return `${signing}.${sigB64}`; } async function verifyJwt(token, secret) { const parts = token.split("."); if (parts.length !== 3) return null; const [headerB64, payloadB64, sigB64] = parts; const enc = new TextEncoder(); const signing = `${headerB64}.${payloadB64}`; try { const key = await hmacKey(secret); const sigRaw = base64UrlDecode(sigB64); const sigBuf = sigRaw.buffer.slice( sigRaw.byteOffset, sigRaw.byteOffset + sigRaw.byteLength ); const valid = await crypto.subtle.verify( "HMAC", key, sigBuf, enc.encode(signing) ); if (!valid) return null; const claims = JSON.parse( new TextDecoder().decode(base64UrlDecode(payloadB64)) ); const now = Math.floor(Date.now() / 1e3); if (typeof claims.exp === "number" && claims.exp < now) return null; return claims; } catch { return null; } } async function resolveAuth(authHeader, config) { if (!authHeader) { return config.allowPublic === false ? { ...ANONYMOUS, authenticated: false } : ANONYMOUS; } const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : authHeader; if (config.apiKey && token === config.apiKey) { return { userId: "service", tenantId: null, roles: ["admin"], claims: { sub: "service" }, authenticated: true }; } if (config.jwtSecret) { const claims = await verifyJwt(token, config.jwtSecret); if (claims) { return { userId: claims.sub ?? null, tenantId: claims.tenantId ?? null, roles: Array.isArray(claims.roles) ? claims.roles : typeof claims.role === "string" ? [claims.role] : [], claims, authenticated: true }; } } return ANONYMOUS; } var GOOGLE_PROVIDER = { name: "google", authUrl: "https://accounts.google.com/o/oauth2/v2/auth", tokenUrl: "https://oauth2.googleapis.com/token", userInfoUrl: "https://www.googleapis.com/oauth2/v3/userinfo", scopes: ["openid", "email", "profile"] }; var GITHUB_PROVIDER = { name: "github", authUrl: "https://github.com/login/oauth/authorize", tokenUrl: "https://github.com/login/oauth/access_token", userInfoUrl: "https://api.github.com/user", scopes: ["read:user", "user:email"] }; function buildOAuthUrl(provider, redirectUri, state) { const params = new URLSearchParams({ client_id: provider.clientId, redirect_uri: redirectUri, response_type: "code", scope: provider.scopes.join(" "), state }); return `${provider.authUrl}?${params}`; } async function exchangeOAuthCode(provider, code, redirectUri) { const tokenRes = await fetch(provider.tokenUrl, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" }, body: new URLSearchParams({ client_id: provider.clientId, client_secret: provider.clientSecret, code, redirect_uri: redirectUri, grant_type: "authorization_code" }) }); if (!tokenRes.ok) { throw new Error(`OAuth token exchange failed: ${tokenRes.status}`); } const tokenData = await tokenRes.json(); const accessToken = tokenData.access_token; const userRes = await fetch(provider.userInfoUrl, { headers: { Authorization: `Bearer ${accessToken}`, Accept: "application/json" } }); if (!userRes.ok) { throw new Error(`OAuth user info fetch failed: ${userRes.status}`); } const user = await userRes.json(); return { id: String(user.id ?? user.sub ?? ""), email: String(user.email ?? ""), name: String(user.name ?? user.login ?? ""), avatarUrl: user.picture ?? user.avatar_url ?? void 0 }; } // src/server/permissions.ts var PermissionRegistry = class { rules = /* @__PURE__ */ new Map(); defaultRule = "authenticated"; /** * Register permissions for an entity type. */ register(entityType, permissions) { this.rules.set(entityType, permissions); } /** * Set the fallback rule used when an entity type has no declared permissions. */ setDefault(rule) { this.defaultRule = rule; } /** * Get the permission rule for a specific operation on an entity type. * Falls back to the default rule if not declared. */ getRule(entityType, op) { const def = this.rules.get(entityType); return def?.[op] ?? this.defaultRule; } /** * Check whether an auth context is allowed to perform an operation. */ check(auth, entityType, op, entity = null) { const rule = this.getRule(entityType, op); return evaluateRule(rule, auth, entity); } /** * Assert access — throws a PermissionError if denied. */ assert(auth, entityType, op, entity = null) { if (!this.check(auth, entityType, op, entity)) { throw new PermissionError(auth, entityType, op); } } }; function evaluateRule(rule, auth, entity) { if (rule === "public") { return true; } if (rule === "authenticated") { return auth.authenticated; } if (rule === "own") { if (!auth.authenticated || !auth.userId) return false; const ownerFact = entity?.facts.find( (f) => f.a === "ownerId" || f.a === "createdBy" ); return ownerFact?.v === auth.userId; } if (typeof rule === "object") { if ("role" in rule) { return auth.roles.includes(rule.role); } if ("roles" in rule) { return rule.roles.some((r) => auth.roles.includes(r)); } if ("fn" in rule) { try { return rule.fn(auth, entity); } catch { return false; } } } return false; } var PermissionError = class extends Error { constructor(auth, entityType, op) { const who = auth.authenticated ? `user:${auth.userId}` : "anonymous"; super(`Permission denied: ${who} cannot ${op} ${entityType}`); this.auth = auth; this.entityType = entityType; this.op = op; this.name = "PermissionError"; } toResponse() { return { error: "Forbidden", message: this.message, code: 403 }; } }; var PUBLIC_READ = { read: "public", create: "authenticated", update: "authenticated", delete: "authenticated" }; var FULLY_PUBLIC = { read: "public", create: "public", update: "public", delete: "public" }; var OWNER_ONLY = { read: "own", create: "authenticated", update: "own", delete: "own" }; var ADMIN_ONLY = { read: { role: "admin" }, create: { role: "admin" }, update: { role: "admin" }, delete: { role: "admin" } }; // src/schema/kernel-resolve.ts import { z } from "zod"; function ontologyTypeName(id) { return id.includes(":") ? id.split(":").pop() : id; } function findOntologyByTypeName(kernel, typeName) { return kernel.listOntologies().find((s) => { const short = ontologyTypeName(s["@id"]); return short === typeName || short.toLowerCase() === typeName.toLowerCase() || s.label === typeName; }); } function schemaHandleFromOntology(def, typeName) { const name = typeName ?? ontologyTypeName(def["@id"]); const relations = {}; for (const field of def.fields) { if (field.valueType === "relation" && field.relation?.targetSchema) { const target = ontologyTypeName(field.relation.targetSchema); relations[field.name] = rel( target, field.relation.cardinality ?? "one" ); } } return { type: name, zod: z.object({}), relations, computed: {}, definition: def, toOntologySchema: () => { throw new Error("schemaHandleFromOntology: legacy adapter not available"); } }; } function createSchemaLookup(kernel) { const cache = /* @__PURE__ */ new Map(); return (typeName) => { if (cache.has(typeName)) return cache.get(typeName); const def = findOntologyByTypeName(kernel, typeName); if (!def) return null; const handle = schemaHandleFromOntology(def, typeName); cache.set(typeName, handle); return handle; }; } function createKernelResolveClient(kernel) { return { read: async (id) => { const entity = kernel.getEntity(id); return entity ? entityRecordToPlain(entity) : null; }, query: async (q) => { const parsed = parseSimple(q); const qr = await kernel.query(parsed); return { bindings: hydrateBindings( kernel, qr.bindings ), executionTime: qr.executionTime }; } }; } async function hydrateAndResolve(kernel, bindings, entityType, resolve) { let entities = hydrateBindings(kernel, bindings); if (!entityType || !resolve || Object.keys(resolve).length === 0) { return entities; } const def = findOntologyByTypeName(kernel, entityType); if (!def) return entities; const schema = schemaHandleFromOntology(def, entityType); const client = createKernelResolveClient(kernel); const lookup = createSchemaLookup(kernel); return resolveRelations(client, schema, entities, resolve, { schemaLookup: lookup }); } // src/server/realtime.ts var SubscriptionManager = class { clients = /* @__PURE__ */ new Map(); pool; permissions; meter; constructor(pool, permissions = null, meter = null) { this.pool = pool; this.permissions = permissions; this.meter = meter; } // ------------------------------------------------------------------------- // Client lifecycle // ------------------------------------------------------------------------- addClient(clientId, ws, auth, tenantId) { this.clients.set(clientId, { id: clientId, ws, subscriptions: /* @__PURE__ */ new Map(), auth, tenantId }); } removeClient(clientId) { this.clients.delete(clientId); } // ------------------------------------------------------------------------- // Message handling // ------------------------------------------------------------------------- async handleMessage(clientId, raw) { const client = this.clients.get(clientId); if (!client) return; let msg; try { msg = JSON.parse(raw); } catch { this._send(client, { type: "error", id: "", message: "Invalid JSON" }); return; } if (msg.type === "ping") { this._send(client, { type: "pong" }); return; } if (msg.type === "subscribe") { await this._handleSubscribe(client, msg.id, msg.query, { tenantId: msg.tenantId, entityType: msg.entityType, resolve: msg.resolve }); return; } if (msg.type === "unsubscribe") { client.subscriptions.delete(msg.id); return; } } // ------------------------------------------------------------------------- // Notify — called after every mutation // ------------------------------------------------------------------------- /** * Re-evaluate all subscriptions for a given tenant and push diffs. * Called after every write op lands. */ async notify(tenantId) { const tid = tenantId ?? null; const dead = []; for (const [clientId, client] of this.clients) { if (client.ws.readyState !== 1) { dead.push(clientId); continue; } for (const [subId, sub] of client.subscriptions) { if (sub.tenantId !== tid) continue; await this._pushUpdate(client, sub); } } for (const id of dead) this.clients.delete(id); } get clientCount() { return this.clients.size; } // ------------------------------------------------------------------------- // Private // ------------------------------------------------------------------------- async _handleSubscribe(client, subId, queryStr, opts = {}) { const tid = opts.tenantId ?? client.tenantId ?? null; let parsedQuery; try { parsedQuery = parseSimple(queryStr); } catch (err) { this._send(client, { type: "error", id: subId, message: `Invalid query: ${err instanceof Error ? err.message : String(err)}` }); return; } const kernel = await this.pool.preload(tid); let result; try { const qr = await kernel.query(parsedQuery); this._recordGraphIo(tid); result = await hydrateAndResolve( kernel, qr.bindings, opts.entityType, opts.resolve ); } catch (err) { this._send(client, { type: "error", id: subId, message: `Query failed: ${err instanceof Error ? err.message : String(err)}` }); return; } const resolved = Boolean(opts.entityType) && Boolean(opts.resolve && Object.keys(opts.resolve).length > 0); const sub = { id: subId, query: queryStr, tenantId: tid, auth: client.auth, lastResult: result, entityType: opts.entityType, resolve: opts.resolve }; client.subscriptions.set(subId, sub); if (tid !== null && client.tenantId !== tid) { client.tenantId = tid; } this._send(client, { type: "subscribed", id: subId }); this._send(client, { type: "data", id: subId, result, diff: { added: result, updated: [], removed: [] }, ...resolved ? { resolved: true } : {} }); } async _pushUpdate(client, sub) { const kernel = await this.pool.preload(sub.tenantId); let newResult; try { const parsed = parseSimple(sub.query); const qr = await kernel.query(parsed); this._recordGraphIo(sub.tenantId); newResult = await hydrateAndResolve( kernel, qr.bindings, sub.entityType, sub.resolve ); } catch { return; } const diff = computeDiff(sub.lastResult, newResult); if (diff.added.length === 0 && diff.updated.length === 0 && diff.removed.length === 0) { return; } sub.lastResult = newResult; const resolved = Boolean(sub.entityType) && Boolean(sub.resolve && Object.keys(sub.resolve).length > 0); this._send(client, { type: "data", id: sub.id, result: newResult, diff, ...resolved ? { resolved: true } : {} }); } _recordGraphIo(tenantId) { this.meter?.recordGraphIo(resolveUsageTenantId(tenantId)); } _send(client, payload) { const data = JSON.stringify(payload); if (this.meter) { const tid = resolveUsageTenantId(client.tenantId ?? DEFAULT_TENANT); this.meter.recordEgress(tid, new TextEncoder().encode(data).length); } try { client.ws.send(data); } catch { } } }; function entityId(row) { return String(row["?e"] ?? row.id ?? row.e ?? JSON.stringify(row)); } function computeDiff(prev, next) { const prevMap = new Map(prev.map((r) => [entityId(r), r])); const nextMap = new Map(next.map((r) => [entityId(r), r])); const added = []; const updated = []; const removed = []; for (const [id, row] of nextMap) { if (!prevMap.has(id)) { added.push(row); } else if (JSON.stringify(prevMap.get(id)) !== JSON.stringify(row)) { updated.push(row); } } for (const [id, row] of prevMap) { if (!nextMap.has(id)) removed.push(row); } return { added, updated, removed }; } // src/mcp/room.ts import { z as z4 } from "zod"; // src/mcp/mcp-auth.ts var McpAuthError = class extends Error { constructor(message = "Authentication required for graph writes. Pass Authorization: Bearer <apiKey or JWT>.") { super(message); this.name = "McpAuthError"; } }; function assertMcpWriteAuthorized(gate) { if (!gate.requireAuthForWrites) return; if (gate.auth.authenticated) return; throw new McpAuthError(); } // src/mcp/room-audit.ts function sanitizeInput(params) { const out = {}; for (const [key, value] of Object.entries(params)) { if (typeof value === "string" && value.length > 2e3) { out[key] = value.slice(0, 2e3) + "\u2026"; } else { out[key] = value; } } return out; } function summarizeText(text4, max = 500) { return text4.length > max ? text4.slice(0, max) + "\u2026" : text4; } async function recordRoomMcpAudit(kernel, agentId, toolName, params, resultText, wctx, relatedEntities = []) { try { const id = `decision:mcp-${crypto.randomUUID().slice(0, 8)}`; const links = relatedEntities.map((targetEntityId) => ({ attribute: "relatedTo", targetEntityId })); await kernel.createEntity( id, "Decision", { title: `MCP ${toolName}`, toolName, input: JSON.stringify(sanitizeInput(params)), outputSummary: summarizeText(resultText), createdBy: agentId, source: "room-mcp" }, links.length > 0 ? links : void 0, wctx ); return id; } catch { return null; } } function scheduleRoomMcpAudit(kernelPromise, agentId, toolName, params, resultText, wctx, relatedEntities) { void kernelPromise.then( (kernel) => recordRoomMcpAudit( kernel, agentId, toolName, params, resultText, wctx, relatedEntities ) ); } // src/mcp/graph-summary.ts var SKIP_ATTRS = /* @__PURE__ */ new Set(["@id", "@type", "id", "type"]); function buildRoomGraphSummary(kernel, tenantId, opts = {}) { const limit = opts.limit ?? 10; const store = kernel.getStore(); let factCount = 0; for (const _ of store.getAllFacts()) factCount++; let linkCount = 0; for (const _ of store.getAllLinks()) linkCount++; const typeCounts = {}; const entityIds = /* @__PURE__ */ new Set(); for (const fact of store.getAllFacts()) { if (fact.a === "type") { const type = String(fact.v); typeCounts[type] = (typeCounts[type] ?? 0) + 1; entityIds.add(fact.e); } } const entityTypes = Object.entries(typeCounts).sort((a, b) => b[1] - a[1]).slice(0, limit).map(([type, count]) => ({ type, count })); const systemOntologies = []; const userOntologies = []; for (const schema of kernel.listOntologies()) { const shortId = schema["@id"].replace(/^(trellis:schema\/|core:)/, ""); if (schema.tier === "user") { userOntologies.push(shortId); } else if (schema.tier !== "core") { systemOntologies.push(shortId); } } const topAttributes = store.getCatalog().filter((c) => !SKIP_ATTRS.has(c.attribute)).sort((a, b) => b.distinctCount - a.distinctCount).slice(0, limit).map((c) => ({ attribute: c.attribute, distinctCount: c.distinctCount, cardinality: c.cardinality })); const relations = /* @__PURE__ */ new Set(); for (const link of store.getAllLinks()) relations.add(link.a); const recentMutations = kernel.readAllOps().slice(-limit).reverse().map((op) => ({ kind: op.kind, agentId: op.agentId, timestamp: op.timestamp, entityId: inferEntityIdFromOp(op) })); return { health: { status: "ok", factCount, linkCount, entityCount: entityIds.size, ops: kernel.readAllOps().length, tenantId }, entityTypes, ontologies: { total: kernel.listOntologies().length, system: systemOntologies.slice(0, limit), user: userOntologies.slice(0, limit) }, topAttributes, links: { total: linkCount, relations: [...relations].sort().slice(0, limit) }, recentMutations }; } function inferEntityIdFromOp(op) { const facts = op.facts ?? op.deleteFacts; if (!facts?.length) return void 0; const typeFact = facts.find((f) => "a" in f && f.a === "type"); return typeFact?.e ?? facts[0]?.e; } // src/mcp/agent-exec.ts import { z as z2 } from "zod"; var pools = /* @__PURE__ */ new Map(); var schedulers = /* @__PURE__ */ new Map(); function getPool(tenantId, pool) { let wp = pools.get(tenantId); if (!wp) { wp = new WorkerPool( () => pool.preload(tenantId), void 0, { concurrency: 2, pollIntervalMs: 500 } ); wp.start(); pools.set(tenantId, wp); } return wp; } function getScheduler(tenantId, pool) { let sc = schedulers.get(tenantId); if (!sc) { sc = new DAGScheduler(getPool(tenantId, pool)); schedulers.set(tenantId, sc); } return sc; } function text(content) { return { content: [{ type: "text", text: content }] }; } function jsonText(data) { return text(JSON.stringify(data, null, 2)); } var agentIdSchema = z2.string().describe("Agent ID (e.g. agent:code-reviewer)"); var inputSchema = z2.string().describe("Task input text"); var runIdSchema = z2.string().describe("Run ID (e.g. run:agent:1234567890)"); var tenantIdSchema = z2.string().optional().describe("Tenant ID for multi-tenant support"); var dagStepSchema = z2.object({ id: z2.string(), agentId: z2.string(), input: z2.string(), dependsOn: z2.array(z2.string()).optional() }); var dagWorkflowSchema = z2.object({ id: z2.string(), name: z2.string(), steps: z2.array(dagStepSchema) }); function registerAgentExecTools(server, tenantPool) { server.registerTool( "worker_pool_status", { description: "Get WorkerPool status (active, queued, concurrency).", inputSchema: { tenantId: tenantIdSchema } }, async ({ tenantId }) => { try { const tid = tenantId ?? "default"; const wp = getPool(tid, tenantPool); return jsonText(wp.getStatus()); } catch (err) { return text(err instanceof Error ? err.message : String(err)); } } ); server.registerTool( "worker_pool_enqueue", { description: "Enqueue an agent run for execution.", inputSchema: { agentId: agentIdSchema, input: inputSchema, tenantId: tenantIdSchema } }, async ({ agentId, input, tenantId }) => { try { const tid = tenantId ?? "default"; const wp = getPool(tid, tenantPool); const runId = await wp.enqueue(agentId, input); return text(runId); } catch (err) { return text(err instanceof Error ? err.message : String(err)); } } ); server.registerTool( "worker_pool_cancel", { description: "Cancel a queued or active run.", inputSchema: { runId: runIdSchema, tenantId: tenantIdSchema } }, async ({ runId, tenantId }) => { try { const tid = tenantId ?? "default"; const wp = getPool(tid, tenantPool); await wp.cancel(runId); return text(`Cancelled: ${runId}`); } catch (err) { return text(err instanceof Error ? err.message : String(err)); } } ); server.registerTool( "worker_pool_pause", { description: "Pause an active run.", inputSchema: { runId: runIdSchema, tenantId: tenantIdSchema } }, async ({ runId, tenantId }) => { try { const tid = tenantId ?? "default"; const wp = getPool(tid, tenantPool); await wp.pause(runId); return text(`Paused: ${runId}`); } catch (err) { return text(err instanceof Error ? err.message : String(err)); } } ); server.registerTool( "worker_pool_resume", { description: "Resume a paused run.", inputSchema: { runId: runIdSchema, tenantId: tenantIdSchema } }, async ({ runId, tenantId }) => { try { const tid = tenantId ?? "default"; const wp = getPool(tid, tenantPool); await wp.resume(runId); return text(`Resumed: ${runId}`); } catch (err) { return text(err instanceof Error ? err.message : String(err)); } } ); server.registerTool( "worker_pool_list", { description: "List queued and active tasks.", inputSchema: { tenantId: tenantIdSchema } }, async ({ tenantId }) => { try { const tid = tenantId ?? "default"; const wp = getPool(tid, tenantPool); return jsonText({ queued: wp.getQueue(), active: wp.getActiveJobs() }); } catch (err) { return text(err instanceof Error ? err.message : String(err)); } } ); server.registerTool( "dag_workflow_run", { description: "Run a multi-step DAG workflow. Steps execute when their dependencies are met.", inputSchema: { workflow: dagWorkflowSchema, tenantId: tenantIdSchema } }, async ({ workflow, tenantId }) => { try { const tid = tenantId ?? "default"; const sc = getScheduler(tid, tenantPool); const runId = await sc.run(workflow); return text(runId); } catch (err) { return text(err instanceof Error ? err.message : String(err)); } } ); server.registerTool( "dag_workflow_status", { description: "Get DAG workflow run status and step details.", inputSchema: { runId: z2.string().describe("Workflow run ID"), tenantId: tenantIdSchema } }, async ({ runId, tenantId }) => { try { const tid = tenantId ?? "default"; const sc = schedulers.get(tid); if (!sc) return text(`No scheduler for tenant "${tid}"`); const run = sc.getRun(runId); if (!run) return text(`Workflow run not found: ${runId}`); return jsonText(run); } catch (err) { return text(err instanceof Error ? err.message : String(err)); } } ); server.registerTool( "dag_workflow_list", { description: "List all DAG workflow runs.", inputSchema: { tenantId: tenantIdSchema } }, async ({ tenantId }) => { try { const tid = tenantId ?? "default"; const sc = schedulers.get(tid); if (!sc) return jsonText([]); return jsonText(sc.listRuns()); } catch (err) { return text(err instanceof Error ? err.message : String(err)); } } ); } // src/mcp/forms.ts import { z as z3 } from "zod"; function text2(content) { return { content: [{ type: "text", text: content }] }; } function jsonText2(data) { return text2(JSON.stringify(data, null, 2)); } function registerFormsTools(server, ctx) { server.registerTool( "trellis_form_descriptor", { description: "Resolve the headless form descriptor for an entity type \u2014 schema-derived, with graph Form overrides applied. Returns the JSON contract UIs render.", inputSchema: { type: z3.string().describe("Entity type name (e.g. Task, Note, Agent)"), mode: z3.enum(["create", "edit", "view"]).optional().describe("Form mode (default: create)"), tenantId: z3.string().optional().describe("Tenant ID") } }, async ({ type, mode, tenantId }) => { try { const kernel = await ctx.pool.preload(tenantId ?? null); const form = resolveFormDescriptor( kernel.listOntologies(), type, { mode: mode ?? "create", overrides: readFormOverrides(kernel) } ); if (!form) { return text2( `No schema registered for entity type "${type}". Register a schema first (client.registerType or POST /ontologies).` ); } return jsonText2(form); } catch (err) { return text2(err instanceof Error ? err.message : String(err)); } } ); server.registerTool( "trellis_form_list", { description: "List entity types with registered schemas \u2014 each derivable into a headless form.", inputSchema: { tenantId: z3.string().optional().describe("Tenant ID") } }, async ({ tenantId }) => { try { const kernel = await ctx.pool.preload(tenantId ?? null); return jsonText2(listFormableTypes(kernel.listOntologies())); } catch (err) { return text2(err instanceof Error ? err.message : String(err)); } } ); } // src/mcp/room.ts var laneSchema = z4.string().optional().describe( "Draft lane for op attribution (e.g. agent:cursor). Defaults to authenticated user or agent:room-mcp." ); var tenantIdSchema2 = z4.string().optional().describe( "Tenant id (e.g. embed-design-review). Overrides session default. Use for Playground multi-tenant rooms." ); var roomSchema = z4.string().optional().describe( "Playground ?room= slug \u2014 resolves to tenant embed-{slug} (same as playground.trellis.computer session rooms)." ); function resolveToolTenant(ctx, args) { return resolveMcpTenantId({ defaultTenantId: ctx.tenantId, headerTenant: ctx.headerTenant, toolTenantId: args.tenantId, roomSlug: args.room }); } function text3(content) { return { content: [{ type: "text", text: content }] }; } function jsonText3(data) { return text3(JSON.stringify(data, null, 2)); } function toolError(err) { if (err instanceof PermissionError || err instanceof McpRateLimitError || err instanceof McpAuthError) { return text3(err.message); } return text3(err instanceof Error ? err.message : String(err)); } async function withMcpIo(ctx, tenantId, fn) { assertMcpBudget(ctx.meter, tenantId); const result = await fn(); ctx.meter?.recordGraphIo(resolveUsageTenantId(tenantId)); return result; } function auditWrite(kernel, wctx, toolName, params, payload, relatedEntities = []) { const agentId = wctx.agentId ?? "agent:room-mcp"; const resultText = typeof payload === "string" ? payload : JSON.stringify(payload); scheduleRoomMcpAudit( Promise.resolve(kernel), agentId, toolName, params, resultText, wctx, relatedEntities ); } function createRoomMcpServer(ctx) { const server = new McpServer({ name: "trellis-room", version: "0.2.0" }); server.registerTool( "get_graph_summary", { description: "Compact graph overview \u2014 health, entity types, ontologies, top attributes, links, recent ops. Call first.", inputSchema: { limit: z4.number().optional().describe("Max items per section (default: 10)"), tenantId: tenantIdSchema2, room: roomSchema } }, async ({ limit, tenantId, room }) => { try { const effectiveTenant = resolveToolTenant(ctx, { tenantId, room }); return await withMcpIo(ctx, effectiveTenant, async () => { const kernel = await ctx.pool.preload(effectiveTenant); return jsonText3( buildRoomGraphSummary(kernel, effectiveTenant, { limit }) ); }); } catch (err) { return toolError(err); } } ); server.registerTool( "graph_health", { description: "Quick liveness check for the Trellis room graph (ops + entity counts).", inputSchema: { tenantId: tenantIdSchema2, room: roomSchema } }, async ({ tenantId, room }) => { try { const effectiveTenant = resolveToolTenant(ctx, { tenantId, room }); return await withMcpIo(ctx, effectiveTenant, async () => { const kernel = await ctx.pool.preload(effectiveTenant); const entities = kernel.listEntities(); const byType = {}; for (const e of entities) { byType[e.type] = (byType[e.type] ?? 0) + 1; } return jsonText3({ status: "ok", ops: kernel.readAllOps().length, entities: entities.length, byType, tenantId: effectiveTenant }); }); } catch (err) { return toolError(err); } } ); server.registerTool( "query_graph", { description: "Run an EQL-S query against the room graph.", inputSchema: { query: z4.string().describe("EQL-S query string"), tenantId: tenantIdSchema2, room: roomSchema } }, async ({ query, tenantId, room }) => { try { const effectiveTenant = resolveToolTenant(ctx, { tenantId, room }); return await withMcpIo(ctx, effectiveTenant, async () => { const parsed = parseSimple(query); const kernel = await ctx.pool.preload(effectiveTenant); const result = await kernel.query(parsed); const bindings = hydrateBindings( kernel, result.bindings ); return jsonText3({ bindings, executionTime: result.executionTime }); }); } catch (err) { return toolError(err); } } ); server.registerTool( "get_node", { description: "Read a single entity by ID.", inputSchema: { id: z4.string().describe("Entity ID"), tenantId: tenantIdSchema2, room: roomSchema } }, async ({ id, tenantId, room }) => { try { const effectiveTenant = resolveToolTenant(ctx, { tenantId, room }); return await withMcpIo(ctx, effectiveTenant, async () => { const kernel = await ctx.pool.preload(effectiveTenant); const entity = kernel.getEntity(id); if (!entity) return text3(`Not found: ${id}`); ctx.permissions?.assert(ctx.auth, entity.type, "read", entity); return jsonText3(entityRecordToPlain(entity)); }); } catch (err) { return toolError(err); } } ); server.registerTool( "create_node", { description: "Create a new entity in the room graph. Optionally pass links for relations.", inputSchema: { type: z4.string().describe("Entity type name"), id: z4.string().optional().describe("Optional explicit entity ID"), attributes: z4.record(z4.string(), z4.unknown()).optional().describe("Entity attributes"), links: z4.array( z4.object({ attribute: z4.string(), targetEntityId: z4.string() }) ).optional().describe("Relation links to create with the entity"), lane: laneSchema, tenantId: tenantIdSchema2, room: roomSchema } }, async ({ type, id, attributes, links, lane, tenantId, room }) => { try { assertMcpWriteAuthorized(ctx); const effectiveTenant = resolveToolTenant(ctx, { tenantId, room }); ctx.permissions?.assert(ctx.auth, type, "create"); const wctx = writeContext(lane, ctx.auth, ctx.headerLane); return await withMcpIo(ctx, effectiveTenant, async () => { const kernel = await ctx.pool.preload(effectiveTenant); const entityId2 = id ?? `${type.toLowerCase()}:${crypto.randomUUID()}`; const attrs = { ...attributes ?? {} }; if (ctx.auth.userId) attrs.createdBy = ctx.auth.userId; if (effectiveTenant) attrs.tenantId = effectiveTenant; attrs.laneId = wctx.agentId; const result = await kernel.createEntity( entityId2, type, attrs, links, wctx ); await ctx.subs.notify(effectiveTenant); const payload = { id: entityId2, op: result.op.hash, lane: wctx.agentId, tenantId: effectiveTenant }; auditWrite( kernel, wctx, "create_node", { type, id, attributes, links, lane, tenantId, room }, payload, [entityId2] ); return jsonText3(payload); }); } catch (err) { return toolError(err); } } ); server.registerTool( "update_node", { description: "Partial update of an entity attributes.", inputSchema: { id: z4.string().describe("Entity ID"), attributes: z4.record(z4.string(), z4.unknown()).describe("Attributes to merge"), lane: laneSchema, tenantId: tenantIdSchema2, room: roomSchema } }, async ({ id, attributes, lane, tenantId, room }) => { try { assertMcpWriteAuthorized(ctx); const effectiveTenant = resolveToolTenant(ctx, { tenantId, room }); const wctx = writeContext(lane, ctx.auth, ctx.headerLane); return await withMcpIo(ctx, effectiveTenant, async () => { const kernel = await ctx.pool.preload(effectiveTenant); const entity = kernel.getEntity(id); if (!entity) return text3(`Not found: ${id}`); ctx.permissions?.assert(ctx.auth, entity.type, "update", entity); await kernel.updateEntity(id, attributes, wctx); await ctx.subs.notify(effectiveTenant); const payload = { id, updated: true, lane: wctx.agentId, tenantId: effectiveTenant }; auditWrite( kernel, wctx, "update_node", { id, attributes, lane, tenantId, room }, payload, [id] ); return jsonText3(payload); }); } catch (err) { return toolError(err); } } ); server.registerTool( "delete_node", { description: "Delete an entity by ID.", inputSchema: { id: z4.string().describe("Entity ID"), lane: laneSchema, tenantId: tenantIdSchema2, room: roomSchema } }, async ({ id, lane, tenantId, room }) => { try { assertMcpWriteAuthorized(ctx); const effectiveTenant = resolveToolTenant(ctx, { tenantId, room }); const wctx = writeContext(lane, ctx.auth, ctx.headerLane); return await withMcpIo(ctx, effectiveTenant, async () => { const kernel = await ctx.pool.preload(effectiveTenant); const entity = kernel.getEntity(id); if (!entity) return text3(`Not found: ${id}`); ctx.permissions?.assert(ctx.auth, entity.type, "delete", entity); await kernel.deleteEntity(id, wctx); await ctx.subs.notify(effectiveTenant); const payload = { id, deleted: true, lane: wctx.agentId, tenantId: effectiveTenant }; auditWrite( kernel, wctx, "delete_node", { id, lane, tenantId, room }, payload, [id] ); return jsonText3(payload); }); } catch (err) { return toolError(err); } } ); server.registerTool( "create_collection_record", { description: "Create a Playground CollectionRecord row (shows in Collections UI). Sets collectionId to collectionMeta:<slug> so recordBelongsToCollection matches. Optionally ensureCollection to create CollectionMeta when the collection is missing.", inputSchema: { collectionSlug: z4.string().optional().describe("Collection slug (e.g. people, ideas). Required unless collectionId is set."), collectionId: z4.string().optional().describe("Explicit collection id (e.g. collectionMeta:people). Overrides slug."), title: z4.string().min(1).describe("Record title shown in the collection"), body: z4.string().optional().describe("Optional record body / notes"), sortOrder: z4.number().int().optional().describe("Row sort order within the collection"), attributes: z4.record(z4.string(), z4.unknown()).optional().describe("Extra typed fields (status, tags, etc.) merged into the record"), ensureCollection: z4.boolean().optional().describe( "When true, create CollectionMeta with stable id collectionMeta:<slug> if missing." ), collectionTitle: z4.string().optional().describe("Title for CollectionMeta when ensureCollection creates it"), lane: laneSchema, tenantId: tenantIdSchema2, room: roomSchema } }, async ({ collectionSlug, collectionId, title, body, sortOrder, attributes, ensureCollection, collectionTitle, lane, tenantId, room }) => { try { assertMcpWriteAuthorized(ctx); const effectiveTenant = resolveToolTenant(ctx, { tenantId, room }); ctx.permissions?.assert(ctx.auth, "CollectionRecord", "create"); const wctx = writeContext(lane, ctx.auth, ctx.headerLane); const resolvedCollectionId = resolveCollectionId({ collectionSlug, collectionId }); const slug = collectionSlug?.trim() || collectionSlugFromMetaId(resolvedCollectionId) || resolvedCollectionId; return await withMcpIo(ctx, effectiveTenant, async () => { const kernel = await ctx.pool.preload(effectiveTenant); let collectionCreated = false; if (ensureCollection) { const existingMeta = kernel.getEntity(resolvedCollectionId); if (!existingMeta) { ctx.permissions?.assert(ctx.auth, "CollectionMeta", "create"); const metaAttrs = { title: collectionTitle?.trim() || slug, slug, sortOrder: 0 }; if (ctx.auth.userId) metaAttrs.createdBy = ctx.auth.userId; if (effectiveTenant) metaAttrs.tenantId = effectiveTenant; metaAttrs.laneId = wctx.agentId; await kernel.createEntity( resolvedCollectionId, "CollectionMeta", metaAttrs, void 0, wctx ); collectionCreated = true; } } const recordId = `collectionRecord:${crypto.randomUUID()}`; const recordAttrs = { ...attributes ?? {}, collectionId: resolvedCollectionId, title }; if (body !== void 0) recordAttrs.body = body; if (sortOrder !== void 0) recordAttrs.sortOrder = sortOrder; if (ctx.auth.userId) recordAttrs.createdBy = ctx.auth.userId; if (effectiveTenant) recordAttrs.tenantId = effectiveTenant; recordAttrs.laneId = wctx.agentId; const result = await kernel.createEntity( recordId, "CollectionRecord", recordAttrs, void 0, wctx ); await ctx.subs.notify(effectiveTenant); const payload = { id: recordId, collectionId: resolvedCollectionId, collectionCreated, op: result.op.hash, lane: wctx.agentId, tenantId: effectiveTenant }; auditWrite( kernel, wctx, "create_collection_record", { collectionSlug, collectionId, title, body, sortOrder, attributes, ensureCollection, collectionTitle, lane, tenantId, room }, payload, collectionCreated ? [recordId, resolvedCollectionId] : [recordId] ); return jsonText3(payload); }); } catch (err) { return toolError(err); } } ); server.registerTool( "link_nodes", { description: "Create a semantic link between two entities (assignedTo, belongsTo, references, dependsOn, \u2026).", inputSchema: { e1: z4.string().describe("Source entity ID"), relation: z4.string().describe("Relation attribute name"), e2: z4.string().describe("Target entity ID"), lane: laneSchema, tenantId: tenantIdSchema2, room: roomSchema } }, async ({ e1, relation, e2, lane, tenantId, room }) => { try { assertMcpWriteAuthorized(ctx); const effectiveTenant = resolveToolTenant(ctx, { tenantId, room }); const wctx = writeContext(lane, ctx.auth, ctx.headerLane); return await withMcpIo(ctx, effectiveTenant, async () => { const kernel = await ctx.pool.preload(effectiveTenant); const source = kernel.getEntity(e1); const target = kernel.getEntity(e2); if (!source) return text3(`Not found: ${e1}`); if (!target) return text3(`Not found: ${e2}`); ctx.permissions?.assert(ctx.auth, source.type, "update", source); const result = await kernel.addLink(e1, relation, e2, wctx); await ctx.subs.notify(effectiveTenant); const payload = { e1, relation, e2, op: result.op.hash, lane: wctx.agentId, tenantId: effectiveTenant }; auditWrite( kernel, wctx, "link_nodes", { e1, relation, e2, lane, tenantId, room }, payload, [e1, e2] ); return jsonText3(payload); }); } catch (err) { return toolError(err); } } ); registerAgentExecTools(server, ctx.pool); registerFormsTools(server, ctx); return server; } // src/server/mcp-gateway.ts var RoomMcpGateway = class { gateway = new StreamableMcpGateway(); async handle(req, ctx, auth, tenantId) { const roomCtx = { pool: ctx.pool, permissions: ctx.permissions, subs: ctx.subs, meter: ctx.meter, auth, tenantId, headerLane: req.headers.get("x-trellis-lane"), headerTenant: req.headers.get("x-trellis-tenant"), requireAuthForWrites: Boolean(ctx.authConfig.apiKey) }; return this.gateway.handle(req, () => createRoomMcpServer(roomCtx)); } async close() { await this.gateway.close(); } }; var roomMcpGateway = new RoomMcpGateway(); // src/server/public-origin.ts function requestPublicOrigin(req, url) { const forwardedProto = req.headers.get("x-forwarded-proto")?.split(",")[0]?.trim(); const forwardedHost = req.headers.get("x-forwarded-host")?.split(",")[0]?.trim() ?? req.headers.get("host") ?? url.host; if (forwardedHost) { let proto = forwardedProto; if (!proto && /\.sprites\.app$/i.test(forwardedHost.split(":")[0] ?? "")) { proto = "https"; } if (proto) { return `${proto}://${forwardedHost}`; } } return url.origin; } // src/server/server.ts var __moduleDir = import.meta.dir ?? dirname(fileURLToPath(import.meta.url)); async function startServer(opts) { return startServerNode(opts); } var startServerCrossRuntime = startServer; function buildServerContext(opts, cronScheduler) { const { pool, permissions, config } = opts; const port = opts.port ?? config.port ?? 3e3; const authConfig = { jwtSecret: config.jwtSecret, apiKey: config.apiKey, allowPublic: true }; const meter = new UsageMeter(); const subs = new SubscriptionManager(pool, permissions ?? null, meter); const enableCors = corsEnabledForConfig(config.apiKey); const handleHttpInner = async (req) => { const url = new URL(req.url); const path = url.pathname; const auth = await resolveAuth( req.headers.get("authorization"), authConfig ); const tenantId = auth.tenantId ?? url.searchParams.get("tenantId") ?? null; try { return await route(req, url, path, auth, tenantId, { pool, permissions: permissions ?? null, subs, meter, authConfig, config, oauthProviders: opts.oauthProviders ?? {}, cronScheduler }); } catch (err) { if (err instanceof PermissionError) { return json(err.toResponse(), 403); } const msg = err instanceof Error ? err.message : String(err); if (process.env.TRELLIS_DEBUG) { console.error( `[trellis] ${req.method} ${path} \u2192 500:`, msg