@axlsdk/studio 0.9.1 → 0.10.1

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.
@@ -25,6 +25,8 @@ __export(server_exports, {
25
25
  createServer: () => createServer
26
26
  });
27
27
  module.exports = __toCommonJS(server_exports);
28
+ var import_node_fs = require("fs");
29
+ var import_node_path = require("path");
28
30
  var import_hono12 = require("hono");
29
31
  var import_cors = require("hono/cors");
30
32
  var import_serve_static = require("@hono/node-server/serve-static");
@@ -61,8 +63,13 @@ var ConnectionManager = class {
61
63
  channels = /* @__PURE__ */ new Map();
62
64
  /** ws -> set of subscribed channels (for cleanup) */
63
65
  connections = /* @__PURE__ */ new Map();
66
+ maxConnections = 100;
64
67
  /** Register a new WS connection. */
65
68
  add(ws) {
69
+ if (this.connections.size >= this.maxConnections) {
70
+ ws.close?.();
71
+ return;
72
+ }
66
73
  this.connections.set(ws, /* @__PURE__ */ new Set());
67
74
  }
68
75
  /** Remove a WS connection and all its subscriptions. */
@@ -78,15 +85,16 @@ var ConnectionManager = class {
78
85
  }
79
86
  this.connections.delete(ws);
80
87
  }
81
- /** Subscribe a connection to a channel. */
88
+ /** Subscribe a connection to a channel. No-op if the connection was not added. */
82
89
  subscribe(ws, channel) {
90
+ if (!this.connections.has(ws)) return;
83
91
  let subs = this.channels.get(channel);
84
92
  if (!subs) {
85
93
  subs = /* @__PURE__ */ new Set();
86
94
  this.channels.set(channel, subs);
87
95
  }
88
96
  subs.add(ws);
89
- this.connections.get(ws)?.add(channel);
97
+ this.connections.get(ws).add(channel);
90
98
  }
91
99
  /** Unsubscribe a connection from a channel. */
92
100
  unsubscribe(ws, channel) {
@@ -127,6 +135,14 @@ var ConnectionManager = class {
127
135
  }
128
136
  }
129
137
  }
138
+ /** Close all connections and clear all state. Used during shutdown. */
139
+ closeAll() {
140
+ for (const ws of this.connections.keys()) {
141
+ ws.close?.();
142
+ }
143
+ this.connections.clear();
144
+ this.channels.clear();
145
+ }
130
146
  /** Get the number of active connections. */
131
147
  get connectionCount() {
132
148
  return this.connections.size;
@@ -137,6 +153,52 @@ var ConnectionManager = class {
137
153
  }
138
154
  };
139
155
 
156
+ // src/server/ws/protocol.ts
157
+ var VALID_CHANNEL_PREFIXES = ["execution:", "trace:"];
158
+ var VALID_EXACT_CHANNELS = ["costs", "decisions"];
159
+ var MAX_CHANNEL_LENGTH = 256;
160
+ function handleWsMessage(raw, socket, connMgr) {
161
+ if (raw.length > 65536) {
162
+ return JSON.stringify({ type: "error", message: "Message too large" });
163
+ }
164
+ let msg;
165
+ try {
166
+ msg = JSON.parse(raw);
167
+ } catch {
168
+ return JSON.stringify({ type: "error", message: "Invalid JSON" });
169
+ }
170
+ switch (msg.type) {
171
+ case "subscribe": {
172
+ const error = validateChannel(msg.channel);
173
+ if (error) return JSON.stringify({ type: "error", message: error });
174
+ connMgr.subscribe(socket, msg.channel);
175
+ return JSON.stringify({ type: "subscribed", channel: msg.channel });
176
+ }
177
+ case "unsubscribe": {
178
+ const error = validateChannel(msg.channel);
179
+ if (error) return JSON.stringify({ type: "error", message: error });
180
+ connMgr.unsubscribe(socket, msg.channel);
181
+ return JSON.stringify({ type: "unsubscribed", channel: msg.channel });
182
+ }
183
+ case "ping":
184
+ return JSON.stringify({ type: "pong" });
185
+ default:
186
+ return JSON.stringify({ type: "error", message: "Unknown message type" });
187
+ }
188
+ }
189
+ function validateChannel(channel) {
190
+ if (typeof channel !== "string" || !channel) {
191
+ return "Missing or invalid channel";
192
+ }
193
+ if (channel.length > MAX_CHANNEL_LENGTH) {
194
+ return `Channel name exceeds ${MAX_CHANNEL_LENGTH} characters`;
195
+ }
196
+ if (!VALID_EXACT_CHANNELS.includes(channel) && !VALID_CHANNEL_PREFIXES.some((p) => channel.startsWith(p))) {
197
+ return `Invalid channel: ${channel}`;
198
+ }
199
+ return null;
200
+ }
201
+
140
202
  // src/server/ws/handler.ts
