fix(sdk,slack): correct GitLab verifier and Slack secret provisioning, skip unroutable channel turns

- webhooks.gitlab used the Svix preset (svix-id/svix-timestamp/svix-signature,
  HMAC over {id}.{timestamp}.{body}), but GitLab doesn't sign bodies: it echoes
  the configured token in X-Gitlab-Token. Every GitLab delivery failed
  verification. Switched it to the shared-secret scheme (header, x-gitlab-token).

- The Slack connector declared secretProvisioning 'integrator', so the Connect
  panel offered a Generate-secret button, but Slack mints its own Signing Secret
  that must be pasted (core already classifies slack as 'provider'). Changed to
  'provider'.

- A verified channel delivery naming a connector the running agent doesn't have
  fell through to a normal turn with an empty incoming message, so the agent
  answered nothing and burned tokens. Handle the unresolved-connector case like a
  stale interaction: warn with the connectorId/deliveryId and skip the turn
  without consuming a turn.
This commit is contained in:
Eric Allam
2026-08-16 23:57:57 +01:00
parent f8d545956e
commit c2f761a289
3 changed files with 22 additions and 5 deletions
+1 -1
View File
@@ -145,7 +145,7 @@ export function slack<TEvent = SlackMessageEvent>(
const source: WebhookSource<TEvent> = {
provider: "slack",
verifier: { kind: "config", config: SLACK_VERIFIER, handshake: SLACK_HANDSHAKE },
secretProvisioning: "integrator",
secretProvisioning: "provider",
};
const messageFilter = options.filter
? `${SELF_MESSAGE_GUARD} && (${options.filter})`
+17 -3
View File
@@ -6972,6 +6972,7 @@ function chatAgent<
let channelAckRef: string | undefined;
let channelStreamEditor: ChannelStreamEditor | undefined;
let droppedStaleInteraction = false;
let droppedUnknownConnector = false;
let channelFinalAnswerPosted = false;
try {
// Extract turn-level context before entering the span. Slim
@@ -7085,6 +7086,15 @@ function chatAgent<
});
}
}
} else {
droppedUnknownConnector = true;
logger.warn(
"chat.agent: no channel connector matches this delivery; skipping turn",
{
connectorId: wireChannelEvent.connectorId,
deliveryId: wireChannelEvent.deliveryId,
}
);
}
}
const incomingMessages: TUIMessage[] = effectiveIncomingMessage
@@ -7334,7 +7344,11 @@ function chatAgent<
// snapshot + `session.out` replay (or `hydrateMessages`,
// which also fires per-turn below). Per-turn handling is
// therefore a delta merge, not a full-history reset.
if (currentWirePayload.trigger !== "action" && !droppedStaleInteraction) {
if (
currentWirePayload.trigger !== "action" &&
!droppedStaleInteraction &&
!droppedUnknownConnector
) {
let cleanedUIMessages: TUIMessage[] = cleanedIncomingMessages;
// Turn-0 head-start with hydrateMessages: the boot seeding from
@@ -7633,12 +7647,12 @@ function chatAgent<
turn--;
}
if (droppedStaleInteraction) {
if (droppedStaleInteraction || droppedUnknownConnector) {
msgSub.off();
turn--;
}
if (!isAction && !droppedStaleInteraction) {
if (!isAction && !droppedStaleInteraction && !droppedUnknownConnector) {
// Mint a scoped public access token once per turn, reused for
// onChatStart, onTurnStart, onTurnComplete, and the turn-complete chunk.
const currentRunId = ctx.run.id;
+4 -1
View File
@@ -163,7 +163,10 @@ export const webhookSources = {
gitlab<T = unknown>(): WebhookSource<T> {
return {
provider: "gitlab",
verifier: { kind: "preset", preset: "svix", config: svixVerifierConfig() },
verifier: {
kind: "config",
config: { scheme: "shared-secret", placement: "header", fieldName: "x-gitlab-token" },
},
secretProvisioning: "integrator",
};
},