@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.
Files changed (151) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +21 -0
  3. package/README.md +47 -0
  4. package/agent/access/slack-attachment-fetch.ts +168 -0
  5. package/agent/app-manifest.ts +149 -0
  6. package/agent/channels/slack-approval-card.ts +275 -0
  7. package/agent/channels/slack-attachments.ts +195 -0
  8. package/agent/channels/slack-commands.ts +227 -0
  9. package/agent/channels/slack-context.ts +185 -0
  10. package/agent/channels/slack-events.ts +5406 -0
  11. package/agent/channels/slack-front.ts +2349 -0
  12. package/agent/channels/slack-identity.ts +520 -0
  13. package/agent/channels/slack-input-response.ts +162 -0
  14. package/agent/channels/slack-mrkdwn.ts +61 -0
  15. package/agent/channels/slack-origin-codec.ts +62 -0
  16. package/agent/channels/slack-post-arm.ts +314 -0
  17. package/agent/channels/slack-read.ts +248 -0
  18. package/agent/channels/slack-route-hold.ts +107 -0
  19. package/agent/channels/slack-shortcut.ts +336 -0
  20. package/agent/channels/slack-signature.ts +53 -0
  21. package/agent/channels/slack-spend-gate.ts +62 -0
  22. package/agent/channels/slack-tenant-fence.ts +99 -0
  23. package/agent/channels/slack-wake-backscroll.ts +604 -0
  24. package/agent/channels/slack-web-api.ts +178 -0
  25. package/agent/channels/slack-webhook-verifier.ts +116 -0
  26. package/agent/channels/slack.ts +1266 -0
  27. package/agent/contributions.ts +32 -0
  28. package/agent/docs-generated.ts +15 -0
  29. package/agent/ops.ts +50 -0
  30. package/agent/provider-events.ts +91 -0
  31. package/agent/register.ts +144 -0
  32. package/agent/surface-parity.ts +99 -0
  33. package/dist/agent/access/slack-attachment-fetch.d.ts +15 -0
  34. package/dist/agent/access/slack-attachment-fetch.d.ts.map +1 -0
  35. package/dist/agent/access/slack-attachment-fetch.js +151 -0
  36. package/dist/agent/access/slack-attachment-fetch.js.map +1 -0
  37. package/dist/agent/app-manifest.d.ts +15 -0
  38. package/dist/agent/app-manifest.d.ts.map +1 -0
  39. package/dist/agent/app-manifest.js +141 -0
  40. package/dist/agent/app-manifest.js.map +1 -0
  41. package/dist/agent/channels/slack-approval-card.d.ts +71 -0
  42. package/dist/agent/channels/slack-approval-card.d.ts.map +1 -0
  43. package/dist/agent/channels/slack-approval-card.js +232 -0
  44. package/dist/agent/channels/slack-approval-card.js.map +1 -0
  45. package/dist/agent/channels/slack-attachments.d.ts +66 -0
  46. package/dist/agent/channels/slack-attachments.d.ts.map +1 -0
  47. package/dist/agent/channels/slack-attachments.js +174 -0
  48. package/dist/agent/channels/slack-attachments.js.map +1 -0
  49. package/dist/agent/channels/slack-commands.d.ts +27 -0
  50. package/dist/agent/channels/slack-commands.d.ts.map +1 -0
  51. package/dist/agent/channels/slack-commands.js +208 -0
  52. package/dist/agent/channels/slack-commands.js.map +1 -0
  53. package/dist/agent/channels/slack-context.d.ts +26 -0
  54. package/dist/agent/channels/slack-context.d.ts.map +1 -0
  55. package/dist/agent/channels/slack-context.js +178 -0
  56. package/dist/agent/channels/slack-context.js.map +1 -0
  57. package/dist/agent/channels/slack-events.d.ts +1654 -0
  58. package/dist/agent/channels/slack-events.d.ts.map +1 -0
  59. package/dist/agent/channels/slack-events.js +4240 -0
  60. package/dist/agent/channels/slack-events.js.map +1 -0
  61. package/dist/agent/channels/slack-front.d.ts +387 -0
  62. package/dist/agent/channels/slack-front.d.ts.map +1 -0
  63. package/dist/agent/channels/slack-front.js +1958 -0
  64. package/dist/agent/channels/slack-front.js.map +1 -0
  65. package/dist/agent/channels/slack-identity.d.ts +245 -0
  66. package/dist/agent/channels/slack-identity.d.ts.map +1 -0
  67. package/dist/agent/channels/slack-identity.js +405 -0
  68. package/dist/agent/channels/slack-identity.js.map +1 -0
  69. package/dist/agent/channels/slack-input-response.d.ts +26 -0
  70. package/dist/agent/channels/slack-input-response.d.ts.map +1 -0
  71. package/dist/agent/channels/slack-input-response.js +127 -0
  72. package/dist/agent/channels/slack-input-response.js.map +1 -0
  73. package/dist/agent/channels/slack-mrkdwn.d.ts +3 -0
  74. package/dist/agent/channels/slack-mrkdwn.d.ts.map +1 -0
  75. package/dist/agent/channels/slack-mrkdwn.js +53 -0
  76. package/dist/agent/channels/slack-mrkdwn.js.map +1 -0
  77. package/dist/agent/channels/slack-origin-codec.d.ts +7 -0
  78. package/dist/agent/channels/slack-origin-codec.d.ts.map +1 -0
  79. package/dist/agent/channels/slack-origin-codec.js +55 -0
  80. package/dist/agent/channels/slack-origin-codec.js.map +1 -0
  81. package/dist/agent/channels/slack-post-arm.d.ts +54 -0
  82. package/dist/agent/channels/slack-post-arm.d.ts.map +1 -0
  83. package/dist/agent/channels/slack-post-arm.js +268 -0
  84. package/dist/agent/channels/slack-post-arm.js.map +1 -0
  85. package/dist/agent/channels/slack-read.d.ts +84 -0
  86. package/dist/agent/channels/slack-read.d.ts.map +1 -0
  87. package/dist/agent/channels/slack-read.js +205 -0
  88. package/dist/agent/channels/slack-read.js.map +1 -0
  89. package/dist/agent/channels/slack-route-hold.d.ts +34 -0
  90. package/dist/agent/channels/slack-route-hold.d.ts.map +1 -0
  91. package/dist/agent/channels/slack-route-hold.js +97 -0
  92. package/dist/agent/channels/slack-route-hold.js.map +1 -0
  93. package/dist/agent/channels/slack-shortcut.d.ts +162 -0
  94. package/dist/agent/channels/slack-shortcut.d.ts.map +1 -0
  95. package/dist/agent/channels/slack-shortcut.js +262 -0
  96. package/dist/agent/channels/slack-shortcut.js.map +1 -0
  97. package/dist/agent/channels/slack-signature.d.ts +19 -0
  98. package/dist/agent/channels/slack-signature.d.ts.map +1 -0
  99. package/dist/agent/channels/slack-signature.js +50 -0
  100. package/dist/agent/channels/slack-signature.js.map +1 -0
  101. package/dist/agent/channels/slack-spend-gate.d.ts +22 -0
  102. package/dist/agent/channels/slack-spend-gate.d.ts.map +1 -0
  103. package/dist/agent/channels/slack-spend-gate.js +48 -0
  104. package/dist/agent/channels/slack-spend-gate.js.map +1 -0
  105. package/dist/agent/channels/slack-tenant-fence.d.ts +61 -0
  106. package/dist/agent/channels/slack-tenant-fence.d.ts.map +1 -0
  107. package/dist/agent/channels/slack-tenant-fence.js +88 -0
  108. package/dist/agent/channels/slack-tenant-fence.js.map +1 -0
  109. package/dist/agent/channels/slack-wake-backscroll.d.ts +188 -0
  110. package/dist/agent/channels/slack-wake-backscroll.d.ts.map +1 -0
  111. package/dist/agent/channels/slack-wake-backscroll.js +473 -0
  112. package/dist/agent/channels/slack-wake-backscroll.js.map +1 -0
  113. package/dist/agent/channels/slack-web-api.d.ts +46 -0
  114. package/dist/agent/channels/slack-web-api.d.ts.map +1 -0
  115. package/dist/agent/channels/slack-web-api.js +147 -0
  116. package/dist/agent/channels/slack-web-api.js.map +1 -0
  117. package/dist/agent/channels/slack-webhook-verifier.d.ts +5 -0
  118. package/dist/agent/channels/slack-webhook-verifier.d.ts.map +1 -0
  119. package/dist/agent/channels/slack-webhook-verifier.js +112 -0
  120. package/dist/agent/channels/slack-webhook-verifier.js.map +1 -0
  121. package/dist/agent/channels/slack.d.ts +37 -0
  122. package/dist/agent/channels/slack.d.ts.map +1 -0
  123. package/dist/agent/channels/slack.js +1169 -0
  124. package/dist/agent/channels/slack.js.map +1 -0
  125. package/dist/agent/contributions.d.ts +17 -0
  126. package/dist/agent/contributions.d.ts.map +1 -0
  127. package/dist/agent/contributions.js +32 -0
  128. package/dist/agent/contributions.js.map +1 -0
  129. package/dist/agent/docs-generated.d.ts +3 -0
  130. package/dist/agent/docs-generated.d.ts.map +1 -0
  131. package/dist/agent/docs-generated.js +13 -0
  132. package/dist/agent/docs-generated.js.map +1 -0
  133. package/dist/agent/ops.d.ts +3 -0
  134. package/dist/agent/ops.d.ts.map +1 -0
  135. package/dist/agent/ops.js +38 -0
  136. package/dist/agent/ops.js.map +1 -0
  137. package/dist/agent/provider-events.d.ts +3 -0
  138. package/dist/agent/provider-events.d.ts.map +1 -0
  139. package/dist/agent/provider-events.js +84 -0
  140. package/dist/agent/provider-events.js.map +1 -0
  141. package/dist/agent/register.d.ts +5 -0
  142. package/dist/agent/register.d.ts.map +1 -0
  143. package/dist/agent/register.js +118 -0
  144. package/dist/agent/register.js.map +1 -0
  145. package/dist/agent/surface-parity.d.ts +3 -0
  146. package/dist/agent/surface-parity.d.ts.map +1 -0
  147. package/dist/agent/surface-parity.js +95 -0
  148. package/dist/agent/surface-parity.js.map +1 -0
  149. package/package.json +209 -0
  150. package/tsconfig.build.json +22 -0
  151. package/tsconfig.json +15 -0
