@andreprado/agentkit 0.1.0 → 0.2.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.
Files changed (104) hide show
  1. package/README.md +8 -77
  2. package/docs/guides/add-channel.md +14 -92
  3. package/docs/guides/add-knowledge.md +0 -21
  4. package/docs/guides/add-tool.md +5 -11
  5. package/docs/guides/channel-security.md +3 -207
  6. package/docs/guides/connect-discord.md +7 -172
  7. package/docs/guides/connect-slack.md +6 -121
  8. package/docs/guides/connect-telegram.md +6 -165
  9. package/docs/guides/connect-whatsapp-evolution.md +6 -116
  10. package/docs/guides/connect-whatsapp-uazapi.md +6 -134
  11. package/docs/guides/connect-whatsapp-zapster.md +6 -202
  12. package/docs/guides/create-agent.md +5 -14
  13. package/docs/guides/debug-channel.md +4 -156
  14. package/docs/guides/improve-local.md +13 -0
  15. package/docs/guides/local-only-migration.md +35 -0
  16. package/docs/guides/replay-local-traces.md +11 -0
  17. package/docs/guides/run-evals.md +2 -4
  18. package/docs/guides/security-rules.md +5 -154
  19. package/docs/guides/use-jev.md +3 -6
  20. package/docs/guides/use-provider.md +0 -3
  21. package/docs/guides/write-feedback.md +10 -0
  22. package/docs/llms-full.txt +27 -448
  23. package/docs/llms.txt +8 -44
  24. package/package.json +2 -4
  25. package/src/cli/commands/channels.ts +8 -1613
  26. package/src/cli/commands/feedback.ts +8 -86
  27. package/src/cli/constants.ts +0 -3
  28. package/src/cli/flags.ts +0 -28
  29. package/src/cli/help.ts +16 -92
  30. package/src/cli/index.ts +15 -1091
  31. package/src/index.ts +6 -158
  32. package/src/providers/pi.ts +2 -0
  33. package/src/runtime/channels/discord.ts +16 -8
  34. package/src/runtime/channels/generic-webhook.ts +21 -313
  35. package/src/runtime/channels/net-guard.ts +480 -0
  36. package/src/runtime/channels/provider-fetch.ts +54 -0
  37. package/src/runtime/channels/slack.ts +9 -3
  38. package/src/runtime/channels/telegram.ts +43 -10
  39. package/src/runtime/channels/whatsapp-evolution.ts +33 -60
  40. package/src/runtime/channels/whatsapp-uazapi.ts +33 -164
  41. package/src/runtime/channels/whatsapp-zapster.ts +35 -7
  42. package/src/runtime/chat.ts +5 -3
  43. package/src/runtime/config.ts +36 -147
  44. package/src/runtime/database.ts +2 -2
  45. package/src/runtime/dev-server.ts +8 -8
  46. package/src/runtime/env.ts +11 -0
  47. package/src/runtime/improve.ts +2 -262
  48. package/src/runtime/inspect.ts +13 -73
  49. package/src/runtime/knowledge/embeddings.ts +45 -7
  50. package/src/runtime/knowledge/ingest.ts +70 -7
  51. package/src/runtime/knowledge/tool.ts +16 -2
  52. package/src/runtime/knowledge/vector.ts +31 -31
  53. package/src/runtime/tool-runner.ts +5 -3
  54. package/src/runtime/tools.ts +10 -14
  55. package/src/storage/sqlite.ts +11 -32
  56. package/src/templates/blank.ts +15 -102
  57. package/src/templates/common.ts +60 -0
  58. package/src/templates/dentista.ts +7 -74
  59. package/src/templates/skills/agentkit-capsule/SKILL.md +5 -7
  60. package/src/templates/skills/agentkit-capsule/references/docs-router.md +2 -3
  61. package/src/templates/skills/agentkit-channels/SKILL.md +6 -119
  62. package/src/templates/skills/agentkit-channels/references/channel-buffering.md +0 -9
  63. package/src/templates/skills/agentkit-channels/references/channel-debugging.md +1 -64
  64. package/src/templates/skills/agentkit-channels/references/discord.md +2 -92
  65. package/src/templates/skills/agentkit-channels/references/slack.md +2 -55
  66. package/src/templates/skills/agentkit-channels/references/telegram.md +2 -71
  67. package/src/templates/skills/agentkit-channels/references/whatsapp-evolution.md +2 -56
  68. package/src/templates/skills/agentkit-channels/references/whatsapp-uazapi.md +2 -53
  69. package/src/templates/skills/agentkit-channels/references/whatsapp-zapster.md +2 -70
  70. package/src/templates/skills/agentkit-database/SKILL.md +2 -4
  71. package/src/templates/skills/agentkit-evals/SKILL.md +1 -1
  72. package/src/templates/skills/agentkit-improve/SKILL.md +6 -85
  73. package/src/templates/skills/agentkit-improve/references/trace-packets.md +1 -1
  74. package/src/templates/skills/agentkit-provider/SKILL.md +0 -1
  75. package/src/templates/skills/agentkit-security/SKILL.md +1 -3
  76. package/src/templates/skills/agentkit-tools/SKILL.md +1 -1
  77. package/src/templates/skills/agentkit-tools/examples/database-write.tool.md +1 -2
  78. package/src/templates/skills/agentkit-troubleshooting/SKILL.md +5 -11
  79. package/src/templates/support.ts +8 -92
  80. package/docs/guides/add-managed-composio.md +0 -165
  81. package/docs/guides/improve-from-production.md +0 -151
  82. package/docs/guides/prepare-deploy.md +0 -227
  83. package/docs/guides/replay-production-traces.md +0 -72
  84. package/docs/guides/send-feedback.md +0 -135
  85. package/src/cli/cloud-client.ts +0 -377
  86. package/src/cli/deploy-chat-ui.ts +0 -606
  87. package/src/cli/deploy-readiness.ts +0 -561
  88. package/src/cloud/artifact.ts +0 -139
  89. package/src/cloud/client.ts +0 -80
  90. package/src/cloud/contracts.ts +0 -63
  91. package/src/cloud/index.ts +0 -3
  92. package/src/runtime/build.ts +0 -43
  93. package/src/runtime/core/deploy-state.ts +0 -54
  94. package/src/runtime/core/manifest.ts +0 -283
  95. package/src/runtime/core/targets.ts +0 -133
  96. package/src/runtime/deploy-readiness.ts +0 -135
  97. package/src/runtime/deploy.ts +0 -1
  98. package/src/runtime/integrations/composio.ts +0 -425
  99. package/src/runtime/targets/cloudflare/build.ts +0 -3319
  100. package/src/runtime/targets/container/build.ts +0 -146
  101. package/src/runtime/targets/container/server.ts +0 -33
  102. package/src/runtime/targets/vps/deploy.ts +0 -223
  103. package/src/templates/skills/agentkit-deploy/SKILL.md +0 -52
  104. package/src/templates/skills/agentkit-integrations/SKILL.md +0 -98
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type AgentRuntime = "local" | "edge";
1
+ export type AgentRuntime = "local";
2
2
  export type AgentProviderName = "test" | "openai" | "openai-codex" | "anthropic" | "openrouter" | "opencode" | "opencode-go" | "custom";
