@andreprado/agentkit 0.1.0-alpha.9 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -1
- package/docs/guides/add-channel.md +251 -7
- package/docs/guides/add-knowledge.md +10 -0
- package/docs/guides/add-managed-composio.md +165 -0
- package/docs/guides/add-tool.md +10 -3
- package/docs/guides/channel-security.md +162 -32
- package/docs/guides/connect-discord.md +178 -0
- package/docs/guides/connect-slack.md +126 -0
- package/docs/guides/connect-telegram.md +61 -1
- package/docs/guides/connect-whatsapp-evolution.md +121 -0
- package/docs/guides/connect-whatsapp-uazapi.md +139 -0
- package/docs/guides/connect-whatsapp-zapster.md +119 -16
- package/docs/guides/create-agent.md +31 -4
- package/docs/guides/debug-channel.md +159 -0
- package/docs/guides/improve-from-production.md +151 -0
- package/docs/guides/prepare-deploy.md +32 -14
- package/docs/guides/replay-production-traces.md +72 -0
- package/docs/guides/run-evals.md +95 -25
- package/docs/guides/security-rules.md +9 -5
- package/docs/guides/send-feedback.md +135 -0
- package/docs/guides/use-jev.md +67 -0
- package/docs/guides/use-provider.md +70 -3
- package/docs/llms-full.txt +295 -25
- package/docs/llms.txt +54 -7
- package/package.json +3 -7
- package/src/cli/args.ts +23 -2
- package/src/cli/cloud-client.ts +121 -9
- package/src/cli/commands/channels.ts +856 -36
- package/src/cli/commands/feedback.ts +438 -0
- package/src/cli/commands/provider.ts +47 -0
- package/src/cli/commands/transcribe.ts +171 -0
- package/src/cli/deploy-chat-ui.ts +232 -18
- package/src/cli/deploy-readiness.ts +227 -14
- package/src/cli/help.ts +67 -9
- package/src/cli/index.ts +740 -35
- package/src/cli/new-command.ts +41 -0
- package/src/cloud/client.ts +4 -3
- package/src/cloud/contracts.ts +1 -1
- package/src/create-project.ts +18 -35
- package/src/index.ts +565 -11
- package/src/providers/codex-auth.ts +111 -0
- package/src/providers/pi.ts +88 -19
- package/src/providers/test.ts +36 -0
- package/src/providers/types.ts +8 -0
- package/src/runtime/channel-test-harness.ts +21 -1
- package/src/runtime/channels/discord.ts +904 -0
- package/src/runtime/channels/generic-webhook.ts +682 -0
- package/src/runtime/channels/net-guard.ts +480 -0
- package/src/runtime/channels/provider-fetch.ts +54 -0
- package/src/runtime/channels/slack.ts +652 -0
- package/src/runtime/channels/telegram.ts +379 -15
- package/src/runtime/channels/whatsapp-evolution.ts +1330 -0
- package/src/runtime/channels/whatsapp-meta.ts +9 -0
- package/src/runtime/channels/whatsapp-uazapi.ts +1192 -0
- package/src/runtime/channels/whatsapp-zapster.ts +702 -40
- package/src/runtime/channels.ts +83 -3
- package/src/runtime/chat.ts +70 -44
- package/src/runtime/config.ts +512 -20
- package/src/runtime/core/manifest.ts +75 -5
- package/src/runtime/core/targets.ts +5 -5
- package/src/runtime/deploy-readiness.ts +34 -4
- package/src/runtime/dev-server.ts +639 -39
- package/src/runtime/env.ts +8 -3
- package/src/runtime/evals.ts +445 -74
- package/src/runtime/improve.ts +868 -0
- package/src/runtime/inspect.ts +173 -4
- package/src/runtime/integrations/composio.ts +425 -0
- package/src/runtime/knowledge/embeddings.ts +45 -7
- package/src/runtime/knowledge/ingest.ts +69 -6
- package/src/runtime/knowledge/retrieve.ts +25 -5
- package/src/runtime/knowledge/schema.ts +45 -1
- package/src/runtime/knowledge/vector.ts +30 -30
- package/src/runtime/prompt-context.ts +141 -0
- package/src/runtime/runtime-contract.ts +71 -7
- package/src/runtime/skills.ts +95 -0
- package/src/runtime/targets/cloudflare/build.ts +1010 -208
- package/src/runtime/targets/container/server.ts +1 -1
- package/src/runtime/targets/vps/deploy.ts +26 -9
- package/src/runtime/tool-runner.ts +9 -1
- package/src/runtime/tools.ts +26 -2
- package/src/runtime/transcription.ts +483 -0
- package/src/storage/sqlite.ts +7 -2
- package/src/templates/blank.ts +37 -9
- package/src/templates/dentista.ts +40 -14
- package/src/templates/skills/agentkit-build-agent/SKILL.md +34 -5
- package/src/templates/skills/agentkit-build-agent/templates/appointment-intake.instructions.md +2 -1
- package/src/templates/skills/agentkit-capsule/SKILL.md +32 -3
- package/src/templates/skills/agentkit-capsule/references/docs-router.md +2 -2
- package/src/templates/skills/agentkit-channels/SKILL.md +66 -1
- package/src/templates/skills/agentkit-channels/references/channel-buffering.md +8 -1
- package/src/templates/skills/agentkit-channels/references/channel-debugging.md +28 -3
- package/src/templates/skills/agentkit-channels/references/discord.md +93 -0
- package/src/templates/skills/agentkit-channels/references/slack.md +56 -0
- package/src/templates/skills/agentkit-channels/references/telegram.md +34 -0
- package/src/templates/skills/agentkit-channels/references/whatsapp-evolution.md +57 -0
- package/src/templates/skills/agentkit-channels/references/whatsapp-uazapi.md +54 -0
- package/src/templates/skills/agentkit-channels/references/whatsapp-zapster.md +42 -8
- package/src/templates/skills/agentkit-database/SKILL.md +11 -0
- package/src/templates/skills/agentkit-deploy/SKILL.md +9 -1
- package/src/templates/skills/agentkit-evals/SKILL.md +77 -13
- package/src/templates/skills/agentkit-evals/templates/multi-turn.eval.md +13 -6
- package/src/templates/skills/agentkit-evals/templates/no-leak.eval.md +8 -4
- package/src/templates/skills/agentkit-evals/templates/smoke.eval.md +8 -4
- package/src/templates/skills/agentkit-evals/templates/tool-call.eval.md +16 -7
- package/src/templates/skills/agentkit-improve/SKILL.md +96 -0
- package/src/templates/skills/agentkit-improve/references/replay-side-effects.md +18 -0
- package/src/templates/skills/agentkit-improve/references/trace-packets.md +22 -0
- package/src/templates/skills/agentkit-improve/templates/regression.eval.md +18 -0
- package/src/templates/skills/agentkit-integrations/SKILL.md +98 -0
- package/src/templates/skills/agentkit-knowledge/SKILL.md +4 -1
- package/src/templates/skills/agentkit-prompts/SKILL.md +3 -1
- package/src/templates/skills/agentkit-provider/SKILL.md +29 -4
- package/src/templates/skills/agentkit-security/SKILL.md +5 -2
- package/src/templates/skills/agentkit-tools/SKILL.md +8 -1
- package/src/templates/skills/agentkit-tools/examples/eval-safe-external-action.tool.md +8 -8
- package/src/templates/skills/agentkit-tools/examples/jev-service-fit.tool.md +110 -0
- package/src/templates/skills/agentkit-troubleshooting/SKILL.md +25 -1
- package/src/templates/support.ts +42 -12
- package/docs/guides/agentkit-skills-architecture.md +0 -471
- package/docs/guides/channels-implementation-map.md +0 -243
- package/docs/guides/channels-production-handoff.md +0 -101
- package/docs/portable-deploy-release-checklist.md +0 -41
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
import { createHash, createHmac, randomUUID, timingSafeEqual } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import type { GenericWebhookOutputChannelConfig } from "../../index";
|
|
4
|
+
import type { ChannelAdapter, ChannelSendInput, ChannelSendResult, RawWebhookEvent } from "../channels";
|
|
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";
|
|
14
|
+
|
|
15
|
+
const DEFAULT_SECRET_NAME = "AGENTKIT_WEBHOOK_SECRET";
|
|
16
|
+
const DEFAULT_OUTPUT_HMAC_HEADER = "X-AgentKit-Webhook-Signature";
|
|
17
|
+
const MAX_OUTPUT_RESPONSE_BYTES = 64_000;
|
|
18
|
+
const GENERIC_WEBHOOK_OUTPUT_URL_LABEL = "Generic webhook output URL";
|
|
19
|
+
|
|
20
|
+
export const genericWebhookChannelAdapter: ChannelAdapter = {
|
|
21
|
+
type: "webhook",
|
|
22
|
+
provider: "generic",
|
|
23
|
+
requiredSecrets: [DEFAULT_SECRET_NAME],
|
|
24
|
+
verifyWebhook(input) {
|
|
25
|
+
const expected = input.secrets[DEFAULT_SECRET_NAME];
|
|
26
|
+
|
|
27
|
+
if (!expected) {
|
|
28
|
+
return {
|
|
29
|
+
ok: false,
|
|
30
|
+
code: "channel_secret_missing",
|
|
31
|
+
message: `Secret ${DEFAULT_SECRET_NAME} is not set for this channel.`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const signature = input.headers.get("x-agentkit-webhook-signature") ?? input.headers.get("x-agentkit-signature");
|
|
36
|
+
|
|
37
|
+
if (signature) {
|
|
38
|
+
const expectedSignature = `sha256=${createHmac("sha256", expected).update(input.rawBody).digest("hex")}`;
|
|
39
|
+
|
|
40
|
+
if (constantTimeEqual(signature.trim(), expectedSignature)) {
|
|
41
|
+
return { ok: true };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
code: "channel_signature_invalid",
|
|
47
|
+
message: "Generic webhook HMAC signature did not match.",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const bearer = readBearerToken(input.headers.get("authorization"));
|
|
52
|
+
const headerToken =
|
|
53
|
+
input.headers.get("x-agentkit-webhook-secret") ??
|
|
54
|
+
input.headers.get("x-agentkit-channel-token") ??
|
|
55
|
+
input.headers.get("x-agentkit-webhook-token");
|
|
56
|
+
|
|
57
|
+
if (constantTimeEqual(bearer, expected) || constantTimeEqual(headerToken ?? "", expected)) {
|
|
58
|
+
return { ok: true };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
ok: false,
|
|
63
|
+
code: "channel_signature_invalid",
|
|
64
|
+
message: "Generic webhook token did not match.",
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
normalizeInbound(input) {
|
|
68
|
+
const body = parseJsonObject(input);
|
|
69
|
+
const eventId = readFirstScalar(body, ["event_id", "eventId", "id"]) ?? readNestedMessageId(body);
|
|
70
|
+
|
|
71
|
+
if (!eventId) {
|
|
72
|
+
throw new AgentKitError("channel_payload_invalid", "Generic webhook payload is missing event_id.");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const text = readMessageText(body);
|
|
76
|
+
const identitySource =
|
|
77
|
+
readFirstScalar(body, [
|
|
78
|
+
"external_id",
|
|
79
|
+
"externalId",
|
|
80
|
+
"user_id",
|
|
81
|
+
"userId",
|
|
82
|
+
"contact_id",
|
|
83
|
+
"contactId",
|
|
84
|
+
"conversation_id",
|
|
85
|
+
"conversationId",
|
|
86
|
+
"client_id",
|
|
87
|
+
"clientId",
|
|
88
|
+
"visitor_id",
|
|
89
|
+
"visitorId",
|
|
90
|
+
"source_id",
|
|
91
|
+
"sourceId",
|
|
92
|
+
]) ?? `event:${eventId}`;
|
|
93
|
+
const identityKey = `webhook:generic:identity:${sha256(identitySource).slice(0, 32)}`;
|
|
94
|
+
const base = {
|
|
95
|
+
type: "webhook" as const,
|
|
96
|
+
provider: "generic" as const,
|
|
97
|
+
providerEventId: eventId,
|
|
98
|
+
dedupeKey: `webhook:generic:${input.channelId}:${eventId}`,
|
|
99
|
+
externalIdentity: {
|
|
100
|
+
key: identityKey,
|
|
101
|
+
redactedDisplay: identityKey,
|
|
102
|
+
},
|
|
103
|
+
receivedAt: input.receivedAt,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
if (!text) {
|
|
107
|
+
return [
|
|
108
|
+
{
|
|
109
|
+
...base,
|
|
110
|
+
kind: "unsupported" as const,
|
|
111
|
+
unsupportedReason: "unsupported_message_type:missing_text",
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return [
|
|
117
|
+
{
|
|
118
|
+
...base,
|
|
119
|
+
kind: "message" as const,
|
|
120
|
+
message: {
|
|
121
|
+
role: "user" as const,
|
|
122
|
+
contentType: "text" as const,
|
|
123
|
+
content: text,
|
|
124
|
+
providerMessageId: readNestedMessageId(body) ?? eventId,
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
];
|
|
128
|
+
},
|
|
129
|
+
async sendMessage(input) {
|
|
130
|
+
if (isGenericWebhookOutputChannel(input.channel)) {
|
|
131
|
+
return sendGenericWebhookOutputMessage(input, input.channel);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
ok: true,
|
|
136
|
+
status: "stubbed",
|
|
137
|
+
providerRequestId: buildGenericWebhookProviderRequestId(input),
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
getStatus() {
|
|
141
|
+
return {
|
|
142
|
+
status: "connected",
|
|
143
|
+
checkedAt: new Date().toISOString(),
|
|
144
|
+
providerMetadata: {
|
|
145
|
+
outbound: "stubbed",
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
type GenericWebhookOutputRequest = {
|
|
152
|
+
method: "POST";
|
|
153
|
+
url: string;
|
|
154
|
+
headers: Record<string, string>;
|
|
155
|
+
body: Record<string, unknown>;
|
|
156
|
+
idempotencyKey: string;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
type GenericWebhookOutputFetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
160
|
+
type GenericWebhookOutputResolver = OutboundResolver;
|
|
161
|
+
type GenericWebhookOutputRequestInit = {
|
|
162
|
+
method: "POST";
|
|
163
|
+
headers: Record<string, string>;
|
|
164
|
+
body: string;
|
|
165
|
+
redirect: "manual";
|
|
166
|
+
signal: AbortSignal;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export function buildGenericWebhookOutputRequest(
|
|
170
|
+
input: ChannelSendInput,
|
|
171
|
+
channel: GenericWebhookOutputChannelConfig,
|
|
172
|
+
): GenericWebhookOutputRequest {
|
|
173
|
+
if (input.message.contentType !== "text") {
|
|
174
|
+
throw new AgentKitError("channel_payload_invalid", "Generic webhook output supports only text channel messages.");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const rawUrl = input.secrets[channel.urlSecret];
|
|
178
|
+
|
|
179
|
+
if (!rawUrl) {
|
|
180
|
+
throw new AgentKitError("channel_secret_missing", `Secret ${channel.urlSecret} is missing.`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const idempotencyKey = buildGenericWebhookOutputRequestId(input);
|
|
184
|
+
const body = {
|
|
185
|
+
event_id: idempotencyKey,
|
|
186
|
+
conversation_id: input.conversationId,
|
|
187
|
+
channel_id: input.channelId,
|
|
188
|
+
external_identity: input.externalIdentity.key,
|
|
189
|
+
message: {
|
|
190
|
+
role: "assistant",
|
|
191
|
+
content_type: "text",
|
|
192
|
+
content: input.message.content,
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
const headers: Record<string, string> = {
|
|
196
|
+
"Content-Type": "application/json",
|
|
197
|
+
Accept: "application/json",
|
|
198
|
+
"X-AgentKit-Event-ID": idempotencyKey,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
applyGenericWebhookOutputAuthHeaders({
|
|
202
|
+
auth: channel.auth,
|
|
203
|
+
secrets: input.secrets,
|
|
204
|
+
headers,
|
|
205
|
+
body: JSON.stringify(body),
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
method: "POST",
|
|
210
|
+
url: validateGenericWebhookOutputUrl(rawUrl),
|
|
211
|
+
headers,
|
|
212
|
+
body,
|
|
213
|
+
idempotencyKey,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export async function sendGenericWebhookOutputMessage(
|
|
218
|
+
input: ChannelSendInput,
|
|
219
|
+
channel: GenericWebhookOutputChannelConfig,
|
|
220
|
+
fetcher?: GenericWebhookOutputFetch,
|
|
221
|
+
resolver: GenericWebhookOutputResolver = resolveOutboundAddresses,
|
|
222
|
+
): Promise<ChannelSendResult> {
|
|
223
|
+
let request: GenericWebhookOutputRequest;
|
|
224
|
+
let resolvedUrl: ResolvedOutboundUrl;
|
|
225
|
+
// Started before DNS resolution so a stalled resolver counts against the deadline.
|
|
226
|
+
const signal = AbortSignal.timeout(15_000);
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
request = buildGenericWebhookOutputRequest(input, channel);
|
|
230
|
+
|
|
231
|
+
if (input.secrets.AGENTKIT_CHANNEL_SEND_DRY_RUN === "1") {
|
|
232
|
+
return {
|
|
233
|
+
ok: true,
|
|
234
|
+
status: "stubbed",
|
|
235
|
+
providerRequestId: request.idempotencyKey,
|
|
236
|
+
providerMetadata: redactedGenericWebhookOutputRequest(request),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
resolvedUrl = await validateGenericWebhookOutputResolvedUrl(request.url, resolver, signal);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
return genericWebhookOutputFailureFromError(error);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let response: Response;
|
|
246
|
+
const requestInit: GenericWebhookOutputRequestInit = {
|
|
247
|
+
method: request.method,
|
|
248
|
+
headers: request.headers,
|
|
249
|
+
body: JSON.stringify(request.body),
|
|
250
|
+
redirect: "manual",
|
|
251
|
+
signal,
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
response = fetcher
|
|
256
|
+
? await fetcher(resolvedUrl.href, requestInit)
|
|
257
|
+
: await pinnedHttpsFetch(resolvedUrl, requestInit, GENERIC_WEBHOOK_OUTPUT_URL_LABEL);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
if (error instanceof AgentKitError) {
|
|
260
|
+
return {
|
|
261
|
+
...genericWebhookOutputFailureFromError(error),
|
|
262
|
+
providerRequestId: request.idempotencyKey,
|
|
263
|
+
providerMetadata: redactedGenericWebhookOutputRequest(request),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
ok: false,
|
|
269
|
+
retryable: true,
|
|
270
|
+
code: "channel_provider_unavailable",
|
|
271
|
+
message: `Generic webhook output request failed before a response: ${redactGenericWebhookOutputText(
|
|
272
|
+
error instanceof Error ? error.message : String(error),
|
|
273
|
+
input.secrets,
|
|
274
|
+
)}`,
|
|
275
|
+
providerRequestId: request.idempotencyKey,
|
|
276
|
+
providerMetadata: redactedGenericWebhookOutputRequest(request),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const payload = await readGenericWebhookOutputResponseBody(response);
|
|
281
|
+
const providerMessageId = readGenericWebhookOutputProviderMessageId(payload) ?? request.idempotencyKey;
|
|
282
|
+
|
|
283
|
+
if (!response.ok) {
|
|
284
|
+
return {
|
|
285
|
+
ok: false,
|
|
286
|
+
retryable: response.status === 429 || response.status >= 500,
|
|
287
|
+
code: "channel_send_failed",
|
|
288
|
+
message: redactGenericWebhookOutputText(readGenericWebhookOutputErrorMessage(payload, response.status), input.secrets),
|
|
289
|
+
providerRequestId: request.idempotencyKey,
|
|
290
|
+
providerMetadata: {
|
|
291
|
+
...redactedGenericWebhookOutputRequest(request),
|
|
292
|
+
status: response.status,
|
|
293
|
+
body: redactGenericWebhookOutputValue(payload, input.secrets),
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
ok: true,
|
|
300
|
+
status: "sent",
|
|
301
|
+
providerRequestId: request.idempotencyKey,
|
|
302
|
+
providerMessageId,
|
|
303
|
+
providerMetadata: {
|
|
304
|
+
...redactedGenericWebhookOutputRequest(request),
|
|
305
|
+
status: response.status,
|
|
306
|
+
body: redactGenericWebhookOutputValue(payload, input.secrets),
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function buildGenericWebhookProviderRequestId(input: ChannelSendInput): string {
|
|
312
|
+
return `webhook:generic:${input.channelId}:${input.conversationId}`;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function buildGenericWebhookOutputRequestId(input: ChannelSendInput): string {
|
|
316
|
+
return `webhook:generic:${input.channelId}:${input.deliveryId ?? input.jobId ?? randomUUID()}`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function isGenericWebhookOutputChannel(value: unknown): value is GenericWebhookOutputChannelConfig {
|
|
320
|
+
return (
|
|
321
|
+
isRecord(value) &&
|
|
322
|
+
value.type === "webhook" &&
|
|
323
|
+
value.provider === "generic" &&
|
|
324
|
+
value.direction === "outbound" &&
|
|
325
|
+
typeof value.urlSecret === "string" &&
|
|
326
|
+
isRecord(value.auth)
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function applyGenericWebhookOutputAuthHeaders(input: {
|
|
331
|
+
auth: GenericWebhookOutputChannelConfig["auth"];
|
|
332
|
+
secrets: Record<string, string>;
|
|
333
|
+
headers: Record<string, string>;
|
|
334
|
+
body: string;
|
|
335
|
+
}): void {
|
|
336
|
+
if (input.auth.type === "none") {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (input.auth.type === "bearer") {
|
|
341
|
+
const token = input.secrets[input.auth.tokenSecret];
|
|
342
|
+
|
|
343
|
+
if (!token) {
|
|
344
|
+
throw new AgentKitError("channel_secret_missing", `Secret ${input.auth.tokenSecret} is missing.`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
input.headers.Authorization = `Bearer ${token}`;
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (input.auth.type === "header") {
|
|
352
|
+
const value = input.secrets[input.auth.valueSecret];
|
|
353
|
+
|
|
354
|
+
if (!value) {
|
|
355
|
+
throw new AgentKitError("channel_secret_missing", `Secret ${input.auth.valueSecret} is missing.`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
input.headers[input.auth.headerName] = value;
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const secret = input.secrets[input.auth.secret];
|
|
363
|
+
|
|
364
|
+
if (!secret) {
|
|
365
|
+
throw new AgentKitError("channel_secret_missing", `Secret ${input.auth.secret} is missing.`);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
input.headers[input.auth.headerName ?? DEFAULT_OUTPUT_HMAC_HEADER] =
|
|
369
|
+
`sha256=${createHmac("sha256", secret).update(input.body).digest("hex")}`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function genericWebhookOutputFailureFromError(error: unknown): ChannelSendResult {
|
|
373
|
+
if (error instanceof AgentKitError) {
|
|
374
|
+
if (error.code === "channel_provider_unavailable") {
|
|
375
|
+
return {
|
|
376
|
+
ok: false,
|
|
377
|
+
retryable: true,
|
|
378
|
+
code: "channel_provider_unavailable",
|
|
379
|
+
message: error.message,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
ok: false,
|
|
385
|
+
retryable: false,
|
|
386
|
+
code: error.code === "channel_secret_missing" ? "channel_secret_missing" : "channel_send_failed",
|
|
387
|
+
message: error.message,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
ok: false,
|
|
393
|
+
retryable: false,
|
|
394
|
+
code: "channel_send_failed",
|
|
395
|
+
message: error instanceof Error ? error.message : String(error),
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function validateGenericWebhookOutputUrl(rawUrl: string): string {
|
|
400
|
+
let url: URL;
|
|
401
|
+
|
|
402
|
+
try {
|
|
403
|
+
url = new URL(rawUrl);
|
|
404
|
+
} catch {
|
|
405
|
+
throw new AgentKitError("channel_payload_invalid", "Generic webhook output URL must be a valid HTTPS URL.");
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (url.protocol !== "https:") {
|
|
409
|
+
throw new AgentKitError("channel_payload_invalid", "Generic webhook output URL must use HTTPS.");
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (url.username || url.password) {
|
|
413
|
+
throw new AgentKitError("channel_payload_invalid", "Generic webhook output URL must not include credentials.");
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (isBlockedOutboundHost(url.hostname)) {
|
|
417
|
+
throw new AgentKitError("channel_payload_invalid", "Generic webhook output URL host is not allowed.");
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return url.href;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
async function validateGenericWebhookOutputResolvedUrl(
|
|
424
|
+
rawUrl: string,
|
|
425
|
+
resolver: GenericWebhookOutputResolver,
|
|
426
|
+
signal: AbortSignal,
|
|
427
|
+
): Promise<ResolvedOutboundUrl> {
|
|
428
|
+
return resolvePublicOutboundUrl(validateGenericWebhookOutputUrl(rawUrl), GENERIC_WEBHOOK_OUTPUT_URL_LABEL, resolver, signal);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
function redactedGenericWebhookOutputRequest(request: GenericWebhookOutputRequest): Record<string, unknown> {
|
|
445
|
+
return {
|
|
446
|
+
method: request.method,
|
|
447
|
+
url: redactGenericWebhookOutputUrl(request.url),
|
|
448
|
+
body: request.body,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function redactGenericWebhookOutputUrl(rawUrl: string): string {
|
|
453
|
+
try {
|
|
454
|
+
const url = new URL(rawUrl);
|
|
455
|
+
url.search = url.search ? "?<redacted>" : "";
|
|
456
|
+
return url.href;
|
|
457
|
+
} catch {
|
|
458
|
+
return "<redacted>";
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async function readGenericWebhookOutputResponseBody(response: Response): Promise<unknown> {
|
|
463
|
+
const text = await readGenericWebhookOutputResponseText(response, MAX_OUTPUT_RESPONSE_BYTES);
|
|
464
|
+
|
|
465
|
+
if (!text) {
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
try {
|
|
470
|
+
return JSON.parse(text) as unknown;
|
|
471
|
+
} catch {
|
|
472
|
+
return text;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async function readGenericWebhookOutputResponseText(response: Response, maxBytes: number): Promise<string> {
|
|
477
|
+
const reader = response.body?.getReader();
|
|
478
|
+
|
|
479
|
+
if (!reader) {
|
|
480
|
+
return "";
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const chunks: Uint8Array[] = [];
|
|
484
|
+
let totalBytes = 0;
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
while (true) {
|
|
488
|
+
const chunk = await reader.read();
|
|
489
|
+
|
|
490
|
+
if (chunk.done) {
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
totalBytes += chunk.value.byteLength;
|
|
495
|
+
|
|
496
|
+
if (totalBytes > maxBytes) {
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
chunks.push(chunk.value);
|
|
501
|
+
}
|
|
502
|
+
} finally {
|
|
503
|
+
reader.releaseLock();
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return new TextDecoder().decode(concatUint8Arrays(chunks));
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function concatUint8Arrays(chunks: Uint8Array[]): Uint8Array {
|
|
510
|
+
const totalBytes = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
|
|
511
|
+
const output = new Uint8Array(totalBytes);
|
|
512
|
+
let offset = 0;
|
|
513
|
+
|
|
514
|
+
for (const chunk of chunks) {
|
|
515
|
+
output.set(chunk, offset);
|
|
516
|
+
offset += chunk.byteLength;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return output;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function readGenericWebhookOutputProviderMessageId(payload: unknown): string | undefined {
|
|
523
|
+
if (!isRecord(payload)) {
|
|
524
|
+
return undefined;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return readFirstScalar(payload, ["message_id", "messageId", "event_id", "eventId", "id"]);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function readGenericWebhookOutputErrorMessage(payload: unknown, status: number): string {
|
|
531
|
+
if (isRecord(payload)) {
|
|
532
|
+
const error = payload.error;
|
|
533
|
+
|
|
534
|
+
if (typeof error === "string") {
|
|
535
|
+
return error;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (isRecord(error)) {
|
|
539
|
+
const message = readFirstScalar(error, ["message", "detail", "code"]);
|
|
540
|
+
|
|
541
|
+
if (message) {
|
|
542
|
+
return message;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const message = readFirstScalar(payload, ["message", "detail", "code"]);
|
|
547
|
+
|
|
548
|
+
if (message) {
|
|
549
|
+
return message;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (typeof payload === "string" && payload.trim()) {
|
|
554
|
+
return payload.trim();
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return `Generic webhook output returned HTTP ${status}.`;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function redactGenericWebhookOutputText(value: string, secrets: Record<string, string>): string {
|
|
561
|
+
let redacted = value;
|
|
562
|
+
|
|
563
|
+
for (const secret of Object.values(secrets)) {
|
|
564
|
+
if (secret) {
|
|
565
|
+
redacted = redacted.split(secret).join("<redacted>");
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return redacted;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function redactGenericWebhookOutputValue(value: unknown, secrets: Record<string, string>): unknown {
|
|
573
|
+
if (typeof value === "string") {
|
|
574
|
+
return redactGenericWebhookOutputText(value, secrets);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (Array.isArray(value)) {
|
|
578
|
+
return value.map((item) => redactGenericWebhookOutputValue(item, secrets));
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (isRecord(value)) {
|
|
582
|
+
const redacted: Record<string, unknown> = {};
|
|
583
|
+
|
|
584
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
585
|
+
redacted[key] = /secret|token|authorization|password|api[_-]?key/i.test(key)
|
|
586
|
+
? "<redacted>"
|
|
587
|
+
: redactGenericWebhookOutputValue(entry, secrets);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return redacted;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return value;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function parseJsonObject(input: RawWebhookEvent): Record<string, unknown> {
|
|
597
|
+
const contentType = input.headers.get("content-type") ?? "";
|
|
598
|
+
|
|
599
|
+
if (contentType && !contentType.toLowerCase().includes("application/json")) {
|
|
600
|
+
throw new AgentKitError("channel_payload_invalid", "Generic webhook payload must use content-type application/json.");
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
try {
|
|
604
|
+
const parsed = JSON.parse(input.rawBody) as unknown;
|
|
605
|
+
|
|
606
|
+
if (isRecord(parsed)) {
|
|
607
|
+
return parsed;
|
|
608
|
+
}
|
|
609
|
+
} catch (error) {
|
|
610
|
+
throw new AgentKitError("channel_payload_invalid", "Generic webhook payload must be valid JSON.", {
|
|
611
|
+
cause: error,
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
throw new AgentKitError("channel_payload_invalid", "Generic webhook payload must be a JSON object.");
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function readMessageText(body: Record<string, unknown>): string | undefined {
|
|
619
|
+
const message = body.message;
|
|
620
|
+
|
|
621
|
+
if (typeof message === "string") {
|
|
622
|
+
return normalizeNonEmpty(message);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (isRecord(message)) {
|
|
626
|
+
return readFirstScalar(message, ["text", "content", "body"]);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return readFirstScalar(body, ["text", "content", "prompt", "input"]);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function readNestedMessageId(body: Record<string, unknown>): string | undefined {
|
|
633
|
+
const message = body.message;
|
|
634
|
+
return isRecord(message) ? readFirstScalar(message, ["id", "message_id", "messageId"]) : undefined;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function readFirstScalar(value: Record<string, unknown>, keys: string[]): string | undefined {
|
|
638
|
+
for (const key of keys) {
|
|
639
|
+
const field = value[key];
|
|
640
|
+
|
|
641
|
+
if (typeof field === "string") {
|
|
642
|
+
const normalized = normalizeNonEmpty(field);
|
|
643
|
+
|
|
644
|
+
if (normalized) {
|
|
645
|
+
return normalized;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (typeof field === "number" && Number.isFinite(field)) {
|
|
650
|
+
return String(field);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
return undefined;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function normalizeNonEmpty(value: string): string | undefined {
|
|
658
|
+
const trimmed = value.trim();
|
|
659
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function readBearerToken(value: string | null): string {
|
|
663
|
+
if (!value?.startsWith("Bearer ")) {
|
|
664
|
+
return "";
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return value.slice("Bearer ".length).trim();
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function constantTimeEqual(left: string, right: string): boolean {
|
|
671
|
+
const leftDigest = createHash("sha256").update(left).digest();
|
|
672
|
+
const rightDigest = createHash("sha256").update(right).digest();
|
|
673
|
+
return timingSafeEqual(leftDigest, rightDigest) && left === right;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function sha256(value: string): string {
|
|
677
|
+
return createHash("sha256").update(value).digest("hex");
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
681
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
682
|
+
}
|