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