@open-everywhere/provider-slack 0.1.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/LICENSE +201 -0
- package/NOTICE +21 -0
- package/README.md +47 -0
- package/agent/access/slack-attachment-fetch.ts +168 -0
- package/agent/app-manifest.ts +149 -0
- package/agent/channels/slack-approval-card.ts +275 -0
- package/agent/channels/slack-attachments.ts +195 -0
- package/agent/channels/slack-commands.ts +227 -0
- package/agent/channels/slack-context.ts +185 -0
- package/agent/channels/slack-events.ts +5406 -0
- package/agent/channels/slack-front.ts +2349 -0
- package/agent/channels/slack-identity.ts +520 -0
- package/agent/channels/slack-input-response.ts +162 -0
- package/agent/channels/slack-mrkdwn.ts +61 -0
- package/agent/channels/slack-origin-codec.ts +62 -0
- package/agent/channels/slack-post-arm.ts +314 -0
- package/agent/channels/slack-read.ts +248 -0
- package/agent/channels/slack-route-hold.ts +107 -0
- package/agent/channels/slack-shortcut.ts +336 -0
- package/agent/channels/slack-signature.ts +53 -0
- package/agent/channels/slack-spend-gate.ts +62 -0
- package/agent/channels/slack-tenant-fence.ts +99 -0
- package/agent/channels/slack-wake-backscroll.ts +604 -0
- package/agent/channels/slack-web-api.ts +178 -0
- package/agent/channels/slack-webhook-verifier.ts +116 -0
- package/agent/channels/slack.ts +1266 -0
- package/agent/contributions.ts +32 -0
- package/agent/docs-generated.ts +15 -0
- package/agent/ops.ts +50 -0
- package/agent/provider-events.ts +91 -0
- package/agent/register.ts +144 -0
- package/agent/surface-parity.ts +99 -0
- package/dist/agent/access/slack-attachment-fetch.d.ts +15 -0
- package/dist/agent/access/slack-attachment-fetch.d.ts.map +1 -0
- package/dist/agent/access/slack-attachment-fetch.js +151 -0
- package/dist/agent/access/slack-attachment-fetch.js.map +1 -0
- package/dist/agent/app-manifest.d.ts +15 -0
- package/dist/agent/app-manifest.d.ts.map +1 -0
- package/dist/agent/app-manifest.js +141 -0
- package/dist/agent/app-manifest.js.map +1 -0
- package/dist/agent/channels/slack-approval-card.d.ts +71 -0
- package/dist/agent/channels/slack-approval-card.d.ts.map +1 -0
- package/dist/agent/channels/slack-approval-card.js +232 -0
- package/dist/agent/channels/slack-approval-card.js.map +1 -0
- package/dist/agent/channels/slack-attachments.d.ts +66 -0
- package/dist/agent/channels/slack-attachments.d.ts.map +1 -0
- package/dist/agent/channels/slack-attachments.js +174 -0
- package/dist/agent/channels/slack-attachments.js.map +1 -0
- package/dist/agent/channels/slack-commands.d.ts +27 -0
- package/dist/agent/channels/slack-commands.d.ts.map +1 -0
- package/dist/agent/channels/slack-commands.js +208 -0
- package/dist/agent/channels/slack-commands.js.map +1 -0
- package/dist/agent/channels/slack-context.d.ts +26 -0
- package/dist/agent/channels/slack-context.d.ts.map +1 -0
- package/dist/agent/channels/slack-context.js +178 -0
- package/dist/agent/channels/slack-context.js.map +1 -0
- package/dist/agent/channels/slack-events.d.ts +1654 -0
- package/dist/agent/channels/slack-events.d.ts.map +1 -0
- package/dist/agent/channels/slack-events.js +4240 -0
- package/dist/agent/channels/slack-events.js.map +1 -0
- package/dist/agent/channels/slack-front.d.ts +387 -0
- package/dist/agent/channels/slack-front.d.ts.map +1 -0
- package/dist/agent/channels/slack-front.js +1958 -0
- package/dist/agent/channels/slack-front.js.map +1 -0
- package/dist/agent/channels/slack-identity.d.ts +245 -0
- package/dist/agent/channels/slack-identity.d.ts.map +1 -0
- package/dist/agent/channels/slack-identity.js +405 -0
- package/dist/agent/channels/slack-identity.js.map +1 -0
- package/dist/agent/channels/slack-input-response.d.ts +26 -0
- package/dist/agent/channels/slack-input-response.d.ts.map +1 -0
- package/dist/agent/channels/slack-input-response.js +127 -0
- package/dist/agent/channels/slack-input-response.js.map +1 -0
- package/dist/agent/channels/slack-mrkdwn.d.ts +3 -0
- package/dist/agent/channels/slack-mrkdwn.d.ts.map +1 -0
- package/dist/agent/channels/slack-mrkdwn.js +53 -0
- package/dist/agent/channels/slack-mrkdwn.js.map +1 -0
- package/dist/agent/channels/slack-origin-codec.d.ts +7 -0
- package/dist/agent/channels/slack-origin-codec.d.ts.map +1 -0
- package/dist/agent/channels/slack-origin-codec.js +55 -0
- package/dist/agent/channels/slack-origin-codec.js.map +1 -0
- package/dist/agent/channels/slack-post-arm.d.ts +54 -0
- package/dist/agent/channels/slack-post-arm.d.ts.map +1 -0
- package/dist/agent/channels/slack-post-arm.js +268 -0
- package/dist/agent/channels/slack-post-arm.js.map +1 -0
- package/dist/agent/channels/slack-read.d.ts +84 -0
- package/dist/agent/channels/slack-read.d.ts.map +1 -0
- package/dist/agent/channels/slack-read.js +205 -0
- package/dist/agent/channels/slack-read.js.map +1 -0
- package/dist/agent/channels/slack-route-hold.d.ts +34 -0
- package/dist/agent/channels/slack-route-hold.d.ts.map +1 -0
- package/dist/agent/channels/slack-route-hold.js +97 -0
- package/dist/agent/channels/slack-route-hold.js.map +1 -0
- package/dist/agent/channels/slack-shortcut.d.ts +162 -0
- package/dist/agent/channels/slack-shortcut.d.ts.map +1 -0
- package/dist/agent/channels/slack-shortcut.js +262 -0
- package/dist/agent/channels/slack-shortcut.js.map +1 -0
- package/dist/agent/channels/slack-signature.d.ts +19 -0
- package/dist/agent/channels/slack-signature.d.ts.map +1 -0
- package/dist/agent/channels/slack-signature.js +50 -0
- package/dist/agent/channels/slack-signature.js.map +1 -0
- package/dist/agent/channels/slack-spend-gate.d.ts +22 -0
- package/dist/agent/channels/slack-spend-gate.d.ts.map +1 -0
- package/dist/agent/channels/slack-spend-gate.js +48 -0
- package/dist/agent/channels/slack-spend-gate.js.map +1 -0
- package/dist/agent/channels/slack-tenant-fence.d.ts +61 -0
- package/dist/agent/channels/slack-tenant-fence.d.ts.map +1 -0
- package/dist/agent/channels/slack-tenant-fence.js +88 -0
- package/dist/agent/channels/slack-tenant-fence.js.map +1 -0
- package/dist/agent/channels/slack-wake-backscroll.d.ts +188 -0
- package/dist/agent/channels/slack-wake-backscroll.d.ts.map +1 -0
- package/dist/agent/channels/slack-wake-backscroll.js +473 -0
- package/dist/agent/channels/slack-wake-backscroll.js.map +1 -0
- package/dist/agent/channels/slack-web-api.d.ts +46 -0
- package/dist/agent/channels/slack-web-api.d.ts.map +1 -0
- package/dist/agent/channels/slack-web-api.js +147 -0
- package/dist/agent/channels/slack-web-api.js.map +1 -0
- package/dist/agent/channels/slack-webhook-verifier.d.ts +5 -0
- package/dist/agent/channels/slack-webhook-verifier.d.ts.map +1 -0
- package/dist/agent/channels/slack-webhook-verifier.js +112 -0
- package/dist/agent/channels/slack-webhook-verifier.js.map +1 -0
- package/dist/agent/channels/slack.d.ts +37 -0
- package/dist/agent/channels/slack.d.ts.map +1 -0
- package/dist/agent/channels/slack.js +1169 -0
- package/dist/agent/channels/slack.js.map +1 -0
- package/dist/agent/contributions.d.ts +17 -0
- package/dist/agent/contributions.d.ts.map +1 -0
- package/dist/agent/contributions.js +32 -0
- package/dist/agent/contributions.js.map +1 -0
- package/dist/agent/docs-generated.d.ts +3 -0
- package/dist/agent/docs-generated.d.ts.map +1 -0
- package/dist/agent/docs-generated.js +13 -0
- package/dist/agent/docs-generated.js.map +1 -0
- package/dist/agent/ops.d.ts +3 -0
- package/dist/agent/ops.d.ts.map +1 -0
- package/dist/agent/ops.js +38 -0
- package/dist/agent/ops.js.map +1 -0
- package/dist/agent/provider-events.d.ts +3 -0
- package/dist/agent/provider-events.d.ts.map +1 -0
- package/dist/agent/provider-events.js +84 -0
- package/dist/agent/provider-events.js.map +1 -0
- package/dist/agent/register.d.ts +5 -0
- package/dist/agent/register.d.ts.map +1 -0
- package/dist/agent/register.js +118 -0
- package/dist/agent/register.js.map +1 -0
- package/dist/agent/surface-parity.d.ts +3 -0
- package/dist/agent/surface-parity.d.ts.map +1 -0
- package/dist/agent/surface-parity.js +95 -0
- package/dist/agent/surface-parity.js.map +1 -0
- package/package.json +209 -0
- package/tsconfig.build.json +22 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// Component 3 of the Slack engagement model (slack-engagement-spec.md): domain-scoped
|
|
2
|
+
// channel reads. "If a domain has 3 channels, the agent knows all 3 and can read them —
|
|
3
|
+
// and NOTHING else." The Slack bot token is workspace-wide (it can read any channel the
|
|
4
|
+
// bot is in), so the boundary is OURS to enforce at the tool layer — the same shape as the
|
|
5
|
+
// connector hard-gate (access/connector-scope.ts): broad credential underneath, narrow
|
|
6
|
+
// allowlist on top, FAIL CLOSED. The domain's channel list IS the allowlist.
|
|
7
|
+
//
|
|
8
|
+
// This module is the shared spine for the Slack read path (channels/channel-read.ts's SLACK
|
|
9
|
+
// adapter, behind the generic read verbs): resolve the allowed channel ids for the session, assert a
|
|
10
|
+
// requested channel is in them (or throw), and make the actual Slack Web API call. The call
|
|
11
|
+
// goes through OE's `callSlackWebApi` (channels/slack-web-api.ts) — the ONE Slack Web API seam,
|
|
12
|
+
// which carries the 429/`Retry-After` backoff — form-encoded with the same `SLACK_BOT_TOKEN` env
|
|
13
|
+
// fallback the Slack channel itself uses.
|
|
14
|
+
import { callSlackWebApi } from "./slack-web-api.js";
|
|
15
|
+
import { auditChannelReadRefusal, bindFrom, getConfig, domainForChannel, resolvePartition, refutedReadAuthorIds, ChannelScopeError, markNotApplied, domainRoster, } from "@open-everywhere/core/agent/adapter-api.js";
|
|
16
|
+
// The SAME per-author membership probe the wake leg uses (OPE-474) — see the adapter's own note on
|
|
17
|
+
// why this is reused rather than re-derived. The direction is safe: slack-events.ts does not import
|
|
18
|
+
// this module, so nothing here closes a cycle.
|
|
19
|
+
import { slackAuthorIsMember } from "./slack-events.js";
|
|
20
|
+
// ChannelScopeError now LIVES in core (channels/channel-read.ts) — it predates the generic read
|
|
21
|
+
// verbs and the platform-parameterised assertSurfaceAllowed throws the same class, so the ONE
|
|
22
|
+
// class identity must sit on the core side of the boundary. Re-exported here so the bundle's own
|
|
23
|
+
// callers/tests keep their import site.
|
|
24
|
+
export { ChannelScopeError };
|
|
25
|
+
// A fully-qualified surface key is `platform/workspace/surface`; for Slack the surface
|
|
26
|
+
// segment IS the channel id. Returns null for non-Slack keys (defensive — domains can span
|
|
27
|
+
// platforms in principle, and we only gate Slack here).
|
|
28
|
+
function channelIdFromKey(key) {
|
|
29
|
+
const parts = key.split("/");
|
|
30
|
+
if (parts.length !== 3 || parts[0] !== "slack")
|
|
31
|
+
return null;
|
|
32
|
+
return parts[2] || null;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The Slack channel ids this session may read: every Slack channel in its explicit domain,
|
|
36
|
+
* PLUS its own current channel (always readable — it's where the agent was engaged). A DM,
|
|
37
|
+
* an unassigned channel, or a criteria pseudo-domain (org/public/dm — no channel list)
|
|
38
|
+
* yields just the current channel. Empty only when the surface isn't a Slack channel.
|
|
39
|
+
*/
|
|
40
|
+
export function allowedChannelIds(ctx) {
|
|
41
|
+
const p = resolvePartition(ctx);
|
|
42
|
+
const ids = new Set();
|
|
43
|
+
const own = channelIdFromKey(p.surfaceKey);
|
|
44
|
+
if (own)
|
|
45
|
+
ids.add(own); // the agent can always read its own channel
|
|
46
|
+
const domain = domainForChannel(p.surfaceKey, getConfig().loadDomains());
|
|
47
|
+
if (domain) {
|
|
48
|
+
for (const key of domainRoster(domain)) {
|
|
49
|
+
const id = channelIdFromKey(key);
|
|
50
|
+
if (id)
|
|
51
|
+
ids.add(id);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { ids, domain: domain?.name };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Assert a requested channel id is readable by this session — else AUDIT the refusal and throw
|
|
58
|
+
* (fail closed). The error names the domain so the model gets an actionable message, not a bare
|
|
59
|
+
* denial. The decision is unchanged from the silent version; only the row is new
|
|
60
|
+
* (core channels/read-scope-audit.ts).
|
|
61
|
+
*/
|
|
62
|
+
export async function assertChannelAllowed(ctx, channelId, deps) {
|
|
63
|
+
const { ids, domain } = allowedChannelIds(ctx);
|
|
64
|
+
if (ids.has(channelId))
|
|
65
|
+
return;
|
|
66
|
+
const p = resolvePartition(ctx);
|
|
67
|
+
await auditChannelReadRefusal(ctx, { platform: "slack", surfaceId: channelId, callerKey: p.surfaceKey, callerDomain: domain, domains: getConfig().loadDomains() }, deps);
|
|
68
|
+
const scope = domain ? `domain "${domain}"` : "this channel's (no multi-channel domain)";
|
|
69
|
+
throw new ChannelScopeError(`channel "${channelId}" is not in ${scope} read scope. The agent may only read channels in its ` +
|
|
70
|
+
`memory domain (memory/config/domains.ts). Add the channel to the domain's \`channels\` to grant it.`);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Low-level POST to a (read-only) Slack Web API method with the bot token, via OE's
|
|
74
|
+
* `callSlackWebApi` (form-encoded — Slack's JSON support is partial: `conversations.replies`/
|
|
75
|
+
* `history` want form). Throws on a missing token or a non-ok Slack response so the tool
|
|
76
|
+
* surfaces a real error. The explicit env pre-check keeps the actionable "not set" message
|
|
77
|
+
* (the seam itself throws a terser "SLACK_BOT_TOKEN is required.").
|
|
78
|
+
*/
|
|
79
|
+
export async function callSlackRead(operation, params) {
|
|
80
|
+
if (!process.env.SLACK_BOT_TOKEN) {
|
|
81
|
+
throw new Error("SLACK_BOT_TOKEN is not set — the Slack read tools need the bot token to call the Web API.");
|
|
82
|
+
}
|
|
83
|
+
// botToken omitted → the seam resolves it from SLACK_BOT_TOKEN (same fallback as before).
|
|
84
|
+
const json = (await callSlackWebApi({ botToken: undefined, operation, body: params }));
|
|
85
|
+
// DEFINITIVE (OPE-821): Slack answers HTTP 200 with `ok:false` when it READ the request and
|
|
86
|
+
// refused it, so the call had no effect. `react` is the caller that acts on this — a refused
|
|
87
|
+
// `reactions.add` proves no reaction landed. A throw from the seam (transport, or a non-2xx)
|
|
88
|
+
// never reaches this line and stays AMBIGUOUS, which is the honest answer for a call that got no
|
|
89
|
+
// reply. Marking it here rather than in `react` keeps ONE statement of what Slack's `ok:false`
|
|
90
|
+
// means, for every method that goes through this transport.
|
|
91
|
+
if (!json.ok)
|
|
92
|
+
throw markNotApplied(new Error(`Slack ${operation} failed: ${json.error ?? "unknown error"}`));
|
|
93
|
+
return json;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Normalize a raw Slack messages array (history/replies) to rendered lines, oldest-first.
|
|
97
|
+
*
|
|
98
|
+
* `author` IS UNCHANGED and stays the display string the model reads (`user`, else `bot_id`, else
|
|
99
|
+
* `(unknown)`) — every render golden depends on it. OPE-474 adds the two GRADING fields beside it
|
|
100
|
+
* rather than reinterpreting it: `author` is chosen for legibility and cannot be parsed back into an
|
|
101
|
+
* identity, which is exactly why the read leg had nothing to probe before.
|
|
102
|
+
*
|
|
103
|
+
* `bot_id` WINS over `user` for the machine classification, even on the messages that carry both (an
|
|
104
|
+
* app posting as a bot user). That direction is fail-OPEN — a machine is dropped from the probe and
|
|
105
|
+
* marks nothing — which is the posture the whole author leg takes.
|
|
106
|
+
*/
|
|
107
|
+
export function renderMessages(raw, opts = {}) {
|
|
108
|
+
if (!Array.isArray(raw))
|
|
109
|
+
return [];
|
|
110
|
+
const out = [];
|
|
111
|
+
for (const m of raw) {
|
|
112
|
+
if (typeof m.subtype === "string" && m.subtype.length > 0)
|
|
113
|
+
continue; // skip joins/edits/system
|
|
114
|
+
const text = typeof m.text === "string" ? m.text : "";
|
|
115
|
+
if (!text)
|
|
116
|
+
continue;
|
|
117
|
+
const user = typeof m.user === "string" && m.user.length > 0 ? m.user : undefined;
|
|
118
|
+
const botId = typeof m.bot_id === "string" && m.bot_id.length > 0 ? m.bot_id : undefined;
|
|
119
|
+
out.push({
|
|
120
|
+
author: m.user ?? m.bot_id ?? "(unknown)",
|
|
121
|
+
ts: m.ts ?? "",
|
|
122
|
+
text,
|
|
123
|
+
...(botId !== undefined ? { bot: true } : user !== undefined ? { authorId: user } : {}),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
// conversations.history returns newest-first; replies returns oldest-first.
|
|
127
|
+
if (opts.oldestFirst === false)
|
|
128
|
+
out.reverse();
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
// --- the SLACK read adapter (moved from core channels/channel-read.ts in OPE-114 stage 4) ---
|
|
132
|
+
// Registered by registerSlackProvider() via registerReadAdapter — the generic read verbs
|
|
133
|
+
// (read_channel / read_thread) resolve it by platform string; behaviour is byte-identical to the
|
|
134
|
+
// in-core adapter (the EXACT calls the retired per-surface Slack read tools made).
|
|
135
|
+
const SLACK_CHANNEL_DEFAULT_LIMIT = 50;
|
|
136
|
+
const SLACK_THREAD_DEFAULT_LIMIT = 100;
|
|
137
|
+
// --- the OPE-474 non-member author leg ---------------------------------------------------------
|
|
138
|
+
//
|
|
139
|
+
// Slack is the one chat surface that can refute an author BY ITSELF: `users.info` reports
|
|
140
|
+
// `is_restricted` / `is_ultra_restricted` / `is_stranger`, which slack-events.ts's
|
|
141
|
+
// {@link slackAuthorIsMember} already folds into one TTL-cached boolean for the wake leg. The read
|
|
142
|
+
// leg reuses THAT probe verbatim — a second implementation of "is this person a member?" is how a
|
|
143
|
+
// wake and a read come to disagree about the same author.
|
|
144
|
+
//
|
|
145
|
+
// 🔴 THE COST IS BOUNDED BY PEOPLE, NOT BY MESSAGES (Tommy, 2026-08-14: no up-front member list).
|
|
146
|
+
// `refutedReadAuthorIds` probes the page's DISTINCT authors, and the probe is TTL-cached across
|
|
147
|
+
// calls, so a 200-message read of a busy channel is a handful of authors and mostly cache hits. An
|
|
148
|
+
// up-front roster would instead size with the WORKSPACE and go stale between reads.
|
|
149
|
+
//
|
|
150
|
+
// THE THREE-WAY VERDICT IS KEPT THREE-WAY. `slackAuthorIsMember` answers `true` (member), `false`
|
|
151
|
+
// (positively refuted) or `undefined` (lookup unavailable), and only `false` refutes. Collapsing
|
|
152
|
+
// `undefined` into a refusal would mark every read on a deployment whose bot token cannot call
|
|
153
|
+
// `users.info` — an always-on bit, arriving through this door.
|
|
154
|
+
/** The Slack team id + the register's arming key for this session's own workspace. Both come from
|
|
155
|
+
* the BINDING, never from inspecting an author id: the platform and the tenant are surface facts. */
|
|
156
|
+
function slackReadTenant(ctx) {
|
|
157
|
+
const p = resolvePartition(ctx);
|
|
158
|
+
return { teamId: p.workspace, tenantKey: p.workspaceKey };
|
|
159
|
+
}
|
|
160
|
+
/** Slack read via OE's callSlackWebApi — domain-allowlist-gated, rendered oldest-first. */
|
|
161
|
+
export const slackReadAdapter = {
|
|
162
|
+
platform: "slack",
|
|
163
|
+
/**
|
|
164
|
+
* OPE-929 — Slack's own spelling of "which room and which thread is this turn in".
|
|
165
|
+
*
|
|
166
|
+
* The CONTAINER is the channel the post-back target names. The THREAD ROOT is `threadTs`, which
|
|
167
|
+
* eve's Slack adapter falls back to the triggering message's own ts — so a bare channel mention
|
|
168
|
+
* carries one too, and reading "this thread" on such a turn reads the one-message thread the
|
|
169
|
+
* mention started, which is what a person means by it.
|
|
170
|
+
*/
|
|
171
|
+
wakeAddress(ctx) {
|
|
172
|
+
const b = bindFrom(ctx);
|
|
173
|
+
const container = typeof b.target?.["channelId"] === "string" ? b.target["channelId"] : undefined;
|
|
174
|
+
if (!container)
|
|
175
|
+
return undefined;
|
|
176
|
+
return { container, ...(b.threadTs ? { threadRoot: b.threadTs } : {}) };
|
|
177
|
+
},
|
|
178
|
+
async readChannel(ctx, channel, limit, call = callSlackRead) {
|
|
179
|
+
await assertChannelAllowed(ctx, channel);
|
|
180
|
+
const res = await call("conversations.history", { channel, limit: limit ?? SLACK_CHANNEL_DEFAULT_LIMIT, inclusive: true });
|
|
181
|
+
return renderMessages(res.messages, { oldestFirst: false });
|
|
182
|
+
},
|
|
183
|
+
async readThread(ctx, channel, threadTs, limit, call = callSlackRead) {
|
|
184
|
+
await assertChannelAllowed(ctx, channel);
|
|
185
|
+
const res = await call("conversations.replies", { channel, ts: threadTs, limit: limit ?? SLACK_THREAD_DEFAULT_LIMIT });
|
|
186
|
+
return renderMessages(res.messages, { oldestFirst: true });
|
|
187
|
+
},
|
|
188
|
+
/** OPE-474 / OPE-534. Both legs run: Slack's own `users.info` refutation, and the operator's
|
|
189
|
+
* author-trust register (which most Slack deployments leave unarmed, so it costs nothing there).
|
|
190
|
+
* It answers with the refuted AUTHOR IDS, because core reads the one answer twice — the turn mark
|
|
191
|
+
* and the `withhold` body replacement. `probe` is injected ONLY so a test can drive the grading
|
|
192
|
+
* without a live Slack — every live call defaults. */
|
|
193
|
+
async refutedAuthorIds(ctx, lines, probe = slackAuthorIsMember) {
|
|
194
|
+
const { teamId, tenantKey } = slackReadTenant(ctx);
|
|
195
|
+
return await refutedReadAuthorIds(lines, tenantKey, {
|
|
196
|
+
surfaceRefuted: async (authorId) => {
|
|
197
|
+
const member = await probe(authorId, teamId);
|
|
198
|
+
return member === undefined ? undefined : !member; // undefined stays "cannot check"
|
|
199
|
+
},
|
|
200
|
+
// No `memberships` leg: Slack has no group-rung key a message carries, so an armed register
|
|
201
|
+
// matches this surface on principal keys alone (`slack/<team>/<user>`).
|
|
202
|
+
});
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
//# sourceMappingURL=slack-read.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slack-read.js","sourceRoot":"","sources":["../../../agent/channels/slack-read.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run `fn` as ONE delivery, so the writes below it can be tracked.
|
|
3
|
+
*
|
|
4
|
+
* ONE caller: `slack-front.ts`'s public route handler, wrapping its whole body. Do not wrap anything
|
|
5
|
+
* else; the value means "the delivery in flight right now", and a second writer would make that
|
|
6
|
+
* false.
|
|
7
|
+
*/
|
|
8
|
+
export declare function withSlackRouteHold<T>(fn: () => T): T;
|
|
9
|
+
/**
|
|
10
|
+
* Track a best-effort write on the delivery in flight, and hand it back so a caller that can also
|
|
11
|
+
* await it may.
|
|
12
|
+
*
|
|
13
|
+
* `work` MUST already swallow its own failures — every caller here is an audit write whose contract
|
|
14
|
+
* is "never throws, never changes the decision". Nothing is caught for it: swallowing a rejection
|
|
15
|
+
* here would let a genuinely broken writer look held.
|
|
16
|
+
*
|
|
17
|
+
* Outside a delivery (a unit test calling a door directly, a future non-route caller) there is
|
|
18
|
+
* nothing to track and the work comes straight back — so `await holdRouteWork(...)` is correct in
|
|
19
|
+
* both worlds and a caller never has to ask which one it is in.
|
|
20
|
+
*/
|
|
21
|
+
export declare function holdRouteWork<T>(work: Promise<T>): Promise<T>;
|
|
22
|
+
/**
|
|
23
|
+
* The promise to hand the platform in place of `work`: it settles when `work` has settled AND every
|
|
24
|
+
* write {@link holdRouteWork} tracked has settled too.
|
|
25
|
+
*
|
|
26
|
+
* 🔴 CALL IT SYNCHRONOUSLY, INSIDE THE DELIVERY — it reads the delivery's tracked set at call time
|
|
27
|
+
* and closes over it, so a call from outside one silently degrades to "just `work`". `slack-front.ts`
|
|
28
|
+
* calls it from the `waitUntil` it hands down, which is exactly that position.
|
|
29
|
+
*
|
|
30
|
+
* It never rejects: a rejected `work` is the caller's own business (eve already logs its background
|
|
31
|
+
* task failures), and a hold that rejected would turn an observability write into a route error.
|
|
32
|
+
*/
|
|
33
|
+
export declare function routeHoldQuiesced(work: Promise<unknown>): Promise<void>;
|
|
34
|
+
//# sourceMappingURL=slack-route-hold.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slack-route-hold.d.ts","sourceRoot":"","sources":["../../../agent/channels/slack-route-hold.ts"],"names":[],"mappings":"AAwDA;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAEpD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAG7D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAWvE"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// THE ROUTE HOLD (OPE-1648) — "keep this best-effort write alive until it lands".
|
|
2
|
+
//
|
|
3
|
+
// WHAT IT CLOSES. Every Slack door answers its 200 IMMEDIATELY: Slack retries a delivery it does
|
|
4
|
+
// not see acked inside three seconds, and eve's own dispatcher returns the response and then
|
|
5
|
+
// finishes the turn in the route's `waitUntil`. On Vercel the serverless invocation can freeze the
|
|
6
|
+
// moment nothing is holding it, so a `void`-ed audit write races that freeze and the row never
|
|
7
|
+
// lands. Those rows are not decoration: `slack.ts`'s `auditWakeDropped` header states that the
|
|
8
|
+
// webhook function's console output never reaches `vercel logs`, so the row is the ONLY durable
|
|
9
|
+
// signal that a wake was refused.
|
|
10
|
+
//
|
|
11
|
+
// 🔴 A LATE `waitUntil` CALL IS NOT A HOLD, WHICH IS WHY THIS FILE EXISTS AT ALL. eve collects the
|
|
12
|
+
// route's background tasks in an array and, the moment the handler resolves, hands the platform
|
|
13
|
+
// `Promise.allSettled(tasks)` (`internal/nitro/routes/channel-dispatch.js`, `flushBackgroundTasks`).
|
|
14
|
+
// `allSettled` reads that array ONCE, synchronously — so a promise pushed after the handler returned
|
|
15
|
+
// is in a list nobody is awaiting, and calling `waitUntil` from a wake route that runs after the 200
|
|
16
|
+
// buys exactly nothing. Almost every write this module holds is in that position.
|
|
17
|
+
//
|
|
18
|
+
// SO THE HOLD RIDES THE WORK THE ROUTE ALREADY REGISTERED. Every one of these writes happens inside
|
|
19
|
+
// a promise the front route handed to `waitUntil` while it was still running — eve's own turn
|
|
20
|
+
// dispatch on the native forward, a FIFO wake, a name-drop. `slack-front.ts` wraps each of those in
|
|
21
|
+
// {@link routeHoldQuiesced} before passing it on, so the promise the platform waits on settles only
|
|
22
|
+
// after the delivery's tracked writes have. Nothing extra is registered: a delivery that queues no
|
|
23
|
+
// background work (a 401, a dropped sibling event) hands the platform nothing, exactly as before.
|
|
24
|
+
//
|
|
25
|
+
// WHY AsyncLocalStorage AND NOT A PARAMETER. The writers do not hold the route args: the mention
|
|
26
|
+
// door's row is written inside eve's `onAppMention` hook, several frames below a hand-off that
|
|
27
|
+
// discards them, and the engagement doors' rows are written from route functions that take deps.
|
|
28
|
+
// Threading a serverless-runtime concern through every wake route's signature to reach them would
|
|
29
|
+
// put it in the type of each one. The front route stows it once instead, for the extent of ONE
|
|
30
|
+
// delivery. One process serves many Slack deliveries at once, and a module global would mix
|
|
31
|
+
// delivery A's writes into delivery B's hold. Same primitive and same reasoning as
|
|
32
|
+
// slack-identity.ts's forwarded-envelope carrier, which is proven to survive the hand-off into eve's
|
|
33
|
+
// handler (slack-enterprise-rung-test.ts §2).
|
|
34
|
+
//
|
|
35
|
+
// 🔴 IT NEVER CHANGES A DECISION, AND IT NEVER THROWS. A hold is observability plumbing: every
|
|
36
|
+
// tracked write already swallows its own failures, and the quiesced promise settles rather than
|
|
37
|
+
// rejects, so a broken sink can never turn a drop into a turn.
|
|
38
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
39
|
+
/**
|
|
40
|
+
* How many drain rounds one quiesce gets. Each round awaits everything registered so far, so a round
|
|
41
|
+
* only follows work that another round's work started. Ten is far past any real chain (a wake queues
|
|
42
|
+
* one audit write, which queues nothing) and it bounds a writer that re-registers itself forever —
|
|
43
|
+
* which would otherwise hold the invocation open until the platform killed it.
|
|
44
|
+
*/
|
|
45
|
+
const MAX_DRAIN_ROUNDS = 10;
|
|
46
|
+
const routeHold = new AsyncLocalStorage();
|
|
47
|
+
/**
|
|
48
|
+
* Run `fn` as ONE delivery, so the writes below it can be tracked.
|
|
49
|
+
*
|
|
50
|
+
* ONE caller: `slack-front.ts`'s public route handler, wrapping its whole body. Do not wrap anything
|
|
51
|
+
* else; the value means "the delivery in flight right now", and a second writer would make that
|
|
52
|
+
* false.
|
|
53
|
+
*/
|
|
54
|
+
export function withSlackRouteHold(fn) {
|
|
55
|
+
return routeHold.run({ pending: [] }, fn);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Track a best-effort write on the delivery in flight, and hand it back so a caller that can also
|
|
59
|
+
* await it may.
|
|
60
|
+
*
|
|
61
|
+
* `work` MUST already swallow its own failures — every caller here is an audit write whose contract
|
|
62
|
+
* is "never throws, never changes the decision". Nothing is caught for it: swallowing a rejection
|
|
63
|
+
* here would let a genuinely broken writer look held.
|
|
64
|
+
*
|
|
65
|
+
* Outside a delivery (a unit test calling a door directly, a future non-route caller) there is
|
|
66
|
+
* nothing to track and the work comes straight back — so `await holdRouteWork(...)` is correct in
|
|
67
|
+
* both worlds and a caller never has to ask which one it is in.
|
|
68
|
+
*/
|
|
69
|
+
export function holdRouteWork(work) {
|
|
70
|
+
routeHold.getStore()?.pending.push(work);
|
|
71
|
+
return work;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The promise to hand the platform in place of `work`: it settles when `work` has settled AND every
|
|
75
|
+
* write {@link holdRouteWork} tracked has settled too.
|
|
76
|
+
*
|
|
77
|
+
* 🔴 CALL IT SYNCHRONOUSLY, INSIDE THE DELIVERY — it reads the delivery's tracked set at call time
|
|
78
|
+
* and closes over it, so a call from outside one silently degrades to "just `work`". `slack-front.ts`
|
|
79
|
+
* calls it from the `waitUntil` it hands down, which is exactly that position.
|
|
80
|
+
*
|
|
81
|
+
* It never rejects: a rejected `work` is the caller's own business (eve already logs its background
|
|
82
|
+
* task failures), and a hold that rejected would turn an observability write into a route error.
|
|
83
|
+
*/
|
|
84
|
+
export function routeHoldQuiesced(work) {
|
|
85
|
+
const hold = routeHold.getStore();
|
|
86
|
+
return (async () => {
|
|
87
|
+
await Promise.allSettled([work]);
|
|
88
|
+
if (!hold)
|
|
89
|
+
return;
|
|
90
|
+
for (let round = 0; round < MAX_DRAIN_ROUNDS && hold.pending.length > 0; round++) {
|
|
91
|
+
const batch = hold.pending;
|
|
92
|
+
hold.pending = [];
|
|
93
|
+
await Promise.allSettled(batch);
|
|
94
|
+
}
|
|
95
|
+
})();
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=slack-route-hold.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slack-route-hold.js","sourceRoot":"","sources":["../../../agent/channels/slack-route-hold.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,EAAE;AACF,iGAAiG;AACjG,6FAA6F;AAC7F,mGAAmG;AACnG,+FAA+F;AAC/F,+FAA+F;AAC/F,gGAAgG;AAChG,kCAAkC;AAClC,EAAE;AACF,mGAAmG;AACnG,gGAAgG;AAChG,qGAAqG;AACrG,qGAAqG;AACrG,qGAAqG;AACrG,kFAAkF;AAClF,EAAE;AACF,oGAAoG;AACpG,8FAA8F;AAC9F,oGAAoG;AACpG,oGAAoG;AACpG,mGAAmG;AACnG,kGAAkG;AAClG,EAAE;AACF,iGAAiG;AACjG,+FAA+F;AAC/F,iGAAiG;AACjG,kGAAkG;AAClG,+FAA+F;AAC/F,4FAA4F;AAC5F,mFAAmF;AACnF,qGAAqG;AACrG,8CAA8C;AAC9C,EAAE;AACF,+FAA+F;AAC/F,gGAAgG;AAChG,+DAA+D;AAE/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAQrD;;;;;GAKG;AACH,MAAM,gBAAgB,GAAG,EAAE,CAAC;AAE5B,MAAM,SAAS,GAAG,IAAI,iBAAiB,EAAa,CAAC;AAErD;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAI,EAAW;IAC/C,OAAO,SAAS,CAAC,GAAG,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;AAC5C,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,aAAa,CAAI,IAAgB;IAC/C,SAAS,CAAC,QAAQ,EAAE,EAAE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACzC,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAsB;IACtD,MAAM,IAAI,GAAG,SAAS,CAAC,QAAQ,EAAE,CAAC;IAClC,OAAO,CAAC,KAAK,IAAI,EAAE;QACjB,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QACjC,IAAI,CAAC,IAAI;YAAE,OAAO;QAClB,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,gBAAgB,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC;YACjF,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC;YAC3B,IAAI,CAAC,OAAO,GAAG,EAAE,CAAC;YAClB,MAAM,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QAClC,CAAC;IACH,CAAC,CAAC,EAAE,CAAC;AACP,CAAC"}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { type WakeAttachment } from "@open-everywhere/core/agent/adapter-api.js";
|
|
2
|
+
import { type ParsedMessageEvent } from "./slack-events.js";
|
|
3
|
+
/**
|
|
4
|
+
* A decoded Slack shortcut invocation, narrowed to the fields a wake needs.
|
|
5
|
+
*
|
|
6
|
+
* The two shortcut families share every field but the target: a `message` shortcut carries the
|
|
7
|
+
* channel it was invoked in and the message it was invoked on; a `global` shortcut carries
|
|
8
|
+
* neither, because Slack fires it from the composer with no conversation in scope.
|
|
9
|
+
*
|
|
10
|
+
* Field-compatible with eve's own `SlackShortcut` union (`eve/channels/slack`), which is the
|
|
11
|
+
* public contract for the same two payloads. We parse the raw payload ourselves because eve's
|
|
12
|
+
* `parseShortcutPayload` is internal to its interactivity handler and is not exported.
|
|
13
|
+
*/
|
|
14
|
+
export interface ParsedSlackShortcut {
|
|
15
|
+
/** `message` for a `message_action` payload, `global` for a `shortcut` payload. */
|
|
16
|
+
readonly kind: "message" | "global";
|
|
17
|
+
/** The shortcut's registered callback id — the operator-chosen name of this trigger. */
|
|
18
|
+
readonly callbackId: string;
|
|
19
|
+
/** Slack's per-invocation trigger id — unique per click. Used as the wake's idempotency key on
|
|
20
|
+
* the ASSUMPTION that a Slack redelivery of one click carries the same trigger id. UNMEASURED:
|
|
21
|
+
* Slack documents stable-`event_id` retry semantics for the Events API, not for interactivity
|
|
22
|
+
* payloads. If the assumption is wrong the cost is a double wake, which is the same fail-open
|
|
23
|
+
* posture the rest of the dedupe layer already takes. See {@link shortcutWakeKey}. */
|
|
24
|
+
readonly triggerId: string;
|
|
25
|
+
/** The Slack user who invoked the shortcut. The wake's principal. */
|
|
26
|
+
readonly userId: string;
|
|
27
|
+
readonly teamId?: string;
|
|
28
|
+
/**
|
|
29
|
+
* The Enterprise Grid organisation the invocation's workspace sits in (`E…`), when the payload
|
|
30
|
+
* named one (OPE-1559). A shortcut never arrives as an event envelope, so there is no
|
|
31
|
+
* `enterprise_id` envelope field to read; the interactivity route names the Grid on the payload
|
|
32
|
+
* itself, in THREE spellings, and all three are read ({@link parseSlackShortcutPayload}).
|
|
33
|
+
*
|
|
34
|
+
* 🔴 IT IS THE ROOM'S GRID, NEVER THE INVOKER'S. Both spellings sit on the payload's own
|
|
35
|
+
* `enterprise` / `team` object, which describes WHERE the invocation happened. The invoker's
|
|
36
|
+
* `user.enterprise_user.enterprise_id` describes where THEY belong, and reading it would let a
|
|
37
|
+
* Slack Connect stranger's own Grid declaration vouch our room. So the author leg keeps its own
|
|
38
|
+
* source (`users.info`, slack-identity.ts) and this one never touches it.
|
|
39
|
+
*/
|
|
40
|
+
readonly enterpriseId?: string;
|
|
41
|
+
/** Present on a `message` shortcut only — the conversation the message lives in. */
|
|
42
|
+
readonly channelId?: string;
|
|
43
|
+
/** Present on a `message` shortcut only. */
|
|
44
|
+
readonly message?: {
|
|
45
|
+
/** The target message's text, verbatim. UNTRUSTED — it is somebody's Slack message. */
|
|
46
|
+
readonly text: string;
|
|
47
|
+
readonly ts: string;
|
|
48
|
+
/** The thread the target message sits in, when it sits in one. */
|
|
49
|
+
readonly threadTs?: string;
|
|
50
|
+
/** The target message's AUTHOR — never the wake's principal (see the file header). */
|
|
51
|
+
readonly userId?: string;
|
|
52
|
+
/** Image/file REFERENCES off the target message's `files[]`, parsed by the SAME
|
|
53
|
+
* {@link parseSlackFiles} the Events API path uses. A shortcut on a message carrying a
|
|
54
|
+
* screenshot must surface that screenshot, exactly as reading the message in the channel
|
|
55
|
+
* would — otherwise the same message means less through this door than through that one.
|
|
56
|
+
* A REFERENCE ONLY: it reaches a wake, and the bot token, solely through
|
|
57
|
+
* {@link shortcutMessageEvent}'s `admitAttachments` gate. */
|
|
58
|
+
readonly attachments?: readonly WakeAttachment[];
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Decode an already-JSON-parsed interactivity payload into a {@link ParsedSlackShortcut}, or
|
|
63
|
+
* `null` for anything that is not a shortcut. TOTAL AND PURE: it never throws and it writes
|
|
64
|
+
* nothing.
|
|
65
|
+
*
|
|
66
|
+
* 🔴 PURITY IS A SECURITY PROPERTY HERE, NOT A STYLE CHOICE (OPE-1442). This runs inside
|
|
67
|
+
* `classifyEnvelope`, which decides what a request IS — long before the dedupe, the readability
|
|
68
|
+
* gate and the ambient-off drop decide whether it wakes anything. An earlier revision registered
|
|
69
|
+
* the target message's attachments right here, so every click seeded the process-global registry
|
|
70
|
+
* (a 1000-entry LRU) even when the shortcut went on to wake NOTHING: repeated clicks on a domain
|
|
71
|
+
* with the unified window off evicted the records real uploads depend on, and `describe_image`
|
|
72
|
+
* then failed closed on a genuine image. The registry is now seeded by the front door's shortcut
|
|
73
|
+
* branch, after the gates it must sit behind.
|
|
74
|
+
*
|
|
75
|
+
* A payload is REFUSED (null) when it cannot produce a wake:
|
|
76
|
+
* • the outer `type` is neither `message_action` nor `shortcut`;
|
|
77
|
+
* • `callback_id`, `trigger_id` or `user.id` is missing — the first two are the trigger's name
|
|
78
|
+
* and its idempotency key, and the third is the principal, so a wake without any of them
|
|
79
|
+
* would have to be invented rather than derived;
|
|
80
|
+
* • a `message_action` that carries no `channel.id` or no `message.ts` — a message shortcut
|
|
81
|
+
* whose target cannot be addressed is not a message shortcut.
|
|
82
|
+
* Refusing is FAIL-CLOSED: the front route acks Slack and wakes nothing.
|
|
83
|
+
*
|
|
84
|
+
* `message.text` is allowed to be EMPTY and still parses. Slack sends an empty `text` for a message
|
|
85
|
+
* whose content lives entirely in blocks or files, and dropping a click a person made is the worse
|
|
86
|
+
* answer. The wake then renders an empty body — it says nothing ABOUT the emptiness. Files on that
|
|
87
|
+
* message ride through as attachments only where the front door admits them
|
|
88
|
+
* ({@link shortcutMessageEvent}'s `admitAttachments`), so a shortcut on a screenshot in a room the
|
|
89
|
+
* app cannot read wakes with an empty body and no file.
|
|
90
|
+
*/
|
|
91
|
+
export declare function parseSlackShortcutPayload(raw: unknown): ParsedSlackShortcut | null;
|
|
92
|
+
/**
|
|
93
|
+
* The idempotency key for one shortcut invocation.
|
|
94
|
+
*
|
|
95
|
+
* Keyed on `trigger_id`, NOT on the target message. What that buys is certain: a person stays free
|
|
96
|
+
* to invoke the same shortcut on the same message twice on purpose, which keying on `channel:ts`
|
|
97
|
+
* (the shape an Events API wake falls back to) would have swallowed for ten minutes.
|
|
98
|
+
*
|
|
99
|
+
* What it de-dupes rests on an UNMEASURED assumption — that a Slack redelivery of one click carries
|
|
100
|
+
* the same trigger id (see {@link ParsedSlackShortcut.triggerId}). Wrong ⇒ a double wake, never a
|
|
101
|
+
* missed one, which is the direction the whole dedupe layer already fails in.
|
|
102
|
+
*
|
|
103
|
+
* It carries no bot-identity prefix, unlike `slack-front.ts`'s `wakeKey`. A trigger id is minted by
|
|
104
|
+
* Slack per app, so two apps in one workspace cannot collide on one; the `oe-shortcut:` prefix
|
|
105
|
+
* keeps it out of the Events API key space.
|
|
106
|
+
*/
|
|
107
|
+
export declare function shortcutWakeKey(sc: ParsedSlackShortcut): string;
|
|
108
|
+
/**
|
|
109
|
+
* The body a GLOBAL shortcut wakes with. It has no message, so the trigger's own name is the whole
|
|
110
|
+
* content — which is the honest thing to hand the model: somebody pressed a named button.
|
|
111
|
+
*
|
|
112
|
+
* The callback id is operator-authored configuration, not somebody's Slack message, so it is NOT
|
|
113
|
+
* fenced as untrusted content — the same reasoning that leaves a slash-command name unfenced.
|
|
114
|
+
*/
|
|
115
|
+
export declare function globalShortcutText(sc: ParsedSlackShortcut): string;
|
|
116
|
+
/**
|
|
117
|
+
* Map a shortcut onto the shared {@link ParsedMessageEvent} shape, so the front door's existing
|
|
118
|
+
* wake spine (partition → seat → window → dedupe → gates → divert) runs over it unchanged.
|
|
119
|
+
*
|
|
120
|
+
* 🔴 `userId` IS THE INVOKER on both kinds. See the file header: the target message's author never
|
|
121
|
+
* becomes the wake's principal.
|
|
122
|
+
*
|
|
123
|
+
* THE TWO TEXT FIELDS CARRY THE MESSAGE-SHORTCUT SPLIT EXACTLY AS THEY ALREADY MEAN IT. A message
|
|
124
|
+
* shortcut is a FORWARD with no covering note — the invoker typed nothing and pointed at somebody
|
|
125
|
+
* else's message — and {@link ParsedMessageEvent} already draws that line: `text` is "own text
|
|
126
|
+
* plus any quoted message", `authoredText` is "the author's OWN typed text". So `text` carries the
|
|
127
|
+
* target message and `authoredText` is empty. Any rule that asks "what did this person SAY?" then
|
|
128
|
+
* correctly answers "nothing", which is true, and the model still receives the message as the
|
|
129
|
+
* wake's content.
|
|
130
|
+
*
|
|
131
|
+
* `ts` differs by kind, and deliberately:
|
|
132
|
+
* • a MESSAGE shortcut takes the target message's real ts, so the reply anchors under the
|
|
133
|
+
* message the shortcut was invoked on (`routeSupervisorEvent` reads `threadTs ?? ts`);
|
|
134
|
+
* • a GLOBAL shortcut has no message, so it MINTS one from `nowMs` in Slack's own
|
|
135
|
+
* `seconds.microseconds` shape. 🔴 It must be ts-SHAPED, not merely unique: the envelope
|
|
136
|
+
* renders `sent-at` by running `Number.parseFloat` over this field (`slackTsToIso`), so a
|
|
137
|
+
* non-numeric id silently becomes a nonsense date — the trigger id `13345224609.7384…` parses
|
|
138
|
+
* to the year 2392. Minting from the clock makes `sent-at` the truth: the moment the person
|
|
139
|
+
* pressed the button. Uniqueness is not this field's job — the wake's idempotency key is
|
|
140
|
+
* {@link shortcutWakeKey}, which does key on the trigger id.
|
|
141
|
+
*
|
|
142
|
+
* `isAppMention` is false on both. It reports whether the TEXT contains `<@bot>`, and neither body
|
|
143
|
+
* does; the "was the agent addressed?" question is answered by the caller passing `directMention`,
|
|
144
|
+
* which is the same seam the single-window mention divert uses.
|
|
145
|
+
*
|
|
146
|
+
* 🔴 `admitAttachments` GATES THE TARGET MESSAGE'S FILES, AND IT DEFAULTS TO FALSE (OPE-1442).
|
|
147
|
+
* Slack delivers a `message_action` for a room the app is not in, and the payload carries that
|
|
148
|
+
* message's `files[].url_private` — bytes the app's own channel reads refuse. So the files ride
|
|
149
|
+
* through only when the caller has POSITIVELY established that the app can read the room
|
|
150
|
+
* (`slackChannelFactsFor`'s `appCanRead`). The default is closed because the cost of the two
|
|
151
|
+
* mistakes is not symmetric: a withheld file is a wake that says less, an admitted one is the bot
|
|
152
|
+
* token downloading somebody's private upload from a room the agent cannot open.
|
|
153
|
+
*
|
|
154
|
+
* WITHHOLDING DROPS THE REFERENCE TOO, rather than passing an id that resolves to nothing. The
|
|
155
|
+
* envelope renders an attachment as an id and `describe_image` fails closed on an id the registry
|
|
156
|
+
* never saw, so an advertised-but-unresolvable file tells the model an image exists and then
|
|
157
|
+
* cannot show it — worse than not mentioning it.
|
|
158
|
+
*/
|
|
159
|
+
export declare function shortcutMessageEvent(sc: ParsedSlackShortcut, channelId: string, channelType?: string, nowMs?: number, opts?: {
|
|
160
|
+
readonly admitAttachments?: boolean;
|
|
161
|
+
}): ParsedMessageEvent;
|
|
162
|
+
//# sourceMappingURL=slack-shortcut.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slack-shortcut.d.ts","sourceRoot":"","sources":["../../../agent/channels/slack-shortcut.ts"],"names":[],"mappings":"AA6BA,OAAO,EAIL,KAAK,cAAc,EACpB,MAAM,4CAA4C,CAAC;AACpD,OAAO,EAAmB,KAAK,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAG7E;;;;;;;;;;GAUG;AACH,MAAM,WAAW,mBAAmB;IAClC,mFAAmF;IACnF,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,QAAQ,CAAC;IACpC,wFAAwF;IACxF,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B;;;;2FAIuF;IACvF,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,qEAAqE;IACrE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,oFAAoF;IACpF,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,4CAA4C;IAC5C,QAAQ,CAAC,OAAO,CAAC,EAAE;QACjB,uFAAuF;QACvF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;QACpB,kEAAkE;QAClE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAC3B,sFAAsF;QACtF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QACzB;;;;;sEAK8D;QAC9D,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;KAClD,CAAC;CACH;AAUD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,OAAO,GAAG,mBAAmB,GAAG,IAAI,CA6ClF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,mBAAmB,GAAG,MAAM,CAE/D;AAsCD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,mBAAmB,GAAG,MAAM,CAElE;AAiBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,wBAAgB,oBAAoB,CAClC,EAAE,EAAE,mBAAmB,EACvB,SAAS,EAAE,MAAM,EACjB,WAAW,CAAC,EAAE,MAAM,EACpB,KAAK,GAAE,MAAmB,EAC1B,IAAI,GAAE;IAAE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAAO,GACjD,kBAAkB,CAuBpB"}
|