@axlsdk/studio 0.20.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -89,6 +89,14 @@ type StudioMiddlewareOptions = {
89
89
  allowed: boolean;
90
90
  metadata?: unknown;
91
91
  }>;
92
+ /**
93
+ * Permit `upgradeWebSocket()` without `verifyUpgrade` in production.
94
+ * This exposes Studio's administrative event channels without SDK-level
95
+ * authentication and should be used only when an outer upgrade gate is
96
+ * independently guaranteed.
97
+ * @default false
98
+ */
99
+ dangerouslyAllowUnauthenticatedWebSockets?: boolean;
92
100
  /**
93
101
  * Optional per-event filter used by multi-tenant deployments to scope the
94
102
  * trace firehose. Called on every outbound WebSocket broadcast; return
@@ -89,6 +89,14 @@ type StudioMiddlewareOptions = {
89
89
  allowed: boolean;
90
90
  metadata?: unknown;
91
91
  }>;
92
+ /**
93
+ * Permit `upgradeWebSocket()` without `verifyUpgrade` in production.
94
+ * This exposes Studio's administrative event channels without SDK-level
95
+ * authentication and should be used only when an outer upgrade gate is
96
+ * independently guaranteed.
97
+ * @default false
98
+ */
99
+ dangerouslyAllowUnauthenticatedWebSockets?: boolean;
92
100
  /**
93
101
  * Optional per-event filter used by multi-tenant deployments to scope the
94
102
  * trace firehose. Called on every outbound WebSocket broadcast; return
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createServer,
3
3
  handleWsMessage
4
- } from "./chunk-KQ3HBRTC.js";
4
+ } from "./chunk-YGLWYDQ3.js";
5
5
 
6
6
  // src/middleware.ts
7
7
  import { resolve as resolve2, dirname } from "path";
@@ -133,6 +133,7 @@ function createStudioMiddleware(options) {
133
133
  runtime,
134
134
  serveClient = true,
135
135
  verifyUpgrade,
136
+ dangerouslyAllowUnauthenticatedWebSockets = false,
136
137
  readOnly = false,
137
138
  filterTraceEvent
138
139
  } = options;
@@ -158,9 +159,9 @@ function createStudioMiddleware(options) {
158
159
  if (filterTraceEvent) {
159
160
  connMgr.setFilter(filterTraceEvent);
160
161
  }
161
- if (process.env.NODE_ENV === "production" && !verifyUpgrade) {
162
+ if (process.env.NODE_ENV === "production" && !verifyUpgrade && !dangerouslyAllowUnauthenticatedWebSockets) {
162
163
  console.warn(
163
- "[axl-studio] WARNING: Studio middleware mounted in production without verifyUpgrade. WebSocket connections are not authenticated. All registered workflows, tools, and agents are accessible. See https://axlsdk.com/docs/studio/security"
164
+ "[axl-studio] WARNING: Studio middleware mounted in production without verifyUpgrade. upgradeWebSocket() will refuse to attach. Provide verifyUpgrade or explicitly set dangerouslyAllowUnauthenticatedWebSockets: true only when an outer upgrade gate is guaranteed."
164
165
  );
165
166
  }
166
167
  const listener = getRequestListener(app.fetch, {
@@ -222,6 +223,11 @@ function createStudioMiddleware(options) {
222
223
  let upgradeHandler;
223
224
  let serverRef;
224
225
  function upgradeWebSocket(server, path) {
226
+ if (process.env.NODE_ENV === "production" && !verifyUpgrade && !dangerouslyAllowUnauthenticatedWebSockets) {
227
+ throw new Error(
228
+ "[axl-studio] Refusing unauthenticated WebSocket upgrades in production. Provide verifyUpgrade or explicitly set dangerouslyAllowUnauthenticatedWebSockets: true."
229
+ );
230
+ }
225
231
  if (wss) {
226
232
  throw new Error(
227
233
  "[axl-studio] upgradeWebSocket() has already been called. Call close() first if you need to re-attach."
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/middleware.ts","../src/eval-loader.ts"],"sourcesContent":["import { resolve, dirname } from 'node:path';\nimport { existsSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport { getRequestListener } from '@hono/node-server';\nimport { WebSocketServer } from 'ws';\nimport { createServer } from './server/index.js';\nimport { handleWsMessage } from './server/ws/protocol.js';\nimport { createEvalLoader } from './eval-loader.js';\nimport type { EvalLoaderConfig } from './eval-loader.js';\nimport type { BufferCaps } from './server/ws/connection-manager.js';\nimport type { AxlRuntime } from '@axlsdk/axl';\nimport type { IncomingMessage, ServerResponse, Server } from 'node:http';\n\nexport type { EvalLoaderConfig } from './eval-loader.js';\nexport type { BufferCaps } from './server/ws/connection-manager.js';\n\nexport type StudioMiddlewareOptions = {\n /** The AxlRuntime instance to observe and control. */\n runtime: AxlRuntime;\n\n /**\n * URL path prefix where Studio is mounted.\n * Must match the mount path in your framework (Express `app.use()`,\n * Fastify `register()`, Hono `app.route()`, etc.). The framework is\n * expected to strip the prefix from `req.url` before calling the handler.\n *\n * Do not set basePath when using a raw `http.Server` as the root handler —\n * leave it empty and mount at root instead.\n *\n * Must start with '/' when non-empty. Trailing slashes are stripped.\n * Only URL-safe characters allowed: [a-zA-Z0-9/_-]\n *\n * @example '/studio'\n * @example '/admin/studio'\n * @example '' — mounted at root (default)\n */\n basePath?: string;\n\n /**\n * Serve the pre-built Studio SPA.\n * Set to false if serving the client from a CDN or separate build.\n * @default true\n */\n serveClient?: boolean;\n\n /**\n * Verify a WebSocket upgrade request before completing the handshake.\n *\n * Return either a boolean (`true` allow, `false` reject) or an object\n * `{ allowed: boolean, metadata?: unknown }` to ALSO attach per-connection\n * metadata that `filterTraceEvent` can later read — typically the auth\n * subject (userId, tenantId, role). Throw to reject with an error.\n *\n * IMPORTANT: WebSocket upgrades bypass Express/Fastify/Koa middleware.\n * If your HTTP routes are behind auth middleware, WS connections are NOT\n * automatically protected. Use this callback to enforce authentication\n * on WebSocket connections.\n *\n * @example Multi-tenant: extract tenant from JWT, attach as metadata\n * verifyUpgrade: async (req) => {\n * const token = req.headers.authorization?.slice(7);\n * const claims = await verifyJwt(token);\n * if (!claims) return false;\n * return { allowed: true, metadata: { userId: claims.sub, tenantId: claims.tid } };\n * }\n */\n verifyUpgrade?: (\n req: IncomingMessage,\n ) =>\n | boolean\n | { allowed: boolean; metadata?: unknown }\n | Promise<boolean | { allowed: boolean; metadata?: unknown }>;\n\n /**\n * Optional per-event filter used by multi-tenant deployments to scope the\n * trace firehose. Called on every outbound WebSocket broadcast; return\n * `true` to deliver to this connection, `false` to drop.\n *\n * `event` is the parsed event payload — for the `trace:*` channel this is\n * an `AxlEvent` from `@axlsdk/axl` (narrow via the discriminated union);\n * for `costs` it's a `CostData`; for `execution:*` / `eval:*` it's the\n * legacy stream-event shape (until PR 3 collapses the wire to `AxlEvent`).\n * Typed `unknown` because the filter runs for every channel.\n * `metadata` is the per-connection metadata attached by `verifyUpgrade`,\n * or `undefined` if `verifyUpgrade` returned a bare boolean.\n *\n * Filter errors are treated as `drop` (fail-closed) so a buggy predicate\n * cannot accidentally leak events cross-tenant.\n *\n * @example Scope trace events by tenant id stored on agent metadata\n * import type { AxlEvent } from '@axlsdk/axl';\n *\n * filterTraceEvent: (event, meta) => {\n * const m = meta as { tenantId?: string } | undefined;\n * if (!m?.tenantId) return false;\n * const e = event as AxlEvent;\n * // Only narrow event.type === 'agent_call_end' events; pass structural\n * // events (workflow_start/end, cost updates) through unconditionally.\n * if (e.type !== 'agent_call_end') return true;\n * return e.workflow?.startsWith(`${m.tenantId}:`) ?? false;\n * }\n */\n filterTraceEvent?: (event: unknown, metadata: unknown) => boolean;\n\n /**\n * Disable all mutating endpoints (execute, test, send, delete, resolve).\n * When true, Studio is observation-only.\n * @default false\n */\n readOnly?: boolean;\n\n /**\n * Lazy-load eval files for the Eval Runner panel.\n *\n * Eval files are dynamically imported on first access to eval endpoints,\n * not at middleware construction time. This means:\n * - Zero cost during normal API operation\n * - Eval files can import from any module without creating circular deps\n * in the static module graph (they're loaded as standalone entry points)\n * - `@axlsdk/eval` can remain a devDependency — eval files never enter\n * the production bundle since bundlers can't see dynamic imports\n *\n * Accepts glob patterns, explicit file paths, or an object with\n * `conditions` for monorepo source export resolution.\n *\n * Eval files are loaded once and cached for the middleware's lifetime.\n * Changes to eval files require a server restart.\n *\n * @example\n * // Single glob pattern\n * evals: 'evals/*.eval.ts'\n *\n * @example\n * // Multiple patterns\n * evals: ['libs/api/evals/*.eval.ts', 'libs/ai/evals/*.eval.ts']\n *\n * @example\n * // With import conditions for monorepo source exports\n * evals: {\n * files: 'libs/api/evals/*.eval.ts',\n * conditions: ['development'],\n * }\n */\n evals?: EvalLoaderConfig;\n\n /**\n * Override the default WebSocket replay-buffer resource caps for\n * production deployments under sustained execution churn.\n *\n * Defaults: `maxEventsPerBuffer: 1000`, `maxBytesPerBuffer: 4 MiB`,\n * `maxActiveBuffers: 256`. Worst-case memory is roughly\n * `maxActiveBuffers × maxBytesPerBuffer` (≈1 GiB at defaults). Tighten\n * either dimension to lower the ceiling at the cost of late-subscriber\n * replay coverage; raise them if you need more event headroom on long\n * verbose-mode streams.\n *\n * Terminal `done` / `error` events are always buffered regardless of\n * caps, so a late subscriber to a completed stream still sees the\n * outcome — only intermediate non-terminal events are dropped.\n *\n * All fields are optional; passing `{}` is a no-op.\n *\n * @example\n * // Halve memory ceiling on a high-churn deployment\n * bufferCaps: { maxActiveBuffers: 128, maxBytesPerBuffer: 2 * 1024 * 1024 }\n */\n bufferCaps?: BufferCaps;\n};\n\n/**\n * Minimal contract a WebSocket connection must satisfy.\n * Matches the `ws` library API (de facto standard in Node.js).\n */\nexport interface StudioWebSocket {\n send(data: string): void;\n close(): void;\n on(event: 'message', fn: (data: string | Buffer) => void): void;\n on(event: 'close', fn: () => void): void;\n on(event: 'error', fn: (err: Error) => void): void;\n}\n\n// Re-export for Hono-in-Hono consumers\nexport { handleWsMessage } from './server/ws/protocol.js';\n\nexport type StudioMiddleware = ReturnType<typeof createStudioMiddleware>;\n\nexport function createStudioMiddleware(options: StudioMiddlewareOptions) {\n const {\n runtime,\n serveClient = true,\n verifyUpgrade,\n readOnly = false,\n filterTraceEvent,\n } = options;\n\n // Normalize basePath: strip trailing slashes, validate format\n const basePath = normalizeBasePath(options.basePath);\n\n // Resolve pre-built SPA assets from this package's dist/\n const staticRoot = serveClient ? resolveClientDist() : undefined;\n\n if (serveClient && !staticRoot) {\n const dir =\n import.meta.dirname ??\n (typeof __dirname !== 'undefined' ? __dirname : dirname(fileURLToPath(import.meta.url)));\n console.warn(\n '[axl-studio] serveClient is true but no pre-built client found at ' +\n `${resolve(dir, 'client')}. Studio UI will not be available. ` +\n 'Set serveClient: false to suppress this warning.',\n );\n }\n\n // Create lazy eval loader if eval files are configured\n const evalLoader = options.evals ? createEvalLoader(options.evals, runtime) : undefined;\n\n const { app, connMgr, traceListener, closeActiveRuns, closeAggregators } = createServer({\n runtime,\n staticRoot,\n basePath,\n readOnly,\n cors: false, // Host framework owns CORS policy\n evalLoader,\n bufferCaps: options.bufferCaps,\n });\n\n // Install the broadcast filter once at construction time. The connMgr\n // applies it on every outbound event; `metadata` is attached to each\n // connection after a successful `verifyUpgrade`.\n if (filterTraceEvent) {\n connMgr.setFilter(filterTraceEvent);\n }\n\n // Log production safety warning\n if (process.env.NODE_ENV === 'production' && !verifyUpgrade) {\n console.warn(\n '[axl-studio] WARNING: Studio middleware mounted in production without verifyUpgrade. ' +\n 'WebSocket connections are not authenticated. All registered workflows, tools, and ' +\n 'agents are accessible. See https://axlsdk.com/docs/studio/security',\n );\n }\n\n // Convert Hono app → Node.js (req, res) handler via @hono/node-server.\n // overrideGlobalObjects: false prevents replacing global.Request/Response,\n // which could break the host application.\n const listener = getRequestListener(app.fetch, {\n overrideGlobalObjects: false,\n });\n\n let closed = false;\n\n function handler(req: IncomingMessage, res: ServerResponse) {\n if (closed) {\n res.writeHead(503, { 'Content-Type': 'application/json' });\n res.end(\n JSON.stringify({\n ok: false,\n error: { code: 'CLOSED', message: 'Studio middleware has been shut down' },\n }),\n );\n return;\n }\n\n // Express/NestJS/Koa body parsers consume the raw IncomingMessage stream\n // and store the parsed result on req.body. When that happens,\n // @hono/node-server's getRequestListener reads an empty stream and Hono\n // sees no body. To fix this, we re-serialize req.body as req.rawBody —\n // a Buffer that getRequestListener checks before falling back to the\n // stream (see newRequestFromIncoming in @hono/node-server/dist/listener.js).\n //\n // Verified against @hono/node-server@1.19.9. If this breaks after an\n // upgrade, check whether the rawBody instanceof Buffer check still exists\n // in newRequestFromIncoming.\n const reqAny = req as unknown as Record<string, unknown>;\n if (reqAny.body != null && !reqAny.rawBody) {\n try {\n reqAny.rawBody = Buffer.from(JSON.stringify(reqAny.body));\n } catch {\n // Non-serializable body — Hono will see an empty body\n }\n }\n\n listener(req, res).catch((err) => {\n console.error('[axl-studio] Unhandled error in request handler:', err);\n if (!res.headersSent) {\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(\n JSON.stringify({\n ok: false,\n error: { code: 'INTERNAL_ERROR', message: 'Internal server error' },\n }),\n );\n }\n });\n }\n\n // Handle an individual WebSocket using the Studio protocol.\n // Adapts any StudioWebSocket to ConnectionManager's internal BroadcastTarget.\n // `metadata` is whatever `verifyUpgrade` attached — passed through so the\n // `filterTraceEvent` callback can read it on every outbound event.\n function handleWebSocket(ws: StudioWebSocket, metadata?: unknown) {\n if (closed) {\n ws.close();\n return;\n }\n const socket = {\n send: (data: string) => ws.send(data),\n close: () => ws.close(),\n };\n connMgr.add(socket);\n if (metadata !== undefined) {\n connMgr.setMetadata(socket, metadata);\n }\n\n ws.on('message', (raw) => {\n const reply = handleWsMessage(String(raw), socket, connMgr);\n if (reply) ws.send(reply);\n });\n\n ws.on('close', () => connMgr.remove(socket));\n ws.on('error', () => connMgr.remove(socket));\n }\n\n // Internal WebSocketServer — created lazily by upgradeWebSocket()\n let wss: InstanceType<typeof WebSocketServer> | undefined;\n // References for cleanup: the upgrade handler and server it's attached to\n let upgradeHandler: ((...args: any[]) => void) | undefined;\n let serverRef: Server | undefined;\n\n // Convenience: attach WS handling to an http.Server.\n function upgradeWebSocket(server: Server, path?: string) {\n if (wss) {\n throw new Error(\n '[axl-studio] upgradeWebSocket() has already been called. ' +\n 'Call close() first if you need to re-attach.',\n );\n }\n\n const wsPath = path ?? (basePath ? `${basePath}/ws` : '/ws');\n\n wss = new WebSocketServer({ noServer: true });\n serverRef = server;\n\n upgradeHandler = async (req: IncomingMessage, socket: any, head: Buffer) => {\n // Match path, ignoring query string\n const pathname = new URL(req.url!, `http://${req.headers.host}`).pathname;\n if (pathname !== wsPath) return; // Let other upgrade handlers run\n\n // Early-reject if close() already started. This is a cheap pre-check —\n // we repeat it AFTER the async verifyUpgrade resolves to close the race\n // where close() runs while verifyUpgrade is still in flight.\n if (closed) {\n socket.destroy();\n return;\n }\n\n // Apply auth verification and capture optional per-connection metadata\n // (e.g. { userId, tenantId }) that filterTraceEvent can later read.\n let connectionMetadata: unknown;\n if (verifyUpgrade) {\n try {\n const result = await verifyUpgrade(req);\n // Normalize both return shapes: plain boolean or { allowed, metadata }\n const allowed = typeof result === 'boolean' ? result : result.allowed;\n if (!allowed) {\n socket.write('HTTP/1.1 401 Unauthorized\\r\\n\\r\\n');\n socket.destroy();\n return;\n }\n if (typeof result === 'object' && result !== null) {\n connectionMetadata = result.metadata;\n }\n } catch {\n socket.write('HTTP/1.1 403 Forbidden\\r\\n\\r\\n');\n socket.destroy();\n return;\n }\n }\n\n // Guard against close() having run during async verifyUpgrade. We check\n // BOTH `closed` (set at the top of `close()`) and `!wss` (set at the\n // end). `closed` is the authoritative signal — checking `!wss` alone\n // wouldn't catch the window where `close()` has marked the middleware\n // as shut down but hasn't yet torn down the WebSocketServer. Failing\n // open here would leak a connection past the middleware's lifetime\n // and leave it holding references the host expected to be released.\n if (closed || !wss) {\n socket.destroy();\n return;\n }\n\n wss.handleUpgrade(req, socket, head, (ws) => {\n handleWebSocket(ws, connectionMetadata);\n });\n };\n\n server.on('upgrade', upgradeHandler);\n }\n\n // Cleanup function for lifecycle management.\n function close() {\n closed = true;\n\n // Abort active streaming eval runs before closing connections\n closeActiveRuns();\n\n // Close all WebSocket connections\n connMgr.closeAll();\n\n // Remove the upgrade listener from the server before closing WSS\n if (upgradeHandler && serverRef) {\n serverRef.removeListener('upgrade', upgradeHandler);\n upgradeHandler = undefined;\n serverRef = undefined;\n }\n\n // Shut down the internal WebSocketServer if one was created\n if (wss) {\n wss.close();\n wss = undefined;\n }\n\n // Remove only our trace event listener from the runtime\n if (traceListener) {\n runtime.removeListener('trace', traceListener);\n }\n\n // Close all aggregators (clear intervals and unsubscribe listeners)\n closeAggregators();\n }\n\n return {\n handler,\n handleWebSocket,\n upgradeWebSocket,\n app,\n connectionManager: connMgr,\n close,\n };\n}\n\n/**\n * Normalize and validate basePath.\n * - Empty string and undefined → ''\n * - Strip trailing slashes\n * - Validate leading slash when non-empty\n * - Reject unsafe characters\n */\nfunction normalizeBasePath(raw?: string): string {\n if (!raw) return '';\n\n // Strip trailing slashes\n const normalized = raw.replace(/\\/+$/, '');\n if (!normalized) return '';\n\n // Must start with /\n if (!normalized.startsWith('/')) {\n throw new Error(`basePath must start with '/' (got '${raw}'). Example: '/studio'`);\n }\n\n // Reject path traversal, consecutive slashes, and unsafe characters\n if (normalized.includes('..')) {\n throw new Error(`basePath must not contain '..' segments (got '${raw}')`);\n }\n if (normalized.includes('//')) {\n throw new Error(`basePath must not contain consecutive slashes (got '${raw}')`);\n }\n if (!/^\\/[a-zA-Z0-9/_-]*$/.test(normalized)) {\n throw new Error(\n `basePath contains invalid characters (got '${raw}'). ` +\n 'Only alphanumeric characters, /, _, and - are allowed.',\n );\n }\n\n return normalized;\n}\n\nfunction resolveClientDist(): string | undefined {\n // Resolve the directory of this file (dist/ in published package).\n const dir =\n import.meta.dirname ??\n (typeof __dirname !== 'undefined' ? __dirname : dirname(fileURLToPath(import.meta.url)));\n const candidate = resolve(dir, 'client');\n return existsSync(resolve(candidate, 'index.html')) ? candidate : undefined;\n}\n","import { resolve, relative, basename } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport type { AxlRuntime, EvalExecuteWorkflow } from '@axlsdk/axl';\nimport { importModule, expandGlob, registerConditions, pickDefault, pickExport } from '@axlsdk/axl';\n\n// In the CJS bundle, tsup stubs import.meta as {} so import.meta.url is\n// undefined. Fall back to __filename (which CJS defines) converted to a\n// file:// URL so tsImport() gets a valid parentURL.\nconst parentURL: string =\n import.meta.url ?? pathToFileURL(typeof __filename !== 'undefined' ? __filename : __dirname).href;\n\n/**\n * Configuration for lazy eval file discovery.\n *\n * - `string` — a glob pattern or explicit file path\n * - `string[]` — multiple patterns/paths\n * - `object` — patterns with optional import conditions\n */\nexport type EvalLoaderConfig =\n | string\n | string[]\n | {\n files: string | string[];\n\n /**\n * Custom Node.js import conditions (e.g., `['development']`).\n *\n * In monorepos, package.json `exports` often use the `development` condition\n * to point at source (`.ts`) instead of built dist. Without this, eval files\n * that import workspace packages resolve to dist files, which may not exist.\n *\n * **WARNING**: Conditions are registered process-wide via `module.register()`.\n * They affect all subsequent imports in the process, not just eval files.\n */\n conditions?: string[];\n };\n\n/**\n * Create a lazy eval loader that resolves file patterns and dynamically imports\n * eval files on first call, registering them with the runtime.\n *\n * The loader is idempotent — subsequent calls return the same promise.\n * Concurrent callers all await the same loading work.\n *\n * Eval files should export a default config with `{ workflow, dataset, scorers }`\n * (the result of `defineEval()` from `@axlsdk/eval`). An optional named export\n * `executeWorkflow` overrides the default `runtime.execute()` behavior.\n *\n * Eval names are the file's path relative to `cwd` (project root), minus the\n * `.eval.*` suffix. This makes names completely stable — a file's name never\n * changes regardless of what other files or patterns exist.\n *\n * @param config Glob patterns, file paths, or object with conditions\n * @param runtime The AxlRuntime to register discovered evals on\n * @param cwd Base directory for resolving patterns and deriving names (default: `process.cwd()`)\n */\nexport function createEvalLoader(\n config: EvalLoaderConfig,\n runtime: AxlRuntime,\n cwd?: string,\n): () => Promise<void> {\n let loadPromise: Promise<void> | undefined;\n const { patterns, conditions } = normalizeConfig(config);\n const baseCwd = cwd ?? process.cwd();\n\n return () => {\n if (!loadPromise) {\n loadPromise = loadEvalFiles(patterns, conditions, baseCwd, runtime).catch((err) => {\n loadPromise = undefined; // Allow retry on next request\n throw err;\n });\n }\n return loadPromise;\n };\n}\n\n// ── Core loading logic ─────────────────────────────────────────────\n\nasync function loadEvalFiles(\n patterns: string[],\n conditions: string[],\n cwd: string,\n runtime: AxlRuntime,\n): Promise<void> {\n if (conditions.length > 0) {\n await registerConditions(conditions);\n }\n\n const files = resolvePatterns(patterns, cwd);\n\n if (files.length === 0) {\n console.warn(`[axl-studio] No eval files found matching: ${patterns.join(', ')}`);\n return;\n }\n\n for (const file of files) {\n try {\n const mod = await importModule(file, parentURL);\n const evalConfig = pickDefault<{\n workflow?: string;\n dataset?: unknown;\n scorers?: unknown;\n }>(mod);\n\n if (!evalConfig || typeof evalConfig !== 'object') {\n console.warn(\n `[axl-studio] Skipping ${file}: default export is ` +\n `${evalConfig === null ? 'null' : typeof evalConfig}, expected an eval config object.`,\n );\n continue;\n }\n if (Array.isArray(evalConfig.scorers) && evalConfig.scorers.length === 0) {\n console.warn(\n `[axl-studio] Skipping ${file}: scorers array is empty — at least one scorer is required.`,\n );\n continue;\n }\n if (!evalConfig.workflow || !evalConfig.dataset || !evalConfig.scorers) {\n const missing = [\n !evalConfig.workflow && 'workflow',\n !evalConfig.dataset && 'dataset',\n !evalConfig.scorers && 'scorers',\n ]\n .filter(Boolean)\n .join(', ');\n const keys = Object.keys(evalConfig).slice(0, 10);\n const got = keys.length > 0 ? ` Got: { ${keys.join(', ')} }.` : '';\n console.warn(\n `[axl-studio] Skipping ${file}: not a valid eval config (missing ${missing}).${got}`,\n );\n continue;\n }\n\n const name = deriveEvalName(file, cwd);\n\n if (runtime.getRegisteredEval(name)) {\n console.warn(\n `[axl-studio] Eval name \"${name}\" from ${file} collides with an ` +\n `already-registered eval — overwriting`,\n );\n }\n\n // Walk the ESM/CJS interop chain symmetrically with the default export\n // so `executeWorkflow` stays visible when tsx loads the eval module as CJS\n // (e.g. a `.ts` file in a package without `\"type\": \"module\"`). Validate\n // the export is callable — a non-function `executeWorkflow` is silently\n // ignored so registration falls back to `runtime.execute()`, with a\n // warning so the user knows their export was rejected.\n const exported = pickExport<unknown>(mod, 'executeWorkflow');\n let customExecute: EvalExecuteWorkflow | undefined;\n if (typeof exported === 'function') {\n customExecute = exported as EvalExecuteWorkflow;\n } else if (exported !== undefined) {\n console.warn(\n `[axl-studio] ${file} exports executeWorkflow but it is ${typeof exported}, ` +\n `not a function — ignoring and falling back to runtime.execute()`,\n );\n }\n runtime.registerEval(name, evalConfig, customExecute);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n console.warn(`[axl-studio] Failed to load eval ${file}: ${msg}`);\n }\n }\n}\n\n// ── Internal helpers ───────────────────────────────────────────────\n\nfunction normalizeConfig(config: EvalLoaderConfig): {\n patterns: string[];\n conditions: string[];\n} {\n if (typeof config === 'string') {\n return { patterns: [config], conditions: [] };\n }\n if (Array.isArray(config)) {\n return { patterns: config, conditions: [] };\n }\n const files = typeof config.files === 'string' ? [config.files] : config.files;\n return { patterns: files, conditions: config.conditions ?? [] };\n}\n\n/**\n * Derive eval name from file path relative to cwd.\n *\n * Examples (cwd = `/project`):\n * - `/project/evals/suggestions.eval.ts` → `\"evals/suggestions\"`\n * - `/project/evals/api/accuracy.eval.ts` → `\"evals/api/accuracy\"`\n */\nfunction deriveEvalName(filePath: string, cwd: string): string {\n const rel = relative(cwd, filePath);\n // Normalize to forward slashes for cross-platform consistency\n const normalized = rel.replace(/\\\\/g, '/');\n // Guard: file outside cwd (symlink, absolute path) — fall back to basename\n if (normalized.startsWith('../')) {\n const base = basename(filePath);\n const stripped = base.replace(/\\.eval\\.[mc]?[jt]sx?$/, '');\n return stripped !== base ? stripped : base.replace(/\\.[mc]?[jt]sx?$/, '') || base;\n }\n // Strip .eval.ts, .eval.mjs, .eval.js, etc.\n const withoutEval = normalized.replace(/\\.eval\\.[mc]?[jt]sx?$/, '');\n if (withoutEval !== normalized) return withoutEval;\n // Fallback: strip extension\n const withoutExt = normalized.replace(/\\.[mc]?[jt]sx?$/, '');\n return withoutExt || normalized;\n}\n\n/**\n * Resolve patterns to absolute file paths.\n *\n * Supports:\n * - Explicit file paths (no wildcards)\n * - Single-directory globs: `dir/*.eval.ts`\n * - Recursive globs: `dir/**\\/*.eval.ts` or `**\\/*.eval.ts`\n *\n * Multi-segment `**` (e.g., `a/**\\/b/**\\/*.ts`) is not supported.\n */\nfunction resolvePatterns(patterns: string[], cwd: string): string[] {\n const files: string[] = [];\n const seen = new Set<string>();\n for (const pattern of patterns) {\n const resolved = pattern.includes('*') ? expandGlob(pattern, cwd) : [resolve(cwd, pattern)];\n for (const file of resolved) {\n if (!seen.has(file)) {\n seen.add(file);\n files.push(file);\n }\n }\n }\n return files;\n}\n\n// `expandGlob` and `registerConditions` are imported from @axlsdk/axl above.\n// The studio-specific glob helpers and conditions registration that used to\n// live here are now shared with @axlsdk/eval to prevent drift.\n"],"mappings":"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","names":["resolve","resolve"]}
1
+ {"version":3,"sources":["../src/middleware.ts","../src/eval-loader.ts"],"sourcesContent":["import { resolve, dirname } from 'node:path';\nimport { existsSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport { getRequestListener } from '@hono/node-server';\nimport { WebSocketServer } from 'ws';\nimport { createServer } from './server/index.js';\nimport { handleWsMessage } from './server/ws/protocol.js';\nimport { createEvalLoader } from './eval-loader.js';\nimport type { EvalLoaderConfig } from './eval-loader.js';\nimport type { BufferCaps } from './server/ws/connection-manager.js';\nimport type { AxlRuntime } from '@axlsdk/axl';\nimport type { IncomingMessage, ServerResponse, Server } from 'node:http';\n\nexport type { EvalLoaderConfig } from './eval-loader.js';\nexport type { BufferCaps } from './server/ws/connection-manager.js';\n\nexport type StudioMiddlewareOptions = {\n /** The AxlRuntime instance to observe and control. */\n runtime: AxlRuntime;\n\n /**\n * URL path prefix where Studio is mounted.\n * Must match the mount path in your framework (Express `app.use()`,\n * Fastify `register()`, Hono `app.route()`, etc.). The framework is\n * expected to strip the prefix from `req.url` before calling the handler.\n *\n * Do not set basePath when using a raw `http.Server` as the root handler —\n * leave it empty and mount at root instead.\n *\n * Must start with '/' when non-empty. Trailing slashes are stripped.\n * Only URL-safe characters allowed: [a-zA-Z0-9/_-]\n *\n * @example '/studio'\n * @example '/admin/studio'\n * @example '' — mounted at root (default)\n */\n basePath?: string;\n\n /**\n * Serve the pre-built Studio SPA.\n * Set to false if serving the client from a CDN or separate build.\n * @default true\n */\n serveClient?: boolean;\n\n /**\n * Verify a WebSocket upgrade request before completing the handshake.\n *\n * Return either a boolean (`true` allow, `false` reject) or an object\n * `{ allowed: boolean, metadata?: unknown }` to ALSO attach per-connection\n * metadata that `filterTraceEvent` can later read — typically the auth\n * subject (userId, tenantId, role). Throw to reject with an error.\n *\n * IMPORTANT: WebSocket upgrades bypass Express/Fastify/Koa middleware.\n * If your HTTP routes are behind auth middleware, WS connections are NOT\n * automatically protected. Use this callback to enforce authentication\n * on WebSocket connections.\n *\n * @example Multi-tenant: extract tenant from JWT, attach as metadata\n * verifyUpgrade: async (req) => {\n * const token = req.headers.authorization?.slice(7);\n * const claims = await verifyJwt(token);\n * if (!claims) return false;\n * return { allowed: true, metadata: { userId: claims.sub, tenantId: claims.tid } };\n * }\n */\n verifyUpgrade?: (\n req: IncomingMessage,\n ) =>\n | boolean\n | { allowed: boolean; metadata?: unknown }\n | Promise<boolean | { allowed: boolean; metadata?: unknown }>;\n\n /**\n * Permit `upgradeWebSocket()` without `verifyUpgrade` in production.\n * This exposes Studio's administrative event channels without SDK-level\n * authentication and should be used only when an outer upgrade gate is\n * independently guaranteed.\n * @default false\n */\n dangerouslyAllowUnauthenticatedWebSockets?: boolean;\n\n /**\n * Optional per-event filter used by multi-tenant deployments to scope the\n * trace firehose. Called on every outbound WebSocket broadcast; return\n * `true` to deliver to this connection, `false` to drop.\n *\n * `event` is the parsed event payload — for the `trace:*` channel this is\n * an `AxlEvent` from `@axlsdk/axl` (narrow via the discriminated union);\n * for `costs` it's a `CostData`; for `execution:*` / `eval:*` it's the\n * legacy stream-event shape (until PR 3 collapses the wire to `AxlEvent`).\n * Typed `unknown` because the filter runs for every channel.\n * `metadata` is the per-connection metadata attached by `verifyUpgrade`,\n * or `undefined` if `verifyUpgrade` returned a bare boolean.\n *\n * Filter errors are treated as `drop` (fail-closed) so a buggy predicate\n * cannot accidentally leak events cross-tenant.\n *\n * @example Scope trace events by tenant id stored on agent metadata\n * import type { AxlEvent } from '@axlsdk/axl';\n *\n * filterTraceEvent: (event, meta) => {\n * const m = meta as { tenantId?: string } | undefined;\n * if (!m?.tenantId) return false;\n * const e = event as AxlEvent;\n * // Only narrow event.type === 'agent_call_end' events; pass structural\n * // events (workflow_start/end, cost updates) through unconditionally.\n * if (e.type !== 'agent_call_end') return true;\n * return e.workflow?.startsWith(`${m.tenantId}:`) ?? false;\n * }\n */\n filterTraceEvent?: (event: unknown, metadata: unknown) => boolean;\n\n /**\n * Disable all mutating endpoints (execute, test, send, delete, resolve).\n * When true, Studio is observation-only.\n * @default false\n */\n readOnly?: boolean;\n\n /**\n * Lazy-load eval files for the Eval Runner panel.\n *\n * Eval files are dynamically imported on first access to eval endpoints,\n * not at middleware construction time. This means:\n * - Zero cost during normal API operation\n * - Eval files can import from any module without creating circular deps\n * in the static module graph (they're loaded as standalone entry points)\n * - `@axlsdk/eval` can remain a devDependency — eval files never enter\n * the production bundle since bundlers can't see dynamic imports\n *\n * Accepts glob patterns, explicit file paths, or an object with\n * `conditions` for monorepo source export resolution.\n *\n * Eval files are loaded once and cached for the middleware's lifetime.\n * Changes to eval files require a server restart.\n *\n * @example\n * // Single glob pattern\n * evals: 'evals/*.eval.ts'\n *\n * @example\n * // Multiple patterns\n * evals: ['libs/api/evals/*.eval.ts', 'libs/ai/evals/*.eval.ts']\n *\n * @example\n * // With import conditions for monorepo source exports\n * evals: {\n * files: 'libs/api/evals/*.eval.ts',\n * conditions: ['development'],\n * }\n */\n evals?: EvalLoaderConfig;\n\n /**\n * Override the default WebSocket replay-buffer resource caps for\n * production deployments under sustained execution churn.\n *\n * Defaults: `maxEventsPerBuffer: 1000`, `maxBytesPerBuffer: 4 MiB`,\n * `maxActiveBuffers: 256`. Worst-case memory is roughly\n * `maxActiveBuffers × maxBytesPerBuffer` (≈1 GiB at defaults). Tighten\n * either dimension to lower the ceiling at the cost of late-subscriber\n * replay coverage; raise them if you need more event headroom on long\n * verbose-mode streams.\n *\n * Terminal `done` / `error` events are always buffered regardless of\n * caps, so a late subscriber to a completed stream still sees the\n * outcome — only intermediate non-terminal events are dropped.\n *\n * All fields are optional; passing `{}` is a no-op.\n *\n * @example\n * // Halve memory ceiling on a high-churn deployment\n * bufferCaps: { maxActiveBuffers: 128, maxBytesPerBuffer: 2 * 1024 * 1024 }\n */\n bufferCaps?: BufferCaps;\n};\n\n/**\n * Minimal contract a WebSocket connection must satisfy.\n * Matches the `ws` library API (de facto standard in Node.js).\n */\nexport interface StudioWebSocket {\n send(data: string): void;\n close(): void;\n on(event: 'message', fn: (data: string | Buffer) => void): void;\n on(event: 'close', fn: () => void): void;\n on(event: 'error', fn: (err: Error) => void): void;\n}\n\n// Re-export for Hono-in-Hono consumers\nexport { handleWsMessage } from './server/ws/protocol.js';\n\nexport type StudioMiddleware = ReturnType<typeof createStudioMiddleware>;\n\nexport function createStudioMiddleware(options: StudioMiddlewareOptions) {\n const {\n runtime,\n serveClient = true,\n verifyUpgrade,\n dangerouslyAllowUnauthenticatedWebSockets = false,\n readOnly = false,\n filterTraceEvent,\n } = options;\n\n // Normalize basePath: strip trailing slashes, validate format\n const basePath = normalizeBasePath(options.basePath);\n\n // Resolve pre-built SPA assets from this package's dist/\n const staticRoot = serveClient ? resolveClientDist() : undefined;\n\n if (serveClient && !staticRoot) {\n const dir =\n import.meta.dirname ??\n (typeof __dirname !== 'undefined' ? __dirname : dirname(fileURLToPath(import.meta.url)));\n console.warn(\n '[axl-studio] serveClient is true but no pre-built client found at ' +\n `${resolve(dir, 'client')}. Studio UI will not be available. ` +\n 'Set serveClient: false to suppress this warning.',\n );\n }\n\n // Create lazy eval loader if eval files are configured\n const evalLoader = options.evals ? createEvalLoader(options.evals, runtime) : undefined;\n\n const { app, connMgr, traceListener, closeActiveRuns, closeAggregators } = createServer({\n runtime,\n staticRoot,\n basePath,\n readOnly,\n cors: false, // Host framework owns CORS policy\n evalLoader,\n bufferCaps: options.bufferCaps,\n });\n\n // Install the broadcast filter once at construction time. The connMgr\n // applies it on every outbound event; `metadata` is attached to each\n // connection after a successful `verifyUpgrade`.\n if (filterTraceEvent) {\n connMgr.setFilter(filterTraceEvent);\n }\n\n // Warn early; upgradeWebSocket() also fails closed unless explicitly acknowledged.\n if (\n process.env.NODE_ENV === 'production' &&\n !verifyUpgrade &&\n !dangerouslyAllowUnauthenticatedWebSockets\n ) {\n console.warn(\n '[axl-studio] WARNING: Studio middleware mounted in production without verifyUpgrade. ' +\n 'upgradeWebSocket() will refuse to attach. Provide verifyUpgrade or explicitly set ' +\n 'dangerouslyAllowUnauthenticatedWebSockets: true only when an outer upgrade gate is guaranteed.',\n );\n }\n\n // Convert Hono app → Node.js (req, res) handler via @hono/node-server.\n // overrideGlobalObjects: false prevents replacing global.Request/Response,\n // which could break the host application.\n const listener = getRequestListener(app.fetch, {\n overrideGlobalObjects: false,\n });\n\n let closed = false;\n\n function handler(req: IncomingMessage, res: ServerResponse) {\n if (closed) {\n res.writeHead(503, { 'Content-Type': 'application/json' });\n res.end(\n JSON.stringify({\n ok: false,\n error: { code: 'CLOSED', message: 'Studio middleware has been shut down' },\n }),\n );\n return;\n }\n\n // Express/NestJS/Koa body parsers consume the raw IncomingMessage stream\n // and store the parsed result on req.body. When that happens,\n // @hono/node-server's getRequestListener reads an empty stream and Hono\n // sees no body. To fix this, we re-serialize req.body as req.rawBody —\n // a Buffer that getRequestListener checks before falling back to the\n // stream (see newRequestFromIncoming in @hono/node-server/dist/listener.js).\n //\n // Verified against @hono/node-server@1.19.9. If this breaks after an\n // upgrade, check whether the rawBody instanceof Buffer check still exists\n // in newRequestFromIncoming.\n const reqAny = req as unknown as Record<string, unknown>;\n if (reqAny.body != null && !reqAny.rawBody) {\n try {\n reqAny.rawBody = Buffer.from(JSON.stringify(reqAny.body));\n } catch {\n // Non-serializable body — Hono will see an empty body\n }\n }\n\n listener(req, res).catch((err) => {\n console.error('[axl-studio] Unhandled error in request handler:', err);\n if (!res.headersSent) {\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(\n JSON.stringify({\n ok: false,\n error: { code: 'INTERNAL_ERROR', message: 'Internal server error' },\n }),\n );\n }\n });\n }\n\n // Handle an individual WebSocket using the Studio protocol.\n // Adapts any StudioWebSocket to ConnectionManager's internal BroadcastTarget.\n // `metadata` is whatever `verifyUpgrade` attached — passed through so the\n // `filterTraceEvent` callback can read it on every outbound event.\n function handleWebSocket(ws: StudioWebSocket, metadata?: unknown) {\n if (closed) {\n ws.close();\n return;\n }\n const socket = {\n send: (data: string) => ws.send(data),\n close: () => ws.close(),\n };\n connMgr.add(socket);\n if (metadata !== undefined) {\n connMgr.setMetadata(socket, metadata);\n }\n\n ws.on('message', (raw) => {\n const reply = handleWsMessage(String(raw), socket, connMgr);\n if (reply) ws.send(reply);\n });\n\n ws.on('close', () => connMgr.remove(socket));\n ws.on('error', () => connMgr.remove(socket));\n }\n\n // Internal WebSocketServer — created lazily by upgradeWebSocket()\n let wss: InstanceType<typeof WebSocketServer> | undefined;\n // References for cleanup: the upgrade handler and server it's attached to\n let upgradeHandler: ((...args: any[]) => void) | undefined;\n let serverRef: Server | undefined;\n\n // Convenience: attach WS handling to an http.Server.\n function upgradeWebSocket(server: Server, path?: string) {\n if (\n process.env.NODE_ENV === 'production' &&\n !verifyUpgrade &&\n !dangerouslyAllowUnauthenticatedWebSockets\n ) {\n throw new Error(\n '[axl-studio] Refusing unauthenticated WebSocket upgrades in production. ' +\n 'Provide verifyUpgrade or explicitly set dangerouslyAllowUnauthenticatedWebSockets: true.',\n );\n }\n\n if (wss) {\n throw new Error(\n '[axl-studio] upgradeWebSocket() has already been called. ' +\n 'Call close() first if you need to re-attach.',\n );\n }\n\n const wsPath = path ?? (basePath ? `${basePath}/ws` : '/ws');\n\n wss = new WebSocketServer({ noServer: true });\n serverRef = server;\n\n upgradeHandler = async (req: IncomingMessage, socket: any, head: Buffer) => {\n // Match path, ignoring query string\n const pathname = new URL(req.url!, `http://${req.headers.host}`).pathname;\n if (pathname !== wsPath) return; // Let other upgrade handlers run\n\n // Early-reject if close() already started. This is a cheap pre-check —\n // we repeat it AFTER the async verifyUpgrade resolves to close the race\n // where close() runs while verifyUpgrade is still in flight.\n if (closed) {\n socket.destroy();\n return;\n }\n\n // Apply auth verification and capture optional per-connection metadata\n // (e.g. { userId, tenantId }) that filterTraceEvent can later read.\n let connectionMetadata: unknown;\n if (verifyUpgrade) {\n try {\n const result = await verifyUpgrade(req);\n // Normalize both return shapes: plain boolean or { allowed, metadata }\n const allowed = typeof result === 'boolean' ? result : result.allowed;\n if (!allowed) {\n socket.write('HTTP/1.1 401 Unauthorized\\r\\n\\r\\n');\n socket.destroy();\n return;\n }\n if (typeof result === 'object' && result !== null) {\n connectionMetadata = result.metadata;\n }\n } catch {\n socket.write('HTTP/1.1 403 Forbidden\\r\\n\\r\\n');\n socket.destroy();\n return;\n }\n }\n\n // Guard against close() having run during async verifyUpgrade. We check\n // BOTH `closed` (set at the top of `close()`) and `!wss` (set at the\n // end). `closed` is the authoritative signal — checking `!wss` alone\n // wouldn't catch the window where `close()` has marked the middleware\n // as shut down but hasn't yet torn down the WebSocketServer. Failing\n // open here would leak a connection past the middleware's lifetime\n // and leave it holding references the host expected to be released.\n if (closed || !wss) {\n socket.destroy();\n return;\n }\n\n wss.handleUpgrade(req, socket, head, (ws) => {\n handleWebSocket(ws, connectionMetadata);\n });\n };\n\n server.on('upgrade', upgradeHandler);\n }\n\n // Cleanup function for lifecycle management.\n function close() {\n closed = true;\n\n // Abort active streaming eval runs before closing connections\n closeActiveRuns();\n\n // Close all WebSocket connections\n connMgr.closeAll();\n\n // Remove the upgrade listener from the server before closing WSS\n if (upgradeHandler && serverRef) {\n serverRef.removeListener('upgrade', upgradeHandler);\n upgradeHandler = undefined;\n serverRef = undefined;\n }\n\n // Shut down the internal WebSocketServer if one was created\n if (wss) {\n wss.close();\n wss = undefined;\n }\n\n // Remove only our trace event listener from the runtime\n if (traceListener) {\n runtime.removeListener('trace', traceListener);\n }\n\n // Close all aggregators (clear intervals and unsubscribe listeners)\n closeAggregators();\n }\n\n return {\n handler,\n handleWebSocket,\n upgradeWebSocket,\n app,\n connectionManager: connMgr,\n close,\n };\n}\n\n/**\n * Normalize and validate basePath.\n * - Empty string and undefined → ''\n * - Strip trailing slashes\n * - Validate leading slash when non-empty\n * - Reject unsafe characters\n */\nfunction normalizeBasePath(raw?: string): string {\n if (!raw) return '';\n\n // Strip trailing slashes\n const normalized = raw.replace(/\\/+$/, '');\n if (!normalized) return '';\n\n // Must start with /\n if (!normalized.startsWith('/')) {\n throw new Error(`basePath must start with '/' (got '${raw}'). Example: '/studio'`);\n }\n\n // Reject path traversal, consecutive slashes, and unsafe characters\n if (normalized.includes('..')) {\n throw new Error(`basePath must not contain '..' segments (got '${raw}')`);\n }\n if (normalized.includes('//')) {\n throw new Error(`basePath must not contain consecutive slashes (got '${raw}')`);\n }\n if (!/^\\/[a-zA-Z0-9/_-]*$/.test(normalized)) {\n throw new Error(\n `basePath contains invalid characters (got '${raw}'). ` +\n 'Only alphanumeric characters, /, _, and - are allowed.',\n );\n }\n\n return normalized;\n}\n\nfunction resolveClientDist(): string | undefined {\n // Resolve the directory of this file (dist/ in published package).\n const dir =\n import.meta.dirname ??\n (typeof __dirname !== 'undefined' ? __dirname : dirname(fileURLToPath(import.meta.url)));\n const candidate = resolve(dir, 'client');\n return existsSync(resolve(candidate, 'index.html')) ? candidate : undefined;\n}\n","import { resolve, relative, basename } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport type { AxlRuntime, EvalExecuteWorkflow } from '@axlsdk/axl';\nimport { importModule, expandGlob, registerConditions, pickDefault, pickExport } from '@axlsdk/axl';\n\n// In the CJS bundle, tsup stubs import.meta as {} so import.meta.url is\n// undefined. Fall back to __filename (which CJS defines) converted to a\n// file:// URL so tsImport() gets a valid parentURL.\nconst parentURL: string =\n import.meta.url ?? pathToFileURL(typeof __filename !== 'undefined' ? __filename : __dirname).href;\n\n/**\n * Configuration for lazy eval file discovery.\n *\n * - `string` — a glob pattern or explicit file path\n * - `string[]` — multiple patterns/paths\n * - `object` — patterns with optional import conditions\n */\nexport type EvalLoaderConfig =\n | string\n | string[]\n | {\n files: string | string[];\n\n /**\n * Custom Node.js import conditions (e.g., `['development']`).\n *\n * In monorepos, package.json `exports` often use the `development` condition\n * to point at source (`.ts`) instead of built dist. Without this, eval files\n * that import workspace packages resolve to dist files, which may not exist.\n *\n * **WARNING**: Conditions are registered process-wide via `module.register()`.\n * They affect all subsequent imports in the process, not just eval files.\n */\n conditions?: string[];\n };\n\n/**\n * Create a lazy eval loader that resolves file patterns and dynamically imports\n * eval files on first call, registering them with the runtime.\n *\n * The loader is idempotent — subsequent calls return the same promise.\n * Concurrent callers all await the same loading work.\n *\n * Eval files should export a default config with `{ workflow, dataset, scorers }`\n * (the result of `defineEval()` from `@axlsdk/eval`). An optional named export\n * `executeWorkflow` overrides the default `runtime.execute()` behavior.\n *\n * Eval names are the file's path relative to `cwd` (project root), minus the\n * `.eval.*` suffix. This makes names completely stable — a file's name never\n * changes regardless of what other files or patterns exist.\n *\n * @param config Glob patterns, file paths, or object with conditions\n * @param runtime The AxlRuntime to register discovered evals on\n * @param cwd Base directory for resolving patterns and deriving names (default: `process.cwd()`)\n */\nexport function createEvalLoader(\n config: EvalLoaderConfig,\n runtime: AxlRuntime,\n cwd?: string,\n): () => Promise<void> {\n let loadPromise: Promise<void> | undefined;\n const { patterns, conditions } = normalizeConfig(config);\n const baseCwd = cwd ?? process.cwd();\n\n return () => {\n if (!loadPromise) {\n loadPromise = loadEvalFiles(patterns, conditions, baseCwd, runtime).catch((err) => {\n loadPromise = undefined; // Allow retry on next request\n throw err;\n });\n }\n return loadPromise;\n };\n}\n\n// ── Core loading logic ─────────────────────────────────────────────\n\nasync function loadEvalFiles(\n patterns: string[],\n conditions: string[],\n cwd: string,\n runtime: AxlRuntime,\n): Promise<void> {\n if (conditions.length > 0) {\n await registerConditions(conditions);\n }\n\n const files = resolvePatterns(patterns, cwd);\n\n if (files.length === 0) {\n console.warn(`[axl-studio] No eval files found matching: ${patterns.join(', ')}`);\n return;\n }\n\n for (const file of files) {\n try {\n const mod = await importModule(file, parentURL);\n const evalConfig = pickDefault<{\n workflow?: string;\n dataset?: unknown;\n scorers?: unknown;\n }>(mod);\n\n if (!evalConfig || typeof evalConfig !== 'object') {\n console.warn(\n `[axl-studio] Skipping ${file}: default export is ` +\n `${evalConfig === null ? 'null' : typeof evalConfig}, expected an eval config object.`,\n );\n continue;\n }\n if (Array.isArray(evalConfig.scorers) && evalConfig.scorers.length === 0) {\n console.warn(\n `[axl-studio] Skipping ${file}: scorers array is empty — at least one scorer is required.`,\n );\n continue;\n }\n if (!evalConfig.workflow || !evalConfig.dataset || !evalConfig.scorers) {\n const missing = [\n !evalConfig.workflow && 'workflow',\n !evalConfig.dataset && 'dataset',\n !evalConfig.scorers && 'scorers',\n ]\n .filter(Boolean)\n .join(', ');\n const keys = Object.keys(evalConfig).slice(0, 10);\n const got = keys.length > 0 ? ` Got: { ${keys.join(', ')} }.` : '';\n console.warn(\n `[axl-studio] Skipping ${file}: not a valid eval config (missing ${missing}).${got}`,\n );\n continue;\n }\n\n const name = deriveEvalName(file, cwd);\n\n if (runtime.getRegisteredEval(name)) {\n console.warn(\n `[axl-studio] Eval name \"${name}\" from ${file} collides with an ` +\n `already-registered eval — overwriting`,\n );\n }\n\n // Walk the ESM/CJS interop chain symmetrically with the default export\n // so `executeWorkflow` stays visible when tsx loads the eval module as CJS\n // (e.g. a `.ts` file in a package without `\"type\": \"module\"`). Validate\n // the export is callable — a non-function `executeWorkflow` is silently\n // ignored so registration falls back to `runtime.execute()`, with a\n // warning so the user knows their export was rejected.\n const exported = pickExport<unknown>(mod, 'executeWorkflow');\n let customExecute: EvalExecuteWorkflow | undefined;\n if (typeof exported === 'function') {\n customExecute = exported as EvalExecuteWorkflow;\n } else if (exported !== undefined) {\n console.warn(\n `[axl-studio] ${file} exports executeWorkflow but it is ${typeof exported}, ` +\n `not a function — ignoring and falling back to runtime.execute()`,\n );\n }\n runtime.registerEval(name, evalConfig, customExecute);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n console.warn(`[axl-studio] Failed to load eval ${file}: ${msg}`);\n }\n }\n}\n\n// ── Internal helpers ───────────────────────────────────────────────\n\nfunction normalizeConfig(config: EvalLoaderConfig): {\n patterns: string[];\n conditions: string[];\n} {\n if (typeof config === 'string') {\n return { patterns: [config], conditions: [] };\n }\n if (Array.isArray(config)) {\n return { patterns: config, conditions: [] };\n }\n const files = typeof config.files === 'string' ? [config.files] : config.files;\n return { patterns: files, conditions: config.conditions ?? [] };\n}\n\n/**\n * Derive eval name from file path relative to cwd.\n *\n * Examples (cwd = `/project`):\n * - `/project/evals/suggestions.eval.ts` → `\"evals/suggestions\"`\n * - `/project/evals/api/accuracy.eval.ts` → `\"evals/api/accuracy\"`\n */\nfunction deriveEvalName(filePath: string, cwd: string): string {\n const rel = relative(cwd, filePath);\n // Normalize to forward slashes for cross-platform consistency\n const normalized = rel.replace(/\\\\/g, '/');\n // Guard: file outside cwd (symlink, absolute path) — fall back to basename\n if (normalized.startsWith('../')) {\n const base = basename(filePath);\n const stripped = base.replace(/\\.eval\\.[mc]?[jt]sx?$/, '');\n return stripped !== base ? stripped : base.replace(/\\.[mc]?[jt]sx?$/, '') || base;\n }\n // Strip .eval.ts, .eval.mjs, .eval.js, etc.\n const withoutEval = normalized.replace(/\\.eval\\.[mc]?[jt]sx?$/, '');\n if (withoutEval !== normalized) return withoutEval;\n // Fallback: strip extension\n const withoutExt = normalized.replace(/\\.[mc]?[jt]sx?$/, '');\n return withoutExt || normalized;\n}\n\n/**\n * Resolve patterns to absolute file paths.\n *\n * Supports:\n * - Explicit file paths (no wildcards)\n * - Single-directory globs: `dir/*.eval.ts`\n * - Recursive globs: `dir/**\\/*.eval.ts` or `**\\/*.eval.ts`\n *\n * Multi-segment `**` (e.g., `a/**\\/b/**\\/*.ts`) is not supported.\n */\nfunction resolvePatterns(patterns: string[], cwd: string): string[] {\n const files: string[] = [];\n const seen = new Set<string>();\n for (const pattern of patterns) {\n const resolved = pattern.includes('*') ? expandGlob(pattern, cwd) : [resolve(cwd, pattern)];\n for (const file of resolved) {\n if (!seen.has(file)) {\n seen.add(file);\n files.push(file);\n }\n }\n }\n return files;\n}\n\n// `expandGlob` and `registerConditions` are imported from @axlsdk/axl above.\n// The studio-specific glob helpers and conditions registration that used to\n// live here are now shared with @axlsdk/eval to prevent drift.\n"],"mappings":"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","names":["resolve","resolve"]}
@@ -1395,7 +1395,10 @@ function createSessionRoutes(connMgr) {
1395
1395
  const body = await c.req.json();
1396
1396
  const session = runtime.session(id);
1397
1397
  const result = await session.send(body.workflow, body.message);
1398
- return c.json({ ok: true, data: { result } });
1398
+ return c.json({
1399
+ ok: true,
1400
+ data: { result: redactValue(result, runtime.isRedactEnabled()) }
1401
+ });
1399
1402
  });
