@dainprotocol/service-sdk 2.0.87 → 2.0.89

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,6 +9,7 @@ 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");
@@ -32,6 +33,21 @@ function debugWarn(...args) {
32
33
  console.warn(...args);
33
34
  }
34
35
  }
36
+ function getFirstForwardedValue(value) {
37
+ if (!value)
38
+ return undefined;
39
+ const first = value.split(",")[0]?.trim();
40
+ return first || undefined;
41
+ }
42
+ function normalizeForwardedPrefix(prefix) {
43
+ if (!prefix)
44
+ return undefined;
45
+ const trimmed = prefix.trim();
46
+ if (!trimmed || trimmed === "/")
47
+ return undefined;
48
+ const normalized = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
49
+ return normalized.replace(/\/+$/, "");
50
+ }
35
51
  /**
36
52
  * Safely parse JSON body from request, returning empty object on failure.
37
53
  */
@@ -63,23 +79,30 @@ function requireScope(requiredScope) {
63
79
  // Helper to sign and stream SSE events
64
80
  function signedStreamSSE(c, privateKey, config, handler) {
65
81
  return (0, streaming_1.streamSSE)(c, async (stream) => {
82
+ const requestSignal = c.req.raw?.signal;
83
+ const isAborted = () => stream.aborted || stream.closed || requestSignal?.aborted === true;
66
84
  const signedStream = {
67
85
  writeSSE: async (event) => {
86
+ if (isAborted())
87
+ return;
68
88
  const timestamp = Date.now().toString();
69
89
  const message = `${event.data}:${timestamp}`;
70
90
  const messageHash = (0, sha256_1.sha256)(message);
71
91
  const signatureBytes = ed25519_1.ed25519.sign(messageHash, privateKey);
72
92
  const signature = (0, utils_1.bytesToHex)(signatureBytes);
73
93
  // Fast path for non-critical events (progress/UI updates)
74
- const isCriticalEvent = event.event === 'result' || event.event === 'process-created';
94
+ const isCriticalEvent = event.event === 'result' ||
95
+ event.event === 'process-created' ||
96
+ event.event === 'datasource-update';
75
97
  const dataWithSignature = isCriticalEvent
76
98
  ? JSON.stringify({
77
99
  data: JSON.parse(event.data),
78
100
  _signature: { signature, timestamp, agentId: config.identity.agentId, orgId: config.identity.orgId, address: config.identity.publicKey }
79
101
  })
80
102
  : `{"data":${event.data},"_signature":{"signature":"${signature}","timestamp":"${timestamp}","agentId":"${config.identity.agentId}","orgId":"${config.identity.orgId}","address":"${config.identity.publicKey}"}}`;
81
- return stream.writeSSE({ event: event.event, data: dataWithSignature, id: event.id });
82
- }
103
+ await stream.writeSSE({ event: event.event, data: dataWithSignature, id: event.id });
104
+ },
105
+ isAborted,
83
106
  };
84
107
  await handler(signedStream);
85
108
  });
@@ -159,11 +182,7 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
159
182
  const isHITLPath = hitlPath && c.req.path === hitlPath;
160
183
  if (c.req.path.startsWith("/oauth2/callback/") ||
161
184
  c.req.path.startsWith("/addons") ||
162
- c.req.path.startsWith("/getAllToolsAsJsonSchema") ||
163
- c.req.path.startsWith("/getSkills") ||
164
- c.req.path.startsWith("/getWebhookTriggers") ||
165
185
  c.req.path.startsWith("/metadata") ||
166
- c.req.path.startsWith("/recommendations") ||
167
186
  c.req.path.startsWith("/ping") ||
168
187
  isWebhookPath ||
169
188
  isHITLPath) {
@@ -177,8 +196,9 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
177
196
  debugLog(`[Auth Middleware] JWT present: ${!!jwtToken}, API Key present: ${!!apiKey}`);
178
197
  if (jwtToken) {
179
198
  // JWT Authentication (Users)
180
- // Auto-detect JWT audience from Host header (works with localhost, tunnels, production)
181
- const host = c.req.header("x-forwarded-host") || c.req.header("host");
199
+ // Auto-detect JWT audience from forwarded headers (works with localhost, path-based tunnels, production)
200
+ const host = getFirstForwardedValue(c.req.header("x-forwarded-host")) ||
201
+ getFirstForwardedValue(c.req.header("host"));
182
202
  if (!host) {
183
203
  throw new http_exception_1.HTTPException(400, { message: "Unable to determine service URL. Host header is required." });
184
204
  }
@@ -186,22 +206,33 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
186
206
  if (host.includes(' ') || host.includes('\n') || host.includes('\r')) {
187
207
  throw new http_exception_1.HTTPException(400, { message: "Invalid Host header format" });
188
208
  }
189
- const xForwardedProto = c.req.header("x-forwarded-proto");
190
- const protocol = xForwardedProto || (host.includes("localhost") ? "http" : "https");
191
- const jwtAudience = `${protocol}://${host}`;
209
+ const xForwardedProto = getFirstForwardedValue(c.req.header("x-forwarded-proto"));
210
+ const protocol = xForwardedProto === "http" || xForwardedProto === "https"
211
+ ? xForwardedProto
212
+ : (host.includes("localhost") || host.startsWith("127.") ? "http" : "https");
213
+ const forwardedPrefix = normalizeForwardedPrefix(getFirstForwardedValue(c.req.header("x-forwarded-prefix")));
214
+ const jwtAudiences = [`${protocol}://${host}`];
215
+ if (forwardedPrefix) {
216
+ jwtAudiences.push(`${protocol}://${host}${forwardedPrefix}`);
217
+ }
192
218
  const publicKeyOrUrl = config.jwtPublicKey || config.dainIdUrl || "https://id.dain.org";
193
219
  const issuer = config.jwtIssuer || "dainid-oauth";
194
220
  const result = await (0, auth_1.verifyJWT)(jwtToken, publicKeyOrUrl, {
195
221
  issuer: issuer,
196
- audience: jwtAudience,
222
+ audience: jwtAudiences,
197
223
  });
198
224
  if (!result.valid) {
199
225
  throw new http_exception_1.HTTPException(401, { message: `JWT verification failed: ${result.error}` });
200
226
  }
201
227
  // Defense in depth: double-check audience claim
202
- if (result.payload.aud !== jwtAudience) {
228
+ const tokenAudiences = Array.isArray(result.payload?.aud)
229
+ ? result.payload.aud
230
+ : [result.payload?.aud];
231
+ const expectedAudienceSet = new Set(jwtAudiences);
232
+ const audienceMatched = tokenAudiences.some((aud) => aud && expectedAudienceSet.has(aud));
233
+ if (!audienceMatched) {
203
234
  throw new http_exception_1.HTTPException(403, {
204
- message: `JWT audience mismatch. Expected: ${jwtAudience}, Got: ${result.payload.aud}`
235
+ message: `JWT audience mismatch. Expected one of: ${jwtAudiences.join(", ")}, Got: ${tokenAudiences.filter(Boolean).join(", ")}`
205
236
  });
206
237
  }
207
238
  c.set('authMethod', 'jwt');
@@ -273,15 +304,73 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
273
304
  debugLog("[getAgentInfo] API Key - Returning agent info:", agentInfo);
274
305
  return agentInfo;
275
306
  }
307
+ async function resolveHealthStatus() {
308
+ if (!config.healthCheck)
309
+ return true;
310
+ try {
311
+ return await config.healthCheck();
312
+ }
313
+ catch {
314
+ return false;
315
+ }
316
+ }
276
317
  // Setup default ping route
277
- app.get("/ping", (c) => c.json({ message: "pong", platform: "DAIN", service_version: metadata.version, dain_sdk_version: package_json_1.default.version }));
318
+ app.get("/ping", async (c) => {
319
+ const healthy = await resolveHealthStatus();
320
+ return c.json({
321
+ message: healthy ? "pong" : "unhealthy",
322
+ platform: "DAIN",
323
+ service_version: metadata.version,
324
+ dain_sdk_version: package_json_1.default.version,
325
+ }, healthy ? 200 : 503);
326
+ });
278
327
  // Metadata endpoint - includes computed supportsUserActions from tools
279
328
  app.get("/metadata", (c) => {
280
329
  // Compute service-level capability: does ANY tool support user actions (HITL)?
281
330
  const supportsUserActions = tools.some((tool) => tool.supportsUserActions === true);
331
+ const requestedContract = c.req.header("x-butterfly-contract") || c.req.header("X-Butterfly-Contract");
332
+ const sdkMajor = Number.parseInt(String(package_json_1.default.version).split(".")[0] || "0", 10);
333
+ const contractVersion = Number.isFinite(sdkMajor) && sdkMajor > 0 ? `${sdkMajor}.0.0` : "0.0.0";
334
+ const capabilities = {
335
+ tools: true,
336
+ contexts: true,
337
+ widgets: true,
338
+ datasources: true,
339
+ streamingTools: true,
340
+ streamingDatasources: true,
341
+ datasourcePolicy: true,
342
+ widgetPolicy: true,
343
+ toolSafety: true,
344
+ };
345
+ const compatibility = (() => {
346
+ if (!requestedContract)
347
+ return undefined;
348
+ const requestedMajor = Number.parseInt(String(requestedContract).split(".")[0] || "0", 10);
349
+ if (!Number.isFinite(requestedMajor) || requestedMajor <= 0) {
350
+ return {
351
+ requested: requestedContract,
352
+ contractVersion,
353
+ ok: false,
354
+ reason: "Invalid requested contract version",
355
+ };
356
+ }
357
+ const ok = requestedMajor === sdkMajor;
358
+ return ok
359
+ ? { requested: requestedContract, contractVersion, ok: true }
360
+ : {
361
+ requested: requestedContract,
362
+ contractVersion,
363
+ ok: false,
364
+ reason: `Requested major ${requestedMajor} does not match service major ${sdkMajor}`,
365
+ };
366
+ })();
282
367
  return c.json({
283
368
  ...metadata,
284
369
  supportsUserActions,
370
+ dainSdkVersion: package_json_1.default.version,
371
+ contractVersion,
372
+ capabilities,
373
+ ...(compatibility ? { compatibility } : {}),
285
374
  });
286
375
  });
287
376
  // Tools list endpoint
@@ -300,6 +389,10 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
300
389
  outputSchema,
301
390
  interface: tool.interface,
302
391
  suggestConfirmation: tool.suggestConfirmation,
392
+ sideEffectClass: tool.sideEffectClass,
393
+ supportsParallel: tool.supportsParallel,
394
+ idempotencyScope: tool.idempotencyScope,
395
+ maxConcurrencyHint: tool.maxConcurrencyHint,
303
396
  supportsUserActions: tool.supportsUserActions,
304
397
  };
305
398
  });
