@open-everywhere/provider-slack 0.1.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/LICENSE +201 -0
- package/NOTICE +21 -0
- package/README.md +47 -0
- package/agent/access/slack-attachment-fetch.ts +168 -0
- package/agent/app-manifest.ts +149 -0
- package/agent/channels/slack-approval-card.ts +275 -0
- package/agent/channels/slack-attachments.ts +195 -0
- package/agent/channels/slack-commands.ts +227 -0
- package/agent/channels/slack-context.ts +185 -0
- package/agent/channels/slack-events.ts +5406 -0
- package/agent/channels/slack-front.ts +2349 -0
- package/agent/channels/slack-identity.ts +520 -0
- package/agent/channels/slack-input-response.ts +162 -0
- package/agent/channels/slack-mrkdwn.ts +61 -0
- package/agent/channels/slack-origin-codec.ts +62 -0
- package/agent/channels/slack-post-arm.ts +314 -0
- package/agent/channels/slack-read.ts +248 -0
- package/agent/channels/slack-route-hold.ts +107 -0
- package/agent/channels/slack-shortcut.ts +336 -0
- package/agent/channels/slack-signature.ts +53 -0
- package/agent/channels/slack-spend-gate.ts +62 -0
- package/agent/channels/slack-tenant-fence.ts +99 -0
- package/agent/channels/slack-wake-backscroll.ts +604 -0
- package/agent/channels/slack-web-api.ts +178 -0
- package/agent/channels/slack-webhook-verifier.ts +116 -0
- package/agent/channels/slack.ts +1266 -0
- package/agent/contributions.ts +32 -0
- package/agent/docs-generated.ts +15 -0
- package/agent/ops.ts +50 -0
- package/agent/provider-events.ts +91 -0
- package/agent/register.ts +144 -0
- package/agent/surface-parity.ts +99 -0
- package/dist/agent/access/slack-attachment-fetch.d.ts +15 -0
- package/dist/agent/access/slack-attachment-fetch.d.ts.map +1 -0
- package/dist/agent/access/slack-attachment-fetch.js +151 -0
- package/dist/agent/access/slack-attachment-fetch.js.map +1 -0
- package/dist/agent/app-manifest.d.ts +15 -0
- package/dist/agent/app-manifest.d.ts.map +1 -0
- package/dist/agent/app-manifest.js +141 -0
- package/dist/agent/app-manifest.js.map +1 -0
- package/dist/agent/channels/slack-approval-card.d.ts +71 -0
- package/dist/agent/channels/slack-approval-card.d.ts.map +1 -0
- package/dist/agent/channels/slack-approval-card.js +232 -0
- package/dist/agent/channels/slack-approval-card.js.map +1 -0
- package/dist/agent/channels/slack-attachments.d.ts +66 -0
- package/dist/agent/channels/slack-attachments.d.ts.map +1 -0
- package/dist/agent/channels/slack-attachments.js +174 -0
- package/dist/agent/channels/slack-attachments.js.map +1 -0
- package/dist/agent/channels/slack-commands.d.ts +27 -0
- package/dist/agent/channels/slack-commands.d.ts.map +1 -0
- package/dist/agent/channels/slack-commands.js +208 -0
- package/dist/agent/channels/slack-commands.js.map +1 -0
- package/dist/agent/channels/slack-context.d.ts +26 -0
- package/dist/agent/channels/slack-context.d.ts.map +1 -0
- package/dist/agent/channels/slack-context.js +178 -0
- package/dist/agent/channels/slack-context.js.map +1 -0
- package/dist/agent/channels/slack-events.d.ts +1654 -0
- package/dist/agent/channels/slack-events.d.ts.map +1 -0
- package/dist/agent/channels/slack-events.js +4240 -0
- package/dist/agent/channels/slack-events.js.map +1 -0
- package/dist/agent/channels/slack-front.d.ts +387 -0
- package/dist/agent/channels/slack-front.d.ts.map +1 -0
- package/dist/agent/channels/slack-front.js +1958 -0
- package/dist/agent/channels/slack-front.js.map +1 -0
- package/dist/agent/channels/slack-identity.d.ts +245 -0
- package/dist/agent/channels/slack-identity.d.ts.map +1 -0
- package/dist/agent/channels/slack-identity.js +405 -0
- package/dist/agent/channels/slack-identity.js.map +1 -0
- package/dist/agent/channels/slack-input-response.d.ts +26 -0
- package/dist/agent/channels/slack-input-response.d.ts.map +1 -0
- package/dist/agent/channels/slack-input-response.js +127 -0
- package/dist/agent/channels/slack-input-response.js.map +1 -0
- package/dist/agent/channels/slack-mrkdwn.d.ts +3 -0
- package/dist/agent/channels/slack-mrkdwn.d.ts.map +1 -0
- package/dist/agent/channels/slack-mrkdwn.js +53 -0
- package/dist/agent/channels/slack-mrkdwn.js.map +1 -0
- package/dist/agent/channels/slack-origin-codec.d.ts +7 -0
- package/dist/agent/channels/slack-origin-codec.d.ts.map +1 -0
- package/dist/agent/channels/slack-origin-codec.js +55 -0
- package/dist/agent/channels/slack-origin-codec.js.map +1 -0
- package/dist/agent/channels/slack-post-arm.d.ts +54 -0
- package/dist/agent/channels/slack-post-arm.d.ts.map +1 -0
- package/dist/agent/channels/slack-post-arm.js +268 -0
- package/dist/agent/channels/slack-post-arm.js.map +1 -0
- package/dist/agent/channels/slack-read.d.ts +84 -0
- package/dist/agent/channels/slack-read.d.ts.map +1 -0
- package/dist/agent/channels/slack-read.js +205 -0
- package/dist/agent/channels/slack-read.js.map +1 -0
- package/dist/agent/channels/slack-route-hold.d.ts +34 -0
- package/dist/agent/channels/slack-route-hold.d.ts.map +1 -0
- package/dist/agent/channels/slack-route-hold.js +97 -0
- package/dist/agent/channels/slack-route-hold.js.map +1 -0
- package/dist/agent/channels/slack-shortcut.d.ts +162 -0
- package/dist/agent/channels/slack-shortcut.d.ts.map +1 -0
- package/dist/agent/channels/slack-shortcut.js +262 -0
- package/dist/agent/channels/slack-shortcut.js.map +1 -0
- package/dist/agent/channels/slack-signature.d.ts +19 -0
- package/dist/agent/channels/slack-signature.d.ts.map +1 -0
- package/dist/agent/channels/slack-signature.js +50 -0
- package/dist/agent/channels/slack-signature.js.map +1 -0
- package/dist/agent/channels/slack-spend-gate.d.ts +22 -0
- package/dist/agent/channels/slack-spend-gate.d.ts.map +1 -0
- package/dist/agent/channels/slack-spend-gate.js +48 -0
- package/dist/agent/channels/slack-spend-gate.js.map +1 -0
- package/dist/agent/channels/slack-tenant-fence.d.ts +61 -0
- package/dist/agent/channels/slack-tenant-fence.d.ts.map +1 -0
- package/dist/agent/channels/slack-tenant-fence.js +88 -0
- package/dist/agent/channels/slack-tenant-fence.js.map +1 -0
- package/dist/agent/channels/slack-wake-backscroll.d.ts +188 -0
- package/dist/agent/channels/slack-wake-backscroll.d.ts.map +1 -0
- package/dist/agent/channels/slack-wake-backscroll.js +473 -0
- package/dist/agent/channels/slack-wake-backscroll.js.map +1 -0
- package/dist/agent/channels/slack-web-api.d.ts +46 -0
- package/dist/agent/channels/slack-web-api.d.ts.map +1 -0
- package/dist/agent/channels/slack-web-api.js +147 -0
- package/dist/agent/channels/slack-web-api.js.map +1 -0
- package/dist/agent/channels/slack-webhook-verifier.d.ts +5 -0
- package/dist/agent/channels/slack-webhook-verifier.d.ts.map +1 -0
- package/dist/agent/channels/slack-webhook-verifier.js +112 -0
- package/dist/agent/channels/slack-webhook-verifier.js.map +1 -0
- package/dist/agent/channels/slack.d.ts +37 -0
- package/dist/agent/channels/slack.d.ts.map +1 -0
- package/dist/agent/channels/slack.js +1169 -0
- package/dist/agent/channels/slack.js.map +1 -0
- package/dist/agent/contributions.d.ts +17 -0
- package/dist/agent/contributions.d.ts.map +1 -0
- package/dist/agent/contributions.js +32 -0
- package/dist/agent/contributions.js.map +1 -0
- package/dist/agent/docs-generated.d.ts +3 -0
- package/dist/agent/docs-generated.d.ts.map +1 -0
- package/dist/agent/docs-generated.js +13 -0
- package/dist/agent/docs-generated.js.map +1 -0
- package/dist/agent/ops.d.ts +3 -0
- package/dist/agent/ops.d.ts.map +1 -0
- package/dist/agent/ops.js +38 -0
- package/dist/agent/ops.js.map +1 -0
- package/dist/agent/provider-events.d.ts +3 -0
- package/dist/agent/provider-events.d.ts.map +1 -0
- package/dist/agent/provider-events.js +84 -0
- package/dist/agent/provider-events.js.map +1 -0
- package/dist/agent/register.d.ts +5 -0
- package/dist/agent/register.d.ts.map +1 -0
- package/dist/agent/register.js +118 -0
- package/dist/agent/register.js.map +1 -0
- package/dist/agent/surface-parity.d.ts +3 -0
- package/dist/agent/surface-parity.d.ts.map +1 -0
- package/dist/agent/surface-parity.js +95 -0
- package/dist/agent/surface-parity.js.map +1 -0
- package/package.json +209 -0
- package/tsconfig.build.json +22 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,1654 @@
|
|
|
1
|
+
import { resolvePartition, type AttachSessionForCancel, type SeatCatchUpRequest, type SeatCatchUpResult, type ExternalPolicy, type IngestedChatMessage, PREFER_NO_REPLY_TOOL, type Partition, type BindableContext, type SingleWindowMentionPolicy, type UnifiedWindowWake, type ReplyDecision, type EngagementRow, type RoomRoster, type ContextLine, type WakeAttachment, type WakeMessageInput, type WakeParticipant, type WakeTrust, type NormalizedWake, type WakeEngagementTier } from "@open-everywhere/core/agent/adapter-api.js";
|
|
2
|
+
import { type SpendGateDecision } from "./slack-spend-gate.js";
|
|
3
|
+
import { type WakeBackscrollLines } from "./slack-wake-backscroll.js";
|
|
4
|
+
export { PREFER_NO_REPLY_TOOL };
|
|
5
|
+
import { type E2eDriverIdentity, type FinalMessageAction, type ReactionSinkDeps, type ReactionIngestOutcome, type ReactionVerb } from "@open-everywhere/core/agent/adapter-api.js";
|
|
6
|
+
/**
|
|
7
|
+
* Minimal Slack `event_callback` envelope shape (the subset the parser reads). Mirrors
|
|
8
|
+
* Eve's internal `SlackEventCallback` without depending on a type it doesn't re-export.
|
|
9
|
+
*/
|
|
10
|
+
export interface SlackEventEnvelope {
|
|
11
|
+
readonly type?: string;
|
|
12
|
+
readonly team_id?: string;
|
|
13
|
+
/** The Enterprise Grid organisation the event's workspace belongs to (`E…`). Slack sends it on the
|
|
14
|
+
* ENVELOPE, beside `team_id`, and only for a Grid install (OPE-1508). */
|
|
15
|
+
readonly enterprise_id?: string;
|
|
16
|
+
readonly event?: Record<string, unknown> & {
|
|
17
|
+
readonly type?: string;
|
|
18
|
+
};
|
|
19
|
+
readonly [key: string]: unknown;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The Enterprise Grid organisation id an event envelope names, or `undefined` (OPE-1508).
|
|
23
|
+
*
|
|
24
|
+
* 🔴 THE ENVELOPE IS THIS FUNCTION'S ONLY SOURCE. Slack puts `enterprise_id` beside `team_id` on the
|
|
25
|
+
* `event_callback` envelope and does NOT repeat it on the inner message object, so an ingress that
|
|
26
|
+
* only ever holds an inner event resolves nothing HERE and must carry the rung another way. Every
|
|
27
|
+
* ingress now carries it, and each reads exactly one source:
|
|
28
|
+
* 1. the wake paths driven from THIS file's own parses, and the group-DM/DM wake in
|
|
29
|
+
* `slack-front.ts` — they hold the envelope, so they call this function;
|
|
30
|
+
* 2. the NATIVE MENTION forward, the common ingress. `slack-front.ts` forwards an ordinary
|
|
31
|
+
* `app_mention` to eve's native channel handler (the single-window divert is the exception),
|
|
32
|
+
* and eve hands that handler a `SlackMessage` whose `raw` is the inner event, with the envelope
|
|
33
|
+
* already discarded. So the front route calls this function and stows the answer around the
|
|
34
|
+
* forward (`withForwardedSlackEnvelope`, slack-identity.ts), and `slack.ts`'s `authFromFacts`
|
|
35
|
+
* reads it back for that one message (OPE-1559);
|
|
36
|
+
* 3. the SHORTCUT ingress. A shortcut arrives on the interactivity route and has no event envelope
|
|
37
|
+
* at all, so `slack-shortcut.ts` reads the payload's own `enterprise.id` OR `team.enterprise_id`
|
|
38
|
+
* — Slack's two spellings for the same rung, one per install shape — into
|
|
39
|
+
* `ParsedSlackShortcut.enterpriseId`, and `shortcutMessageEvent` spreads it onto the
|
|
40
|
+
* `ParsedMessageEvent` (OPE-1559);
|
|
41
|
+
* 4. the `/oe` SLASH COMMAND door, which has its own route and its own urlencoded body.
|
|
42
|
+
* `slack-commands.ts` reads the `enterprise_id` FORM FIELD straight into the bindable context
|
|
43
|
+
* it hands `bindSlack` (OPE-1559).
|
|
44
|
+
* All four end at the same `enterprise_id` auth attribute, so one operator declaration answers for
|
|
45
|
+
* every door. Each source describes the ROOM's Grid; none of them reads the author's.
|
|
46
|
+
*
|
|
47
|
+
* The AUTHOR leg does not depend on any of this: it reads the author's own workspace and Grid from
|
|
48
|
+
* `users.info` ({@link slackAuthorIsMember}), so who counts as a member is decided the same way on
|
|
49
|
+
* every ingress.
|
|
50
|
+
*
|
|
51
|
+
* A non-string or empty value is `undefined` — the same defensive read `team_id` gets, because both
|
|
52
|
+
* are Slack's raw JSON.
|
|
53
|
+
*/
|
|
54
|
+
export declare function slackEnterpriseIdFromEnvelope(envelope: SlackEventEnvelope): string | undefined;
|
|
55
|
+
/** A Slack `message` event narrowed to the fields the wake path needs. */
|
|
56
|
+
export interface ParsedMessageEvent {
|
|
57
|
+
readonly channelId: string;
|
|
58
|
+
/** The message ts. */
|
|
59
|
+
readonly ts: string;
|
|
60
|
+
/** Thread root ts — present only for a threaded reply (engagement is per-thread). */
|
|
61
|
+
readonly threadTs?: string;
|
|
62
|
+
readonly teamId?: string;
|
|
63
|
+
/** The Enterprise Grid organisation the event's workspace sits in, when the envelope named one
|
|
64
|
+
* ({@link slackEnterpriseIdFromEnvelope}). The trust walk's rung above the workspace. */
|
|
65
|
+
readonly enterpriseId?: string;
|
|
66
|
+
readonly userId?: string;
|
|
67
|
+
readonly botId?: string;
|
|
68
|
+
readonly subtype?: string;
|
|
69
|
+
readonly channelType?: string;
|
|
70
|
+
/** Plain text (mrkdwn) of the reply: the author's own text, plus any QUOTED SLACK MESSAGE
|
|
71
|
+
* Slack parked in the message's legacy `attachments[]` — a forward, a share, or a message
|
|
72
|
+
* permalink unfurl — appended under it ({@link resolveSlackInboundText}).
|
|
73
|
+
*
|
|
74
|
+
* The de-dup unit is one EXTRACTED FIELD (an attachment's `text`, a block's line), never a
|
|
75
|
+
* physical line: a field is kept or dropped whole. A field is dropped when the author's own
|
|
76
|
+
* text already carries it as a phrase, or when the SAME attachment restated it. Two
|
|
77
|
+
* DIFFERENT attachments carrying the same field both survive — that is two quoted messages
|
|
78
|
+
* that agree, not a duplicate. A message with no quoted-message attachment is unchanged. */
|
|
79
|
+
readonly text: string;
|
|
80
|
+
/**
|
|
81
|
+
* The author's OWN typed text — `inner.text` verbatim, with no quoted content merged in.
|
|
82
|
+
*
|
|
83
|
+
* Use this, never {@link text}, for any rule that asks "what did this person SAY?" — the
|
|
84
|
+
* mention-sibling suppression and the name-drop wake needle both do. Reading the merged
|
|
85
|
+
* `text` there makes the agent act on words the author never typed: a forward of a message
|
|
86
|
+
* that @mentions the bot would be suppressed as a mention sibling, and a forward of a message
|
|
87
|
+
* naming the agent would wake it though nobody named it here. See the suppression in
|
|
88
|
+
* slack-front.ts for what is measured and what is assumed about Slack's `app_mention`.
|
|
89
|
+
*/
|
|
90
|
+
readonly authoredText: string;
|
|
91
|
+
/**
|
|
92
|
+
* Pre-edit text, from Slack's `previous_message` — present ONLY for a `message_changed`
|
|
93
|
+
* edit (undefined otherwise, including a message_changed missing the field). Feeds the
|
|
94
|
+
* wake envelope's `<previous-body>` (wake-envelope.ts) so a judged turn can diff old↔new.
|
|
95
|
+
*/
|
|
96
|
+
readonly previousText?: string;
|
|
97
|
+
/** True when the text @mentions the bot — those go through the normal mention path. */
|
|
98
|
+
readonly isAppMention: boolean;
|
|
99
|
+
/** Image/file attachment REFERENCES parsed off Slack's `files[]` (docs Part B) — surfaced in
|
|
100
|
+
* the wake envelope so the agent knows an image exists and can `describe_image` it. Empty/
|
|
101
|
+
* absent when the message carries no files. Never the bytes — just id/name/mimetype/url. */
|
|
102
|
+
readonly attachments?: readonly WakeAttachment[];
|
|
103
|
+
}
|
|
104
|
+
/** Parse Slack's `files[]` (on a message event's inner message) into attachment references.
|
|
105
|
+
* Only well-formed entries survive; an image mimetype ⇒ kind "image" (describable), else
|
|
106
|
+
* "file" (referenced, not yet readable). Pure; tolerant of Slack's loose shapes. */
|
|
107
|
+
export declare function parseSlackFiles(files: unknown): WakeAttachment[];
|
|
108
|
+
/**
|
|
109
|
+
* The text the model should see for one inbound Slack message: the author's own `text`, plus
|
|
110
|
+
* any QUOTED SLACK MESSAGE Slack put in `attachments[]` (a forward, a share, or a message
|
|
111
|
+
* permalink unfurl — see {@link hasSharedMessageAttachment}, which deliberately EXCLUDES a
|
|
112
|
+
* plain link unfurl) appended under it. A quoted field the author already typed is dropped, as is a field
|
|
113
|
+
* one attachment restates within itself. A field two DIFFERENT attachments carry is kept twice
|
|
114
|
+
* — that is two quoted messages that happen to agree, not a duplicate.
|
|
115
|
+
*
|
|
116
|
+
* IDENTITY on the common path: a message with no legacy `attachments[]`, and one whose
|
|
117
|
+
* attachments yield no new line, return the SAME `text` string unchanged. Only a message
|
|
118
|
+
* that actually carries unseen quoted content grows.
|
|
119
|
+
*
|
|
120
|
+
* 🔴 TRUST: the quoted content is merged INTO the author's body, so it INHERITS the forwarder's
|
|
121
|
+
* trust tier — it does not carry its own. `authorTrustAdmitsUnfencedBody` (wake-envelope.ts)
|
|
122
|
+
* keys on the author, so a vouched member's forward renders `from="human" trust="peer"` and the
|
|
123
|
+
* whole body is delivered UNFENCED: no `<untrusted_external_data>` wrapper, and no attribution
|
|
124
|
+
* separating the forwarder's words from the quoted author's. A member can therefore hand the
|
|
125
|
+
* model an outside author's words under their own standing. The body is still XML-escaped, so
|
|
126
|
+
* this is a standing question, not a structural one. It IS a widening of what a forward can put in
|
|
127
|
+
* front of the model, and it is deliberate. Attributing the quoted block to its own author is the
|
|
128
|
+
* improvement this leaves on the table.
|
|
129
|
+
*
|
|
130
|
+
* 🔴 THE SAME BOUNDARY NOW HOLDS ON BOTH PATHS (OPE-1380). eve's own inbound path merges legacy
|
|
131
|
+
* attachments too (`inbound.js` calls `resolveSlackInboundMrkdwn`), and it applies no such filter —
|
|
132
|
+
* so the `@mention` forward used to admit the plain LINK unfurl this function refuses.
|
|
133
|
+
* {@link stripUnfencedSlackAttachments} removes those attachments from the body before the forward,
|
|
134
|
+
* which is how the native path is held to `hasSharedMessageAttachment` without reaching inside eve.
|
|
135
|
+
* A quoted Slack message still reaches the model on both paths; scraped page text now reaches it on
|
|
136
|
+
* neither.
|
|
137
|
+
*/
|
|
138
|
+
/**
|
|
139
|
+
* Drop from a RAW webhook body every legacy attachment that is not a quoted Slack message, so the
|
|
140
|
+
* NATIVE forward path obeys the same boundary the diverted path does (OPE-1380).
|
|
141
|
+
*
|
|
142
|
+
* 🔴 WHY THIS EXISTS AT ALL. OPE-1374 fenced OUR path: {@link resolveSlackInboundText} reads only
|
|
143
|
+
* attachments {@link hasSharedMessageAttachment} admits, so a plain LINK unfurl's scraped page text
|
|
144
|
+
* never reaches the model. A direct `@mention` does not take that path — `slack-front.ts` forwards
|
|
145
|
+
* the request to eve's native `slackChannel`, whose own extractor
|
|
146
|
+
* (`dist/src/public/channels/slack/inbound-content.js`, `resolveSlackInboundMrkdwn`) flattens EVERY
|
|
147
|
+
* legacy attachment into the message body. It uses `hasSharedMessageAttachment` only to pick a merge
|
|
148
|
+
* STRATEGY, never to decide what may be extracted. So the same link unfurl the wake path refuses was
|
|
149
|
+
* merged, unfenced and untainted, into a `trust="peer"` member message on the mention path.
|
|
150
|
+
*
|
|
151
|
+
* WE CANNOT FENCE INSIDE EVE, SO WE FENCE WHAT EVE IS GIVEN. The attachment is removed from the body
|
|
152
|
+
* before the forward. The author's own `text` is untouched — including the URL they typed, which is
|
|
153
|
+
* the only part of a link unfurl a person actually wrote.
|
|
154
|
+
*
|
|
155
|
+
* 🔴 IDENTITY ON THE COMMON PATH, and that is load-bearing rather than an optimisation. A body with
|
|
156
|
+
* no `event.attachments`, and one whose attachments are ALL quoted Slack messages, is returned as
|
|
157
|
+
* the SAME string — so its Slack signature still verifies at the inner handler and the forward stays
|
|
158
|
+
* byte-identical to today's. Only a body that really carries an unfenced attachment is rewritten,
|
|
159
|
+
* and `slack-front.ts` re-signs exactly those.
|
|
160
|
+
*
|
|
161
|
+
* NEVER THROWS. Any shape this cannot parse returns the original body: losing the fence on an odd
|
|
162
|
+
* payload is recoverable, and a throw here would 500 a webhook Slack then replays forever — the same
|
|
163
|
+
* direction {@link resolveSlackInboundText} takes for the same reason.
|
|
164
|
+
*/
|
|
165
|
+
export declare function stripUnfencedSlackAttachments(rawBody: string): string;
|
|
166
|
+
export declare function resolveSlackInboundText(text: string, inner: Record<string, unknown>): string;
|
|
167
|
+
/**
|
|
168
|
+
* Parse a Slack `event_callback` envelope carrying a `message` (or `app_mention`) event
|
|
169
|
+
* into {@link ParsedMessageEvent}, or null when it isn't a usable message event (missing
|
|
170
|
+
* ids, wrong outer type). Pure — no I/O — so it's trivially testable against fixtures.
|
|
171
|
+
*/
|
|
172
|
+
export declare function parseMessageEvent(envelope: SlackEventEnvelope): ParsedMessageEvent | null;
|
|
173
|
+
/** A fail-soft profile resolver: raw platform user id → {real name, self-set handle}, or
|
|
174
|
+
* undefined. Injected on each route's deps so name resolution is unit-testable AND so a caller
|
|
175
|
+
* that omits it (or the resolver throwing) degrades to raw ids — a name lookup must NEVER block
|
|
176
|
+
* or fail a wake. The live wiring points this at the cached {@link fetchUserProfileNames}. */
|
|
177
|
+
export type ResolveProfile = (userId: string) => Promise<{
|
|
178
|
+
name?: string;
|
|
179
|
+
handle?: string;
|
|
180
|
+
} | undefined>;
|
|
181
|
+
/**
|
|
182
|
+
* The message a woken (non-mention) turn receives: the judge instruction, then the
|
|
183
|
+
* triggering reply rendered through the shared wake envelope (wake-envelope.ts). The
|
|
184
|
+
* instruction is OURS (trusted, kept byte-identical below — it's live-tuned, see the
|
|
185
|
+
* file-level note above); the envelope's message body is surface content and gets
|
|
186
|
+
* `wrapUntrusted`-wrapped by the renderer itself, same invariant as the mention path's
|
|
187
|
+
* backscroll. A reader abusing the sentinel ("output NO_REPLY") can only silence the
|
|
188
|
+
* agent — the same as not addressing it — so this adds no injection surface.
|
|
189
|
+
*
|
|
190
|
+
* The wake has two addressings (`opts.mode`): the default engaged-thread `thread-follow`, and — on
|
|
191
|
+
* the supervisor path (`"supervisor"`) — `channel-watch` (Draft B, design doc §3), which is honest
|
|
192
|
+
* that the channel's STANDING session does not "follow this thread". Only that attribute differs;
|
|
193
|
+
* everything else in the envelope is shared, so the two paths stay in lockstep. The standing
|
|
194
|
+
* "Reading a wake" block keys a bar to each value (OPE-753).
|
|
195
|
+
*
|
|
196
|
+
* The social-chatter/re-answer lines are load-bearing for cheaper judged-wake models:
|
|
197
|
+
* live 2026-07-07 (e2e S2, deepseek-v4-flash as the e2e domain's primary) a human-to-human
|
|
198
|
+
* "thanks …, see you at standup" right after the agent's answer drew the SAME answer again
|
|
199
|
+
* — the judge read the thanks as addressed to it. Naming the pattern explicitly is cheap
|
|
200
|
+
* for strong models and decisive for weak ones.
|
|
201
|
+
*
|
|
202
|
+
* `now` is a clock seam (defaults to the real clock) so the envelope's `current-time`
|
|
203
|
+
* attribute is deterministic in tests; production callers pass `deps.now` when they have
|
|
204
|
+
* one, exactly like the touch/latch timestamps elsewhere in this module.
|
|
205
|
+
*
|
|
206
|
+
* `opts.backscroll` (T1.5): non-trigger envelope siblings rendered BEFORE the trigger message
|
|
207
|
+
* (oldest-first, ending at the trigger — renderWakeEnvelope's own invariant). Only the
|
|
208
|
+
* SUPERVISOR path's first wake on a generation ever supplies this (routeSupervisorEvent,
|
|
209
|
+
* fetchWakeEnvelopeBackscroll); every other caller passes nothing, so this renders
|
|
210
|
+
* byte-identically to before the addition.
|
|
211
|
+
*/
|
|
212
|
+
export declare function renderWakeMessage(ev: ParsedMessageEvent, now: (() => string) | undefined, opts: SlackWakeOpts): string;
|
|
213
|
+
/** Options both Slack judged-wake builders accept — the exact shape {@link renderWakeMessage}
|
|
214
|
+
* exposed inline before the delegate refactor (unchanged caller-facing signature). */
|
|
215
|
+
export interface SlackWakeOpts {
|
|
216
|
+
readonly memberCount?: number;
|
|
217
|
+
/**
|
|
218
|
+
* Resolved per-wake trust tier for the TRIGGER author (identity/wake-trust.ts — the audit-wedge
|
|
219
|
+
* F3 assignment: roster admins → principal, internal members → peer, bots/external/unresolvable →
|
|
220
|
+
* relay, fail-closed). Every live route resolves and passes it.
|
|
221
|
+
*
|
|
222
|
+
* 🔴 REQUIRED, NOT OPTIONAL (OPE-596), and it is why `opts` itself is a REQUIRED argument on every
|
|
223
|
+
* builder that takes this bag. The field used to default to `"peer"`, which is fail-OPEN, and
|
|
224
|
+
* since OPE-577 the tier also decides whether the body carries the `<untrusted_external_data>`
|
|
225
|
+
* fence — so an absent tier REMOVED a fence. Leaving `opts` optional would have kept the same hole
|
|
226
|
+
* behind a different door: a caller that passes no bag at all still states no tier. A caller that
|
|
227
|
+
* cannot resolve a tier states the fail-closed one itself: `relay` for a machine author, `unauth`
|
|
228
|
+
* for a human the gate cannot vouch.
|
|
229
|
+
*/
|
|
230
|
+
readonly trust: WakeTrust;
|
|
231
|
+
/** ENVELOPE-SIBLING backscroll (supervisor first-wake only): the carried TAIL of the outgoing
|
|
232
|
+
* generation, or — with no recorded tail — the capped platform-history fallback. */
|
|
233
|
+
readonly backscroll?: readonly WakeMessageInput[];
|
|
234
|
+
/** The trigger author's resolved display name / handle (fail-safe: absent ⇒ raw id in `author`). */
|
|
235
|
+
readonly triggerAuthorName?: string;
|
|
236
|
+
readonly triggerAuthorHandle?: string;
|
|
237
|
+
/** The `<participants>` id→name/handle block (fail-safe: absent ⇒ no block). */
|
|
238
|
+
readonly participants?: readonly WakeParticipant[];
|
|
239
|
+
/** Wake mode. Default (undefined) = the engaged-thread `thread-follow` addressing;
|
|
240
|
+
* `"supervisor"` = `channel-watch` — the channel supervisor is the channel's STANDING session, it
|
|
241
|
+
* doesn't "follow this thread". Only {@link routeSupervisorEvent} passes it. */
|
|
242
|
+
readonly mode?: "supervisor";
|
|
243
|
+
/** Engagement calibration for a `channel-watch` wake (memory/domain.ts engagementTierFor), rendered
|
|
244
|
+
* as the `<wake engagement>` attribute. Only the supervisor path passes it; unset ⇒ no attribute. */
|
|
245
|
+
readonly engagementTier?: WakeEngagementTier;
|
|
246
|
+
/**
|
|
247
|
+
* The trigger's thread carries a PARTICIPATING engagement row and is not stood down — render
|
|
248
|
+
* `follow-up="thread-engaged"`, the envelope's own word for "you are already in this
|
|
249
|
+
* conversation" (the standing "Reading a wake" block, instructions/access.ts). A WATCHER row
|
|
250
|
+
* (`participating: false`, what a name-drop latches) is not an enrolment and states nothing.
|
|
251
|
+
*
|
|
252
|
+
* 🔴 IT DOES NOT TOUCH `addressing`, DELIBERATELY. The supervisor path's `channel-watch` is the
|
|
253
|
+
* honest tier for a STANDING session (the Draft B decision this file's renderWakeMessage header
|
|
254
|
+
* records, pinned in slack-engagement-test.ts), and enrolment is a different fact from the
|
|
255
|
+
* silence bar. The two attributes were always meant to be read together; only one of them was
|
|
256
|
+
* ever emitted here.
|
|
257
|
+
*
|
|
258
|
+
* MEASURED (ship #195, F-EDIT / OPE-1673): the seat answered the thread root at 06:19:24Z, then
|
|
259
|
+
* declined the follow-up AND its edit as `not_relevant` — "not addressed to me" — because both
|
|
260
|
+
* wakes rendered as bare ambient activity. The engagement row `markEngaged` wrote for the root
|
|
261
|
+
* mention was in the store throughout; `routeSupervisorEvent` already reads it for the
|
|
262
|
+
* stood-down gate, so stating it costs no extra read.
|
|
263
|
+
*
|
|
264
|
+
* Unset/false ⇒ no attribute, byte-identical to before.
|
|
265
|
+
*/
|
|
266
|
+
readonly threadEngaged?: boolean;
|
|
267
|
+
/** Host-minted wake id (rendered `<wake wake-id=…>`) — SINGLE-WINDOW unified path only
|
|
268
|
+
* (channels/unified-window.ts): the id the model echoes to `reply` so the host resolves the
|
|
269
|
+
* wake's origin from the map. Unset ⇒ byte-identical (no attribute). */
|
|
270
|
+
readonly wakeId?: string;
|
|
271
|
+
/** SINGLE-WINDOW addressed @mention (the GitHub reference's exact field —
|
|
272
|
+
* github-supervisor.ts GitHubSupervisorWakeInput.directMention): render the supervisor wake
|
|
273
|
+
* as `direct-mention` — the full judged envelope (so the `wake-id` survives for the reply
|
|
274
|
+
* tool, the ONLY reply route on the unified channel) but with the ADDRESSED preamble that
|
|
275
|
+
* tells the supervisor it was @mentioned and owes a reply, instead of the silence-biased
|
|
276
|
+
* channel-watch preamble. Unset/false ⇒ the channel-watch wake, byte-identical to today.
|
|
277
|
+
* Only routeSupervisorEvent's single-window mention path sets it (the front route kept the
|
|
278
|
+
* mention because the seam said the unified window owns it). */
|
|
279
|
+
readonly directMention?: boolean;
|
|
280
|
+
/** THE DM WAKE (OPE-351 stage 2): render this as a direct-message wake — envelope `reason="dm"`
|
|
281
|
+
* with the ADDRESSED preamble. Set only by slack-front.ts's `scheduleDmWake`. See
|
|
282
|
+
* {@link slackDmWake} for why a DM is always addressed. Unset ⇒ byte-identical to today. */
|
|
283
|
+
readonly dm?: boolean;
|
|
284
|
+
/**
|
|
285
|
+
* The surface POSITIVELY REFUTED the trigger author's membership — a guest, a Connect stranger.
|
|
286
|
+
* It rides to the render as the trigger message's own `refuted`, which is a ONE-WAY FENCE
|
|
287
|
+
* BACKSTOP (wake-envelope.ts {@link authorTrustAdmitsUnfencedBody}): it can only ADD the
|
|
288
|
+
* `<untrusted_external_data>` fence to a body whose TIER already reads member-grade, never remove
|
|
289
|
+
* one, and it never moves the reference-only note.
|
|
290
|
+
*
|
|
291
|
+
* 🔴 IT EXISTS FOR THE DM FAMILY, where the tier and the refutation come apart (OPE-1486). Every
|
|
292
|
+
* CHANNEL route folds its refutation into `surfaceInternal` first, so a refuted channel author is
|
|
293
|
+
* already `unauth` and already fenced — those routes state nothing here and are byte-identical. A
|
|
294
|
+
* GROUP DM deliberately keeps the ROOM's tier, so an invited guest is still answered at
|
|
295
|
+
* `principal`; without this field that guest's body would reach the model UNFENCED. Absent means
|
|
296
|
+
* "not refuted", which includes "the probe could not answer" — this leg fails OPEN.
|
|
297
|
+
*/
|
|
298
|
+
readonly triggerRefuted?: boolean;
|
|
299
|
+
/**
|
|
300
|
+
* THE NAME-DROP TIER on a standing-seat wake (OPE-1725): a person said the agent's name in this
|
|
301
|
+
* room without @-mentioning it, and the name-drop judge admitted the wake. Renders
|
|
302
|
+
* `addressing="name-drop"` plus the `follow-up="follows-on-reply"` contract.
|
|
303
|
+
* 🔴 IT IS A TIER, NOT A SESSION. Set only by {@link routeSupervisorEvent}'s name-drop path, which
|
|
304
|
+
* is the seat road: the wake lands in the ROOM'S OWN SEAT with the earlier exchange already in
|
|
305
|
+
* context, never in a fresh eve-native per-thread session. {@link directMention} wins where both
|
|
306
|
+
* are somehow set — an @mention is the more addressed fact. Unset ⇒ the channel-watch wake,
|
|
307
|
+
* byte-identical to today.
|
|
308
|
+
*/
|
|
309
|
+
readonly nameDrop?: boolean;
|
|
310
|
+
/** This surface cannot read its own history between wakes, so the seat took no catch-up
|
|
311
|
+
* (channels/seat-catch-up.ts `catchUpUnavailableReason`). Rendered as a trusted `<system-note>`;
|
|
312
|
+
* unset ⇒ no note, which is every Slack wake, because Slack registers a fetch. */
|
|
313
|
+
readonly catchUpUnavailable?: string;
|
|
314
|
+
/**
|
|
315
|
+
* THE NAME-DROP HYDRATION, ON THE SEAT ROAD (OPE-1725). The surrounding conversation
|
|
316
|
+
* {@link routeNameDropEvent} fetches for a name-drop, as RAW {@link ContextLine}s — renderWake
|
|
317
|
+
* wraps them through the shared `renderContext`, exactly as it does on the native route.
|
|
318
|
+
*
|
|
319
|
+
* 🔴 IT IS THE SIDE-CHANNEL SLOT, NOT THE ENVELOPE BACKSCROLL, and the two must not be confused.
|
|
320
|
+
* {@link backscroll} carries the seat's own bootstrap/catch-up read as envelope siblings; this
|
|
321
|
+
* carries the name-drop judge's own "what is being discussed" hydration, which is the ONE slot
|
|
322
|
+
* `withholdNameDropContext` fills (OPE-525). Set only by the seat road's name-drop path; unset ⇒
|
|
323
|
+
* no context block at all, byte-identical to an ambient seat wake.
|
|
324
|
+
*
|
|
325
|
+
* 🔴 IT CARRIES ONLY WHAT {@link backscroll} DOES NOT (OPE-1743). The caller de-duplicates the two
|
|
326
|
+
* slots by message id before it fills them, so one wake renders each room message ONCE: the
|
|
327
|
+
* envelope keeps the overlap with its per-message author, tier and send time, and this slot keeps
|
|
328
|
+
* the older conversation the envelope's window never reached. ONE EXEMPTION: a line whose author the
|
|
329
|
+
* surface positively REFUTED stays here even when the envelope carries the same message, because the
|
|
330
|
+
* two slots are graded by different legs and the envelope's copy may be graded by neither (OPE-1742).
|
|
331
|
+
*/
|
|
332
|
+
readonly contextLines?: readonly ContextLine[];
|
|
333
|
+
/** `renderContext`'s provenance label for {@link contextLines}. */
|
|
334
|
+
readonly contextSource?: string;
|
|
335
|
+
/** OUR trusted read-more instruction, appended OUTSIDE the untrusted wrap by `renderContext`
|
|
336
|
+
* and only when the rendered slice fills the count cap. */
|
|
337
|
+
readonly readMoreHint?: string;
|
|
338
|
+
}
|
|
339
|
+
/** The engaged-conversation `thread-follow` judged wake (the default {@link renderWakeMessage} path). */
|
|
340
|
+
export declare function slackThreadFollowWake(ev: ParsedMessageEvent, opts: SlackWakeOpts): NormalizedWake;
|
|
341
|
+
/** The standing-session supervisor judged wake (routeSupervisorEvent's path): `channel-watch`
|
|
342
|
+
* addressing by default, or — when {@link SlackWakeOpts.directMention} says the single-window seam
|
|
343
|
+
* kept the @mention — the ADDRESSED `direct-mention` addressing, which keeps the envelope and its
|
|
344
|
+
* wake-id and tells the standing block this wake is owed an answer. */
|
|
345
|
+
export declare function slackChannelWatchWake(ev: ParsedMessageEvent, opts: SlackWakeOpts): NormalizedWake;
|
|
346
|
+
/**
|
|
347
|
+
* THE DM WAKE (OPE-351 stage 2; group DMs added at stage 4) — the first producer of the `dm` envelope
|
|
348
|
+
* reason, which has been in the vocabulary (channels/wake-envelope.ts {@link WakeReason}) since it was
|
|
349
|
+
* reverse-engineered from Tag with no emitter behind it.
|
|
350
|
+
*
|
|
351
|
+
* ADDRESSING IS `direct-mention`, ALWAYS, and it is doing two jobs at once. Semantically it is simply
|
|
352
|
+
* TRUE for a 1:1: everything said in a 1:1 DM is said to the agent, so a DM is never judged for whether
|
|
353
|
+
* it was addressed — the engagement-tier dial, the name-drop judge and the channel-watch silence bias
|
|
354
|
+
* are all category errors here (slack-front.ts routes a DM around routeSupervisorEvent entirely for that
|
|
355
|
+
* reason). Mechanically it is LOAD-BEARING: `direct-mention` is what stamps `oe_addressing` through
|
|
356
|
+
* the divert, which marks the wake as OWED a reply. It selects the `[direct]` preamble, whose reply
|
|
357
|
+
* mandate names the wake-id the model must echo back, and it classifies the drop row if the turn
|
|
358
|
+
* ends in prose anyway.
|
|
359
|
+
* 🔴 IT IS NOT A DELIVERY PATH (OPE-1982). It used to arm the unified channel's addressed-prose
|
|
360
|
+
* bridge, so a DM answer written as prose instead of a `reply` call still reached the room. That
|
|
361
|
+
* bridge is deleted: on the unified channel a turn's closing prose is DROPPED on every addressing
|
|
362
|
+
* and re-driven once towards the tool, so a DM answer reaches its room only through a `reply` call.
|
|
363
|
+
*
|
|
364
|
+
* A GROUP DM IS THE SAME WAKE. The addressing is identical (the drop classification and the reply mandate
|
|
365
|
+
* are needed just the same), and the room shape reaches the model on `<channel type="mpim">` against
|
|
366
|
+
* a 1:1's `type="im"`. The old `groupDm` flag selected the opening clause of a PROSE preamble that no
|
|
367
|
+
* longer exists, so OPE-770 deleted it — the schema render has no prose to select. The silence escape
|
|
368
|
+
* the reply mandate carries is what makes unconditional WAKING compatible with conditional REPLYING
|
|
369
|
+
* in a room that also holds other humans.
|
|
370
|
+
*
|
|
371
|
+
* `reason` is `dm` rather than `mention` because the envelope's reason describes the SURFACE SHAPE the
|
|
372
|
+
* wake arrived on, and telling a DM it was "@mentioned" is a small lie that costs orientation. A group
|
|
373
|
+
* DM keeps `dm` for the same reason: {@link WakeReason}'s own definition is "a direct-message-EQUIVALENT
|
|
374
|
+
* surface", which an mpim is — inventing a fourth value for it would extrapolate a Tag-derived
|
|
375
|
+
* vocabulary with no evidence behind the new entry.
|
|
376
|
+
*/
|
|
377
|
+
export declare function slackDmWake(ev: ParsedMessageEvent, opts: SlackWakeOpts): NormalizedWake;
|
|
378
|
+
/**
|
|
379
|
+
* What `message.completed` (channels/slack.ts) should actually DO with a terminal
|
|
380
|
+
* {@link FinalMessageAction} ("silent" or "post" — never "buffer-tool-status", which is
|
|
381
|
+
* an intermediate step with no turn-final effects at all and is handled before this is
|
|
382
|
+
* called), given whether an approval card already posted THIS turn (a propose tool —
|
|
383
|
+
* tools/set_spend_limit.ts and its siblings — calling requestApproval; see
|
|
384
|
+
* approvals/card-signal.ts).
|
|
385
|
+
*
|
|
386
|
+
* "reply" is the outcome for BOTH an ordinary post and a card-suppressed one: a card
|
|
387
|
+
* posting mid-turn still means the agent ACTED and is a PARTICIPANT in this thread —
|
|
388
|
+
* 👀 clears, the thread engages, oeUninvited clears — exactly like any other reply.
|
|
389
|
+
* `postText: false` gates ONLY the actual `channel.thread.post` call (Tommy, live: "the
|
|
390
|
+
* card should suffice" — the card already says everything a narrated reply would
|
|
391
|
+
* repeat). A refusal/validation path in a propose tool never reaches requestApproval at
|
|
392
|
+
* all (see tools/set_spend_limit.ts's header), so its thrown error still surfaces as a
|
|
393
|
+
* normal `postText: true` reply — only a turn where a card was ACTUALLY posted is
|
|
394
|
+
* suppressed.
|
|
395
|
+
*/
|
|
396
|
+
export type TurnEffects = {
|
|
397
|
+
readonly kind: "silent";
|
|
398
|
+
} | {
|
|
399
|
+
readonly kind: "reply";
|
|
400
|
+
readonly postText: boolean;
|
|
401
|
+
};
|
|
402
|
+
export declare function turnEffectsFor(action: Extract<FinalMessageAction, {
|
|
403
|
+
kind: "silent";
|
|
404
|
+
} | {
|
|
405
|
+
kind: "post";
|
|
406
|
+
}>, cardPostedThisTurn: boolean): TurnEffects;
|
|
407
|
+
/** Dependencies injected into {@link routeMessageEvent} so the decision is fully testable. */
|
|
408
|
+
export interface MessageRouteDeps {
|
|
409
|
+
/** Engagement lookup (defaults to the operational store via isThreadEngaged). */
|
|
410
|
+
readonly isEngaged: (p: ReturnType<typeof resolvePartition>, threadTs: string) => Promise<boolean>;
|
|
411
|
+
/** Read the full engagement record — used to set oeUninvited from the thread's
|
|
412
|
+
* `participating` flag. Optional so older test doubles keep compiling (absent ⇒
|
|
413
|
+
* fail-safe uninvited); live wiring passes the real store call. */
|
|
414
|
+
readonly getEngagement?: (p: Partition, threadTs: string) => Promise<EngagementRow | null>;
|
|
415
|
+
/** The wake — continue the thread's session with this reply. */
|
|
416
|
+
readonly wake: (input: {
|
|
417
|
+
channelId: string;
|
|
418
|
+
threadTs: string;
|
|
419
|
+
auth: unknown;
|
|
420
|
+
message: string;
|
|
421
|
+
state?: unknown;
|
|
422
|
+
}) => Promise<void>;
|
|
423
|
+
/** Refresh the engagement's last-turn time after a successful wake. */
|
|
424
|
+
readonly touch?: (p: ReturnType<typeof resolvePartition>, threadTs: string, atIso: string) => Promise<void>;
|
|
425
|
+
/** Clock seam for the touch timestamp (tests pass a fixed value). */
|
|
426
|
+
readonly now?: () => string;
|
|
427
|
+
/**
|
|
428
|
+
* Positively confirm the channel is internal (TTL-cached conversations.info,
|
|
429
|
+
* fail-closed) so the wake auth can carry the trust verdict. Optional: absent (or
|
|
430
|
+
* unconfirmed) the wake still delivers, just with an UNSTAMPED auth — the session
|
|
431
|
+
* fails closed to `external` (no shared memory / domain skills) for that turn.
|
|
432
|
+
*/
|
|
433
|
+
readonly confirmInternal?: (channelId: string) => Promise<boolean>;
|
|
434
|
+
/** Append a durable audit row. Optional; best-effort. */
|
|
435
|
+
readonly audit?: (action: string, detail: Record<string, unknown>) => Promise<void>;
|
|
436
|
+
/** Resolve the trigger author's display name for the envelope (fail-soft). Optional so
|
|
437
|
+
* existing test doubles keep compiling AND so a wake never depends on a name lookup — absent
|
|
438
|
+
* or throwing ⇒ the raw id renders. Live wiring points this at {@link fetchUserProfileNames}. */
|
|
439
|
+
readonly resolveProfile?: ResolveProfile;
|
|
440
|
+
}
|
|
441
|
+
/** The outcome of routing one message event — the decision plus whether a wake fired. */
|
|
442
|
+
export interface RouteOutcome extends ReplyDecision {
|
|
443
|
+
readonly woke: boolean;
|
|
444
|
+
}
|
|
445
|
+
export declare function bindableFor(ev: ParsedMessageEvent): BindableContext;
|
|
446
|
+
/**
|
|
447
|
+
* Route one parsed message event: apply the delivery policy and, if it passes, wake the
|
|
448
|
+
* engaged thread's session. Returns the decision + whether a wake fired. All side effects
|
|
449
|
+
* go through injected deps so the policy ordering is unit-tested without Slack or a live
|
|
450
|
+
* runtime. The engagement registry is consulted ONLY when the cheap, pure disqualifiers
|
|
451
|
+
* pass (subtype/bot/mention/not-in-thread), keeping the DB off the hot path for chatter.
|
|
452
|
+
*/
|
|
453
|
+
export declare function routeMessageEvent(ev: ParsedMessageEvent, deps: MessageRouteDeps): Promise<RouteOutcome>;
|
|
454
|
+
/**
|
|
455
|
+
* Production deps: engagement from the operational store, and a wake built over a route
|
|
456
|
+
* handler's `send`. `send` is the channel-route primitive that continues a session by
|
|
457
|
+
* continuation token — `slackContinuationToken(channel, thread)` is the Slack thread's
|
|
458
|
+
* token, so this delivers the reply straight into the live thread session.
|
|
459
|
+
*/
|
|
460
|
+
/** The channel-route `send` shape the wake paths need (mirrors Eve's SendFn loosely). */
|
|
461
|
+
export type WakeSend = (input: {
|
|
462
|
+
message: string;
|
|
463
|
+
context?: readonly string[];
|
|
464
|
+
}, opts: {
|
|
465
|
+
auth: unknown;
|
|
466
|
+
continuationToken: string;
|
|
467
|
+
state?: unknown;
|
|
468
|
+
}) => Promise<unknown>;
|
|
469
|
+
/**
|
|
470
|
+
* The SPEND GATE at the ENGAGEMENT door (OPE-33) — the second call site of the ONE gate
|
|
471
|
+
* (slack-spend-gate.ts); the first is slack.ts `onInbound` (the mention door).
|
|
472
|
+
*
|
|
473
|
+
* WHY BOTH: on eve 0.27.5 the compiled slackChannel drops self-authored messages
|
|
474
|
+
* (`isSelfAuthoredSlackMessage`, matching `raw.app_id` against the envelope's `api_app_id`)
|
|
475
|
+
* BEFORE `onAppMention`/`onDirectMessage` run — and Slack stamps `app_id` on anything an app
|
|
476
|
+
* posts through the Web API, user token included. Every driver-app-authored mention therefore
|
|
477
|
+
* skips the mention door entirely and re-enters through THIS path (live-proven 2026-07-25:
|
|
478
|
+
* capped surfaces never refused). Gating only the mention handler is gating one of two doors.
|
|
479
|
+
*
|
|
480
|
+
* Refuse-and-drop semantics mirror the mention door exactly: over cap + no admin bypass ⇒ post
|
|
481
|
+
* the refusal, return "declined", and the caller dispatches NO model turn — DMs included
|
|
482
|
+
* (OPE-267 removed checkCap's DM exemption). Fail-open on any gate error (the helper's own
|
|
483
|
+
* console.warn).
|
|
484
|
+
*
|
|
485
|
+
* The refusal is posted through {@link postOnce}, keyed on (surface, thread root): an ambient
|
|
486
|
+
* channel over cap answers ONCE per conversation instead of once per message, while a genuinely
|
|
487
|
+
* new thread still gets told why it's being ignored. Fail-open by contract — a dedupe-store
|
|
488
|
+
* outage posts anyway.
|
|
489
|
+
*/
|
|
490
|
+
export declare function slackWakeSpendGate(ev: ParsedMessageEvent, p: Partition, threadRootTs: string): Promise<SpendGateDecision>;
|
|
491
|
+
/**
|
|
492
|
+
* {@link tenantFenceDeclined} for a door the FRONT route owns — the diverted DM window and the
|
|
493
|
+
* shortcut ingress (OPE-1644).
|
|
494
|
+
*
|
|
495
|
+
* 🔴 WHY IT IS EXPORTED RATHER THAN COPIED. Those doors live in `slack-front.ts`, whose
|
|
496
|
+
* `scheduleDmWake` dispatches a full model turn onto the DM SEAT — the largest grant this
|
|
497
|
+
* deployment makes, holding a window token, delegation and memory — and which consulted no fence at
|
|
498
|
+
* all. A second copy of the rule at a second door is precisely the drift `slack-tenant-fence.ts`
|
|
499
|
+
* exists to prevent, so the front route calls THIS, and gets the same decision, the same audit row
|
|
500
|
+
* and the same silent-drop posture. Only `door` and `route` differ: an operator reading the trail
|
|
501
|
+
* needs to know which ingress refused.
|
|
502
|
+
*/
|
|
503
|
+
export declare function slackFrontTenantFenceDeclined(ev: ParsedMessageEvent, door: string, route: string): boolean;
|
|
504
|
+
/**
|
|
505
|
+
* THE TENANT FENCE AT THE INTERACTIVITY CLICK INGRESS (OPE-1680) — the six `block_actions` /
|
|
506
|
+
* `view_submission` branches `slack-front.ts` owns, which arrive on the interactivity URL and carry
|
|
507
|
+
* no {@link ParsedMessageEvent}.
|
|
508
|
+
*
|
|
509
|
+
* 🔴 WHY A CLICK IS FENCED THOUGH IT RUNS NO MODEL TURN. What an unadmitted workspace reaches here is
|
|
510
|
+
* not a turn — it is the ALIASING the fence exists for, which is the same argument OPE-1633 used to
|
|
511
|
+
* fence `/oe`. On Slack a member id is issued PER WORKSPACE, so a foreign workspace's click is a
|
|
512
|
+
* bare `U…` that our own machinery must then decide about: it approves or rejects a pending action,
|
|
513
|
+
* opens a credential-intake modal, stores a pasted secret, mints an OAuth URL, answers a
|
|
514
|
+
* credential-grant ask, or cancels a running session. Every one of those consults the roster, writes
|
|
515
|
+
* audit rows into this deployment's trail, and posts back on OUR bot token.
|
|
516
|
+
*
|
|
517
|
+
* 🔴 WHAT THE FOREIGN CLICKER SEES: THE ACK, AND NOTHING ELSE. Slack still gets the empty 200 an
|
|
518
|
+
* interactivity route owes, so it does not retry. Nothing is POSTED back, for the reason every wake
|
|
519
|
+
* door gives — we cannot safely identify anybody in an unadmitted workspace, and a foreign tenant
|
|
520
|
+
* learning it reached a live deployment is more than it needs to know. "A silent button looks
|
|
521
|
+
* broken" does not apply: every one of these six buttons is posted BY THIS DEPLOYMENT, into a room
|
|
522
|
+
* it chose, and it posts into no unadmitted workspace — so a refused click has no legitimate reader
|
|
523
|
+
* waiting on it.
|
|
524
|
+
*
|
|
525
|
+
* 🔴 AND IT IS NOT A FORWARD. `forwardNative()` is the front route's fall-through when a handler
|
|
526
|
+
* disagrees with the classification; a tenant refusal must never take it, because eve's own channel
|
|
527
|
+
* asks no tenant question at all and would decide the same payload one layer down.
|
|
528
|
+
*
|
|
529
|
+
* The ROW is the same `surface.wake_dropped` row, carrying the same {@link UNADMITTED_TENANT_DROP_REASON},
|
|
530
|
+
* that every other door writes — so ONE control-API query still finds every tenant drop on this
|
|
531
|
+
* deployment. What differs is the key: a click has no message, so the row carries the click's own
|
|
532
|
+
* channel where the payload names one and {@link SLACK_INTERACTIVITY_SURFACE} where it does not, and
|
|
533
|
+
* its `ts` is `null` rather than an invented stamp. The partition resolves from those ids exactly as
|
|
534
|
+
* a message's does — the ORG is always this deployment's, and the platform/workspace/surface record
|
|
535
|
+
* the FOREIGN ids, which is what makes the row a statement about who was refused.
|
|
536
|
+
*/
|
|
537
|
+
export declare function slackInteractivityTenantFenceDeclined(ids: {
|
|
538
|
+
readonly teamId?: string | undefined;
|
|
539
|
+
readonly enterpriseId?: string | undefined;
|
|
540
|
+
readonly channelId?: string | undefined;
|
|
541
|
+
readonly userId?: string | undefined;
|
|
542
|
+
}, door: string, route: string): boolean;
|
|
543
|
+
/**
|
|
544
|
+
* The audit reason a STOOD-DOWN thread's dropped wake records (OPE-1669), and it is exported so the
|
|
545
|
+
* contract test pins the door against the constant rather than against a copy of its wording.
|
|
546
|
+
*
|
|
547
|
+
* 🔴 IT IS THE ROW THE SHIP HAD NO WAY TO ASK FOR. On #191 a plain thread-follow into a thread whose
|
|
548
|
+
* `engagement.stood_down` row said `marked:true` was answered anyway, and the trail could not say
|
|
549
|
+
* whether the door had fired and the reply plane leaked, or the door had never fired at all — this
|
|
550
|
+
* drop wrote nothing, so its absence proved nothing. It writes one row now, carrying the ANCHOR, so
|
|
551
|
+
* it joins to `engagement.stood_down` on the engagement registry's own key.
|
|
552
|
+
* The old rationale for silence was log volume in a chatty stopped thread. It is answered, not
|
|
553
|
+
* ignored: this fires only on a thread that is BOTH engaged and stopped, which is a conversation the
|
|
554
|
+
* agent was explicitly asked to leave — bounded by real human traffic in one thread, and the one
|
|
555
|
+
* question an operator asks about such a thread is exactly the one this row answers.
|
|
556
|
+
*/
|
|
557
|
+
export declare const STOOD_DOWN_DROP_REASON = "thread stopped \u2014 deterministic silence until an explicit @-mention re-invites";
|
|
558
|
+
/**
|
|
559
|
+
* The audit reason the DM DOOR records when it drops a bot-authored message (OPE-1022).
|
|
560
|
+
*
|
|
561
|
+
* Deliberately NOT {@link BOT_AUTHOR_DROP_REASON}. The engagement door drops only a bot the driver
|
|
562
|
+
* allowlist does not vouch for, so its row can blame the allowlist. The DM door admits NO bot
|
|
563
|
+
* author at all — an allowlisted driver's DM is dropped here too — so a row that said "is not an
|
|
564
|
+
* allowlisted driver" would send the reader to fix an allowlist that was never consulted. The
|
|
565
|
+
* `vouchedDriver` detail carries that distinction as data instead.
|
|
566
|
+
*/
|
|
567
|
+
export declare const DM_BOT_AUTHOR_DROP_REASON = "bot author \u2014 the DM door admits no bot author";
|
|
568
|
+
/**
|
|
569
|
+
* Audit the DM door's bot-author drop, and answer whether a row was written (OPE-1022).
|
|
570
|
+
*
|
|
571
|
+
* WHY THIS EXISTS. `slack-front.ts`'s DM branch dropped every bot-authored `message.im` /
|
|
572
|
+
* `message.mpim` with a bare `return OK()` — no audit row, no log line. Slack stamps `bot_id` on
|
|
573
|
+
* ANYTHING an app posts through the Web API, a user token included, so an operator probing the
|
|
574
|
+
* deployment with a script sees the whole surface go silent and has nothing to read. That is the
|
|
575
|
+
* exact shape diagnosed on 2026-08-23: two probe DMs vanished with zero trace and read as "Slack
|
|
576
|
+
* inbound is dead", when the drop was working as designed and only its silence was the defect.
|
|
577
|
+
*
|
|
578
|
+
* OUR OWN ECHO IS NOT AUDITED. Slack re-delivers every reply the bot posts as a `message.im` event
|
|
579
|
+
* carrying our own `bot_id`, so an unconditional row would double every reply with noise and make
|
|
580
|
+
* the trail unreadable — the same reasoning that keeps the ordinary engagement non-candidates out
|
|
581
|
+
* of the log. `selfBotId` comes from `auth.test` and is best-effort: when it cannot be resolved we
|
|
582
|
+
* audit the echo too, because an over-full trail beats a silent one.
|
|
583
|
+
*
|
|
584
|
+
* Best-effort by contract: never throws, never changes the decision. The caller hands the returned
|
|
585
|
+
* promise to `waitUntil` so the write survives the route's immediate 200.
|
|
586
|
+
*/
|
|
587
|
+
export declare function auditDmBotAuthorDrop(ev: ParsedMessageEvent, dmKind: "im" | "mpim", selfBotId: string | undefined): Promise<boolean>;
|
|
588
|
+
/**
|
|
589
|
+
* THE DM DOOR'S NAMED-DRIVER CARVE-OUT (OPE-1023, OPE-1739). True ⇒ this bot-authored DM is one of
|
|
590
|
+
* the deployment's
|
|
591
|
+
* NAMED e2e driver and the door admits it; false ⇒ the door drops it, which is what every bot
|
|
592
|
+
* author gets by default.
|
|
593
|
+
*
|
|
594
|
+
* WHY IT EXISTS. Slack stamps `bot_id` on anything an app posts through the Web API, a USER token
|
|
595
|
+
* included (measured 2026-08-23). So the e2e driver — which posts as a real human through an `xoxp`
|
|
596
|
+
* token — cannot drive a real Slack DM: its post reaches this door as bot-authored and drops. The
|
|
597
|
+
* harness worked around it by FORGING a signed `event_callback` with no `bot_id`, which meant every
|
|
598
|
+
* DM leg in the pack exercised a synthetic envelope and a regression in Slack's own DM delivery to
|
|
599
|
+
* us was invisible. This carve-out lets the driver in through the real front door.
|
|
600
|
+
*
|
|
601
|
+
* WHY IT IS NOT THE `OE_TEST_DRIVER_IDS` ALLOWLIST, which already lifts the same bot at the
|
|
602
|
+
* engagement and mention doors. A DM seat holds a window token, delegation and memory, so admitting
|
|
603
|
+
* a bot into a DM is a strictly larger grant than admitting one into a watched channel. Reusing the
|
|
604
|
+
* channel allowlist would have widened the DM door for every deployment that already sets it, on
|
|
605
|
+
* upgrade, with no diff naming the change. The setting is the diff.
|
|
606
|
+
*
|
|
607
|
+
* POSTURE (every part load-bearing):
|
|
608
|
+
* - EMPTY BY DEFAULT. The shipped `wake.e2eDrivers` is empty, and an empty list matches nothing
|
|
609
|
+
* in `isE2eDriver` — so an unconfigured deployment reaches the same verdict, and the same
|
|
610
|
+
* `surface.wake_dropped` row, that it did before this carve-out existed.
|
|
611
|
+
* - NAMED IDENTITIES. The setting holds a LIST of `{ surface, principal }` entries, and this door
|
|
612
|
+
* reads the `slack` ones alone — each a `teamId:botId` pair a bot is admitted by matching WHOLE.
|
|
613
|
+
* The list is long because our e2e pack drives two Slack accounts (OPE-1739) and several other
|
|
614
|
+
* surfaces besides (OPE-1754); it is a longer roll of named identities, not a wider grant, and
|
|
615
|
+
* the grammar has no syntax for "any bot" in a workspace.
|
|
616
|
+
* - EXACT MATCH, WORKSPACE-PINNED AND SURFACE-PINNED. `teamId` and `botId` must BOTH match on the
|
|
617
|
+
* SAME listed pair, on an entry whose `surface` is `slack`; a driver named for one workspace —
|
|
618
|
+
* or for another surface — has no standing here. No prefixes, no wildcards.
|
|
619
|
+
* - `bot_id` IS THE FIELD SLACK SENDS. A `message.im`/`message.mpim` event carries `bot_id` (and
|
|
620
|
+
* the human `user`); it does not carry `app_id` on the message itself, so the pair is written in
|
|
621
|
+
* the terms the payload actually uses.
|
|
622
|
+
* - LOUD. The caller writes a `surface.wake_admitted` row ({@link auditDmDriverAdmitted}), so a
|
|
623
|
+
* soak review can tell an admitted driver DM from a human DM.
|
|
624
|
+
*
|
|
625
|
+
* Loop safety is unaffected: OE's own reply echo carries OE's own bot record, and the front route
|
|
626
|
+
* refuses a match against its own identity before it admits anything.
|
|
627
|
+
*/
|
|
628
|
+
export declare function dmDoorAdmitsDriver(ev: ParsedMessageEvent, configured?: readonly E2eDriverIdentity[]): boolean;
|
|
629
|
+
/** The audit reason the DM door records when {@link dmDoorAdmitsDriver} admits a bot author. One
|
|
630
|
+
* string, so a control-API query can find every such admission. */
|
|
631
|
+
export declare const DM_DRIVER_ADMIT_REASON = "e2e driver \u2014 the DM door admits a named driver identity";
|
|
632
|
+
/**
|
|
633
|
+
* Make the DM door's driver ADMISSION observable (OPE-1023) — the counterpart of
|
|
634
|
+
* {@link auditDmBotAuthorDrop}, which makes its refusals observable.
|
|
635
|
+
*
|
|
636
|
+
* A door that quietly widened for one identity would be worse than the silent drop OPE-1022 fixed:
|
|
637
|
+
* a soak reviewer reading a woken DM seat could not tell whether a human or the driver spoke. This
|
|
638
|
+
* row says which, and names the carve-out that let it through.
|
|
639
|
+
*
|
|
640
|
+
* Best-effort by contract: never throws, never changes the decision. The caller hands the returned
|
|
641
|
+
* promise to `waitUntil`, because the route wakes and returns before the write would land.
|
|
642
|
+
*/
|
|
643
|
+
export declare function auditDmDriverAdmitted(ev: ParsedMessageEvent, dmKind: "im" | "mpim"): Promise<void>;
|
|
644
|
+
export declare function liveDeps(send: WakeSend): MessageRouteDeps;
|
|
645
|
+
export declare const _engagementWakeMode: (ev: ParsedMessageEvent) => import("@open-everywhere/core/agent/adapter-api.js").SessionMode;
|
|
646
|
+
/** One wake needle: an id (for logs/attribution) + the pattern that fires it. */
|
|
647
|
+
export interface WakeTrigger {
|
|
648
|
+
readonly id: string;
|
|
649
|
+
readonly pattern: RegExp;
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* The wake triggers for a channel: the unified wake token set (always first — see
|
|
653
|
+
* {@link wakeNameDropTokens}), plus any caller-supplied extras (a future per-domain hook — none
|
|
654
|
+
* today). `displayName` is the surface's live vendor display name, or `undefined` when the lookup
|
|
655
|
+
* failed. Kept a pure builder so the list is assembled the same way everywhere the route needs it.
|
|
656
|
+
*/
|
|
657
|
+
export declare function buildWakeTriggers(displayName: string | undefined, extra?: readonly WakeTrigger[]): WakeTrigger[];
|
|
658
|
+
/** First trigger whose pattern matches `text`, or undefined when none do. */
|
|
659
|
+
export declare function matchWakeTriggers(text: string, triggers: readonly WakeTrigger[]): WakeTrigger | undefined;
|
|
660
|
+
/** Inputs to the pure name-drop policy — staged by {@link routeNameDropEvent}. */
|
|
661
|
+
export interface NameDropInput {
|
|
662
|
+
readonly inThread: boolean;
|
|
663
|
+
readonly authorIsBot: boolean;
|
|
664
|
+
readonly isAppMention: boolean;
|
|
665
|
+
readonly subtype?: string;
|
|
666
|
+
readonly channelType?: string;
|
|
667
|
+
readonly textMatchesName: boolean;
|
|
668
|
+
readonly domainEnabled: boolean;
|
|
669
|
+
readonly channelConfirmedInternal: boolean;
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* The name-drop policy. Every disqualifier before the single `deliver: true`, same
|
|
673
|
+
* fail-safe shape as {@link shouldDeliverReply}. Channels and private groups only —
|
|
674
|
+
* DMs always respond via the mention path, and group DMs are conversations the agent
|
|
675
|
+
* was explicitly added to person-by-person, where name-drop barging feels worst.
|
|
676
|
+
*/
|
|
677
|
+
export declare function shouldNameDropWake(input: NameDropInput): ReplyDecision;
|
|
678
|
+
/**
|
|
679
|
+
* Did the built-in name-drop ABSTAIN on this message, as opposed to dropping it for policy?
|
|
680
|
+
*
|
|
681
|
+
* 🔴 THE WHOLE WAKE-ARM RUNG RESTS ON THIS DISTINCTION (OPE-1367, docs/extending/drop-in-arms.md).
|
|
682
|
+
* {@link shouldNameDropWake} answers `deliver: false` for seven different reasons and only ONE of
|
|
683
|
+
* them is "I have no opinion": the text does not name the agent. The other six are policy — a DM
|
|
684
|
+
* or group DM (the mention path already always-wakes a DM), a bot author, an `app_mention`, a
|
|
685
|
+
* message subtype, a domain that never enabled the name-drop wake, and a channel the internal probe
|
|
686
|
+
* could not confirm. A consumer's arm may fill the abstention. It may not overturn a policy drop.
|
|
687
|
+
*
|
|
688
|
+
* 🔴 DERIVED FROM THE BUILT-IN, NEVER RE-LISTED. This asks the built-in itself: with the name
|
|
689
|
+
* matched and everything else exactly as it is, would it wake? A hand-kept copy of the
|
|
690
|
+
* disqualifiers would go stale the first time somebody adds one to the policy above, and the arm
|
|
691
|
+
* would then reach a message the policy had just started dropping.
|
|
692
|
+
*/
|
|
693
|
+
export declare function nameDropAbstained(input: NameDropInput): boolean;
|
|
694
|
+
/**
|
|
695
|
+
* The message a name-drop turn receives. A HIGHER bar than the engaged-thread judge:
|
|
696
|
+
* the agent was never invited into this conversation, so "when in doubt, stay out". The
|
|
697
|
+
* trigger renders through the same shared wake envelope as {@link renderWakeMessage} (see
|
|
698
|
+
* the T0.2/T0.3 note above it) — a top-level name-drop is `channel-activity`, an in-thread one
|
|
699
|
+
* is `thread-activity` (name-drop blanket-drops `message_changed`, see {@link
|
|
700
|
+
* shouldNameDropWake}, so it never sees an edit). `opts` carries the resolved trigger-author
|
|
701
|
+
* name/handle + `<participants>` block (fail-safe: absent ⇒ raw id in `author`, no block).
|
|
702
|
+
*/
|
|
703
|
+
export declare function renderNameDropMessage(ev: ParsedMessageEvent, name: string, now: (() => string) | undefined, opts: {
|
|
704
|
+
/** Resolved per-wake trust tier — 🔴 REQUIRED (OPE-596). This wrapper used to omit the field
|
|
705
|
+
* from its own opts type entirely, so every render it produced took {@link slackNameDropWake}'s
|
|
706
|
+
* fail-OPEN `"peer"` default whatever the author's real tier was. */
|
|
707
|
+
readonly trust: WakeTrust;
|
|
708
|
+
readonly triggerAuthorName?: string;
|
|
709
|
+
readonly triggerAuthorHandle?: string;
|
|
710
|
+
readonly participants?: readonly WakeParticipant[];
|
|
711
|
+
}): string;
|
|
712
|
+
/** The uninvited `name-drop` judged wake ({@link routeNameDropEvent}'s path). No memberCount /
|
|
713
|
+
* envelope-sibling backscroll; a top-level trigger has no thread yet (replying CREATES
|
|
714
|
+
* one). `contextLines` (W3) carries the surrounding-conversation hydration as RAW lines — renderWake
|
|
715
|
+
* wraps them via the shared renderContext, so no pre-rendered string flows through the plumbing. */
|
|
716
|
+
export declare function slackNameDropWake(ev: ParsedMessageEvent, name: string, opts: {
|
|
717
|
+
readonly triggerAuthorName?: string;
|
|
718
|
+
readonly triggerAuthorHandle?: string;
|
|
719
|
+
readonly participants?: readonly WakeParticipant[];
|
|
720
|
+
readonly contextLines?: readonly ContextLine[];
|
|
721
|
+
readonly contextSource?: string;
|
|
722
|
+
readonly readMoreHint?: string;
|
|
723
|
+
/** Resolved per-wake trust tier — 🔴 REQUIRED, and `opts` with it (see
|
|
724
|
+
* {@link SlackWakeOpts.trust} for the whole reason). */
|
|
725
|
+
readonly trust: WakeTrust;
|
|
726
|
+
}): NormalizedWake;
|
|
727
|
+
/**
|
|
728
|
+
* Fetch the name-drop wake's surrounding conversation, RE-GRADE it and ADMIT it, on demand
|
|
729
|
+
* (OPE-1725, policed by OPE-1742).
|
|
730
|
+
*
|
|
731
|
+
* 🔴 IT IS A POLICED READ ON THE SEAT ROAD, AND ONLY THERE. Where `deps.seatRoute` is present the
|
|
732
|
+
* lines it returns have passed the domain's own admission policy (`seatAdmissionFor(channelKey)`,
|
|
733
|
+
* applied through channels/seat-admission.ts `admitContextLines`), the same policy the bootstrap
|
|
734
|
+
* backscroll and the since-last-wake catch-up apply — so a SEATED wake's three reads of the room
|
|
735
|
+
* admit one author identically. An UNSEATED deployment takes this same thunk and applies NO
|
|
736
|
+
* admission at all: its lines are byte-identical to the pre-OPE-1742 read, so a `members-only`
|
|
737
|
+
* domain with no seat still hydrates a refused author's line. That is deliberate, and the reason
|
|
738
|
+
* is at the call site ("THE SEAT ROAD ONLY") — `admission` is the STANDING SEAT's entry policy by
|
|
739
|
+
* declaration, and an unseated deployment has no seat for it to govern. `null` includes "the policy
|
|
740
|
+
* admitted nothing", which renders no context block rather than an empty one.
|
|
741
|
+
*
|
|
742
|
+
* 🔴 ONE HYDRATION, BOTH ROADS. `routeNameDropEvent` is the only SLACK route that fills the
|
|
743
|
+
* `contextLines` side-channel slot — Discord's gateway fills it too and calls no transform, so an
|
|
744
|
+
* excluded author's hydrated lines survive there (access/external-policy.ts `withholdWakeBodies`
|
|
745
|
+
* keeps that gap stated) — and `withholdNameDropContext` is the only thing that withholds a
|
|
746
|
+
* refuted author's bytes out of Slack's copy (OPE-525). When the judged wake hands off to the seat,
|
|
747
|
+
* the seat wake must carry THE SAME block — otherwise the room's hydration silently stops reaching
|
|
748
|
+
* the model on every seated deployment, and the withhold control has nothing left to act on.
|
|
749
|
+
*
|
|
750
|
+
* 🔴 IT IS A THUNK, NOT A VALUE, AND THAT IS WHAT KEEPS THE COST ORDER RIGHT. Each road pays for the
|
|
751
|
+
* history call only after ITS OWN tenant fence, spend gate and external-participant policy have
|
|
752
|
+
* admitted the wake — the seat road runs its copies inside {@link routeSupervisorEvent}, which is
|
|
753
|
+
* after the hand-off. Hydrating eagerly here would buy a Slack page for a wake the seat then refuses.
|
|
754
|
+
*
|
|
755
|
+
* `null` = nothing to add, or ANY failure: the fetch and the regrade are both fail-soft, so a wake
|
|
756
|
+
* still fires with just the message, exactly as it did before the hydration existed.
|
|
757
|
+
*/
|
|
758
|
+
export type NameDropContextHydrator = () => Promise<WakeBackscrollLines | null>;
|
|
759
|
+
/** Dependencies for {@link routeNameDropEvent} — injected so the chain is unit-testable. */
|
|
760
|
+
export interface NameDropRouteDeps {
|
|
761
|
+
/** Is name-drop wake enabled for this channel's domain? (sync — config lookup). */
|
|
762
|
+
readonly enabledFor: (surfaceKey: string) => boolean;
|
|
763
|
+
/** Positively confirm the channel is internal (not externally shared). Fail-closed. */
|
|
764
|
+
readonly confirmInternal: (channelId: string) => Promise<boolean>;
|
|
765
|
+
/**
|
|
766
|
+
* Recent conversation backscroll for the wake (surrounding thread/channel history), as RAW
|
|
767
|
+
* {@link ContextLine}s + source + hint (W3) — renderWake wraps them via the shared renderContext,
|
|
768
|
+
* so wrapping happens in exactly one place instead of pre-rendered strings threading through the
|
|
769
|
+
* plumbing. Fetched only AFTER the gate decides deliver:true, so a declined message never pays the
|
|
770
|
+
* API call. Fail-soft: `null` (or an absent dep) = wake with just the message, exactly as before.
|
|
771
|
+
* Optional so existing test doubles keep compiling.
|
|
772
|
+
* `threadAdmissionSlice` asks the THREAD leg for its wider admission slice (OPE-1744): the
|
|
773
|
+
* hydrator sets it on the seat road, where it admits first and caps itself afterwards.
|
|
774
|
+
*/
|
|
775
|
+
readonly backscroll?: (input: {
|
|
776
|
+
channelId: string;
|
|
777
|
+
ts: string;
|
|
778
|
+
threadTs?: string;
|
|
779
|
+
threadAdmissionSlice?: boolean;
|
|
780
|
+
}) => Promise<WakeBackscrollLines | null>;
|
|
781
|
+
/** The per-author membership probe the hydrated-context withhold uses (OPE-525,
|
|
782
|
+
* {@link withholdNameDropContext}). Defaults to the live {@link slackAuthorIsMember}; injectable
|
|
783
|
+
* so the withhold is testable without Slack. Never called on the shipped `taint` grain. */
|
|
784
|
+
readonly authorIsMember?: SlackMemberProbe;
|
|
785
|
+
/** Start the judged turn in a thread rooted at the triggering message. `context` (when present)
|
|
786
|
+
* is the backscroll — ADDITIONAL history the channel appends before the delivery turn. */
|
|
787
|
+
readonly wake: (input: {
|
|
788
|
+
channelId: string;
|
|
789
|
+
threadTs: string;
|
|
790
|
+
auth: unknown;
|
|
791
|
+
message: string;
|
|
792
|
+
context?: readonly string[];
|
|
793
|
+
state?: unknown;
|
|
794
|
+
}) => Promise<void>;
|
|
795
|
+
/**
|
|
796
|
+
* The bot's SLACK display name (cached lookup) — the agent's PUBLIC FACE: people type the
|
|
797
|
+
* name they see in the sidebar. It is ALSO the name the agent identifies as, unless the
|
|
798
|
+
* deployment sets the `identity.name` setting — that canonical name wins for
|
|
799
|
+
* self-identification on every surface, AND (OPE-571) joins this trigger's own token set
|
|
800
|
+
* alongside the display name (see {@link wakeNameDropTokens}), extendable/narrowable by
|
|
801
|
+
* `wake.nameDrop.include`/`wake.nameDrop.exclude`.
|
|
802
|
+
* Fail-soft: undefined (lookup failed) = no candidate at all = no name-drop wake, regardless
|
|
803
|
+
* of `identity.name` or the wake settings.
|
|
804
|
+
*/
|
|
805
|
+
readonly displayName: () => Promise<string | undefined>;
|
|
806
|
+
/**
|
|
807
|
+
* WARM-ON-WAKE latch: engage the thread as a WATCHER (participating:false) right before
|
|
808
|
+
* the wake, so a later message rolls into THIS session even if this turn judges NO_REPLY.
|
|
809
|
+
* Best-effort + idempotent — never fails the wake. Optional so older test doubles keep
|
|
810
|
+
* compiling; live wiring passes markThreadEngaged.
|
|
811
|
+
*/
|
|
812
|
+
readonly markEngaged?: (p: Partition, threadTs: string, atIso: string, opts?: {
|
|
813
|
+
participating?: boolean;
|
|
814
|
+
}) => Promise<void>;
|
|
815
|
+
/** Clock seam for the warm-latch timestamp (tests pass a fixed value). */
|
|
816
|
+
readonly now?: () => string;
|
|
817
|
+
/** Resolve the trigger author's display name for the envelope (fail-soft — absent/throwing ⇒
|
|
818
|
+
* raw id renders, the wake still fires). Live wiring: {@link fetchUserProfileNames}. */
|
|
819
|
+
readonly resolveProfile?: ResolveProfile;
|
|
820
|
+
/**
|
|
821
|
+
* 🔴 THE SEAT ROAD (OPE-1725). Present ⇔ this channel HAS a standing seat, and then every gate in
|
|
822
|
+
* this route is still the judge — the name match, the domain's `nameDropWake` opt-in, the
|
|
823
|
+
* fail-closed internal confirmation, the wake arm — while the WAKE ITSELF goes to that seat
|
|
824
|
+
* ({@link routeSupervisorEvent} with `nameDrop: true`) instead of opening eve's native per-thread
|
|
825
|
+
* session below.
|
|
826
|
+
* WHY THE SPLIT IS HERE AND NOT IN SLACK-FRONT: the candidacy decision is asynchronous (it needs
|
|
827
|
+
* the live display name) and it is this route's own, so deciding the road before it would mean a
|
|
828
|
+
* second copy of the judge. The seat route runs its own tenant fence, spend gate, external-policy
|
|
829
|
+
* check and trust resolution, so this route hands off BEFORE its own copies of those and never
|
|
830
|
+
* runs them twice.
|
|
831
|
+
* It is handed the HYDRATOR and nothing else. The seat wake renders no matched name (the standing
|
|
832
|
+
* identity line states the agent's own name every turn), so passing one would be a parameter
|
|
833
|
+
* nothing reads — but the surrounding-conversation hydration IS this route's own, and the seat
|
|
834
|
+
* wake must carry the same block, withheld the same way (see {@link NameDropContextHydrator}).
|
|
835
|
+
* Absent ⇒ the native per-thread wake below, which is what an unseated deployment keeps, byte for
|
|
836
|
+
* byte.
|
|
837
|
+
*/
|
|
838
|
+
readonly seatRoute?: (hydrateContext: NameDropContextHydrator) => Promise<RouteOutcome>;
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Route one TOP-LEVEL message as a name-drop candidate: pure checks, then the name
|
|
842
|
+
* regex, then config, then the (cached) internal-channel confirmation, then the wake.
|
|
843
|
+
* The reply thread is rooted at the triggering message's own ts — replying creates
|
|
844
|
+
* the thread, exactly like a human clicking "reply in thread".
|
|
845
|
+
*/
|
|
846
|
+
export declare function routeNameDropEvent(ev: ParsedMessageEvent, deps: NameDropRouteDeps): Promise<RouteOutcome>;
|
|
847
|
+
export declare function botDisplayName(): Promise<string | undefined>;
|
|
848
|
+
/** Reset the cached display name (tests only). */
|
|
849
|
+
export declare function _resetBotDisplayName(): void;
|
|
850
|
+
/**
|
|
851
|
+
* Positively confirm a channel is internal (fail-closed, TTL-cached — see
|
|
852
|
+
* channelInternalAndMeta). Exported because the routine dispatcher
|
|
853
|
+
* (schedules/dispatch.ts) performs the same live recheck before stamping trust
|
|
854
|
+
* onto a fired session — one gate, two security consumers.
|
|
855
|
+
*/
|
|
856
|
+
export declare function confirmChannelInternal(channelId: string): Promise<boolean>;
|
|
857
|
+
/**
|
|
858
|
+
* Positively confirm a channel is internal (fail-closed, same TTL-cached
|
|
859
|
+
* conversations.info as {@link confirmChannelInternal}) AND surface its member count —
|
|
860
|
+
* the count `confirmChannelInternal`'s boolean-only return discards. The T1.3 supervisor
|
|
861
|
+
* path needs both: the verdict gates the wake (fail-closed), the count enriches the wake
|
|
862
|
+
* envelope's `<channel member-count>`. One cached fetch serves both, so this is a widening
|
|
863
|
+
* of the existing gate, not a second API call.
|
|
864
|
+
*/
|
|
865
|
+
export declare function confirmChannelInternalWithMeta(channelId: string): Promise<{
|
|
866
|
+
internal: boolean;
|
|
867
|
+
memberCount?: number;
|
|
868
|
+
}>;
|
|
869
|
+
/**
|
|
870
|
+
* Is this Slack author a member this deployment admits? `true` = a vouched member, `false` = a guest
|
|
871
|
+
* (multi-channel or single-channel), a Connect stranger, a deleted account, or someone from a
|
|
872
|
+
* workspace the operator has not declared. `undefined` = no verdict: an empty `userId` (nobody to
|
|
873
|
+
* ask about), no bot token, a transport failure, or a Slack `ok:false` body. Every caller must read
|
|
874
|
+
* `undefined` as "not proven to be a non-member" — see this section's fail-OPEN note.
|
|
875
|
+
*
|
|
876
|
+
* 🔴 "THE EXPECTED WORKSPACE" IS A RUNG WALK SINCE OPE-1508, not an equality. The event's own
|
|
877
|
+
* workspace is a member with no declaration needed; any OTHER workspace is a member only where the
|
|
878
|
+
* operator declared it, or the Slack Enterprise Grid above it — which is what admits a Grid
|
|
879
|
+
* colleague whose home `team_id` is a sibling workspace. A BOT author walks the same rungs: the
|
|
880
|
+
* workspace's own app is a member, a foreign app is a guest. `is_bot` no longer refutes on its own.
|
|
881
|
+
* `partnerFactsFromInfo` (slack-identity.ts) holds the whole rule and its reasoning.
|
|
882
|
+
*
|
|
883
|
+
* 🔴 AN ABSENT `teamId` IS A GUEST QUESTION, NOT A "COULD NOT ASK" (OPE-1579). A caller that names
|
|
884
|
+
* no workspace still gets a verdict, because the walk reads the author's own workspace and Grid off
|
|
885
|
+
* `users.info`: `true` follows the operator's declaration, `false` follows its absence. A missing
|
|
886
|
+
* workspace is therefore never a reason to answer `undefined` — that verdict belongs to the four
|
|
887
|
+
* causes listed above. `partnerFactsFromInfo` decides both this probe and the 1:1 DM gate, so the
|
|
888
|
+
* two readers cannot drift on what an unnamed workspace means.
|
|
889
|
+
*
|
|
890
|
+
* Keyed by `teamId` too, because `partnerFactsFromInfo` compares `team_id` against it: caching one
|
|
891
|
+
* verdict per user id alone would let a workspace-A answer decide a workspace-B question. The `/`
|
|
892
|
+
* separator cannot collide: a Slack team id and a Slack user id are both `[A-Z0-9]+`, so two
|
|
893
|
+
* different (team, user) pairs can never compose the same key. The operator's declarations are
|
|
894
|
+
* process-wide config, so they add no dimension to the key.
|
|
895
|
+
*/
|
|
896
|
+
export declare function slackAuthorIsMember(userId: string, teamId: string | undefined): Promise<boolean | undefined>;
|
|
897
|
+
/** Reset the cached per-author membership verdicts (tests only). */
|
|
898
|
+
export declare function _resetAuthorMemberCache(): void;
|
|
899
|
+
/**
|
|
900
|
+
* {@link slackAuthorIsMember}, inverted to a GUEST verdict (OPE-589) — the ONE reading both the
|
|
901
|
+
* approval clicker floor's leg D and the HITL-answer admission's leg B reuse, so the two planes
|
|
902
|
+
* cannot drift on what "positively a guest" means for a per-person click. `undefined` stays
|
|
903
|
+
* `undefined`: "we could not ask" must never become a fabricated guest OR member verdict.
|
|
904
|
+
*/
|
|
905
|
+
export declare function slackClickerIsGuest(userId: string, teamId: string | undefined): Promise<boolean | undefined>;
|
|
906
|
+
/**
|
|
907
|
+
* The SURFACE-INTERNAL fact a chat wake site should hand {@link resolveWakeTrust} for a CHANNEL
|
|
908
|
+
* message: the room is internal AND the author is not a positively-identified non-member.
|
|
909
|
+
*
|
|
910
|
+
* 🔴 IT NARROWS THE WAKE TIER ONLY, never the wake AUTH. The `internal` verdict also stamps
|
|
911
|
+
* `trustChecked`/`isExternal` onto the delivery auth, which is what gives the whole channel session
|
|
912
|
+
* its memory tier. A guest posting one message must not demote a channel's memory for everybody, so
|
|
913
|
+
* the auth keeps reading the ROOM's verdict and only the per-message trust tier reads this one.
|
|
914
|
+
*
|
|
915
|
+
* This is the same composition Discord and Teams already make one layer earlier: their per-author
|
|
916
|
+
* signals (`member.pending`, Entra `userType: guest`) demote `binding.trust`, which is what
|
|
917
|
+
* `resolveWakeTrustFromContext` reads as its surface verdict. Slack had no such fold because its
|
|
918
|
+
* probe was DM-only.
|
|
919
|
+
*/
|
|
920
|
+
/** The probe as its two consumers below take it — injected ONLY so a unit test can drive the
|
|
921
|
+
* composition without a live `users.info`. Every live call site lets it default. */
|
|
922
|
+
export type SlackMemberProbe = (userId: string, teamId: string | undefined) => Promise<boolean | undefined>;
|
|
923
|
+
/**
|
|
924
|
+
* BOTH facts a chat wake site needs about its trigger author, from ONE probe call.
|
|
925
|
+
*
|
|
926
|
+
* `surfaceInternal` is the surface fact {@link resolveWakeTrust} takes: the room is internal AND the
|
|
927
|
+
* author is not a positively-identified non-member.
|
|
928
|
+
*
|
|
929
|
+
* 🔴 IT NARROWS THE WAKE TIER ONLY, never the wake AUTH. The `internal` verdict also stamps
|
|
930
|
+
* `trustChecked`/`isExternal` onto the delivery auth, which is what gives the whole channel session
|
|
931
|
+
* its memory tier. A guest posting one message must not demote a channel's memory for everybody, so
|
|
932
|
+
* the auth keeps reading the ROOM's verdict and only the per-message trust tier reads this one.
|
|
933
|
+
*
|
|
934
|
+
* `refuted` is the separate question the chat-taint join asks (access/taint-signal.ts `nonMemberChatTrust`): did the
|
|
935
|
+
* probe POSITIVELY say this person is not a member? The two are not the same predicate and must not
|
|
936
|
+
* be re-derived from each other — `surfaceInternal: false` is also what an EXTERNAL ROOM produces,
|
|
937
|
+
* and an external room is not evidence about the author at all.
|
|
938
|
+
*
|
|
939
|
+
* Deriving both here, from one verdict, is what stops them disagreeing. The probe is TTL-cached, so
|
|
940
|
+
* a second call would be cheap but a second READ would be a second chance to be inconsistent.
|
|
941
|
+
*/
|
|
942
|
+
export declare function slackWakeAuthorFacts(channelInternal: boolean, userId: string | undefined, teamId: string | undefined, probe?: SlackMemberProbe): Promise<{
|
|
943
|
+
readonly surfaceInternal: boolean;
|
|
944
|
+
readonly refuted: boolean;
|
|
945
|
+
}>;
|
|
946
|
+
/**
|
|
947
|
+
* The resolved external-participant policy for one room: the ORG DEFAULT, with the DOMAIN OVERRIDE
|
|
948
|
+
* applied when the domain registry can be read. An unreadable registry degrades to the org default
|
|
949
|
+
* rather than throwing — the policy plane must never be the thing that breaks a wake.
|
|
950
|
+
*/
|
|
951
|
+
export declare function slackExternalPolicyForRoom(roomKey: string): ExternalPolicy;
|
|
952
|
+
/**
|
|
953
|
+
* THE EXTERNAL-PARTICIPANT VERDICT for one Slack message — pure, so the four outcomes are tested
|
|
954
|
+
* without a Slack call, a store or a live workspace. The four values name WHAT THE CALLER OWES; the
|
|
955
|
+
* reasoning is stated once, on discord-gateway.ts's `DiscordExternalVerdict`.
|
|
956
|
+
*
|
|
957
|
+
* 🔴 THE AUTHOR GRAIN IS TESTED FIRST, AND THE ORDER MATTERS — an excluded author is owed a notice
|
|
958
|
+
* they can act on, where the room announcement tells the whole room something about the room.
|
|
959
|
+
*/
|
|
960
|
+
export type SlackExternalVerdict = "proceed" | "withheld-addressed" | "withheld-ambient" | "silent-room";
|
|
961
|
+
export declare function slackExternalVerdict(input: {
|
|
962
|
+
readonly policy: ExternalPolicy;
|
|
963
|
+
readonly author: IngestedChatMessage;
|
|
964
|
+
/** The message @mentions the bot, or arrived in a DM — an ADDRESSED message is owed an answer. */
|
|
965
|
+
readonly addressed: boolean;
|
|
966
|
+
readonly isDm: boolean;
|
|
967
|
+
/** The SURFACE's vendor verdict — Slack Connect's `is_ext_shared`. Absent means "not reported",
|
|
968
|
+
* which includes "the probe could not answer": this leg fails OPEN. */
|
|
969
|
+
readonly vendorExternal?: boolean;
|
|
970
|
+
/** The DOMAIN-grain surface key (a thread asks its parent channel), for the room classification. */
|
|
971
|
+
readonly roomKey: string;
|
|
972
|
+
}): SlackExternalVerdict;
|
|
973
|
+
/**
|
|
974
|
+
* Tell an EXCLUDED sender, PRIVATELY, that the agent will not act on their message (behaviour 1).
|
|
975
|
+
* Best-effort. Answers whether the notice REACHED the sender.
|
|
976
|
+
*
|
|
977
|
+
* 🔴 THIS NOTICE NEVER GOES TO THE CHANNEL, AND THAT RULING STANDS. Its text names the organisation
|
|
978
|
+
* ("messages from outside this organisation"), so posting it where the room can read it would
|
|
979
|
+
* announce that this person is classified as an outsider — a disclosure about a member of the room,
|
|
980
|
+
* not a notice to the sender. If `conversations.open` fails (no token, the sender cannot be DM'd,
|
|
981
|
+
* Slack refuses), THIS notice is not said anywhere.
|
|
982
|
+
*
|
|
983
|
+
* 🔴 THE `false` RETURN IS LOAD-BEARING (OPE-885). It does not mean "say nothing at all" any more.
|
|
984
|
+
* An EXPLICITLY ADDRESSED wake that this notice could not reach falls back to the room's fixed
|
|
985
|
+
* `EXTERNAL_MENTION_ACK`, which is a different string precisely because it discloses nothing — see
|
|
986
|
+
* `applySlackExternalPolicy`. So the caller needs the truth about delivery, and every failure arm
|
|
987
|
+
* here answers `false` rather than swallowing into a silent `void`.
|
|
988
|
+
*
|
|
989
|
+
* A message that ARRIVED in a DM needs no open — it posts straight back into that conversation.
|
|
990
|
+
*/
|
|
991
|
+
export declare function postSlackExternalSenderNotice(userId: string | undefined, dmChannelId?: string, seams?: {
|
|
992
|
+
readonly open?: (userId: string) => Promise<string | undefined>;
|
|
993
|
+
readonly post?: (channelId: string, text: string) => Promise<boolean>;
|
|
994
|
+
}): Promise<boolean>;
|
|
995
|
+
/**
|
|
996
|
+
* Post the SUPPRESSED-WAKE half of the silent-room announcement pair (OPE-504) — AT MOST ONCE PER
|
|
997
|
+
* ROOM, EVER. Slack had the JOIN half only ({@link announceSilentJoin}); this is its twin, and the
|
|
998
|
+
* pair is now the closed set `SILENT_NOTICE_KINDS` names.
|
|
999
|
+
*
|
|
1000
|
+
* FAIL-CLOSED on the claim, which is the inverse of `postOnce`'s posture and deliberate: a duplicate
|
|
1001
|
+
* here is not a lost message, it is exactly the repetition the pair exists to avoid.
|
|
1002
|
+
*/
|
|
1003
|
+
export declare function announceSlackSilentRoom(org: string, surfaceKey: string, channelId: string, store?: {
|
|
1004
|
+
claimNotice: (org: string, key: string, at: string) => Promise<boolean>;
|
|
1005
|
+
}, post?: (channelId: string, text: string) => Promise<void>): Promise<boolean>;
|
|
1006
|
+
/** The one call each Slack wake path makes once its author evidence is in hand: decide, pay what the
|
|
1007
|
+
* verdict owes, and answer whether the path must stand down. Exported for slack-front.ts, whose DM
|
|
1008
|
+
* seat and native-mention forward are the two paths that do not live in this file. */
|
|
1009
|
+
export declare function applySlackExternalPolicy(ev: ParsedMessageEvent, p: Partition, facts: {
|
|
1010
|
+
readonly trust: WakeTrust;
|
|
1011
|
+
readonly refuted: boolean;
|
|
1012
|
+
}, route: string): Promise<SlackExternalVerdict>;
|
|
1013
|
+
/**
|
|
1014
|
+
* Withhold an EXCLUDED author's HYDRATED CONTEXT LINES on the name-drop wake (OPE-525) — the
|
|
1015
|
+
* side-channel slot `withholdWakeBodies` cannot fill on its own.
|
|
1016
|
+
*
|
|
1017
|
+
* 🔴 WHY THE TRANSFORM NEEDS HELP HERE. Its automatic leg reaches an author whose ENVELOPE message
|
|
1018
|
+
* it withheld, and reads their id off that message. A name-drop's hydration quotes people who wrote
|
|
1019
|
+
* nothing in the envelope at all, so there is no withheld message to read an id from. This resolves
|
|
1020
|
+
* the answer for each distinct hydrated sender and hands the transform the sync predicate it takes.
|
|
1021
|
+
*
|
|
1022
|
+
* 🔴 A POSITIVE REFUTATION ONLY, AND ONLY FOR A HUMAN SENDER. `probe` returning `undefined` (no
|
|
1023
|
+
* token, an API error) is NOT a refutation — absence of evidence, and this leg fails OPEN like every
|
|
1024
|
+
* other in the plane. A `bot` line carries a bot id, and a machine author is never excluded, so only
|
|
1025
|
+
* `senderType: "user"` lines are asked about.
|
|
1026
|
+
*
|
|
1027
|
+
* COSTS NOTHING ON THE SHIPPED GRAIN: `taint` returns the wake by reference before any probe runs.
|
|
1028
|
+
* On `withhold` it costs one TTL-cached `users.info` per DISTINCT hydrated author, resolved in
|
|
1029
|
+
* parallel, and the render budget caps the hydration at ten lines.
|
|
1030
|
+
*/
|
|
1031
|
+
export declare function withholdNameDropContext(nw: NormalizedWake, policy: ExternalPolicy, teamId: string | undefined, probe?: SlackMemberProbe): Promise<NormalizedWake>;
|
|
1032
|
+
/**
|
|
1033
|
+
* Re-grade a first-wake BACKSCROLL bundle's per-message trust with the same per-author probe
|
|
1034
|
+
* (OPE-466). `backscrollTrustResolver` is SYNCHRONOUS and takes the ROOM's internal verdict, so every
|
|
1035
|
+
* sibling in a confirmed-internal channel resolved `peer` — including a guest's. This runs after the
|
|
1036
|
+
* fetch, where the author ids are in hand, and demotes only what the probe positively refutes.
|
|
1037
|
+
*
|
|
1038
|
+
* ONE ANSWER FOR BOTH CONSUMERS, and that is why it re-grades the array rather than computing a
|
|
1039
|
+
* side-channel boolean: the demoted tier reaches the RENDER (the model sees `trust="unauth"` on that
|
|
1040
|
+
* line, and the envelope's reference-only `<system-note>` appears) and the taint JOIN reads the same
|
|
1041
|
+
* field. A boolean computed beside the array would let the two disagree.
|
|
1042
|
+
*
|
|
1043
|
+
* ONLY EVER DOWN. `relay` (machine authors) and an already-`unauth` sibling are untouched, and
|
|
1044
|
+
* nothing is ever promoted. Distinct authors only — a bundle re-quoting one person costs one probe,
|
|
1045
|
+
* and the probe is TTL-cached on top of that. Fail-OPEN: an unresolvable author keeps its tier.
|
|
1046
|
+
*/
|
|
1047
|
+
export declare function regradeSlackBackscrollTrust(messages: readonly WakeMessageInput[], teamId: string | undefined, probe?: SlackMemberProbe): Promise<readonly WakeMessageInput[]>;
|
|
1048
|
+
/**
|
|
1049
|
+
* Resolve + regrade PER-LINE trust for a name-drop wake's {@link ContextLine} backscroll (OPE-475)
|
|
1050
|
+
* — the ContextLine analogue of {@link regradeSlackBackscrollTrust}, converging the two backscroll
|
|
1051
|
+
* shapes on ONE trust story instead of a second mechanism. The same two steps, in the same order:
|
|
1052
|
+
*
|
|
1053
|
+
* 1. every `senderType==="user"` line starts at the tier {@link resolveWakeTrust} gives a
|
|
1054
|
+
* positively-confirmed-internal channel (peer, or principal for a domain admin) — the SAME
|
|
1055
|
+
* synchronous resolution {@link backscrollTrustResolver} runs for the graded WakeMessageInput
|
|
1056
|
+
* shape, just applied per ContextLine instead of per WakeMessageInput;
|
|
1057
|
+
* 2. the SAME per-author probe ({@link slackAuthorIsMember}) then demotes any REFUTED non-member
|
|
1058
|
+
* to `trust:"unauth", refuted:true` — set together on the SAME line the render reads
|
|
1059
|
+
* ({@link renderContext}'s `trust="unauth"` marker + reference-only note), so the render and
|
|
1060
|
+
* the taint join can never disagree about one line.
|
|
1061
|
+
*
|
|
1062
|
+
* `agent`/`bot`/`unknown` lines are left untouched (no `trust`, no `refuted`) — machine or
|
|
1063
|
+
* unattributable authors are not this leg's question, matching {@link backscrollTrustResolver}'s
|
|
1064
|
+
* own machine-author floor. FAIL-OPEN: an unresolvable probe (network error, no token) leaves a
|
|
1065
|
+
* line at its pre-probe tier — the SAME carriage rule the taint join itself uses, never a demotion
|
|
1066
|
+
* on an assumption.
|
|
1067
|
+
*/
|
|
1068
|
+
export declare function regradeSlackContextLineTrust(lines: readonly ContextLine[], channelConfirmedInternal: boolean, channelKey: string | undefined, teamId: string | undefined, probe?: SlackMemberProbe): Promise<readonly ContextLine[]>;
|
|
1069
|
+
/**
|
|
1070
|
+
* The LIVE single-window mention decision for one Slack @mention — the Slack arm of the shared
|
|
1071
|
+
* seam (unified-window.ts {@link singleWindowMentionPolicy}), mirroring the GitHub reference
|
|
1072
|
+
* (github.ts shouldSuppressNativeMention / ambientDelivery). Consulted by BOTH of slack-front's
|
|
1073
|
+
* mention legs — the `app_mention` native-forward (suppress?) and its `message.channels` sibling
|
|
1074
|
+
* (keep as an addressed wake?) — so the two stay exactly complementary: one call shape, one
|
|
1075
|
+
* TTL-cached channel fact underneath.
|
|
1076
|
+
*
|
|
1077
|
+
* Inputs to the seam:
|
|
1078
|
+
* • `singleWindow` — does this surface have a STANDING SEAT (the domain grain's window bit)? Since
|
|
1079
|
+
* OPE-1695 that is every channel the bot is in, and the chattiness flag has no part in it: a
|
|
1080
|
+
* mention wakes the room's own seat whether or not ambient talk does. Any config error ⇒ false —
|
|
1081
|
+
* the native split, never a throw on the webhook path.
|
|
1082
|
+
* • `privacyOk` — Slack's POSITIVE privacy fact, by exact analogy to GitHub's
|
|
1083
|
+
* `repository.private === true` floor: the channel must be POSITIVELY CONFIRMED INTERNAL
|
|
1084
|
+
* (the same fail-closed TTL-cached conversations.info gate the supervisor wake + wake-trust
|
|
1085
|
+
* paths use — externally-shared/unconfirmable ⇒ false) AND be a real channel/group (the exact
|
|
1086
|
+
* surface {@link shouldSupervisorWake} accepts — an mpim/im mention must keep its native path
|
|
1087
|
+
* or suppressing it would gap, since the supervisor gate would drop the sibling).
|
|
1088
|
+
*
|
|
1089
|
+
* Fail-closed throughout: unknown privacy, config error, or API failure all yield today's
|
|
1090
|
+
* behaviour (no suppression, no addressed wake). The non-single-window fast path returns without
|
|
1091
|
+
* any Slack API call, so every ordinary workspace mention stays byte-identical AND call-free.
|
|
1092
|
+
*/
|
|
1093
|
+
export declare function slackSingleWindowMentionPolicy(channelId: string, surfaceKey: string): Promise<SingleWindowMentionPolicy>;
|
|
1094
|
+
/**
|
|
1095
|
+
* The two facts the DM-FAMILY front route needs about a conversation it cannot classify from the
|
|
1096
|
+
* envelope: its CHANNEL TYPE (`channel` | `group` | `im` | `mpim`, `undefined` when undetermined) and
|
|
1097
|
+
* whether the trust gate POSITIVELY confirms it internal. OPE-351 stage 4.
|
|
1098
|
+
*
|
|
1099
|
+
* WHY IT EXISTS AT ALL: **a raw `app_mention` envelope carries no `channel_type`**, so the front route
|
|
1100
|
+
* cannot tell an @mention in a GROUP DM from an @mention in a channel — and the two now need opposite
|
|
1101
|
+
* routing (a group DM's mention must land in the mpim's own standing window; a channel's must not).
|
|
1102
|
+
* The internal bit rides along because it comes from the SAME response and the mpim wake needs it for
|
|
1103
|
+
* `resolveWakeTrust`: a group DM has no single "partner" to trust-check the way a 1:1 does, so the
|
|
1104
|
+
* surface's own confirmed-internal verdict is what stands in — the same substitution
|
|
1105
|
+
* {@link routeSupervisorEvent} makes for a channel.
|
|
1106
|
+
*
|
|
1107
|
+
* WHY AN API CALL IS ACCEPTABLE HERE, having been deliberately avoided in
|
|
1108
|
+
* {@link slackSingleWindowMentionPolicy}. That function's common-path cost is genuinely zero because
|
|
1109
|
+
* it short-circuits on a SYNC config read. This one cannot: the fact it needs lives only in Slack. But
|
|
1110
|
+
* it is the SAME TTL-cached `conversations.info` the trust gate, the supervisor wake and the join intro
|
|
1111
|
+
* already share, and — the point that settles it — eve's native mention path resolves the identical
|
|
1112
|
+
* fact per mention through `enrichVisibility` (channels/slack.ts) with NO cache at all. A cached read
|
|
1113
|
+
* at the front is therefore strictly cheaper than the uncached read the forward it replaces triggers.
|
|
1114
|
+
*
|
|
1115
|
+
* FAIL-SOFT, AND THE CALLER MUST READ THAT AS "keep today's routing". A workspace whose app predates
|
|
1116
|
+
* the `mpim:read` scope (added to the manifest at OPE-332) gets `missing_scope` here on every group-DM
|
|
1117
|
+
* lookup: this answers `{ internal: false }` with NO channelType, the mention forwards to the native
|
|
1118
|
+
* path exactly as it did before stage 4, and the caller logs the miss by name — degraded, not broken,
|
|
1119
|
+
* and never a crash on an absent field.
|
|
1120
|
+
*
|
|
1121
|
+
* `appCanRead` is the THIRD fact, and it is fail-CLOSED rather than fail-soft: see
|
|
1122
|
+
* {@link appCanReadConversation}. It answers "can this app read this room at all", which the
|
|
1123
|
+
* internal/external verdict does not — a public channel the app never joined is internal AND
|
|
1124
|
+
* unreadable. The shortcut route (slack-front.ts, OPE-1442) reads it to decide whether the target
|
|
1125
|
+
* message's FILES may be registered and fetched with the bot token.
|
|
1126
|
+
*
|
|
1127
|
+
* `extShared` and `trustChecked` are the RAW platform halves `internal` joins away (OPE-1486). The
|
|
1128
|
+
* group-DM wake stamps them onto its auth so `bindSlack` grades an mpim on the same two facts a
|
|
1129
|
+
* channel is graded on — which is what lets the member plane's G4b demote a Slack Connect group DM
|
|
1130
|
+
* out of a workspace-wide `internal` declaration. Both are undefined when the probe answered
|
|
1131
|
+
* nothing, and an undefined one stamps nothing: absence never demotes.
|
|
1132
|
+
*/
|
|
1133
|
+
export declare function slackChannelFactsFor(channelId: string): Promise<{
|
|
1134
|
+
internal: boolean;
|
|
1135
|
+
channelType?: string;
|
|
1136
|
+
appCanRead: boolean;
|
|
1137
|
+
extShared?: boolean;
|
|
1138
|
+
trustChecked?: boolean;
|
|
1139
|
+
}>;
|
|
1140
|
+
/**
|
|
1141
|
+
* SYNC predicate: does this surface route its wakes through a standing seat's WINDOW (a solo
|
|
1142
|
+
* per-surface window, a named group, or a cross-surface one)? True ⇔ the supervisor grain uses a
|
|
1143
|
+
* window — the SAME fact {@link slackSingleWindowMentionPolicy}'s `singleWindow` reads, without the
|
|
1144
|
+
* async privacy floor. A seated surface's mentions divert into its seat and its thread follow-ups
|
|
1145
|
+
* ride the seat (never a native per-thread session), so slack-front uses this to keep an ENGAGED
|
|
1146
|
+
* thread on the supervisor path rather than misrouting it to routeMessageEvent (task #100). Any
|
|
1147
|
+
* config-load error ⇒ false (fail-safe to the native per-channel split).
|
|
1148
|
+
*/
|
|
1149
|
+
export declare function slackDomainUsesUnifiedWindow(surfaceKey: string): boolean;
|
|
1150
|
+
export declare function slackForm(method: string, body: string, token?: string | undefined): Promise<Record<string, unknown> | undefined>;
|
|
1151
|
+
/**
|
|
1152
|
+
* Render a `conversations.history` response (newest-first, raw Slack JSON — probe,
|
|
1153
|
+
* never assume) as a compact oldest-first transcript, or undefined when there's
|
|
1154
|
+
* nothing readable. System subtypes (joins/renames/…) are noise and dropped; when
|
|
1155
|
+
* the block cap trims, it trims the OLDEST lines — the recent tail is what an intro
|
|
1156
|
+
* needs. Pure, so the shape is unit-testable without Slack.
|
|
1157
|
+
*/
|
|
1158
|
+
export declare function transcriptFromHistory(history: unknown): string | undefined;
|
|
1159
|
+
export declare function fetchUserProfileNames(userId: string): Promise<{
|
|
1160
|
+
name?: string;
|
|
1161
|
+
handle?: string;
|
|
1162
|
+
} | undefined>;
|
|
1163
|
+
type RosterEntry = {
|
|
1164
|
+
nativeId: string;
|
|
1165
|
+
name?: string;
|
|
1166
|
+
handle?: string;
|
|
1167
|
+
};
|
|
1168
|
+
/** Test seam / operator escape: drop the memoized roster so the next search re-reads Slack. */
|
|
1169
|
+
export declare function _resetWorkspaceRosterCache(): void;
|
|
1170
|
+
/**
|
|
1171
|
+
* Every human account in the workspace, as `{nativeId, name, handle}` — fail-soft to `[]` on a
|
|
1172
|
+
* missing scope, an API error, or a throw. The CALLER does the matching.
|
|
1173
|
+
*
|
|
1174
|
+
* Memoized for {@link SLACK_ROSTER_TTL_MS} (see the section header for why that TTL and not the
|
|
1175
|
+
* process-lifetime memo the profile cache uses). Concurrent callers inside one window share ONE
|
|
1176
|
+
* in-flight walk, so a burst cannot multiply the Tier-2 calls the cache exists to collapse.
|
|
1177
|
+
*/
|
|
1178
|
+
export declare function fetchWorkspaceRoster(): Promise<readonly RosterEntry[]>;
|
|
1179
|
+
export declare function fetchUserDisplayName(userId: string): Promise<string | undefined>;
|
|
1180
|
+
export declare function fetchChannelDisplayName(channelId: string): Promise<string | undefined>;
|
|
1181
|
+
/**
|
|
1182
|
+
* Live Slack roster for a governance channel's floor tripwire (OPE-110, access/audience-floor.ts).
|
|
1183
|
+
* Two fail-closed Slack reads: conversations.info (is_private + externally-shared bit) and
|
|
1184
|
+
* conversations.members (member ids). The bot's own user id is excluded so the agent doesn't count
|
|
1185
|
+
* as a non-admin member. FAIL CLOSED: any failed/absent lookup ⇒ `resolved: false` ⇒ the floor can
|
|
1186
|
+
* never arm. `hasGuest` uses the channel's externally-shared bit as the available external signal;
|
|
1187
|
+
* per-member guest classification rides the guest-detection work (design §2/§10 fragile-point #1) and
|
|
1188
|
+
* is a future tightening, not a loosening (an internal-only private channel already needs every
|
|
1189
|
+
* member on the roster to arm). Governance channels are small by config lint, so a single members
|
|
1190
|
+
* page suffices; a channel larger than one page fails closed on the (unset) cursor rather than
|
|
1191
|
+
* silently attesting a partial roster.
|
|
1192
|
+
*/
|
|
1193
|
+
export declare function slackGovernanceRoster(channelId: string): Promise<RoomRoster | undefined>;
|
|
1194
|
+
/**
|
|
1195
|
+
* Register {@link slackGovernanceRoster} as the governance floor-tripwire member supplier (OPE-110).
|
|
1196
|
+
* Core owns the seam + the fail-closed default; this is the Slack arm, wired at boot from
|
|
1197
|
+
* register-adapters via the adapter-api barrel (the app never reaches into channels/ directly — the
|
|
1198
|
+
* boundary rule). v1 is Slack-only (design fragile-point #1: membership APIs are the ground truth,
|
|
1199
|
+
* Slack/Discord/Teams first); a non-slack channel key returns undefined ⇒ that governance channel
|
|
1200
|
+
* simply can't arm yet, never a false attest.
|
|
1201
|
+
*/
|
|
1202
|
+
export declare function registerSlackGovernanceMembers(): void;
|
|
1203
|
+
/** Production deps for the name-drop route: domain config + live Slack confirmation. */
|
|
1204
|
+
export declare function liveNameDropDeps(send: WakeSend): NameDropRouteDeps;
|
|
1205
|
+
/** The reserved pseudo-thread sentinel for a supervisor generation — seeded into
|
|
1206
|
+
* `state.threadTs` (non-empty) to make eve's re-anchor structurally unreachable (amendment
|
|
1207
|
+
* A). Deliberately not a real Slack ts, so it can never collide with a thread token. */
|
|
1208
|
+
export declare function supervisorSentinel(generation: number): string;
|
|
1209
|
+
/** The reserved continuation token for a channel's supervisor session at a given generation:
|
|
1210
|
+
* `${channelId}:oe-sup:g<generation>` — built via {@link slackContinuationToken} so it shares
|
|
1211
|
+
* eve's exact `${channelId}:${threadTs}` namespacing with thread tokens (the threadTs part is
|
|
1212
|
+
* the sentinel, which no real thread can ever be). */
|
|
1213
|
+
export declare function supervisorToken(channelId: string, generation: number): string;
|
|
1214
|
+
/**
|
|
1215
|
+
* The FIFO serialization key for a channel's supervisor wakes (slack-front.ts
|
|
1216
|
+
* `scheduleUnownedWake`) — deliberately GENERATION-INDEPENDENT, unlike {@link supervisorToken}.
|
|
1217
|
+
* The FIFO's job is to serialize supervisor delivery for one channel, and that must hold ACROSS
|
|
1218
|
+
* a rotation boundary (T1.5): keying on the generation-bearing token would split one channel's
|
|
1219
|
+
* serialized queue in two the instant a rotation minted a new generation, letting a pre- and
|
|
1220
|
+
* post-rotation wake run concurrently. Keying on the channel alone fixes that AND needs no store
|
|
1221
|
+
* read on the hot path (the earlier hardcode assumed generation was always 1 pre-rotation — see
|
|
1222
|
+
* the T1.3 comment this replaces).
|
|
1223
|
+
*/
|
|
1224
|
+
export declare function supervisorFifoKey(channelId: string): string;
|
|
1225
|
+
/** Inputs to the pure supervisor-wake gate — staged by {@link routeSupervisorEvent}. */
|
|
1226
|
+
export interface SupervisorWakeInput {
|
|
1227
|
+
readonly authorIsBot: boolean;
|
|
1228
|
+
readonly isAppMention: boolean;
|
|
1229
|
+
readonly subtype?: string;
|
|
1230
|
+
readonly channelType?: string;
|
|
1231
|
+
readonly channelConfirmedInternal: boolean;
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* The supervisor-wake gate. UNLIKE {@link shouldNameDropWake} there is NO name-regex needle —
|
|
1235
|
+
* the supervisor judges EVERY unowned message (always-on teammate). Disqualifiers, in cost
|
|
1236
|
+
* order: a non-edit subtype (system messages, and the bot's own posts arrive as subtypeless
|
|
1237
|
+
* `bot_message`s caught by authorIsBot) → a bot author (relay/`trust="relay"` is Phase 2, so
|
|
1238
|
+
* bot-authored still DROPS here) → an @mention (eve's own turn owns it) → non-channel/group
|
|
1239
|
+
* surfaces → the fail-closed internal-channel confirmation. A `message_changed` edit is KEPT
|
|
1240
|
+
* (the supervisor should notice edits — it wakes with `reason="message-edited"`).
|
|
1241
|
+
*/
|
|
1242
|
+
export declare function shouldSupervisorWake(input: SupervisorWakeInput): ReplyDecision;
|
|
1243
|
+
/** Dependencies for {@link routeSupervisorEvent} — injected so the chain is unit-testable. */
|
|
1244
|
+
export interface SupervisorRouteDeps {
|
|
1245
|
+
/** Positively confirm the channel is internal (fail-closed) AND surface its member count,
|
|
1246
|
+
* one cached conversations.info. Unconfirmable/external → { internal: false }. */
|
|
1247
|
+
readonly confirmInternalWithMeta: (channelId: string) => Promise<{
|
|
1248
|
+
internal: boolean;
|
|
1249
|
+
memberCount?: number;
|
|
1250
|
+
}>;
|
|
1251
|
+
/**
|
|
1252
|
+
* Create-or-fetch the live supervisor session for (org, surfaceKey). Called ONLY after the
|
|
1253
|
+
* internal confirmation passes, so an external channel never mints a registry row. Three
|
|
1254
|
+
* outcomes, all reported via `freshGeneration`: (1) no row yet → mint generation 1; (2) an
|
|
1255
|
+
* existing row that {@link shouldRotate} says has aged out (expiry, quiet-gap or a backstop) →
|
|
1256
|
+
* rotate to generation+1; (3) an existing row that's still current → returned as-is.
|
|
1257
|
+
* `freshGeneration` is true for (1) and (2) — a BRAND-NEW OR JUST-ROTATED generation's first
|
|
1258
|
+
* wake carries channel backscroll (Tag §4.1); false for (3), an ongoing generation's later
|
|
1259
|
+
* wake, which carries none.
|
|
1260
|
+
*/
|
|
1261
|
+
readonly getOrCreateSession: (org: string, surfaceKey: string, channelId: string, atIso: string) => Promise<{
|
|
1262
|
+
token: string;
|
|
1263
|
+
generation: number;
|
|
1264
|
+
freshGeneration: boolean;
|
|
1265
|
+
tail: readonly WakeMessageInput[];
|
|
1266
|
+
/** ISO-8601 of the most recent wake this generation delivered (the registry row's
|
|
1267
|
+
* `lastWakeAt`), or null on a freshly minted/rotated generation. The lower bound of the
|
|
1268
|
+
* since-last-wake catch-up read (OPE-1695 (b)); absent ⇒ the bootstrap window. */
|
|
1269
|
+
lastWakeAt?: string | null;
|
|
1270
|
+
}>;
|
|
1271
|
+
/** Record one wake on the current generation (lastWakeAt/turnCount). Best-effort. */
|
|
1272
|
+
readonly touchSession?: (org: string, surfaceKey: string, atIso: string) => Promise<void>;
|
|
1273
|
+
/**
|
|
1274
|
+
* Fetch the seat's channel backscroll as envelope siblings (fetchWakeEnvelopeBackscroll). ONE
|
|
1275
|
+
* read: a FRESH generation's BOOTSTRAP window. It used to serve the since-last-wake catch-up too,
|
|
1276
|
+
* through the `sinceMs` arm below; OPE-1726 moved that read to {@link catchUp} and the shared core
|
|
1277
|
+
* path, so no caller sets `sinceMs` any more. Optional so older test doubles keep compiling
|
|
1278
|
+
* (absent ⇒ no backscroll, same as an empty result); fail-soft by construction (the live impl
|
|
1279
|
+
* never throws).
|
|
1280
|
+
*/
|
|
1281
|
+
readonly fetchBackscroll?: (input: {
|
|
1282
|
+
channelId: string;
|
|
1283
|
+
ts: string;
|
|
1284
|
+
/** The route's confirmInternal verdict for this surface — the SAME fact the trigger trust
|
|
1285
|
+
* uses (F3). The supervisor fetch only runs when internal, so it's true here; passing it
|
|
1286
|
+
* keeps the backscroll resolver deriving from the caller's real verdict, not a hardcode. */
|
|
1287
|
+
surfaceInternal: boolean;
|
|
1288
|
+
/** Domain-grain channel key (`platform/workspace/surface`) so a DOMAIN-admin backscroll author
|
|
1289
|
+
* resolves `principal`, not just org-roster admins (matches the trigger trust, which has it).
|
|
1290
|
+
* It also selects the domain's ADMISSION policy in the live wiring. */
|
|
1291
|
+
channelKey?: string;
|
|
1292
|
+
/** 🔴 DEAD since OPE-1726 — no caller sets it, and the live impl's bootstrap arm is the only one
|
|
1293
|
+
* reached. It was the catch-up's lower bound when Slack owned a private copy of that read; the
|
|
1294
|
+
* read now runs through {@link catchUp} and core `seatCatchUp`. Kept only so an older test
|
|
1295
|
+
* double that still passes it keeps compiling. Do NOT build a new catch-up on it: a window
|
|
1296
|
+
* fetched here bypasses the shared bounds, the domain's admission policy and the token cap. */
|
|
1297
|
+
sinceMs?: number;
|
|
1298
|
+
}) => Promise<readonly WakeMessageInput[]>;
|
|
1299
|
+
/**
|
|
1300
|
+
* THE SINCE-LAST-WAKE CATCH-UP, through the ONE shared path every surface now takes
|
|
1301
|
+
* (channels/seat-catch-up.ts `seatCatchUp`). It replaces the `sinceMs` arm of
|
|
1302
|
+
* {@link fetchBackscroll}, which was Slack's private copy of a read five other surfaces did not
|
|
1303
|
+
* have: the bounds, the domain's admission policy and the token cap live in core, and Slack
|
|
1304
|
+
* contributes only its registered fetch.
|
|
1305
|
+
* Optional so older test doubles keep compiling (absent ⇒ no catch-up, which is what an ongoing
|
|
1306
|
+
* generation's wake carried before OPE-1695). Fail-soft by construction.
|
|
1307
|
+
*/
|
|
1308
|
+
readonly catchUp?: (req: SeatCatchUpRequest) => Promise<SeatCatchUpResult>;
|
|
1309
|
+
/** The per-author membership probe the NAME-DROP hydration's withhold uses (OPE-525,
|
|
1310
|
+
* {@link withholdNameDropContext}) — the same dep, spelled the same way, as the native route it
|
|
1311
|
+
* replaces ({@link NameDropRouteDeps.authorIsMember}). Defaults to the live
|
|
1312
|
+
* {@link slackAuthorIsMember}; injectable so the withhold is testable without Slack. Never called
|
|
1313
|
+
* on the shipped `taint` grain, and never on a wake that carries no hydrated lines. */
|
|
1314
|
+
readonly authorIsMember?: SlackMemberProbe;
|
|
1315
|
+
/**
|
|
1316
|
+
* Start/continue the supervisor session under its reserved token. `window` is present ONLY on
|
|
1317
|
+
* the SINGLE-WINDOW unified path (unifiedMeta reported `singleWindow` — channels/
|
|
1318
|
+
* unified-window.ts): the wakeId already rendered into this wake's envelope plus the wake's
|
|
1319
|
+
* reply-target origin, which the live wiring registers in the durable map by DIVERTING the
|
|
1320
|
+
* wake through eve's cross-channel `receive` into the synthetic `unified` channel instead of
|
|
1321
|
+
* this channel's own `send`. Absent ⇒ today's send path, byte-identical.
|
|
1322
|
+
*/
|
|
1323
|
+
readonly wake: (input: {
|
|
1324
|
+
token: string;
|
|
1325
|
+
auth: unknown;
|
|
1326
|
+
message: string;
|
|
1327
|
+
/** The untrusted-wrapped side-channel context block(s) — the NAME-DROP hydration this seat wake
|
|
1328
|
+
* carries (OPE-1725), already withheld. Omitted on every other seat wake, so the payload stays
|
|
1329
|
+
* message-only and byte-identical to today. */
|
|
1330
|
+
context?: readonly string[];
|
|
1331
|
+
state: unknown;
|
|
1332
|
+
window?: UnifiedWindowWake;
|
|
1333
|
+
}) => Promise<void>;
|
|
1334
|
+
/** Append a durable audit row (e.g. `engagement.stop`). Optional; best-effort. */
|
|
1335
|
+
readonly audit?: (action: string, detail: Record<string, unknown>) => Promise<void>;
|
|
1336
|
+
/**
|
|
1337
|
+
* ENGAGE the trigger's thread as a PARTICIPATING surface (task #100 — window-riding stop parity):
|
|
1338
|
+
* called ONLY on an ADDRESSED `directMention` so a mention diverted into the window enrolls its
|
|
1339
|
+
* thread exactly as the native path does (slack.ts onInbound `markThreadEngaged`) and the Discord
|
|
1340
|
+
* divert does (divertDiscordWakeToUnifiedWindow, commit d4d6a17). That row is what the stop leg
|
|
1341
|
+
* ({@link getEngagement}) reads on a later bare "stop". Optional + best-effort; absent ⇒ no enroll.
|
|
1342
|
+
*/
|
|
1343
|
+
readonly markEngaged?: (p: Partition, threadTs: string, atIso: string, opts?: {
|
|
1344
|
+
participating?: boolean;
|
|
1345
|
+
}) => Promise<void>;
|
|
1346
|
+
/** Read the trigger thread's engagement record — the ONE read shared by the stop leg AND the
|
|
1347
|
+
* stopped-suppression gate (task #100 + follow-up). A stop only acks a thread the supervisor is
|
|
1348
|
+
* participating in; a NON-mention message in a `stopped` thread is dropped before any wake.
|
|
1349
|
+
* Optional so older test doubles keep compiling (absent ⇒ neither leg fires); live wiring passes
|
|
1350
|
+
* the real store call. */
|
|
1351
|
+
readonly getEngagement?: (p: Partition, threadTs: string) => Promise<EngagementRow | null>;
|
|
1352
|
+
/** Mark the trigger thread STOPPED (task #100 follow-up stop leg). On the window path a stop
|
|
1353
|
+
* must NOT delete the row — the durable `stopped` marker is what the suppression gate reads to
|
|
1354
|
+
* drop later non-mention wakes (a deleted row suppresses nothing here). Sets the flag, keeps the
|
|
1355
|
+
* row. Optional; best-effort. (Discord keeps DELETing via its own path — see disengageThread.) */
|
|
1356
|
+
readonly markStopped?: (p: Partition, threadTs: string) => Promise<boolean>;
|
|
1357
|
+
/** Clock seam (defaults to the real clock) — the createdAt/lastWakeAt + envelope current-time. */
|
|
1358
|
+
readonly now?: () => string;
|
|
1359
|
+
/** Resolve trigger + backscroll author display names for the envelope (fail-soft — absent or
|
|
1360
|
+
* throwing ⇒ raw ids render, the wake still fires). Live wiring: {@link fetchUserProfileNames}. */
|
|
1361
|
+
readonly resolveProfile?: ResolveProfile;
|
|
1362
|
+
/** Resolve the surface's engagement calibration tier (memory/domain.ts engagementTierFor) for
|
|
1363
|
+
* the channel-watch preamble. Optional + fail-soft: absent or throwing ⇒ neutral (the
|
|
1364
|
+
* pre-tier byte-identical preamble). Live wiring reads the loaded domain config. */
|
|
1365
|
+
readonly engagementTier?: (surfaceKey: string) => WakeEngagementTier;
|
|
1366
|
+
/**
|
|
1367
|
+
* UNIFIED cross-surface supervisor grain for this channel (channels/unified-supervisor.ts). Absent
|
|
1368
|
+
* or `{ unified: false }` ⇒ the per-container path, byte-identical to today. `{ unified: true }` ⇒
|
|
1369
|
+
* the wake is stamped with its `<channel platform>` address + the P5 disclosure preamble. This dep
|
|
1370
|
+
* controls only the WAKE render; the registry-row/token re-key lives inside getOrCreateSession /
|
|
1371
|
+
* touchSession (which receive the surfaceKey and resolve the same grain). Optional +
|
|
1372
|
+
* fail-soft: absent or throwing ⇒ not unified (the byte-identical path). Live wiring reads the
|
|
1373
|
+
* loaded domain config.
|
|
1374
|
+
*/
|
|
1375
|
+
readonly unifiedMeta?: (surfaceKey: string) => {
|
|
1376
|
+
unified: boolean;
|
|
1377
|
+
label?: string;
|
|
1378
|
+
singleWindow?: boolean;
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1382
|
+
* Route one unowned message to the channel supervisor: pure disqualifiers, then the
|
|
1383
|
+
* fail-closed internal-channel confirmation (which also surfaces the member count for the
|
|
1384
|
+
* envelope), then create-or-fetch the supervisor session and deliver the judged wake. The
|
|
1385
|
+
* caller (slack-front) only reaches here once the domain has opted into ambientSupervisor.
|
|
1386
|
+
*
|
|
1387
|
+
* `opts.directMention` (SINGLE-WINDOW @mention routing — the GitHub reference's
|
|
1388
|
+
* `treatMentionAsDirect`/`directMention` shape): the trigger is the `message.channels` SIBLING of
|
|
1389
|
+
* an `app_mention` that slack-front kept because {@link slackSingleWindowMentionPolicy} said the
|
|
1390
|
+
* unified window owns it (native forward suppressed). The wake then renders as an ADDRESSED
|
|
1391
|
+
* `direct-mention` (envelope + wake-id kept, silence bias dropped) and the minted window carries
|
|
1392
|
+
* `addressing:"direct-mention"` so the unified channel arms the reply bridge. The final say stays
|
|
1393
|
+
* with the SAME shared seam ({@link singleWindowMentionPolicy}) re-checked here against this
|
|
1394
|
+
* route's own singleWindow + internal facts — a directMention flag on a non-single-window or
|
|
1395
|
+
* non-internal wake degrades safely to the plain channel-watch treatment. Unset ⇒ byte-identical.
|
|
1396
|
+
*/
|
|
1397
|
+
export declare function routeSupervisorEvent(ev: ParsedMessageEvent, deps: SupervisorRouteDeps, opts?: {
|
|
1398
|
+
readonly directMention?: boolean;
|
|
1399
|
+
readonly nameDrop?: boolean;
|
|
1400
|
+
/**
|
|
1401
|
+
* THE NAME-DROP HYDRATION, HANDED ACROSS THE SEAT HAND-OFF (OPE-1725) — see
|
|
1402
|
+
* {@link NameDropContextHydrator}. Read only on the `nameDrop` tier, and only AFTER this route's
|
|
1403
|
+
* own tenant fence, spend gate and external-participant policy have admitted the wake, so a
|
|
1404
|
+
* refused seat wake buys no Slack page. Absent ⇒ no side-channel context block, which is every
|
|
1405
|
+
* other seat wake.
|
|
1406
|
+
*/
|
|
1407
|
+
readonly nameDropContext?: NameDropContextHydrator;
|
|
1408
|
+
}): Promise<RouteOutcome>;
|
|
1409
|
+
/**
|
|
1410
|
+
* Production deps for the supervisor route: the fail-closed internal+meta gate, a
|
|
1411
|
+
* create-or-fetch over the channel_session registry (generation 1 on first mint; rotates to
|
|
1412
|
+
* generation+1 when {@link shouldRotate} says the current generation has aged out — T1.5), a
|
|
1413
|
+
* best-effort touch, the live time-windowed first-wake backscroll fetcher, and a wake that
|
|
1414
|
+
* continues the session under its reserved token.
|
|
1415
|
+
*/
|
|
1416
|
+
export declare function liveSupervisorDeps(send: WakeSend): SupervisorRouteDeps;
|
|
1417
|
+
/** The per-turn reply target snapshotted at `turn.started` from the delivery's auth attributes
|
|
1418
|
+
* (`channel_id`/`thread_ts`). `threadTs` present ⇔ the triggering message was itself in a
|
|
1419
|
+
* thread; absent ⇔ it was a top-level channel message. */
|
|
1420
|
+
export interface SupervisorReplyTarget {
|
|
1421
|
+
readonly channelId?: string;
|
|
1422
|
+
readonly threadTs?: string;
|
|
1423
|
+
}
|
|
1424
|
+
/** The pure post plan for a supervisor turn. `post: null` = fail safe (no resolvable target →
|
|
1425
|
+
* post NOTHING). `bindThread` = the reply lands in a real thread and should mark it engaged
|
|
1426
|
+
* under the supervisor as owner (Tag "the channel session answers a cheap thread and now owns
|
|
1427
|
+
* it"); false for a top-level reply (binds nothing). */
|
|
1428
|
+
export interface SupervisorReplyPlan {
|
|
1429
|
+
readonly post: {
|
|
1430
|
+
readonly channel: string;
|
|
1431
|
+
readonly threadTs?: string;
|
|
1432
|
+
} | null;
|
|
1433
|
+
readonly bindThread: boolean;
|
|
1434
|
+
readonly reason: string;
|
|
1435
|
+
}
|
|
1436
|
+
/**
|
|
1437
|
+
* Pure: resolve WHERE a supervisor turn's reply posts, from the per-turn target snapshot.
|
|
1438
|
+
* FAIL SAFE: a missing/empty `channelId` yields `post: null` — the caller posts nothing rather
|
|
1439
|
+
* than guessing a target. A non-empty `threadTs` means the trigger was threaded ⇒ post into that
|
|
1440
|
+
* thread AND bind it; absent ⇒ a top-level channel post with no `thread_ts` and no bind.
|
|
1441
|
+
*/
|
|
1442
|
+
export declare function planSupervisorReply(target: SupervisorReplyTarget | undefined): SupervisorReplyPlan;
|
|
1443
|
+
/** Pure gate: is THIS turn a supervisor turn? The supervisor host-side reply branch (and the
|
|
1444
|
+
* `turn.started` target snapshot) fire ONLY when this is true, so a non-supervisor turn takes
|
|
1445
|
+
* eve's default reply path byte-identically. `oeSupervisor` is seeded by T1.3's wake `state`. */
|
|
1446
|
+
export declare function isSupervisorReplyTurn(state: unknown): boolean;
|
|
1447
|
+
/** Pure: the per-wake assistant-thread OVERRIDE for a DM-seat turn, or undefined when there is
|
|
1448
|
+
* none. Returns the snapshotted wake thread (`oeWakeThreadTs`) ONLY when it is present AND differs
|
|
1449
|
+
* from the session's persisted `threadTs` (the frozen seat anchor). Undefined for every non-DM
|
|
1450
|
+
* turn (`oeWakeThreadTs` unset), a DM whose wake IS the session thread (its first wake), and a
|
|
1451
|
+
* blank snapshot — every such case falls back to `state.threadTs`, byte-identical to pre-OPE-98. */
|
|
1452
|
+
export declare function dmWakeThreadOverride(state: unknown): string | undefined;
|
|
1453
|
+
/** Pure: the Slack thread a turn's reply + thinking-status must target, from the turn's channel
|
|
1454
|
+
* state. The DM-seat per-wake override wins; otherwise the session's persisted `state.threadTs`
|
|
1455
|
+
* (undefined ⇒ a top-level post with no `thread_ts`). This is the ONE targeting rule both the reply
|
|
1456
|
+
* post and the assistant-status set/clear read, so on a DM seat they can never diverge onto
|
|
1457
|
+
* different threads (the OPE-98 failure: reply on one thread, spinner stuck on another). */
|
|
1458
|
+
export declare function replyThreadTsForState(state: unknown): string | undefined;
|
|
1459
|
+
/** Dependencies injected into {@link deliverSupervisorReply} so the dispatch is testable without
|
|
1460
|
+
* Slack or a live runtime. `post` = the host-side `slackForm("chat.postMessage")` seam;
|
|
1461
|
+
* `resolveOwnerToken` = the supervisor's registry token (T1.3) for thread binding; `bind` =
|
|
1462
|
+
* `markThreadEngaged(…, { participating: true, ownerToken })`. */
|
|
1463
|
+
export interface SupervisorReplyDeps {
|
|
1464
|
+
/** Whether the turn's text should actually post (false when a card already narrated it this
|
|
1465
|
+
* turn — the default path's `postText` gate; the reply still counts as participation). */
|
|
1466
|
+
readonly postText: boolean;
|
|
1467
|
+
/** The reply body (only posted when `postText`). */
|
|
1468
|
+
readonly text: string;
|
|
1469
|
+
/** The per-turn target snapshot from `channel.state.oeReplyTarget`. */
|
|
1470
|
+
readonly target: SupervisorReplyTarget | undefined;
|
|
1471
|
+
/** Host-side post: `slackForm("chat.postMessage", { channel, thread_ts?, text })`. */
|
|
1472
|
+
readonly post: (channelId: string, threadTs: string | undefined, text: string) => Promise<void>;
|
|
1473
|
+
/** The supervisor session's reserved token (the ownerToken for a bound thread), or undefined
|
|
1474
|
+
* when it can't be resolved — in which case the thread is left unbound (the reply still posts). */
|
|
1475
|
+
readonly resolveOwnerToken: () => Promise<string | undefined>;
|
|
1476
|
+
/** Mark the replied-into thread engaged with the supervisor as owner. */
|
|
1477
|
+
readonly bind: (threadTs: string, ownerToken: string) => Promise<void>;
|
|
1478
|
+
}
|
|
1479
|
+
export interface SupervisorReplyResult {
|
|
1480
|
+
readonly posted: boolean;
|
|
1481
|
+
readonly bound: boolean;
|
|
1482
|
+
readonly reason: string;
|
|
1483
|
+
}
|
|
1484
|
+
/**
|
|
1485
|
+
* Dispatch a supervisor turn's reply host-side (NEVER via eve's `channel.thread.post`). Resolves
|
|
1486
|
+
* the target with {@link planSupervisorReply}: no target ⇒ posts nothing (fail safe); a top-level
|
|
1487
|
+
* target ⇒ posts to the channel with NO `thread_ts`; a threaded target ⇒ posts into the thread
|
|
1488
|
+
* AND binds it to the supervisor (ownerToken = the supervisor's registry token). Binding is
|
|
1489
|
+
* independent of `postText` — a card-narrated reply still participated, so it still owns the thread.
|
|
1490
|
+
*/
|
|
1491
|
+
export declare function deliverSupervisorReply(deps: SupervisorReplyDeps): Promise<SupervisorReplyResult>;
|
|
1492
|
+
/** A Slack `member_joined_channel` event narrowed to what the intro path needs. */
|
|
1493
|
+
export interface ParsedChannelJoinEvent {
|
|
1494
|
+
readonly channelId: string;
|
|
1495
|
+
/** The join event's ts — the "X joined" notice the intro threads under. */
|
|
1496
|
+
readonly ts: string;
|
|
1497
|
+
readonly teamId?: string;
|
|
1498
|
+
/** The Enterprise Grid organisation the join happened in ({@link slackEnterpriseIdFromEnvelope}) —
|
|
1499
|
+
* carried so the intro turn's binding walks the SAME rungs a mention in that channel walks. */
|
|
1500
|
+
readonly enterpriseId?: string;
|
|
1501
|
+
/** Who joined. The front route only proceeds when this is the bot's OWN user id. */
|
|
1502
|
+
readonly userId: string;
|
|
1503
|
+
/** Who invited them, when Slack says (the human actor behind the join). */
|
|
1504
|
+
readonly inviterId?: string;
|
|
1505
|
+
/** member_joined_channel's own vocabulary: "C" (public channel) | "G" (private). */
|
|
1506
|
+
readonly channelKind?: string;
|
|
1507
|
+
}
|
|
1508
|
+
/**
|
|
1509
|
+
* Parse a `member_joined_channel` envelope into {@link ParsedChannelJoinEvent}, or null
|
|
1510
|
+
* when it isn't one (wrong outer/event type, missing ids). Pure — no I/O.
|
|
1511
|
+
*/
|
|
1512
|
+
export declare function parseChannelJoinEvent(envelope: SlackEventEnvelope): ParsedChannelJoinEvent | null;
|
|
1513
|
+
/** The BindableContext a join event implies — same shape {@link bindableFor} builds. */
|
|
1514
|
+
export declare function bindableForJoin(ev: ParsedChannelJoinEvent): BindableContext;
|
|
1515
|
+
/** The channel metadata the join dispatch classifies from — one conversations.info. */
|
|
1516
|
+
export interface ChannelJoinMeta {
|
|
1517
|
+
readonly channelId: string;
|
|
1518
|
+
readonly name?: string;
|
|
1519
|
+
readonly topic?: string;
|
|
1520
|
+
readonly purpose?: string;
|
|
1521
|
+
readonly memberCount?: number;
|
|
1522
|
+
readonly channelType?: string;
|
|
1523
|
+
readonly isPrivate?: boolean;
|
|
1524
|
+
/**
|
|
1525
|
+
* The VENDOR's own external verdict for this channel (OPE-504) — `conversations.info` reported
|
|
1526
|
+
* `is_ext_shared` / `is_org_shared` / `is_shared` / `is_pending_ext_shared`, i.e. a Slack Connect
|
|
1527
|
+
* or otherwise shared channel that can hold people from another organisation.
|
|
1528
|
+
*
|
|
1529
|
+
* 🔴 IT IS TRUE ONLY ON A SUCCESSFUL PROBE THAT SAID YES. `channelFactsFromInfo` fail-softs an
|
|
1530
|
+
* unreadable response to no facts at all, and this field stays absent there rather than becoming
|
|
1531
|
+
* `true` — "we could not ask" is not "we asked and it is external". Absent therefore means "not
|
|
1532
|
+
* reported", which the external-room classification reads as no evidence (fail-open), exactly like
|
|
1533
|
+
* every other carriage leg in that plane.
|
|
1534
|
+
*/
|
|
1535
|
+
readonly vendorExternal?: boolean;
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* Extract {@link ChannelJoinMeta} from a `conversations.info` response. Defensive like
|
|
1539
|
+
* {@link channelFactsFromInfo} (raw Slack JSON — probe, never assume); a failed lookup
|
|
1540
|
+
* yields metadata-less meta, and the dispatch degrades to "(unknown)" fields.
|
|
1541
|
+
*/
|
|
1542
|
+
export declare function channelJoinMetaFromInfo(channelId: string, info: unknown): ChannelJoinMeta;
|
|
1543
|
+
/** The read-up context the route gathers before the intro wake (all fail-soft). */
|
|
1544
|
+
export interface ChannelJoinContext {
|
|
1545
|
+
/** Compact oldest-first backscroll of the channel ({@link fetchChannelTranscript}). */
|
|
1546
|
+
readonly transcript?: string;
|
|
1547
|
+
/** Display name of whoever invited the bot. */
|
|
1548
|
+
readonly inviterName?: string;
|
|
1549
|
+
}
|
|
1550
|
+
/** The route-posted ack that covers the read+judge latency (Tag's "reading up" reply). */
|
|
1551
|
+
export declare const JOIN_ACK_TEXT = "\uD83D\uDC4B Just joined \u2014 reading up on this channel; back in a moment.";
|
|
1552
|
+
export declare function renderChannelJoinMessage(meta: ChannelJoinMeta, context?: ChannelJoinContext): string;
|
|
1553
|
+
/** Dependencies for {@link routeChannelJoinEvent} — injected so the chain is unit-testable. */
|
|
1554
|
+
export interface ChannelJoinRouteDeps {
|
|
1555
|
+
/** Has this surface already been introduced? (shared marker — onboarding/onboarding.ts). */
|
|
1556
|
+
readonly isOnboarded: (p: ReturnType<typeof resolvePartition>) => Promise<boolean>;
|
|
1557
|
+
/** Confirm internal + hand back the metadata from the SAME conversations.info. Fail-closed. */
|
|
1558
|
+
readonly confirmInternal: (channelId: string) => Promise<{
|
|
1559
|
+
internal: boolean;
|
|
1560
|
+
meta?: ChannelJoinMeta;
|
|
1561
|
+
}>;
|
|
1562
|
+
/** Start the judged intro turn, threaded under the join notice. */
|
|
1563
|
+
readonly wake: (input: {
|
|
1564
|
+
channelId: string;
|
|
1565
|
+
threadTs: string;
|
|
1566
|
+
auth: unknown;
|
|
1567
|
+
message: string;
|
|
1568
|
+
state?: unknown;
|
|
1569
|
+
}) => Promise<void>;
|
|
1570
|
+
/** Claim the surface as onboarded BEFORE the wake (the double-intro race gate). */
|
|
1571
|
+
readonly markOnboarded: (p: ReturnType<typeof resolvePartition>) => Promise<void>;
|
|
1572
|
+
/** The channel's recent backscroll as a compact transcript (fail-soft undefined). */
|
|
1573
|
+
readonly transcript: (channelId: string) => Promise<string | undefined>;
|
|
1574
|
+
/** A user's display name, for "invited by" (fail-soft undefined). */
|
|
1575
|
+
readonly userName: (userId: string) => Promise<string | undefined>;
|
|
1576
|
+
/** Post the immediate "reading up" ack under the join notice (fail-soft). */
|
|
1577
|
+
readonly ack: (channelId: string, threadTs: string, text: string) => Promise<void>;
|
|
1578
|
+
}
|
|
1579
|
+
/**
|
|
1580
|
+
* Post the JOIN half of the silent-room announcement pair (OPE-504) — AT MOST ONCE PER ROOM, EVER.
|
|
1581
|
+
*
|
|
1582
|
+
* The claim (`claimNotice`) is a permanent row, not a TTL and not a process-local flag: a re-invite,
|
|
1583
|
+
* a redeploy or a cold start must not make the agent say it again. Fail-CLOSED on the claim — if the
|
|
1584
|
+
* store cannot prove the notice is unsaid, the agent stays quiet, because a repeat here is exactly
|
|
1585
|
+
* the periodic announcement the design rejects.
|
|
1586
|
+
*
|
|
1587
|
+
* Exported for the test, which is the only way to drive it without a live Slack.
|
|
1588
|
+
*/
|
|
1589
|
+
export declare function announceSilentJoin(ev: ParsedChannelJoinEvent, p: ReturnType<typeof resolvePartition>, meta: ChannelJoinMeta | undefined, deps: Pick<ChannelJoinRouteDeps, "ack">, store?: {
|
|
1590
|
+
claimNotice: (org: string, key: string, at: string) => Promise<boolean>;
|
|
1591
|
+
}): Promise<void>;
|
|
1592
|
+
/**
|
|
1593
|
+
* Route one bot-join event: once-only check, then the fail-closed internal confirmation,
|
|
1594
|
+
* then a judged intro wake threaded under the join notice. The caller (slack-front.ts)
|
|
1595
|
+
* has already verified the joiner IS the bot and de-duped the event.
|
|
1596
|
+
*/
|
|
1597
|
+
export declare function routeChannelJoinEvent(ev: ParsedChannelJoinEvent, deps: ChannelJoinRouteDeps): Promise<RouteOutcome>;
|
|
1598
|
+
/** A Slack reaction_added / reaction_removed event, narrowed to the fields ingestion needs. */
|
|
1599
|
+
export interface ParsedSlackReaction {
|
|
1600
|
+
readonly verb: ReactionVerb;
|
|
1601
|
+
/** WHO reacted. */
|
|
1602
|
+
readonly reactorId: string;
|
|
1603
|
+
/** The bare Slack emoji name (may carry a `::skin-tone-N` suffix — the classifier strips it). */
|
|
1604
|
+
readonly emoji: string;
|
|
1605
|
+
/** The reacted message's channel + ts (`item.channel` / `item.ts`). */
|
|
1606
|
+
readonly channelId: string;
|
|
1607
|
+
readonly messageTs: string;
|
|
1608
|
+
/** The AUTHOR of the reacted message (`item_user`), when Slack supplies it — used to
|
|
1609
|
+
* decide onBotReply against the bot's own user id. */
|
|
1610
|
+
readonly itemUser?: string;
|
|
1611
|
+
readonly teamId?: string;
|
|
1612
|
+
}
|
|
1613
|
+
/**
|
|
1614
|
+
* Parse a Slack reaction envelope into {@link ParsedSlackReaction}, or null when it isn't a
|
|
1615
|
+
* usable reaction on a message (wrong outer/event type, a non-`message` item, missing ids).
|
|
1616
|
+
* Pure — no I/O — so it's trivially testable against fixtures. Note Slack only carries
|
|
1617
|
+
* `item.channel`/`item.ts` for `item.type === "message"` (file/comment reactions have a
|
|
1618
|
+
* different item shape and no channel/ts — dropped here as not-a-message-reaction).
|
|
1619
|
+
*/
|
|
1620
|
+
export declare function parseSlackReactionEvent(envelope: SlackEventEnvelope): ParsedSlackReaction | null;
|
|
1621
|
+
/** The BindableContext a reaction implies — same shape {@link bindableFor} builds, but from
|
|
1622
|
+
* the reaction's own coordinates (a reaction event carries no channel_type). */
|
|
1623
|
+
export declare function bindableForReaction(ev: ParsedSlackReaction): BindableContext;
|
|
1624
|
+
/** Dependencies for {@link routeReactionEvent} — injected so parse+route+record is unit-tested
|
|
1625
|
+
* without a live store or Slack. `botIdentity` is the ALREADY-RESOLVED identity (the front
|
|
1626
|
+
* route resolves it once, as it does for the wake/join paths — passing a value, not a lookup,
|
|
1627
|
+
* keeps this module free of a slack-front.ts import cycle). */
|
|
1628
|
+
export interface SlackReactionRouteDeps {
|
|
1629
|
+
readonly botIdentity: {
|
|
1630
|
+
readonly userId?: string;
|
|
1631
|
+
readonly botId?: string;
|
|
1632
|
+
};
|
|
1633
|
+
readonly sink: ReactionSinkDeps;
|
|
1634
|
+
/** eve's I/O-free cancel door, present only when the front route handed one in (the ✋ stop arm,
|
|
1635
|
+
* OPE-418). Absent ⇒ a ✋ is ingested as an ordinary reaction and nothing is cancelled. */
|
|
1636
|
+
readonly attachSession?: AttachSessionForCancel;
|
|
1637
|
+
}
|
|
1638
|
+
/**
|
|
1639
|
+
* Route one parsed Slack reaction: drop the bot's OWN reaction (its 👀/status reactions are
|
|
1640
|
+
* not feedback), resolve whether the target is the bot's own message (item_user === bot),
|
|
1641
|
+
* then hand the surface-neutral {@link ingestReaction} the parsed reaction. Returns the
|
|
1642
|
+
* ingest outcome (recorded / telemetry-only / ignored). All side effects go through the
|
|
1643
|
+
* injected sink.
|
|
1644
|
+
*/
|
|
1645
|
+
export declare function routeReactionEvent(ev: ParsedSlackReaction, deps: SlackReactionRouteDeps): Promise<ReactionIngestOutcome>;
|
|
1646
|
+
/** Production deps for the reaction route: the resolved bot identity + the live audit/telemetry
|
|
1647
|
+
* sink. `botIdentity` is passed in by the front route (which owns the cached auth.test lookup). */
|
|
1648
|
+
export declare function liveReactionDeps(botIdentity: {
|
|
1649
|
+
readonly userId?: string;
|
|
1650
|
+
readonly botId?: string;
|
|
1651
|
+
}, attachSession?: AttachSessionForCancel): SlackReactionRouteDeps;
|
|
1652
|
+
/** Production deps for the channel-join route: the shared marker + live Slack confirmation. */
|
|
1653
|
+
export declare function liveChannelJoinDeps(send: WakeSend): ChannelJoinRouteDeps;
|
|
1654
|
+
//# sourceMappingURL=slack-events.d.ts.map
|