@open-everywhere/provider-slack 0.1.5 → 0.3.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 (68) hide show
  1. package/NOTICE +9 -9
  2. package/README.md +1 -1
  3. package/agent/app-manifest.ts +44 -9
  4. package/agent/channels/slack-commands.ts +9 -5
  5. package/agent/channels/slack-events.ts +76 -13
  6. package/agent/channels/slack-front.ts +4 -80
  7. package/agent/channels/slack-post-arm.ts +1 -42
  8. package/agent/channels/slack-task-card.ts +182 -0
  9. package/agent/channels/slack.ts +518 -468
  10. package/agent/contributions.ts +1 -1
  11. package/agent/docs-generated.ts +1 -1
  12. package/agent/ops.ts +20 -2
  13. package/agent/setup/claim.ts +16 -1
  14. package/agent/setup/eve-credentials.ts +10 -2
  15. package/agent/setup/page.ts +79 -14
  16. package/agent/surface-parity.ts +0 -6
  17. package/dist/agent/app-manifest.d.ts +20 -4
  18. package/dist/agent/app-manifest.d.ts.map +1 -1
  19. package/dist/agent/app-manifest.js +29 -6
  20. package/dist/agent/app-manifest.js.map +1 -1
  21. package/dist/agent/channels/slack-commands.d.ts.map +1 -1
  22. package/dist/agent/channels/slack-commands.js +9 -6
  23. package/dist/agent/channels/slack-commands.js.map +1 -1
  24. package/dist/agent/channels/slack-events.d.ts +28 -6
  25. package/dist/agent/channels/slack-events.d.ts.map +1 -1
  26. package/dist/agent/channels/slack-events.js +74 -14
  27. package/dist/agent/channels/slack-events.js.map +1 -1
  28. package/dist/agent/channels/slack-front.d.ts +1 -15
  29. package/dist/agent/channels/slack-front.d.ts.map +1 -1
  30. package/dist/agent/channels/slack-front.js +4 -62
  31. package/dist/agent/channels/slack-front.js.map +1 -1
  32. package/dist/agent/channels/slack-post-arm.d.ts +1 -8
  33. package/dist/agent/channels/slack-post-arm.d.ts.map +1 -1
  34. package/dist/agent/channels/slack-post-arm.js +1 -40
  35. package/dist/agent/channels/slack-post-arm.js.map +1 -1
  36. package/dist/agent/channels/slack-task-card.d.ts +38 -0
  37. package/dist/agent/channels/slack-task-card.d.ts.map +1 -0
  38. package/dist/agent/channels/slack-task-card.js +156 -0
  39. package/dist/agent/channels/slack-task-card.js.map +1 -0
  40. package/dist/agent/channels/slack.d.ts.map +1 -1
  41. package/dist/agent/channels/slack.js +516 -466
  42. package/dist/agent/channels/slack.js.map +1 -1
  43. package/dist/agent/contributions.d.ts +1 -1
  44. package/dist/agent/contributions.d.ts.map +1 -1
  45. package/dist/agent/contributions.js +1 -1
  46. package/dist/agent/contributions.js.map +1 -1
  47. package/dist/agent/docs-generated.js +1 -1
  48. package/dist/agent/docs-generated.js.map +1 -1
  49. package/dist/agent/ops.d.ts +5 -0
  50. package/dist/agent/ops.d.ts.map +1 -1
  51. package/dist/agent/ops.js +13 -2
  52. package/dist/agent/ops.js.map +1 -1
  53. package/dist/agent/setup/claim.d.ts.map +1 -1
  54. package/dist/agent/setup/claim.js +15 -2
  55. package/dist/agent/setup/claim.js.map +1 -1
  56. package/dist/agent/setup/eve-credentials.d.ts +9 -2
  57. package/dist/agent/setup/eve-credentials.d.ts.map +1 -1
  58. package/dist/agent/setup/eve-credentials.js +9 -1
  59. package/dist/agent/setup/eve-credentials.js.map +1 -1
  60. package/dist/agent/setup/page.d.ts +2 -0
  61. package/dist/agent/setup/page.d.ts.map +1 -1
  62. package/dist/agent/setup/page.js +71 -15
  63. package/dist/agent/setup/page.js.map +1 -1
  64. package/dist/agent/surface-parity.d.ts.map +1 -1
  65. package/dist/agent/surface-parity.js +0 -6
  66. package/dist/agent/surface-parity.js.map +1 -1
  67. package/package.json +10 -8
  68. package/agent/channels/slack-approval-card.ts +0 -275