@@ -400,14 +493,26 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
400
493
  oauth2Client,
401
494
  app
402
495
  });
403
- return {
496
+ const response = {
404
497
  id: widget.id,
405
498
  name: widget.name,
406
499
  description: widget.description,
407
500
  icon: widget.icon,
408
501
  size: widget.size || "sm",
502
+ refreshIntervalMs: widget.refreshIntervalMs,
409
503
  ...widgetData
410
504
  };
505
+ if (!("freshness" in response)) {
506
+ response.freshness = {
507
+ freshAt: Date.now(),
508
+ transport: "poll",
509
+ requestedPolicy: {
510
+ refreshIntervalMs: widget.refreshIntervalMs,
511
+ },
512
+ scope: "account",
513
+ };
514
+ }
515
+ return response;
411
516
  }));
412
517
  const validWidgets = widgetsFull.filter(w => w !== null);
413
518
  const processedResponse = await processPluginsForResponse(validWidgets, body, { extraData: { plugins: processedPluginData.plugins } });
@@ -448,8 +553,19 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
448
553
  description: widget.description,
449
554
  icon: widget.icon,
450
555
  size: widget.size || "sm",
556
+ refreshIntervalMs: widget.refreshIntervalMs,
451
557
  ...widgetData
452
558
  };
559
+ if (!("freshness" in response)) {
560
+ response.freshness = {
561
+ freshAt: Date.now(),
562
+ transport: "poll",
563
+ requestedPolicy: {
564
+ refreshIntervalMs: widget.refreshIntervalMs,
565
+ },
566
+ scope: "account",
567
+ };
568
+ }
453
569
  const processedResponse = await processPluginsForResponse(response, body, { extraData: { plugins: processedPluginData.plugins } });
