@dainprotocol/service-sdk 2.0.91 → 2.0.92

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.
@@ -9,15 +9,16 @@ const cors_1 = require("hono/cors");
9
9
  const http_exception_1 = require("hono/http-exception");
10
10
  const auth_1 = require("./auth");
11
11
  const schemaStructure_1 = require("../lib/schemaStructure");
12
+ const toolDiscovery_1 = require("../lib/toolDiscovery");
12
13
  const oauth2_1 = require("./oauth2");
13
14
  const bs58_1 = tslib_1.__importDefault(require("bs58"));
14
15
  const processes_1 = require("./processes");
15
16
  const streaming_1 = require("hono/streaming");
16
17
  const package_json_1 = tslib_1.__importDefault(require("../../package.json"));
17
18
  const zod_1 = require("zod");
18
- const ed25519_1 = require("@noble/curves/ed25519");
19
- const sha256_1 = require("@noble/hashes/sha256");
20
- const utils_1 = require("@noble/hashes/utils");
19
+ const ed25519_js_1 = require("@noble/curves/ed25519.js");
20
+ const sha2_js_1 = require("@noble/hashes/sha2.js");
21
+ const utils_js_1 = require("@noble/hashes/utils.js");
21
22
  const auth_2 = require("./auth");
22
23
  const core_1 = require("./core");
23
24
  const hitl_1 = require("./hitl");
@@ -32,6 +33,46 @@ function debugWarn(...args) {
32
33
  console.warn(...args);
33
34
  }
34
35
  }