package/NOTICE CHANGED
@@ -12,28 +12,28 @@ Apache License, Version 2.0.
12
12
 
13
13
  Modifications to eve
14
14
 
15
- The @open-everywhere/core package distributes patches/eve+0.54.3.patch. That
16
- patch file is a modification of eve 0.54.3. Vercel licenses eve 0.54.3 under the
17
- Apache License, Version 2.0. The copyright notice of eve 0.54.3 reads
15
+ The @open-everywhere/core package distributes patches/eve+0.71.0.patch. That
16
+ patch file is a modification of eve 0.71.0. Vercel licenses eve 0.71.0 under the
17
+ Apache License, Version 2.0. The copyright notice of eve 0.71.0 reads
18
18
  "Copyright 2026 Vercel, Inc. and contributors". The Open Everywhere authors made
19
19
  the changes in the patch file.
20
20
 
21
- The patch modifies six files of eve 0.54.3:
21
+ The patch modifies four files of eve 0.71.0:
22
22
 
23
- dist/src/context/dynamic-tool-lifecycle.js
24
23
  dist/src/execution/web-fetch/request.js
25
24
  dist/src/harness/tool-loop.js
26
25
  dist/src/internal/authored-package-boundary.js
27
26
  dist/src/protocol/message.d.ts
28
- dist/src/tasks/session-index.js
29
27
 
30
28
  Open Everywhere does not ship the eve package. eve is a peer dependency, so the
31
- consumer installs eve 0.54.3 from the registry. The patch file carries the
29
+ consumer installs eve 0.71.0 from the registry. The patch file carries the
32
30
  modified portions of eve. The oe-eve-patch command, which
33
31
  @open-everywhere/core ships, applies them to the eve that the consumer
34
- installed. The command writes no file outside the consumer's own project.
32
+ installed. The command patches no file outside the consumer's own project. To
33
+ restore an eve that holds an older Open Everywhere patch, it also writes to npm's
34
+ cache and to a temporary directory.
35
35
 
36
- The attribution notice of eve 0.54.3 follows, as section 4(d) of the Apache
36
+ The attribution notice of eve 0.71.0 follows, as section 4(d) of the Apache
37
37
  License, Version 2.0 requires. The notices for eve's bundled code-mode runtime
38
38
  are not reproduced, because the patch modifies no part of that runtime.
39
39
 
package/README.md CHANGED
@@ -15,7 +15,7 @@ asserts that **every** one of them is absent from `packages/core/agent`. For Sla
15
15
  ## Install
16
16
 
17
17
  ```sh
18
- npm i @open-everywhere/core @open-everywhere/provider-slack eve@0.54.3
18
+ npm i @open-everywhere/core @open-everywhere/provider-slack eve@0.71.0
19
19
  npx oe-eve-patch
20
20
  ```
21
21
 
@@ -16,9 +16,16 @@
16
16
  * operator, who has usually not deployed yet when they run it. */
17
17
  export const SLACK_DEPLOY_HOST_PLACEHOLDER = "YOUR-DEPLOY-URL";
18
18
 
19
- /** The app name the manifest carries when the operator names none. */
19
+ /** The app name the manifest carries when the operator names none AND the deployment configures no
20
+ * `identity.name`. The same brand default core's `agentDisplayName()` falls back to. */
20
21
  export const SLACK_DEFAULT_APP_NAME = "Open Everywhere";
21
22
 
23
+ /** The slash command's name when the caller passes none — the shipped `identity.commandName`. */
24
+ const SLACK_DEFAULT_COMMAND_NAME = "oe";
25
+
26
+ /** Slack's limit on a shortcut's name. A longer one fails at paste time. */
27
+ export const SLACK_SHORTCUT_NAME_MAX = 24;
28
+
22
29
  /** Slack's limit on an app name (`display_information.name`). A longer name fails at paste time. */