454
570
  return c.json(processedResponse);
455
571
  });
@@ -479,6 +595,12 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
479
595
  name: datasource.name,
480
596
  description: datasource.description,
481
597
  type: datasource.type,
598
+ refreshIntervalMs: datasource.refreshIntervalMs,
599
+ maxStalenessMs: datasource.maxStalenessMs,
600
+ transport: datasource.transport,
601
+ priority: datasource.priority,
602
+ scope: datasource.scope,
603
+ dataClass: datasource.dataClass,
482
604
  inputSchema: (0, schemaStructure_1.getDetailedSchemaStructure)(datasource.input),
483
605
  };
484
606
  }
@@ -516,12 +638,32 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
516
638
  plugins: pluginsData,
517
639
  oauth2Client
518
640
  });
641
+ const requestedPolicy = {
642
+ refreshIntervalMs: datasource.refreshIntervalMs,
643
+ maxStalenessMs: datasource.maxStalenessMs,
644
+ transport: datasource.transport,
645
+ priority: datasource.priority,
646
+ scope: datasource.scope,
647
+ dataClass: datasource.dataClass,
648
+ };
519
649
  const response = {
520
650
  id: datasource.id,
521
651
  name: datasource.name,
522
652
  description: datasource.description,
523
653
  type: datasource.type,
654
+ refreshIntervalMs: datasource.refreshIntervalMs,
655
+ maxStalenessMs: datasource.maxStalenessMs,
656
+ transport: datasource.transport,
657
+ priority: datasource.priority,
658
+ scope: datasource.scope,
659
+ dataClass: datasource.dataClass,
524
660
  data,
661
+ freshness: {
662
+ freshAt: Date.now(),
663
+ transport: datasource.transport || "poll",
664
+ requestedPolicy,
665
+ scope: datasource.scope,
666
+ },
525
667
  };
526
668
  const processedResponse = await processPluginsForResponse(response, { plugins: pluginsData }, { extraData: { plugins: pluginsData } });
527
669
  return c.json(processedResponse);
@@ -539,6 +681,149 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
539
681
  throw error;
540
682
  }
541
683
  });