3
3
  export type AccessMode = "private" | "token" | "public";
4
4
  export type ToolVisibility = "user" | "internal";
@@ -275,38 +275,12 @@ export type AgentChannel =
275
275
  | GenericWebhookChannelConfig
276
276
  | GenericWebhookOutputChannelConfig;
277
277
 
278
- export type IntegrationTagsConfig = {
279
- enable?: string[];
280
- disable?: string[];
281
- };
282
-
283
- export type ComposioManagedIntegrationInput = {
284
- name?: string;
285
- toolkits: string[];
286
- tools?: Record<string, string[]>;
287
- tags?: IntegrationTagsConfig;
288
- confirmExternalWrites?: boolean;
289
- };
290
-
291
- export type ComposioManagedIntegration = {
292
- type: "composio";
293
- provider: "composio";
294
- mode: "managed";
295
- name: string;
296
- toolkits: string[];
297
- tools?: Record<string, string[]>;
298
- tags?: IntegrationTagsConfig;
299
- confirmExternalWrites?: boolean;
300
- };
301
-
302
- export type AgentIntegration = ComposioManagedIntegration;
303
-
304
278
  export type AgentDatabaseConfig =
305
279
  | {
306
280
  driver: "none";
307
281
  }
308
282
  | {
309
- driver: "turso";
283
+ driver: "sqlite";
310
284
  schema?: string;
311
285
  migrations?: string;
312
286
  };
@@ -316,8 +290,7 @@ export type AgentFilesConfig =
316
290
  driver: "none";
317
291
  }