36
+ /**
37
+ * Extract a user-safe error message. If `err` is a ZodError (or `err.message`
38
+ * looks like a raw Zod v4 issue-array JSON `"[{...}]"`), re-summarize as
39
+ * `"<path>: <msg>; <path>: <msg>"` so it never lands verbatim in chat text.
40
+ *
41
+ * Backstop for the tool-output validation leak: the 2026-04-17 whitelist
42
+ * regression surfaced `[{"code":"invalid_type","path":["markets"],...}]` in
43
+ * user-visible chat because the four SSE/non-SSE error exits below all do
44
+ * `text: \`Error: ${error.message}\``. This helper neutralizes that class
45
+ * of bug for ANY ZodError thrown anywhere in the tool-exec pipeline, not
46
+ * just the one we already fixed at the source in core.ts.
47
+ */
48
+ function sanitizeErrorMessage(err) {
49
+ if (err instanceof zod_1.z.ZodError) {
50
+ const summary = err.issues
51
+ .map((i) => `${i.path.join(".") || "root"}: ${i.message}`)
52
+ .join("; ");
53
+ return summary || "Validation failed";
54
+ }
55
+ const raw = err?.message;
56
+ if (typeof raw !== "string" || raw.length === 0)
57
+ return "Unknown error";
58
+ const trimmed = raw.trimStart();
59
+ if (trimmed.startsWith("[{") || trimmed.startsWith("[ {")) {
60
+ try {
61
+ const parsed = JSON.parse(trimmed);
62
+ if (Array.isArray(parsed)) {
63
+ const summary = parsed
64
+ .map((i) => `${Array.isArray(i.path) ? i.path.join(".") || "root" : "root"}: ${i.message ?? "invalid"}`)
65
+ .join("; ");
66
+ if (summary)
67
+ return summary;
68
+ }
69
+ }
70
+ catch {
71
+ /* fall through, return raw message */
72
+ }
73
+ }
74
+ return raw;
75
+ }
35
76
  function getFirstForwardedValue(value) {
36
77
  if (!value)
37
78
  return undefined;
@@ -78,23 +119,30 @@ function requireScope(requiredScope) {
78
119
  // Helper to sign and stream SSE events
79
120
  function signedStreamSSE(c, privateKey, config, handler) {
80
121
  return (0, streaming_1.streamSSE)(c, async (stream) => {
122
+ const requestSignal = c.req.raw?.signal;
123
+ const isAborted = () => stream.aborted || stream.closed || requestSignal?.aborted === true;
81
124
  const signedStream = {
82
125
  writeSSE: async (event) => {
126
+ if (isAborted())
127
+ return;
83
128
  const timestamp = Date.now().toString();
84
129
  const message = `${event.data}:${timestamp}`;
85
- const messageHash = (0, sha256_1.sha256)(message);
86
- const signatureBytes = ed25519_1.ed25519.sign(messageHash, privateKey);
87
- const signature = (0, utils_1.bytesToHex)(signatureBytes);
130
+ const messageHash = (0, sha2_js_1.sha256)((0, utils_js_1.utf8ToBytes)(message));
131
+ const signatureBytes = ed25519_js_1.ed25519.sign(messageHash, privateKey);
132
+ const signature = (0, utils_js_1.bytesToHex)(signatureBytes);
88
133
  // Fast path for non-critical events (progress/UI updates)
89
- const isCriticalEvent = event.event === 'result' || event.event === 'process-created';
134
+ const isCriticalEvent = event.event === 'result' ||
135
+ event.event === 'process-created' ||
136
+ event.event === 'datasource-update';
90
137
  const dataWithSignature = isCriticalEvent
91
138
  ? JSON.stringify({
92
139
  data: JSON.parse(event.data),
93
140
  _signature: { signature, timestamp, agentId: config.identity.agentId, orgId: config.identity.orgId, address: config.identity.publicKey }
94
141
  })
95
142
  : `{"data":${event.data},"_signature":{"signature":"${signature}","timestamp":"${timestamp}","agentId":"${config.identity.agentId}","orgId":"${config.identity.orgId}","address":"${config.identity.publicKey}"}}`;
96
- return stream.writeSSE({ event: event.event, data: dataWithSignature, id: event.id });
97
- }
143
+ await stream.writeSSE({ event: event.event, data: dataWithSignature, id: event.id });
144
+ },
145
+ isAborted,
98
146
  };
99
147
  await handler(signedStream);
100
148
  });
@@ -174,11 +222,7 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
174
222
  const isHITLPath = hitlPath && c.req.path === hitlPath;
175
223
  if (c.req.path.startsWith("/oauth2/callback/") ||
176
224
  c.req.path.startsWith("/addons") ||
177
- c.req.path.startsWith("/getAllToolsAsJsonSchema") ||
178
- c.req.path.startsWith("/getSkills") ||
179
- c.req.path.startsWith("/getWebhookTriggers") ||
180
225
  c.req.path.startsWith("/metadata") ||
181
- c.req.path.startsWith("/recommendations") ||
182
226
  c.req.path.startsWith("/ping") ||
183
227
  isWebhookPath ||
184
228
  isHITLPath) {
@@ -300,15 +344,73 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
300
344
  debugLog("[getAgentInfo] API Key - Returning agent info:", agentInfo);
301
345
  return agentInfo;
302
346
  }
347
+ async function resolveHealthStatus() {
348
+ if (!config.healthCheck)
349
+ return true;
350
+ try {
351
+ return await config.healthCheck();
352
+ }
353
+ catch {
354
+ return false;
355
+ }
356
+ }
303
357
  // Setup default ping route
304
- app.get("/ping", (c) => c.json({ message: "pong", platform: "DAIN", service_version: metadata.version, dain_sdk_version: package_json_1.default.version }));
358
+ app.get("/ping", async (c) => {
359
+ const healthy = await resolveHealthStatus();
360
+ return c.json({
361
+ message: healthy ? "pong" : "unhealthy",
362
+ platform: "DAIN",
363
+ service_version: metadata.version,
364
+ dain_sdk_version: package_json_1.default.version,
365
+ }, healthy ? 200 : 503);
366
+ });
305
367
  // Metadata endpoint - includes computed supportsUserActions from tools
306
368
  app.get("/metadata", (c) => {
307
369
  // Compute service-level capability: does ANY tool support user actions (HITL)?
308
370
  const supportsUserActions = tools.some((tool) => tool.supportsUserActions === true);
371
+ const requestedContract = c.req.header("x-butterfly-contract") || c.req.header("X-Butterfly-Contract");
372
+ const sdkMajor = Number.parseInt(String(package_json_1.default.version).split(".")[0] || "0", 10);
373
+ const contractVersion = Number.isFinite(sdkMajor) && sdkMajor > 0 ? `${sdkMajor}.0.0` : "0.0.0";
374
+ const capabilities = {
375
+ tools: true,
376
+ contexts: true,
377
+ widgets: true,
378
+ datasources: true,
379
+ streamingTools: true,
380
+ streamingDatasources: true,
381
+ datasourcePolicy: true,
382
+ widgetPolicy: true,
383
+ toolSafety: true,
384
+ };
385
+ const compatibility = (() => {
386
+ if (!requestedContract)
387
+ return undefined;
388
+ const requestedMajor = Number.parseInt(String(requestedContract).split(".")[0] || "0", 10);
389
+ if (!Number.isFinite(requestedMajor) || requestedMajor <= 0) {
390
+ return {
391
+ requested: requestedContract,
392
+ contractVersion,
393
+ ok: false,
394
+ reason: "Invalid requested contract version",
395
+ };
396
+ }
397
+ const ok = requestedMajor === sdkMajor;
398
+ return ok
399
+ ? { requested: requestedContract, contractVersion, ok: true }
400
+ : {
401
+ requested: requestedContract,
402
+ contractVersion,
403
+ ok: false,
404
+ reason: `Requested major ${requestedMajor} does not match service major ${sdkMajor}`,
405
+ };
406
+ })();
309
407
  return c.json({
310
408
  ...metadata,
311
409
  supportsUserActions,
410
+ dainSdkVersion: package_json_1.default.version,
411
+ contractVersion,
412
+ capabilities,
413
+ ...(compatibility ? { compatibility } : {}),
312
414
  });
313
415
  });
314
416
  // Tools list endpoint
@@ -327,6 +429,10 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
327
429
  outputSchema,
328
430
  interface: tool.interface,
329
431
  suggestConfirmation: tool.suggestConfirmation,
432
+ sideEffectClass: tool.sideEffectClass,
433
+ supportsParallel: tool.supportsParallel,
434
+ idempotencyScope: tool.idempotencyScope,
435
+ maxConcurrencyHint: tool.maxConcurrencyHint,
330
436
  supportsUserActions: tool.supportsUserActions,
331
437
  };
332
438
  });