684
+ // Stream datasource updates over SSE. This is primarily used for "fresh" UIs
685
+ // (positions/orders) where clients want stream-first updates with a poll fallback.
686
+ //
687
+ // Note: This does not require services to implement a separate streaming backend.
688
+ // The server re-runs the datasource handler on an interval and streams results.
689
+ app.post("/datasources/:datasourceId/stream", async (c) => {
690
+ const datasource = datasources.find((ds) => ds.id === c.req.param("datasourceId"));
691
+ if (!datasource) {
692
+ throw new http_exception_1.HTTPException(404, { message: "Datasource not found" });
693
+ }
694
+ const agentInfo = await getAgentInfo(c);
695
+ const rawBody = await safeParseBody(c);
696
+ const hasWrappedParams = rawBody &&
697
+ typeof rawBody === "object" &&
698
+ !Array.isArray(rawBody) &&
699
+ "params" in rawBody;
700
+ const requestedIntervalMsRaw = hasWrappedParams ? rawBody.intervalMs : undefined;
701
+ const requestParamsRaw = hasWrappedParams ? rawBody.params : rawBody;
702
+ let params = await processPluginsForRequest(requestParamsRaw && typeof requestParamsRaw === "object" ? requestParamsRaw : {}, agentInfo);
703
+ const pluginsData = (params.plugins && typeof params.plugins === "object"
704
+ ? params.plugins
705
+ : {});
706
+ delete params.plugins;
707
+ let parsedParams;
708
+ try {
709
+ parsedParams = datasource.input.parse(params);
710
+ }
711
+ catch (error) {
712
+ if (error instanceof zod_1.z.ZodError) {
713
+ const missingParams = error.issues
714
+ .map((issue) => issue.path.join("."))
715
+ .join(", ");
716
+ return c.json({
717
+ error: `Missing or invalid parameters: ${missingParams}`,
718
+ code: "INVALID_PARAMS"
719
+ }, 400);
720
+ }
721
+ throw error;
722
+ }
723
+ const pluginRequestContext = hasWrappedParams
724
+ ? {
725
+ params: parsedParams,
726
+ intervalMs: requestedIntervalMsRaw,
727
+ plugins: pluginsData,
728
+ }
729
+ : {
730
+ ...(typeof parsedParams === "object" && parsedParams !== null
731
+ ? parsedParams
732
+ : {}),
733
+ plugins: pluginsData,
734
+ };
735
+ const oauth2Client = app.oauth2?.getClient();
736
+ const requestedPolicy = {
737
+ refreshIntervalMs: datasource.refreshIntervalMs,
738
+ maxStalenessMs: datasource.maxStalenessMs,
739
+ transport: datasource.transport,
740
+ priority: datasource.priority,
741
+ scope: datasource.scope,
742
+ dataClass: datasource.dataClass,
743
+ };
744
+ const requestedIntervalMs = typeof requestedIntervalMsRaw === "number" && Number.isFinite(requestedIntervalMsRaw) && requestedIntervalMsRaw > 0
745
+ ? requestedIntervalMsRaw
746
+ : null;
747
+ const baseIntervalMs = requestedIntervalMs ??
748
+ (typeof datasource.refreshIntervalMs === "number" && datasource.refreshIntervalMs > 0
749
+ ? datasource.refreshIntervalMs
750
+ : 15_000);
751
+ const intervalMs = Math.max(1_000, Math.floor(baseIntervalMs));
752
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
753
+ debugLog(`[SSE] Datasource ${datasource.id} stream start (${intervalMs}ms interval)`);
754
+ return signedStreamSSE(c, privateKey, config, async (stream) => {
755
+ let eventId = 0;
756
+ const emitUpdate = async () => {
757
+ const data = await datasource.getDatasource(agentInfo, parsedParams, {
758
+ plugins: pluginsData,
759
+ oauth2Client,
760
+ });
761
+ const response = {
762
+ id: datasource.id,
763
+ name: datasource.name,
764
+ description: datasource.description,
765
+ type: datasource.type,
766
+ refreshIntervalMs: datasource.refreshIntervalMs,
767
+ maxStalenessMs: datasource.maxStalenessMs,
768
+ transport: datasource.transport,
769
+ priority: datasource.priority,
770
+ scope: datasource.scope,
771
+ dataClass: datasource.dataClass,
772
+ data,
773
+ freshness: {
774
+ freshAt: Date.now(),
775
+ transport: "stream",
776
+ requestedPolicy,
777
+ scope: datasource.scope,
778
+ },
779
+ };
780
+ const processedResponse = await processPluginsForResponse(response, pluginRequestContext, { extraData: { plugins: pluginsData } });
781
+ await stream.writeSSE({
782
+ event: "datasource-update",
783
+ data: JSON.stringify(processedResponse),
784
+ id: String(eventId++),
785
+ });
786
+ };
787
+ // Send initial snapshot immediately.
788
+ try {
789
+ await emitUpdate();
790
+ }
791
+ catch (error) {
792
+ console.error(`[SSE] Datasource ${datasource.id} initial update failed:`, error);
793
+ if (!stream.isAborted()) {
794
+ await stream.writeSSE({
795
+ event: "error",
796
+ data: JSON.stringify({ message: error?.message || "Datasource stream error" }),
797
+ });
798
+ }
799
+ return;
800
+ }
801
+ // Continue sending updates until client disconnects.
802
+ // Hono exposes the underlying Request via c.req.raw, which includes an AbortSignal.
803
+ const signal = c.req.raw?.signal;
804
+ while (!stream.isAborted() && !signal?.aborted) {
805
+ await sleep(intervalMs);
806
+ if (stream.isAborted() || signal?.aborted)
807
+ break;
808
+ try {
809
+ await emitUpdate();
810
+ }
811
+ catch (error) {
812
+ if (stream.isAborted() || signal?.aborted)
813
+ break;
814
+ console.error(`[SSE] Datasource ${datasource.id} update failed:`, error);
815
+ // Keep the stream alive; clients should rely on poll fallback if needed.
816
+ if (!stream.isAborted()) {
817
+ await stream.writeSSE({
818
+ event: "error",
819
+ data: JSON.stringify({ message: error?.message || "Datasource stream error" }),
820
+ });
821
+ }
822
+ }
823
+ }
824
+ debugLog(`[SSE] Datasource ${datasource.id} stream end`);
825
+ });
826
+ });
542
827
  function mapAgentInfo(agent) {
543
828
  return {
544
829
  id: agent.id,
@@ -615,13 +900,23 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
615
900
  outputSchema,
616
901
  interface: tool.interface,
617
902
  suggestConfirmation: tool.suggestConfirmation,
903
+ sideEffectClass: tool.sideEffectClass,
904
+ supportsParallel: tool.supportsParallel,
905
+ idempotencyScope: tool.idempotencyScope,
906
+ maxConcurrencyHint: tool.maxConcurrencyHint,
618
907
  supportsUserActions: tool.supportsUserActions,
619
908
  };
620
909
  }