@@ -0,0 +1,232 @@
1
+ // THE SLACK RETURN LEG FOR A BRIDGED APPROVAL CARD (OPE-1720) — what makes the button the unified
2
+ // post arm now renders actually settle the approval it names.
3
+ //
4
+ // WHY THIS EXISTS RATHER THAN EVE'S OWN HITL ROUTE. eve's Slack channel has an interactivity seam for
5
+ // its own `eve_input:` cards, and `slack-input-response.ts` is OE's admission on it. That seam cannot
6
+ // serve this card, and the reason is structural rather than a not-yet: eve's own handler stores the
7
+ // card it minted in the SLACK channel's state (`defaults.js` writes `state.pendingApprovalCards`) and
8
+ // resolves the click against that channel's session address. A DM seat's parked request lives on the
9
+ // UNIFIED window — another channel, another session, another state bag — so a click on a card OE
10
+ // posted there has nothing for eve's route to resolve. It is OE's card, so it is OE's click.
11
+ //
12
+ // 🔴 THE SAME ADMISSION, THOUGH. `admitInputResponse` (core approvals/input-response-floor.ts) is the
13
+ // ONE decision both HITL answer doors take, and `ingressSpendGate` with door `"answer"` is the same
14
+ // spend floor `admitSlackInputResponse` consults FIRST for the same reason: a tap RESUMES a parked
15
+ // turn, which is a decision to spend more on a scope nobody asked whether it could afford. This
16
+ // module adds no floor of its own — it resolves the click, asks those two, and delivers.
17
+ //
18
+ // 🔴 A REFUSAL LEAVES THE CARD LIVE. Nothing here consumes a row on any outcome but a delivered
19
+ // answer: the approval stays parked, the card stays tappable for somebody who may answer it, and the
20
+ // question stays answerable IN WORDS through the text leg (`question-bridge.ts`), which is untouched.
21
+ // That is the same "a refusal is not a drop" posture the text leg holds.
22
+ import { admitInputResponse, APPROVAL_BUTTON_LABELS, APPROVAL_CARD_REFUSED_AUDIT_ACTION as CORE_APPROVAL_CARD_REFUSED_AUDIT_ACTION, APPROVAL_CARD_SETTLED_AUDIT_ACTION as CORE_APPROVAL_CARD_SETTLED_AUDIT_ACTION, auditApprovalCardTap, deploymentOrg, forgetApprovalCardTicket, forgetBridgedQuestion, getOperationalStore, ingressSpendGate, parseApprovalClickId, readBridgedQuestion, readApprovalCardTicket, resolveApprovalCardClick, resolvePartition, unifiedThreadRef, SLACK_SPEND_BADGES, } from "@open-everywhere/core/agent/adapter-api.js";
23
+ import { slackClickerIsGuest, slackForm } from "./slack-events.js";
24
+ const log = (m) => console.log(`[open-everywhere:slack] approval card: ${m}`);
25
+ /** The `input_response.routed` sibling for a tap: an approval settled through a BUTTON rather than a
26
+ * typed reply. RE-EXPORTED FROM CORE, never re-spelled here — one action name for every surface, so
27
+ * a scorer that joins on the row cannot under-count one of them (OPE-1856). */
28
+ export const APPROVAL_CARD_SETTLED_AUDIT_ACTION = CORE_APPROVAL_CARD_SETTLED_AUDIT_ACTION;
29
+ /** A verified tap that settled nothing, carrying WHICH refusal it was. Core-owned, as above. */
30
+ export const APPROVAL_CARD_REFUSED_AUDIT_ACTION = CORE_APPROVAL_CARD_REFUSED_AUDIT_ACTION;
31
+ /**
32
+ * Read the one `oe_input:` action out of a verified `block_actions` payload. `undefined` when the
33
+ * payload is not one of ours or is missing a field the decision needs — the front route then leaves
34
+ * it alone rather than half-handling it.
35
+ *
36
+ * THE FIRST `oe_input:` ACTION WINS and the rest are ignored: Slack sends one action per click, and a
37
+ * payload carrying two of ours is not a shape this host mints.
38
+ */
39
+ export function parseSlackApprovalClick(payload) {
40
+ if (typeof payload !== "object" || payload === null)
41
+ return undefined;
42
+ const p = payload;
43
+ if (!Array.isArray(p.actions))
44
+ return undefined;
45
+ let actionId;
46
+ for (const a of p.actions) {
47
+ const id = typeof a === "object" && a !== null ? a.action_id : undefined;
48
+ if (typeof id === "string" && parseApprovalClickId(id) !== undefined) {
49
+ actionId = id;
50
+ break;
51
+ }
52
+ }
53
+ if (actionId === undefined)
54
+ return undefined;
55
+ const channel = typeof p.channel === "object" && p.channel !== null ? p.channel.id : undefined;
56
+ const user = typeof p.user === "object" && p.user !== null ? p.user.id : undefined;
57
+ const team = typeof p.team === "object" && p.team !== null ? p.team.id : undefined;
58
+ if (typeof channel !== "string" || channel.length === 0)
59
+ return undefined;
60
+ if (typeof user !== "string" || user.length === 0)
61
+ return undefined;
62
+ return { actionId, channelId: channel, userId: user, ...(typeof team === "string" && team.length > 0 ? { teamId: team } : {}) };
63
+ }
64
+ /** The bindable a partition resolve needs from a raw interactivity payload. The team id is the ONE
65
+ * attribute that matters here, and it is read off the wire rather than through a placeholder — the
66
+ * same reading, and the same reason, as `slackAuthTeamId` in slack-input-response.ts. */
67
+ function bindableForClick(click) {
68
+ return {
69
+ channel: { kind: "slack" },
70
+ session: { auth: { current: { attributes: { ...(click.teamId ? { team_id: click.teamId } : {}), channel_id: click.channelId } } } },
71
+ };
72
+ }
73
+ /**
74
+ * The host copy that rides the delivered answer's `text` — the LABEL THIS DEPLOYMENT PUT ON THE
75
+ * BUTTON, read back out of the same allowlist that minted it, so the transcript says what the person
76
+ * actually chose. A tap carries no words, and writing the person's prose for them is not an option.
77
+ * The fallback capitalises the eve-minted option id, which can only be reached by a row written
78
+ * before an id left the allowlist; the DECISION is never this value, because eve settles on
79
+ * `optionId` alone.
80
+ */
81
+ function approvalAnswerText(optionId) {
82
+ return APPROVAL_BUTTON_LABELS.get(optionId.toLowerCase())?.label ?? optionId.charAt(0).toUpperCase() + optionId.slice(1);
83
+ }
84
+ /** Show the tapper alone why nothing happened. EPHEMERAL, never a message in the room: the card is
85
+ * still live for somebody who may answer it, and a public "you may not answer" note tells the room
86
+ * nothing it needs. Best-effort — a failed notice never changes the verdict. */
87
+ async function notifyTapper(channelId, userId, text) {
88
+ try {
89
+ await slackForm("chat.postEphemeral", new URLSearchParams({ channel: channelId, user: userId, text }).toString());
90
+ }
91
+ catch (err) {
92
+ log(`refusal notice failed to post: ${String(err)}`);
93
+ }
94
+ }
95
+ /**
96
+ * Handle one verified `oe_input:` click: resolve it, admit it, deliver the answer to eve, consume the
97
+ * rows. NEVER THROWS — the front route has already 200'd the interaction, and an unhandled throw here
98
+ * would only lose the log line.
99
+ *
100
+ * 🔴 THE ANSWER TEXT IS HOST COPY, and it must be. A tap carries no words, so a transcript that read
101
+ * the button as the person's own prose would be inventing their words. What rides is the host label
102
+ * this deployment put on that button (`Approve` / `Cancel`), which is what the person actually chose,
103
+ * and the `optionId` beside it is what eve settles on — `resolveApprovalOutcome` reads `optionId`
104
+ * ALONE, so the text is transcript only and never the decision.
105
+ *
106
+ * 🔴 CONSUME-ONCE, AND BOTH ROWS. The ticket goes so a second tap on the same card cannot answer a
107
+ * request eve has already settled; the outstanding-question row goes so the room's NEXT message is an
108
+ * ordinary wake again rather than a second answer to a settled request. Both happen only after eve
109
+ * accepted the delivery, exactly as the text leg orders them.
110
+ *
111
+ * THE LIMIT OF THAT, stated rather than implied: the read and the delete are not one atomic step, and
112
+ * the delete runs only AFTER `respond` returns, so anything arriving while the first tap is still in
113
+ * flight resolves against rows that are both still present. Two such deliveries each spend an
114
+ * admission, each call `respond`, and each can write a settled audit row — so one approval can read
115
+ * as decided by two taps. It is not a second DECISION: both had to be admitted and both had to name
116
+ * an option the request offered, and eve's own resolution refuses the second delivery for a request
117
+ * it already settled. A delivery that arrives after the first settle RETURNED is the covered case —
118
+ * the ticket is gone and it resolves `no-ticket`.
119
+ *
120
+ * 🔴 A SECOND DELIVERY IS MOST LIKELY TO BE SLACK'S OWN, NOT A SECOND PERSON, and this handler does
121
+ * not ack until the settle returns. Slack expects an interactivity endpoint to answer within about
122
+ * three seconds; what it does past that — show the tapper a connection error, redeliver, or both —
123
+ * is VENDOR BEHAVIOUR THIS TREE HAS NOT MEASURED, so do not read the bound above as covering it.
124
+ * Acking first and settling in the background would remove the delay and widen exactly the window
125
+ * this paragraph describes; the ordering here takes the narrower window instead. Whoever measures
126
+ * the real behaviour should revisit that trade, with the measurement written down.
127
+ */
128
+ export async function handleSlackApprovalCardClick(payload, respond) {
129
+ // 🔴 HOISTED OUT OF THE `try` SO THE `catch` CAN STAMP ITS ROW (OPE-1856). A throw anywhere below
130
+ // — including from the partition resolve itself — must still leave a row saying this deployment
131
+ // took a tap and settled nothing; core's auditor accepts an absent partition for that case.
132
+ let tapper = "";
133
+ let auditPartition;
134
+ try {
135
+ const click = parseSlackApprovalClick(payload);
136
+ if (click === undefined)
137
+ return { handled: false, reason: "not an oe_input: click" };
138
+ const parsed = parseApprovalClickId(click.actionId);
139
+ if (parsed === undefined)
140
+ return { handled: false, reason: "oe_input: action id did not parse" };
141
+ tapper = click.userId;
142
+ const org = deploymentOrg();
143
+ const store = getOperationalStore();
144
+ const partition = resolvePartition(bindableForClick(click));
145
+ auditPartition = partition;
146
+ // THE SPEND GATE FIRST, ahead of the identity admission — the same order and the same reason
147
+ // `admitSlackInputResponse` takes: over budget outranks identity, and a decline must not spend a
148
+ // roster lookup. Its refusal copy is its own; a decline here leaves the request PENDING.
149
+ const spend = await ingressSpendGate({
150
+ bindable: bindableForClick(click),
151
+ door: "answer",
152
+ principalId: click.userId,
153
+ workspaceRef: click.teamId ?? null,
154
+ badges: SLACK_SPEND_BADGES,
155
+ post: async (text) => {
156
+ await notifyTapper(click.channelId, click.userId, text);
157
+ },
158
+ });
159
+ if (spend === "declined") {
160
+ // AUDITED, not only logged (OPE-1856): a decline is this deployment refusing a person's tap,
161
+ // and a refusal nobody can count is indistinguishable from a tap that never arrived.
162
+ await auditApprovalCardTap({ platform: "slack", principalId: click.userId, partition, outcome: { kind: "refused", reason: "spend-declined" } });
163
+ log(`tap declined by the spend gate — the approval stays parked`);
164
+ return { handled: true, reason: "spend-declined" };
165
+ }
166
+ // The room the tap came from, in the SAME vocabulary the card's ticket stored — derived through
167
+ // core's own codec rather than spelled here, so the two sides cannot drift on the key's shape.
168
+ const containerKey = unifiedThreadRef({ platform: "slack", channelId: click.channelId }).channelKey;
169
+ const routing = await resolveApprovalCardClick({ ticket: parsed.ticket, optionId: parsed.optionId, containerKey }, {
170
+ read: (ticket) => readApprovalCardTicket(store, org, ticket),
171
+ // The join with the TEXT leg — see the dep's own note. Fail-CLOSED on an unreadable row: a
172
+ // store this leg cannot read cannot say the request is still parked, and settling on that
173
+ // guess is the one direction that spends a governed decision twice.
174
+ stillOutstanding: async (bareToken, requestId) => {
175
+ const outstanding = await readBridgedQuestion(store, org, bareToken).catch(() => undefined);
176
+ return outstanding?.requestId === requestId;
177
+ },
178
+ admit: async () => {
179
+ // Leg B, the same positive guest verdict both other clicker floors use, and inert on an
180
+ // unreadable probe (`undefined` never refuses).
181
+ const answererIsGuest = await slackClickerIsGuest(click.userId, click.teamId).catch(() => undefined);
182
+ const verdict = await admitInputResponse({
183
+ org: partition.org,
184
+ platform: partition.platform,
185
+ workspace: partition.workspace,
186
+ surface: partition.surface,
187
+ surfaceKey: partition.surfaceKey,
188
+ principalId: click.userId,
189
+ responses: 1,
190
+ ...(answererIsGuest === undefined ? {} : { answererIsGuest }),
191
+ });
192
+ return { ok: verdict.ok, floor: verdict.ok ? verdict.floor : "", notice: verdict.ok ? "" : verdict.notice };
193
+ },
194
+ });
195
+ if (routing.kind === "refuse") {
196
+ await auditApprovalCardTap({ platform: "slack", principalId: click.userId, partition, outcome: { kind: "refused", reason: routing.reason } });
197
+ if (routing.notice !== undefined)
198
+ await notifyTapper(click.channelId, click.userId, routing.notice);
199
+ log(`tap settled nothing (${routing.reason}) — the approval stays parked and answerable in words`);
200
+ return { handled: true, reason: routing.reason, ...(routing.notice ? { notice: routing.notice } : {}) };
201
+ }
202
+ const label = approvalAnswerText(routing.optionId);
203
+ // 🔴 DELIVERED BY SESSION ID, never by the window token (OPE-1863): a continuation token is
204
+ // channel-local, and this route is the SLACK channel's, so `from(bareToken)` here named a
205
+ // session that never existed and the settle threw into the catch below. The token's job is the
206
+ // outstanding-question row on the two lines that follow.
207
+ await respond(routing.sessionId, { requestId: routing.requestId, optionId: routing.optionId, text: label });
208
+ // CONSUME-ONCE, and only now that eve accepted the delivery. A throw above skips both, which
209
+ // leaves the card tappable and the question answerable — the safe direction.
210
+ await forgetApprovalCardTicket(store, org, parsed.ticket);
211
+ await forgetBridgedQuestion(store, org, routing.bareToken);
212
+ await auditApprovalCardTap({
213
+ platform: "slack",
214
+ principalId: click.userId,
215
+ partition,
216
+ outcome: { kind: "settled", requestId: routing.requestId, optionId: routing.optionId, floor: routing.floor },
217
+ });
218
+ log(`tap settled ${routing.requestId} as "${routing.optionId}" (floor ${routing.floor})`);
219
+ return { handled: true, reason: "settled" };
220
+ }
221
+ catch (err) {
222
+ // The approval is still parked and the card is still tappable, so the honest outcome is to say so
223
+ // and leave both rows in place. Never a throw: this runs after the interaction was acknowledged.
224
+ // AUDITED, not only logged (OPE-1856): ship #212's S-EVE-CARD leg could not say whether its tap
225
+ // had been refused or had thrown, because neither arm left a row. `auditApprovalCardTap` never
226
+ // throws, so this cannot turn a failed tap into a failed response.
227
+ await auditApprovalCardTap({ platform: "slack", principalId: tapper, ...(auditPartition === undefined ? {} : { partition: auditPartition }), outcome: { kind: "refused", reason: "error" } });
228
+ log(`tap failed (${err?.message ?? String(err)}) — nothing was settled, the approval stays parked`);
229
+ return { handled: true, reason: "error" };
230
+ }
231
+ }
232
+ //# sourceMappingURL=slack-approval-card.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slack-approval-card.js","sourceRoot":"","sources":["../../../agent/channels/slack-approval-card.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,66 @@
1
+ /** A resolved attachment record — its `urlPrivate` came from a Slack event's `files[].url_private`
2
+ * (trusted provenance), never a model-supplied string. */
3
+ export interface SlackAttachmentRecord {
4
+ /** The Slack `url_private` download URL from the event's `files[]`. */
5
+ readonly urlPrivate: string;
6
+ /** IANA media type from the event (`files[].mimetype`), when known — describe_image uses it
7
+ * because the id-based ref carries no filename extension to infer from. */
8
+ readonly mimeType?: string;
9
+ /** Original filename from the event, when known — the raw-bytes read path (read_file_upload)
10
+ * infers a type from its extension when the mimetype is absent. */
11
+ readonly name?: string;
12
+ /** The Slack team id the event belonged to (for future multi-bot token routing). */
13
+ readonly teamId?: string;
14
+ /** The platform that minted this attachment id — always "slack" for records written here. Stamped
15
+ * at ingest so the attachment plane's provenance is explicit end to end (channels/attachments.ts). */
16
+ readonly platform?: "slack";
17
+ }
18
+ /**
19
+ * Record an image attachment's trusted `url_private` under its Slack file id. THIS LIST IS THE
20
+ * AUDIT LIST for "who may write a fetchable url_private into the registry", so keep it exact.
21
+ * Two writers, both of trusted provenance:
22
+ * • `parseMessageEvent` (slack-events.ts) — an Events API message, which only arrives for a room
23
+ * the app is subscribed to and in;
24
+ * • `registerAttachments` in the front route's SHORTCUT branch (slack-front.ts) — a message
25
+ * shortcut, which Slack delivers even for a room the app is NOT in. It therefore registers
26
+ * only what that branch's readability and wake gates admitted (OPE-1442). The shortcut PARSER
27
+ * registers nothing: it runs at classification time, in front of those gates.
28
+ * A falsy id or an empty url is ignored (nothing to resolve later). Re-registering an id refreshes
29
+ * its recency.
30
+ */
31
+ export declare function registerSlackAttachment(fileId: string | undefined, rec: SlackAttachmentRecord): void;
32
+ /** The trusted record for a Slack file id, or undefined when this session never saw an event
33
+ * carrying it (→ describe_image fails closed rather than fetching an arbitrary URL). PROCESS-LOCAL:
34
+ * a cross-process worker must go through {@link resolveSlackAttachmentDurable}. */
35
+ export declare function resolveSlackAttachment(fileId: string): SlackAttachmentRecord | undefined;
36
+ /**
37
+ * Mirror one event attachment into the DURABLE store (task #107) so a delegated worker in a
38
+ * SEPARATE process can resolve it. Called ONLY from the trusted parse-consumption path (slack-front),
39
+ * with `org` resolved from the same event — never a model-supplied string. Best-effort: a store
40
+ * failure must never break the wake (the in-memory path still serves the same-process seat), so it
41
+ * swallows and logs. `org` defaults to the deployment org (single-tenant) when omitted.
42
+ */
43
+ export declare function persistSlackAttachment(fileId: string | undefined, rec: SlackAttachmentRecord, org?: string): Promise<void>;
44
+ /**
45
+ * Resolve a file id ACROSS processes: the in-memory registry first (same-process fast path), then
46
+ * the durable store (the cross-process bridge a delegated worker needs). Returns undefined when
47
+ * neither knows the id (→ the caller fails closed rather than fetching an arbitrary URL). `org`
48
+ * defaults to the deployment org (single-tenant) when omitted.
49
+ */
50
+ export declare function resolveSlackAttachmentDurable(fileId: string, org?: string): Promise<SlackAttachmentRecord | undefined>;
51
+ /**
52
+ * Register Slack as an attachment-plane resolver (channels/attachments.ts) — the neutral seam
53
+ * describe_image consults so its contract carries no per-platform union arm. The resolver wraps the
54
+ * SAME two guarantees the in-memory/durable registry + host-pinned fetch already provide:
55
+ * • it resolves a model-named id ONLY through the trusted registry (in-memory then durable,
56
+ * org-scoped) — an id no real Slack event recorded returns `undefined`, so the plane fails
57
+ * closed rather than fetching an arbitrary URL;
58
+ * • the download rides `fetchSlackAttachment` (bot token, HOST-PINNED to Slack, SSRF-guarded,
59
+ * redirect:error) against the event-supplied `url_private` — never a model string.
60
+ * A recognised id whose fetch fails THROWS (the plane surfaces it as a fetch failure), distinct
61
+ * from an unknown id (undefined). Idempotent per platform key.
62
+ */
63
+ export declare function registerSlackAttachmentResolver(): void;
64
+ /** Test seam: clear the in-memory registry so a test starts from empty. */
65
+ export declare function _resetSlackAttachmentsForTest(): void;
66
+ //# sourceMappingURL=slack-attachments.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slack-attachments.d.ts","sourceRoot":"","sources":["../../../agent/channels/slack-attachments.ts"],"names":[],"mappings":"AAsCA;2DAC2D;AAC3D,MAAM,WAAW,qBAAqB;IACpC,uEAAuE;IACvE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B;gFAC4E;IAC5E,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;wEACoE;IACpE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,oFAAoF;IACpF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;2GACuG;IACvG,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B;AAUD;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,EAAE,qBAAqB,GAAG,IAAI,CASpG;AAED;;oFAEoF;AACpF,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,qBAAqB,GAAG,SAAS,CAExF;AAED;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,EAAE,qBAAqB,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAahI;AAED;;;;;GAKG;AACH,wBAAsB,6BAA6B,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,GAAG,SAAS,CAAC,CAY5H;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,+BAA+B,IAAI,IAAI,CAoCtD;AAMD,2EAA2E;AAC3E,wBAAgB,6BAA6B,IAAI,IAAI,CAEpD"}
@@ -0,0 +1,174 @@
1
+ // Trusted Slack attachment registry — the provenance bridge between a Slack event's
2
+ // `files[].url_private` and the `describe_image` tool.
3
+ //
4
+ // THE PROBLEM IT SOLVES. A Slack image sits behind `url_private`, which needs the bot token to
5
+ // download. That token must NEVER ride a model-chosen URL (an exfil channel). So describe_image
6
+ // can't let the model hand it a Slack URL string — but the model still needs to point at *which*
7
+ // image it wants to see. This registry is the indirection: the wake pipeline (parseMessageEvent)
8
+ // registers each image attachment's `url_private` under its Slack FILE ID (trusted provenance —
9
+ // the event, not the model). describe_image's slack-attachment path takes a model-named file id,
10
+ // resolves it HERE to the event-supplied `url_private`, and hands THAT to the authenticated fetch
11
+ // helper (access/slack-attachment-fetch.ts). A model-supplied URL string can therefore never reach
12
+ // the token-bearing fetch — the model can only *name* an id that a real event already recorded.
13
+ //
14
+ // BOUNDED: a long-lived process must not accumulate url_private entries without limit, so the map
15
+ // evicts oldest-first past a cap. An evicted id simply fails closed in describe_image ("no such
16
+ // attachment") — a re-post (or a fresh wake) re-registers it. url_private links are themselves
17
+ // short-lived Slack-side, so unbounded retention would be pointless as well as leaky.
18
+ //
19
+ // THE CROSS-PROCESS HALF (task #107). The map above is PROCESS-LOCAL, so a DELEGATED worker — a
20
+ // one-shot task in a SEPARATE session/process from the seat that saw the upload (delegation-reshape-
21
+ // plan-2026-07-16.md) — can't resolve a file id it never received, and "read this uploaded file"
22
+ // couldn't be delegated. `persistSlackAttachment` mirrors each event attachment into the DURABLE
23
+ // operational store (org-scoped, TTL-pruned), and `resolveSlackAttachmentDurable` reads in-memory
24
+ // first (same-process fast path) then the store (cross-process). Written ONLY from the trusted parse
25
+ // path — never a model string — the same provenance contract the in-memory map holds. Non-image
26
+ // uploads are persisted too (the in-memory map is image-only for describe_image), so the raw-bytes
27
+ // read path (read_file_upload) can resolve a CSV/text/log an image reader never registered.
28
+ import { getOperationalStore, deploymentOrg, resolvePartition, registerAttachmentResolver, AttachmentOversizeError, } from "@open-everywhere/core/agent/adapter-api.js";
29
+ import { fetchSlackAttachment } from "../access/slack-attachment-fetch.js";
30
+ /** How long a durable attachment row is retained. Slack `url_private` links are short-lived
31
+ * server-side, and a delegated worker resolves its brief within hours — a day bounds the table
32
+ * without stranding a legitimately slow task. */
33
+ const SLACK_ATTACHMENT_TTL_MS = 24 * 60 * 60 * 1000;
34
+ const MAX_ENTRIES = 1000;
35
+ const registry = new Map();
36
+ /**
37
+ * Record an image attachment's trusted `url_private` under its Slack file id. THIS LIST IS THE
38
+ * AUDIT LIST for "who may write a fetchable url_private into the registry", so keep it exact.
39
+ * Two writers, both of trusted provenance:
40
+ * • `parseMessageEvent` (slack-events.ts) — an Events API message, which only arrives for a room
41
+ * the app is subscribed to and in;
42
+ * • `registerAttachments` in the front route's SHORTCUT branch (slack-front.ts) — a message
43
+ * shortcut, which Slack delivers even for a room the app is NOT in. It therefore registers
44
+ * only what that branch's readability and wake gates admitted (OPE-1442). The shortcut PARSER
45
+ * registers nothing: it runs at classification time, in front of those gates.
46
+ * A falsy id or an empty url is ignored (nothing to resolve later). Re-registering an id refreshes
47
+ * its recency.
48
+ */
49
+ export function registerSlackAttachment(fileId, rec) {
50
+ if (!fileId || !rec.urlPrivate)
51
+ return;
52
+ if (registry.has(fileId))
53
+ registry.delete(fileId); // refresh insertion order (LRU-ish)
54
+ registry.set(fileId, { ...rec, platform: "slack" }); // stamp provenance for the attachment plane
55
+ while (registry.size > MAX_ENTRIES) {
56
+ const oldest = registry.keys().next().value;
57
+ if (oldest === undefined)
58
+ break;
59
+ registry.delete(oldest);
60
+ }
61
+ }
62
+ /** The trusted record for a Slack file id, or undefined when this session never saw an event
63
+ * carrying it (→ describe_image fails closed rather than fetching an arbitrary URL). PROCESS-LOCAL:
64
+ * a cross-process worker must go through {@link resolveSlackAttachmentDurable}. */
65
+ export function resolveSlackAttachment(fileId) {
66
+ return registry.get(fileId);
67
+ }
68
+ /**
69
+ * Mirror one event attachment into the DURABLE store (task #107) so a delegated worker in a
70
+ * SEPARATE process can resolve it. Called ONLY from the trusted parse-consumption path (slack-front),
71
+ * with `org` resolved from the same event — never a model-supplied string. Best-effort: a store
72
+ * failure must never break the wake (the in-memory path still serves the same-process seat), so it
73
+ * swallows and logs. `org` defaults to the deployment org (single-tenant) when omitted.
74
+ */
75
+ export async function persistSlackAttachment(fileId, rec, org) {
76
+ if (!fileId || !rec.urlPrivate)
77
+ return;
78
+ const row = {
79
+ urlPrivate: rec.urlPrivate,
80
+ ...(rec.mimeType ? { mimeType: rec.mimeType } : {}),
81
+ ...(rec.name ? { name: rec.name } : {}),
82
+ ...(rec.teamId ? { teamId: rec.teamId } : {}),
83
+ };
84
+ try {
85
+ await getOperationalStore().putSlackAttachment(org ?? deploymentOrg(), fileId, row, new Date().toISOString(), SLACK_ATTACHMENT_TTL_MS);
86
+ }
87
+ catch (err) {
88
+ console.warn(`[open-everywhere:slack-attachments] dropped a durable attachment row for "${fileId}": ${String(err)}`);
89
+ }
90
+ }
91
+ /**
92
+ * Resolve a file id ACROSS processes: the in-memory registry first (same-process fast path), then
93
+ * the durable store (the cross-process bridge a delegated worker needs). Returns undefined when
94
+ * neither knows the id (→ the caller fails closed rather than fetching an arbitrary URL). `org`
95
+ * defaults to the deployment org (single-tenant) when omitted.
96
+ */
97
+ export async function resolveSlackAttachmentDurable(fileId, org) {
98
+ const local = registry.get(fileId);
99
+ if (local)
100
+ return local;
101
+ try {
102
+ const row = await getOperationalStore().getSlackAttachment(org ?? deploymentOrg(), fileId);
103
+ // Stamp provenance on the durable read too (the store is Slack-specific) so every resolved
104
+ // SlackAttachmentRecord carries `platform`, cross-process included.
105
+ return row ? { ...row, platform: "slack" } : undefined;
106
+ }
107
+ catch (err) {
108
+ console.warn(`[open-everywhere:slack-attachments] durable attachment lookup for "${fileId}" failed: ${String(err)}`);
109
+ return undefined;
110
+ }
111
+ }
112
+ /**
113
+ * Register Slack as an attachment-plane resolver (channels/attachments.ts) — the neutral seam
114
+ * describe_image consults so its contract carries no per-platform union arm. The resolver wraps the
115
+ * SAME two guarantees the in-memory/durable registry + host-pinned fetch already provide:
116
+ * • it resolves a model-named id ONLY through the trusted registry (in-memory then durable,
117
+ * org-scoped) — an id no real Slack event recorded returns `undefined`, so the plane fails
118
+ * closed rather than fetching an arbitrary URL;
119
+ * • the download rides `fetchSlackAttachment` (bot token, HOST-PINNED to Slack, SSRF-guarded,
120
+ * redirect:error) against the event-supplied `url_private` — never a model string.
121
+ * A recognised id whose fetch fails THROWS (the plane surfaces it as a fetch failure), distinct
122
+ * from an unknown id (undefined). Idempotent per platform key.
123
+ */
124
+ export function registerSlackAttachmentResolver() {
125
+ registerAttachmentResolver({
126
+ platform: "slack",
127
+ async resolve(attachmentId, ctx) {
128
+ const rec = await resolveSlackAttachmentDurable(attachmentId, resolvePartition(ctx).org);
129
+ if (!rec)
130
+ return undefined; // not a Slack id we saw → let the plane fail closed
131
+ const res = await fetchSlackAttachment(ctx, { urlPrivate: rec.urlPrivate, id: attachmentId });
132
+ if (!res.ok)
133
+ throw new Error(`fetching Slack attachment "${attachmentId}" failed (HTTP ${res.status})`);
134
+ return { bytes: new Uint8Array(await res.arrayBuffer()), ref: `slack-file:${attachmentId}`, mimeType: rec.mimeType };
135
+ },
136
+ // Size-guarded variant (task #107) for read_file_upload's raw-bytes read path. It resolves the
137
+ // TRUSTED record up front (no fetch) so the caller can refuse a type before any download, then
138
+ // defers the host-pinned, bot-token fetch to `pull` — which preserves the Content-Length
139
+ // pre-check EXACTLY: it rejects (AttachmentOversizeError, phase "declared") off the declared
140
+ // length BEFORE reading the body, and re-checks the actual byte count after (phase "actual").
141
+ async resolveGuarded(attachmentId, ctx) {
142
+ const rec = await resolveSlackAttachmentDurable(attachmentId, resolvePartition(ctx).org);
143
+ if (!rec)
144
+ return undefined; // not a Slack id we saw → let the plane fail closed
145
+ return {
146
+ ref: `slack-file:${attachmentId}`,
147
+ mimeType: rec.mimeType,
148
+ name: rec.name,
149
+ async pull({ maxBytes }) {
150
+ const res = await fetchSlackAttachment(ctx, { urlPrivate: rec.urlPrivate, id: attachmentId });
151
+ if (!res.ok)
152
+ throw new Error(`fetching the upload failed (HTTP ${res.status})`);
153
+ // Content-Length pre-check off the declared length — reject before reading the body.
154
+ const declared = Number.parseInt(res.headers.get("content-length") ?? "", 10);
155
+ if (Number.isFinite(declared) && declared > maxBytes)
156
+ throw new AttachmentOversizeError(declared, "declared", maxBytes);
157
+ const bytes = new Uint8Array(await res.arrayBuffer());
158
+ // Post-read cap — a chunked response can exceed the (possibly-absent) Content-Length.
159
+ if (bytes.byteLength > maxBytes)
160
+ throw new AttachmentOversizeError(bytes.byteLength, "actual", maxBytes);
161
+ return bytes;
162
+ },
163
+ };
164
+ },
165
+ });
166
+ }
167
+ // NOTE (OPE-114 stage 4): the resolver is no longer wired at module load — registerSlackProvider()
168
+ // (agent/register.ts) calls registerSlackAttachmentResolver() at boot, the same "register in every
169
+ // process" posture (register-adapters.ts runs in every process that can execute describe_image).
170
+ /** Test seam: clear the in-memory registry so a test starts from empty. */
171
+ export function _resetSlackAttachmentsForTest() {
172
+ registry.clear();
173
+ }
174
+ //# sourceMappingURL=slack-attachments.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slack-attachments.js","sourceRoot":"","sources":["../../../agent/channels/slack-attachments.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ import { type BindableContext } from "@open-everywhere/core/agent/adapter-api.js";
2
+ /** The public URL Slack POSTs the "/oe" slash command to (the app's Slash Command Request URL). */
3
+ export declare const SLACK_COMMANDS_ROUTE = "/eve/v1/slack/commands";
4
+ /** Build the BindableContext the identity seam (bindFrom/bindSlack) expects from Slack's
5
+ * slash-command form fields. Slack's slash-command payload carries no `channel_type` /
6
+ * `is_private` bit (unlike an app_mention/message event) — `bindSlack`'s existing fallbacks
7
+ * already handle that (DM detection via the `channel_id`'s "D" prefix; visibility fails
8
+ * closed to private), same posture as every other under-specified Slack event.
9
+ *
10
+ * 🔴 IT DOES CARRY THE ENTERPRISE GRID RUNG (OPE-1559). Slack sends `enterprise_id` as a form field
11
+ * on a Grid, beside `team_id`, and it reaches `RawIdentity.trustEnterprise` → the declaration walk →
12
+ * `binding.declaredTrust`. That is the OPERATOR-DECLARATION half — the half `roomVouchedInternalFrom`
13
+ * reads — so a Grid declared once now answers here the same way it answers at every other door,
14
+ * instead of resolving as though the line were absent.
15
+ *
16
+ * 🔴 STATE ITS REACH EXACTLY, because this door is narrower than the wake doors. For a CHANNEL
17
+ * command it does NOT change `binding.trust`: this payload carries no `conversations.info` verdict,
18
+ * so `slackTrust(undefined, undefined, "channel")` fails closed to `external`, and `bindFrom` takes
19
+ * that raw verdict ahead of any vouch. So a channel `/oe` stays `trust: "external"` with the rung
20
+ * and without it, and the budget scope `domainNameForScope` picks is unchanged. The rung reaches
21
+ * `binding.trust` only on a DM-shaped `/oe` (a `D…` channel id), where the mapper leaves trust
22
+ * undefined and the vouch is what answers. Spread conditionally, like every other door: a
23
+ * single-workspace install stamps nothing. */
24
+ export declare function bindableForSlashCommand(params: URLSearchParams): BindableContext;
25
+ declare const _default: import("eve/channels").Channel<undefined, Record<string, unknown>, Record<string, unknown>>;
26
+ export default _default;
27
+ //# sourceMappingURL=slack-commands.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slack-commands.d.ts","sourceRoot":"","sources":["../../../agent/channels/slack-commands.ts"],"names":[],"mappings":"AAoCA,OAAO,EASL,KAAK,eAAe,EACrB,MAAM,4CAA4C,CAAC;AAOpD,mGAAmG;AACnG,eAAO,MAAM,oBAAoB,2BAA2B,CAAC;AAS7D;;;;;;;;;;;;;;;;;;;+CAmB+C;AAC/C,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,eAAe,GAAG,eAAe,CAkBhF"}