141
203
  function createWsHandlers(connMgr) {
142
204
  return {
@@ -144,37 +206,8 @@ function createWsHandlers(connMgr) {
144
206
  connMgr.add(ws);
145
207
  },
146
208
  onMessage(event, ws) {
147
- let msg;
148
- try {
149
- msg = JSON.parse(String(event.data));
150
- } catch {
151
- const err = { type: "error", message: "Invalid JSON" };
152
- ws.send(JSON.stringify(err));
153
- return;
154
- }
155
- switch (msg.type) {
156
- case "subscribe": {
157
- connMgr.subscribe(ws, msg.channel);
158
- const reply = { type: "subscribed", channel: msg.channel };
159
- ws.send(JSON.stringify(reply));
160
- break;
161
- }
162
- case "unsubscribe": {
163
- connMgr.unsubscribe(ws, msg.channel);
164
- const reply = { type: "unsubscribed", channel: msg.channel };
165
- ws.send(JSON.stringify(reply));
166
- break;
167
- }
168
- case "ping": {
169
- const reply = { type: "pong" };
170
- ws.send(JSON.stringify(reply));
171
- break;
172
- }
173
- default: {
174
- const err = { type: "error", message: `Unknown message type` };
175
- ws.send(JSON.stringify(err));
176
- }
177
- }
209
+ const reply = handleWsMessage(String(event.data), ws, connMgr);
210
+ if (reply) ws.send(reply);
178
211
  },
179
212
  onClose(_event, ws) {
180
213
  connMgr.remove(ws);
@@ -269,8 +302,8 @@ var health_default = app;
269
302
  var import_hono2 = require("hono");
270
303
  var import_axl = require("@axlsdk/axl");
271
304
  function createWorkflowRoutes(connMgr) {
272
- const app8 = new import_hono2.Hono();
273
- app8.get("/workflows", (c) => {
305
+ const app7 = new import_hono2.Hono();
306
+ app7.get("/workflows", (c) => {
274
307
  const runtime = c.get("runtime");
275
308
  const workflows = runtime.getWorkflows().map((w) => ({
276
309
  name: w.name,
@@ -279,7 +312,7 @@ function createWorkflowRoutes(connMgr) {
279
312
  }));
280
313
  return c.json({ ok: true, data: workflows });
281
314
  });
282
- app8.get("/workflows/:name", (c) => {
315
+ app7.get("/workflows/:name", (c) => {
283
316
  const runtime = c.get("runtime");
284
317
  const name = c.req.param("name");
285
318
  const workflow = runtime.getWorkflow(name);
@@ -298,7 +331,7 @@ function createWorkflowRoutes(connMgr) {
298
331
  }
299
332
  });
300
333
  });
301
- app8.post("/workflows/:name/execute", async (c) => {
334
+ app7.post("/workflows/:name/execute", async (c) => {
302
335
  const runtime = c.get("runtime");
303
336
  const name = c.req.param("name");
304
337
  const workflow = runtime.getWorkflow(name);
@@ -329,7 +362,7 @@ function createWorkflowRoutes(connMgr) {
329
362
  const result = await runtime.execute(name, body.input ?? {}, { metadata: body.metadata });
330
363
  return c.json({ ok: true, data: { result } });
331
364
  });
332
- return app8;
365
+ return app7;
333
366
  }
334
367
 
335
368
  // src/server/routes/executions.ts
@@ -363,8 +396,8 @@ var executions_default = app2;
363
396
  // src/server/routes/sessions.ts
364
397
  var import_hono4 = require("hono");
365
398
  function createSessionRoutes(connMgr) {
366
- const app8 = new import_hono4.Hono();
367
- app8.get("/sessions", async (c) => {
399
+ const app7 = new import_hono4.Hono();
400
+ app7.get("/sessions", async (c) => {
368
401
  const runtime = c.get("runtime");
369
402
  const store = runtime.getStateStore();
370
403
  if (!store.listSessions) {
@@ -378,7 +411,7 @@ function createSessionRoutes(connMgr) {
378
411
  }
379
412
  return c.json({ ok: true, data: sessions });
380
413
  });
381
- app8.get("/sessions/:id", async (c) => {
414
+ app7.get("/sessions/:id", async (c) => {
382
415
  const runtime = c.get("runtime");
383
416
  const store = runtime.getStateStore();
384
417
  const id = c.req.param("id");
@@ -386,7 +419,7 @@ function createSessionRoutes(connMgr) {
386
419
  const handoffHistory = await store.getSessionMeta(id, "handoffHistory");
387
420
  return c.json({ ok: true, data: { id, history, handoffHistory: handoffHistory ?? [] } });
388
421
  });
389
- app8.post("/sessions/:id/send", async (c) => {
422
+ app7.post("/sessions/:id/send", async (c) => {
390
423
  const runtime = c.get("runtime");
391
424
  const id = c.req.param("id");
392
425
  const body = await c.req.json();
@@ -394,7 +427,7 @@ function createSessionRoutes(connMgr) {
394
427
  const result = await session.send(body.workflow, body.message);
395
428
  return c.json({ ok: true, data: { result } });
396
429
  });
397
- app8.post("/sessions/:id/stream", async (c) => {
430
+ app7.post("/sessions/:id/stream", async (c) => {
398
431
  const runtime = c.get("runtime");
399
432
  const id = c.req.param("id");
400
433
  const body = await c.req.json();
@@ -415,14 +448,14 @@ function createSessionRoutes(connMgr) {
415
448
  })();
416
449
  return c.json({ ok: true, data: { executionId, streaming: true } });
417
450
  });
418
- app8.delete("/sessions/:id", async (c) => {
451
+ app7.delete("/sessions/:id", async (c) => {
419
452
  const runtime = c.get("runtime");
420
453
  const store = runtime.getStateStore();
421
454
  const id = c.req.param("id");
422
455
  await store.deleteSession(id);
423
456
  return c.json({ ok: true, data: { deleted: true } });
424
457
  });
425
- return app8;
458
+ return app7;
426
459
  }
427
460
 
428
461
  // src/server/routes/agents.ts
@@ -655,61 +688,65 @@ var decisions_default = app6;
655
688
  // src/server/routes/costs.ts
656
689
  var import_hono9 = require("hono");
657
690
  function createCostRoutes(costAggregator) {
658
- const app8 = new import_hono9.Hono();
659
- app8.get("/costs", (c) => {
691
+ const app7 = new import_hono9.Hono();
692
+ app7.get("/costs", (c) => {
660
693
  return c.json({ ok: true, data: costAggregator.getData() });
661
694
  });
662
- app8.post("/costs/reset", (c) => {
695
+ app7.post("/costs/reset", (c) => {
663
696
  costAggregator.reset();
664
697
  return c.json({ ok: true, data: { reset: true } });
665
698
  });
666
- return app8;
699
+ return app7;
667
700
  }
668
701
 
669
702
  // src/server/routes/evals.ts
670
703
  var import_hono10 = require("hono");
671
- var app7 = new import_hono10.Hono();
672
- app7.get("/evals", async (c) => {
673
- const runtime = c.get("runtime");
674
- const evals = runtime.getRegisteredEvals();
675
- return c.json({ ok: true, data: evals });
676
- });
677
- app7.post("/evals/:name/run", async (c) => {
678
- const runtime = c.get("runtime");
679
- const name = c.req.param("name");
680
- const entry = runtime.getRegisteredEval(name);
681
- if (!entry) {
682
- return c.json(
683
- { ok: false, error: { code: "NOT_FOUND", message: `Eval "${name}" not found` } },
684
- 404
685
- );
686
- }
687
- try {
688
- const result = await runtime.runRegisteredEval(name);
689
- return c.json({ ok: true, data: result });
690
- } catch (err) {
691
- const message = err instanceof Error ? err.message : String(err);
692
- return c.json({ ok: false, error: { code: "EVAL_ERROR", message } }, 400);
693
- }
694
- });
695
- app7.post("/evals/compare", async (c) => {
696
- const runtime = c.get("runtime");
697
- const body = await c.req.json();
698
- try {
699
- const result = await runtime.evalCompare(body.baseline, body.candidate);
700
- return c.json({ ok: true, data: result });
701
- } catch (err) {
702
- const message = err instanceof Error ? err.message : String(err);
703
- return c.json({ ok: false, error: { code: "EVAL_ERROR", message } }, 400);
704
- }
705
- });
706
- var evals_default = app7;
704
+ function createEvalRoutes(evalLoader) {
705
+ const app7 = new import_hono10.Hono();
706
+ app7.get("/evals", async (c) => {
707
+ if (evalLoader) await evalLoader();
708
+ const runtime = c.get("runtime");
709
+ const evals = runtime.getRegisteredEvals();
710
+ return c.json({ ok: true, data: evals });
711
+ });
712
+ app7.post("/evals/:name/run", async (c) => {
713
+ if (evalLoader) await evalLoader();
714
+ const runtime = c.get("runtime");
715
+ const name = c.req.param("name");
716
+ const entry = runtime.getRegisteredEval(name);
717
+ if (!entry) {
718
+ return c.json(
719
+ { ok: false, error: { code: "NOT_FOUND", message: `Eval "${name}" not found` } },
720
+ 404
721
+ );
722
+ }
723
+ try {
724
+ const result = await runtime.runRegisteredEval(name);
725
+ return c.json({ ok: true, data: result });
726
+ } catch (err) {
727
+ const message = err instanceof Error ? err.message : String(err);
728
+ return c.json({ ok: false, error: { code: "EVAL_ERROR", message } }, 400);
729
+ }
730
+ });
731
+ app7.post("/evals/compare", async (c) => {
732
+ const runtime = c.get("runtime");
733
+ const body = await c.req.json();
734
+ try {
735
+ const result = await runtime.evalCompare(body.baseline, body.candidate);
736
+ return c.json({ ok: true, data: result });
737
+ } catch (err) {
738
+ const message = err instanceof Error ? err.message : String(err);
739
+ return c.json({ ok: false, error: { code: "EVAL_ERROR", message } }, 400);
740
+ }
741
+ });
742
+ return app7;
743
+ }
707
744
 
708
745
  // src/server/routes/playground.ts
709
746
  var import_hono11 = require("hono");
710
747
  function createPlaygroundRoutes(connMgr) {
711
- const app8 = new import_hono11.Hono();
712
- app8.post("/playground/chat", async (c) => {
748
+ const app7 = new import_hono11.Hono();
749
+ app7.post("/playground/chat", async (c) => {
713
750
  const runtime = c.get("runtime");
714
751
  const body = await c.req.json();
715
752
  const workflowName = body.workflow ?? runtime.getWorkflowNames()[0];
@@ -740,21 +777,53 @@ function createPlaygroundRoutes(connMgr) {
740
777
  data: { sessionId, executionId, streaming: true }
741
778
  });
742
779
  });
743
- return app8;
780
+ return app7;
744
781
  }
745
782
 
746
783
  // src/server/index.ts
747
784
  function createServer(options) {
748
- const { runtime, staticRoot } = options;
749
- const app8 = new import_hono12.Hono();
785
+ const { runtime, staticRoot, basePath = "", readOnly = false } = options;
786
+ const app7 = new import_hono12.Hono();
750
787
  const connMgr = new ConnectionManager();
751
788
  const costAggregator = new CostAggregator(connMgr);
752
- app8.use("*", (0, import_cors.cors)());
753
- app8.use("*", errorHandler);
754
- app8.use("*", async (c, next) => {
789
+ if (options.cors !== false) {
790
+ app7.use("*", (0, import_cors.cors)());
791
+ }
792
+ app7.use("*", errorHandler);
793
+ app7.use("*", async (c, next) => {
755
794
  c.set("runtime", runtime);
756
795
  await next();
757
796
  });
797
+ if (readOnly) {
798
+ const blocked = [
799
+ "POST /api/workflows",
800
+ "POST /api/executions",
801
+ "POST /api/sessions",
802
+ "DELETE /api/sessions",
803
+ "PUT /api/memory",
804
+ "DELETE /api/memory",
805
+ "POST /api/decisions",
806
+ "POST /api/costs",
807
+ "POST /api/tools",
808
+ "POST /api/evals",
809
+ "POST /api/playground"
810
+ ];
811
+ app7.use("/api/*", async (c, next) => {
812
+ const apiIdx = c.req.path.indexOf("/api/");
813
+ const apiPath = apiIdx >= 0 ? c.req.path.slice(apiIdx) : c.req.path;
814
+ const key = `${c.req.method} ${apiPath}`;
815
+ if (blocked.some((b) => key.startsWith(b))) {
816
+ return c.json(
817
+ {
818
+ ok: false,
819
+ error: { code: "READ_ONLY", message: "Studio is mounted in read-only mode" }
820
+ },
821
+ 405
822
+ );
823
+ }
824
+ await next();
825
+ });
826
+ }
758
827
  const api = new import_hono12.Hono();
759
828
  api.route("/", health_default);
760
829
  api.route("/", createWorkflowRoutes(connMgr));
@@ -765,10 +834,10 @@ function createServer(options) {
765
834
  api.route("/", memory_default);
766
835
  api.route("/", decisions_default);
767
836
  api.route("/", createCostRoutes(costAggregator));
768
- api.route("/", evals_default);
837
+ api.route("/", createEvalRoutes(options.evalLoader));
769
838
  api.route("/", createPlaygroundRoutes(connMgr));
770
- app8.route("/api", api);
771
- runtime.on("trace", (event) => {
839
+ app7.route("/api", api);
840
+ const traceListener = (event) => {
772
841
  const traceEvent = event;
773
842
  if (traceEvent.executionId) {
774
843
  connMgr.broadcastWithWildcard(`trace:${traceEvent.executionId}`, traceEvent);
@@ -777,12 +846,47 @@ function createServer(options) {
777
846
  if (traceEvent.type === "await_human") {
778
847
  connMgr.broadcast("decisions", traceEvent);
779
848
  }
780
- });
849
+ };
850
+ runtime.on("trace", traceListener);
781
851
  if (staticRoot) {
782
- app8.use("/*", (0, import_serve_static.serveStatic)({ root: staticRoot }));
783
- app8.get("*", (0, import_serve_static.serveStatic)({ root: staticRoot, path: "/index.html" }));
852
+ app7.use(
853
+ "/*",
854
+ (0, import_serve_static.serveStatic)({
855
+ root: staticRoot,
856
+ rewriteRequestPath: basePath ? (path) => path.startsWith(basePath) ? path.slice(basePath.length) || "/" : path : void 0
857
+ })
858
+ );
859
+ if (basePath) {
860
+ const indexPath = (0, import_node_path.resolve)(staticRoot, "index.html");
861
+ if (!(0, import_node_fs.existsSync)(indexPath)) {
862
+ console.warn(`[axl-studio] index.html not found at ${indexPath}`);
863
+ } else {
864
+ const indexHtml = (0, import_node_fs.readFileSync)(indexPath, "utf-8");
865
+ const safeBasePath = JSON.stringify(basePath).replace(/</g, "\\u003c");
866
+ const injectedHtml = indexHtml.replace(
867
+ "</head>",
868
+ `<base href="${basePath}/">
869
+ <script>window.__AXL_STUDIO_BASE__=${safeBasePath}</script>
870
+ </head>`
871
+ );
872
+ if (injectedHtml === indexHtml) {
873
+ console.warn(
874
+ "[axl-studio] Could not inject basePath into index.html \u2014 </head> tag not found. The SPA may not route correctly."
875
+ );
876
+ }
877
+ app7.get("*", (c) => c.html(injectedHtml));
878
+ }
879
+ } else {
880
+ app7.get("*", (0, import_serve_static.serveStatic)({ root: staticRoot, path: "/index.html" }));
881
+ }
784
882
  }
785
- return { app: app8, connMgr, costAggregator, createWsHandlers: () => createWsHandlers(connMgr) };
883
+ return {
884
+ app: app7,
885
+ connMgr,
886
+ costAggregator,
887
+ createWsHandlers: () => createWsHandlers(connMgr),
888
+ traceListener
889
+ };
786
890
  }
787
891
  // Annotate the CommonJS export names for ESM import in node:
788
892
  0 && (module.exports = {