621
910
  app.get("/getAllToolsAsJsonSchema", (c) => {
911
+ const toolInfo = tools.map(mapToolToJsonSchema);
912
+ const reccomendedPrompts = toolboxes.map((toolbox) => toolbox.recommendedPrompt);
622
913
  return c.json({
623
- tools: tools.map(mapToolToJsonSchema),
624
- reccomendedPrompts: toolboxes.map((toolbox) => toolbox.recommendedPrompt),
914
+ tools: toolInfo,
915
+ reccomendedPrompts,
916
+ schemaVersion: (0, toolDiscovery_1.computeToolDiscoverySchemaVersion)({
917
+ tools: toolInfo,
918
+ reccomendedPrompts,
919
+ }),
625
920
  });
626
921
  });
627
922
  app.post("/getAllToolsAsJsonSchema", async (c) => {
@@ -630,9 +925,14 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
630
925
  const processedPluginData = await processPluginsForRequest(body, agentInfo);
631
926
  const toolInfo = tools.map(mapToolToJsonSchema);
632
927
  debugLog(`[getAllToolsAsJsonSchema POST] Returning ${toolInfo.length} tools`);
928
+ const reccomendedPrompts = toolboxes.map((toolbox) => toolbox.recommendedPrompt);
633
929
  const response = {
634
930
  tools: toolInfo,
635
- reccomendedPrompts: toolboxes.map((toolbox) => toolbox.recommendedPrompt),
931
+ reccomendedPrompts,
932
+ schemaVersion: (0, toolDiscovery_1.computeToolDiscoverySchemaVersion)({
933
+ tools: toolInfo,
934
+ reccomendedPrompts,
935
+ }),
636
936
  };
637
937
  const processedResponse = await processPluginsForResponse(response, body, { extraData: { plugins: processedPluginData.plugins } });
638
938
  return c.json(processedResponse);
@@ -819,6 +1119,10 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
819
1119
  outputSchema: (0, schemaStructure_1.getDetailedSchemaStructure)(tool.output),
820
1120
  interface: tool.interface,
821
1121
  suggestConfirmation: tool.suggestConfirmation,
1122
+ sideEffectClass: tool.sideEffectClass,
1123
+ supportsParallel: tool.supportsParallel,
1124
+ idempotencyScope: tool.idempotencyScope,
1125
+ maxConcurrencyHint: tool.maxConcurrencyHint,
822
1126
  };
823
1127
  return c.json(toolDetails);
824
1128
  }
@@ -846,6 +1150,11 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
846
1150
  pricing: tool.pricing,
847
1151
  inputSchema: (0, schemaStructure_1.getDetailedSchemaStructure)(tool.input),
848
1152
  outputSchema: (0, schemaStructure_1.getDetailedSchemaStructure)(tool.output),
1153
+ suggestConfirmation: tool.suggestConfirmation,
1154
+ sideEffectClass: tool.sideEffectClass,
1155
+ supportsParallel: tool.supportsParallel,
1156
+ idempotencyScope: tool.idempotencyScope,
1157
+ maxConcurrencyHint: tool.maxConcurrencyHint,
849
1158
  }
