@open-everywhere/provider-slack 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent/access/slack-attachment-fetch.ts +2 -1
- package/agent/app-manifest.ts +18 -2
- package/agent/channels/slack-agent-session.ts +338 -0
- package/agent/channels/slack-attachments.ts +20 -11
- package/agent/channels/slack-commands.ts +172 -12
- package/agent/channels/slack-context.ts +0 -5
- package/agent/channels/slack-events.ts +161 -33
- package/agent/channels/slack-follow-ups.ts +148 -0
- package/agent/channels/slack-front.ts +232 -26
- package/agent/channels/slack-home.ts +9 -3
- package/agent/channels/slack-identity.ts +43 -2
- package/agent/channels/slack-memory-canvas.ts +192 -51
- package/agent/channels/slack-origin-codec.ts +9 -0
- package/agent/channels/slack-post-arm.ts +68 -11
- package/agent/channels/slack-read.ts +27 -2
- package/agent/channels/slack-reply-blocks.ts +180 -0
- package/agent/channels/slack-reply-feedback.ts +75 -0
- package/agent/channels/slack-shortcut.ts +1 -1
- package/agent/channels/slack-voice-clips.ts +82 -0
- package/agent/channels/slack-wake-backscroll.ts +0 -5
- package/agent/channels/slack.ts +74 -13
- package/agent/docs-generated.ts +1299 -2
- package/agent/ops.ts +14 -21
- package/agent/register.ts +13 -0
- package/agent/setup/credentials.ts +17 -0
- package/agent/setup/page.ts +6 -3
- package/agent/surface-parity.ts +2 -0
- package/dist/agent/access/slack-attachment-fetch.d.ts.map +1 -1
- package/dist/agent/access/slack-attachment-fetch.js.map +1 -1
- package/dist/agent/app-manifest.d.ts.map +1 -1
- package/dist/agent/app-manifest.js +18 -2
- package/dist/agent/app-manifest.js.map +1 -1
- package/dist/agent/channels/slack-agent-session.d.ts +85 -0
- package/dist/agent/channels/slack-agent-session.d.ts.map +1 -0
- package/dist/agent/channels/slack-agent-session.js +282 -0
- package/dist/agent/channels/slack-agent-session.js.map +1 -0
- package/dist/agent/channels/slack-attachments.d.ts +11 -0
- package/dist/agent/channels/slack-attachments.d.ts.map +1 -1
- package/dist/agent/channels/slack-attachments.js +21 -14
- package/dist/agent/channels/slack-attachments.js.map +1 -1
- package/dist/agent/channels/slack-commands.d.ts +113 -10
- package/dist/agent/channels/slack-commands.d.ts.map +1 -1
- package/dist/agent/channels/slack-commands.js +146 -13
- package/dist/agent/channels/slack-commands.js.map +1 -1
- package/dist/agent/channels/slack-context.d.ts +1 -2
- package/dist/agent/channels/slack-context.d.ts.map +1 -1
- package/dist/agent/channels/slack-context.js +0 -4
- package/dist/agent/channels/slack-context.js.map +1 -1
- package/dist/agent/channels/slack-events.d.ts +53 -100
- package/dist/agent/channels/slack-events.d.ts.map +1 -1
- package/dist/agent/channels/slack-events.js +142 -26
- package/dist/agent/channels/slack-events.js.map +1 -1
- package/dist/agent/channels/slack-follow-ups.d.ts +44 -0
- package/dist/agent/channels/slack-follow-ups.d.ts.map +1 -0
- package/dist/agent/channels/slack-follow-ups.js +129 -0
- package/dist/agent/channels/slack-follow-ups.js.map +1 -0
- package/dist/agent/channels/slack-front.d.ts +28 -45
- package/dist/agent/channels/slack-front.d.ts.map +1 -1
- package/dist/agent/channels/slack-front.js +207 -22
- package/dist/agent/channels/slack-front.js.map +1 -1
- package/dist/agent/channels/slack-home.d.ts.map +1 -1
- package/dist/agent/channels/slack-home.js +10 -3
- package/dist/agent/channels/slack-home.js.map +1 -1
- package/dist/agent/channels/slack-identity.d.ts +33 -21
- package/dist/agent/channels/slack-identity.d.ts.map +1 -1
- package/dist/agent/channels/slack-identity.js +36 -2
- package/dist/agent/channels/slack-identity.js.map +1 -1
- package/dist/agent/channels/slack-memory-canvas.d.ts +21 -5
- package/dist/agent/channels/slack-memory-canvas.d.ts.map +1 -1
- package/dist/agent/channels/slack-memory-canvas.js +162 -52
- package/dist/agent/channels/slack-memory-canvas.js.map +1 -1
- package/dist/agent/channels/slack-origin-codec.d.ts.map +1 -1
- package/dist/agent/channels/slack-origin-codec.js +10 -0
- package/dist/agent/channels/slack-origin-codec.js.map +1 -1
- package/dist/agent/channels/slack-post-arm.d.ts +21 -1
- package/dist/agent/channels/slack-post-arm.d.ts.map +1 -1
- package/dist/agent/channels/slack-post-arm.js +57 -10
- package/dist/agent/channels/slack-post-arm.js.map +1 -1
- package/dist/agent/channels/slack-read.d.ts +11 -1
- package/dist/agent/channels/slack-read.d.ts.map +1 -1
- package/dist/agent/channels/slack-read.js +27 -3
- package/dist/agent/channels/slack-read.js.map +1 -1
- package/dist/agent/channels/slack-reply-blocks.d.ts +29 -0
- package/dist/agent/channels/slack-reply-blocks.d.ts.map +1 -0
- package/dist/agent/channels/slack-reply-blocks.js +172 -0
- package/dist/agent/channels/slack-reply-blocks.js.map +1 -0
- package/dist/agent/channels/slack-reply-feedback.d.ts +23 -0
- package/dist/agent/channels/slack-reply-feedback.d.ts.map +1 -0
- package/dist/agent/channels/slack-reply-feedback.js +63 -0
- package/dist/agent/channels/slack-reply-feedback.js.map +1 -0
- package/dist/agent/channels/slack-shortcut.d.ts +10 -0
- package/dist/agent/channels/slack-shortcut.d.ts.map +1 -1
- package/dist/agent/channels/slack-shortcut.js +1 -1
- package/dist/agent/channels/slack-shortcut.js.map +1 -1
- package/dist/agent/channels/slack-voice-clips.d.ts +16 -0
- package/dist/agent/channels/slack-voice-clips.d.ts.map +1 -0
- package/dist/agent/channels/slack-voice-clips.js +80 -0
- package/dist/agent/channels/slack-voice-clips.js.map +1 -0
- package/dist/agent/channels/slack-wake-backscroll.d.ts +0 -2
- package/dist/agent/channels/slack-wake-backscroll.d.ts.map +1 -1
- package/dist/agent/channels/slack-wake-backscroll.js +0 -4
- package/dist/agent/channels/slack-wake-backscroll.js.map +1 -1
- package/dist/agent/channels/slack.d.ts +22 -1
- package/dist/agent/channels/slack.d.ts.map +1 -1
- package/dist/agent/channels/slack.js +63 -13
- package/dist/agent/channels/slack.js.map +1 -1
- package/dist/agent/docs-generated.d.ts.map +1 -1
- package/dist/agent/docs-generated.js +1299 -2
- package/dist/agent/docs-generated.js.map +1 -1
- package/dist/agent/ops.d.ts +1 -1
- package/dist/agent/ops.d.ts.map +1 -1
- package/dist/agent/ops.js +15 -21
- package/dist/agent/ops.js.map +1 -1
- package/dist/agent/register.d.ts.map +1 -1
- package/dist/agent/register.js +12 -1
- package/dist/agent/register.js.map +1 -1
- package/dist/agent/setup/credentials.d.ts +12 -0
- package/dist/agent/setup/credentials.d.ts.map +1 -1
- package/dist/agent/setup/credentials.js +17 -0
- package/dist/agent/setup/credentials.js.map +1 -1
- package/dist/agent/setup/page.d.ts.map +1 -1
- package/dist/agent/setup/page.js +6 -3
- 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 +2 -0
- package/dist/agent/surface-parity.js.map +1 -1
- package/package.json +31 -3
|
@@ -42,13 +42,17 @@ import {
|
|
|
42
42
|
oeSlash,
|
|
43
43
|
parseSlackCommandText,
|
|
44
44
|
resolvePartition,
|
|
45
|
+
auditSurfaceModeFrom,
|
|
45
46
|
getOperationalStore,
|
|
46
47
|
admittedTenantsFromConfig,
|
|
47
48
|
unadmittedTenantLog,
|
|
48
49
|
UNADMITTED_TENANT_DROP_REASON,
|
|
49
50
|
type BindableContext,
|
|
51
|
+
type SurfaceBinding,
|
|
50
52
|
} from "@open-everywhere/core/agent/adapter-api.js";
|
|
51
53
|
import { slackTenantAdmission } from "./slack-tenant-fence.js";
|
|
54
|
+
import { slackAuthorIsMember, slackChannelFactsFor } from "./slack-events.js";
|
|
55
|
+
import { slackMode } from "./slack-identity.js";
|
|
52
56
|
|
|
53
57
|
function log(msg: string): void {
|
|
54
58
|
console.log(`[open-everywhere:slack-commands] ${msg}`);
|
|
@@ -64,6 +68,21 @@ function formField(params: URLSearchParams, name: string): string | undefined {
|
|
|
64
68
|
return v && v.length > 0 ? v : undefined;
|
|
65
69
|
}
|
|
66
70
|
|
|
71
|
+
/** What the route learned about a command's trust before it binds. `dmPartnerTrust` is the invoker's own
|
|
72
|
+
* verdict in a 1:1 DM. `room` is a channel's `conversations.info` verdict, present whenever Slack answered
|
|
73
|
+
* it, whoever typed the command. `invokerMember` is present only when `users.info` CONFIRMED the invoker a
|
|
74
|
+
* member (see {@link slashRoomVerdict}). The person's binding stamps `room` only beside `invokerMember`;
|
|
75
|
+
* the room's binding stamps it alone (OPE-2588). */
|
|
76
|
+
export interface SlashTrustFacts {
|
|
77
|
+
readonly dmPartnerTrust?: "internal" | "external";
|
|
78
|
+
readonly room?: { readonly extShared: boolean };
|
|
79
|
+
readonly invokerMember?: true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Which verdict a slash binding resolves: the PERSON's (the room's verdict folded with the invoker
|
|
83
|
+
* check, which every member gate reads) or the ROOM's alone (which the room's budget reads). */
|
|
84
|
+
export type SlashTrustLeg = "person" | "room";
|
|
85
|
+
|
|
67
86
|
/** Build the BindableContext the identity seam (bindFrom/bindSlack) expects from Slack's
|
|
68
87
|
* slash-command form fields. Slack's slash-command payload carries no `channel_type` /
|
|
69
88
|
* `is_private` bit (unlike an app_mention/message event) — `bindSlack`'s existing fallbacks
|
|
@@ -76,16 +95,36 @@ function formField(params: URLSearchParams, name: string): string | undefined {
|
|
|
76
95
|
* reads — so a Grid declared once now answers here the same way it answers at every other door,
|
|
77
96
|
* instead of resolving as though the line were absent.
|
|
78
97
|
*
|
|
79
|
-
* 🔴 STATE ITS REACH EXACTLY
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
|
|
98
|
+
* 🔴 STATE ITS REACH EXACTLY. For a CHANNEL command the rung does NOT change `binding.trust`: the
|
|
99
|
+
* room's own verdict answers there (see below), and `bindFrom` takes that raw verdict ahead of any
|
|
100
|
+
* vouch. The rung reaches `binding.trust` only on a DM-shaped `/oe` (a `D…` channel id), and only
|
|
101
|
+
* when the invoker probe gave no verdict: the mapper then leaves trust undefined and the vouch
|
|
102
|
+
* answers (see below). Spread conditionally, like every other door: a single-workspace install
|
|
103
|
+
* stamps nothing.
|
|
104
|
+
*
|
|
105
|
+
* 🔴 A CHANNEL COMMAND CARRIES THE ROOM'S OWN VERDICT (OPE-2579). Slack's slash payload carries no
|
|
106
|
+
* sharing bit, so before this every channel `/oe` bound `slackTrust(undefined, undefined,
|
|
107
|
+
* "channel")`, which fails closed to `external`. A member in an internal channel was then refused
|
|
108
|
+
* `/oe suggest on` as an outsider. The route now passes {@link slashRoomVerdict}'s answer as
|
|
109
|
+
* `is_ext_shared` + `trust_checked`, the two attributes a channel message stamps from the same
|
|
110
|
+
* TTL-cached `conversations.info`. So a channel command reads the room's `internal` or `external` as a
|
|
111
|
+
* message in that room does. On the `person` leg (the default) the verdict is stamped only for an
|
|
112
|
+
* invoker `users.info` confirms a member. On the `room` leg it is stamped for anybody, because that
|
|
113
|
+
* leg answers the room's budget, not a member gate (OPE-2588, {@link bindSlashCommand}). No verdict
|
|
114
|
+
* stamps nothing, and the command stays fail-closed `external`.
|
|
115
|
+
*
|
|
116
|
+
* 🔴 A DM COMMAND CARRIES THE INVOKER'S OWN VERDICT (OPE-2579). In a 1:1 DM the invoker IS the
|
|
117
|
+
* partner, and the route passes {@link slashInvokerDmTrust}'s answer as `dm_partner_trust`, the
|
|
118
|
+
* attribute the chat DM leg stamps from the same `users.info` probe. Before this, a workspace vouch
|
|
119
|
+
* alone answered a DM command, so a guest or a Connect stranger in their DM with the app read
|
|
120
|
+
* `internal` here while the same person read `external` in a DM turn. A verdict outranks the vouch
|
|
121
|
+
* in both directions, exactly as in a DM turn: a guest reads `external` under a workspace vouch, and
|
|
122
|
+
* a confirmed member reads `internal` with no declaration at all. No verdict leaves the attribute
|
|
123
|
+
* unset and the binding resolves as before. This reaches every `/oe` command typed in a DM, not
|
|
124
|
+
* only `watch`. */
|
|
125
|
+
export function bindableForSlashCommand(params: URLSearchParams, invoker?: SlashTrustFacts, leg: SlashTrustLeg = "person"): BindableContext {
|
|
88
126
|
const enterpriseId = formField(params, "enterprise_id");
|
|
127
|
+
const room = invoker?.room && (leg === "room" || invoker.invokerMember === true) ? invoker.room : undefined;
|
|
89
128
|
return {
|
|
90
129
|
channel: { kind: "slack" },
|
|
91
130
|
session: {
|
|
@@ -95,6 +134,8 @@ export function bindableForSlashCommand(params: URLSearchParams): BindableContex
|
|
|
95
134
|
team_id: formField(params, "team_id") ?? "",
|
|
96
135
|
channel_id: formField(params, "channel_id") ?? "",
|
|
97
136
|
...(enterpriseId ? { enterprise_id: enterpriseId } : {}),
|
|
137
|
+
...(invoker?.dmPartnerTrust ? { dm_partner_trust: invoker.dmPartnerTrust } : {}),
|
|
138
|
+
...(room ? { is_ext_shared: String(room.extShared), trust_checked: "true" } : {}),
|
|
98
139
|
},
|
|
99
140
|
principalId: formField(params, "user_id") ?? "",
|
|
100
141
|
principalType: "slack-user",
|
|
@@ -104,6 +145,120 @@ export function bindableForSlashCommand(params: URLSearchParams): BindableContex
|
|
|
104
145
|
};
|
|
105
146
|
}
|
|
106
147
|
|
|
148
|
+
/** How long each trust read (the invoker probe, the room read) may take before the route answers without
|
|
149
|
+
* it. Slack wants its ack inside three seconds, and `slackForm`'s 429 backoff can wait longer than that. */
|
|
150
|
+
const INVOKER_PROBE_MS = 1_500;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The invoker's own membership verdict for a 1:1 DM command, in `dm_partner_trust`'s vocabulary, or
|
|
154
|
+
* `{}` when there is none to give (OPE-2579). It is the chat DM leg's reading of the same probe
|
|
155
|
+
* (slack-front.ts `liveDmPartnerTrust`): `slackAuthorIsMember` decides, so the two doors cannot
|
|
156
|
+
* drift on what a guest is. A channel or group DM asks nothing here: there the ROOM's verdict is the
|
|
157
|
+
* binding's trust, and {@link slashRoomVerdict} reads it.
|
|
158
|
+
*
|
|
159
|
+
* 🔴 NO VERDICT IS NOT A VERDICT. No user id, no bot token, a refused `users.info`, a transport
|
|
160
|
+
* failure or a probe slower than {@link INVOKER_PROBE_MS} all answer `{}`, which leaves the binding
|
|
161
|
+
* exactly as it resolved before this probe existed. An unanswered probe never invents a guest, and
|
|
162
|
+
* never invents a member.
|
|
163
|
+
*/
|
|
164
|
+
export async function slashInvokerDmTrust(
|
|
165
|
+
params: URLSearchParams,
|
|
166
|
+
probe: (userId: string, teamId: string | undefined) => Promise<boolean | undefined> = slackAuthorIsMember,
|
|
167
|
+
): Promise<{ readonly dmPartnerTrust?: "internal" | "external" }> {
|
|
168
|
+
const userId = formField(params, "user_id");
|
|
169
|
+
if (!userId || slackMode(undefined, formField(params, "channel_id")) !== "dm") return {};
|
|
170
|
+
const member = await withinAckBudget(probe(userId, formField(params, "team_id")));
|
|
171
|
+
return member === undefined ? {} : { dmPartnerTrust: member ? "internal" : "external" };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** The room facts {@link slashRoomVerdict} reads: `slackChannelFactsFor`'s shape, which every channel wake reads. */
|
|
175
|
+
export type SlashRoomFacts = (channelId: string) => Promise<{ readonly channelType?: string; readonly extShared?: boolean; readonly trustChecked?: boolean }>;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* A CHANNEL command's room verdict and invoker check, or `{}` when there is no room verdict to give
|
|
179
|
+
* (OPE-2579). The room read is `slackChannelFactsFor`, the TTL-cached `conversations.info` every
|
|
180
|
+
* channel wake reads, so the command door and the message door cannot disagree about one room.
|
|
181
|
+
*
|
|
182
|
+
* 🔴 THE INVOKER LEG, AND IT FAILS CLOSED. A channel message keeps the room's verdict on the auth and marks
|
|
183
|
+
* a guest author on the side (slack-events.ts `slackWakeAuthorFacts`), because one guest's message must not
|
|
184
|
+
* demote the channel's memory for everybody. A command has no session to protect: its binding exists for
|
|
185
|
+
* one answer, and `/oe suggest on` and `/oe watch more|less` read `binding.trust` as "is this person a
|
|
186
|
+
* member". So the route folds the author leg into the binding's `trust`, the composition Discord and
|
|
187
|
+
* Teams make: `invokerMember` is set only when `users.info` CONFIRMS the invoker a member, and the
|
|
188
|
+
* person's binding stamps the room's verdict only beside it. A guest, a Slack Connect stranger, and a
|
|
189
|
+
* person the probe could not check (no `users:read`, a refusal, a timeout) all leave `trust` fail-closed
|
|
190
|
+
* `external`. The message door fails OPEN on an unanswered probe because it marks a turn. Here `trust`
|
|
191
|
+
* IS the member gate, and a channel has no declaration to fall back to the way a DM command does.
|
|
192
|
+
*
|
|
193
|
+
* 🔴 THE ROOM'S VERDICT IS RETURNED FOR EVERY INVOKER (OPE-2588). `/oe budget` names the room's budget,
|
|
194
|
+
* and a channel's turns are charged under the room's domain, whoever typed them. So {@link bindSlashCommand}
|
|
195
|
+
* binds the room's verdict alone into `roomTrust`, and the budget reads that. A guest's `trust` is
|
|
196
|
+
* unchanged, and `/oe budget` reads both to refuse a guest in an internal channel (commands/core.ts
|
|
197
|
+
* `budgetWithheldFrom`). `/oe cover on` refuses outside a DM before it reads trust.
|
|
198
|
+
*
|
|
199
|
+
* 🔴 ONLY A ROOM IN THE CHANNEL MODE IS STAMPED. A 1:1 DM has its own leg ({@link slashInvokerDmTrust}).
|
|
200
|
+
* A group DM, or a room whose type Slack did not report, stamps nothing, so its binding is unchanged.
|
|
201
|
+
* Stamping a sharing verdict on a group DM the binding still calls a channel would mix two modes' rules.
|
|
202
|
+
*
|
|
203
|
+
* 🔴 NO VERDICT IS NOT A VERDICT. A failed or refused `conversations.info`, a response without the check,
|
|
204
|
+
* or a read slower than {@link INVOKER_PROBE_MS} answers `{}`. The command then binds `external`, as
|
|
205
|
+
* before this read existed. The room read and the invoker probe run in parallel inside one ack budget.
|
|
206
|
+
*/
|
|
207
|
+
export async function slashRoomVerdict(
|
|
208
|
+
params: URLSearchParams,
|
|
209
|
+
roomFacts: SlashRoomFacts = slackChannelFactsFor,
|
|
210
|
+
probe: (userId: string, teamId: string | undefined) => Promise<boolean | undefined> = slackAuthorIsMember,
|
|
211
|
+
): Promise<SlashTrustFacts> {
|
|
212
|
+
const channelId = formField(params, "channel_id");
|
|
213
|
+
if (!channelId || slackMode(undefined, channelId) === "dm") return {};
|
|
214
|
+
const userId = formField(params, "user_id");
|
|
215
|
+
const [facts, member] = await Promise.all([
|
|
216
|
+
withinAckBudget(roomFacts(channelId)),
|
|
217
|
+
userId ? withinAckBudget(probe(userId, formField(params, "team_id"))) : Promise.resolve(undefined),
|
|
218
|
+
]);
|
|
219
|
+
if (!facts || facts.trustChecked !== true || typeof facts.extShared !== "boolean") return {};
|
|
220
|
+
if (facts.channelType !== "channel" && facts.channelType !== "group") return {};
|
|
221
|
+
return { room: { extShared: facts.extShared }, ...(member === true ? { invokerMember: true as const } : {}) };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Both legs, for whichever kind of room the command came from. Never throws. */
|
|
225
|
+
export async function slashTrustFacts(params: URLSearchParams): Promise<SlashTrustFacts> {
|
|
226
|
+
return slackMode(undefined, formField(params, "channel_id")) === "dm" ? slashInvokerDmTrust(params) : slashRoomVerdict(params);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* The slash command's binding, with the room's trust and the person's trust kept apart (OPE-2588).
|
|
231
|
+
*
|
|
232
|
+
* `trust` is the PERSON leg: the member gate `/oe suggest on`, `/oe watch more|less` and `/oe cover on`
|
|
233
|
+
* read. `roomTrust` is the ROOM leg: the same `bindFrom` walk over the room's verdict alone, so the
|
|
234
|
+
* declarations and the fail-closed default apply to it unchanged. `/oe budget` and the caps behind
|
|
235
|
+
* `raise` and `clear` read it through `roomTrustOf`. Before this the binding carried one value, so a
|
|
236
|
+
* guest in an internal channel read the external domain's budget while that channel's turns were
|
|
237
|
+
* charged under the internal domain. Now `/oe budget` refuses that guest, because a guest must not read
|
|
238
|
+
* an internal room's caps and spend (Tommy's ruling on #1431).
|
|
239
|
+
*
|
|
240
|
+
* In a DM both legs bind the same context, so the two values are equal. Every other field comes from
|
|
241
|
+
* the person's binding. The two bindings differ only in the two trust attributes, which move no key.
|
|
242
|
+
*/
|
|
243
|
+
export function bindSlashCommand(params: URLSearchParams, facts: SlashTrustFacts): SurfaceBinding {
|
|
244
|
+
const person = bindFrom(bindableForSlashCommand(params, facts, "person"));
|
|
245
|
+
const room = bindFrom(bindableForSlashCommand(params, facts, "room"));
|
|
246
|
+
return { ...person, roomTrust: room.trust };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** `p`'s answer, or `undefined` when it throws or takes longer than {@link INVOKER_PROBE_MS}. */
|
|
250
|
+
async function withinAckBudget<T>(p: Promise<T>): Promise<T | undefined> {
|
|
251
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
252
|
+
const timeout = new Promise<undefined>((resolve) => {
|
|
253
|
+
timer = setTimeout(() => resolve(undefined), INVOKER_PROBE_MS);
|
|
254
|
+
});
|
|
255
|
+
try {
|
|
256
|
+
return await Promise.race([p.catch(() => undefined), timeout]);
|
|
257
|
+
} finally {
|
|
258
|
+
clearTimeout(timer);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
107
262
|
/**
|
|
108
263
|
* Make a refused slash command OBSERVABLE, the same way `slack.ts`'s `auditWakeDropped` makes a
|
|
109
264
|
* refused wake observable: the webhook function's console output never reaches `vercel logs`, so
|
|
@@ -123,16 +278,18 @@ export function bindableForSlashCommand(params: URLSearchParams): BindableContex
|
|
|
123
278
|
*/
|
|
124
279
|
async function auditCommandDropped(teamId: string | undefined, channelId: string | undefined, reason: string, enterpriseId?: string): Promise<void> {
|
|
125
280
|
try {
|
|
126
|
-
const
|
|
281
|
+
const bindable = {
|
|
127
282
|
channel: { kind: "slack" },
|
|
128
283
|
session: { auth: { current: { attributes: { team_id: teamId ?? "", channel_id: channelId ?? "" }, principalId: "", principalType: "slack-user" } } },
|
|
129
|
-
} as BindableContext
|
|
284
|
+
} as BindableContext;
|
|
285
|
+
const p = resolvePartition(bindable);
|
|
130
286
|
await getOperationalStore().appendAudit({
|
|
131
287
|
ts: new Date().toISOString(),
|
|
132
288
|
org: p.org,
|
|
133
289
|
platform: p.platform,
|
|
134
290
|
workspace: p.workspace,
|
|
135
291
|
surface: p.surface,
|
|
292
|
+
...auditSurfaceModeFrom(bindable),
|
|
136
293
|
action: "surface.wake_dropped",
|
|
137
294
|
detail: { reason, door: "slash-command", channel: channelId ?? null, team: teamId ?? null, enterprise: enterpriseId ?? null },
|
|
138
295
|
});
|
|
@@ -142,6 +299,9 @@ async function auditCommandDropped(teamId: string | undefined, channelId: string
|
|
|
142
299
|
}
|
|
143
300
|
|
|
144
301
|
function ephemeralResponse(text: string): Response {
|
|
302
|
+
// OPE-2490: an empty reply means the command's card posted in this channel and IS the answer. An
|
|
303
|
+
// empty 200 acknowledges the command, and Slack then shows nothing beside the card.
|
|
304
|
+
if (text.length === 0) return new Response(null, { status: 200 });
|
|
145
305
|
return new Response(JSON.stringify({ response_type: "ephemeral", text }), {
|
|
146
306
|
status: 200,
|
|
147
307
|
headers: { "content-type": "application/json" },
|
|
@@ -219,7 +379,7 @@ export default defineChannel({
|
|
|
219
379
|
}
|
|
220
380
|
|
|
221
381
|
const cmd = parseSlackCommandText(formField(params, "text"));
|
|
222
|
-
const binding =
|
|
382
|
+
const binding = bindSlashCommand(params, await slashTrustFacts(params));
|
|
223
383
|
try {
|
|
224
384
|
const reply = await runOeCommand(cmd, binding, { attachSession });
|
|
225
385
|
return ephemeralResponse(reply);
|
|
@@ -38,11 +38,6 @@ import { loadThreadContextMessages } from "eve/channels/slack";
|
|
|
38
38
|
import type { SlackMessage } from "eve/channels/slack";
|
|
39
39
|
import { MAX_MESSAGES, renderContext, type ContextLine } from "@open-everywhere/core/agent/adapter-api.js";
|
|
40
40
|
|
|
41
|
-
// Re-export the shared render + line shape so slack-wake-backscroll.ts (which imports them from
|
|
42
|
-
// here) keeps compiling unchanged after the extraction, and any future Slack consumer has one
|
|
43
|
-
// import site. The mapping (`linesFromMessages`) is defined below — it's Slack-message-shaped.
|
|
44
|
-
export { renderContext, type ContextLine };
|
|
45
|
-
|
|
46
41
|
// When the rendered slice fills MAX_MESSAGES exactly, there's very likely more history above
|
|
47
42
|
// it (both fetchers below request MAX_MESSAGES-worth from Slack, so filling the cap usually
|
|
48
43
|
// means older messages were left out) — tell the model this is a bounded slice, not the whole
|