@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.
Files changed (45) hide show
  1. package/agent/app-manifest.ts +1 -0
  2. package/agent/channels/slack-events.ts +22 -19
  3. package/agent/channels/slack-front.ts +10 -87
  4. package/agent/channels/slack-post-arm.ts +1 -42
  5. package/agent/channels/slack-wake-backscroll.ts +3 -2
  6. package/agent/channels/slack.ts +2 -2
  7. package/agent/docs-generated.ts +1 -1
  8. package/agent/setup/claim.ts +16 -1
  9. package/agent/setup/page.ts +68 -6
  10. package/agent/surface-parity.ts +0 -6
  11. package/dist/agent/app-manifest.d.ts.map +1 -1
  12. package/dist/agent/app-manifest.js +1 -0
  13. package/dist/agent/app-manifest.js.map +1 -1
  14. package/dist/agent/channels/slack-events.d.ts +5 -5
  15. package/dist/agent/channels/slack-events.d.ts.map +1 -1
  16. package/dist/agent/channels/slack-events.js +20 -17
  17. package/dist/agent/channels/slack-events.js.map +1 -1
  18. package/dist/agent/channels/slack-front.d.ts +0 -14
  19. package/dist/agent/channels/slack-front.d.ts.map +1 -1
  20. package/dist/agent/channels/slack-front.js +11 -70
  21. package/dist/agent/channels/slack-front.js.map +1 -1
  22. package/dist/agent/channels/slack-post-arm.d.ts +1 -8
  23. package/dist/agent/channels/slack-post-arm.d.ts.map +1 -1
  24. package/dist/agent/channels/slack-post-arm.js +1 -40
  25. package/dist/agent/channels/slack-post-arm.js.map +1 -1
  26. package/dist/agent/channels/slack-wake-backscroll.d.ts +2 -2
  27. package/dist/agent/channels/slack-wake-backscroll.d.ts.map +1 -1
  28. package/dist/agent/channels/slack-wake-backscroll.js +3 -3
  29. package/dist/agent/channels/slack-wake-backscroll.js.map +1 -1
  30. package/dist/agent/channels/slack.js +2 -2
  31. package/dist/agent/channels/slack.js.map +1 -1
  32. package/dist/agent/docs-generated.js +1 -1
  33. package/dist/agent/docs-generated.js.map +1 -1
  34. package/dist/agent/setup/claim.d.ts.map +1 -1
  35. package/dist/agent/setup/claim.js +15 -2
  36. package/dist/agent/setup/claim.js.map +1 -1
  37. package/dist/agent/setup/page.d.ts +2 -0
  38. package/dist/agent/setup/page.d.ts.map +1 -1
  39. package/dist/agent/setup/page.js +60 -6
  40. package/dist/agent/setup/page.js.map +1 -1
  41. package/dist/agent/surface-parity.d.ts.map +1 -1
  42. package/dist/agent/surface-parity.js +0 -6
  43. package/dist/agent/surface-parity.js.map +1 -1
  44. package/package.json +2 -7
  45. package/agent/channels/slack-approval-card.ts +0 -275
@@ -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 shipped grains (`taint` / `engage`), so a deployment that has not opted in is
1436
- // byte-identical. It sits here because this is where the author evidence first exists — see the
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 `OE_TEST_DRIVER_IDS` allowlist vouches for this bot at the engagement and mention
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 `OE_TEST_DRIVER_IDS` ALLOWLIST, which already lifts the same bot at 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 shipped `taint` grain. */
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 shipped `taint` grain"
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 shipped grains.
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
- // shipped `taint` grain, so nothing moves for a deployment that has not opted in.
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 SHIPPED GRAIN: `taint` returns the wake by reference before any probe runs.
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 opts into ambientSupervisor
3826
- // (memory/domain.ts, fail-closed off) AND the channel is POSITIVELY confirmed internal,
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 shipped `taint` grain, and never on a wake that carries no hydrated lines. */
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 opted into ambientSupervisor.
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 shipped grains (`taint` / `engage`).
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 shipped `taint` grain, so no
4359
- // golden moves on a deployment that has not opted in.
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 shipped `taint` grain it returns 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 shipped `taint` grain, so nothing moves for a deployment that has
4480
- // not opted in. Run only on the name-drop tier, which is the only tier that carries the slot.
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): today's default is the
1203
- // name-drop path (per-domain opt-in, fail-closed). When the channel's domain opts into
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 today. Parametrised on `ev` so both the app_mention divert and a plain
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 DM as well. Nothing moves
1470
- // on the SUPPRESSION PATH for a default deployment: `withholdsBody` is inert under the shipped
1471
- // `taint` grain, so only a deployment that explicitly excluded refuted authors is suppressed
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 shipped `taint` / `engage` pair no evidence is resolved at all and
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. Shared since OPE-1720 with the unified post above, so a
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. 7d default; overridable for ops/testing. */
275
- export const RAW_FETCH_REACHBACK_HOURS = Number(process.env.OE_SUPERVISOR_BACKSCROLL_REACHBACK_HOURS) || 168;
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;
@@ -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 (OE_TEST_DRIVER_IDS `teamId:botId`). The driver posts with a USER token, but
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` isn't in OE_TEST_DRIVER_IDS, its mentions no longer produce
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;