@open-everywhere/provider-slack 0.2.1 → 0.4.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/agent/app-manifest.ts +1 -0
- package/agent/channels/slack-events.ts +22 -19
- package/agent/channels/slack-front.ts +10 -87
- package/agent/channels/slack-post-arm.ts +1 -42
- package/agent/channels/slack-wake-backscroll.ts +3 -2
- package/agent/channels/slack.ts +2 -2
- package/agent/docs-generated.ts +1 -1
- package/agent/setup/claim.ts +16 -1
- package/agent/setup/page.ts +68 -6
- package/agent/surface-parity.ts +0 -6
- package/dist/agent/app-manifest.d.ts.map +1 -1
- package/dist/agent/app-manifest.js +1 -0
- package/dist/agent/app-manifest.js.map +1 -1
- package/dist/agent/channels/slack-events.d.ts +5 -5
- package/dist/agent/channels/slack-events.d.ts.map +1 -1
- package/dist/agent/channels/slack-events.js +20 -17
- package/dist/agent/channels/slack-events.js.map +1 -1
- package/dist/agent/channels/slack-front.d.ts +0 -14
- package/dist/agent/channels/slack-front.d.ts.map +1 -1
- package/dist/agent/channels/slack-front.js +11 -70
- package/dist/agent/channels/slack-front.js.map +1 -1
- package/dist/agent/channels/slack-post-arm.d.ts +1 -8
- package/dist/agent/channels/slack-post-arm.d.ts.map +1 -1
- package/dist/agent/channels/slack-post-arm.js +1 -40
- package/dist/agent/channels/slack-post-arm.js.map +1 -1
- package/dist/agent/channels/slack-wake-backscroll.d.ts +2 -2
- package/dist/agent/channels/slack-wake-backscroll.d.ts.map +1 -1
- package/dist/agent/channels/slack-wake-backscroll.js +3 -3
- package/dist/agent/channels/slack-wake-backscroll.js.map +1 -1
- package/dist/agent/channels/slack.js +2 -2
- package/dist/agent/channels/slack.js.map +1 -1
- package/dist/agent/docs-generated.js +1 -1
- package/dist/agent/docs-generated.js.map +1 -1
- package/dist/agent/setup/claim.d.ts.map +1 -1
- package/dist/agent/setup/claim.js +15 -2
- package/dist/agent/setup/claim.js.map +1 -1
- package/dist/agent/setup/page.d.ts +2 -0
- package/dist/agent/setup/page.d.ts.map +1 -1
- package/dist/agent/setup/page.js +60 -6
- package/dist/agent/setup/page.js.map +1 -1
- package/dist/agent/surface-parity.d.ts.map +1 -1
- package/dist/agent/surface-parity.js +0 -6
- package/dist/agent/surface-parity.js.map +1 -1
- package/package.json +2 -7
- package/agent/channels/slack-approval-card.ts +0 -275
package/agent/app-manifest.ts
CHANGED
|
@@ -240,6 +240,7 @@ export const SLACK_APP_MANIFEST_STEPS: readonly string[] = [
|
|
|
240
240
|
"Pick the workspace, choose JSON, and paste the manifest below.",
|
|
241
241
|
"Next → Create.",
|
|
242
242
|
"Install to Workspace, and approve the scopes Slack lists.",
|
|
243
|
+
"Unless you set identity.name and chose your own icon, give the app the project mark: Basic Information → Display Information → App icon, and upload https://raw.githubusercontent.com/open-everywhere/open-everywhere/main/brand/png/mark-square-1024.png. Slack's manifest has no icon field, so this step stays by hand.",
|
|
243
244
|
"Before the first deploy, Slack cannot verify the Event Subscriptions URL: it answers only once a deploy that holds SLACK_SIGNING_SECRET is up. After the deploy, paste the manifest again with the real host (App Manifest → Save Changes), then retry the verification under Event Subscriptions.",
|
|
244
245
|
];
|
|
245
246
|
|
|
@@ -1430,10 +1430,11 @@ export async function routeMessageEvent(ev: ParsedMessageEvent, deps: MessageRou
|
|
|
1430
1430
|
authorId: ev.userId,
|
|
1431
1431
|
surfaceInternal: authorFacts.surfaceInternal,
|
|
1432
1432
|
channelKey: domainKeyOf(p),
|
|
1433
|
+
...(ev.enterpriseId === undefined ? {} : { enterpriseId: ev.enterpriseId }),
|
|
1433
1434
|
});
|
|
1434
1435
|
// ── THE EXTERNAL-PARTICIPANT POLICY (OPE-507 / OPE-504) ────────────────────────────────────────
|
|
1435
|
-
// Inert on the
|
|
1436
|
-
//
|
|
1436
|
+
// Inert only on the `taint` / `engage` opt-out, where it is byte-identical to before it
|
|
1437
|
+
// existed; the shipped grains suppress here. It sits here because this is where the author evidence first exists — see the
|
|
1437
1438
|
// block comment on `slackExternalVerdict` for why that is lower than Discord's gate.
|
|
1438
1439
|
{
|
|
1439
1440
|
const verdict = await applySlackExternalPolicy(ev, p, { trust: wakeTrust, refuted: authorFacts.refuted }, "routeMessageEvent");
|
|
@@ -1861,7 +1862,7 @@ export async function auditDmBotAuthorDrop(
|
|
|
1861
1862
|
dmKind,
|
|
1862
1863
|
botId: ev.botId,
|
|
1863
1864
|
team: ev.teamId ?? null,
|
|
1864
|
-
// TRUE ⇒ the `
|
|
1865
|
+
// TRUE ⇒ the `wake.driverAllowlist` setting vouches for this bot at the engagement and mention
|
|
1865
1866
|
// doors, and the DM door dropped it anyway. That is not a defect: the DM door reads its OWN,
|
|
1866
1867
|
// narrower lever (`wake.e2eDrivers`, {@link dmDoorAdmitsDriver}), so a bot on the channel
|
|
1867
1868
|
// allowlist has no DM standing. The row carries the distinction as data.
|
|
@@ -1883,7 +1884,7 @@ export async function auditDmBotAuthorDrop(
|
|
|
1883
1884
|
* DM leg in the pack exercised a synthetic envelope and a regression in Slack's own DM delivery to
|
|
1884
1885
|
* us was invisible. This carve-out lets the driver in through the real front door.
|
|
1885
1886
|
*
|
|
1886
|
-
* WHY IT IS NOT THE `
|
|
1887
|
+
* WHY IT IS NOT THE `wake.driverAllowlist` SETTING, which already lifts the same bot at the
|
|
1887
1888
|
* engagement and mention doors. A DM seat holds a window token, delegation and memory, so admitting
|
|
1888
1889
|
* a bot into a DM is a strictly larger grant than admitting one into a watched channel. Reusing the
|
|
1889
1890
|
* channel allowlist would have widened the DM door for every deployment that already sets it, on
|
|
@@ -2267,7 +2268,7 @@ export interface NameDropRouteDeps {
|
|
|
2267
2268
|
readonly backscroll?: (input: { channelId: string; ts: string; threadTs?: string; threadAdmissionSlice?: boolean }) => Promise<WakeBackscrollLines | null>;
|
|
2268
2269
|
/** The per-author membership probe the hydrated-context withhold uses (OPE-525,
|
|
2269
2270
|
* {@link withholdNameDropContext}). Defaults to the live {@link slackAuthorIsMember}; injectable
|
|
2270
|
-
* so the withhold is testable without Slack. Never called on the
|
|
2271
|
+
* so the withhold is testable without Slack. Never called on the `taint` opt-out grain. */
|
|
2271
2272
|
readonly authorIsMember?: SlackMemberProbe;
|
|
2272
2273
|
/** Start the judged turn in a thread rooted at the triggering message. `context` (when present)
|
|
2273
2274
|
* is the backscroll — ADDITIONAL history the channel appends before the delivery turn. */
|
|
@@ -2481,7 +2482,7 @@ export async function routeNameDropEvent(ev: ParsedMessageEvent, deps: NameDropR
|
|
|
2481
2482
|
// reference-only and marks the taint join too — not just the trigger's own author. Fail-soft:
|
|
2482
2483
|
// a regrade failure keeps the fetched (ungraded) lines, exactly like every other backscroll leg.
|
|
2483
2484
|
// 🔴 THE REGRADE KEEPS THE LIVE PROBE, and `deps.authorIsMember` must NOT be threaded here. That
|
|
2484
|
-
// dep is the WITHHOLD's probe and its contract is "never called on the
|
|
2485
|
+
// dep is the WITHHOLD's probe and its contract is "never called on the `taint` opt-out grain"
|
|
2485
2486
|
// (OPE-525); this regrade runs on every hydration, whatever the grain, so sharing the dep would
|
|
2486
2487
|
// break that contract and make a grain-gated lookup look grain-blind. The two share the
|
|
2487
2488
|
// TTL-cached probe underneath, which is where the call is actually saved.
|
|
@@ -2600,10 +2601,11 @@ export async function routeNameDropEvent(ev: ParsedMessageEvent, deps: NameDropR
|
|
|
2600
2601
|
authorId: ev.userId,
|
|
2601
2602
|
surfaceInternal: authorFacts.surfaceInternal,
|
|
2602
2603
|
channelKey: domainKeyOf(p),
|
|
2604
|
+
...(ev.enterpriseId === undefined ? {} : { enterpriseId: ev.enterpriseId }),
|
|
2603
2605
|
});
|
|
2604
2606
|
// ── THE EXTERNAL-PARTICIPANT POLICY (OPE-507 / OPE-504) ────────────────────────────────────────
|
|
2605
2607
|
// A name-drop is AMBIENT by construction (the gate above disqualifies an @mention), so an excluded
|
|
2606
|
-
// author's name-drop is owed nothing and is suppressed in silence. Inert on the
|
|
2608
|
+
// author's name-drop is owed nothing and is suppressed in silence. Inert only on the opt-out grains.
|
|
2607
2609
|
{
|
|
2608
2610
|
const verdict = await applySlackExternalPolicy(ev, p, { trust: wakeTrust, refuted: authorFacts.refuted }, "routeNameDropEvent");
|
|
2609
2611
|
if (verdict !== "proceed") return { deliver: false, reason: externalPolicyDropReason(verdict), woke: false };
|
|
@@ -2620,7 +2622,7 @@ export async function routeNameDropEvent(ev: ParsedMessageEvent, deps: NameDropR
|
|
|
2620
2622
|
// about the people quoted in that hydration: a member name-drops the bot in a channel where a
|
|
2621
2623
|
// refuted guest has been talking, and without this the guest's bytes reach the model verbatim.
|
|
2622
2624
|
// `withholdNameDropContext` runs the shared transform and returns the wake BY REFERENCE on the
|
|
2623
|
-
//
|
|
2625
|
+
// `taint` opt-out grain, so nothing moves for a deployment that opted out.
|
|
2624
2626
|
const rendered = renderWake(
|
|
2625
2627
|
await withholdNameDropContext(
|
|
2626
2628
|
slackNameDropWake(ev, name!, {
|
|
@@ -3203,7 +3205,7 @@ function externalPolicyDropReason(verdict: SlackExternalVerdict): string {
|
|
|
3203
3205
|
* other in the plane. A `bot` line carries a bot id, and a machine author is never excluded, so only
|
|
3204
3206
|
* `senderType: "user"` lines are asked about.
|
|
3205
3207
|
*
|
|
3206
|
-
* COSTS NOTHING ON THE
|
|
3208
|
+
* COSTS NOTHING ON THE OPT-OUT GRAIN: `taint` returns the wake by reference before any probe runs.
|
|
3207
3209
|
* On `withhold` it costs one TTL-cached `users.info` per DISTINCT hydrated author, resolved in
|
|
3208
3210
|
* parallel, and the render budget caps the hydration at ten lines.
|
|
3209
3211
|
*/
|
|
@@ -3822,8 +3824,8 @@ export function liveNameDropDeps(send: WakeSend): NameDropRouteDeps {
|
|
|
3822
3824
|
|
|
3823
3825
|
// --- channel-supervisor delivery (Slack Claude-Tag parity, T1.3) ---------------
|
|
3824
3826
|
//
|
|
3825
|
-
// The "always-on teammate" leg. When a channel's domain
|
|
3826
|
-
// (memory/domain.ts,
|
|
3827
|
+
// The "always-on teammate" leg. When a channel's domain has ambientSupervisor on
|
|
3828
|
+
// (memory/domain.ts, default ON since OPE-2409) AND the channel is POSITIVELY confirmed internal,
|
|
3827
3829
|
// an unowned message (top-level, or in-thread but not engaged) is delivered to a single
|
|
3828
3830
|
// long-lived CHANNEL-SUPERVISOR session instead of the name-drop-only path. The supervisor
|
|
3829
3831
|
// is one durable eve session per channel, addressed by a RESERVED continuation token
|
|
@@ -4001,7 +4003,7 @@ export interface SupervisorRouteDeps {
|
|
|
4001
4003
|
* {@link withholdNameDropContext}) — the same dep, spelled the same way, as the native route it
|
|
4002
4004
|
* replaces ({@link NameDropRouteDeps.authorIsMember}). Defaults to the live
|
|
4003
4005
|
* {@link slackAuthorIsMember}; injectable so the withhold is testable without Slack. Never called
|
|
4004
|
-
* on the
|
|
4006
|
+
* on the `taint` opt-out grain, and never on a wake that carries no hydrated lines. */
|
|
4005
4007
|
readonly authorIsMember?: SlackMemberProbe;
|
|
4006
4008
|
/**
|
|
4007
4009
|
* Start/continue the supervisor session under its reserved token. `window` is present ONLY on
|
|
@@ -4068,7 +4070,7 @@ export interface SupervisorRouteDeps {
|
|
|
4068
4070
|
* Route one unowned message to the channel supervisor: pure disqualifiers, then the
|
|
4069
4071
|
* fail-closed internal-channel confirmation (which also surfaces the member count for the
|
|
4070
4072
|
* envelope), then create-or-fetch the supervisor session and deliver the judged wake. The
|
|
4071
|
-
* caller (slack-front) only reaches here once the domain has
|
|
4073
|
+
* caller (slack-front) only reaches here once the domain has ambientSupervisor on (the default).
|
|
4072
4074
|
*
|
|
4073
4075
|
* `opts.directMention` (SINGLE-WINDOW @mention routing — the GitHub reference's
|
|
4074
4076
|
* `treatMentionAsDirect`/`directMention` shape): the trigger is the `message.channels` SIBLING of
|
|
@@ -4217,10 +4219,11 @@ export async function routeSupervisorEvent(
|
|
|
4217
4219
|
authorId: ev.userId,
|
|
4218
4220
|
surfaceInternal: authorFacts.surfaceInternal,
|
|
4219
4221
|
channelKey: domainKeyOf(p),
|
|
4222
|
+
...(ev.enterpriseId === undefined ? {} : { enterpriseId: ev.enterpriseId }),
|
|
4220
4223
|
});
|
|
4221
4224
|
// ── THE EXTERNAL-PARTICIPANT POLICY (OPE-507 / OPE-504) ────────────────────────────────────────
|
|
4222
4225
|
// ABOVE THE SESSION MINT, deliberately: a suppressed wake must not rotate a generation or claim a
|
|
4223
|
-
// fresh generation's first-wake backscroll. Inert on the
|
|
4226
|
+
// fresh generation's first-wake backscroll. Inert only on the `taint` / `engage` opt-out.
|
|
4224
4227
|
const externalPolicy = slackExternalPolicyForRoom(domainKeyOf(p));
|
|
4225
4228
|
{
|
|
4226
4229
|
const verdict = await applySlackExternalPolicy(ev, p, { trust: wakeTrust, refuted: authorFacts.refuted }, "routeSupervisorEvent");
|
|
@@ -4355,8 +4358,8 @@ export async function routeSupervisorEvent(
|
|
|
4355
4358
|
// BEHAVIOUR 3 (OPE-507) — the ARRIVED-MARKER over the first-wake backscroll. The trigger author
|
|
4356
4359
|
// cleared the gate above, so this wake fires; a SIBLING the probe refuted has not, and under
|
|
4357
4360
|
// `withhold` their bytes must not enter. The transform replaces the body, the pre-edit body and the
|
|
4358
|
-
// attachments, and it returns the SAME ARRAY by reference on the
|
|
4359
|
-
// golden moves on a deployment that
|
|
4361
|
+
// attachments, and it returns the SAME ARRAY by reference on the `taint` opt-out grain, so no
|
|
4362
|
+
// golden moves on a deployment that opted out.
|
|
4360
4363
|
const backscroll = priorContext.map((m) => (withholdsBody(m, externalPolicy) ? withheldWakeMessage(m) : m));
|
|
4361
4364
|
|
|
4362
4365
|
// POSITIVELY confirmed internal → stamp the trust verdict into the wake auth, exactly like the
|
|
@@ -4371,7 +4374,7 @@ export async function routeSupervisorEvent(
|
|
|
4371
4374
|
//
|
|
4372
4375
|
// 🔴 A WITHHELD SIBLING NEVER KEYS TAINT. `ingestedAfterWithholding` REMOVES the entries the
|
|
4373
4376
|
// transform above stripped, because their bytes did not enter — a join over a message that did not
|
|
4374
|
-
// ingest would claim an exposure that did not happen. On the
|
|
4377
|
+
// ingest would claim an exposure that did not happen. On the `taint` opt-out grain it returns the
|
|
4375
4378
|
// SAME ARRAY by reference, so the auth bag is byte-identical.
|
|
4376
4379
|
//
|
|
4377
4380
|
// 🔴 THE HYDRATED NAME-DROP LINES JOIN TOO (OPE-475/OPE-525, carried onto the seat road by
|
|
@@ -4476,8 +4479,8 @@ export async function routeSupervisorEvent(
|
|
|
4476
4479
|
// and without this the guest's bytes reach the model verbatim. It is the SAME function the
|
|
4477
4480
|
// native route calls, over the WHOLE wake — so the envelope backscroll's own withholding (the
|
|
4478
4481
|
// `priorContext` map above) and the hydrated lines are decided by one transform, and it returns
|
|
4479
|
-
// the wake BY REFERENCE on the
|
|
4480
|
-
//
|
|
4482
|
+
// the wake BY REFERENCE on the `taint` opt-out grain, so nothing moves for a deployment that
|
|
4483
|
+
// opted out. Run only on the name-drop tier, which is the only tier that carries the slot.
|
|
4481
4484
|
const wake = slackChannelWatchWake(ev, {
|
|
4482
4485
|
mode: "supervisor",
|
|
4483
4486
|
trust: wakeTrust,
|
|
@@ -105,7 +105,6 @@ import {
|
|
|
105
105
|
OE_CONNECT_ACTION_PREFIX,
|
|
106
106
|
handleConnectClick as liveHandleConnectClick,
|
|
107
107
|
OE_GRANT_ACTION_PREFIX,
|
|
108
|
-
OE_INPUT_CLICK_PREFIX,
|
|
109
108
|
handleGrantCardClick as liveHandleGrantCardClick,
|
|
110
109
|
buildRecord,
|
|
111
110
|
enqueue as enqueueLogRecord,
|
|
@@ -118,7 +117,6 @@ import {
|
|
|
118
117
|
recordWakeTail,
|
|
119
118
|
tailAuthorGrain,
|
|
120
119
|
dmWindowRegistryKey,
|
|
121
|
-
approvalCardRespondVia,
|
|
122
120
|
stampSurfaceEventVerified,
|
|
123
121
|
} from "@open-everywhere/core/agent/adapter-api.js";
|
|
124
122
|
import {
|
|
@@ -136,11 +134,6 @@ import {
|
|
|
136
134
|
import { buildSlackAuth, withForwardedSlackEnvelope, type ForwardedSlackEnvelope } from "./slack-identity.js";
|
|
137
135
|
import { holdRouteWork, routeHoldQuiesced, withSlackRouteHold } from "./slack-route-hold.js";
|
|
138
136
|
import { persistSlackAttachment, registerSlackAttachment } from "./slack-attachments.js";
|
|
139
|
-
import {
|
|
140
|
-
handleSlackApprovalCardClick as liveHandleApprovalCardClick,
|
|
141
|
-
type ApprovalCardClickOutcome,
|
|
142
|
-
type ApprovalRespondFn,
|
|
143
|
-
} from "./slack-approval-card.js";
|
|
144
137
|
import {
|
|
145
138
|
parseSlackShortcutPayload,
|
|
146
139
|
shortcutMessageEvent,
|
|
@@ -181,11 +174,6 @@ export type FrontDecision =
|
|
|
181
174
|
* OWNER of the ask answered it in their own DM. Core verifies the clicker IS that owner, then
|
|
182
175
|
* approves or declines. Never forwarded. */
|
|
183
176
|
| { readonly kind: "grant-click"; readonly reason: string; readonly payload: unknown }
|
|
184
|
-
/** A tap on a BRIDGED APPROVAL CARD the unified post arm rendered (OPE-1720). OURS, never
|
|
185
|
-
* forwarded: eve's own interactivity route resolves a click against its Slack channel's state and
|
|
186
|
-
* session address, and this card's parked request lives on the unified window — see
|
|
187
|
-
* `slack-approval-card.ts`'s header for why a forward could only drop it. */
|
|
188
|
-
| { readonly kind: "input-click"; readonly reason: string; readonly payload: unknown }
|
|
189
177
|
/** An `app_mention` — the native slackChannel path owns it UNLESS the handler's single-window
|
|
190
178
|
* policy says the unified window does (then the forward is suppressed and the mention's
|
|
191
179
|
* `message.channels` sibling carries it as an addressed wake). Carries `env` so the handler
|
|
@@ -298,28 +286,6 @@ function parseOeGrantClickPayload(rawBody: string): unknown | undefined {
|
|
|
298
286
|
return isOurs ? p : undefined;
|
|
299
287
|
}
|
|
300
288
|
|
|
301
|
-
/**
|
|
302
|
-
* Best-effort decode of a urlencoded body into an `oe_input:` bridged-approval-card `block_actions`
|
|
303
|
-
* payload (core channels/approval-card.ts) — undefined for anything that isn't one. Never throws.
|
|
304
|
-
*
|
|
305
|
-
* 🔴 NOT eve's `eve_input:`, which still FORWARDS. The two prefixes are one character apart on
|
|
306
|
-
* purpose — both name an input request — and only one of them is ours. `startsWith` distinguishes
|
|
307
|
-
* them cleanly because neither is a prefix of the other, and the order of the two branches never
|
|
308
|
-
* matters: eve's clicks reach no branch here at all and fall to the urlencoded forward.
|
|
309
|
-
*/
|
|
310
|
-
function parseOeInputClickPayload(rawBody: string): unknown | undefined {
|
|
311
|
-
const p = decodeInteractivityPayload(rawBody);
|
|
312
|
-
if (!p || p.type !== "block_actions" || !Array.isArray(p.actions)) return undefined;
|
|
313
|
-
const isOurs = (p.actions as unknown[]).some(
|
|
314
|
-
(a) =>
|
|
315
|
-
typeof a === "object" &&
|
|
316
|
-
a !== null &&
|
|
317
|
-
typeof (a as Record<string, unknown>).action_id === "string" &&
|
|
318
|
-
((a as Record<string, unknown>).action_id as string).startsWith(OE_INPUT_CLICK_PREFIX),
|
|
319
|
-
);
|
|
320
|
-
return isOurs ? p : undefined;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
289
|
/**
|
|
324
290
|
* Best-effort decode of a urlencoded body into OUR credential-intake `view_submission` (its
|
|
325
291
|
* `view.callback_id` is our modal's) — undefined for any other view_submission (Eve's own modals
|
|
@@ -399,10 +365,6 @@ export function classifyEnvelope(rawBody: string, contentType: string): FrontDec
|
|
|
399
365
|
if (grantClick !== undefined) {
|
|
400
366
|
return { kind: "grant-click", reason: "block_actions oe_grant: click — ours, never forwarded", payload: grantClick };
|
|
401
367
|
}
|
|
402
|
-
const inputClick = parseOeInputClickPayload(rawBody);
|
|
403
|
-
if (inputClick !== undefined) {
|
|
404
|
-
return { kind: "input-click", reason: "block_actions oe_input: click — ours, never forwarded", payload: inputClick };
|
|
405
|
-
}
|
|
406
368
|
// A SHORTCUT (OPE-1376) — a `message_action` or `shortcut` payload. Ours, never forwarded; see
|
|
407
369
|
// the `shortcut` arm of FrontDecision for why a forward could not do anything with it. It is
|
|
408
370
|
// matched LAST among the urlencoded branches and cannot race any of them: every branch above
|
|
@@ -595,10 +557,6 @@ export interface FrontOptions {
|
|
|
595
557
|
* branch schedules it through the route's `waitUntil`, so this handler may outlive the empty
|
|
596
558
|
* Slack acknowledgement. */
|
|
597
559
|
readonly handleGrantCardClick?: (payload: unknown) => Promise<boolean>;
|
|
598
|
-
/** Bridged-approval-card `oe_input:` tap handler (OPE-1720, defaults to
|
|
599
|
-
* `./slack-approval-card.ts`). Injectable so the classification and the branch are testable
|
|
600
|
-
* without a store, a Slack token or eve's `respond` primitive. */
|
|
601
|
-
readonly handleApprovalCardClick?: (payload: unknown, respond: ApprovalRespondFn) => Promise<ApprovalCardClickOutcome>;
|
|
602
560
|
/** SINGLE-WINDOW mention policy for one (channel, surface) — should the unified window own an
|
|
603
561
|
* @mention here (suppress the native forward + keep the sibling as an addressed wake)?
|
|
604
562
|
* Defaults to the live {@link slackSingleWindowMentionPolicy} (grain + fail-closed
|
|
@@ -1034,7 +992,6 @@ export function buildFrontHandler(innerHandler: RouteHandler, opts: FrontOptions
|
|
|
1034
992
|
const handleSetKeyViewSubmission = opts.handleSetKeyViewSubmission ?? liveHandleSetKeyViewSubmission;
|
|
1035
993
|
const handleConnectClick = opts.handleConnectClick ?? liveHandleConnectClick;
|
|
1036
994
|
const handleGrantCardClick = opts.handleGrantCardClick ?? liveHandleGrantCardClick;
|
|
1037
|
-
const handleApprovalCardClick = opts.handleApprovalCardClick ?? liveHandleApprovalCardClick;
|
|
1038
995
|
const ambientEnabledFor = opts.ambientEnabledFor ?? ((surfaceKey: string) => ambientSupervisorEnabled(surfaceKey, getConfig().loadDomains()));
|
|
1039
996
|
const seatedSurface = opts.seatedSurfaceFor ?? slackDomainUsesUnifiedWindow;
|
|
1040
997
|
const mentionPolicyFor = opts.mentionPolicyFor ?? slackSingleWindowMentionPolicy;
|
|
@@ -1199,16 +1156,16 @@ export function buildFrontHandler(innerHandler: RouteHandler, opts: FrontOptions
|
|
|
1199
1156
|
const notYetSeen = (env: SlackEventEnvelope, ev: ParsedMessageEvent): Promise<boolean> =>
|
|
1200
1157
|
notYetSeenKey(wakeKey(env, ev.channelId, ev.ts), ev);
|
|
1201
1158
|
|
|
1202
|
-
// UNOWNED message dispatch (top-level, or in-thread but not engaged):
|
|
1203
|
-
// name-drop path (per-domain opt-in, fail-closed). When the channel's domain
|
|
1204
|
-
// ambientSupervisor, the SAME unowned traffic routes to the channel-supervisor session instead
|
|
1159
|
+
// UNOWNED message dispatch (top-level, or in-thread but not engaged): the fallback is the
|
|
1160
|
+
// name-drop path (per-domain opt-in, fail-closed). When the channel's domain has
|
|
1161
|
+
// ambientSupervisor on (the default since OPE-2409), the SAME unowned traffic routes to the channel-supervisor session instead
|
|
1205
1162
|
// (slack-events.ts routeSupervisorEvent) — the "always-on teammate" leg. `directMention` is the
|
|
1206
1163
|
// SINGLE-WINDOW addressed-mention divert (an app_mention event routed here from the mention
|
|
1207
1164
|
// branch): it forces the supervisor path (the only road into the unified window) and renders an
|
|
1208
1165
|
// addressed `direct-mention` wake. The supervisor wake serializes on its OWN FIFO key (the
|
|
1209
1166
|
// supervisor token), never shared with a thread FIFO. Fail-closed: any partition/config error,
|
|
1210
1167
|
// or the flag off, falls through to the UNCHANGED name-drop dispatch — so ambientSupervisor-off
|
|
1211
|
-
// is byte-identical to
|
|
1168
|
+
// is byte-identical to the pre-ambient path. Parametrised on `ev` so both the app_mention divert and a plain
|
|
1212
1169
|
// message.channels wake feed one dispatcher.
|
|
1213
1170
|
// `onWake` runs SYNCHRONOUSLY, once, immediately before a wake is scheduled — and never on the
|
|
1214
1171
|
// addressed-drop path below. The shortcut route (OPE-1442) uses it to seed the attachment
|
|
@@ -1449,6 +1406,7 @@ export function buildFrontHandler(innerHandler: RouteHandler, opts: FrontOptions
|
|
|
1449
1406
|
authorId: ev.userId,
|
|
1450
1407
|
surfaceInternal,
|
|
1451
1408
|
channelKey: p.surfaceKey,
|
|
1409
|
+
...(ev.enterpriseId === undefined ? {} : { enterpriseId: ev.enterpriseId }),
|
|
1452
1410
|
});
|
|
1453
1411
|
// ── THE EXTERNAL-PARTICIPANT POLICY (OPE-507 author grain) ──────────────────────────────────
|
|
1454
1412
|
//
|
|
@@ -1466,10 +1424,9 @@ export function buildFrontHandler(innerHandler: RouteHandler, opts: FrontOptions
|
|
|
1466
1424
|
// 🔴 A GROUP DM IS SUPPRESSIBLE ON THE AUTHOR GRAIN TOO (OPE-1486). It was not before, because
|
|
1467
1425
|
// an mpim resolved no refutation at all — the limit the old note here recorded. The author leg
|
|
1468
1426
|
// above now resolves one from the same probe a channel uses, so an operator who set
|
|
1469
|
-
// `externalPolicy.authorGrain: "withhold"` gets that rule in a group
|
|
1470
|
-
//
|
|
1471
|
-
//
|
|
1472
|
-
// here — which is the rule it asked for. That is a claim about SUPPRESSION alone: on `taint`
|
|
1427
|
+
// `externalPolicy.authorGrain: "withhold"` (shipped since 2026-10-10) gets that rule in a group
|
|
1428
|
+
// DM as well. `withholdsBody` is inert only under the `taint` opt-out, so a deployment that opted
|
|
1429
|
+
// out is never suppressed here. That is a claim about SUPPRESSION alone: on `taint`
|
|
1473
1430
|
// the same refutation still newly stamps `oe_chat_nonmember` and still newly fences the body,
|
|
1474
1431
|
// which is the fix. The room leg stays off for the whole DM family by the note above: on a DM
|
|
1475
1432
|
// the trust plane asks about the PARTNER, not about the room.
|
|
@@ -2009,7 +1966,7 @@ export function buildFrontHandler(innerHandler: RouteHandler, opts: FrontOptions
|
|
|
2009
1966
|
//
|
|
2010
1967
|
// 🔴 IT COSTS A PROBE, SO IT IS SKIPPED WHEN IT CANNOT MATTER. Unlike the three routes, this
|
|
2011
1968
|
// leg resolves no author facts of its own, so gating it means one `users.info` call. The inert
|
|
2012
|
-
// check runs first: on the
|
|
1969
|
+
// check runs first: on the `taint` / `engage` opt-out pair no evidence is resolved at all and
|
|
2013
1970
|
// this whole block is a single config read. The probe is TTL-cached per author on top of that.
|
|
2014
1971
|
//
|
|
2015
1972
|
// FAIL-OPEN on anything unreadable — an unparseable event, an unresolvable partition, a probe
|
|
@@ -2027,6 +1984,7 @@ export function buildFrontHandler(innerHandler: RouteHandler, opts: FrontOptions
|
|
|
2027
1984
|
authorId: mev.userId,
|
|
2028
1985
|
surfaceInternal: facts.surfaceInternal,
|
|
2029
1986
|
channelKey: roomKey,
|
|
1987
|
+
...(mev.enterpriseId === undefined ? {} : { enterpriseId: mev.enterpriseId }),
|
|
2030
1988
|
});
|
|
2031
1989
|
const verdict = await applySlackExternalPolicy(mev, p, { trust: wakeTrust, refuted: facts.refuted }, "native-mention-forward");
|
|
2032
1990
|
if (verdict !== "proceed") {
|
|
@@ -2140,41 +2098,6 @@ export function buildFrontHandler(innerHandler: RouteHandler, opts: FrontOptions
|
|
|
2140
2098
|
return new Response(null, { status: 200 });
|
|
2141
2099
|
}
|
|
2142
2100
|
|
|
2143
|
-
if (decision.kind === "input-click") {
|
|
2144
|
-
// THE TENANT FENCE (OPE-1680), above the handler, for the same reason every other interactivity
|
|
2145
|
-
// branch carries it: a tap RESUMES a parked turn, which is this deployment spending on a scope
|
|
2146
|
-
// an unadmitted workspace never had standing to decide.
|
|
2147
|
-
if (slackInteractivityTenantFenceDeclined(interactivityFenceIds(decision.payload), "input-click", "front-interactivity-branch")) {
|
|
2148
|
-
return fencedAck(new Response(null, { status: 200 }));
|
|
2149
|
-
}
|
|
2150
|
-
// Ours (core channels/approval-card.ts): somebody tapped a bridged eve approval this
|
|
2151
|
-
// deployment posted onto a unified origin. The handler resolves the ticket, admits the tap
|
|
2152
|
-
// through the SAME floor the typed answer takes, and delivers `respond([{requestId, optionId}])`
|
|
2153
|
-
// to the parked WINDOW session.
|
|
2154
|
-
//
|
|
2155
|
-
// 🔴 THE DELIVERY GOES BY SESSION ID (OPE-1863). `args.from` is CHANNEL-LOCAL — on this route
|
|
2156
|
-
// it addresses `slack:<token>`, while the parked session lives at `unified:<token>` — so the
|
|
2157
|
-
// settle threw "the target session was not found" and every tap left the approval parked.
|
|
2158
|
-
// `args.attachSession` dispatches by durable session id from any channel, and
|
|
2159
|
-
// `approvalCardRespondVia` is the one road every surface binds. Bound HERE and injected because
|
|
2160
|
-
// the route handler is the only place eve hands out either primitive.
|
|
2161
|
-
//
|
|
2162
|
-
// The 200 goes out AFTER the settle, deliberately, and the cost is stated rather than hidden:
|
|
2163
|
-
// Slack expects this endpoint to answer in about three seconds, and a settle that runs longer
|
|
2164
|
-
// leaves the tapper waiting on whatever Slack does then. Acking first and settling in the
|
|
2165
|
-
// background would fix the delay and WIDEN the handler's own non-atomic window — see the
|
|
2166
|
-
// "THE LIMIT OF THAT" paragraph in `slack-approval-card.ts`, which also records that the
|
|
2167
|
-
// past-deadline behaviour is unmeasured here.
|
|
2168
|
-
const respondToApproval: ApprovalRespondFn = approvalCardRespondVia(args.attachSession);
|
|
2169
|
-
const tap = await handleApprovalCardClick(decision.payload, respondToApproval);
|
|
2170
|
-
console.log(`[open-everywhere:slack] oe_input: tap — ${tap.reason}`);
|
|
2171
|
-
// `handled:false` means the payload carried our prefix but not a shape the handler could read.
|
|
2172
|
-
// Forward it rather than swallow it, the same defensive shape the grant and setkey branches
|
|
2173
|
-
// take: a click this deployment cannot act on is better handed on than silently dropped.
|
|
2174
|
-
if (!tap.handled) return forwardNative();
|
|
2175
|
-
return new Response(null, { status: 200 });
|
|
2176
|
-
}
|
|
2177
|
-
|
|
2178
2101
|
if (decision.kind === "setkey-submit") {
|
|
2179
2102
|
// THE TENANT FENCE (OPE-1680), above the handler — this is the branch that WRITES A SECRET into
|
|
2180
2103
|
// this deployment's store, keyed to the intake the payload names. A `view_submission` carries
|
|
@@ -162,44 +162,9 @@ async function liveSlackPost(origin: SlackOrigin, text: string, replier?: Replie
|
|
|
162
162
|
return postSlackMessage(body, replier, "unified reply couldn't post to Slack");
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
/**
|
|
166
|
-
* UNIFIED, WITH BUTTONS (OPE-1720) — `liveSlackPost` plus one `actions` block, so an interactive card
|
|
167
|
-
* can reach a unified origin (a DM seat, a single-window channel) instead of only a DM.
|
|
168
|
-
*
|
|
169
|
-
* IDENTICAL GEOMETRY TO THE PLAIN POST: same channel, same `thread_ts` resolution, same replier
|
|
170
|
-
* fallback through {@link postSlackMessage} — the ONLY difference is the `blocks` field, so a card
|
|
171
|
-
* lands exactly where the same reply would have. An `actions` list that renders to NOTHING (every
|
|
172
|
-
* entry over Slack's caps) posts the plain message rather than a `blocks` array with an empty row.
|
|
173
|
-
*/
|
|
174
|
-
async function liveSlackPostWithActions(origin: SlackOrigin, text: string, actions: readonly DirectMessageAction[], replier?: ReplierLabel): Promise<string> {
|
|
175
|
-
return postSlackMessage(slackUnifiedActionForm(origin, text, actions), replier, "unified reply couldn't post to Slack");
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* PURE: the exact `chat.postMessage` form a unified card post sends. Split out from the dispatch so
|
|
180
|
-
* the BLOCK PAYLOAD itself is pinned by a contract test with no Slack token and no transport — the
|
|
181
|
-
* shape is the contract, and a shape Slack rejects costs the room the whole message, not just the
|
|
182
|
-
* buttons.
|
|
183
|
-
*/
|
|
184
|
-
export function slackUnifiedActionForm(origin: Pick<SlackOrigin, "channelId" | "threadTs" | "replyAnchorTs">, text: string, actions: readonly DirectMessageAction[]): URLSearchParams {
|
|
185
|
-
const rendered = markdownToSlackMrkdwn(text);
|
|
186
|
-
const body = new URLSearchParams({ channel: origin.channelId, text: rendered });
|
|
187
|
-
const threadTarget = slackThreadTarget(origin);
|
|
188
|
-
if (threadTarget) body.set("thread_ts", threadTarget);
|
|
189
|
-
const actionBlock = slackDirectMessageActionBlock(actions);
|
|
190
|
-
if (actionBlock !== undefined) {
|
|
191
|
-
// The text block rides ALONGSIDE the buttons, for the reason `deliverSlackDirectMessage` states:
|
|
192
|
-
// once `blocks` is present `text` is only the notification fallback, so without the section the
|
|
193
|
-
// room would see a row of buttons over an empty message.
|
|
194
|
-
body.set("blocks", JSON.stringify([{ text: { text: rendered, type: "mrkdwn" }, type: "section" }, actionBlock]));
|
|
195
|
-
}
|
|
196
|
-
return body;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
165
|
/**
|
|
200
166
|
* THE COMPONENT PLANE (OPE-1060 S3) — the `actions` a {@link DirectMessageRequest} carries, rendered
|
|
201
|
-
* as ONE Slack `actions` block of buttons.
|
|
202
|
-
* bridged approval card and a DM card cannot drift in how a button is built or capped.
|
|
167
|
+
* as ONE Slack `actions` block of buttons.
|
|
203
168
|
*
|
|
204
169
|
* 🔴 THE TEXT STILL STANDS ON ITS OWN. `post-arms.ts` states the contract: an arm with no component
|
|
205
170
|
* plane delivers the text alone and never fakes the buttons, so every caller's copy already says what
|
|
@@ -298,12 +263,6 @@ export const slackPostArm: PostArm = {
|
|
|
298
263
|
dispatchReply: (target, text, replier) => dispatchSlackReply(target as SlackTarget, text, replier),
|
|
299
264
|
dispatchUnified: (origin, text, deps: UnifiedReplyDeps, replier) =>
|
|
300
265
|
deps.postSlack ? deps.postSlack(origin as SlackOrigin, text) : liveSlackPost(origin as SlackOrigin, text, replier),
|
|
301
|
-
// OPE-1720: Slack declares the unified component plane because the tap route exists on it —
|
|
302
|
-
// `slack-front.ts` classifies the `oe_input:` click and `slack-approval-card.ts` settles it. The
|
|
303
|
-
// injected `postSlack` override is honoured VERBATIM and drops the buttons: a test that scripted a
|
|
304
|
-
// text-only post must not start receiving a card it never asked for.
|
|
305
|
-
dispatchUnifiedActions: (origin, text, actions, deps: UnifiedReplyDeps, replier) =>
|
|
306
|
-
deps.postSlack ? deps.postSlack(origin as SlackOrigin, text) : liveSlackPostWithActions(origin as SlackOrigin, text, actions, replier),
|
|
307
266
|
describeRedriveTarget: (origin) => {
|
|
308
267
|
const o = origin as SlackOrigin;
|
|
309
268
|
return `slack ${o.threadTs ? `thread in ${o.channelId}` : `channel ${o.channelId}`}`;
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
type SeatCatchUpFetch,
|
|
40
40
|
type SeatCatchUpRequest,
|
|
41
41
|
type WakeMessageInput,
|
|
42
|
+
SUPERVISOR_BACKSCROLL_REACHBACK_HOURS,
|
|
42
43
|
} from "@open-everywhere/core/agent/adapter-api.js";
|
|
43
44
|
|
|
44
45
|
// Re-export the shared cap so this module's public surface (and its test, which imports it from
|
|
@@ -271,8 +272,8 @@ function cappedEntry(lines: readonly ContextLine[], source: string): WakeBackscr
|
|
|
271
272
|
/** Reachback (hours) for the raw Slack window: how far back from the trigger conversations.history
|
|
272
273
|
* is paginated. Generous enough that the SPINE (the most-recent cluster, exempt from the 36h
|
|
273
274
|
* enrichment cap) can still reach across a weekend/overnight gap (~66h Fri-4pm→Mon-10am), yet a
|
|
274
|
-
* hard floor that bounds the fetch.
|
|
275
|
-
export const RAW_FETCH_REACHBACK_HOURS =
|
|
275
|
+
* hard floor that bounds the fetch. 7 days — the core constant both adapters take (OPE-2411). */
|
|
276
|
+
export const RAW_FETCH_REACHBACK_HOURS = SUPERVISOR_BACKSCROLL_REACHBACK_HOURS;
|
|
276
277
|
|
|
277
278
|
/** conversations.history page size (Slack max is 1000; 200 keeps each page cheap). */
|
|
278
279
|
const RAW_FETCH_PAGE_LIMIT = 200;
|
package/agent/channels/slack.ts
CHANGED
|
@@ -589,7 +589,7 @@ async function onInbound(
|
|
|
589
589
|
// wake: burned inference at best, a mention ping-pong at worst. Our own engagement and
|
|
590
590
|
// name-drop wake paths drop bot authors too (slack-events.ts authorIsBot); this closes
|
|
591
591
|
// the one remaining leg. ONE carve-out, same as the Discord gateway: an allowlisted e2e
|
|
592
|
-
// driver (
|
|
592
|
+
// driver (a `wake.driverAllowlist` `teamId:botId` pair). The driver posts with a USER token, but
|
|
593
593
|
// Slack stamps app-authored posts with the app's bot_id regardless (live 2026-07-07 —
|
|
594
594
|
// the plan's "xoxp needs no seam" premise was wrong). Fail-closed, guild^Wteam-pinned,
|
|
595
595
|
// exact-id, loud — see testing/driver-allowlist.ts for the full posture.
|
|
@@ -599,7 +599,7 @@ async function onInbound(
|
|
|
599
599
|
// binds fail-closed external and memoryless". That was the bug, not a tolerable degradation:
|
|
600
600
|
// an un-gated model turn ran on every non-allowlisted bot mention, ABOVE the spend gate, so a
|
|
601
601
|
// capped surface kept spending. It is now a REAL drop (see DROP). Note the consequence for the
|
|
602
|
-
// e2e driver: if its `teamId:botId`
|
|
602
|
+
// e2e driver: if its `teamId:botId` is not in `wake.driverAllowlist`, its mentions no longer produce
|
|
603
603
|
// a degraded reply — they produce NO reply plus a `surface.wake_dropped` audit row saying so.
|
|
604
604
|
if (message.author?.isBot) {
|
|
605
605
|
const rawBotId = typeof (message.raw as { bot_id?: unknown })?.bot_id === "string" ? (message.raw as { bot_id: string }).bot_id : undefined;
|