@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,405 @@
|
|
|
1
|
+
// Pure Slack→OpenEverywhere identity capture AND interpretation. The Slack channel (slack.ts)
|
|
2
|
+
// calls the capture half to stow RAW Slack facts (team/channel/user ids + channel_type +
|
|
3
|
+
// is_private) into the session auth context's `attributes`; the INTERPRETATION of those facts into
|
|
4
|
+
// mode/visibility/trust (slackMode/slackVisibility/slackTrust + the bindSlack mapper) moved here
|
|
5
|
+
// from core identity/binding.ts in OPE-114 stage 4 — still one tested place for the
|
|
6
|
+
// security-critical "is this private?" decision, now on the bundle side of the boundary.
|
|
7
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
8
|
+
import { declaredWorkspaceTrust, rootDmGrainAttrs, surfaceKeySegment, } from "@open-everywhere/core/agent/adapter-api.js";
|
|
9
|
+
/** Build the auth context OpenEverywhere dispatches with — raw facts as string attributes. */
|
|
10
|
+
export function buildSlackAuth(input) {
|
|
11
|
+
const attributes = {
|
|
12
|
+
channel_id: input.channelId,
|
|
13
|
+
user_id: input.userId,
|
|
14
|
+
};
|
|
15
|
+
if (input.teamId)
|
|
16
|
+
attributes.team_id = input.teamId;
|
|
17
|
+
if (input.enterpriseId)
|
|
18
|
+
attributes.enterprise_id = input.enterpriseId;
|
|
19
|
+
if (input.threadTs)
|
|
20
|
+
attributes.thread_ts = input.threadTs;
|
|
21
|
+
if (input.channelType)
|
|
22
|
+
attributes.channel_type = input.channelType;
|
|
23
|
+
if (typeof input.isPrivate === "boolean")
|
|
24
|
+
attributes.is_private = String(input.isPrivate);
|
|
25
|
+
if (typeof input.isExternal === "boolean")
|
|
26
|
+
attributes.is_ext_shared = String(input.isExternal);
|
|
27
|
+
if (typeof input.trustChecked === "boolean")
|
|
28
|
+
attributes.trust_checked = String(input.trustChecked);
|
|
29
|
+
if (input.dmPartnerTrust)
|
|
30
|
+
attributes.dm_partner_trust = input.dmPartnerTrust;
|
|
31
|
+
// Root-DM grain stamp (window-tool-gating §D-DM, widened to group DMs): a 1:1 DM OR an mpim (group
|
|
32
|
+
// DM) is a STANDING seat ⇒ narrowed hands — both are closed multi-party conversations with no
|
|
33
|
+
// channel identity. A DM is unambiguously channelType "im" (the DM path always sets it); the
|
|
34
|
+
// id-prefix "D" is the slackMode fallback for a DM with no type captured. mpim is ONLY matched via
|
|
35
|
+
// the explicit channelType — Slack's legacy "G"-prefixed ids are ambiguous between mpim and a
|
|
36
|
+
// private "group" channel, so there is no safe id-prefix fallback for it (unlike "D"/DM);
|
|
37
|
+
// channelType undefined ⇒ never inferred as mpim (fails closed to unstamped/full, not over-narrowed).
|
|
38
|
+
const isDmFamily = input.channelType === "im" || input.channelType === "mpim" || (input.channelType === undefined && input.channelId.startsWith("D"));
|
|
39
|
+
Object.assign(attributes, rootDmGrainAttrs(isDmFamily));
|
|
40
|
+
return { authenticator: "slack", principalId: input.userId, principalType: "slack-user", attributes };
|
|
41
|
+
}
|
|
42
|
+
const forwardedEnvelope = new AsyncLocalStorage();
|
|
43
|
+
/**
|
|
44
|
+
* Run `fn` with `envelope` readable by anything it awaits. ONE caller — `slack-front.ts`'s
|
|
45
|
+
* `forwardNative`, around the hand-off to eve's native `slackChannel`. Do not wrap anything else:
|
|
46
|
+
* the value means "the envelope of the delivery being forwarded right now", and a second writer
|
|
47
|
+
* would make that false.
|
|
48
|
+
*
|
|
49
|
+
* `undefined` runs `fn` with NO carrier, so an ordinary single-workspace install (and any body the
|
|
50
|
+
* front could not parse) costs one `AsyncLocalStorage.run` and stows nothing.
|
|
51
|
+
*/
|
|
52
|
+
export function withForwardedSlackEnvelope(envelope, fn) {
|
|
53
|
+
return envelope === undefined ? fn() : forwardedEnvelope.run(envelope, fn);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The Enterprise Grid organisation of the message being handled on the native forward, or
|
|
57
|
+
* `undefined` outside a forward, on a non-Grid delivery, or for any message the carrier does not
|
|
58
|
+
* describe. See the block above for why the match is checked rather than assumed.
|
|
59
|
+
*/
|
|
60
|
+
export function forwardedSlackEnterpriseId(message) {
|
|
61
|
+
const carried = forwardedEnvelope.getStore();
|
|
62
|
+
if (!carried)
|
|
63
|
+
return undefined;
|
|
64
|
+
return carried.channelId === message.channelId && carried.ts === message.ts ? carried.enterpriseId : undefined;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The Enterprise Grid organisation a `users.info` user object belongs to (`E…`), or `undefined`.
|
|
68
|
+
* Grid nests it as `enterprise_user: { enterprise_id }`; an ordinary workspace's user object carries
|
|
69
|
+
* no such field, so every level is probed rather than assumed (Slack's raw JSON).
|
|
70
|
+
*/
|
|
71
|
+
function enterpriseIdOfUser(u) {
|
|
72
|
+
const eu = u["enterprise_user"];
|
|
73
|
+
if (!eu || typeof eu !== "object")
|
|
74
|
+
return undefined;
|
|
75
|
+
const id = eu["enterprise_id"];
|
|
76
|
+
return typeof id === "string" && id.length > 0 ? id : undefined;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Does the operator's tracked config vouch this Slack WORKSPACE — the rung walk a person's home
|
|
80
|
+
* workspace goes through when it is NOT the workspace the event arrived in (OPE-1508, Tommy's ruling
|
|
81
|
+
* of 2026-09-07).
|
|
82
|
+
*
|
|
83
|
+
* 🔴 THE PROBLEM IT SOLVES. On Enterprise Grid a person's home `team_id` is their own workspace, not
|
|
84
|
+
* the workspace the message was posted in, so the plain `team_id === expectedTeamId` test read every
|
|
85
|
+
* Grid colleague as a stranger. The fix is NOT to stop comparing: it is to ask the register whether
|
|
86
|
+
* the author's OWN workspace is vouched, and the enterprise rung is what lets one line answer for
|
|
87
|
+
* every workspace in the Grid —
|
|
88
|
+
* `defineSurface({ key: "slack/E0GRID", trust: "internal" })`.
|
|
89
|
+
*
|
|
90
|
+
* 🔴 ABSENCE INFERS NON-MEMBER, which is the ruling's other half. An undeclared sibling workspace is
|
|
91
|
+
* a GUEST here, exactly as a Slack Connect stranger from an unrelated org is. The operator vouches by
|
|
92
|
+
* writing a line in `agent/identity/surfaces.ts`; there is no probe that could infer it, and a Grid
|
|
93
|
+
* an operator has not named is not one this deployment has asserted anything about.
|
|
94
|
+
*
|
|
95
|
+
* PURE and SYNCHRONOUS — the register read is (identity/trust.ts's own contract), so this adds no
|
|
96
|
+
* I/O to a leg that already pays for one `users.info`.
|
|
97
|
+
*/
|
|
98
|
+
export function slackWorkspaceVouched(teamId, enterpriseId) {
|
|
99
|
+
const team = surfaceKeySegment(teamId);
|
|
100
|
+
if (team.length === 0)
|
|
101
|
+
return false; // no workspace named ⇒ nothing to ask about ⇒ not vouched
|
|
102
|
+
const enterprise = surfaceKeySegment(enterpriseId);
|
|
103
|
+
return (declaredWorkspaceTrust(`slack/${team}`, enterprise.length === 0 ? undefined : `slack/${enterprise}`) === "internal");
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* DM-partner trust from a `users.info` response — the authoritative source for "is this person an
|
|
107
|
+
* internal member?". Defensive (Slack's raw JSON) and FAIL-CLOSED: a missing/failed lookup, a guest
|
|
108
|
+
* (restricted / ultra_restricted), a Connect stranger, a deleted account, or a workspace this
|
|
109
|
+
* deployment does not admit all yield `external`. Mirrors channelFactsFromInfo for the DM universe,
|
|
110
|
+
* and — since OPE-466 — it is also the ONE reader behind the per-author membership probe
|
|
111
|
+
* (`slackAuthorIsMember`), so a DM partner and a channel/group-DM author cannot drift apart on what
|
|
112
|
+
* a member is.
|
|
113
|
+
*
|
|
114
|
+
* 🔴 THE WORKSPACE TEST IS A RUNG WALK, NOT AN EQUALITY (OPE-1508). The author's OWN workspace
|
|
115
|
+
* answers first and needs no declaration: `team_id === expectedTeamId` is a member, which is what
|
|
116
|
+
* every single-workspace deployment has always relied on. A DIFFERENT workspace then asks the
|
|
117
|
+
* operator's register through {@link slackWorkspaceVouched}, so an Enterprise Grid colleague whose
|
|
118
|
+
* home `team_id` is a sibling workspace is a member when the operator declared the Grid, and a guest
|
|
119
|
+
* when they did not. Read the ruling in {@link slackWorkspaceVouched}.
|
|
120
|
+
*
|
|
121
|
+
* 🔴 AN ABSENT `expectedTeamId` IS A GUEST QUESTION, NOT A SKIPPED ONE. A caller that knows no
|
|
122
|
+
* workspace gets the register walk alone, so an undeclared author is a guest. The body says which
|
|
123
|
+
* live envelope produces that absence and what admitting it cost.
|
|
124
|
+
*
|
|
125
|
+
* 🔴 A BOT IS JUDGED BY ITS WORKSPACE, WITH NO CARVE-OUT (OPE-1508, the same ruling). `is_bot` used
|
|
126
|
+
* to refute on its own, which marked the deployment's OWN admitted apps as guests — including the
|
|
127
|
+
* e2e driver bot in a group DM. It is gone from the refutation: the workspace's own app carries that
|
|
128
|
+
* workspace's `team_id` and is a member, a foreign app carries another and is a guest unless the
|
|
129
|
+
* operator vouched its workspace. `deleted` still refutes, because a deleted account is not a member
|
|
130
|
+
* of anything.
|
|
131
|
+
*
|
|
132
|
+
* 🔴 WHAT THIS WIDENS — EVERY CONSUMER, NOT THE FIRST ONE. This verdict has four readers, and both
|
|
133
|
+
* halves of the ruling (a bot is not refuted; a declared foreign workspace is admitted) reach all
|
|
134
|
+
* four. Naming one and calling it plain is how a wrong claim survives:
|
|
135
|
+
* 1. the 1:1 DM's SHARED READ LAYER — `internal` selects the org brain, so a DM with the
|
|
136
|
+
* workspace's own app now binds `internal` where it bound `external`;
|
|
137
|
+
* 2. the CHAT AUTHOR TAINT — `slackAuthorIsMember` (slack-events.ts), which stamps
|
|
138
|
+
* `oe_chat_nonmember`, fences the author's words, and is what the member plane refuses on;
|
|
139
|
+
* 3. the APPROVAL CLICKER FLOOR leg D (access/../approvals/clicker-floor.ts), through
|
|
140
|
+
* `slackClickerIsGuest` — who may EXECUTE a member-tier approval card;
|
|
141
|
+
* 4. the HITL ANSWER FLOOR leg B (approvals/input-response-floor.ts), same probe — who may resume
|
|
142
|
+
* a parked turn, which is a spend commitment.
|
|
143
|
+
* So a workspace-installed app's bot user, and a person in a partner workspace the operator declared
|
|
144
|
+
* internal, now clear (3) and (4) where they were refused as guests. That is the ruling applied
|
|
145
|
+
* consistently rather than a side effect — the bot rule was asked and answered at the author leg,
|
|
146
|
+
* and one function is what keeps the four readers from disagreeing — but it is a wider change than
|
|
147
|
+
* the DM layer alone, so it is written here in full.
|
|
148
|
+
*
|
|
149
|
+
* The guest flags are unchanged and still refute on every one of the four. Consumers (3) and (4)
|
|
150
|
+
* describe this same rule in their own headers, corrected under OPE-1558 — they sit behind the merge
|
|
151
|
+
* fence's approvals-plane rule, which is why the correction landed in its own reviewed change rather
|
|
152
|
+
* than in this one.
|
|
153
|
+
*/
|
|
154
|
+
export function partnerFactsFromInfo(info, expectedTeamId) {
|
|
155
|
+
const u = info?.user;
|
|
156
|
+
if (!u || typeof u !== "object")
|
|
157
|
+
return { dmPartnerTrust: "external" }; // lookup failed → fail closed
|
|
158
|
+
const guestOrStranger = Boolean(u["is_restricted"] || u["is_ultra_restricted"] || u["is_stranger"]);
|
|
159
|
+
const deleted = Boolean(u["deleted"]);
|
|
160
|
+
const homeTeamId = typeof u["team_id"] === "string" ? u["team_id"] : undefined;
|
|
161
|
+
// 🔴 NO EXPECTED WORKSPACE IS NOT AN OWN-WORKSPACE ANSWER. This used to read the absence as
|
|
162
|
+
// "member", which admitted EVERY author the guest flags did not refute. The absence is reachable
|
|
163
|
+
// from the callers, not hypothetical: `slack-events.ts` builds its `teamId` as
|
|
164
|
+
// `envelope.team_id ?? e.team` and `slack-input-response.ts` reads the raw `team_id` attribute, so
|
|
165
|
+
// an inbound payload that names no workspace hands `undefined` down here. The shape expected to
|
|
166
|
+
// produce it is a Grid ORG-WIDE install, which carries an `enterprise_id`; that vendor detail is
|
|
167
|
+
// NOT verified here, and nothing below rests on it — the arm is fail-closed either way. With
|
|
168
|
+
// `is_bot` gone from the refutation (the ruling above), a
|
|
169
|
+
// FOREIGN app's bot user carrying no `is_restricted` / `is_stranger` flag then resolved as a
|
|
170
|
+
// MEMBER — no `oe_chat_nonmember` stamp, no untrusted fence, and the same wrong verdict on both
|
|
171
|
+
// approval floors. So absence falls to the register walk below, exactly as a differing workspace
|
|
172
|
+
// does: ABSENCE INFERS NON-MEMBER. A Grid the operator declared still answers `internal` here,
|
|
173
|
+
// because the walk reads the author's OWN home workspace and enterprise off `users.info` and needs
|
|
174
|
+
// nothing from the envelope.
|
|
175
|
+
const ownWorkspace = expectedTeamId !== undefined && expectedTeamId.length > 0 && homeTeamId === expectedTeamId;
|
|
176
|
+
const admittedWorkspace = ownWorkspace || slackWorkspaceVouched(homeTeamId, enterpriseIdOfUser(u));
|
|
177
|
+
const internal = !guestOrStranger && !deleted && admittedWorkspace;
|
|
178
|
+
return { dmPartnerTrust: internal ? "internal" : "external" };
|
|
179
|
+
}
|
|
180
|
+
/** What an app_mention envelope tells us directly (often just channel_type). */
|
|
181
|
+
export function channelFactsFromRaw(raw) {
|
|
182
|
+
const channelType = typeof raw["channel_type"] === "string" ? raw["channel_type"] : undefined;
|
|
183
|
+
return { channelType };
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Facts from a `conversations.info` response — the authoritative source for the
|
|
187
|
+
* public/private bit AND the external-sharing bit that app_mention events don't carry.
|
|
188
|
+
* Defensive: the response is Slack's raw JSON, so everything is probed, never assumed.
|
|
189
|
+
*
|
|
190
|
+
* External sharing is the memory trust signal: a Slack Connect / shared / org-shared
|
|
191
|
+
* channel may contain people outside the org, so its memory is gated off entirely
|
|
192
|
+
* (binding.ts slackTrust). We treat ANY of is_shared / is_ext_shared / is_org_shared /
|
|
193
|
+
* is_pending_ext_shared as external. A successful fetch sets `trustChecked: true` so the
|
|
194
|
+
* binding can distinguish "checked, internal" from "never checked" (which fails closed).
|
|
195
|
+
*/
|
|
196
|
+
export function channelFactsFromInfo(info) {
|
|
197
|
+
const ch = info?.channel;
|
|
198
|
+
if (!ch || typeof ch !== "object")
|
|
199
|
+
return {};
|
|
200
|
+
const isPrivate = typeof ch["is_private"] === "boolean" ? ch["is_private"] : undefined;
|
|
201
|
+
const isExternal = Boolean(ch["is_ext_shared"] || ch["is_org_shared"] || ch["is_shared"] || ch["is_pending_ext_shared"]);
|
|
202
|
+
let channelType;
|
|
203
|
+
if (ch["is_im"])
|
|
204
|
+
channelType = "im";
|
|
205
|
+
else if (ch["is_mpim"])
|
|
206
|
+
channelType = "mpim";
|
|
207
|
+
else if (ch["is_group"] || ch["is_private"])
|
|
208
|
+
channelType = "group";
|
|
209
|
+
else if (ch["is_channel"])
|
|
210
|
+
channelType = "channel";
|
|
211
|
+
// `is_member` is the APP's own membership of the room. Slack sends it for a channel/group and
|
|
212
|
+
// omits it for a DM, so a missing field stays undefined rather than becoming false — the
|
|
213
|
+
// readability rule (slack-events.ts appCanReadConversation) decides what a missing field means
|
|
214
|
+
// per channel type, and this function only reports what Slack said.
|
|
215
|
+
//
|
|
216
|
+
// 🔴 THE PRIVATE-CHANNEL HALF IS MEASURED (OPE-1447, 2026-09-08). A live `conversations.info` on a
|
|
217
|
+
// private fixture channel the caller is in answered `is_member: true` — the field is PRESENT for a
|
|
218
|
+
// private channel, so `appCanReadConversation` returns readable and the shortcut route keeps that
|
|
219
|
+
// room's files. It also answered `is_private: true, is_channel: true, is_group: false`: Slack no
|
|
220
|
+
// longer returns the legacy `is_group` shape for a private channel, which is why the `is_private`
|
|
221
|
+
// arm above is load-bearing rather than belt-and-braces. Measured with a USER token, not the app's
|
|
222
|
+
// bot token, so what stays unproven is narrow: whether a BOT token gets the same field.
|
|
223
|
+
const appInChannel = typeof ch["is_member"] === "boolean" ? ch["is_member"] : undefined;
|
|
224
|
+
// The fetch succeeded (we have a channel object), so trust is authoritatively known.
|
|
225
|
+
return { channelType, isPrivate, isExternal, trustChecked: true, ...(appInChannel === undefined ? {} : { appInChannel }) };
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Can THIS APP read the conversation {@link channelFactsFromInfo} just described? Positive and
|
|
229
|
+
* FAIL-CLOSED: it says yes only on a probe that SUCCEEDED and said the app is in the room.
|
|
230
|
+
*
|
|
231
|
+
* The rule differs by channel type because Slack's answer does:
|
|
232
|
+
* • a `channel` or `group` carries `is_member`, which is exactly "is this app in the room" — the
|
|
233
|
+
* fact `conversations.history` enforces when it refuses a non-member with `not_in_channel`. A
|
|
234
|
+
* public channel the app never joined answers `is_member: false` on a lookup that otherwise
|
|
235
|
+
* SUCCEEDS, so `trustChecked` alone would admit it;
|
|
236
|
+
* • an `im` or `mpim` carries no `is_member`, so this arm rests on an INFERENCE, not on a checked
|
|
237
|
+
* fact: Slack is documented to answer `conversations.info` for a DM only to an app that is in
|
|
238
|
+
* it, and a room the app cannot see answers `channel_not_found` — which reaches here as no
|
|
239
|
+
* channel object at all, hence no `trustChecked`. STILL UNMEASURED (OPE-1447), in the same sense
|
|
240
|
+
* `shortcutWakeKey`'s trigger-id assumption is: no offline tier can drive a live
|
|
241
|
+
* `conversations.info` against a DM the app is not in. A 2026-09-08 attempt got as far as a
|
|
242
|
+
* `channel_not_found` for a non-member caller, which POINTS the right way but proves nothing:
|
|
243
|
+
* neither e2e Slack token holds `im:read`, so the member CONTROL could not be driven, and
|
|
244
|
+
* `channel_not_found` without it may be the no-scope answer rather than the no-membership one.
|
|
245
|
+
* Settling it needs one probe under the app's own BOT token, on a ship soak. If the inference is
|
|
246
|
+
* wrong, this arm admits that DM's files. The channel/group arm below does not depend on it;
|
|
247
|
+
* • anything else — a failed probe, a missing token, an unrecognised channel type — is NO.
|
|
248
|
+
*
|
|
249
|
+
* The Slack shortcut route reads it (OPE-1442): Slack delivers a `message_action` for a room the
|
|
250
|
+
* app is not in, so this is what keeps the target message's FILES from being fetched with the bot
|
|
251
|
+
* token out of a room the agent's own channel reads refuse.
|
|
252
|
+
*/
|
|
253
|
+
export function appCanReadConversation(facts) {
|
|
254
|
+
if (facts.trustChecked !== true)
|
|
255
|
+
return false;
|
|
256
|
+
if (facts.channelType === "im" || facts.channelType === "mpim")
|
|
257
|
+
return true;
|
|
258
|
+
return facts.appInChannel === true;
|
|
259
|
+
}
|
|
260
|
+
/** Slack channel_type / id → session mode (channel | dm | mpim). */
|
|
261
|
+
export function slackMode(channelType, channelId) {
|
|
262
|
+
if (channelType === "im")
|
|
263
|
+
return "dm";
|
|
264
|
+
if (channelType === "mpim")
|
|
265
|
+
return "mpim";
|
|
266
|
+
if (channelType === "channel" || channelType === "group")
|
|
267
|
+
return "channel";
|
|
268
|
+
// No type captured — the `D` id prefix reliably marks a DM; otherwise assume a channel.
|
|
269
|
+
if (channelId && channelId.startsWith("D"))
|
|
270
|
+
return "dm";
|
|
271
|
+
return "channel";
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Slack facts → visibility. FAIL CLOSED: anything we can't positively confirm as
|
|
275
|
+
* a public channel is treated as `private`, so a surface's learnings never leak
|
|
276
|
+
* up to shared memory on an under-specified event. (A correctly-wired channel
|
|
277
|
+
* always sets is_private, so the fail-closed branch is the misconfigured/unknown
|
|
278
|
+
* case, where private is the safe default.)
|
|
279
|
+
*/
|
|
280
|
+
export function slackVisibility(channelType, isPrivate, mode) {
|
|
281
|
+
if (isPrivate === "true")
|
|
282
|
+
return "private";
|
|
283
|
+
if (isPrivate === "false")
|
|
284
|
+
return "public";
|
|
285
|
+
// DMs / group-DMs are never "public" surfaces.
|
|
286
|
+
if (mode === "dm" || mode === "mpim")
|
|
287
|
+
return "private";
|
|
288
|
+
if (channelType === "channel")
|
|
289
|
+
return "public"; // an explicit public channel
|
|
290
|
+
return "private"; // unknown -> fail closed
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Slack sharing facts → trust. FAIL CLOSED for shared memory: a positively
|
|
294
|
+
* externally-shared channel (`is_ext_shared="true"`) is `external`; so is a channel we
|
|
295
|
+
* never managed to check (`trust_checked` not "true") — because on a real Slack channel
|
|
296
|
+
* we always attempt conversations.info, so an absent check means the lookup failed and
|
|
297
|
+
* we can't vouch for the membership.
|
|
298
|
+
*
|
|
299
|
+
* 🔴 A GROUP DM READS THE PLATFORM'S POSITIVE EXTERNAL SIGNAL, EXACTLY AS A CHANNEL DOES
|
|
300
|
+
* (OPE-1486, Tommy's 2026-09-07 ruling). A group DM is a shared room with several people in it,
|
|
301
|
+
* so `binding.trust` on an mpim is the DEMOTION half of the member plane's G4b vouch
|
|
302
|
+
* (access/member-plane.ts `roomVouchedInternalFrom`): the operator's declaration vouches the room
|
|
303
|
+
* at any rung, and the platform's `is_ext_shared` is what forbids that declaration re-promoting a
|
|
304
|
+
* Slack Connect group DM. Returning a blanket `internal` here made that demotion unreachable, so a
|
|
305
|
+
* cross-org group DM was vouched under a workspace-wide `internal` line.
|
|
306
|
+
*
|
|
307
|
+
* 🔴 THE mpim DEMOTION IS POSITIVE-ONLY — the channel's `trust_checked` fail-close does NOT apply
|
|
308
|
+
* to it, and that asymmetry is the ruling rather than an oversight. A group DM has no shared-memory
|
|
309
|
+
* silo to protect (it is DM-family), and its `conversations.info` needs the separate `mpim:read`
|
|
310
|
+
* scope, so failing an unchecked mpim closed would demote every group DM in a workspace that never
|
|
311
|
+
* granted that scope — a deployment-wide regression bought for no signal. What demotes an mpim is
|
|
312
|
+
* evidence, never the absence of it. 🔴 THE LIMIT THAT BUYS: a workspace WITHOUT `mpim:read` resolves
|
|
313
|
+
* neither fact, so a Slack Connect group DM there is still vouched under a coarse declaration. The
|
|
314
|
+
* runbook (docs/deploy/surfaces/slack.md) states that precondition to the operator.
|
|
315
|
+
*
|
|
316
|
+
* `binding.trust` on an mpim has READERS BEYOND G4b, and every one of them TIGHTENS on this
|
|
317
|
+
* demotion: the role resolve falls to the `public` domain, domain skills drop, and the
|
|
318
|
+
* access-request, agent-create and identity-link paths refuse. None widens, and memory is untouched
|
|
319
|
+
* — a group DM reads no shared silo at any trust (memory/resolve.ts).
|
|
320
|
+
*
|
|
321
|
+
* A 1:1 DM never reaches the sharing question at all: `bindSlack` binds a `dm` from the PARTNER's
|
|
322
|
+
* verified trust, so `mode === "dm"` here answers the personal silo's `internal` unchanged.
|
|
323
|
+
*/
|
|
324
|
+
export function slackTrust(isExternal, trustChecked, mode) {
|
|
325
|
+
if (mode === "dm")
|
|
326
|
+
return "internal"; // personal silo; the partner's trust is bindSlack's own read
|
|
327
|
+
if (isExternal === "true")
|
|
328
|
+
return "external"; // Slack Connect / shared / org-shared — a group DM too
|
|
329
|
+
if (mode === "mpim")
|
|
330
|
+
return "internal"; // DM-family: demoted by evidence only, never by its absence
|
|
331
|
+
if (trustChecked !== "true")
|
|
332
|
+
return "external"; // never confirmed → fail closed
|
|
333
|
+
return "internal";
|
|
334
|
+
}
|
|
335
|
+
function slackAuthOf(ctx) {
|
|
336
|
+
return ctx.session?.auth?.current ?? ctx.session?.auth?.initiator ?? null;
|
|
337
|
+
}
|
|
338
|
+
function slackAttr(a, keys) {
|
|
339
|
+
if (!a)
|
|
340
|
+
return undefined;
|
|
341
|
+
for (const k of keys) {
|
|
342
|
+
const v = a[k];
|
|
343
|
+
if (typeof v === "string" && v.length > 0)
|
|
344
|
+
return v;
|
|
345
|
+
if (Array.isArray(v) && typeof v[0] === "string" && v[0].length > 0)
|
|
346
|
+
return v[0];
|
|
347
|
+
}
|
|
348
|
+
return undefined;
|
|
349
|
+
}
|
|
350
|
+
// Auth principalId, treating empty string as absent so it falls through to the user_id
|
|
351
|
+
// attribute rather than binding a blank id.
|
|
352
|
+
function slackAuthPrincipalId(ctx) {
|
|
353
|
+
const pid = slackAuthOf(ctx)?.principalId;
|
|
354
|
+
return pid && pid.length > 0 ? pid : undefined;
|
|
355
|
+
}
|
|
356
|
+
// Eve's own auth identities (e.g. the approval-resume step's ctx, rebuilt from the
|
|
357
|
+
// approval click rather than our wake stamp) carry the composite "slack:<team>:<user>"
|
|
358
|
+
// AuthKey form, where buildSlackAuth stamps the RAW user id. Every scopeKey (grants,
|
|
359
|
+
// partition, memory) is keyed on the raw id, so a composite resolving here splits the
|
|
360
|
+
// caller's identity across the approval boundary (live 2026-07-21: the post-approval
|
|
361
|
+
// connector gate refused "ungranted" — scopeKey dm:slack:T…:U… vs the granted dm:U…).
|
|
362
|
+
// Raw Slack ids never contain ":", so collapsing to the last segment is loss-free.
|
|
363
|
+
function rawSlackPrincipal(pid) {
|
|
364
|
+
if (!pid || !pid.startsWith("slack:"))
|
|
365
|
+
return pid;
|
|
366
|
+
const last = pid.slice(pid.lastIndexOf(":") + 1);
|
|
367
|
+
return last.length > 0 ? last : undefined;
|
|
368
|
+
}
|
|
369
|
+
export const bindSlack = (ctx) => {
|
|
370
|
+
const a = slackAuthOf(ctx)?.attributes;
|
|
371
|
+
const channelId = slackAttr(a, ["channel_id", "channelId"]);
|
|
372
|
+
const channelType = slackAttr(a, ["channel_type", "channelType"]);
|
|
373
|
+
const isPrivate = slackAttr(a, ["is_private", "isPrivate"]);
|
|
374
|
+
const mode = slackMode(channelType, channelId);
|
|
375
|
+
// For a DM, trust = the verified PARTNER trust (users.info, stowed by slack.ts). Absent →
|
|
376
|
+
// undefined → bindFrom fails the DM closed. For a channel, the conversations.info verdict.
|
|
377
|
+
const dmRaw = slackAttr(a, ["dm_partner_trust", "dmPartnerTrust"]);
|
|
378
|
+
const dmPartnerTrust = dmRaw === "internal" || dmRaw === "external" ? dmRaw : undefined;
|
|
379
|
+
return {
|
|
380
|
+
workspace: slackAttr(a, ["team_id", "teamId"]),
|
|
381
|
+
// The ENTERPRISE rung above the workspace (OPE-1508). Present only on an Enterprise Grid event
|
|
382
|
+
// whose envelope carried `enterprise_id`, so an ordinary install contributes nothing and its
|
|
383
|
+
// trust walk is unchanged. Core spells it and composes `slack/<enterpriseId>`; one declaration
|
|
384
|
+
// at that key vouches every workspace in the Grid, and the workspace's own line still wins.
|
|
385
|
+
trustEnterprise: slackAttr(a, ["enterprise_id", "enterpriseId"]),
|
|
386
|
+
surface: channelId,
|
|
387
|
+
mode,
|
|
388
|
+
visibility: slackVisibility(channelType, isPrivate, mode),
|
|
389
|
+
trust: mode === "dm" ? dmPartnerTrust : slackTrust(slackAttr(a, ["is_ext_shared", "isExtShared"]), slackAttr(a, ["trust_checked", "trustChecked"]), mode),
|
|
390
|
+
trustVerified: mode === "dm" ? dmPartnerTrust !== undefined : undefined,
|
|
391
|
+
principalId: rawSlackPrincipal(slackAuthPrincipalId(ctx)) ?? slackAttr(a, ["user_id", "userId"]),
|
|
392
|
+
// Routine post-back targets the channel (a fresh message), not an old thread.
|
|
393
|
+
target: channelId ? { channelId } : undefined,
|
|
394
|
+
// Eve's adapter sets thread_ts to the triggering message's own ts when it isn't a
|
|
395
|
+
// thread reply, so a channel mention always carries the thread its reply lands in.
|
|
396
|
+
// The unified divert forwards the RAW event auth, which for a top-level mention has
|
|
397
|
+
// no thread_ts — its anchor rides the oe_reply_anchor stamp instead (unified-window.ts
|
|
398
|
+
// OE_REPLY_ANCHOR_ATTR), so approval cards/post_file/notices thread under the mention
|
|
399
|
+
// on the divert path exactly as they do natively (live-found 2026-07-19: J3's card
|
|
400
|
+
// posted to the channel root). The wake ORIGIN stays anchor-free — §S2/ownership
|
|
401
|
+
// still read a top-level wake as top-level.
|
|
402
|
+
threadTs: slackAttr(a, ["thread_ts", "threadTs"]) ?? slackAttr(a, ["oe_reply_anchor", "oeReplyAnchor"]),
|
|
403
|
+
};
|
|
404
|
+
};
|
|
405
|
+
//# sourceMappingURL=slack-identity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slack-identity.js","sourceRoot":"","sources":["../../../agent/channels/slack-identity.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { SlackInputResponseContext, SlackInputResponseResult, SlackInputResponseSubmission } from "eve/channels/slack";
|
|
2
|
+
import { type IngressSpendGateInput, type InputResponseAdmission, type InputResponseScope, type SpendGateDecision } from "@open-everywhere/core/agent/adapter-api.js";
|
|
3
|
+
export interface SlackInputResponseDeps {
|
|
4
|
+
/** The core admission. Injected in tests; the live default is the shared decision. */
|
|
5
|
+
readonly admit?: (scope: InputResponseScope) => Promise<InputResponseAdmission>;
|
|
6
|
+
/** Render the refusal to the answerer alone. Live default is `chat.postEphemeral` in the thread. */
|
|
7
|
+
readonly notify?: (userId: string, text: string) => Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* Positive per-answerer guest verdict for leg B (OPE-589). Live default is `slackClickerIsGuest`
|
|
10
|
+
* (channels/slack-events.ts). `undefined` is ALWAYS inert.
|
|
11
|
+
*/
|
|
12
|
+
readonly guestVerdict?: (userId: string, teamId: string | undefined) => Promise<boolean | undefined>;
|
|
13
|
+
/** The shared spend-admission decision (OPE-588). Injected in tests; the live default is
|
|
14
|
+
* `ingressSpendGate` (spend/ingress-gate.ts) — the SAME function every ordinary ingress door
|
|
15
|
+
* consults. */
|
|
16
|
+
readonly spendGate?: (input: IngressSpendGateInput) => Promise<SpendGateDecision>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Admit or refuse one eve-owned HITL answer submitted through Slack interactivity.
|
|
20
|
+
*
|
|
21
|
+
* Returns eve's own default result (`{ auth: ctx.defaultAuth }`) on an allow, so an admitted answer
|
|
22
|
+
* is byte-identical to today's behaviour. Returns `null` on a refusal, which leaves the request
|
|
23
|
+
* PENDING — eve has no cancel on this seam (see the core module's residual note).
|
|
24
|
+
*/
|
|
25
|
+
export declare function admitSlackInputResponse(ctx: SlackInputResponseContext, submission: SlackInputResponseSubmission, deps?: SlackInputResponseDeps): Promise<SlackInputResponseResult>;
|
|
26
|
+
//# sourceMappingURL=slack-input-response.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slack-input-response.d.ts","sourceRoot":"","sources":["../../../agent/channels/slack-input-response.ts"],"names":[],"mappings":"AAsCA,OAAO,KAAK,EACV,yBAAyB,EACzB,wBAAwB,EACxB,4BAA4B,EAC7B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAML,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACvB,MAAM,4CAA4C,CAAC;AA0BpD,MAAM,WAAW,sBAAsB;IACrC,sFAAsF;IACtF,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAChF,oGAAoG;IACpG,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE;;;OAGG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,KAAK,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;IACrG;;oBAEgB;IAChB,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,qBAAqB,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAAC;CACnF;AAED;;;;;;GAMG;AACH,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,yBAAyB,EAC9B,UAAU,EAAE,4BAA4B,EACxC,IAAI,GAAE,sBAA2B,GAChC,OAAO,CAAC,wBAAwB,CAAC,CAuDnC"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// THE SLACK ARM OF THE HITL-ANSWER ADMISSION (OPE-574).
|
|
2
|
+
//
|
|
3
|
+
// eve's Slack channel accepts an answer to any parked `ask_question` / session-limit prompt from
|
|
4
|
+
// whoever pressed the widget, with no check at all: `defaultOnInputResponse(ctx){return{auth:
|
|
5
|
+
// ctx.defaultAuth}}`. Authoring `onInputResponse` replaces that default with OE code. The decision
|
|
6
|
+
// itself lives in core (approvals/input-response-floor.ts) so the Teams arm reaches the same
|
|
7
|
+
// verdict; this file is the Slack-shaped edge — resolve the partition, hand the facts over, render
|
|
8
|
+
// the refusal.
|
|
9
|
+
//
|
|
10
|
+
// 🔴 FAIL-OPEN ON A THROW, deliberately. eve logs a thrown hook and treats it as REJECTION
|
|
11
|
+
// (`slack/input-response.js` catches and returns `null`), so a transient store or roster blip
|
|
12
|
+
// inside our code would silently make every question unanswerable — the exact outcome the
|
|
13
|
+
// clicker floor's own catch exists to prevent. Anything unexpected therefore returns eve's own
|
|
14
|
+
// default result and says so in the log; only a real verdict refuses.
|
|
15
|
+
//
|
|
16
|
+
// The refusal is EPHEMERAL, not a thread post. The card is still live for someone who may answer,
|
|
17
|
+
// and a public "you may not answer" note in the room adds nothing the answerer does not already
|
|
18
|
+
// need to know. This mirrors the Discord approval refusal (an ephemeral), not the Slack approval
|
|
19
|
+
// refusal (a thread note, which exists there because the CARD is public and its state matters to
|
|
20
|
+
// the room).
|
|
21
|
+
//
|
|
22
|
+
// LEG B HAS TEETH HERE (OPE-589) — the core header's "Slack cannot [answer B]" is now stale. This
|
|
23
|
+
// module reuses `slackClickerIsGuest` (channels/slack-events.ts) — `slackAuthorIsMember`, the SAME
|
|
24
|
+
// `users.info` call OPE-466 already makes to mark a non-member's chat message, inverted to a guest
|
|
25
|
+
// verdict. The clicker floor's own leg D (approvals/flow.ts's `handleApprovalPayload`) reuses the
|
|
26
|
+
// SAME function, so the two planes cannot drift on what "positively a guest" means. An unresolvable
|
|
27
|
+
// probe returns `undefined` and the leg stays INERT, exactly as it did before.
|
|
28
|
+
//
|
|
29
|
+
// 🔴 SPEND GATE (OPE-588), FIRST — ahead of the room/guest admission below. This click RESUMES a
|
|
30
|
+
// parked turn: a decision to spend more, taken through a button, on a scope nobody asked whether it
|
|
31
|
+
// could afford. Every ordinary ingress door consults the ONE shared spend gate before it dispatches
|
|
32
|
+
// a turn (spend/ingress-gate.ts); eve's own interactivity route calls `onInputResponse` and then
|
|
33
|
+
// `.respond()` directly, with no call site of that gate anywhere on the seam — verified against the
|
|
34
|
+
// installed eve/dist. `ingressSpendGate` fails open on a broken check and fails closed (visible
|
|
35
|
+
// refusal, door `"answer"`) on a definite over-cap, the same posture the DM/channel doors already
|
|
36
|
+
// hold. A decline returns eve's own reject-the-answer `null`, so the request stays PENDING — the
|
|
37
|
+
// same outcome a room/guest refusal already leaves.
|
|
38
|
+
import { admitInputResponse, ingressSpendGate, resolvePartition, SLACK_SPEND_BADGES, } from "@open-everywhere/core/agent/adapter-api.js";
|
|
39
|
+
import { slackClickerIsGuest } from "./slack-events.js";
|
|
40
|
+
/**
|
|
41
|
+
* The RAW `team_id` off the interaction's own auth attributes — never `resolvePartition`'s
|
|
42
|
+
* `workspace` field (OPE-589 review fix). `resolvePartition` degrades an unnamed workspace to the
|
|
43
|
+
* `UNNAMED_TENANT` placeholder ("dev"), which is harmless where `workspace` only ever fed a
|
|
44
|
+
* surfaceKey lookup, but the guest-verdict probe below compares it against Slack's REAL `team_id`
|
|
45
|
+
* (`partnerFactsFromInfo`'s `ownWorkspace` check) — a placeholder there does not mean "no team", it
|
|
46
|
+
* means "a team literally named dev", and would falsely fail that comparison and refuse a genuine
|
|
47
|
+
* member. Reading the wire attribute directly keeps "no team known" as `undefined`. The probe then
|
|
48
|
+
* answers that absence from the operator's register alone (OPE-1508): a declared workspace or Grid
|
|
49
|
+
* still clears leg B, and an undeclared one is a GUEST, because absence infers non-member. Only an
|
|
50
|
+
* unreadable `users.info` leaves leg B inert. This is
|
|
51
|
+
* the same shape `flow.ts`'s `handleApprovalPayload` already reads off the raw Slack
|
|
52
|
+
* payload for the SAME probe on the approval-click side. LOCAL COPY of the attribute read (the
|
|
53
|
+
* `slack-identity.ts` header states the precedent): a three-line structural read over the public
|
|
54
|
+
* auth shape, not worth a barrel export.
|
|
55
|
+
*/
|
|
56
|
+
function slackAuthTeamId(auth) {
|
|
57
|
+
const v = auth.attributes?.["team_id"];
|
|
58
|
+
if (typeof v === "string" && v.length > 0)
|
|
59
|
+
return v;
|
|
60
|
+
if (Array.isArray(v) && typeof v[0] === "string" && v[0].length > 0)
|
|
61
|
+
return v[0];
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Admit or refuse one eve-owned HITL answer submitted through Slack interactivity.
|
|
66
|
+
*
|
|
67
|
+
* Returns eve's own default result (`{ auth: ctx.defaultAuth }`) on an allow, so an admitted answer
|
|
68
|
+
* is byte-identical to today's behaviour. Returns `null` on a refusal, which leaves the request
|
|
69
|
+
* PENDING — eve has no cancel on this seam (see the core module's residual note).
|
|
70
|
+
*/
|
|
71
|
+
export async function admitSlackInputResponse(ctx, submission, deps = {}) {
|
|
72
|
+
const allow = { auth: ctx.defaultAuth };
|
|
73
|
+
try {
|
|
74
|
+
const bindable = {
|
|
75
|
+
channel: { kind: "slack" },
|
|
76
|
+
session: { auth: { current: ctx.defaultAuth } },
|
|
77
|
+
};
|
|
78
|
+
const p = resolvePartition(bindable);
|
|
79
|
+
const teamId = slackAuthTeamId(ctx.defaultAuth);
|
|
80
|
+
// SPEND GATE (OPE-588) — FIRST, ahead of the room/guest admission below (see the header note).
|
|
81
|
+
// A decline skips the guest probe entirely (over budget outranks identity) and returns eve's
|
|
82
|
+
// reject-the-answer `null`, leaving the request PENDING exactly like a room/guest refusal.
|
|
83
|
+
const spendGate = deps.spendGate ?? ingressSpendGate;
|
|
84
|
+
const spendVerdict = await spendGate({
|
|
85
|
+
bindable,
|
|
86
|
+
door: "answer",
|
|
87
|
+
principalId: submission.user.id,
|
|
88
|
+
workspaceRef: teamId ?? null,
|
|
89
|
+
badges: SLACK_SPEND_BADGES,
|
|
90
|
+
post: async (text) => {
|
|
91
|
+
await ctx.thread.postEphemeral(submission.user.id, text);
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
if (spendVerdict === "declined")
|
|
95
|
+
return null;
|
|
96
|
+
const guestVerdict = deps.guestVerdict ?? slackClickerIsGuest;
|
|
97
|
+
const answererIsGuest = await guestVerdict(submission.user.id, teamId).catch(() => undefined);
|
|
98
|
+
const verdict = await (deps.admit ?? admitInputResponse)({
|
|
99
|
+
org: p.org,
|
|
100
|
+
platform: p.platform,
|
|
101
|
+
workspace: p.workspace,
|
|
102
|
+
surface: p.surface,
|
|
103
|
+
surfaceKey: p.surfaceKey,
|
|
104
|
+
principalId: submission.user.id,
|
|
105
|
+
responses: submission.inputResponses.length,
|
|
106
|
+
// Leg B (OPE-589): a POSITIVE per-answerer guest verdict from `slackAuthorIsMember`, or
|
|
107
|
+
// omitted when the probe could not answer — `undefined` never refuses.
|
|
108
|
+
...(answererIsGuest === undefined ? {} : { answererIsGuest }),
|
|
109
|
+
});
|
|
110
|
+
if (verdict.ok)
|
|
111
|
+
return allow;
|
|
112
|
+
const notify = deps.notify ??
|
|
113
|
+
(async (userId, text) => {
|
|
114
|
+
await ctx.thread.postEphemeral(userId, text);
|
|
115
|
+
});
|
|
116
|
+
await notify(submission.user.id, verdict.notice).catch((err) => {
|
|
117
|
+
// The refusal still stands — the human just does not see why. One log line, never a throw.
|
|
118
|
+
console.error(`[open-everywhere:slack] HITL refusal notice failed to post: ${String(err)}`);
|
|
119
|
+
});
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
console.error(`[open-everywhere:slack] HITL answer admission failed softly — accepting the answer: ${String(err)}`);
|
|
124
|
+
return allow;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=slack-input-response.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slack-input-response.js","sourceRoot":"","sources":["../../../agent/channels/slack-input-response.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slack-mrkdwn.d.ts","sourceRoot":"","sources":["../../../agent/channels/slack-mrkdwn.ts"],"names":[],"mappings":"AAsBA,uDAAuD;AACvD,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAqC1D"}
|