318
292
  | {
319
- driver: "r2";
320
- bucket?: string;
293
+ driver: "local";
321
294
  prefix?: string;
322
295
  maxUploadBytes?: number;
323
296
  };
@@ -417,10 +390,10 @@ export type ToolRenderContext<Input = unknown> = {
417
390
  };
418
391
 
419
392
  export type ToolRuntimeContext = {
420
- environment: "local" | "hosted" | "eval" | "test";
393
+ environment: "local" | "eval" | "test";
421
394
  invocation: "chat" | "tool" | "dev" | "channel" | "eval" | "sync";
422
- target: "bun" | "cloudflare";
423
- database: "sqlite" | "turso" | "none";
395
+ target: "bun";
396
+ database: "sqlite" | "none";
424
397
  };
425
398
 
426
399
  export type ToolClockContext = {
@@ -472,7 +445,6 @@ export type AgentConfig = {
472
445
  instructions: string;
473
446
  secrets: string[];
474
447
  tools?: AgentTool[];
475
- integrations?: AgentIntegration[];
476
448
  channels?: AgentChannel[];
477
449
  transcription?: AgentTranscriptionConfig;
478
450
  knowledge?: AgentKnowledgeConfig;
@@ -495,25 +467,6 @@ export function defineTool<Input = unknown, Output = unknown>(tool: AgentTool<In
495
467
  return tool;
496
468
  }
497
469
 
498
- export function composioManaged(input: ComposioManagedIntegrationInput): ComposioManagedIntegration {
499
- const name = normalizeIntegrationName(input.name ?? "composio", "composio integration name");
500
- const toolkits = normalizeToolkitList(input.toolkits, "composioManaged.toolkits");
501
- const tools = input.tools ? normalizeComposioToolMap(input.tools, toolkits) : undefined;
502
- const tags = input.tags ? normalizeIntegrationTags(input.tags) : undefined;
503
- const confirmExternalWrites = input.confirmExternalWrites;
504
-
505
- return {
506
- type: "composio",
507
- provider: "composio",
508
- mode: "managed",
509
- name,
510
- toolkits,
511
- ...(tools ? { tools } : {}),
512
- ...(tags ? { tags } : {}),
513
- ...(confirmExternalWrites === undefined ? {} : { confirmExternalWrites }),
514
- };
515
- }
516
-
517
470
  export function websiteChannel(input: WebsiteChannelInput): WebsiteChannelConfig {
518
471
  return {
519
472
  type: "website",
@@ -663,109 +616,6 @@ function normalizeSecretName(secret: string, context: string): string {
663
616
  return secret;
664
617
  }
665
618
 
666
- function normalizeIntegrationName(value: string, context: string): string {
667
- const name = value.trim();
668
-
669
- if (!/^[a-z][a-z0-9-]*$/.test(name)) {
670
- throw new Error(`${context} must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens.`);
671
- }
672
-
673
- return name;
674
- }
675
-
676
- function normalizeToolkitList(values: string[], context: string): string[] {
677
- if (!Array.isArray(values) || values.length === 0) {
678
- throw new Error(`${context} must include at least one toolkit slug.`);
679
- }
680
-
681
- const toolkits = new Set<string>();
682
-
683
- for (const value of values) {
684
- if (typeof value !== "string") {
685
- throw new Error(`${context} entries must be toolkit slug strings.`);
686
- }
687
-
688
- const toolkit = value.trim().toLowerCase();
689
-
690
- if (!/^[a-z][a-z0-9_-]*$/.test(toolkit)) {
691
- throw new Error(`${context} entries must start with a letter and contain only letters, numbers, underscores, and hyphens.`);
692
- }
693
-
694
- toolkits.add(toolkit);
695
- }
696
-
697
- return Array.from(toolkits).sort();
698
- }
699
-
700
- function normalizeComposioToolMap(values: Record<string, string[]>, toolkits: string[]): Record<string, string[]> | undefined {
701
- const toolkitSet = new Set(toolkits);
702
- const normalized: Record<string, string[]> = {};
703
-
704
- for (const [rawToolkit, rawTools] of Object.entries(values)) {
705
- const toolkit = rawToolkit.trim().toLowerCase();
706
-
707
- if (!toolkitSet.has(toolkit)) {
708
- throw new Error(`composioManaged.tools.${rawToolkit} must reference a toolkit listed in composioManaged.toolkits.`);
709
- }
710
-
711
- if (!Array.isArray(rawTools) || rawTools.length === 0) {
712
- throw new Error(`composioManaged.tools.${rawToolkit} must include at least one Composio action slug.`);
713
- }
714
-
715
- const tools = new Set<string>();
716
-
717
- for (const rawTool of rawTools) {
718
- if (typeof rawTool !== "string") {
719
- throw new Error(`composioManaged.tools.${rawToolkit} entries must be Composio action slug strings.`);
720
- }
721
-
722
- const tool = rawTool.trim().toUpperCase();
723
-
724
- if (!/^[A-Z][A-Z0-9_]*$/.test(tool)) {
725
- throw new Error(`composioManaged.tools.${rawToolkit} entries must be uppercase Composio action slugs.`);
726
- }
727
-
728
- tools.add(tool);
729
- }
730
-
731
- normalized[toolkit] = Array.from(tools).sort();
732
- }
733
-
734
- return Object.keys(normalized).length > 0 ? normalized : undefined;
735
- }
736
-
737
- function normalizeIntegrationTags(tags: IntegrationTagsConfig): IntegrationTagsConfig {
738
- const normalized: IntegrationTagsConfig = {};
739
-
740
- if (tags.enable !== undefined) {
741
- normalized.enable = normalizeTagList(tags.enable, "composioManaged.tags.enable");
742
- }
743
-
744
- if (tags.disable !== undefined) {
745
- normalized.disable = normalizeTagList(tags.disable, "composioManaged.tags.disable");
746
- }
747
-
748
- return normalized;
749
- }
750
-
751
- function normalizeTagList(values: string[], context: string): string[] {
752
- if (!Array.isArray(values)) {
753
- throw new Error(`${context} must be an array of tag strings.`);
754
- }
755
-
756
- return Array.from(
757
- new Set(
758
- values.map((value) => {
759
- if (typeof value !== "string" || value.trim().length === 0) {
760
- throw new Error(`${context} entries must be non-empty strings.`);
761
- }
762
-
763
- return value.trim();
764
- }),
765
- ),
766
- ).sort();
767
- }
768
-
769
619
  function normalizeChannelBuffer(input: ChannelBufferInput, context: string): ChannelBufferConfig {
770
620
  if (input.mode === "off") {
771
621
  return { mode: "off" };
@@ -928,8 +778,6 @@ export { getConversationTraceFromCwd } from "./runtime/traces";
928
778
  export { runToolFromCwd } from "./runtime/tool-runner";
929
779
  export { startAgentDevServer } from "./runtime/dev-server";
930
780
  export { inspectAgentCapsule } from "./runtime/inspect";
931
- export { buildAgentCapsule } from "./runtime/build";
932
- export { deployBuildToAgentKitCloud } from "./runtime/deploy";
933
781
  export { AgentKitError } from "./runtime/errors";
934
782
  export type {
935
783
  ChannelAdapter,
@@ -117,7 +117,9 @@ export function providerToolResultText(result: {
117
117
  visibility?: "user" | "internal";
118
118
  rendered?: { text?: string };
119
119
  output: unknown;
120
+ modelText?: string;
120
121
  }): string {
122
+ if (result.modelText !== undefined) return result.modelText;
121
123
  if (result.visibility === "internal") {
122
124
  return `Internal tool ${result.name} completed.`;
123
125
  }
@@ -8,6 +8,10 @@ import type {
8
8
  WebhookVerificationResult,
9
9
  } from "../channels";
10
10
  import { AgentKitError } from "../errors";
11
+ import {
12
+ CHANNEL_SEND_TIMEOUT_MS,
13
+ classifySendRequestError,
14
+ } from "./provider-fetch";
11
15
 
12
16
  const DISCORD_INTERACTION_PING = 1;
13
17
  const DISCORD_INTERACTION_APPLICATION_COMMAND = 2;
@@ -120,9 +124,9 @@ export const discordChannelAdapter: ChannelAdapter = {
120
124
  }
121
125
 
122
126
  return {
123
- status: "waiting_for_provider_event",
127
+ status: "needs_setup",
124
128
  checkedAt: new Date().toISOString(),
125
- message: "Invite the Discord bot to a server and enable Message Content Intent.",
129
+ message: "AgentKit does not connect to the Discord Gateway. Use interactions or run your own Gateway bridge to forward authenticated bot events; inviting the bot alone is insufficient. Bridge connectivity cannot be verified here.",
126
130
  };
127
131
  }
128
132
 
@@ -331,16 +335,18 @@ export async function sendDiscordMessage(
331
335
 
332
336
  try {
333
337
  response = await fetcher(actualRequest.url, {
338
+ signal: AbortSignal.timeout(CHANNEL_SEND_TIMEOUT_MS),
334
339
  method: actualRequest.method,
335
340
  headers: actualRequest.headers,
336
341
  body: JSON.stringify(actualRequest.body),
337
342
  });
338
343
  } catch (error) {
344
+ const failure = classifySendRequestError(error);
339
345
  return {
340
346
  ok: false,
341
- retryable: true,
342
- code: "channel_provider_unavailable",
343
- message: `Discord bot message request failed before a response: ${redactDiscordToken(
347
+ retryable: failure.retryable,
348
+ code: failure.code,
349
+ message: `Discord bot message request ${failure.reason}: ${redactDiscordToken(
344
350
  error instanceof Error ? error.message : String(error),
345
351
  input.secrets.DISCORD_BOT_TOKEN,
346
352
  )}`,
@@ -457,6 +463,7 @@ export async function sendDiscordInteractionFollowup(
457
463
 
458
464
  try {
459
465
  response = await fetcher(actualRequest.url, {
466
+ signal: AbortSignal.timeout(CHANNEL_SEND_TIMEOUT_MS),
460
467
  method: actualRequest.method,
461
468
  headers: {
462
469
  "Content-Type": "application/json",
@@ -464,11 +471,12 @@ export async function sendDiscordInteractionFollowup(
464
471
  body: JSON.stringify(actualRequest.body),
465
472
  });
466
473
  } catch (error) {
474
+ const failure = classifySendRequestError(error);
467
475
  return {
468
476
  ok: false,
469
- retryable: true,
470
- code: "channel_provider_unavailable",
471
- message: `Discord follow-up request failed before a response: ${redactDiscordToken(
477
+ retryable: failure.retryable,
478
+ code: failure.code,
479
+ message: `Discord follow-up request ${failure.reason}: ${redactDiscordToken(
472
480
  error instanceof Error ? error.message : String(error),
473
481
  replyTarget.interactionToken,
474
482
  )}`,
@@ -1,16 +1,21 @@
1
1
  import { createHash, createHmac, randomUUID, timingSafeEqual } from "node:crypto";
2
- import { lookup as dnsLookup } from "node:dns/promises";
3
- import { request as httpsRequest } from "node:https";
4
- import type { LookupFunction } from "node:net";
5
- import { Readable } from "node:stream";
6
2
 
7
3
  import type { GenericWebhookOutputChannelConfig } from "../../index";
8
4
  import type { ChannelAdapter, ChannelSendInput, ChannelSendResult, RawWebhookEvent } from "../channels";
9
5
  import { AgentKitError } from "../errors";
6
+ import {
7
+ isBlockedOutboundHost,
8
+ pinnedHttpsFetch,
9
+ resolveOutboundAddresses,
10
+ resolvePublicOutboundUrl,
11
+ type OutboundResolver,
12
+ type ResolvedOutboundUrl,
13
+ } from "./net-guard";
10
14
 
11
15
  const DEFAULT_SECRET_NAME = "AGENTKIT_WEBHOOK_SECRET";
12
16
  const DEFAULT_OUTPUT_HMAC_HEADER = "X-AgentKit-Webhook-Signature";
13
17
  const MAX_OUTPUT_RESPONSE_BYTES = 64_000;
18
+ const GENERIC_WEBHOOK_OUTPUT_URL_LABEL = "Generic webhook output URL";
14
19
 
15
20
  export const genericWebhookChannelAdapter: ChannelAdapter = {
16
21
  type: "webhook",
@@ -152,12 +157,7 @@ type GenericWebhookOutputRequest = {
152
157
  };
153
158
 
154
159
  type GenericWebhookOutputFetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
155
- type GenericWebhookOutputResolver = (hostname: string) => Promise<string[]>;
156
- type GenericWebhookOutputResolvedUrl = {
157
- href: string;
158
- address: string;
159
- family: 4 | 6;
160
- };
160
+ type GenericWebhookOutputResolver = OutboundResolver;
161
161
  type GenericWebhookOutputRequestInit = {
162
162
  method: "POST";
163
163
  headers: Record<string, string>;
@@ -218,10 +218,12 @@ export async function sendGenericWebhookOutputMessage(
218
218
  input: ChannelSendInput,
219
219
  channel: GenericWebhookOutputChannelConfig,
220
220
  fetcher?: GenericWebhookOutputFetch,
221
- resolver: GenericWebhookOutputResolver = resolveGenericWebhookOutputAddresses,
221
+ resolver: GenericWebhookOutputResolver = resolveOutboundAddresses,
222
222
  ): Promise<ChannelSendResult> {
223
223
  let request: GenericWebhookOutputRequest;
224
- let resolvedUrl: GenericWebhookOutputResolvedUrl;
224
+ let resolvedUrl: ResolvedOutboundUrl;
225
+ // Started before DNS resolution so a stalled resolver counts against the deadline.
226
+ const signal = AbortSignal.timeout(15_000);
225
227
 
226
228
  try {
227
229
  request = buildGenericWebhookOutputRequest(input, channel);
@@ -235,7 +237,7 @@ export async function sendGenericWebhookOutputMessage(
235
237
  };
236
238
  }
237
239
 
238
- resolvedUrl = await validateGenericWebhookOutputResolvedUrl(request.url, resolver);
240
+ resolvedUrl = await validateGenericWebhookOutputResolvedUrl(request.url, resolver, signal);
239
241
  } catch (error) {
240
242
  return genericWebhookOutputFailureFromError(error);
241
243
  }
@@ -246,13 +248,13 @@ export async function sendGenericWebhookOutputMessage(
246
248
  headers: request.headers,
247
249
  body: JSON.stringify(request.body),
248
250
  redirect: "manual",
249
- signal: AbortSignal.timeout(15_000),
251
+ signal,
250
252
  };
251
253
 
252
254
  try {
253
255
  response = fetcher
254
256
  ? await fetcher(resolvedUrl.href, requestInit)
255
- : await fetchGenericWebhookOutputResponse(resolvedUrl, requestInit);
257
+ : await pinnedHttpsFetch(resolvedUrl, requestInit, GENERIC_WEBHOOK_OUTPUT_URL_LABEL);
256
258
  } catch (error) {
257
259
  if (error instanceof AgentKitError) {
258
260
  return {
@@ -411,7 +413,7 @@ function validateGenericWebhookOutputUrl(rawUrl: string): string {
411
413
  throw new AgentKitError("channel_payload_invalid", "Generic webhook output URL must not include credentials.");
412
414
  }
413
415
 
414
- if (isBlockedGenericWebhookOutputHost(url.hostname)) {
416
+ if (isBlockedOutboundHost(url.hostname)) {
415
417
  throw new AgentKitError("channel_payload_invalid", "Generic webhook output URL host is not allowed.");
416
418
  }
417
419
 
@@ -421,317 +423,23 @@ function validateGenericWebhookOutputUrl(rawUrl: string): string {
421
423
  async function validateGenericWebhookOutputResolvedUrl(
422
424
  rawUrl: string,
423
425
  resolver: GenericWebhookOutputResolver,
424
- ): Promise<GenericWebhookOutputResolvedUrl> {
425
- const href = validateGenericWebhookOutputUrl(rawUrl);
426
- const url = new URL(href);
427
- let addresses: string[];
428
-
429
- try {
430
- addresses = await resolver(url.hostname);
431
- } catch (error) {
432
- throw new AgentKitError(
433
- "channel_provider_unavailable",
434
- `Generic webhook output URL host could not be resolved: ${error instanceof Error ? error.message : String(error)}`,
435
- );
436
- }
437
-
438
- if (addresses.length === 0) {
439
- throw new AgentKitError("channel_provider_unavailable", "Generic webhook output URL host did not resolve.");
440
- }
441
-
442
- for (const address of addresses) {
443
- if (isBlockedGenericWebhookOutputHost(address)) {
444
- throw new AgentKitError("channel_payload_invalid", "Generic webhook output URL host resolved to a blocked address.");
445
- }
446
- }
447
-
448
- const address = normalizeGenericWebhookOutputHost(addresses[0]);
449
-
450
- return {
451
- href,
452
- address,
453
- family: parseIpv4Octets(address) ? 4 : 6,
454
- };
455
- }
456
-
457
- function fetchGenericWebhookOutputResponse(
458
- resolvedUrl: GenericWebhookOutputResolvedUrl,
459
- init: GenericWebhookOutputRequestInit,
460
- ): Promise<Response> {
461
- const url = new URL(resolvedUrl.href);
462
- const lookup: LookupFunction = (_hostname, _options, callback) => {
463
- callback(null, resolvedUrl.address, resolvedUrl.family);
464
- };
465
-
466
- return new Promise((resolve, reject) => {
467
- const request = httpsRequest(
468
- url,
469
- {
470
- method: init.method,
471
- headers: init.headers,
472
- lookup,
473
- signal: init.signal,
474
- },
475
- (response) => {
476
- try {
477
- assertAllowedGenericWebhookOutputRemoteAddress(response.socket.remoteAddress);
478
- } catch (error) {
479
- response.destroy(error instanceof Error ? error : new Error(String(error)));
480
- reject(error);
481
- return;
482
- }
483
-
484
- resolve(
485
- new Response(Readable.toWeb(response) as unknown as ReadableStream<Uint8Array>, {
486
- status: response.statusCode ?? 502,
487
- headers: headersFromIncomingMessage(response.headers),
488
- }),
489
- );
490
- },
491
- );
492
-
493
- request.on("socket", (socket) => {
494
- const validateSocket = () => {
495
- try {
496
- assertAllowedGenericWebhookOutputRemoteAddress(socket.remoteAddress);
497
- } catch (error) {
498
- request.destroy(error instanceof Error ? error : new Error(String(error)));
499
- }
500
- };
501
-
502
- socket.once("connect", validateSocket);
503
- socket.once("secureConnect", validateSocket);
504
- });
505
- request.once("error", reject);
506
- request.end(init.body);
507
- });
508
- }
509
-
510
- function assertAllowedGenericWebhookOutputRemoteAddress(address: string | undefined): void {
511
- if (!address || isBlockedGenericWebhookOutputHost(address)) {
512
- throw new AgentKitError("channel_payload_invalid", "Generic webhook output URL connected to a blocked address.");
513
- }
514
- }
515
-
516
- function headersFromIncomingMessage(headers: Record<string, string | string[] | number | undefined>): Headers {
517
- const result = new Headers();
518
-
519
- for (const [key, value] of Object.entries(headers)) {
520
- if (Array.isArray(value)) {
521
- for (const item of value) {
522
- result.append(key, item);
523
- }
524
- continue;
525
- }
526
-
527
- if (value !== undefined) {
528
- result.set(key, String(value));
529
- }
530
- }
531
-
532
- return result;
533
- }
534
-
535
- async function resolveGenericWebhookOutputAddresses(hostname: string): Promise<string[]> {
536
- const host = normalizeGenericWebhookOutputHost(hostname);
537
-
538
- if (parseIpv4Octets(host) || parseIpv6Segments(host)) {
539
- return [host];
540
- }
541
-
542
- const records = await dnsLookup(host, {
543
- all: true,
544
- verbatim: true,
545
- });
546
-
547
- return records.map((record) => record.address);
548
- }
549
-
550
- function isBlockedGenericWebhookOutputHost(hostname: string): boolean {
551
- const host = normalizeGenericWebhookOutputHost(hostname);
552
-
553
- if (
554
- host === "localhost" ||
555
- host.endsWith(".localhost") ||
556
- host === "metadata.google.internal"
557
- ) {
558
- return true;
559
- }
560
-
561
- const ipv4 = parseIpv4Octets(host);
562
-
563
- if (ipv4) {
564
- return isBlockedIpv4Octets(ipv4);
565
- }
566
-
567
- const ipv6 = parseIpv6Segments(host);
568
-
569
- if (ipv6) {
570
- return isBlockedIpv6Segments(ipv6);
571
- }
572
-
573
- return false;
574
- }
575
-
576
- function normalizeGenericWebhookOutputHost(hostname: string): string {
577
- return hostname.toLowerCase().replace(/^\[/, "").replace(/\]$/, "").replace(/\.$/, "");
578
- }
579
-
580
- function parseIpv4Octets(host: string): number[] | undefined {
581
- const parts = host.split(".");
582
-
583
- if (parts.length !== 4) {
584
- return undefined;
585
- }
586
-
587
- const octets = parts.map((part) => Number(part));
588
-
589
- if (octets.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
590
- return undefined;
591
- }
592
-
593
- return octets;
426
+ signal: AbortSignal,
427
+ ): Promise<ResolvedOutboundUrl> {
428
+ return resolvePublicOutboundUrl(validateGenericWebhookOutputUrl(rawUrl), GENERIC_WEBHOOK_OUTPUT_URL_LABEL, resolver, signal);
594
429
  }
595
430
 
596
- function isBlockedIpv4Octets(parts: number[]): boolean {
597
- const [a, b] = parts;
598
431
 
599
- return (
600
- a === 0 ||
601
- a === 10 ||
602
- a === 127 ||
603
- (a === 100 && b >= 64 && b <= 127) ||
604
- (a === 169 && b === 254) ||
605
- (a === 172 && b >= 16 && b <= 31) ||
606
- (a === 192 && b === 168) ||
607
- (a === 198 && (b === 18 || b === 19)) ||
608
- a >= 224
609
- );
610
- }
611
432
 
612
- function parseIpv6Segments(host: string): number[] | undefined {
613
- if (!host.includes(":")) {
614
- return undefined;
615
- }
616
433
 
617
- if (host === "::") {
618
- return new Array(8).fill(0);
619
- }
620
434
 
621
- const [leftRaw, rightRaw, extra] = host.split("::");
622
435
 
623
- if (extra !== undefined) {
624
- return undefined;
625
- }
626
436
 
627
- const left = parseIpv6SegmentSide(leftRaw ?? "");
628
- const right = parseIpv6SegmentSide(rightRaw ?? "");
629
437
 
630
- if (!left || !right) {
631
- return undefined;
632
- }
633
438
 
634
- if (rightRaw === undefined) {
635
- return left.length === 8 ? left : undefined;
636
- }
637
-
638
- const missing = 8 - left.length - right.length;
639
-
640
- if (missing < 1) {
641
- return undefined;
642
- }
643
-
644
- return [...left, ...new Array(missing).fill(0), ...right];
645
- }
646
439
 
647
- function parseIpv6SegmentSide(value: string): number[] | undefined {
648
- if (!value) {
649
- return [];
650
- }
651
440
 
652
- const segments: number[] = [];
653
441
 
654
- const parts = value.split(":");
655
-
656
- for (const [index, part] of parts.entries()) {
657
- if (part.includes(".")) {
658
- if (index !== parts.length - 1) {
659
- return undefined;
660
- }
661
-
662
- const octets = parseIpv4Octets(part);
663
-
664
- if (!octets) {
665
- return undefined;
666
- }
667
-
668
- segments.push((octets[0] << 8) | octets[1], (octets[2] << 8) | octets[3]);
669
- continue;
670
- }
671
442
 
672
- if (!/^[0-9a-f]{1,4}$/i.test(part)) {
673
- return undefined;
674
- }
675
-
676
- segments.push(parseInt(part, 16));
677
- }
678
-
679
- return segments;
680
- }
681
-
682
- function isBlockedIpv6Segments(parts: number[]): boolean {
683
- if (parts.length !== 8) {
684
- return true;
685
- }
686
-
687
- const embeddedIpv4 = ipv4OctetsFromIpv6Segments(parts);
688
-
689
- if (embeddedIpv4) {
690
- return isBlockedIpv4Octets(embeddedIpv4);
691
- }
692
-
693
- const isAllZero = parts.every((part) => part === 0);
694
-
695
- if (isAllZero || (parts.slice(0, 7).every((part) => part === 0) && parts[7] === 1)) {
696
- return true;
697
- }
698
-
699
- const first = parts[0];
700
-
701
- return (
702
- (first & 0xfe00) === 0xfc00 ||
703
- (first & 0xffc0) === 0xfe80 ||
704
- (first & 0xff00) === 0xff00
705
- );
706
- }
707
-
708
- function ipv4OctetsFromIpv6Segments(parts: number[]): number[] | undefined {
709
- const trailingIpv4 = [
710
- (parts[6] >> 8) & 0xff,
711
- parts[6] & 0xff,
712
- (parts[7] >> 8) & 0xff,
713
- parts[7] & 0xff,
714
- ];
715
-
716
- if (parts.slice(0, 5).every((part) => part === 0) && (parts[5] === 0xffff || parts[5] === 0)) {
717
- return trailingIpv4;
718
- }
719
-
720
- if (parts[0] === 0x64 && parts[1] === 0xff9b && parts.slice(2, 6).every((part) => part === 0)) {
721
- return trailingIpv4;
722
- }
723
-
724
- if (parts[0] === 0x2002) {
725
- return [
726
- (parts[1] >> 8) & 0xff,
727
- parts[1] & 0xff,
728
- (parts[2] >> 8) & 0xff,
729
- parts[2] & 0xff,
730
- ];
731
- }
732
-
733
- return undefined;
734
- }
735
443
 
736
444
  function redactedGenericWebhookOutputRequest(request: GenericWebhookOutputRequest): Record<string, unknown> {
737
445
  return {