@@ -427,14 +533,26 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
427
533
  oauth2Client,
428
534
  app
429
535
  });
430
- return {
536
+ const response = {
431
537
  id: widget.id,
432
538
  name: widget.name,
433
539
  description: widget.description,
434
540
  icon: widget.icon,
435
541
  size: widget.size || "sm",
542
+ refreshIntervalMs: widget.refreshIntervalMs,
436
543
  ...widgetData
437
544
  };
545
+ if (!("freshness" in response)) {
546
+ response.freshness = {
547
+ freshAt: Date.now(),
548
+ transport: "poll",
549
+ requestedPolicy: {
550
+ refreshIntervalMs: widget.refreshIntervalMs,
551
+ },
552
+ scope: "account",
553
+ };
554
+ }
555
+ return response;
438
556
  }));
439
557
  const validWidgets = widgetsFull.filter(w => w !== null);
440
558
  const processedResponse = await processPluginsForResponse(validWidgets, body, { extraData: { plugins: processedPluginData.plugins } });
@@ -475,8 +593,19 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
475
593
  description: widget.description,
476
594
  icon: widget.icon,
477
595
  size: widget.size || "sm",
596
+ refreshIntervalMs: widget.refreshIntervalMs,
478
597
  ...widgetData
479
598
  };
599
+ if (!("freshness" in response)) {
600
+ response.freshness = {
601
+ freshAt: Date.now(),
602
+ transport: "poll",
603
+ requestedPolicy: {
604
+ refreshIntervalMs: widget.refreshIntervalMs,
605
+ },
606
+ scope: "account",
607
+ };
608
+ }
480
609
  const processedResponse = await processPluginsForResponse(response, body, { extraData: { plugins: processedPluginData.plugins } });
481
610
  return c.json(processedResponse);
482
611
  });
@@ -506,6 +635,12 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
506
635
  name: datasource.name,
507
636
  description: datasource.description,
508
637
  type: datasource.type,
638
+ refreshIntervalMs: datasource.refreshIntervalMs,
639
+ maxStalenessMs: datasource.maxStalenessMs,
640
+ transport: datasource.transport,
641
+ priority: datasource.priority,
642
+ scope: datasource.scope,
643
+ dataClass: datasource.dataClass,
509
644
  inputSchema: (0, schemaStructure_1.getDetailedSchemaStructure)(datasource.input),
510
645
  };
511
646
  }
@@ -543,12 +678,32 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
543
678
  plugins: pluginsData,
544
679
  oauth2Client
545
680
  });
681
+ const requestedPolicy = {
682
+ refreshIntervalMs: datasource.refreshIntervalMs,
683
+ maxStalenessMs: datasource.maxStalenessMs,
684
+ transport: datasource.transport,
685
+ priority: datasource.priority,
686
+ scope: datasource.scope,
687
+ dataClass: datasource.dataClass,
688
+ };
546
689
  const response = {
547
690
  id: datasource.id,
548
691
  name: datasource.name,
549
692
  description: datasource.description,
550
693
  type: datasource.type,
694
+ refreshIntervalMs: datasource.refreshIntervalMs,
695
+ maxStalenessMs: datasource.maxStalenessMs,
696
+ transport: datasource.transport,
697
+ priority: datasource.priority,
698
+ scope: datasource.scope,
699
+ dataClass: datasource.dataClass,
551
700
  data,
701
+ freshness: {
702
+ freshAt: Date.now(),
703
+ transport: datasource.transport || "poll",
704
+ requestedPolicy,
705
+ scope: datasource.scope,
706
+ },
552
707
  };
553
708
  const processedResponse = await processPluginsForResponse(response, { plugins: pluginsData }, { extraData: { plugins: pluginsData } });
554
709
  return c.json(processedResponse);
@@ -566,6 +721,149 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
566
721
  throw error;
567
722
  }
568
723
  });
