@open-everywhere/provider-slack 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent/access/slack-attachment-fetch.ts +3 -1
- package/agent/app-manifest.ts +48 -10
- package/agent/channels/slack-commands.ts +5 -1
- package/agent/channels/slack-events.ts +13 -4
- package/agent/channels/slack-front.ts +21 -6
- package/agent/channels/slack-read.ts +9 -3
- package/agent/channels/slack-setup-page.ts +20 -0
- package/agent/channels/slack-web-api.ts +34 -7
- package/agent/channels/slack.ts +11 -3
- package/agent/contributions.ts +1 -0
- package/agent/docs-generated.ts +1 -1
- package/agent/ops.ts +29 -61
- package/agent/register.ts +27 -1
- package/agent/setup/claim.ts +343 -0
- package/agent/setup/credentials.ts +265 -0
- package/agent/setup/eve-credentials.ts +77 -0
- package/agent/setup/install-state.ts +248 -0
- package/agent/setup/page.ts +579 -0
- package/agent/setup/slack-api-setup.ts +176 -0
- package/agent/status.ts +81 -0
- package/dist/agent/access/slack-attachment-fetch.d.ts.map +1 -1
- package/dist/agent/access/slack-attachment-fetch.js +3 -1
- package/dist/agent/access/slack-attachment-fetch.js.map +1 -1
- package/dist/agent/app-manifest.d.ts +4 -4
- package/dist/agent/app-manifest.d.ts.map +1 -1
- package/dist/agent/app-manifest.js +48 -10
- package/dist/agent/app-manifest.js.map +1 -1
- package/dist/agent/channels/slack-commands.d.ts.map +1 -1
- package/dist/agent/channels/slack-commands.js +5 -1
- package/dist/agent/channels/slack-commands.js.map +1 -1
- package/dist/agent/channels/slack-events.d.ts +1 -1
- package/dist/agent/channels/slack-events.d.ts.map +1 -1
- package/dist/agent/channels/slack-events.js +13 -4
- package/dist/agent/channels/slack-events.js.map +1 -1
- package/dist/agent/channels/slack-front.d.ts +1 -1
- package/dist/agent/channels/slack-front.d.ts.map +1 -1
- package/dist/agent/channels/slack-front.js +20 -5
- package/dist/agent/channels/slack-front.js.map +1 -1
- package/dist/agent/channels/slack-read.d.ts +6 -1
- package/dist/agent/channels/slack-read.d.ts.map +1 -1
- package/dist/agent/channels/slack-read.js +9 -3
- package/dist/agent/channels/slack-read.js.map +1 -1
- package/dist/agent/channels/slack-setup-page.d.ts +3 -0
- package/dist/agent/channels/slack-setup-page.d.ts.map +1 -0
- package/dist/agent/channels/slack-setup-page.js +13 -0
- package/dist/agent/channels/slack-setup-page.js.map +1 -0
- package/dist/agent/channels/slack-web-api.d.ts +3 -0
- package/dist/agent/channels/slack-web-api.d.ts.map +1 -1
- package/dist/agent/channels/slack-web-api.js +26 -6
- package/dist/agent/channels/slack-web-api.js.map +1 -1
- package/dist/agent/channels/slack.d.ts.map +1 -1
- package/dist/agent/channels/slack.js +11 -3
- package/dist/agent/channels/slack.js.map +1 -1
- package/dist/agent/contributions.d.ts +5 -0
- package/dist/agent/contributions.d.ts.map +1 -1
- package/dist/agent/contributions.js +1 -0
- package/dist/agent/contributions.js.map +1 -1
- package/dist/agent/docs-generated.js +1 -1
- package/dist/agent/docs-generated.js.map +1 -1
- package/dist/agent/ops.d.ts +1 -1
- package/dist/agent/ops.d.ts.map +1 -1
- package/dist/agent/ops.js +29 -47
- package/dist/agent/ops.js.map +1 -1
- package/dist/agent/register.d.ts.map +1 -1
- package/dist/agent/register.js +26 -2
- package/dist/agent/register.js.map +1 -1
- package/dist/agent/setup/claim.d.ts +73 -0
- package/dist/agent/setup/claim.d.ts.map +1 -0
- package/dist/agent/setup/claim.js +298 -0
- package/dist/agent/setup/claim.js.map +1 -0
- package/dist/agent/setup/credentials.d.ts +56 -0
- package/dist/agent/setup/credentials.d.ts.map +1 -0
- package/dist/agent/setup/credentials.js +214 -0
- package/dist/agent/setup/credentials.js.map +1 -0
- package/dist/agent/setup/eve-credentials.d.ts +22 -0
- package/dist/agent/setup/eve-credentials.d.ts.map +1 -0
- package/dist/agent/setup/eve-credentials.js +73 -0
- package/dist/agent/setup/eve-credentials.js.map +1 -0
- package/dist/agent/setup/install-state.d.ts +139 -0
- package/dist/agent/setup/install-state.d.ts.map +1 -0
- package/dist/agent/setup/install-state.js +196 -0
- package/dist/agent/setup/install-state.js.map +1 -0
- package/dist/agent/setup/page.d.ts +29 -0
- package/dist/agent/setup/page.d.ts.map +1 -0
- package/dist/agent/setup/page.js +460 -0
- package/dist/agent/setup/page.js.map +1 -0
- package/dist/agent/setup/slack-api-setup.d.ts +62 -0
- package/dist/agent/setup/slack-api-setup.d.ts.map +1 -0
- package/dist/agent/setup/slack-api-setup.js +138 -0
- package/dist/agent/setup/slack-api-setup.js.map +1 -0
- package/dist/agent/status.d.ts +4 -0
- package/dist/agent/status.d.ts.map +1 -0
- package/dist/agent/status.js +79 -0
- package/dist/agent/status.js.map +1 -0
- package/package.json +36 -3
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
type EgressDispatch,
|
|
42
42
|
type EgressRefusalCause,
|
|
43
43
|
} from "@open-everywhere/core/agent/adapter-api.js";
|
|
44
|
+
import { slackBotToken } from "../setup/credentials.js";
|
|
44
45
|
|
|
45
46
|
// THE CREDENTIAL'S IDENTITY, FOR THE AUDIT ROW (OPE-1547) — the ref NAME, never the value. This is
|
|
46
47
|
// the one host-side seam that attaches a credential, so before this ticket it was the one place
|
|
@@ -126,7 +127,8 @@ export async function fetchSlackAttachment(ctx: BindableContext, ref: TrustedSla
|
|
|
126
127
|
});
|
|
127
128
|
throw new Error(`describe_image: refusing to send the Slack bot token to non-Slack host "${safe.url.hostname}".`);
|
|
128
129
|
}
|
|
129
|
-
|
|
130
|
+
// Resolved, not read off the env (OPE-2231) — the token may live in the credential store.
|
|
131
|
+
const token = await slackBotToken();
|
|
130
132
|
if (!token) {
|
|
131
133
|
throw new Error("describe_image: SLACK_BOT_TOKEN is not set — the bot token is required to download a Slack attachment.");
|
|
132
134
|
}
|
package/agent/app-manifest.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
// THE Slack app manifest — one definition, three readers (OPE-1603).
|
|
2
2
|
//
|
|
3
|
-
// This file is the only place the manifest exists. The
|
|
3
|
+
// This file is the only place the manifest exists. The setup check's Slack step
|
|
4
4
|
// (packages/create/src/steps/slack.ts) prints it, the ops MCP tool `surface_manifest` serves it
|
|
5
5
|
// with this deployment's host substituted, and `scripts/slack-scope-audit-test.ts` parses the bot
|
|
6
|
-
// scope array below against the runbook's documented scopes. It used to live in the
|
|
6
|
+
// scope array below against the runbook's documented scopes. It used to live in the setup CLI, where
|
|
7
7
|
// the ops server could not reach it at all.
|
|
8
8
|
//
|
|
9
|
-
// 🔴 THIS MODULE IMPORTS NOTHING, DELIBERATELY. The
|
|
9
|
+
// 🔴 THIS MODULE IMPORTS NOTHING, DELIBERATELY. The setup CLI depends on the bundle for this
|
|
10
10
|
// document alone, so a leaf module costs it no transitive weight — not the adapter-api barrel, not
|
|
11
11
|
// eve, not the Slack Web API client. `agent/ops.ts` does the typed registration; this file holds
|
|
12
12
|
// only the vendor's own knowledge.
|
|
13
13
|
|
|
14
14
|
/** The token the request URLs carry where the deployment's own host belongs. The ops plane
|
|
15
|
-
* substitutes it (packages/core/agent/ops/surface-manifest.ts); the
|
|
15
|
+
* substitutes it (packages/core/agent/ops/surface-manifest.ts); the setup check leaves it for the
|
|
16
16
|
* operator, who has usually not deployed yet when they run it. */
|
|
17
17
|
export const SLACK_DEPLOY_HOST_PLACEHOLDER = "YOUR-DEPLOY-URL";
|
|
18
18
|
|
|
@@ -22,7 +22,7 @@ export const SLACK_DEFAULT_APP_NAME = "Open Everywhere";
|
|
|
22
22
|
/** Slack's limit on an app name (`display_information.name`). A longer name fails at paste time. */
|
|
23
23
|
export const SLACK_APP_NAME_MAX = 35;
|
|
24
24
|
|
|
25
|
-
/** Why `raw` cannot name the Slack app, or `undefined` when it can. The
|
|
25
|
+
/** Why `raw` cannot name the Slack app, or `undefined` when it can. The setup CLI's
|
|
26
26
|
* `--app-name` reads this; the ops tool applies core's generic check with {@link SLACK_APP_NAME_MAX}. */
|
|
27
27
|
export function slackAppNameProblem(raw: string): string | undefined {
|
|
28
28
|
const name = raw.trim();
|
|
@@ -38,8 +38,8 @@ export interface SlackAppManifestOptions {
|
|
|
38
38
|
readonly appName?: string;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
/** A ready-to-paste Slack app manifest (Slack's "create from manifest" flow) with the
|
|
42
|
-
*
|
|
41
|
+
/** A ready-to-paste Slack app manifest (Slack's "create from manifest" flow) with the scope/event
|
|
42
|
+
* set docs/deploy/surfaces/slack.md walks through by hand. */
|
|
43
43
|
export function slackAppManifestTemplate(opts: SlackAppManifestOptions = {}): string {
|
|
44
44
|
const appName = opts.appName?.trim() || SLACK_DEFAULT_APP_NAME;
|
|
45
45
|
return JSON.stringify(
|
|
@@ -62,8 +62,10 @@ export function slackAppManifestTemplate(opts: SlackAppManifestOptions = {}): st
|
|
|
62
62
|
{
|
|
63
63
|
command: "/oe",
|
|
64
64
|
url: `https://${SLACK_DEPLOY_HOST_PLACEHOLDER}/eve/v1/slack/commands`,
|
|
65
|
-
description: "Open Everywhere control commands
|
|
66
|
-
|
|
65
|
+
description: "Open Everywhere control-plane commands (stop, status, budget) — no model turn.",
|
|
66
|
+
// Every subcommand `parseOeCommandTokens` accepts (packages/core/agent/commands/core.ts).
|
|
67
|
+
// scripts/slack-app-manifest-test.ts fails if one is missing here.
|
|
68
|
+
usage_hint: "stop | status | budget [raise <usd> | clear] | rate <n> | help",
|
|
67
69
|
should_escape: false,
|
|
68
70
|
},
|
|
69
71
|
],
|
|
@@ -92,6 +94,13 @@ export function slackAppManifestTemplate(opts: SlackAppManifestOptions = {}): st
|
|
|
92
94
|
],
|
|
93
95
|
},
|
|
94
96
|
oauth_config: {
|
|
97
|
+
// THE OAUTH REDIRECT BACK TO THIS DEPLOYMENT (OPE-2231). `/setup` installs the app by sending
|
|
98
|
+
// the installer to Slack's authorize URL and taking the `code` on the way back, and Slack
|
|
99
|
+
// refuses a redirect to a URL the app does not DECLARE — so the agent-led install needs this
|
|
100
|
+
// line in the manifest, not just in the code that handles the callback. It is harmless on the
|
|
101
|
+
// hand-paste path: an app created from this manifest and installed from the Slack console
|
|
102
|
+
// never visits the URL.
|
|
103
|
+
redirect_urls: [`https://${SLACK_DEPLOY_HOST_PLACEHOLDER}/setup/callback`],
|
|
95
104
|
scopes: {
|
|
96
105
|
bot: [
|
|
97
106
|
"app_mentions:read",
|
|
@@ -119,10 +128,20 @@ export function slackAppManifestTemplate(opts: SlackAppManifestOptions = {}): st
|
|
|
119
128
|
// asked in an ordinary room is delivered to the requester's DM, never rendered there.
|
|
120
129
|
"im:write",
|
|
121
130
|
"groups:history",
|
|
131
|
+
// Group-DM messages — the scope `message.mpim` below needs (Slack's event reference).
|
|
132
|
+
"mpim:history",
|
|
122
133
|
// The group-DM (`mpim`) half of `im:read` — same two calls, same call sites. Code-derived
|
|
123
134
|
// rather than live-proven: the roster/disclosure paths are keyed on the room, and a
|
|
124
135
|
// member-tier click or a card can land in a group DM exactly as it can in a 1:1.
|
|
125
136
|
"mpim:read",
|
|
137
|
+
// files.getUploadURLExternal + files.completeUploadExternal — the `post_file` tool's
|
|
138
|
+
// upload flow (packages/core/agent/tools/post_file.ts). Without it every file post fails
|
|
139
|
+
// with `missing_scope`.
|
|
140
|
+
"files:write",
|
|
141
|
+
// The authenticated download of a Slack file's `url_private`
|
|
142
|
+
// (access/slack-attachment-fetch.ts fetchSlackAttachment), which `describe_image` uses to
|
|
143
|
+
// read an image a person posted. Slack serves that URL only to an app with this scope.
|
|
144
|
+
"files:read",
|
|
126
145
|
"reactions:write",
|
|
127
146
|
"reactions:read",
|
|
128
147
|
"pins:read",
|
|
@@ -136,7 +155,26 @@ export function slackAppManifestTemplate(opts: SlackAppManifestOptions = {}): st
|
|
|
136
155
|
settings: {
|
|
137
156
|
event_subscriptions: {
|
|
138
157
|
request_url: `https://${SLACK_DEPLOY_HOST_PLACEHOLDER}/eve/v1/slack`,
|
|
139
|
-
|
|
158
|
+
// EVERY event the front route acts on (slack-front.ts classifyEnvelope), and each one's
|
|
159
|
+
// scope is in the bot list above, per Slack's event reference:
|
|
160
|
+
// app_mention → app_mentions:read · message.im → im:history
|
|
161
|
+
// message.channels → channels:history (an engaged thread's follow-up with no re-mention)
|
|
162
|
+
// message.groups → groups:history (the same, in private channels)
|
|
163
|
+
// message.mpim → mpim:history (group DMs; without it only an @mention there is heard)
|
|
164
|
+
// member_joined_channel → channels:read, groups:read, mpim:read (the channel-join intro)
|
|
165
|
+
// reaction_added, reaction_removed → reactions:read (reaction feedback)
|
|
166
|
+
// scripts/slack-app-manifest-test.ts derives the handled set from the code and fails if
|
|
167
|
+
// this list drops one (OPE-2223).
|
|
168
|
+
bot_events: [
|
|
169
|
+
"app_mention",
|
|
170
|
+
"message.im",
|
|
171
|
+
"message.channels",
|
|
172
|
+
"message.groups",
|
|
173
|
+
"message.mpim",
|
|
174
|
+
"member_joined_channel",
|
|
175
|
+
"reaction_added",
|
|
176
|
+
"reaction_removed",
|
|
177
|
+
],
|
|
140
178
|
},
|
|
141
179
|
// The INTERACTIVITY Request URL — the SAME public route Event Subscriptions uses. Every
|
|
142
180
|
// button click and every shortcut lands here, and the front route classifies which handler
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
|
|
35
35
|
import { defineChannel, POST } from "eve/channels";
|
|
36
36
|
import { verifySlackSignature } from "./slack-front.js";
|
|
37
|
+
import { slackSigningSecret } from "../setup/credentials.js";
|
|
37
38
|
import {
|
|
38
39
|
bindFrom,
|
|
39
40
|
runOeCommand,
|
|
@@ -152,7 +153,10 @@ export default defineChannel({
|
|
|
152
153
|
// keeps stop free and working while the scope is over its spend cap.
|
|
153
154
|
POST(SLACK_COMMANDS_ROUTE, async (req, { attachSession, waitUntil }) => {
|
|
154
155
|
const rawBody = await req.text();
|
|
155
|
-
|
|
156
|
+
// Resolved, not read off the env (OPE-2231): a `/setup` run can have put the signing secret in
|
|
157
|
+
// the credential store, and `/oe stop` is the affordance a person reaches for when the agent is
|
|
158
|
+
// doing the wrong thing. An install where stop 401s is an install where stop is missing.
|
|
159
|
+
if (!verifySlackSignature(rawBody, req.headers, await slackSigningSecret())) {
|
|
156
160
|
return new Response("unauthorized", { status: 401 });
|
|
157
161
|
}
|
|
158
162
|
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import { slackContinuationToken } from "eve/channels/slack";
|
|
20
20
|
import { callSlackWebApi } from "./slack-web-api.js";
|
|
21
|
+
import { slackBotToken } from "../setup/credentials.js";
|
|
21
22
|
import { RateLimitExhaustedError } from "@open-everywhere/core/agent/adapter-api.js";
|
|
22
23
|
import {
|
|
23
24
|
resolvePartition,
|
|
@@ -3449,9 +3450,17 @@ const TRANSCRIPT_TOTAL_CHARS = 4000; // whole-block cap — the wake prompt stay
|
|
|
3449
3450
|
// second fetch-Slack-with-a-bot-token helper (house pattern: one client, many callers).
|
|
3450
3451
|
// `token` is the multi-bot seam (item 27): every Slack API call in the app funnels
|
|
3451
3452
|
// through here, so tier-2 per-app token routing is one argument at this signature
|
|
3452
|
-
// instead of a hunt through every caller.
|
|
3453
|
-
|
|
3454
|
-
|
|
3453
|
+
// instead of a hunt through every caller.
|
|
3454
|
+
//
|
|
3455
|
+
// 🔴 THE DEFAULT IS RESOLVED, NOT AN ENV READ (OPE-2231). It used to default to
|
|
3456
|
+
// `process.env.SLACK_BOT_TOKEN` in the parameter list, which a default cannot be any more: the token
|
|
3457
|
+
// may live in the credential store, so resolving it is async. The resolution therefore moved into the
|
|
3458
|
+
// body. An explicitly-passed token (the multi-bot seam) still wins untouched; no caller in the tree
|
|
3459
|
+
// passes one today, which is why this is a default-shape change and not a behaviour change for any
|
|
3460
|
+
// existing call site.
|
|
3461
|
+
export async function slackForm(method: string, body: string, token?: string): Promise<Record<string, unknown> | undefined> {
|
|
3462
|
+
const resolved = token ?? (await slackBotToken());
|
|
3463
|
+
if (!resolved) return undefined;
|
|
3455
3464
|
try {
|
|
3456
3465
|
// Route through OE's `callSlackWebApi` (channels/slack-web-api.ts) — the ONE seam that carries
|
|
3457
3466
|
// the 429/`Retry-After` backoff (OPE-1211). `body` is a pre-encoded form string on this seam, so
|
|
@@ -3459,7 +3468,7 @@ export async function slackForm(method: string, body: string, token: string | un
|
|
|
3459
3468
|
// through as the bot token. Fail-soft to undefined is preserved: the guard above handles a
|
|
3460
3469
|
// missing token, and any throw is swallowed below — an exhausted retry included.
|
|
3461
3470
|
return (await callSlackWebApi({
|
|
3462
|
-
botToken:
|
|
3471
|
+
botToken: resolved,
|
|
3463
3472
|
operation: method,
|
|
3464
3473
|
body: Object.fromEntries(new URLSearchParams(body)),
|
|
3465
3474
|
})) as Record<string, unknown>;
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
|
|
42
42
|
import { POST, type Channel, type HttpRouteDefinition, type RouteHandlerArgs } from "eve/channels";
|
|
43
43
|
import { SLACK_SIGNATURE_HEADER, SLACK_TIMESTAMP_HEADER, slackSignature, slackSignatureMatches } from "./slack-signature.js";
|
|
44
|
+
import { slackSigningSecret } from "../setup/credentials.js";
|
|
44
45
|
import { slackContinuationToken } from "eve/channels/slack";
|
|
45
46
|
import { callSlackWebApi } from "./slack-web-api.js";
|
|
46
47
|
import {
|
|
@@ -636,7 +637,7 @@ export interface FrontOptions {
|
|
|
636
637
|
/** Open (or reuse) the 1:1 DM channel with a Slack member, returning its `D…` id — the landing
|
|
637
638
|
* place a GLOBAL shortcut wakes in (OPE-1376), since a global shortcut names no conversation.
|
|
638
639
|
* `conversations.open` is idempotent, so this is a resolve rather than a create. Needs the
|
|
639
|
-
* `im:write` bot scope, which the runbook and the
|
|
640
|
+
* `im:write` bot scope, which the runbook and the app manifest already require for the
|
|
640
641
|
* governance DM-delivery arm. Defaults to the live call; FAIL-CLOSED: undefined ⇒ the shortcut
|
|
641
642
|
* wakes nothing and says so, never a wake in a guessed room. Injectable for tests. */
|
|
642
643
|
readonly openDirectMessageChannel?: (userId: string) => Promise<string | undefined>;
|
|
@@ -1006,7 +1007,15 @@ export function buildFrontHandler(innerHandler: RouteHandler, opts: FrontOptions
|
|
|
1006
1007
|
const fifo = opts.fifo ?? new ThreadFifo(recordWakeFailure);
|
|
1007
1008
|
const getBotIdentity = opts.getBotIdentity ?? resolveBotIdentity;
|
|
1008
1009
|
const now = opts.now ?? (() => Date.now());
|
|
1009
|
-
|
|
1010
|
+
// THE SIGNING SECRET RESOLVES PER REQUEST, AND IT IS ASYNC SINCE OPE-2231. It used to be a
|
|
1011
|
+
// property read on `process.env`; a `/setup` run can now have put it in the credential store
|
|
1012
|
+
// instead, so the resolver may touch the vault. It is awaited ONCE at the top of
|
|
1013
|
+
// `handleFrontRequest` and the VALUE is used by both readers below (the inbound verify and the
|
|
1014
|
+
// native forward's re-sign) — one resolution per request, and the two can never disagree about
|
|
1015
|
+
// which secret this request is being held to, which is what the re-sign depends on. A deployment
|
|
1016
|
+
// with `SLACK_SIGNING_SECRET` set never reaches the store (the resolver is env-first), so this is
|
|
1017
|
+
// the same property read it always was for every pre-OPE-2231 deployment.
|
|
1018
|
+
const resolveSigningSecret = async (): Promise<string | undefined> => opts.signingSecret ?? (await slackSigningSecret());
|
|
1010
1019
|
const markEventSeen = opts.markEventSeen ?? liveMarkEventSeen;
|
|
1011
1020
|
// OPE-589 leg D: reuse the taint-marking member probe (slack-events.ts `slackClickerIsGuest`,
|
|
1012
1021
|
// itself `slackAuthorIsMember` inverted). Same `users.info` call and cache the channel-taint leg
|
|
@@ -1069,7 +1078,8 @@ export function buildFrontHandler(innerHandler: RouteHandler, opts: FrontOptions
|
|
|
1069
1078
|
|
|
1070
1079
|
async function handleFrontRequest(req: Request, args: RouteHandlerArgs): Promise<Response> {
|
|
1071
1080
|
const rawBody = await req.text();
|
|
1072
|
-
|
|
1081
|
+
const requestSigningSecret = await resolveSigningSecret();
|
|
1082
|
+
if (!verifySlackSignature(rawBody, req.headers, requestSigningSecret, now())) {
|
|
1073
1083
|
return new Response("unauthorized", { status: 401 });
|
|
1074
1084
|
}
|
|
1075
1085
|
const decision = classifyEnvelope(rawBody, req.headers.get("content-type") ?? "");
|
|
@@ -1086,8 +1096,12 @@ export function buildFrontHandler(innerHandler: RouteHandler, opts: FrontOptions
|
|
|
1086
1096
|
*
|
|
1087
1097
|
* 🔴 THE RE-SIGN IS WHAT MAKES A REWRITE POSSIBLE. The inner handler re-verifies the Slack
|
|
1088
1098
|
* signature over the body it receives, so a changed body needs a signature over those bytes. The
|
|
1089
|
-
* secret is the one this route just VERIFIED the inbound signature with,
|
|
1090
|
-
* the
|
|
1099
|
+
* secret is the one this route just VERIFIED the inbound signature with, and eve resolves through
|
|
1100
|
+
* the SAME seam (OPE-2231: `channels/slack.ts` hands `slackChannel` a `webhookVerifier` built on
|
|
1101
|
+
* `setup/credentials.ts`), so there is no path on which the two can disagree about which secret
|
|
1102
|
+
* this request is held to. Before that wiring there WAS one — eve fell back to
|
|
1103
|
+
* `process.env.SLACK_SIGNING_SECRET` while this route read the credential store — and a
|
|
1104
|
+
* store-configured deployment 401'd in the inner handler after passing here.
|
|
1091
1105
|
*
|
|
1092
1106
|
* FAIL-SOFT TO TODAY'S BEHAVIOUR. If the body carried nothing to strip (the common path), or the
|
|
1093
1107
|
* signature cannot be reminted (no secret, no timestamp header), the ORIGINAL bytes and headers
|
|
@@ -1107,7 +1121,8 @@ export function buildFrontHandler(innerHandler: RouteHandler, opts: FrontOptions
|
|
|
1107
1121
|
withForwardedSlackEnvelope(forwardedEnvelopeOf(rawBody), () => {
|
|
1108
1122
|
const fenced = stripUnfencedSlackAttachments(rawBody);
|
|
1109
1123
|
const ts = req.headers.get(SLACK_TIMESTAMP_HEADER);
|
|
1110
|
-
|
|
1124
|
+
// The SAME value the inbound signature was just verified against — see the resolver above.
|
|
1125
|
+
const secret = requestSigningSecret;
|
|
1111
1126
|
if (fenced === rawBody || !ts || !secret) {
|
|
1112
1127
|
return innerHandler(new Request(req.url, { method: "POST", headers: req.headers, body: rawBody }), args);
|
|
1113
1128
|
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
// fallback the Slack channel itself uses.
|
|
14
14
|
|
|
15
15
|
import { callSlackWebApi } from "./slack-web-api.js";
|
|
16
|
+
import { slackBotToken } from "../setup/credentials.js";
|
|
16
17
|
import {
|
|
17
18
|
auditChannelReadRefusal,
|
|
18
19
|
bindFrom,
|
|
@@ -101,14 +102,19 @@ interface SlackReadResponse {
|
|
|
101
102
|
* Low-level POST to a (read-only) Slack Web API method with the bot token, via OE's
|
|
102
103
|
* `callSlackWebApi` (form-encoded — Slack's JSON support is partial: `conversations.replies`/
|
|
103
104
|
* `history` want form). Throws on a missing token or a non-ok Slack response so the tool
|
|
104
|
-
* surfaces a real error. The explicit
|
|
105
|
+
* surfaces a real error. The explicit pre-check keeps the actionable "not set" message
|
|
105
106
|
* (the seam itself throws a terser "SLACK_BOT_TOKEN is required.").
|
|
107
|
+
*
|
|
108
|
+
* 🔴 THE PRE-CHECK ASKS THE RESOLVER, NOT `process.env` (OPE-2231). It used to read the env var
|
|
109
|
+
* directly, which became a false negative the moment `/setup` could put the token in the credential
|
|
110
|
+
* store instead: the tools would refuse with "not set" on a deployment that holds a perfectly good
|
|
111
|
+
* stored token. Same message, same actionable text, asked of the one thing that knows.
|
|
106
112
|
*/
|
|
107
113
|
export async function callSlackRead(operation: string, params: Record<string, string | number | boolean>): Promise<SlackReadResponse> {
|
|
108
|
-
if (!
|
|
114
|
+
if (!(await slackBotToken())) {
|
|
109
115
|
throw new Error("SLACK_BOT_TOKEN is not set — the Slack read tools need the bot token to call the Web API.");
|
|
110
116
|
}
|
|
111
|
-
// botToken omitted → the seam resolves it
|
|
117
|
+
// botToken omitted → the seam resolves it the same way this pre-check just did.
|
|
112
118
|
const json = (await callSlackWebApi({ botToken: undefined, operation, body: params })) as SlackReadResponse;
|
|
113
119
|
// DEFINITIVE (OPE-821): Slack answers HTTP 200 with `ok:false` when it READ the request and
|
|
114
120
|
// refused it, so the call had no effect. `react` is the caller that acts on this — a refused
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Deployment mount for the agent-led Slack setup page (OPE-2231); the behaviour lives in
|
|
2
|
+
// agent/setup/page.ts. Three routes, and all three fail closed (404) until the credential vault is
|
|
3
|
+
// armed AND no Slack install is recorded — a deployment past setup serves nothing here.
|
|
4
|
+
|
|
5
|
+
import { defineChannel, GET, POST } from "eve/channels";
|
|
6
|
+
import {
|
|
7
|
+
SLACK_SETUP_CALLBACK_PATH,
|
|
8
|
+
SLACK_SETUP_PATH,
|
|
9
|
+
handleSlackSetupCallback,
|
|
10
|
+
handleSlackSetupGet,
|
|
11
|
+
handleSlackSetupPost,
|
|
12
|
+
} from "../setup/page.js";
|
|
13
|
+
|
|
14
|
+
export default defineChannel({
|
|
15
|
+
routes: [
|
|
16
|
+
GET(SLACK_SETUP_PATH, async (req) => handleSlackSetupGet(req)),
|
|
17
|
+
POST(SLACK_SETUP_PATH, async (req) => handleSlackSetupPost(req)),
|
|
18
|
+
GET(SLACK_SETUP_CALLBACK_PATH, async (req) => handleSlackSetupCallback(req)),
|
|
19
|
+
],
|
|
20
|
+
});
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
// • Form encoding. Slack's JSON support is partial (`conversations.replies`/`history` want form),
|
|
24
24
|
// so form is the only safe default. A null or undefined value is dropped; a string, number or
|
|
25
25
|
// boolean is stringified; anything else is JSON-encoded.
|
|
26
|
-
// • Token resolution. An explicit token wins, otherwise
|
|
26
|
+
// • Token resolution. An explicit token wins, otherwise the deployment's resolved bot token
|
|
27
|
+
// (`SLACK_BOT_TOKEN`, then the credential store — OPE-2231). Neither ⇒ throw
|
|
27
28
|
// `SLACK_BOT_TOKEN is required.` — the same message and the same fail-loud posture. ONE
|
|
28
29
|
// DELIBERATE NARROWING: eve's `SlackBotToken` also accepts a function (the secret-manager and
|
|
29
30
|
// rotation form). No caller in this repo passes one, so this seam takes a string. Widening it
|
|
@@ -40,6 +41,7 @@
|
|
|
40
41
|
// — a NAMED failure. Non-429 responses are untouched.
|
|
41
42
|
|
|
42
43
|
import { withRetryAfter, type RateLimitRetryOptions } from "@open-everywhere/core/agent/adapter-api.js";
|
|
44
|
+
import { slackBotToken } from "../setup/credentials.js";
|
|
43
45
|
|
|
44
46
|
/** Slack's Web API base. A trailing slash is required — `new URL(operation, base)` resolves the
|
|
45
47
|
* operation against the LAST path segment otherwise. */
|
|
@@ -129,9 +131,17 @@ const PROBE_OPERATIONS: ReadonlySet<string> = new Set(["auth.test", "conversatio
|
|
|
129
131
|
const PROBE_MAX_ATTEMPTS = 2;
|
|
130
132
|
const PROBE_CAP_MS = 2_000;
|
|
131
133
|
|
|
132
|
-
/**
|
|
133
|
-
|
|
134
|
-
|
|
134
|
+
/**
|
|
135
|
+
* An explicit token, else the deployment's resolved bot token (env first, then the credential store
|
|
136
|
+
* — `agent/setup/credentials.ts`). Throws eve's exact message when neither is set.
|
|
137
|
+
*
|
|
138
|
+
* 🔴 ASYNC SINCE OPE-2231, and the awaits are the whole change at the call site. The token used to be
|
|
139
|
+
* a property read; it can now come from the encrypted vault, because `/setup` captures it there. The
|
|
140
|
+
* resolver memoises a store hit per process, and a deployment with `SLACK_BOT_TOKEN` set never
|
|
141
|
+
* reaches the store at all, so the hot path for every pre-OPE-2231 deployment is unchanged.
|
|
142
|
+
*/
|
|
143
|
+
async function resolveBotToken(botToken: string | undefined): Promise<string> {
|
|
144
|
+
const token = botToken ?? (await slackBotToken());
|
|
135
145
|
if (!token) throw new Error("SLACK_BOT_TOKEN is required.");
|
|
136
146
|
return token;
|
|
137
147
|
}
|
|
@@ -144,16 +154,33 @@ function resolveBotToken(botToken: string | undefined): string {
|
|
|
144
154
|
* outlasts the bounded attempts.
|
|
145
155
|
*/
|
|
146
156
|
export async function callSlackWebApi(
|
|
147
|
-
input: {
|
|
157
|
+
input: {
|
|
158
|
+
readonly botToken?: string;
|
|
159
|
+
readonly operation: string;
|
|
160
|
+
readonly body: unknown;
|
|
161
|
+
/** Send NO `Authorization` header. The one legitimate use is `oauth.v2.access`, which Slack
|
|
162
|
+
* authenticates by client credentials in the body — see the note at the resolution below. */
|
|
163
|
+
readonly unauthenticated?: boolean;
|
|
164
|
+
},
|
|
148
165
|
deps: SlackWebApiDeps = {},
|
|
149
166
|
): Promise<SlackWebApiResponse> {
|
|
150
|
-
|
|
167
|
+
// THE ONE OPERATION WITH NO BEARER TOKEN TO RESOLVE (OPE-2231). `oauth.v2.access` is
|
|
168
|
+
// authenticated by the app's client credentials IN THE BODY, and it is the call that MINTS the bot
|
|
169
|
+
// token — so demanding one to make it is a chicken-and-egg, and passing the client secret as a
|
|
170
|
+
// bearer would be a different, wrong credential in the `Authorization` header. `unauthenticated`
|
|
171
|
+
// is therefore an explicit opt-out rather than a token that happens to be absent: the DEFAULT path
|
|
172
|
+
// still throws when no token resolves, so no ordinary call can silently lose its authorization
|
|
173
|
+
// header by forgetting to pass one.
|
|
174
|
+
const token = input.unauthenticated === true ? undefined : await resolveBotToken(input.botToken);
|
|
151
175
|
const encoded = encodeSlackForm(input.body);
|
|
152
176
|
const url = new URL(input.operation, deps.apiBase ?? SLACK_API_BASE).toString();
|
|
153
177
|
const send = (): Promise<Response> =>
|
|
154
178
|
(deps.fetch ?? (globalThis.fetch as SlackFetchLike))(url, {
|
|
155
179
|
body: encoded,
|
|
156
|
-
headers: {
|
|
180
|
+
headers: {
|
|
181
|
+
...(token === undefined ? {} : { authorization: `Bearer ${token}` }),
|
|
182
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
183
|
+
},
|
|
157
184
|
method: "POST",
|
|
158
185
|
});
|
|
159
186
|
|
package/agent/channels/slack.ts
CHANGED
|
@@ -11,11 +11,13 @@
|
|
|
11
11
|
// a random status VERB per update ("Tinkering…"), configurable per org/domain — see
|
|
12
12
|
// statusVerb() below.
|
|
13
13
|
//
|
|
14
|
-
// Credentials:
|
|
15
|
-
//
|
|
16
|
-
//
|
|
14
|
+
// Credentials: supplied to eve (OPE-2231), as a VERIFIER and a token FUNCTION rather than two
|
|
15
|
+
// strings — agent/setup/eve-credentials.ts. Both resolve env-first and then this deployment's
|
|
16
|
+
// encrypted credential store, which is where a `/setup` run puts them. They used to be omitted, which
|
|
17
|
+
// left eve reading `process.env` on a path OE no longer does. Capturing channel_type/is_private is a RAW-fact job; the
|
|
17
18
|
// security-critical interpretation (fail-closed to private) lives in the binding.
|
|
18
19
|
|
|
20
|
+
import { SLACK_EVE_CREDENTIALS } from "../setup/eve-credentials.js";
|
|
19
21
|
import {
|
|
20
22
|
slackChannel,
|
|
21
23
|
type SlackContext,
|
|
@@ -764,6 +766,12 @@ export async function admitInbound(
|
|
|
764
766
|
// session).
|
|
765
767
|
const slackMentionChannel = slackChannel({
|
|
766
768
|
route: INTERNAL_SLACK_ROUTE,
|
|
769
|
+
// 🔴 EVE RESOLVES THROUGH THE SAME SEAM OE DOES (OPE-2231), and this line is not optional. Eve's
|
|
770
|
+
// POST handler re-verifies the Slack signature and resolves its own bot token; with no `credentials`
|
|
771
|
+
// it read both from `process.env` and nowhere else. Once `/setup` could put them in the credential
|
|
772
|
+
// store instead, that made a store-configured deployment one where OE's verification passed and
|
|
773
|
+
// eve's 401'd — nothing acked, nothing woken. See agent/setup/eve-credentials.ts.
|
|
774
|
+
credentials: SLACK_EVE_CREDENTIALS,
|
|
767
775
|
// 🔴 QUEUE, NEVER STEER (OPE-412). eve's own default is `steer`, and a steer delivery carrying
|
|
768
776
|
// a message CANCELS the turn in flight — rolling session history back to the last completed
|
|
769
777
|
// model call while that call's tool side effects (posts, PRs, writes) stay real and unrecorded.
|
package/agent/contributions.ts
CHANGED
|
@@ -28,5 +28,6 @@ export const CONTRIBUTIONS = {
|
|
|
28
28
|
// ── channels ─────────────────────────────────────────────────────────────────────────────────
|
|
29
29
|
{ kind: "channel", name: "slack", module: "agent/channels/slack.js", summary: "The Slack surface. Wires Slack's webhook → an OpenEverywhere session, and — the point of the \"Slack-binding\" slice — populates the session auth context with the REAL Slack identity (team/channel/user…" },
|
|
30
30
|
{ kind: "channel", name: "slack-commands", module: "agent/channels/slack-commands.js", summary: "The Slack SLASH-COMMAND ingestion surface for \"/oe\" — the zero-inference control-plane escape hatch (backlog item 23): pure logic, instant code-rendered responses, NO model turn, so it keeps…" },
|
|
31
|
+
{ kind: "channel", name: "slack-setup-page", module: "agent/channels/slack-setup-page.js", summary: "The agent-led Slack setup page (OPE-2231) — `GET`/`POST /setup` create the Slack app from the shipped manifest with one pasted app configuration token, and `GET /setup/callback` takes the OAuth install's bot token. All three 404 until the credential vault is armed AND no install is recorded." },
|
|
31
32
|
],
|
|
32
33
|
} as const;
|