23
30
  export const SLACK_APP_NAME_MAX = 35;
24
31
 
@@ -31,17 +38,44 @@ export function slackAppNameProblem(raw: string): string | undefined {
31
38
  return undefined;
32
39
  }
33
40
 
34
- /** Options for {@link slackAppManifestTemplate}. */
41
+ /** Options for {@link slackAppManifestTemplate}. This module imports nothing, so the caller reads
42
+ * the deployment's settings and passes them in (OPE-2334): `agent/ops.ts` and the `/setup` page
43
+ * read core's resolvers, and the setup CLI reads the app's own settings file. */
35
44
  export interface SlackAppManifestOptions {
36
- /** The app name and the bot user's display name. Default {@link SLACK_DEFAULT_APP_NAME}. A
37
- * second install in one workspace needs its own name, or the two apps look the same. */
45
+ /** An EXPLICIT app name (`--app-name`, the ops tool's `appName`, the `/setup` form). It wins
46
+ * over {@link agentName}. A second install in one workspace needs its own name, or the two apps
47
+ * look the same. */
38
48
  readonly appName?: string;
49
+ /** The deployment's configured name — the `identity.name` setting. It names the app, the bot user,
50
+ * the shortcuts and the command description when no explicit {@link appName} is given. Absent ⇒
51
+ * {@link SLACK_DEFAULT_APP_NAME}. */
52
+ readonly agentName?: string;
53
+ /** The slash command's name without the slash — the `identity.commandName` setting. Absent ⇒
54
+ * {@link SLACK_DEFAULT_COMMAND_NAME}. */
55
+ readonly commandName?: string;
56
+ }
57
+
58
+ /** The app name a manifest carries: the explicit name, else the configured name, else the default.
59
+ * A configured name past Slack's limit is cut to fit, because Slack refuses the whole paste. */
60
+ export function slackManifestAppName(opts: SlackAppManifestOptions = {}): string {
61
+ const explicit = opts.appName?.trim();
62
+ if (explicit) return explicit;
63
+ const configured = opts.agentName?.trim();
64
+ if (configured) return configured.slice(0, SLACK_APP_NAME_MAX).trim();
65
+ return SLACK_DEFAULT_APP_NAME;
66
+ }
67
+
68
+ /** A shortcut name that names the app, or the neutral form when the name does not fit. */
69
+ function shortcutName(prefix: string, appName: string, neutral: string): string {
70
+ const named = `${prefix} ${appName}`;
71
+ return named.length <= SLACK_SHORTCUT_NAME_MAX ? named : neutral;
39
72
  }
40
73
 
41
74
  /** A ready-to-paste Slack app manifest (Slack's "create from manifest" flow) with the scope/event
42
75
  * set docs/deploy/surfaces/slack.md walks through by hand. */