724
+ // Stream datasource updates over SSE. This is primarily used for "fresh" UIs
725
+ // (positions/orders) where clients want stream-first updates with a poll fallback.
726
+ //
727
+ // Note: This does not require services to implement a separate streaming backend.
728
+ // The server re-runs the datasource handler on an interval and streams results.
729
+ app.post("/datasources/:datasourceId/stream", async (c) => {
730
+ const datasource = datasources.find((ds) => ds.id === c.req.param("datasourceId"));
731
+ if (!datasource) {
732
+ throw new http_exception_1.HTTPException(404, { message: "Datasource not found" });
733
+ }
734
+ const agentInfo = await getAgentInfo(c);
735
+ const rawBody = await safeParseBody(c);
736
+ const hasWrappedParams = rawBody &&
737
+ typeof rawBody === "object" &&
738
+ !Array.isArray(rawBody) &&
739
+ "params" in rawBody;
740
+ const requestedIntervalMsRaw = hasWrappedParams ? rawBody.intervalMs : undefined;
741
+ const requestParamsRaw = hasWrappedParams ? rawBody.params : rawBody;
742
+ let params = await processPluginsForRequest(requestParamsRaw && typeof requestParamsRaw === "object" ? requestParamsRaw : {}, agentInfo);
743
+ const pluginsData = (params.plugins && typeof params.plugins === "object"
744
+ ? params.plugins
745
+ : {});
746
+ delete params.plugins;
747
+ let parsedParams;
748
+ try {
749
+ parsedParams = datasource.input.parse(params);
750
+ }
751
+ catch (error) {
752
+ if (error instanceof zod_1.z.ZodError) {
753
+ const missingParams = error.issues
754
+ .map((issue) => issue.path.join("."))
755
+ .join(", ");
756
+ return c.json({
757
+ error: `Missing or invalid parameters: ${missingParams}`,
758
+ code: "INVALID_PARAMS"
759
+ }, 400);
760
+ }
761
+ throw error;
762
+ }
763
+ const pluginRequestContext = hasWrappedParams
764
+ ? {
765
+ params: parsedParams,
766
+ intervalMs: requestedIntervalMsRaw,
767
+ plugins: pluginsData,
768
+ }
769
+ : {
770
+ ...(typeof parsedParams === "object" && parsedParams !== null
771
+ ? parsedParams
772
+ : {}),
773
+ plugins: pluginsData,
774
+ };
775
+ const oauth2Client = app.oauth2?.getClient();
776
+ const requestedPolicy = {
777
+ refreshIntervalMs: datasource.refreshIntervalMs,
778
+ maxStalenessMs: datasource.maxStalenessMs,
779
+ transport: datasource.transport,
780
+ priority: datasource.priority,
781
+ scope: datasource.scope,
782
+ dataClass: datasource.dataClass,
783
+ };
784
+ const requestedIntervalMs = typeof requestedIntervalMsRaw === "number" && Number.isFinite(requestedIntervalMsRaw) && requestedIntervalMsRaw > 0
785
+ ? requestedIntervalMsRaw
786
+ : null;
787
+ const baseIntervalMs = requestedIntervalMs ??
788
+ (typeof datasource.refreshIntervalMs === "number" && datasource.refreshIntervalMs > 0
789
+ ? datasource.refreshIntervalMs
790
+ : 15_000);
791
+ const intervalMs = Math.max(1_000, Math.floor(baseIntervalMs));
792
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
793
+ debugLog(`[SSE] Datasource ${datasource.id} stream start (${intervalMs}ms interval)`);
794
+ return signedStreamSSE(c, privateKey, config, async (stream) => {
795
+ let eventId = 0;
796
+ const emitUpdate = async () => {
797
+ const data = await datasource.getDatasource(agentInfo, parsedParams, {
798
+ plugins: pluginsData,
799
+ oauth2Client,
800
+ });
801
+ const response = {
802
+ id: datasource.id,
803
+ name: datasource.name,
804
+ description: datasource.description,
805
+ type: datasource.type,
806
+ refreshIntervalMs: datasource.refreshIntervalMs,
807
+ maxStalenessMs: datasource.maxStalenessMs,
808
+ transport: datasource.transport,
809
+ priority: datasource.priority,
810
+ scope: datasource.scope,
811
+ dataClass: datasource.dataClass,
812
+ data,
813
+ freshness: {
814
+ freshAt: Date.now(),
815
+ transport: "stream",
816
+ requestedPolicy,
817
+ scope: datasource.scope,
818
+ },
819
+ };
820
+ const processedResponse = await processPluginsForResponse(response, pluginRequestContext, { extraData: { plugins: pluginsData } });
821
+ await stream.writeSSE({
822
+ event: "datasource-update",
823
+ data: JSON.stringify(processedResponse),
824
+ id: String(eventId++),
825
+ });
826
+ };
827
+ // Send initial snapshot immediately.
828
+ try {
829
+ await emitUpdate();
830
+ }
831
+ catch (error) {
832
+ console.error(`[SSE] Datasource ${datasource.id} initial update failed:`, error);
833
+ if (!stream.isAborted()) {
834
+ await stream.writeSSE({
835
+ event: "error",
836
+ data: JSON.stringify({ message: error?.message || "Datasource stream error" }),
837
+ });
838
+ }
839
+ return;
840
+ }
841
+ // Continue sending updates until client disconnects.
842
+ // Hono exposes the underlying Request via c.req.raw, which includes an AbortSignal.
843
+ const signal = c.req.raw?.signal;
844
+ while (!stream.isAborted() && !signal?.aborted) {
845
+ await sleep(intervalMs);
846
+ if (stream.isAborted() || signal?.aborted)
847
+ break;
848
+ try {
849
+ await emitUpdate();
850
+ }
851
+ catch (error) {
852
+ if (stream.isAborted() || signal?.aborted)
853
+ break;
854
+ console.error(`[SSE] Datasource ${datasource.id} update failed:`, error);
855
+ // Keep the stream alive; clients should rely on poll fallback if needed.
856
+ if (!stream.isAborted()) {
857
+ await stream.writeSSE({
858
+ event: "error",
859
+ data: JSON.stringify({ message: error?.message || "Datasource stream error" }),
860
+ });
861
+ }
862
+ }
863
+ }
864
+ debugLog(`[SSE] Datasource ${datasource.id} stream end`);
865
+ });
866
+ });
569
867
  function mapAgentInfo(agent) {
570
868
  return {
571
869
  id: agent.id,
@@ -642,13 +940,23 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
642
940
  outputSchema,
643
941
  interface: tool.interface,
644
942
  suggestConfirmation: tool.suggestConfirmation,
943
+ sideEffectClass: tool.sideEffectClass,
944
+ supportsParallel: tool.supportsParallel,
945
+ idempotencyScope: tool.idempotencyScope,
946
+ maxConcurrencyHint: tool.maxConcurrencyHint,
645
947
  supportsUserActions: tool.supportsUserActions,
646
948
  };
647
949
  }
