@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.
- package/NOTICE +9 -9
- package/README.md +1 -1
- package/agent/app-manifest.ts +44 -9
- package/agent/channels/slack-commands.ts +9 -5
- package/agent/channels/slack-events.ts +76 -13
- package/agent/channels/slack-front.ts +4 -80
- package/agent/channels/slack-post-arm.ts +1 -42
- package/agent/channels/slack-task-card.ts +182 -0
- package/agent/channels/slack.ts +518 -468
- package/agent/contributions.ts +1 -1
- package/agent/docs-generated.ts +1 -1
- package/agent/ops.ts +20 -2
- package/agent/setup/claim.ts +16 -1
- package/agent/setup/eve-credentials.ts +10 -2
- package/agent/setup/page.ts +79 -14
- package/agent/surface-parity.ts +0 -6
- package/dist/agent/app-manifest.d.ts +20 -4
- package/dist/agent/app-manifest.d.ts.map +1 -1
- package/dist/agent/app-manifest.js +29 -6
- package/dist/agent/app-manifest.js.map +1 -1
- package/dist/agent/channels/slack-commands.d.ts.map +1 -1
- package/dist/agent/channels/slack-commands.js +9 -6
- package/dist/agent/channels/slack-commands.js.map +1 -1
- package/dist/agent/channels/slack-events.d.ts +28 -6
- package/dist/agent/channels/slack-events.d.ts.map +1 -1
- package/dist/agent/channels/slack-events.js +74 -14
- package/dist/agent/channels/slack-events.js.map +1 -1
- package/dist/agent/channels/slack-front.d.ts +1 -15
- package/dist/agent/channels/slack-front.d.ts.map +1 -1
- package/dist/agent/channels/slack-front.js +4 -62
- 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-task-card.d.ts +38 -0
- package/dist/agent/channels/slack-task-card.d.ts.map +1 -0
- package/dist/agent/channels/slack-task-card.js +156 -0
- package/dist/agent/channels/slack-task-card.js.map +1 -0
- package/dist/agent/channels/slack.d.ts.map +1 -1
- package/dist/agent/channels/slack.js +516 -466
- package/dist/agent/channels/slack.js.map +1 -1
- package/dist/agent/contributions.d.ts +1 -1
- package/dist/agent/contributions.d.ts.map +1 -1
- package/dist/agent/contributions.js +1 -1
- package/dist/agent/contributions.js.map +1 -1
- package/dist/agent/docs-generated.js +1 -1
- package/dist/agent/docs-generated.js.map +1 -1
- package/dist/agent/ops.d.ts +5 -0
- package/dist/agent/ops.d.ts.map +1 -1
- package/dist/agent/ops.js +13 -2
- package/dist/agent/ops.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/eve-credentials.d.ts +9 -2
- package/dist/agent/setup/eve-credentials.d.ts.map +1 -1
- package/dist/agent/setup/eve-credentials.js +9 -1
- package/dist/agent/setup/eve-credentials.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 +71 -15
- 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 +10 -8
- 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.
|
|
16
|
-
patch file is a modification of eve 0.
|
|
17
|
-
Apache License, Version 2.0. The copyright notice of eve 0.
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
18
|
+
npm i @open-everywhere/core @open-everywhere/provider-slack eve@0.71.0
|
|
19
19
|
npx oe-eve-patch
|
|
20
20
|
```
|
|
21
21
|
|
package/agent/app-manifest.ts
CHANGED
|
@@ -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
|
-
/**
|
|
37
|
-
* second install in one workspace needs its own name, or the two apps
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
208
|
-
|
|
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
|
|
213
|
-
return ephemeralResponse(
|
|
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(`"
|
|
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
|
-
*
|
|
3688
|
-
* conversations.members (member ids).
|
|
3689
|
-
*
|
|
3690
|
-
*
|
|
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
|
|
3695
|
-
*
|
|
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)
|
|
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
|
|
3709
|
-
//
|
|
3710
|
-
// partial
|
|
3711
|
-
|
|
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
|
|
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
|
|
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.
|
|
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}`}`;
|