1400
1403
  app5.post("/sessions/:id/stream", async (c) => {
1401
1404
  const runtime = c.get("runtime");
@@ -1404,9 +1407,13 @@ function createSessionRoutes(connMgr) {
1404
1407
  const session = runtime.session(id);
1405
1408
  const stream = await session.stream(body.workflow, body.message);
1406
1409
  const executionId = `session-${id}-${Date.now()}`;
1410
+ const redactOn = runtime.isRedactEnabled();
1407
1411
  (async () => {
1408
1412
  for await (const event of stream) {
1409
- connMgr.broadcastWithWildcard(`execution:${executionId}`, event);
1413
+ connMgr.broadcastWithWildcard(
1414
+ `execution:${executionId}`,
1415
+ redactStreamEvent(event, redactOn)
1416
+ );
1410
1417
  }
1411
1418
  })();
1412
1419
  return c.json({ ok: true, data: { executionId, streaming: true } });
@@ -16112,7 +16119,7 @@ function createServer(options) {
16112
16119
  emptyState: emptyEvalTrendData,
16113
16120
  windows
16114
16121
  });
16115
- if (options.cors !== false) {
16122
+ if (options.cors === true) {
16116
16123
  app5.use("*", (0, import_cors.cors)());
16117
16124
  }
16118
16125
  app5.onError(errorHandler);