43
76
  export function slackAppManifestTemplate(opts: SlackAppManifestOptions = {}): string {
44
- const appName = opts.appName?.trim() || SLACK_DEFAULT_APP_NAME;
77
+ const appName = slackManifestAppName(opts);
78
+ const commandName = opts.commandName?.trim() || SLACK_DEFAULT_COMMAND_NAME;
45
79
  return JSON.stringify(
46
80
  {
47
81
  display_information: { name: appName },
@@ -60,9 +94,10 @@ export function slackAppManifestTemplate(opts: SlackAppManifestOptions = {}): st
60
94
  // configures SEPARATELY from the Events request URL above.
61
95
  slash_commands: [
62
96
  {
63
- command: "/oe",
97
+ // The `identity.commandName` setting (OPE-2334). The route checks the same name.
98
+ command: `/${commandName}`,
64
99
  url: `https://${SLACK_DEPLOY_HOST_PLACEHOLDER}/eve/v1/slack/commands`,
65
- description: "Open Everywhere control-plane commands (stop, status, budget) — no model turn.",
100
+ description: `${appName} control-plane commands (stop, status, budget) — no model turn.`,
66
101
  // Every subcommand `parseOeCommandTokens` accepts (packages/core/agent/commands/core.ts).
67
102
  // scripts/slack-app-manifest-test.ts fails if one is missing here.
68
103
  usage_hint: "stop | status | budget [raise <usd> | clear] | rate <n> | help",
@@ -80,13 +115,13 @@ export function slackAppManifestTemplate(opts: SlackAppManifestOptions = {}): st
80
115
  // receives, so renaming one here changes only the trigger name the model is told about.
81
116
  shortcuts: [
82
117
  {
83
- name: "Send to Open Everywhere",
118
+ name: shortcutName("Send to", appName, "Send to the agent"),
84
119
  type: "message",
85
120
  callback_id: "oe_message_shortcut",
86
121
  description: "Wake the agent on this message.",
87
122
  },
88
123
  {
89
- name: "Ask Open Everywhere",
124
+ name: shortcutName("Ask", appName, "Ask the agent"),
90
125
  type: "global",
91
126
  callback_id: "oe_global_shortcut",
92
127
  description: "Wake the agent in a DM.",
@@ -38,6 +38,7 @@ import { slackSigningSecret } from "../setup/credentials.js";
38
38
  import {
39
39
  bindFrom,
40
40
  runOeCommand,
41
+ oeSlash,
41
42
  parseSlackCommandText,
42
43
  resolvePartition,
43
44
  getOperationalStore,
@@ -204,13 +205,16 @@ export default defineChannel({
204
205
  }
205
206
 
206
207
  const command = formField(params, "command");
207
- if (command !== "/oe") {
208
- // Defensive, not a security gate: this route is dedicated to "/oe" — the Slack app's
208
+ // The command's name is the `identity.commandName` setting (OPE-2334), the same value the
209
+ // manifest declares, so a renamed command is answered here rather than refused.
210
+ const expected = oeSlash();
211
+ if (command !== expected) {
212
+ // Defensive, not a security gate: this route is dedicated to the one command — the Slack app's
209
213
  // Slash Command config should only ever point ONE command at it. Anything else is a
210
214
  // misconfiguration (a second command aimed at the same URL); answer politely instead
211
215
  // of a bare 404/500 so it's obvious in Slack what to fix.
212
- log(`unexpected slash command "${command ?? "?"}" hit the /oe route — check the Slack app's Slash Command config`);
213
- return ephemeralResponse('This endpoint only handles "/oe" — check your Slack app\'s Slash Command configuration.');
216
+ log(`unexpected slash command "${command ?? "?"}" hit the ${expected} route — check the Slack app's Slash Command config`);
217
+ return ephemeralResponse(`This endpoint only handles "${expected}" — check your Slack app's Slash Command configuration.`);
214
218
  }
215
219
 
216
220
  const cmd = parseSlackCommandText(formField(params, "text"));
@@ -223,7 +227,7 @@ export default defineChannel({
223
227
  // (e.g. proposing from a DM with no explicit scope) — surface it verbatim. Never let a
224
228
  // thrown error escape as a 500; Slack's slash-command UX for that is an ugly
225
229
  // "operation_timeout"/dispatch_failed toast.
226
- log(`"/oe" command failed softly: ${String(err)}`);
230
+ log(`"${expected}" command failed softly: ${String(err)}`);
227
231
  return ephemeralResponse(`Couldn't complete that: ${err instanceof Error ? err.message : String(err)}`);
228
232
  }
229
233
  }),
@@ -1430,6 +1430,7 @@ 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
1436
  // Inert on the shipped grains (`taint` / `engage`), so a deployment that has not opted in is
@@ -2600,6 +2601,7 @@ 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
@@ -3682,33 +3684,91 @@ export function fetchChannelDisplayName(channelId: string): Promise<string | und
3682
3684
  return p;
3683
3685
  }
3684
3686
 
3687
+ /** Slack error codes that mean this app STRUCTURALLY cannot read a room's roster (OPE-2306): a scope
3688
+ * the install does not hold, a room it cannot see, a permission or Grid grant it lacks. A retry
3689
+ * cannot change them, so the roster is `unobservable` and the observed-guest legs stay inert. Every
3690
+ * other non-ok code is `failed` ON PURPOSE (fail closed): an unlisted steady-state code makes the
3691
+ * connector-disclosure floor refuse until it is added here. */
3692
+ const SLACK_ROSTER_UNOBSERVABLE_ERRORS: ReadonlySet<string> = new Set([
3693
+ "missing_scope",
3694
+ "channel_not_found",
3695
+ "not_in_channel",
3696
+ "method_not_supported_for_channel_type",
3697
+ "no_permission",
3698
+ "team_access_not_granted",
3699
+ ]);
3700
+
3701
+ /** The unresolved state of a non-ok Slack answer. No answer at all (a network failure, an exhausted
3702
+ * 429 retry, no bot token: slackForm returns `undefined`) is `failed`, and so is an unknown code. */
3703
+ function slackRosterFailureState(res: Record<string, unknown> | undefined): "unobservable" | "failed" {
3704
+ return typeof res?.error === "string" && SLACK_ROSTER_UNOBSERVABLE_ERRORS.has(res.error) ? "unobservable" : "failed";
3705
+ }
3706
+
3685
3707
  /**
3686
3708
  * Live Slack roster for a governance channel's floor tripwire (OPE-110, access/audience-floor.ts).
3687
- * Two fail-closed Slack reads: conversations.info (is_private + externally-shared bit) and
3688
- * conversations.members (member ids). The bot's own user id is excluded so the agent doesn't count
3689
- * as a non-admin member. FAIL CLOSED: any failed/absent lookup ⇒ `resolved: false` ⇒ the floor can
3690
- * never arm. `hasGuest` uses the channel's externally-shared bit as the available external signal;
3709
+ * Up to three Slack reads: conversations.info (is_private + externally-shared bit),
3710
+ * conversations.members (member ids) and auth.test (the bot's own user id). The roster excludes the
3711
+ * bot's own user id, so the agent doesn't count as a non-admin member. A failed conversations.info
3712
+ * or conversations.members read, a members list of a NON-SHARED channel longer than one page, or a
3713
+ * thrown error returns `resolved: false`, with an `unresolvedState` (OPE-2306). A transient failure
3714
+ * is `failed`: a non-ok answer outside SLACK_ROSTER_UNOBSERVABLE_ERRORS, no answer, a thrown error, a
3715
+ * conversations.info answer with no channel object. A steady state is `unobservable`: a code in that
3716
+ * set, or a non-shared members list longer than one page. A failed auth.test does not unresolve the roster: the roster stays resolved and counts the bot as a
3717
+ * non-admin member, so the governance channel does not arm. `resolved: false` is closed for the
3718
+ * governance tripwire: the floor computes to `guest`, so the channel can never arm. It is
3719
+ * also closed for the clicker floor's beneficiary leg, which refuses on it (`unattested-room`). It
3720
+ * is closed for the connector-disclosure floor when it is `failed`: that floor refuses until the
3721
+ * roster resolves (OPE-2306). It is NOT closed for the observed-guest legs of the triggered-fire
3722
+ * disclosure floor, the card-disclosure floor and the approval-card clicker floor, nor for the
3723
+ * connector-disclosure floor on an `unobservable` roster. Those refuse only an OBSERVED guest, so
3724
+ * they stay INERT (access/audience-floor.ts, the polarity rule). Core memoizes a `resolved: false`
3725
+ * answer for UNRESOLVED_ROSTER_TTL_MS (30 s) only, not for the full memo TTL (OPE-2150), so one
3726
+ * failed read disarms the inert legs, and refuses the connector-disclosure floor, for at most that
3727
+ * short window.
3728
+ * `hasGuest` uses the channel's externally-shared bit as the available external signal;
3691
3729
  * per-member guest classification rides the guest-detection work (design §2/§10 fragile-point #1) and
3692
3730
  * is a future tightening, not a loosening (an internal-only private channel already needs every
3693
3731
  * member on the roster to arm). Governance channels are small by config lint, so a single members
3694
- * page suffices; a channel larger than one page fails closed on the (unset) cursor rather than
3695
- * silently attesting a partial roster.
3732
+ * page suffices. A NON-SHARED channel larger than one page returns `resolved: false` rather than a
3733
+ * partial roster: the governance channel does not arm, and the disclosure floors stay inert
3734
+ * (`unobservable`: the member list is incomplete and a retry gets the same page count). An
3735
+ * EXTERNALLY-SHARED channel larger than one page resolves with `hasGuest: true` and an empty member
3736
+ * list, without a read of the other pages (OPE-2311). The floor counts the channel's shared bit as
3737
+ * a guest present, the same signal the one-page path uses (the bit includes a pending external
3738
+ * invitation and an Enterprise Grid org share). So the floor is `guest` and the observed-guest legs
3739
+ * of every consumer refuse.
3696
3740
  */
3697
3741
  export async function slackGovernanceRoster(channelId: string): Promise<RoomRoster | undefined> {
3698
3742
  try {
3699
3743
  const info = await slackForm("conversations.info", `channel=${encodeURIComponent(channelId)}`);
3700
- if (info?.ok !== true) return { humanMemberIds: [], resolved: false, isPrivate: false, hasGuest: false };
3744
+ if (info?.ok !== true) {
3745
+ return { humanMemberIds: [], resolved: false, unresolvedState: slackRosterFailureState(info), isPrivate: false, hasGuest: false };
3746
+ }
3701
3747
  const facts = channelFactsFromInfo(info);
3702
3748
  const members = await slackForm("conversations.members", `channel=${encodeURIComponent(channelId)}&limit=200`);
3703
3749
  if (members?.ok !== true || !Array.isArray(members.members)) {
3704
- return { humanMemberIds: [], resolved: false, isPrivate: false, hasGuest: false };
3750
+ return { humanMemberIds: [], resolved: false, unresolvedState: slackRosterFailureState(members), isPrivate: false, hasGuest: false };
3705
3751
  }
3706
3752
  const cursor = (members.response_metadata as { next_cursor?: string } | undefined)?.next_cursor;
3707
3753
  if (cursor && cursor.length > 0) {
3708
- // More than one page: a governance channel this large is off-policy (lint caps channel COUNT,
3709
- // not membership, but a >200-member "admin" room is a smell) — fail closed rather than attest a
3710
- // partial roster as if it were the whole audience.
3711
- return { humanMemberIds: [], resolved: false, isPrivate: false, hasGuest: false };
3754
+ // More than one page. An externally-shared channel needs no member list: the floor counts the
3755
+ // shared bit as a guest present, as the one-page path does (OPE-2311). So the roster resolves
3756
+ // with `hasGuest: true` and an EMPTY member list, never a partial one. The floor is `guest` for every consumer, and
3757
+ // the observed-guest legs refuse. The share bit is a steady state, so the full memo TTL is fine.
3758
+ if (facts.trustChecked === true && facts.isExternal === true) {
3759
+ return { humanMemberIds: [], resolved: true, isPrivate: facts.isPrivate === true, hasGuest: true };
3760
+ }
3761
+ // An ok conversations.info with no channel object is an odd answer, not a steady state: it is
3762
+ // `failed`, as on the one-page path below.
3763
+ if (facts.trustChecked !== true) {
3764
+ return { humanMemberIds: [], resolved: false, unresolvedState: "failed", isPrivate: false, hasGuest: false };
3765
+ }
3766
+ // A non-shared channel this large: do not attest a partial roster as if it were the whole
3767
+ // audience (a >200-member "admin" room is off-policy anyway). An unresolved roster keeps the
3768
+ // governance channel disarmed, but it leaves the disclosure floors INERT, not closed
3769
+ // (access/audience-floor.ts, the polarity rule). `unobservable` (OPE-2306): a retry gets the
3770
+ // same page count.
3771
+ return { humanMemberIds: [], resolved: false, unresolvedState: "unobservable", isPrivate: false, hasGuest: false };
3712
3772
  }
3713
3773
  const auth = await slackForm("auth.test", "");
3714
3774
  const botId = auth?.ok === true && typeof auth.user_id === "string" ? auth.user_id : undefined;
@@ -3718,11 +3778,13 @@ export async function slackGovernanceRoster(channelId: string): Promise<RoomRost
3718
3778
  return {
3719
3779
  humanMemberIds,
3720
3780
  resolved: facts.trustChecked === true, // conversations.info authoritative
3781
+ // An ok conversations.info with no channel object is an odd answer, not a steady state.
3782
+ ...(facts.trustChecked === true ? {} : { unresolvedState: "failed" as const }),
3721
3783
  isPrivate: facts.isPrivate === true,
3722
3784
  hasGuest: facts.isExternal === true,
3723
3785
  };
3724
3786
  } catch {
3725
- return { humanMemberIds: [], resolved: false, isPrivate: false, hasGuest: false };
3787
+ return { humanMemberIds: [], resolved: false, unresolvedState: "failed", isPrivate: false, hasGuest: false };
3726
3788
  }
3727
3789
  }
3728
3790
 
@@ -4157,6 +4219,7 @@ export async function routeSupervisorEvent(
4157
4219
  authorId: ev.userId,
4158
4220
  surfaceInternal: authorFacts.surfaceInternal,
4159
4221
  channelKey: domainKeyOf(p),
4222
+ ...(ev.enterpriseId === undefined ? {} : { enterpriseId: ev.enterpriseId }),
4160
4223
  });
4161
4224
  // ── THE EXTERNAL-PARTICIPANT POLICY (OPE-507 / OPE-504) ────────────────────────────────────────
4162
4225
  // ABOVE THE SESSION MINT, deliberately: a suppressed wake must not rotate a generation or claim a
@@ -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
@@ -576,7 +538,7 @@ export interface FrontOptions {
576
538
  * tests can stub it without the propose/store/executor machinery it drags in.
577
539
  *
578
540
  * 🔴 IT TAKES THE ROUTE'S `waitUntil` (OPE-1709), for the same reason the Discord front's
579
- * approval branch does: "Approve for session" releases the sibling cards the grant already
541
+ * approval branch does: "Approve for this routine" releases the sibling cards the grant already
580
542
  * covers, and each release runs a real `execute`. That work belongs off the interactivity
581
543
  * response path. The handler falls back to its old awaited shape when this is absent, which is
582
544
  * exactly what a stub that ignores the second argument gets. */
@@ -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;
@@ -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
  //
@@ -2027,6 +1985,7 @@ export function buildFrontHandler(innerHandler: RouteHandler, opts: FrontOptions
2027
1985
  authorId: mev.userId,
2028
1986
  surfaceInternal: facts.surfaceInternal,
2029
1987
  channelKey: roomKey,
1988
+ ...(mev.enterpriseId === undefined ? {} : { enterpriseId: mev.enterpriseId }),
2030
1989
  });
2031
1990
  const verdict = await applySlackExternalPolicy(mev, p, { trust: wakeTrust, refuted: facts.refuted }, "native-mention-forward");
2032
1991
  if (verdict !== "proceed") {
@@ -2058,7 +2017,7 @@ export function buildFrontHandler(innerHandler: RouteHandler, opts: FrontOptions
2058
2017
  //
2059
2018
  // 🔴 `args.waitUntil` RIDES ALONG (OPE-1709), and it is the HELD one (see the route hold
2060
2019
  // above), so the deferred work is quiesced before the delivery's own promise resolves. What
2061
- // it moves off this path is the session-grant release: an "Approve for session" click on a
2020
+ // it moves off this path is the session-grant release: an "Approve for this routine" click on a
2062
2021
  // fan-out decides and EXECUTES the sibling cards the grant already covers, and that loop has
2063
2022
  // no business inside Slack's three-second interactivity budget.
2064
2023
  if (await handleApproval(decision.payload, { waitUntil: args.waitUntil })) {
@@ -2140,41 +2099,6 @@ export function buildFrontHandler(innerHandler: RouteHandler, opts: FrontOptions
2140
2099
  return new Response(null, { status: 200 });
2141
2100
  }
2142
2101
 
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
2102
  if (decision.kind === "setkey-submit") {
2179
2103
  // THE TENANT FENCE (OPE-1680), above the handler — this is the branch that WRITES A SECRET into
2180
2104
  // 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}`}`;