@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.
- package/README.md +8 -77
- package/docs/guides/add-channel.md +14 -92
- package/docs/guides/add-knowledge.md +0 -21
- package/docs/guides/add-tool.md +5 -11
- package/docs/guides/channel-security.md +3 -207
- package/docs/guides/connect-discord.md +7 -172
- package/docs/guides/connect-slack.md +6 -121
- package/docs/guides/connect-telegram.md +6 -165
- package/docs/guides/connect-whatsapp-evolution.md +6 -116
- package/docs/guides/connect-whatsapp-uazapi.md +6 -134
- package/docs/guides/connect-whatsapp-zapster.md +6 -202
- package/docs/guides/create-agent.md +5 -14
- package/docs/guides/debug-channel.md +4 -156
- package/docs/guides/improve-local.md +13 -0
- package/docs/guides/local-only-migration.md +35 -0
- package/docs/guides/replay-local-traces.md +11 -0
- package/docs/guides/run-evals.md +2 -4
- package/docs/guides/security-rules.md +5 -154
- package/docs/guides/use-jev.md +3 -6
- package/docs/guides/use-provider.md +0 -3
- package/docs/guides/write-feedback.md +10 -0
- package/docs/llms-full.txt +27 -448
- package/docs/llms.txt +8 -44
- package/package.json +2 -4
- package/src/cli/commands/channels.ts +8 -1613
- package/src/cli/commands/feedback.ts +8 -86
- package/src/cli/constants.ts +0 -3
- package/src/cli/flags.ts +0 -28
- package/src/cli/help.ts +16 -92
- package/src/cli/index.ts +15 -1091
- package/src/index.ts +6 -158
- package/src/providers/pi.ts +2 -0
- package/src/runtime/channels/discord.ts +16 -8
- package/src/runtime/channels/generic-webhook.ts +21 -313
- package/src/runtime/channels/net-guard.ts +480 -0
- package/src/runtime/channels/provider-fetch.ts +54 -0
- package/src/runtime/channels/slack.ts +9 -3
- package/src/runtime/channels/telegram.ts +43 -10
- package/src/runtime/channels/whatsapp-evolution.ts +33 -60
- package/src/runtime/channels/whatsapp-uazapi.ts +33 -164
- package/src/runtime/channels/whatsapp-zapster.ts +35 -7
- package/src/runtime/chat.ts +5 -3
- package/src/runtime/config.ts +36 -147
- package/src/runtime/database.ts +2 -2
- package/src/runtime/dev-server.ts +8 -8
- package/src/runtime/env.ts +11 -0
- package/src/runtime/improve.ts +2 -262
- package/src/runtime/inspect.ts +13 -73
- package/src/runtime/knowledge/embeddings.ts +45 -7
- package/src/runtime/knowledge/ingest.ts +70 -7
- package/src/runtime/knowledge/tool.ts +16 -2
- package/src/runtime/knowledge/vector.ts +31 -31
- package/src/runtime/tool-runner.ts +5 -3
- package/src/runtime/tools.ts +10 -14
- package/src/storage/sqlite.ts +11 -32
- package/src/templates/blank.ts +15 -102
- package/src/templates/common.ts +60 -0
- package/src/templates/dentista.ts +7 -74
- package/src/templates/skills/agentkit-capsule/SKILL.md +5 -7
- package/src/templates/skills/agentkit-capsule/references/docs-router.md +2 -3
- package/src/templates/skills/agentkit-channels/SKILL.md +6 -119
- package/src/templates/skills/agentkit-channels/references/channel-buffering.md +0 -9
- package/src/templates/skills/agentkit-channels/references/channel-debugging.md +1 -64
- package/src/templates/skills/agentkit-channels/references/discord.md +2 -92
- package/src/templates/skills/agentkit-channels/references/slack.md +2 -55
- package/src/templates/skills/agentkit-channels/references/telegram.md +2 -71
- package/src/templates/skills/agentkit-channels/references/whatsapp-evolution.md +2 -56
- package/src/templates/skills/agentkit-channels/references/whatsapp-uazapi.md +2 -53
- package/src/templates/skills/agentkit-channels/references/whatsapp-zapster.md +2 -70
- package/src/templates/skills/agentkit-database/SKILL.md +2 -4
- package/src/templates/skills/agentkit-evals/SKILL.md +1 -1
- package/src/templates/skills/agentkit-improve/SKILL.md +6 -85
- package/src/templates/skills/agentkit-improve/references/trace-packets.md +1 -1
- package/src/templates/skills/agentkit-provider/SKILL.md +0 -1
- package/src/templates/skills/agentkit-security/SKILL.md +1 -3
- package/src/templates/skills/agentkit-tools/SKILL.md +1 -1
- package/src/templates/skills/agentkit-tools/examples/database-write.tool.md +1 -2
- package/src/templates/skills/agentkit-troubleshooting/SKILL.md +5 -11
- package/src/templates/support.ts +8 -92
- package/docs/guides/add-managed-composio.md +0 -165
- package/docs/guides/improve-from-production.md +0 -151
- package/docs/guides/prepare-deploy.md +0 -227
- package/docs/guides/replay-production-traces.md +0 -72
- package/docs/guides/send-feedback.md +0 -135
- package/src/cli/cloud-client.ts +0 -377
- package/src/cli/deploy-chat-ui.ts +0 -606
- package/src/cli/deploy-readiness.ts +0 -561
- package/src/cloud/artifact.ts +0 -139
- package/src/cloud/client.ts +0 -80
- package/src/cloud/contracts.ts +0 -63
- package/src/cloud/index.ts +0 -3
- package/src/runtime/build.ts +0 -43
- package/src/runtime/core/deploy-state.ts +0 -54
- package/src/runtime/core/manifest.ts +0 -283
- package/src/runtime/core/targets.ts +0 -133
- package/src/runtime/deploy-readiness.ts +0 -135
- package/src/runtime/deploy.ts +0 -1
- package/src/runtime/integrations/composio.ts +0 -425
- package/src/runtime/targets/cloudflare/build.ts +0 -3319
- package/src/runtime/targets/container/build.ts +0 -146
- package/src/runtime/targets/container/server.ts +0 -33
- package/src/runtime/targets/vps/deploy.ts +0 -223
- package/src/templates/skills/agentkit-deploy/SKILL.md +0 -52
- package/src/templates/skills/agentkit-integrations/SKILL.md +0 -98
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type AgentRuntime = "local"
|
|
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: "
|
|
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: "
|
|
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" | "
|
|
393
|
+
environment: "local" | "eval" | "test";
|
|
421
394
|
invocation: "chat" | "tool" | "dev" | "channel" | "eval" | "sync";
|
|
422
|
-
target: "bun"
|
|
423
|
-
database: "sqlite" | "
|
|
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,
|
package/src/providers/pi.ts
CHANGED
|
@@ -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: "
|
|
127
|
+
status: "needs_setup",
|
|
124
128
|
checkedAt: new Date().toISOString(),
|
|
125
|
-
message: "
|
|
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:
|
|
342
|
-
code:
|
|
343
|
-
message: `Discord bot message request
|
|
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:
|
|
470
|
-
code:
|
|
471
|
-
message: `Discord follow-up request
|
|
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 =
|
|
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 =
|
|
221
|
+
resolver: GenericWebhookOutputResolver = resolveOutboundAddresses,
|
|
222
222
|
): Promise<ChannelSendResult> {
|
|
223
223
|
let request: GenericWebhookOutputRequest;
|
|
224
|
-
let resolvedUrl:
|
|
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
|
|
251
|
+
signal,
|
|
250
252
|
};
|
|
251
253
|
|
|
252
254
|
try {
|
|
253
255
|
response = fetcher
|
|
254
256
|
? await fetcher(resolvedUrl.href, requestInit)
|
|
255
|
-
: await
|
|
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 (
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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 {
|