648
950
  app.get("/getAllToolsAsJsonSchema", (c) => {
951
+ const toolInfo = tools.map(mapToolToJsonSchema);
952
+ const reccomendedPrompts = toolboxes.map((toolbox) => toolbox.recommendedPrompt);
649
953
  return c.json({
650
- tools: tools.map(mapToolToJsonSchema),
651
- reccomendedPrompts: toolboxes.map((toolbox) => toolbox.recommendedPrompt),
954
+ tools: toolInfo,
955
+ reccomendedPrompts,
956
+ schemaVersion: (0, toolDiscovery_1.computeToolDiscoverySchemaVersion)({
957
+ tools: toolInfo,
958
+ reccomendedPrompts,
959
+ }),
652
960
  });
653
961
  });
654
962
  app.post("/getAllToolsAsJsonSchema", async (c) => {
@@ -657,9 +965,14 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
657
965
  const processedPluginData = await processPluginsForRequest(body, agentInfo);
658
966
  const toolInfo = tools.map(mapToolToJsonSchema);
659
967
  debugLog(`[getAllToolsAsJsonSchema POST] Returning ${toolInfo.length} tools`);
968
+ const reccomendedPrompts = toolboxes.map((toolbox) => toolbox.recommendedPrompt);
660
969
  const response = {
661
970
  tools: toolInfo,
662
- reccomendedPrompts: toolboxes.map((toolbox) => toolbox.recommendedPrompt),
971
+ reccomendedPrompts,
972
+ schemaVersion: (0, toolDiscovery_1.computeToolDiscoverySchemaVersion)({
973
+ tools: toolInfo,
974
+ reccomendedPrompts,
975
+ }),
663
976
  };
664
977
  const processedResponse = await processPluginsForResponse(response, body, { extraData: { plugins: processedPluginData.plugins } });
665
978
  return c.json(processedResponse);
@@ -846,6 +1159,10 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
846
1159
  outputSchema: (0, schemaStructure_1.getDetailedSchemaStructure)(tool.output),
847
1160
  interface: tool.interface,
848
1161
  suggestConfirmation: tool.suggestConfirmation,
1162
+ sideEffectClass: tool.sideEffectClass,
1163
+ supportsParallel: tool.supportsParallel,
1164
+ idempotencyScope: tool.idempotencyScope,
1165
+ maxConcurrencyHint: tool.maxConcurrencyHint,
849
1166
  };
850
1167
  return c.json(toolDetails);
851
1168
  }
@@ -873,6 +1190,11 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
873
1190
  pricing: tool.pricing,
874
1191
  inputSchema: (0, schemaStructure_1.getDetailedSchemaStructure)(tool.input),
875
1192
  outputSchema: (0, schemaStructure_1.getDetailedSchemaStructure)(tool.output),
1193
+ suggestConfirmation: tool.suggestConfirmation,
1194
+ sideEffectClass: tool.sideEffectClass,
1195
+ supportsParallel: tool.supportsParallel,
1196
+ idempotencyScope: tool.idempotencyScope,
1197
+ maxConcurrencyHint: tool.maxConcurrencyHint,
876
1198
  }
877
1199
  : {
878
1200
  id: toolId,
@@ -898,7 +1220,13 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
898
1220
  }
899
1221
  });
900
1222
  // Health check endpoint
901
- app.get("/health", (c) => c.json({ status: "healthy", timestamp: new Date().toISOString() }));
1223
+ app.get("/health", async (c) => {
1224
+ const healthy = await resolveHealthStatus();
1225
+ return c.json({
1226
+ status: healthy ? "healthy" : "unhealthy",
1227
+ timestamp: new Date().toISOString(),
1228
+ }, healthy ? 200 : 503);
1229
+ });
902
1230
  // Setup custom routes if provided