850
1159
  : {
851
1160
  id: toolId,
@@ -871,7 +1180,13 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
871
1180
  }
872
1181
  });
873
1182
  // Health check endpoint
874
- app.get("/health", (c) => c.json({ status: "healthy", timestamp: new Date().toISOString() }));
1183
+ app.get("/health", async (c) => {
1184
+ const healthy = await resolveHealthStatus();
1185
+ return c.json({
1186
+ status: healthy ? "healthy" : "unhealthy",
1187
+ timestamp: new Date().toISOString(),
1188
+ }, healthy ? 200 : 503);
1189
+ });
875
1190
  // Setup custom routes if provided
876
1191
  if (config.routes) {
877
1192
  config.routes(app);
@@ -924,31 +1239,11 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
924
1239
  }
925
1240
  try {
926
1241
  await app.oauth2.handleCallback(code, state);
927
- return c.html(`
928
- <html>
929
- <body>
930
- <script>
931
- window.opener.postMessage({ type: 'oauth2-success', provider: '${provider}' }, '*');
932
- window.close();
933
- </script>
934
- <h1>Authentication successful! You can close this window.</h1>
935
- </body>
936
- </html>
937
- `);
1242
+ return c.html("<html><body><script>window.opener.postMessage({ type: 'oauth2-success', provider: '" + provider + "' }, '*');window.close();</script><h1>Authentication successful! You can close this window.</h1></body></html>");
938
1243
  }
939
1244
  catch (error) {
940
1245
  console.error("OAuth callback error:", error);
941
- return c.html(`
942
- <html>
943
- <body>
944
- <script>
945
- window.opener.postMessage({ type: 'oauth2-error', provider: '${provider}', error: '${error.message}' }, '*');
946
- window.close();
947
- </script>
948
- <h1>Authentication failed! You can close this window.</h1>
949
- </body>
950
- </html>
951
- `);
1246
+ return c.html("<html><body><script>window.opener.postMessage({ type: 'oauth2-error', provider: '" + provider + "', error: '" + error.message + "' }, '*');window.close();</script><h1>Authentication failed! You can close this window.</h1></body></html>");
952
1247
  }
953
1248
  });
954
1249
  // Make oauth2Handler available to tools
@@ -1175,64 +1470,231 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
1175
1470
  });
1176
1471
  // Toolboxes list endpoint
1177
1472
  app.get("/toolboxes", (c) => c.json(toolboxes));
