/** * Per-channel and per-DM Slack workspace routing. * * Routing is decided BEFORE any agent runs, from durable facts only, so it can * never be a model judgement. The resolver is pure with respect to the database * reads it is handed: it returns a decision, and the caller persists and acts on * it. * * Two rules are load-bearing and easy to break by accident: * * A mapped thread keeps the workspace it was created in, unconditionally, * including after the channel has been re-pointed. Otherwise a live * conversation would silently change tenants mid-thread. * * A subject who lacks access to the routed workspace is NEVER quietly served * from another workspace they happen to belong to. Every failure is explicit * and creates no session. */ import type { AccessGrant } from "@opengeni/contracts"; import type { SlackChannelRoute, SlackInteractionInboxEntry, SlackRoutableWorkspace, SlackUserDmRoute } from "@opengeni/db"; export type SlackRouteTenancy = { accountId: string; workspaceId: string; }; export type SlackRouteResolution = (SlackRouteTenancy & { kind: "resolved"; label: string | null; source: "thread" | "prefix" | "channel" | "dm_route" | "dm_personal" | "sole_candidate" | "installation"; }) | { kind: "ask"; candidates: readonly SlackRoutableWorkspace[]; } | { kind: "denied"; reason: "no_access_to_named" | "no_access_to_route" | "no_candidates"; requested: string | null; candidates: readonly SlackRoutableWorkspace[]; }; /** * A Slack direct message to the bot, by any trigger that can carry one. * * `isDirectMessageShortcut` is deliberately narrower: it means "a message * shortcut invoked inside a DM", which is a private-handoff concern. Routing * cares about the broader question of whether this conversation is one human's * private channel with the bot, which is also true of an ordinary `dm` event. */ export declare function isSlackDirectMessageConversation(entry: Pick): boolean; /** * Split a leading bot mention off the message text. * * Slack delivers an `app_mention` with the mention still in the text * (`<@U123> do the thing`), and OpenGeni stores it verbatim. The workspace * prefix is only an override when it is the first thing the person typed, so it * is parsed after the mention rather than at byte 0 of the raw text. Everything * split off here is put back, so the message the model sees is unchanged apart * from the addressing the person used to route it. */ export declare function splitSlackLeadingMention(text: string, botUserId: string | null): { lead: string; rest: string; }; /** * The strict `in : ...` override. * * Parsed only at byte 0, so ordinary prose that happens to contain the word * cannot trigger it, and matched case-insensitively against the exact label of a * workspace the subject can already start work in. A prefix that names nothing * recognizable is NOT a suggestion that falls through: it is a refusal, because * silently ignoring an explicit override is how a message lands somewhere the * person did not intend. */ export declare function parseSlackWorkspacePrefix(text: string): { requested: string; remainder: string; } | null; export type SlackRouteInputs = { /** The installation binding's tenancy. Always the fallback, never a silent one. */ home: SlackRouteTenancy; entry: Pick; /** * The tenancy of an interaction already mapped to this thread, if any. A * mapped thread wins unconditionally. */ threadTenancy: SlackRouteTenancy | null; channelRoute: SlackChannelRoute | null; dmRoute: SlackUserDmRoute | null; /** The subject's own personal workspace in the home organization, if any. */ personalWorkspaceId: string | null; /** Workspaces this subject may actually start work in, ordered stably. */ candidates: readonly SlackRoutableWorkspace[]; /** The installation's bot user, so a mention does not hide the prefix. */ botUserId: string | null; /** `OPENGENI_SLACK_WORKSPACE_ROUTING_ENABLED`. */ routingEnabled: boolean; /** Whether the first-use picker exists yet. Until it does, ambiguity keeps home. */ askEnabled: boolean; }; /** * Decide which workspace this Slack message starts work in. * * First match wins, and the order is deliberate: continuity beats an explicit * override beats configuration beats derivation beats asking. */ export declare function resolveSlackWorkspaceRoute(input: SlackRouteInputs): SlackRouteResolution; /** * The message text the model actually sees. * * A prefix override is addressing information, not part of the request, so it is * stripped exactly once and only when it matched. */ export declare function slackRoutedRequestText(text: string, resolution: SlackRouteResolution, botUserId: string | null): string; export type SlackRouteAuthorization = { kind: "authorized"; grant: AccessGrant; tenancy: SlackRouteTenancy; label: string | null; } | { kind: "denied"; reason: "no_access_to_route"; tenancy: SlackRouteTenancy; };