@open-everywhere/provider-slack 0.8.1 → 0.9.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/agent/channels/slack-agent-session.ts +100 -63
- package/agent/channels/slack-events.ts +1 -1
- package/agent/channels/slack-front.ts +15 -8
- package/agent/channels/slack-identity.ts +8 -0
- package/agent/channels/slack-web-api.ts +3 -3
- package/agent/channels/slack-working-signs.ts +71 -0
- package/agent/channels/slack.ts +36 -50
- package/agent/docs-generated.ts +36 -16
- package/agent/ops.ts +4 -0
- package/agent/register.ts +4 -2
- package/agent/setup/claim.ts +9 -8
- package/agent/setup/credentials.ts +82 -79
- package/agent/setup/eve-credentials.ts +2 -2
- package/agent/setup/page.ts +3 -3
- package/agent/status.ts +12 -4
- package/dist/agent/channels/slack-agent-session.d.ts +19 -3
- package/dist/agent/channels/slack-agent-session.d.ts.map +1 -1
- package/dist/agent/channels/slack-agent-session.js +102 -65
- package/dist/agent/channels/slack-agent-session.js.map +1 -1
- package/dist/agent/channels/slack-events.js +1 -1
- package/dist/agent/channels/slack-events.js.map +1 -1
- package/dist/agent/channels/slack-front.d.ts.map +1 -1
- package/dist/agent/channels/slack-front.js +16 -9
- package/dist/agent/channels/slack-front.js.map +1 -1
- package/dist/agent/channels/slack-identity.d.ts +7 -0
- package/dist/agent/channels/slack-identity.d.ts.map +1 -1
- package/dist/agent/channels/slack-identity.js +2 -0
- package/dist/agent/channels/slack-identity.js.map +1 -1
- package/dist/agent/channels/slack-web-api.js +3 -3
- package/dist/agent/channels/slack-web-api.js.map +1 -1
- package/dist/agent/channels/slack-working-signs.d.ts +34 -0
- package/dist/agent/channels/slack-working-signs.d.ts.map +1 -0
- package/dist/agent/channels/slack-working-signs.js +63 -0
- package/dist/agent/channels/slack-working-signs.js.map +1 -0
- package/dist/agent/channels/slack.d.ts.map +1 -1
- package/dist/agent/channels/slack.js +28 -46
- package/dist/agent/channels/slack.js.map +1 -1
- package/dist/agent/docs-generated.d.ts.map +1 -1
- package/dist/agent/docs-generated.js +36 -16
- package/dist/agent/docs-generated.js.map +1 -1
- package/dist/agent/ops.d.ts.map +1 -1
- package/dist/agent/ops.js +4 -1
- package/dist/agent/ops.js.map +1 -1
- package/dist/agent/register.d.ts.map +1 -1
- package/dist/agent/register.js +4 -2
- package/dist/agent/register.js.map +1 -1
- package/dist/agent/setup/claim.d.ts.map +1 -1
- package/dist/agent/setup/claim.js +9 -8
- package/dist/agent/setup/claim.js.map +1 -1
- package/dist/agent/setup/credentials.d.ts +21 -20
- package/dist/agent/setup/credentials.d.ts.map +1 -1
- package/dist/agent/setup/credentials.js +71 -75
- package/dist/agent/setup/credentials.js.map +1 -1
- package/dist/agent/setup/eve-credentials.js +2 -2
- package/dist/agent/setup/eve-credentials.js.map +1 -1
- package/dist/agent/setup/page.js +3 -3
- package/dist/agent/setup/page.js.map +1 -1
- package/dist/agent/status.d.ts +10 -2
- package/dist/agent/status.d.ts.map +1 -1
- package/dist/agent/status.js +12 -4
- package/dist/agent/status.js.map +1 -1
- package/package.json +6 -2
package/agent/channels/slack.ts
CHANGED
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
// (the defaults derive a thinner, workspace-scoped auth). The typing indicator shows
|
|
11
11
|
// a code-owned STEP LABEL while a tool runs ("Searching Notion…", OPE-2449, see
|
|
12
12
|
// slack-step-label.ts), and otherwise a random status VERB ("Tinkering…"),
|
|
13
|
-
// configurable per org/domain — see
|
|
13
|
+
// configurable per org/domain — see slackStatusVerb() in slack-working-signs.ts.
|
|
14
14
|
//
|
|
15
15
|
// Credentials: supplied to eve (OPE-2231), as a VERIFIER and a token FUNCTION rather than two
|
|
16
|
-
// strings — agent/setup/eve-credentials.ts. Both resolve
|
|
17
|
-
//
|
|
16
|
+
// strings — agent/setup/eve-credentials.ts. Both resolve the operator secret first (OPE-2650: the db
|
|
17
|
+
// store, then its env var on-ramp), then the value a `/setup` run stored. They used to be omitted, which
|
|
18
18
|
// left eve reading `process.env` on a path OE no longer does. Capturing channel_type/is_private is a RAW-fact job; the
|
|
19
19
|
// security-critical interpretation (fail-closed to private) lives in the binding.
|
|
20
20
|
|
|
@@ -43,7 +43,6 @@ import {
|
|
|
43
43
|
getOperationalStore,
|
|
44
44
|
postOnce,
|
|
45
45
|
outboundKey,
|
|
46
|
-
statusVerbsFor,
|
|
47
46
|
getConfig,
|
|
48
47
|
finalMessageActionForTurn,
|
|
49
48
|
NO_REPLY_SENTINEL_REASON,
|
|
@@ -76,7 +75,6 @@ import {
|
|
|
76
75
|
introMessage,
|
|
77
76
|
instructionBullet,
|
|
78
77
|
ONBOARDED_MARKER,
|
|
79
|
-
STATUS_REACTION,
|
|
80
78
|
renderPinned,
|
|
81
79
|
isPinnedStatusMessage,
|
|
82
80
|
classifyWakeMapping,
|
|
@@ -89,7 +87,6 @@ import {
|
|
|
89
87
|
type BindableContext,
|
|
90
88
|
type ReactionState,
|
|
91
89
|
dmTrustTtlMs,
|
|
92
|
-
swallowRoomOp,
|
|
93
90
|
domainRoster,
|
|
94
91
|
domainKeyOf,
|
|
95
92
|
resolveWakeTrust,
|
|
@@ -105,7 +102,8 @@ import { admitSlackInputResponse } from "./slack-input-response.js";
|
|
|
105
102
|
import { taskCardConfidentiality } from "./slack-task-card.js";
|
|
106
103
|
import { turnEffectsFor, botDisplayName, slackForm, isSupervisorReplyTurn, deliverSupervisorReply, dmWakeThreadOverride, replyThreadTsForState, slackAuthorIsMember, slackWakeAuthorFacts, type SlackMemberProbe, type SupervisorReplyTarget } from "./slack-events.js";
|
|
107
104
|
import { markdownToSlackMrkdwn } from "./slack-mrkdwn.js";
|
|
108
|
-
import {
|
|
105
|
+
import { type StepAction } from "./slack-step-label.js";
|
|
106
|
+
import { setSlackStatusReaction, setSlackThreadStatus, SLACK_WORKING_STATUS, slackStatusVerb, slackStepStatus, type SlackSignRequest } from "./slack-working-signs.js";
|
|
109
107
|
import type { AuditDetail } from "@open-everywhere/core/agent/adapter-api.js";
|
|
110
108
|
|
|
111
109
|
// WARM_SILENT_LIMIT (the warm-thread shed threshold) now lives in channels/engagement.ts so
|
|
@@ -122,13 +120,8 @@ import type { AuditDetail } from "@open-everywhere/core/agent/adapter-api.js";
|
|
|
122
120
|
// "working" from the inbound handlers we already own — so the HITL approval UI
|
|
123
121
|
// (input.requested), reply rendering (message.completed), and typing all stay intact.
|
|
124
122
|
|
|
125
|
-
// --- status verbs (
|
|
126
|
-
//
|
|
127
|
-
// pick per status update from the channel's effective vocabulary (framework defaults
|
|
128
|
-
// ← org ← role ← domain, extend-or-replace; memory/domain.ts statusVerbsFor — a
|
|
129
|
-
// domain's ROLE preset contributes its themed verbs as its own rung). Orgs add house
|
|
130
|
-
// verbs, a team adds its in-jokes. Cosmetic only, so ANY failure falls back to a
|
|
131
|
-
// plain verb — the status line must never take a turn down.
|
|
123
|
+
// --- status verbs: slack-working-signs.ts `slackStatusVerb` (OPE-2632 moved them there, so both doors
|
|
124
|
+
// draw one vocabulary). ---------------------------------------------------------
|
|
132
125
|
/** The team a channel belongs to per THIS deploy's domain config (`slack/<team>/<channel>` entries),
|
|
133
126
|
* or undefined when the channel is unmapped. Fail-soft: config errors read as "unknown", never
|
|
134
127
|
* throw into the inbound path. Used only as the last leg of onInbound's teamId heal. */
|
|
@@ -150,29 +143,15 @@ function teamIdFromDomainConfig(channelId: string | undefined): string | undefin
|
|
|
150
143
|
return undefined;
|
|
151
144
|
}
|
|
152
145
|
|
|
153
|
-
function statusVerb(state: { channelId?: string | null; teamId?: string | null }): string {
|
|
154
|
-
try {
|
|
155
|
-
const p = resolvePartition({
|
|
156
|
-
channel: { kind: "slack" },
|
|
157
|
-
session: { auth: { current: { attributes: { team_id: state.teamId ?? "", channel_id: state.channelId ?? "" }, principalId: "", principalType: "slack-user" } } },
|
|
158
|
-
});
|
|
159
|
-
const cfg = getConfig();
|
|
160
|
-
const verbs = statusVerbsFor(p.surfaceKey, cfg.loadDomains(), cfg.loadRoles?.() ?? []);
|
|
161
|
-
return `${verbs[Math.floor(Math.random() * verbs.length)]}…`;
|
|
162
|
-
} catch {
|
|
163
|
-
return "Working…";
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
146
|
// Shared by `reasoning.appended` and `actions.requested`: the clean status verb, or a CODE-owned
|
|
168
147
|
// step label (OPE-2449, `slack-step-label.ts`), and NEVER the model's own in-progress text. We used to show the model's pre-tool-call
|
|
169
148
|
// preamble here (Tag-style, written for humans) — dropped 2026-07 (Tommy, live): that
|
|
170
149
|
// text streams in mid-sentence, so it could flash a truncated fragment ("I'll go away and
|
|
171
150
|
// look to setu…") for a moment before reverting to a verb, which read as broken. One
|
|
172
151
|
// helper, one behaviour, so neither call site can drift back to surfacing partial text.
|
|
173
|
-
async function refreshTypingStatus(channel: SlackEventContext,
|
|
152
|
+
async function refreshTypingStatus(channel: SlackEventContext, actions?: readonly StepAction[]): Promise<void> {
|
|
174
153
|
try {
|
|
175
|
-
await setWakeTyping(channel,
|
|
154
|
+
await setWakeTyping(channel, slackStepStatus(actions, channel.state));
|
|
176
155
|
} catch {
|
|
177
156
|
/* typing indicator is best-effort */
|
|
178
157
|
}
|
|
@@ -190,12 +169,7 @@ async function setWakeTyping(channel: SlackEventContext, status?: string): Promi
|
|
|
190
169
|
const override = dmWakeThreadOverride(channel.state);
|
|
191
170
|
const channelId = channel.state.channelId ?? undefined;
|
|
192
171
|
if (override && channelId) {
|
|
193
|
-
|
|
194
|
-
const body = new URLSearchParams({ channel_id: channelId, thread_ts: override, status: status ?? "" });
|
|
195
|
-
await slackForm("assistant.threads.setStatus", body.toString());
|
|
196
|
-
} catch {
|
|
197
|
-
/* thinking-status is best-effort */
|
|
198
|
-
}
|
|
172
|
+
await setSlackThreadStatus(formSign, { channelId, threadTs: override }, status ?? "");
|
|
199
173
|
return;
|
|
200
174
|
}
|
|
201
175
|
await channel.thread.startTyping(status);
|
|
@@ -284,19 +258,31 @@ async function setStatusReaction(slack: SlackHandle, state: ReactionState | "non
|
|
|
284
258
|
const timestamp = threadTsOverride ?? slack.threadTs;
|
|
285
259
|
if (!channel || !timestamp) return;
|
|
286
260
|
// The shared egress exit (core's channels/egress-spoof.ts, OPE-1132). This glyph is a room post,
|
|
287
|
-
// and
|
|
288
|
-
// `
|
|
289
|
-
//
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
261
|
+
// and `slack.request` is eve's own channel handle, so core's Slack bridge gate never sees it.
|
|
262
|
+
// `setSlackStatusReaction` asks `swallowRoomOp` for that reason.
|
|
263
|
+
// OPE-2632: the shared implementation (slack-working-signs.ts), which the DM door's signs call too.
|
|
264
|
+
await setSlackStatusReaction(
|
|
265
|
+
async (method, params) => {
|
|
266
|
+
try {
|
|
267
|
+
await slack.request(method, { ...params });
|
|
268
|
+
} catch {
|
|
269
|
+
/* already-reacted / no-reaction / missing-scope are all non-fatal */
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
{ channel, timestamp },
|
|
273
|
+
state,
|
|
274
|
+
);
|
|
298
275
|
}
|
|
299
276
|
|
|
277
|
+
/** The host-side sign seam for {@link setWakeTyping}'s per-wake override: `slackForm`, never throwing. */
|
|
278
|
+
const formSign: SlackSignRequest = async (method, params) => {
|
|
279
|
+
try {
|
|
280
|
+
await slackForm(method, new URLSearchParams({ ...params }).toString());
|
|
281
|
+
} catch {
|
|
282
|
+
/* thinking-status is best-effort */
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
|
|
300
286
|
// Mirror surface-tier STATUS.md to a single pinned Slack message (find it via a
|
|
301
287
|
// stable header marker so we don't persist a message ts across sessions).
|
|
302
288
|
async function syncPinnedStatus(channel: SlackEventContext): Promise<void> {
|
|
@@ -712,7 +698,7 @@ async function onInbound(
|
|
|
712
698
|
}
|
|
713
699
|
|
|
714
700
|
try {
|
|
715
|
-
await ctx.thread.startTyping(
|
|
701
|
+
await ctx.thread.startTyping(slackStatusVerb({ channelId: message.channelId, teamId: message.teamId }));
|
|
716
702
|
} catch {
|
|
717
703
|
/* typing indicator is best-effort */
|
|
718
704
|
}
|
|
@@ -934,7 +920,7 @@ const slackMentionChannel = slackChannel({
|
|
|
934
920
|
channel.state.pendingTaskResults = null;
|
|
935
921
|
channel.state.pendingToolCallMessage = null;
|
|
936
922
|
(channel.state as unknown as Record<string, unknown>).oeReasoningTypingAtMs = null;
|
|
937
|
-
await setWakeTyping(channel,
|
|
923
|
+
await setWakeTyping(channel, SLACK_WORKING_STATUS);
|
|
938
924
|
},
|
|
939
925
|
// Keep the model's REASONING out of the channel. GLM-5.2 runs with reasoning:"medium",
|
|
940
926
|
// and Eve's default `reasoning.appended` handler pipes the chain-of-thought straight into
|
|
@@ -978,7 +964,7 @@ const slackMentionChannel = slackChannel({
|
|
|
978
964
|
// so the indicator flashed a truncated fragment ("I'll go away and look to setu…"). That
|
|
979
965
|
// stays dropped: no model text reaches the status line.
|
|
980
966
|
"actions.requested": async (data, channel) => {
|
|
981
|
-
await refreshTypingStatus(channel,
|
|
967
|
+
await refreshTypingStatus(channel, data.actions as readonly StepAction[] | undefined);
|
|
982
968
|
},
|
|
983
969
|
// 🔴 THE OUTPUT FENCE (OPE-2449). eve's defaults for these two events show
|
|
984
970
|
// `presentation[callId].label`, which a tool's `label.complete`/`label.delta` builds from its
|
package/agent/docs-generated.ts
CHANGED
|
@@ -159,7 +159,13 @@ export const PROVIDER_DOCS: readonly DocEntry[] = [
|
|
|
159
159
|
"Connect one included). A DM does not count these two bits as an outside audience. A DM refuses an",
|
|
160
160
|
"approval card only on what Slack observes: the Slack Connect bits (`is_ext_shared`,",
|
|
161
161
|
"`is_pending_ext_shared`), or a partner whose `users.info` carries a guest flag. A group DM and a",
|
|
162
|
-
"channel still count all four share bits. The",
|
|
162
|
+
"channel still count all four share bits. The MCP-server propose card passes the same floor (OPE-2640).",
|
|
163
|
+
"Before that card, `propose_mcp_server` refuses a DM whose partner verdict from `users.info` is",
|
|
164
|
+
"external. That verdict does not read the share bits, but it does read the line. Without the line, a",
|
|
165
|
+
"full member at home in another workspace of the Grid is refused. The integration test",
|
|
166
|
+
"`tests/integration/approvals/grid-dm-card.test.ts` forges the Grid DM, with a Slack Connect DM as the",
|
|
167
|
+
"refused control. The live leg `S-DM-MCP-PROPOSE` posts the card in the board's own DM, which is not on",
|
|
168
|
+
"a Grid. The",
|
|
163
169
|
"**click** on that card is a member question, so it does read the line: a member-tier click from a",
|
|
164
170
|
"person whose home workspace is not the click's workspace counts as a guest click without it.",
|
|
165
171
|
"",
|
|
@@ -250,11 +256,13 @@ export const PROVIDER_DOCS: readonly DocEntry[] = [
|
|
|
250
256
|
"[the control plane](../../security/control-plane.md)); a reopen keeps every stored credential, so the",
|
|
251
257
|
"agent stays on Slack while you walk the page again.",
|
|
252
258
|
"",
|
|
253
|
-
"🔴 **If `SLACK_BOT_TOKEN` or `SLACK_SIGNING_SECRET` is set
|
|
254
|
-
"
|
|
255
|
-
"
|
|
256
|
-
"
|
|
257
|
-
"
|
|
259
|
+
"🔴 **If `SLACK_BOT_TOKEN` or `SLACK_SIGNING_SECRET` is set — as an operator secret (`oe secret set`)",
|
|
260
|
+
"or as its env var on-ramp — `/setup` answers 404.** The resolver takes the operator secret first",
|
|
261
|
+
"(OPE-2650; before it, the env var) — deliberately, so that landing this page cannot change which Slack",
|
|
262
|
+
"app a working deployment speaks as — which means a run on such a deployment would store credentials",
|
|
263
|
+
"nothing would ever read. Rather than let you complete a run that changes nothing, the page refuses. To",
|
|
264
|
+
"move an existing deployment onto the `/setup` values, run `oe secret rm` for both names, unset both",
|
|
265
|
+
"variables and redeploy; `/setup` then opens",
|
|
258
266
|
"and the deployment logs a setup code on that start.",
|
|
259
267
|
"",
|
|
260
268
|
"This is also why an existing deployment does **not** get an open setup page when it upgrades: it is",
|
|
@@ -284,7 +292,7 @@ export const PROVIDER_DOCS: readonly DocEntry[] = [
|
|
|
284
292
|
"",
|
|
285
293
|
"**2. The `/setup` window.** The page is open from the first start with a valid",
|
|
286
294
|
"`OE_CREDENTIAL_ENCRYPTION_KEY` until the first Slack install, unless `SLACK_BOT_TOKEN` or",
|
|
287
|
-
"`SLACK_SIGNING_SECRET` is set
|
|
295
|
+
"`SLACK_SIGNING_SECRET` is set (as an operator secret or its env var). It answers 404 while the key is unset. In that",
|
|
288
296
|
"window, anyone who can read the deployment's logs can claim the page: every member of the Vercel",
|
|
289
297
|
"team, and every reader of a log drain. The code is 128 bits, so a guess does not work. A claim lets",
|
|
290
298
|
"the holder create a Slack app in their own workspace that points at this deployment, and lock",
|
|
@@ -675,20 +683,26 @@ export const PROVIDER_DOCS: readonly DocEntry[] = [
|
|
|
675
683
|
"",
|
|
676
684
|
"## Step 2 — Configure the environment",
|
|
677
685
|
"",
|
|
678
|
-
"
|
|
686
|
+
"Both Slack values are **operator secrets** (OPE-2650): store them in the deployment's encrypted",
|
|
687
|
+
"credential store, from your own terminal, with the same `DATABASE_URL` and",
|
|
688
|
+
"`OE_CREDENTIAL_ENCRYPTION_KEY` the deployment reads:",
|
|
679
689
|
"",
|
|
680
690
|
"```bash",
|
|
681
|
-
"SLACK_BOT_TOKEN
|
|
682
|
-
"SLACK_SIGNING_SECRET
|
|
691
|
+
"npx oe secret set SLACK_BOT_TOKEN # Bot User OAuth Token from Step 1.4 (value on stdin)",
|
|
692
|
+
"npx oe secret set SLACK_SIGNING_SECRET # Signing Secret from Step 1.5 (value on stdin)",
|
|
683
693
|
"```",
|
|
684
694
|
"",
|
|
695
|
+
"A stored value takes effect within thirty seconds, with no redeploy. The env vars of the same names",
|
|
696
|
+
"still work as the on-ramp: the deployment reads one only when nothing is stored under its name.",
|
|
697
|
+
"",
|
|
685
698
|
"Optional, but relevant to the silo model:",
|
|
686
699
|
"",
|
|
687
700
|
"```bash",
|
|
688
|
-
"OE_MEMORY_PROVIDER=git # memory backend — git | sqlite",
|
|
689
701
|
"OE_MEMORY_DIR=/abs/path/to/store",
|
|
690
702
|
"```",
|
|
691
703
|
"",
|
|
704
|
+
"The memory backend (`git` | `sqlite`) is the `memory.provider` setting in `agent/settings/settings.ts`, not an env var.",
|
|
705
|
+
"",
|
|
692
706
|
"**Identity binds automatically.** You do **not** set workspace, surface, mode, or visibility by hand. The Slack channel reads `team_id`, `channel_id`, `channel_type`, and `is_private` straight off each inbound event into the session auth context, and `bindSlack` derives workspace = the Slack workspace, surface = the channel, plus mode and visibility. The `deploymentIdentity.workspace` / `.surface` declaration in `agent/identity/surfaces.ts` is only the `eve dev` (no-surface HTTP) gap-filler — once Slack is wired it is ignored.",
|
|
693
707
|
"",
|
|
694
708
|
"## Step 3 — Invoke it",
|
|
@@ -742,10 +756,12 @@ export const PROVIDER_DOCS: readonly DocEntry[] = [
|
|
|
742
756
|
" reaction via the `react` tool — a bare emoji ack (e.g. 👍) instead of a low-content reply in a",
|
|
743
757
|
" busy thread. It's a genuine alternative response, not a status indicator, and is audited like",
|
|
744
758
|
" any other tool call.",
|
|
745
|
-
"4. Test a DM: open a direct message with the bot and send a plain message (no `@mention` needed in a DM). The DM surface's memory is isolated and `private`. While the turn works, the bot shows the same
|
|
746
|
-
" - **👀 on your message.** A message inside a DM thread gets it on the thread's first message, as in a channel. It goes away when the turn ends
|
|
747
|
-
" - **The status line**
|
|
748
|
-
" -
|
|
759
|
+
"4. Test a DM: open a direct message with the bot and send a plain message (no `@mention` needed in a DM). The DM surface's memory is isolated and `private`. While the turn works, the bot shows the same signs a mention gets. A group DM with the bot shows them too.",
|
|
760
|
+
" - **👀 on your message.** A message inside a DM thread gets it on the thread's first message, as in a channel. It goes away when the turn ends. A DM reply stays flat, as described above.",
|
|
761
|
+
" - **The status line** under your message: `Working...` at the start, then a live step label while a tool runs (`Reading the thread…`), from the same code-owned table a mention uses. It goes away when the turn ends.",
|
|
762
|
+
" - **🛑 when the turn fails for good.** The 👀 goes, 🛑 goes on your message, and the status line clears. In a 1:1 DM the agent session also leaves `processing`, so Slack takes its Stop button off the dead turn. The next turn on the same message takes the 🛑 off again.",
|
|
763
|
+
" - **A group DM** has no agent session, so it has no Stop button and no session state. It shows every other sign.",
|
|
764
|
+
" - **If a sign is missing**, the app lacks a scope or the agent view. The 👀 and the 🛑 need `reactions:write`. The status line needs `assistant:write` and the agent view (`features.agent_view`). All three come from the manifest: paste it again (App Manifest → Save Changes) and reinstall if Slack asks. The deployment logs the refused call once per process, with this fix in the line: search the logs for `[open-everywhere:slack-session]`.",
|
|
749
765
|
"",
|
|
750
766
|
"### What it can read on Slack",
|
|
751
767
|
"",
|
|
@@ -1206,7 +1222,11 @@ export const PROVIDER_DOCS: readonly DocEntry[] = [
|
|
|
1206
1222
|
"**What the tests prove.** The unit pack `test:slack-doc-write` (packages/core) pins the room fence, the",
|
|
1207
1223
|
"cell encoding, the row match, the card text and the exact Slack calls an approval sends. The live e2e",
|
|
1208
1224
|
"scenario `S-CANVAS-LIST` asks the agent for a canvas in a real channel, approves the card, and reads the",
|
|
1209
|
-
"canvas back from Slack.",
|
|
1225
|
+
"canvas back from Slack. The live e2e scenario `S-LIST-WRITE` asks the agent to add a row to a fixture",
|
|
1226
|
+
"List in a real channel and then to change that row, approves each card, and reads the row back from",
|
|
1227
|
+
"Slack. It needs a List titled \"OE e2e List fixture\", with a text column named \"Note\", shared in the e2e",
|
|
1228
|
+
"channel, and a driver token with `lists:read`. The clean-up also needs `lists:write`: without it,",
|
|
1229
|
+
"each run leaves its row in the List.",
|
|
1210
1230
|
"",
|
|
1211
1231
|
"## Slash commands: `/oe` (zero-inference control-plane)",
|
|
1212
1232
|
"",
|
package/agent/ops.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
configuredAgentName,
|
|
32
32
|
declaredMcpServers,
|
|
33
33
|
getSettings,
|
|
34
|
+
registerOperatorSecretNames,
|
|
34
35
|
registerSurfaceAppManifest,
|
|
35
36
|
} from "@open-everywhere/core/agent/adapter-api-ops.js";
|
|
36
37
|
import { SLACK_AS_YOU_NAME } from "./slack-as-you.js";
|
|
@@ -60,6 +61,9 @@ export function slackManifestOptionsFromSettings(opts: { readonly appName?: stri
|
|
|
60
61
|
|
|
61
62
|
/** Register the slack ops-plane arm (the app manifest). */
|
|
62
63
|
export function registerSlackOps(): void {
|
|
64
|
+
// The two Slack credentials are operator secrets (OPE-2650): `scaffold_env` marks them, so the setup
|
|
65
|
+
// flow names `secret_set` as the step that stores them.
|
|
66
|
+
registerOperatorSecretNames(["SLACK_BOT_TOKEN", "SLACK_SIGNING_SECRET"]);
|
|
63
67
|
// THE PASTE-READY APP MANIFEST (OPE-1603). Minting the Slack app stays human, so the ops plane's
|
|
64
68
|
// job is to hand the operator the exact document. The bundle owns the document (agent/
|
|
65
69
|
// app-manifest.ts); core substitutes this deployment's host into it.
|
package/agent/register.ts
CHANGED
|
@@ -59,7 +59,7 @@ import {
|
|
|
59
59
|
registerBotAskProbe,
|
|
60
60
|
registerSurfaceSessionState,
|
|
61
61
|
} from "@open-everywhere/core/agent/adapter-api.js";
|
|
62
|
-
import { slackSurfaceSessionPin, slackSurfaceSessionSetter } from "./channels/slack-agent-session.js";
|
|
62
|
+
import { slackSurfaceSessionPin, slackSurfaceSessionSetter, slackSurfaceSessionStepper } from "./channels/slack-agent-session.js";
|
|
63
63
|
import { slackFriendlyNameResolver } from "./channels/slack-friendly-names.js";
|
|
64
64
|
import { PROVIDER_DOCS } from "./docs-generated.js";
|
|
65
65
|
import { slackSurfaceStatus } from "./status.js";
|
|
@@ -125,7 +125,9 @@ export function registerSlackProvider(): void {
|
|
|
125
125
|
// SLACK'S AGENT SESSION STATE (OPE-2519): `processing` while a turn runs (Slack then shows its Stop
|
|
126
126
|
// button), and `active` or `suspended` at the park. Core's turn-anchor hook drives it; the setter
|
|
127
127
|
// names the person's DM session from the wake's auth (channels/slack-agent-session.ts).
|
|
128
|
-
|
|
128
|
+
// OPE-2632: the same setter and the stepper draw a DM-door turn's working signs (1:1 and group DM):
|
|
129
|
+
// 👀, the status line with its step labels, and 🛑 when the turn fails for good.
|
|
130
|
+
registerSurfaceSessionState("slack", slackSurfaceSessionSetter(), slackSurfaceSessionPin, slackSurfaceSessionStepper());
|
|
129
131
|
// The bot's own Slack display name (OPE-587) — the arm the instruction assembler renders as a
|
|
130
132
|
// HANDLE line on a Slack-pinned session ("your mention/handle here appears as X"), never as
|
|
131
133
|
// identity since OPE-610 settled the naming model. `botDisplayName` memoises one auth.test +
|
package/agent/setup/claim.ts
CHANGED
|
@@ -63,7 +63,7 @@ import {
|
|
|
63
63
|
} from "@open-everywhere/core/agent/adapter-api.js";
|
|
64
64
|
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
65
65
|
import { SLACK_COOKIE_KEY_REF, SLACK_SETUP_CODE_REF, readInstallRecord, type SlackInstallStateDeps } from "./install-state.js";
|
|
66
|
-
import {
|
|
66
|
+
import { slackOperatorSecretPresent } from "./credentials.js";
|
|
67
67
|
|
|
68
68
|
/** The cookie the page sets once the code matches. `__Host-` pins it to this exact host with no
|
|
69
69
|
* domain and a `/` path, which a subdomain cannot overwrite. */
|
|
@@ -192,13 +192,14 @@ export async function slackSetupOpen(deps: SlackClaimDeps = {}): Promise<boolean
|
|
|
192
192
|
// stored credentials, lock the page behind themselves, and have `slackInstallFacts()` publish their
|
|
193
193
|
// workspace as this deployment's install.
|
|
194
194
|
//
|
|
195
|
-
// Closing on
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
|
|
195
|
+
// Closing on operator-secret presence costs nothing a real `/setup` operator wants: the resolver
|
|
196
|
+
// puts the operator secret first (OPE-2650 moved the two env vars there, with env as its on-ramp),
|
|
197
|
+
// so a deployment with either one set would IGNORE everything a run stored anyway — the page would
|
|
198
|
+
// have been a no-op with a credential write. An operator who wants to migrate onto the `/setup`
|
|
199
|
+
// values removes the operator secrets and the env vars, and redeploys. Screen 2's shadow notice
|
|
200
|
+
// still covers the case where one is set after an install.
|
|
201
|
+
const present = await slackOperatorSecretPresent({ ...(deps.env ? { env: deps.env } : {}) });
|
|
202
|
+
if (present.botToken || present.signingSecret) return false;
|
|
202
203
|
|
|
203
204
|
// 🔴 ONLY AN ABSENT ROW OPENS THE PAGE, and the earlier draft got this backwards. It read through
|
|
204
205
|
// `slackInstallState`, which collapses a store fault, a malformed row and an unknown record version
|