1178
- // Recommendations endpoint - returns cards for AI selection
1179
- app.get("/recommendations", (c) => {
1180
- const cards = [];
1473
+ function normalizeRecommendationText(value) {
1474
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
1475
+ }
1476
+ function tokenizeRecommendationQuery(query) {
1477
+ const normalized = normalizeRecommendationText(query);
1478
+ if (!normalized)
1479
+ return [];
1480
+ return Array.from(new Set(normalized.split(/\s+/).filter((token) => token.length >= 2)));
1481
+ }
1482
+ function buildRecommendationIndex() {
1483
+ const index = new Map();
1181
1484
  for (const tool of tools) {
1182
- if (tool.recommendations?.length) {
1183
- for (const card of tool.recommendations) {
1184
- const hasDynamicSchema = !!card.inputSchema;
1185
- cards.push({
1186
- id: card.id,
1187
- tags: card.tags,
1188
- ui: hasDynamicSchema ? undefined : card.ui,
1189
- inputSchema: hasDynamicSchema ? (0, schemaStructure_1.zodToJsonSchema)(card.inputSchema) : undefined,
1190
- toolId: tool.id,
1191
- toolName: tool.name,
1192
- });
1485
+ if (!tool.recommendations?.length)
1486
+ continue;
1487
+ for (const card of tool.recommendations) {
1488
+ const scopedCardId = `${tool.id}:${card.id}`;
1489
+ if (index.has(scopedCardId)) {
1490
+ throw new Error(`Duplicate recommendation card ID "${scopedCardId}". Card IDs must be unique per tool.`);
1193
1491
  }
1492
+ const isDynamic = !!card.inputSchema;
1493
+ const inputSchema = isDynamic ? (0, schemaStructure_1.zodToJsonSchema)(card.inputSchema) : undefined;
1494
+ const searchText = normalizeRecommendationText([tool.id, tool.name, card.id, ...card.tags].join(" "));
1495
+ index.set(scopedCardId, {
1496
+ cardId: scopedCardId,
1497
+ localCardId: card.id,
1498
+ toolId: tool.id,
1499
+ toolName: tool.name,
1500
+ tags: card.tags,
1501
+ inputSchema,
1502
+ ui: isDynamic ? undefined : card.ui,
1503
+ isDynamic,
1504
+ card,
1505
+ searchText,
1506
+ });
1194
1507
  }
1195
1508
  }
1196
- return c.json({ cards, count: cards.length });
1197
- });
1198
- // Generate recommendation card UI
1199
- app.post("/recommendations/:id/generate", async (c) => {
1200
- const cardId = c.req.param("id");
1201
- const body = await c.req.json().catch(() => ({}));
1202
- const params = body.params ?? {};
1203
- const context = body.context ?? {};
1204
- let foundCard;
1205
- let foundTool;
1206
- for (const tool of tools) {
1207
- const card = tool.recommendations?.find((r) => r.id === cardId);
1208
- if (card) {
1209
- foundCard = card;
1210
- foundTool = tool;
1211
- break;
1509
+ return index;
1510
+ }
1511
+ function scoreRecommendationCard(card, tokens) {
1512
+ if (tokens.length === 0)
1513
+ return 0;
1514
+ let score = 0;
1515
+ const tagSet = new Set(card.tags.map((tag) => normalizeRecommendationText(tag)));
1516
+ const scopedId = normalizeRecommendationText(card.cardId);
1517
+ const toolName = normalizeRecommendationText(card.toolName);
1518
+ for (const token of tokens) {
1519
+ if (scopedId === token || card.localCardId.toLowerCase() === token) {
1520
+ score += 6;
1521
+ continue;
1522
+ }
1523
+ if (tagSet.has(token)) {
1524
+ score += 4;
1525
+ }
1526
+ if (toolName.includes(token)) {
1527
+ score += 3;
1528
+ }
1529
+ if (card.searchText.includes(token)) {
1530
+ score += 1;
1212
1531
  }
1213
1532
  }
1214
- if (!foundCard) {
1215
- return c.json({ error: `Card not found: ${cardId}` }, 404);
1216
- }
1217
- const baseResponse = { id: cardId, toolId: foundTool?.id, toolName: foundTool?.name };
1218
- if (!foundCard.inputSchema) {
1219
- return c.json({ ...baseResponse, ui: foundCard.ui });
1220
- }
1221
- const parseResult = foundCard.inputSchema.safeParse(params);
1222
- if (!parseResult.success) {
1223
- return c.json({ error: "Invalid parameters", details: parseResult.error.format() }, 400);
1224
- }
1225
- if (typeof foundCard.ui !== "function") {
1226
- return c.json({ error: `Card ${cardId} has inputSchema but ui is not a function` }, 500);
1227
- }
1228
- try {
1229
- const ui = await foundCard.ui(parseResult.data, context);
1230
- return c.json({ ...baseResponse, ui, params: parseResult.data });
1231
- }
1232
- catch (e) {
1233
- const message = e instanceof Error ? e.message : "Unknown error";
1234
- return c.json({ error: `Failed to generate UI: ${message}` }, 500);
1533
+ return score;
1534
+ }
1535
+ const recommendationCardsById = buildRecommendationIndex();
1536
+ const recommendationSearchSchema = zod_1.z.object({
1537
+ query: zod_1.z.string().optional(),
1538
+ limit: zod_1.z.number().int().min(1).max(50).optional().default(12),
1539
+ toolIds: zod_1.z.array(zod_1.z.string()).optional(),
1540
+ includeStaticUI: zod_1.z.boolean().optional().default(false),
1541
+ });
1542
+ app.post("/recommendations/search", async (c) => {
1543
+ const agentInfo = await getAgentInfo(c);
1544
+ const body = await safeParseBody(c);
1545
+ const processedPluginData = await processPluginsForRequest(body, agentInfo);
1546
+ const requestData = { ...processedPluginData };
1547
+ delete requestData.plugins;
1548
+ const parsed = recommendationSearchSchema.safeParse(requestData);
1549
+ if (!parsed.success) {
1550
+ return c.json({
1551
+ error: "Invalid recommendation search request",
1552
+ details: parsed.error.format(),
1553
+ }, 400);
1554
+ }
1555
+ const { query, limit, toolIds, includeStaticUI, } = parsed.data;
1556
+ const toolIdSet = toolIds?.length ? new Set(toolIds) : null;
1557
+ const filteredCards = Array.from(recommendationCardsById.values()).filter((card) => !toolIdSet || toolIdSet.has(card.toolId));
1558
+ const tokens = tokenizeRecommendationQuery(query ?? "");
1559
+ const ranked = filteredCards
1560
+ .map((card, index) => ({
1561
+ ...card,
1562
+ score: scoreRecommendationCard(card, tokens),
1563
+ index,
1564
+ }))
1565
+ .sort((a, b) => {
1566
+ if (tokens.length === 0)
1567
+ return a.index - b.index;
1568
+ if (b.score !== a.score)
1569
+ return b.score - a.score;
1570
+ if (a.toolName !== b.toolName)
1571
+ return a.toolName.localeCompare(b.toolName);
1572
+ return a.localCardId.localeCompare(b.localCardId);
1573
+ })
1574
+ .slice(0, limit);
1575
+ const cards = ranked.map((card) => ({
1576
+ cardId: card.cardId,
1577
+ localCardId: card.localCardId,
1578
+ toolId: card.toolId,
1579
+ toolName: card.toolName,
1580
+ tags: card.tags,
1581
+ score: card.score,
1582
+ inputSchema: card.inputSchema,
1583
+ ui: includeStaticUI && !card.isDynamic ? card.ui : undefined,
1584
+ isDynamic: card.isDynamic,
1585
+ }));
1586
+ const response = {
1587
+ cards,
1588
+ count: cards.length,
1589
+ query,
1590
+ };
1591
+ const processedResponse = await processPluginsForResponse(response, body, { extraData: { plugins: processedPluginData.plugins } });
1592
+ return c.json(processedResponse);
1593
+ });
1594
+ const recommendationRenderSchema = zod_1.z.object({
1595
+ cards: zod_1.z
1596
+ .array(zod_1.z.object({
1597
+ cardId: zod_1.z.string().min(1),
1598
+ params: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional(),
1599
+ context: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional(),
1600
+ }))
1601
+ .min(1)
1602
+ .max(8),
1603
+ continueOnError: zod_1.z.boolean().optional().default(true),
1604
+ });
1605
+ app.post("/recommendations/render", async (c) => {
1606
+ const agentInfo = await getAgentInfo(c);
1607
+ const body = await safeParseBody(c);
1608
+ const processedPluginData = await processPluginsForRequest(body, agentInfo);
1609
+ const requestData = { ...processedPluginData };
1610
+ delete requestData.plugins;
1611
+ const parsed = recommendationRenderSchema.safeParse(requestData);
1612
+ if (!parsed.success) {
1613
+ return c.json({
1614
+ error: "Invalid recommendation render request",
1615
+ details: parsed.error.format(),
1616
+ }, 400);
1617
+ }
1618
+ const { cards: requestedCards, continueOnError } = parsed.data;
1619
+ const renderedCards = [];
1620
+ const renderErrors = [];
1621
+ for (const requested of requestedCards) {
1622
+ const card = recommendationCardsById.get(requested.cardId);
1623
+ if (!card) {
1624
+ renderErrors.push({
1625
+ cardId: requested.cardId,
1626
+ code: "not_found",
1627
+ message: `Card not found: ${requested.cardId}`,
1628
+ });
1629
+ if (!continueOnError)
1630
+ break;
1631
+ continue;
1632
+ }
1633
+ if (!card.isDynamic) {
1634
+ renderedCards.push({
1635
+ cardId: card.cardId,
1636
+ localCardId: card.localCardId,
1637
+ toolId: card.toolId,
1638
+ toolName: card.toolName,
1639
+ tags: card.tags,
1640
+ ui: card.card.ui,
1641
+ });
1642
+ continue;
1643
+ }
1644
+ const paramsInput = requested.params ?? {};
1645
+ const parseResult = card.card.inputSchema.safeParse(paramsInput);
1646
+ if (!parseResult.success) {
1647
+ renderErrors.push({
1648
+ cardId: card.cardId,
1649
+ code: "invalid_params",
1650
+ message: "Invalid parameters",
1651
+ details: parseResult.error.format(),
1652
+ });
1653
+ if (!continueOnError)
1654
+ break;
1655
+ continue;
1656
+ }
1657
+ if (typeof card.card.ui !== "function") {
1658
+ renderErrors.push({
1659
+ cardId: card.cardId,
1660
+ code: "render_failed",
1661
+ message: `Card "${card.cardId}" is dynamic but has no UI generator function`,
1662
+ });
1663
+ if (!continueOnError)
1664
+ break;
1665
+ continue;
1666
+ }
1667
+ try {
1668
+ const ui = await card.card.ui(parseResult.data, requested.context ?? {});
1669
+ renderedCards.push({
1670
+ cardId: card.cardId,
1671
+ localCardId: card.localCardId,
1672
+ toolId: card.toolId,
1673
+ toolName: card.toolName,
1674
+ tags: card.tags,
1675
+ ui,
1676
+ params: parseResult.data,
1677
+ });
1678
+ }
1679
+ catch (error) {
1680
+ renderErrors.push({
1681
+ cardId: card.cardId,
1682
+ code: "render_failed",
1683
+ message: error instanceof Error ? error.message : "Failed to render recommendation card",
1684
+ });
1685
+ if (!continueOnError)
1686
+ break;
1687
+ }
1235
1688
  }
1689
+ const response = {
1690
+ cards: renderedCards,
1691
+ errors: renderErrors,
1692
+ renderedCount: renderedCards.length,
1693
+ requestedCount: requestedCards.length,
1694
+ };
1695
+ const processedResponse = await processPluginsForResponse(response, body, { extraData: { plugins: processedPluginData.plugins } });
1696
+ const statusCode = !continueOnError && renderErrors.length > 0 ? 400 : 200;
1697
+ return c.json(processedResponse, statusCode);
1236
1698
  });
1237
1699
  // Process request with plugins
1238
1700
  async function processPluginsForRequest(request, agentInfo) {