903
1231
  if (config.routes) {
904
1232
  config.routes(app);
@@ -1182,64 +1510,231 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
1182
1510
  });
1183
1511
  // Toolboxes list endpoint
1184
1512
  app.get("/toolboxes", (c) => c.json(toolboxes));
1185
- // Recommendations endpoint - returns cards for AI selection
1186
- app.get("/recommendations", (c) => {
1187
- const cards = [];
1513
+ function normalizeRecommendationText(value) {
1514
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
1515
+ }
1516
+ function tokenizeRecommendationQuery(query) {
1517
+ const normalized = normalizeRecommendationText(query);
1518
+ if (!normalized)
1519
+ return [];
1520
+ return Array.from(new Set(normalized.split(/\s+/).filter((token) => token.length >= 2)));
1521
+ }
1522
+ function buildRecommendationIndex() {
1523
+ const index = new Map();
1188
1524
  for (const tool of tools) {
1189
- if (tool.recommendations?.length) {
1190
- for (const card of tool.recommendations) {
1191
- const hasDynamicSchema = !!card.inputSchema;
1192
- cards.push({
1193
- id: card.id,
1194
- tags: card.tags,
1195
- ui: hasDynamicSchema ? undefined : card.ui,
1196
- inputSchema: hasDynamicSchema ? (0, schemaStructure_1.zodToJsonSchema)(card.inputSchema) : undefined,
1197
- toolId: tool.id,
1198
- toolName: tool.name,
1199
- });
1525
+ if (!tool.recommendations?.length)
1526
+ continue;
1527
+ for (const card of tool.recommendations) {
1528
+ const scopedCardId = `${tool.id}:${card.id}`;
1529
+ if (index.has(scopedCardId)) {
1530
+ throw new Error(`Duplicate recommendation card ID "${scopedCardId}". Card IDs must be unique per tool.`);
1200
1531
  }
1532
+ const isDynamic = !!card.inputSchema;
1533
+ const inputSchema = isDynamic ? (0, schemaStructure_1.zodToJsonSchema)(card.inputSchema) : undefined;
1534
+ const searchText = normalizeRecommendationText([tool.id, tool.name, card.id, ...card.tags].join(" "));
1535
+ index.set(scopedCardId, {
1536
+ cardId: scopedCardId,
1537
+ localCardId: card.id,
1538
+ toolId: tool.id,
1539
+ toolName: tool.name,
1540
+ tags: card.tags,
1541
+ inputSchema,
1542
+ ui: isDynamic ? undefined : card.ui,
1543
+ isDynamic,
1544
+ card,
1545
+ searchText,
1546
+ });
1201
1547
  }
1202
1548
  }
1203
- return c.json({ cards, count: cards.length });
1204
- });
1205
- // Generate recommendation card UI
1206
- app.post("/recommendations/:id/generate", async (c) => {
1207
- const cardId = c.req.param("id");
1208
- const body = await c.req.json().catch(() => ({}));
1209
- const params = body.params ?? {};
1210
- const context = body.context ?? {};
1211
- let foundCard;
1212
- let foundTool;
1213
- for (const tool of tools) {
1214
- const card = tool.recommendations?.find((r) => r.id === cardId);
1215
- if (card) {
1216
- foundCard = card;
1217
- foundTool = tool;
1218
- break;
1549
+ return index;
1550
+ }
1551
+ function scoreRecommendationCard(card, tokens) {
1552
+ if (tokens.length === 0)
1553
+ return 0;
1554
+ let score = 0;
1555
+ const tagSet = new Set(card.tags.map((tag) => normalizeRecommendationText(tag)));
1556
+ const scopedId = normalizeRecommendationText(card.cardId);
1557
+ const toolName = normalizeRecommendationText(card.toolName);
1558
+ for (const token of tokens) {
1559
+ if (scopedId === token || card.localCardId.toLowerCase() === token) {
1560
+ score += 6;
1561
+ continue;
1562
+ }
1563
+ if (tagSet.has(token)) {
1564
+ score += 4;
1565
+ }
1566
+ if (toolName.includes(token)) {
1567
+ score += 3;
1568
+ }
1569
+ if (card.searchText.includes(token)) {
1570
+ score += 1;
1219
1571
  }
1220
1572
  }
1221
- if (!foundCard) {
1222
- return c.json({ error: `Card not found: ${cardId}` }, 404);
1223
- }
1224
- const baseResponse = { id: cardId, toolId: foundTool?.id, toolName: foundTool?.name };
1225
- if (!foundCard.inputSchema) {
1226
- return c.json({ ...baseResponse, ui: foundCard.ui });
1227
- }
1228
- const parseResult = foundCard.inputSchema.safeParse(params);
1229
- if (!parseResult.success) {
1230
- return c.json({ error: "Invalid parameters", details: parseResult.error.format() }, 400);
1231
- }
1232
- if (typeof foundCard.ui !== "function") {
1233
- return c.json({ error: `Card ${cardId} has inputSchema but ui is not a function` }, 500);
1234
- }
1235
- try {
1236
- const ui = await foundCard.ui(parseResult.data, context);
1237
- return c.json({ ...baseResponse, ui, params: parseResult.data });
1238
- }
1239
- catch (e) {
1240
- const message = e instanceof Error ? e.message : "Unknown error";
1241
- return c.json({ error: `Failed to generate UI: ${message}` }, 500);
1573
+ return score;
1574
+ }
1575
+ const recommendationCardsById = buildRecommendationIndex();
1576
+ const recommendationSearchSchema = zod_1.z.object({
1577
+ query: zod_1.z.string().optional(),
1578
+ limit: zod_1.z.number().int().min(1).max(50).optional().default(12),
1579
+ toolIds: zod_1.z.array(zod_1.z.string()).optional(),
1580
+ includeStaticUI: zod_1.z.boolean().optional().default(false),
1581
+ });
1582
+ app.post("/recommendations/search", async (c) => {
1583
+ const agentInfo = await getAgentInfo(c);
1584
+ const body = await safeParseBody(c);
1585
+ const processedPluginData = await processPluginsForRequest(body, agentInfo);
1586
+ const requestData = { ...processedPluginData };
1587
+ delete requestData.plugins;
1588
+ const parsed = recommendationSearchSchema.safeParse(requestData);
1589
+ if (!parsed.success) {
1590
+ return c.json({
1591
+ error: "Invalid recommendation search request",
1592
+ details: parsed.error.format(),
1593
+ }, 400);
1594
+ }
1595
+ const { query, limit, toolIds, includeStaticUI, } = parsed.data;
1596
+ const toolIdSet = toolIds?.length ? new Set(toolIds) : null;
1597
+ const filteredCards = Array.from(recommendationCardsById.values()).filter((card) => !toolIdSet || toolIdSet.has(card.toolId));
1598
+ const tokens = tokenizeRecommendationQuery(query ?? "");
1599
+ const ranked = filteredCards
1600
+ .map((card, index) => ({
1601
+ ...card,
1602
+ score: scoreRecommendationCard(card, tokens),
1603
+ index,
1604
+ }))
1605
+ .sort((a, b) => {
1606
+ if (tokens.length === 0)
1607
+ return a.index - b.index;
1608
+ if (b.score !== a.score)
1609
+ return b.score - a.score;
1610
+ if (a.toolName !== b.toolName)
1611
+ return a.toolName.localeCompare(b.toolName);
1612
+ return a.localCardId.localeCompare(b.localCardId);
1613
+ })
1614
+ .slice(0, limit);
1615
+ const cards = ranked.map((card) => ({
1616
+ cardId: card.cardId,
1617
+ localCardId: card.localCardId,
1618
+ toolId: card.toolId,
1619
+ toolName: card.toolName,
1620
+ tags: card.tags,
1621
+ score: card.score,
1622
+ inputSchema: card.inputSchema,
1623
+ ui: includeStaticUI && !card.isDynamic ? card.ui : undefined,
1624
+ isDynamic: card.isDynamic,
1625
+ }));
1626
+ const response = {
1627
+ cards,
1628
+ count: cards.length,
1629
+ query,
1630
+ };
1631
+ const processedResponse = await processPluginsForResponse(response, body, { extraData: { plugins: processedPluginData.plugins } });
1632
+ return c.json(processedResponse);
1633
+ });
1634
+ const recommendationRenderSchema = zod_1.z.object({
1635
+ cards: zod_1.z
1636
+ .array(zod_1.z.object({
1637
+ cardId: zod_1.z.string().min(1),
1638
+ params: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional(),
1639
+ context: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional(),
1640
+ }))
1641
+ .min(1)
1642
+ .max(8),
1643
+ continueOnError: zod_1.z.boolean().optional().default(true),
1644
+ });
1645
+ app.post("/recommendations/render", async (c) => {
1646
+ const agentInfo = await getAgentInfo(c);
1647
+ const body = await safeParseBody(c);
1648
+ const processedPluginData = await processPluginsForRequest(body, agentInfo);
1649
+ const requestData = { ...processedPluginData };
1650
+ delete requestData.plugins;
1651
+ const parsed = recommendationRenderSchema.safeParse(requestData);
1652
+ if (!parsed.success) {
1653
+ return c.json({
1654
+ error: "Invalid recommendation render request",
1655
+ details: parsed.error.format(),
1656
+ }, 400);
1657
+ }
1658
+ const { cards: requestedCards, continueOnError } = parsed.data;
1659
+ const renderedCards = [];
1660
+ const renderErrors = [];
1661
+ for (const requested of requestedCards) {
1662
+ const card = recommendationCardsById.get(requested.cardId);
1663
+ if (!card) {
1664
+ renderErrors.push({
1665
+ cardId: requested.cardId,
1666
+ code: "not_found",
1667
+ message: `Card not found: ${requested.cardId}`,
1668
+ });
1669
+ if (!continueOnError)
1670
+ break;
1671
+ continue;
1672
+ }
1673
+ if (!card.isDynamic) {
1674
+ renderedCards.push({
1675
+ cardId: card.cardId,
1676
+ localCardId: card.localCardId,
1677
+ toolId: card.toolId,
1678
+ toolName: card.toolName,
1679
+ tags: card.tags,
1680
+ ui: card.card.ui,
1681
+ });
1682
+ continue;
1683
+ }
1684
+ const paramsInput = requested.params ?? {};
1685
+ const parseResult = card.card.inputSchema.safeParse(paramsInput);
1686
+ if (!parseResult.success) {
1687
+ renderErrors.push({
1688
+ cardId: card.cardId,
1689
+ code: "invalid_params",
1690
+ message: "Invalid parameters",
1691
+ details: parseResult.error.format(),
1692
+ });
1693
+ if (!continueOnError)
1694
+ break;
1695
+ continue;
1696
+ }
1697
+ if (typeof card.card.ui !== "function") {
1698
+ renderErrors.push({
1699
+ cardId: card.cardId,
1700
+ code: "render_failed",
1701
+ message: `Card "${card.cardId}" is dynamic but has no UI generator function`,
1702
+ });
1703
+ if (!continueOnError)
1704
+ break;
1705
+ continue;
1706
+ }
1707
+ try {
1708
+ const ui = await card.card.ui(parseResult.data, requested.context ?? {});
1709
+ renderedCards.push({
1710
+ cardId: card.cardId,
1711
+ localCardId: card.localCardId,
1712
+ toolId: card.toolId,
1713
+ toolName: card.toolName,
1714
+ tags: card.tags,
1715
+ ui,
1716
+ params: parseResult.data,
1717
+ });
1718
+ }
1719
+ catch (error) {
1720
+ renderErrors.push({
1721
+ cardId: card.cardId,
1722
+ code: "render_failed",
1723
+ message: error instanceof Error ? error.message : "Failed to render recommendation card",
1724
+ });
1725
+ if (!continueOnError)
1726
+ break;
1727
+ }
1242
1728
  }
1729
+ const response = {
1730
+ cards: renderedCards,
1731
+ errors: renderErrors,
1732
+ renderedCount: renderedCards.length,
1733
+ requestedCount: requestedCards.length,
1734
+ };
1735
+ const processedResponse = await processPluginsForResponse(response, body, { extraData: { plugins: processedPluginData.plugins } });
1736
+ const statusCode = !continueOnError && renderErrors.length > 0 ? 400 : 200;
1737
+ return c.json(processedResponse, statusCode);
1243
1738
  });
1244
1739
  // Process request with plugins
1245
1740
  async function processPluginsForRequest(request, agentInfo) {
@@ -1420,6 +1915,7 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
1420
1915
  }
1421
1916
  catch (error) {
1422
1917
  console.error(`Error in stream for ${tool.id}:`, error);
1918
+ const safeMsg = sanitizeErrorMessage(error);
1423
1919
  // Send an error result rather than letting the stream end without a response
1424
1920
  try {
1425
1921
  if (withContext) {
@@ -1427,8 +1923,8 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
1427
1923
  event: 'result',
1428
1924
  data: JSON.stringify({
1429
1925
  toolResult: {
1430
- error: error.message || "Unknown error",
1431
- text: `Error: ${error.message || "Unknown error"}`,
1926
+ error: safeMsg,
1927
+ text: `Error: ${safeMsg}`,
1432
1928
  data: null,
1433
1929
  ui: null
1434
1930
  },
@@ -1441,8 +1937,8 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
1441
1937
  await stream.writeSSE({
1442
1938
  event: 'result',
1443
1939
  data: JSON.stringify({
1444
- error: error.message || "Unknown error",
1445
- text: `Error: ${error.message || "Unknown error"}`,
1940
+ error: safeMsg,
1941
+ text: `Error: ${safeMsg}`,
1446
1942
  data: null,
1447
1943
  ui: null
1448
1944
  }),
@@ -1534,13 +2030,14 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
1534
2030
  }
1535
2031
  catch (error) {
1536
2032
  console.error(`Error executing tool ${tool.id} (non-streaming):`, error);
2033
+ const safeMsg = sanitizeErrorMessage(error);
1537
2034
  // Return formatted error response to match streaming behavior
1538
2035
  let errorResponse;
1539
2036
  if (withContext) {
1540
2037
  errorResponse = {
1541
2038
  toolResult: {
1542
- error: error.message || "Unknown error",
1543
- text: `Error: ${error.message || "Unknown error"}`,
2039
+ error: safeMsg,
2040
+ text: `Error: ${safeMsg}`,
1544
2041
  data: null,
1545
2042
  ui: null
1546
2043
  },
@@ -1549,8 +2046,8 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
1549
2046
  }
1550
2047
  else {
1551
2048
  errorResponse = {
1552
- error: error.message || "Unknown error",
1553
- text: `Error: ${error.message || "Unknown error"}`,
2049
+ error: safeMsg,
2050
+ text: `Error: ${safeMsg}`,
1554
2051
  data: null,
1555
2052
  ui: null
1556
2053
  };