@assemblyline-agents/slack 5.0.10 → 6.0.1
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/README.md +76 -33
- package/ai.assemblyline/connections/slack.json +43 -0
- package/ai.assemblyline/index.cjs +94 -0
- package/dist/channel-operations.d.ts +51 -0
- package/dist/channel-operations.d.ts.map +1 -0
- package/dist/channel-operations.js +145 -0
- package/dist/channel-operations.js.map +1 -0
- package/dist/file-retrieval.d.ts +5 -0
- package/dist/file-retrieval.d.ts.map +1 -0
- package/dist/file-retrieval.js +77 -0
- package/dist/file-retrieval.js.map +1 -0
- package/dist/index.d.ts +10 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +101 -70
- package/dist/index.js.map +1 -1
- package/dist/message-delivery.d.ts +22 -0
- package/dist/message-delivery.d.ts.map +1 -0
- package/dist/message-delivery.js +82 -0
- package/dist/message-delivery.js.map +1 -0
- package/dist/slack-api.d.ts.map +1 -1
- package/dist/slack-api.js +7 -4
- package/dist/slack-api.js.map +1 -1
- package/dist/tools.d.ts +8 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +65 -0
- package/dist/tools.js.map +1 -0
- package/package.json +6 -3
- package/plugin.json +11 -0
- package/templates/slack-app-manifest.json +4 -1
package/README.md
CHANGED
|
@@ -2,12 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
Official Assembly Line Slack channel plugin.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
keeps the bot token out of tool input and returns only the destination and
|
|
10
|
-
provider message identifiers; arbitrary Slack Web API methods are rejected.
|
|
5
|
+
The `slack` channel profile verifies Events API requests and delivers replies,
|
|
6
|
+
files, and recent-activity context through the Slack Web API. A single
|
|
7
|
+
installation requires `SLACK_SIGNING_SECRET`, `SLACK_BOT_TOKEN`, and the
|
|
8
|
+
documented bot scopes.
|
|
11
9
|
|
|
12
10
|
Inbound message text is decoded from Slack's mrkdwn transport form before it
|
|
13
11
|
reaches the agent: auto-linked URLs (`<https://…>`, `<https://…|label>`)
|
|
@@ -17,8 +15,56 @@ pairing packets arrives exactly as the user typed it. Mention and command
|
|
|
17
15
|
sequences (`<@…>`, `<#…>`, `<!…>`) pass through unchanged.
|
|
18
16
|
|
|
19
17
|
Inbound files preserve their Slack media type and attachment classification.
|
|
20
|
-
Voice clips
|
|
21
|
-
|
|
18
|
+
Voice clips use the Slack channel profile's default OpenRouter transcription
|
|
19
|
+
before the model receives the turn. Set `channels.slack.audio` to `openai` or
|
|
20
|
+
`disabled` only when the agent needs a different policy.
|
|
21
|
+
|
|
22
|
+
Selecting `channels: [slack]` also activates the plugin-owned
|
|
23
|
+
`slack_retrieve_file` tool. The tool retrieves a private Slack file by id into
|
|
24
|
+
`/workspace/slack-files` for analysis, root `deliver_artifact` delivery, or a
|
|
25
|
+
subagent `handoff_artifact`. It enforces a 50 MB limit and uses the credentials
|
|
26
|
+
for the event's workspace.
|
|
27
|
+
|
|
28
|
+
Authored tools can use two typed, host-owned channel operations without gaining
|
|
29
|
+
access to a raw Slack Web API method:
|
|
30
|
+
|
|
31
|
+
- `resolve_dm_by_email` looks up a workspace member and opens or resumes the
|
|
32
|
+
bot's DM with that person. It requires only `users:read`, `users:read.email`,
|
|
33
|
+
and `im:write`.
|
|
34
|
+
- `post_message` posts standard Markdown to a channel or DM. It requires only
|
|
35
|
+
`chat:write`.
|
|
36
|
+
|
|
37
|
+
Use `callSlackChannelOperation` to keep operation names, inputs, and results
|
|
38
|
+
typed:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { callSlackChannelOperation } from "@assemblyline-agents/slack";
|
|
42
|
+
|
|
43
|
+
const destination = await callSlackChannelOperation(ctx, "resolve_dm_by_email", {
|
|
44
|
+
email: ownerEmail,
|
|
45
|
+
teamId
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
await callSlackChannelOperation(ctx, "post_message", {
|
|
49
|
+
channel: destination.channelId,
|
|
50
|
+
teamId,
|
|
51
|
+
text: "## Review ready\n\nThe memo is ready for review.",
|
|
52
|
+
idempotencyKey: ctx.idempotencyKey(`memo-ready:${memoId}`),
|
|
53
|
+
unfurlLinks: false,
|
|
54
|
+
unfurlMedia: false
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Both operations select the existing single- or multi-workspace bot credential.
|
|
59
|
+
`post_message` uses the same 12,000-character Markdown splitting, broadcast
|
|
60
|
+
neutralization, retry policy, and phase-aware errors as normal Slack delivery.
|
|
61
|
+
Each chunk receives a stable Slack `client_msg_id` derived from the caller's
|
|
62
|
+
idempotency key, so a retry does not duplicate a successfully accepted chunk.
|
|
63
|
+
The operation allowlist and per-operation input validation prevent authored
|
|
64
|
+
tools from passing arbitrary Slack API methods or payload fields.
|
|
65
|
+
|
|
66
|
+
The optional Slack MCP connection remains an explicit `capabilities: [slack]`
|
|
67
|
+
selection.
|
|
22
68
|
|
|
23
69
|
Outbound replies use Slack's standard Markdown, so headings, bold text, labeled
|
|
24
70
|
links, lists, tables, and code from an agent render natively without
|
|
@@ -35,8 +81,10 @@ neutralizes broadcast commands: `<!here>`, `<!channel>`, and `<!everyone>`
|
|
|
35
81
|
input therefore cannot ping the whole channel; links and user mentions in
|
|
36
82
|
outbound text keep working.
|
|
37
83
|
|
|
38
|
-
File delivery is explicit and durable.
|
|
39
|
-
for every completed `/workspace` file it needs to
|
|
84
|
+
File delivery is explicit and durable. A root agent calls `deliver_artifact`
|
|
85
|
+
once for every completed `/workspace` or `/files/handoffs` file it needs to
|
|
86
|
+
attach. A subagent calls `handoff_artifact` instead; the root then decides
|
|
87
|
+
whether to attach the returned file. The connector posts
|
|
40
88
|
the final response together with all selected files and reports success only
|
|
41
89
|
when Slack confirms every file id. Upload or completion failures leave the
|
|
42
90
|
combined delivery unsent for that attempt and the durable queue retries it. If
|
|
@@ -83,9 +131,10 @@ callbacks may omit that ID, so only that callback shape is checked against the
|
|
|
83
131
|
configured signing-secret set; team-identified events remain pinned to their
|
|
84
132
|
mapped secret.
|
|
85
133
|
|
|
86
|
-
Slack turns include a provider-scoped user principal by default.
|
|
87
|
-
|
|
88
|
-
|
|
134
|
+
Slack turns include a provider-scoped user principal by default. A custom local
|
|
135
|
+
channel plugin can wrap `defineSlackChannel` to map the sender to a canonical
|
|
136
|
+
company identity before hooks run. This callback is plugin code, not an
|
|
137
|
+
`agent.md` option:
|
|
89
138
|
|
|
90
139
|
```ts
|
|
91
140
|
export default defineSlackChannel({
|
|
@@ -104,14 +153,14 @@ export default defineSlackChannel({
|
|
|
104
153
|
|
|
105
154
|
Slack also reports each surface's privacy. DMs and assistant threads are
|
|
106
155
|
private to the authenticated principal; channel threads are shared. When the
|
|
107
|
-
agent opts into `audienceIsolation: true` in `agent.
|
|
156
|
+
agent opts into `audienceIsolation: true` in `agent.md`, the runtime uses that
|
|
108
157
|
boundary for memory, history, personal connections, subagents, schedules, and
|
|
109
158
|
delivery, so private DM context is unavailable in a channel or another user's
|
|
110
159
|
DM. Without the opt-in (the default), every surface is trusted and the signal
|
|
111
160
|
has no effect.
|
|
112
161
|
|
|
113
|
-
Under isolation, a
|
|
114
|
-
|
|
162
|
+
Under isolation, a custom Slack implementation for a personal workspace can
|
|
163
|
+
mark every surface private:
|
|
115
164
|
|
|
116
165
|
```ts
|
|
117
166
|
export default defineSlackChannel({
|
|
@@ -121,24 +170,17 @@ export default defineSlackChannel({
|
|
|
121
170
|
|
|
122
171
|
## Install
|
|
123
172
|
|
|
124
|
-
```sh
|
|
125
|
-
npm install @assemblyline-agents/slack
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
With the Assembly Line CLI, add the channel instead:
|
|
129
|
-
|
|
130
173
|
```sh
|
|
131
174
|
assembly-line add slack /path/to/agent
|
|
132
175
|
```
|
|
133
176
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
[`templates/slack-app-manifest.json`](templates/slack-app-manifest.json)
|
|
137
|
-
contains the bot scopes and
|
|
138
|
-
expected by the connector.
|
|
177
|
+
The command installs the package and selects `channels: [slack]` in `agent.md`.
|
|
178
|
+
It does not create a root channel file. The reusable
|
|
179
|
+
[`templates/slack-app-manifest.json`](templates/slack-app-manifest.json)
|
|
180
|
+
contains the expected bot scopes and event subscriptions.
|
|
139
181
|
|
|
140
|
-
1.
|
|
141
|
-
|
|
182
|
+
1. Copy the package template and replace
|
|
183
|
+
`https://your-agent.example.com/slack/events` with the deployed agent URL.
|
|
142
184
|
2. In Slack's app dashboard, create an app **from an app manifest** and paste or
|
|
143
185
|
upload the JSON.
|
|
144
186
|
3. Install the app to the workspace.
|
|
@@ -146,10 +188,11 @@ expected by the connector.
|
|
|
146
188
|
`SLACK_BOT_TOKEN` from **OAuth & Permissions** in the agent environment.
|
|
147
189
|
5. Verify the installation with `assembly-line channels check /path/to/agent`.
|
|
148
190
|
|
|
149
|
-
The loading indicator
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
191
|
+
The loading indicator and `post_message` operation need `chat:write`.
|
|
192
|
+
`resolve_dm_by_email` needs `users:read`, `users:read.email`, and `im:write`.
|
|
193
|
+
The default manifest also includes optional `groups:history` so private-channel
|
|
194
|
+
thread context works without another reinstall. Remove `groups:history` if the
|
|
195
|
+
bot will never be invited to private channels.
|
|
153
196
|
|
|
154
197
|
## Part of Assembly Line
|
|
155
198
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"kind": "slack",
|
|
3
|
+
"role": "connection",
|
|
4
|
+
"packageName": "@assemblyline-agents/slack",
|
|
5
|
+
"helper": "defineSlackConnection",
|
|
6
|
+
"protocol": "mcp",
|
|
7
|
+
"transport": "http",
|
|
8
|
+
"provider": "slack",
|
|
9
|
+
"description": "Slack workspace search, channels, messages, files, and users through Slack MCP.",
|
|
10
|
+
"defaultUrl": "https://mcp.slack.com/mcp",
|
|
11
|
+
"urlEnv": "SLACK_MCP_URL",
|
|
12
|
+
"tokenEnv": "SLACK_MCP_TOKEN",
|
|
13
|
+
"tokenRequired": true,
|
|
14
|
+
"capabilities": [
|
|
15
|
+
"mcp",
|
|
16
|
+
"live-tools",
|
|
17
|
+
"streamable-http"
|
|
18
|
+
],
|
|
19
|
+
"scopes": [],
|
|
20
|
+
"subject": "workspace",
|
|
21
|
+
"readToolPatterns": [
|
|
22
|
+
"regex:(^|[-_])(search|read|list|fetch|get|retrieve|query|lookup|find|describe|details?|discover)([-_]|$)"
|
|
23
|
+
],
|
|
24
|
+
"writeToolPatterns": [
|
|
25
|
+
"regex:(^|[-_])(add|append|archive|assign|book|buy|cancel|checkout|close|complete|create|delete|draft|duplicate|edit|execute|insert|invite|kick|link|merge|modify|move|mutate|open|pay|post|publish|purchase|push|react|reaction|remove|rename|reply|reserve|resolve|reopen|run|schedule|send|set|share|submit|transfer|transition|unassign|unlink|update|upload|write)([-_]|$)"
|
|
26
|
+
],
|
|
27
|
+
"setup": [
|
|
28
|
+
{
|
|
29
|
+
"kind": "connection",
|
|
30
|
+
"name": "slack-developer-configuration",
|
|
31
|
+
"required": true,
|
|
32
|
+
"message": "Configure Slack credentials, OAuth application, or MCP bridge for this deployment."
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
"requiredConfig": [],
|
|
36
|
+
"optionalConfig": [
|
|
37
|
+
"SLACK_MCP_URL"
|
|
38
|
+
],
|
|
39
|
+
"requiredCredentials": [
|
|
40
|
+
"SLACK_MCP_TOKEN"
|
|
41
|
+
],
|
|
42
|
+
"optionalCredentials": []
|
|
43
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
"channels": {
|
|
3
|
+
"slack": {
|
|
4
|
+
"definition": "@assemblyline-agents/slack",
|
|
5
|
+
"factory": "defineSlackChannel",
|
|
6
|
+
"transport": "http",
|
|
7
|
+
"route": "/slack/events",
|
|
8
|
+
"methods": [
|
|
9
|
+
"POST"
|
|
10
|
+
],
|
|
11
|
+
"requiredCredentials": [
|
|
12
|
+
"SLACK_SIGNING_SECRET",
|
|
13
|
+
"SLACK_BOT_TOKEN"
|
|
14
|
+
],
|
|
15
|
+
"optionalConfig": [
|
|
16
|
+
"SLACK_BOT_USER_ID",
|
|
17
|
+
"SLACK_ASSISTANT_ENABLED"
|
|
18
|
+
],
|
|
19
|
+
"optionalCredentials": [
|
|
20
|
+
"SLACK_WORKSPACE_CREDENTIALS_JSON"
|
|
21
|
+
],
|
|
22
|
+
"defaultAudio": "openrouter",
|
|
23
|
+
"configSchema": {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"additionalProperties": false
|
|
26
|
+
},
|
|
27
|
+
"metadata": {
|
|
28
|
+
"provider": "slack",
|
|
29
|
+
"ingress": {
|
|
30
|
+
"requiredSecretEnv": [
|
|
31
|
+
[
|
|
32
|
+
"SLACK_SIGNING_SECRET"
|
|
33
|
+
]
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
"installation": {
|
|
37
|
+
"requiredBotScopes": [
|
|
38
|
+
"app_mentions:read",
|
|
39
|
+
"channels:history",
|
|
40
|
+
"chat:write",
|
|
41
|
+
"files:read",
|
|
42
|
+
"files:write",
|
|
43
|
+
"im:history",
|
|
44
|
+
"im:write",
|
|
45
|
+
"users:read",
|
|
46
|
+
"users:read.email"
|
|
47
|
+
],
|
|
48
|
+
"optionalBotScopes": [
|
|
49
|
+
"assistant:write",
|
|
50
|
+
"groups:history"
|
|
51
|
+
],
|
|
52
|
+
"channelOperations": {
|
|
53
|
+
"resolve_dm_by_email": {
|
|
54
|
+
"requiredBotScopes": ["users:read", "users:read.email", "im:write"]
|
|
55
|
+
},
|
|
56
|
+
"post_message": {
|
|
57
|
+
"requiredBotScopes": ["chat:write"]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"connections": {
|
|
65
|
+
"slack": {
|
|
66
|
+
"contract": "./ai.assemblyline/connections/slack.json",
|
|
67
|
+
"definition": "@assemblyline-agents/slack",
|
|
68
|
+
"factory": "defineSlackConnection"
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"tools": {
|
|
72
|
+
"slack_retrieve_file": {
|
|
73
|
+
"definition": "@assemblyline-agents/slack",
|
|
74
|
+
"factory": "defineSlackRetrieveFileTool",
|
|
75
|
+
"description": "Retrieve an existing private Slack file by id into the run sandbox.",
|
|
76
|
+
"inputSchema": {
|
|
77
|
+
"type": "object",
|
|
78
|
+
"additionalProperties": false,
|
|
79
|
+
"properties": {
|
|
80
|
+
"fileId": { "type": "string" },
|
|
81
|
+
"filename": { "type": "string" },
|
|
82
|
+
"teamId": { "type": "string" }
|
|
83
|
+
},
|
|
84
|
+
"required": ["fileId"]
|
|
85
|
+
},
|
|
86
|
+
"capability": {
|
|
87
|
+
"visibility": "deferred",
|
|
88
|
+
"execution": "direct",
|
|
89
|
+
"namespace": "slack",
|
|
90
|
+
"tags": ["slack", "file", "attachment", "download"]
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { JsonObject, ToolExecutionContext } from "@assemblyline-agents/core";
|
|
2
|
+
import type { ChannelContext, ChannelTurn } from "@assemblyline-agents/runtime";
|
|
3
|
+
export declare const SLACK_CHANNEL_OPERATIONS: readonly ["resolve_dm_by_email", "post_message"];
|
|
4
|
+
export type SlackChannelOperation = typeof SLACK_CHANNEL_OPERATIONS[number];
|
|
5
|
+
/** Exact bot-token scopes used by each semantic operation. */
|
|
6
|
+
export declare const SLACK_CHANNEL_OPERATION_REQUIRED_BOT_SCOPES: {
|
|
7
|
+
readonly resolve_dm_by_email: readonly ["users:read", "users:read.email", "im:write"];
|
|
8
|
+
readonly post_message: readonly ["chat:write"];
|
|
9
|
+
};
|
|
10
|
+
export interface SlackResolveDmByEmailInput extends JsonObject {
|
|
11
|
+
email: string;
|
|
12
|
+
teamId?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface SlackResolveDmByEmailResult extends JsonObject {
|
|
15
|
+
ok: true;
|
|
16
|
+
userId: string;
|
|
17
|
+
channelId: string;
|
|
18
|
+
teamId?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface SlackPostMessageInput extends JsonObject {
|
|
21
|
+
channel: string;
|
|
22
|
+
text: string;
|
|
23
|
+
idempotencyKey: string;
|
|
24
|
+
teamId?: string;
|
|
25
|
+
threadTs?: string;
|
|
26
|
+
unfurlLinks?: boolean;
|
|
27
|
+
unfurlMedia?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface SlackPostMessageResult extends JsonObject {
|
|
30
|
+
ok: true;
|
|
31
|
+
provider: "slack";
|
|
32
|
+
mode: "chat.postMessage";
|
|
33
|
+
channel: string;
|
|
34
|
+
threadTs?: string;
|
|
35
|
+
slackMessageTs: string[];
|
|
36
|
+
messageCount: number;
|
|
37
|
+
idempotencyKey: string;
|
|
38
|
+
}
|
|
39
|
+
export interface SlackChannelOperationInputs {
|
|
40
|
+
resolve_dm_by_email: SlackResolveDmByEmailInput;
|
|
41
|
+
post_message: SlackPostMessageInput;
|
|
42
|
+
}
|
|
43
|
+
export interface SlackChannelOperationResults {
|
|
44
|
+
resolve_dm_by_email: SlackResolveDmByEmailResult;
|
|
45
|
+
post_message: SlackPostMessageResult;
|
|
46
|
+
}
|
|
47
|
+
/** Typed authored-tool entrypoint for the current Slack channel capability. */
|
|
48
|
+
export declare function callSlackChannelOperation<Operation extends SlackChannelOperation>(ctx: Pick<ToolExecutionContext, "channel">, operation: Operation, input: SlackChannelOperationInputs[Operation]): Promise<SlackChannelOperationResults[Operation]>;
|
|
49
|
+
/** Runtime dispatcher. Only the named semantic operations can reach Slack. */
|
|
50
|
+
export declare function executeSlackChannelOperation(operation: string, input: JsonObject, turn: ChannelTurn, ctx: ChannelContext): Promise<JsonObject>;
|
|
51
|
+
//# sourceMappingURL=channel-operations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel-operations.d.ts","sourceRoot":"","sources":["../src/channel-operations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAClF,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAchF,eAAO,MAAM,wBAAwB,kDAAmD,CAAC;AACzF,MAAM,MAAM,qBAAqB,GAAG,OAAO,wBAAwB,CAAC,MAAM,CAAC,CAAC;AAE5E,8DAA8D;AAC9D,eAAO,MAAM,2CAA2C;;;CAGa,CAAC;AAEtE,MAAM,WAAW,0BAA2B,SAAQ,UAAU;IAC5D,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,2BAA4B,SAAQ,UAAU;IAC7D,EAAE,EAAE,IAAI,CAAC;IACT,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,qBAAsB,SAAQ,UAAU;IACvD,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,sBAAuB,SAAQ,UAAU;IACxD,EAAE,EAAE,IAAI,CAAC;IACT,QAAQ,EAAE,OAAO,CAAC;IAClB,IAAI,EAAE,kBAAkB,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,2BAA2B;IAC1C,mBAAmB,EAAE,0BAA0B,CAAC;IAChD,YAAY,EAAE,qBAAqB,CAAC;CACrC;AAED,MAAM,WAAW,4BAA4B;IAC3C,mBAAmB,EAAE,2BAA2B,CAAC;IACjD,YAAY,EAAE,sBAAsB,CAAC;CACtC;AAED,+EAA+E;AAC/E,wBAAsB,yBAAyB,CAAC,SAAS,SAAS,qBAAqB,EACrF,GAAG,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC,EAC1C,SAAS,EAAE,SAAS,EACpB,KAAK,EAAE,2BAA2B,CAAC,SAAS,CAAC,GAC5C,OAAO,CAAC,4BAA4B,CAAC,SAAS,CAAC,CAAC,CAKlD;AAED,8EAA8E;AAC9E,wBAAsB,4BAA4B,CAChD,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,UAAU,EACjB,IAAI,EAAE,WAAW,EACjB,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC,UAAU,CAAC,CAQrB"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { postSlackMarkdown, slackPhaseError, slackTextDeliveryMetadata } from "./message-delivery.js";
|
|
2
|
+
import { slackApi } from "./slack-api.js";
|
|
3
|
+
const SLACK_CONVERSATION_ID = /^[CDG][A-Z0-9]+$/u;
|
|
4
|
+
const SLACK_DM_ID = /^D[A-Z0-9]+$/u;
|
|
5
|
+
const SLACK_TEAM_ID = /^T[A-Z0-9]+$/u;
|
|
6
|
+
const SLACK_TIMESTAMP = /^\d+\.\d+$/u;
|
|
7
|
+
const SLACK_USER_ID = /^[UW][A-Z0-9]+$/u;
|
|
8
|
+
export const SLACK_CHANNEL_OPERATIONS = ["resolve_dm_by_email", "post_message"];
|
|
9
|
+
/** Exact bot-token scopes used by each semantic operation. */
|
|
10
|
+
export const SLACK_CHANNEL_OPERATION_REQUIRED_BOT_SCOPES = {
|
|
11
|
+
resolve_dm_by_email: ["users:read", "users:read.email", "im:write"],
|
|
12
|
+
post_message: ["chat:write"]
|
|
13
|
+
};
|
|
14
|
+
/** Typed authored-tool entrypoint for the current Slack channel capability. */
|
|
15
|
+
export async function callSlackChannelOperation(ctx, operation, input) {
|
|
16
|
+
if (!ctx.channel?.perform) {
|
|
17
|
+
throw new Error("Slack operations require a host-owned Slack channel capability.");
|
|
18
|
+
}
|
|
19
|
+
return await ctx.channel.perform(operation, input);
|
|
20
|
+
}
|
|
21
|
+
/** Runtime dispatcher. Only the named semantic operations can reach Slack. */
|
|
22
|
+
export async function executeSlackChannelOperation(operation, input, turn, ctx) {
|
|
23
|
+
if (operation === "resolve_dm_by_email") {
|
|
24
|
+
return resolveSlackDmByEmail(validateResolveDmInput(input), turn, ctx);
|
|
25
|
+
}
|
|
26
|
+
if (operation === "post_message") {
|
|
27
|
+
return postSlackMessage(validatePostMessageInput(input), turn, ctx);
|
|
28
|
+
}
|
|
29
|
+
throw new Error(`Unsupported Slack channel operation: ${operation}.`);
|
|
30
|
+
}
|
|
31
|
+
async function resolveSlackDmByEmail(input, turn, ctx) {
|
|
32
|
+
const teamId = input.teamId ?? slackTeamIdForTurn(turn);
|
|
33
|
+
let lookup;
|
|
34
|
+
try {
|
|
35
|
+
lookup = await slackApi(ctx, "users.lookupByEmail", { email: input.email }, teamId);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
throw slackPhaseError(`Slack direct-message user lookup failed for ${input.email}`, error);
|
|
39
|
+
}
|
|
40
|
+
const userId = stringValue(recordValue(lookup.user)?.id);
|
|
41
|
+
if (!userId || !SLACK_USER_ID.test(userId)) {
|
|
42
|
+
throw new Error(`Slack direct-message user lookup returned no user id for ${input.email}.`);
|
|
43
|
+
}
|
|
44
|
+
let opened;
|
|
45
|
+
try {
|
|
46
|
+
opened = await slackApi(ctx, "conversations.open", { users: userId }, teamId);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
throw slackPhaseError(`Slack direct-message open failed for user ${userId}`, error);
|
|
50
|
+
}
|
|
51
|
+
const channelId = stringValue(recordValue(opened.channel)?.id);
|
|
52
|
+
if (!channelId || !SLACK_DM_ID.test(channelId)) {
|
|
53
|
+
throw new Error(`Slack direct-message open returned no valid channel id for user ${userId}.`);
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
ok: true,
|
|
57
|
+
userId,
|
|
58
|
+
channelId,
|
|
59
|
+
...(teamId ? { teamId } : {})
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
async function postSlackMessage(input, turn, ctx) {
|
|
63
|
+
const teamId = input.teamId ?? slackTeamIdForTurn(turn);
|
|
64
|
+
const results = await postSlackMarkdown(input.text, input.channel, input.threadTs, ctx, teamId, {
|
|
65
|
+
idempotencyKey: input.idempotencyKey,
|
|
66
|
+
...(input.unfurlLinks !== undefined ? { unfurlLinks: input.unfurlLinks } : {}),
|
|
67
|
+
...(input.unfurlMedia !== undefined ? { unfurlMedia: input.unfurlMedia } : {})
|
|
68
|
+
});
|
|
69
|
+
return {
|
|
70
|
+
...slackTextDeliveryMetadata(input.channel, input.threadTs, input.idempotencyKey, results),
|
|
71
|
+
ok: true
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function validateResolveDmInput(input) {
|
|
75
|
+
assertAllowedKeys(input, new Set(["email", "teamId"]), "resolve_dm_by_email");
|
|
76
|
+
const email = requiredString(input.email, "Slack resolve_dm_by_email requires email.");
|
|
77
|
+
if (!email.includes("@"))
|
|
78
|
+
throw new Error("Slack resolve_dm_by_email requires a valid email address.");
|
|
79
|
+
const teamId = optionalTeamId(input.teamId);
|
|
80
|
+
return { email, ...(teamId ? { teamId } : {}) };
|
|
81
|
+
}
|
|
82
|
+
function validatePostMessageInput(input) {
|
|
83
|
+
assertAllowedKeys(input, new Set(["channel", "text", "idempotencyKey", "teamId", "threadTs", "unfurlLinks", "unfurlMedia"]), "post_message");
|
|
84
|
+
const channel = requiredString(input.channel, "Slack post_message requires channel.").toUpperCase();
|
|
85
|
+
if (!SLACK_CONVERSATION_ID.test(channel)) {
|
|
86
|
+
throw new Error("Slack post_message requires a channel id beginning with C, D, or G.");
|
|
87
|
+
}
|
|
88
|
+
const text = requiredString(input.text, "Slack post_message requires non-empty text.", false);
|
|
89
|
+
const idempotencyKey = requiredString(input.idempotencyKey, "Slack post_message requires idempotencyKey.");
|
|
90
|
+
const teamId = optionalTeamId(input.teamId);
|
|
91
|
+
const threadTs = optionalString(input.threadTs);
|
|
92
|
+
if (threadTs && !SLACK_TIMESTAMP.test(threadTs)) {
|
|
93
|
+
throw new Error("Slack post_message threadTs must be a Slack timestamp.");
|
|
94
|
+
}
|
|
95
|
+
if (input.unfurlLinks !== undefined && typeof input.unfurlLinks !== "boolean") {
|
|
96
|
+
throw new Error("Slack post_message unfurlLinks must be a boolean.");
|
|
97
|
+
}
|
|
98
|
+
if (input.unfurlMedia !== undefined && typeof input.unfurlMedia !== "boolean") {
|
|
99
|
+
throw new Error("Slack post_message unfurlMedia must be a boolean.");
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
channel,
|
|
103
|
+
text,
|
|
104
|
+
idempotencyKey,
|
|
105
|
+
...(teamId ? { teamId } : {}),
|
|
106
|
+
...(threadTs ? { threadTs } : {}),
|
|
107
|
+
...(typeof input.unfurlLinks === "boolean" ? { unfurlLinks: input.unfurlLinks } : {}),
|
|
108
|
+
...(typeof input.unfurlMedia === "boolean" ? { unfurlMedia: input.unfurlMedia } : {})
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function slackTeamIdForTurn(turn) {
|
|
112
|
+
const slack = recordValue(turn.metadata?.slack);
|
|
113
|
+
const delivery = recordValue(turn.delivery);
|
|
114
|
+
return stringValue(slack?.teamId) ?? stringValue(delivery?.teamId);
|
|
115
|
+
}
|
|
116
|
+
function optionalTeamId(value) {
|
|
117
|
+
const teamId = optionalString(value)?.toUpperCase();
|
|
118
|
+
if (teamId && !SLACK_TEAM_ID.test(teamId)) {
|
|
119
|
+
throw new Error("Slack teamId must begin with T.");
|
|
120
|
+
}
|
|
121
|
+
return teamId;
|
|
122
|
+
}
|
|
123
|
+
function requiredString(value, message, trim = true) {
|
|
124
|
+
if (typeof value !== "string" || !value.trim())
|
|
125
|
+
throw new Error(message);
|
|
126
|
+
return trim ? value.trim() : value;
|
|
127
|
+
}
|
|
128
|
+
function optionalString(value) {
|
|
129
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
130
|
+
}
|
|
131
|
+
function assertAllowedKeys(input, allowed, operation) {
|
|
132
|
+
const unsupported = Object.keys(input).filter((key) => !allowed.has(key));
|
|
133
|
+
if (unsupported.length > 0) {
|
|
134
|
+
throw new Error(`Slack ${operation} does not accept: ${unsupported.join(", ")}.`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function recordValue(value) {
|
|
138
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
139
|
+
? value
|
|
140
|
+
: undefined;
|
|
141
|
+
}
|
|
142
|
+
function stringValue(value) {
|
|
143
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=channel-operations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel-operations.js","sourceRoot":"","sources":["../src/channel-operations.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { JsonObject } from "@assemblyline-agents/core";
|
|
2
|
+
import type { ChannelContext, ChannelTurn } from "@assemblyline-agents/runtime";
|
|
3
|
+
/** Fetch a private Slack file inside the trusted channel boundary. */
|
|
4
|
+
export declare function retrieveSlackFile(input: JsonObject, turn: ChannelTurn, ctx: ChannelContext): Promise<JsonObject>;
|
|
5
|
+
//# sourceMappingURL=file-retrieval.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-retrieval.d.ts","sourceRoot":"","sources":["../src/file-retrieval.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAMhF,sEAAsE;AACtE,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,UAAU,CAAC,CAmCtH"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { slackWorkspaceCredentials } from "./workspace-credentials.js";
|
|
2
|
+
const MAX_FILE_BYTES = 50 * 1024 * 1024;
|
|
3
|
+
const SLACK_FILE_ID = /^F[A-Z0-9]+$/u;
|
|
4
|
+
/** Fetch a private Slack file inside the trusted channel boundary. */
|
|
5
|
+
export async function retrieveSlackFile(input, turn, ctx) {
|
|
6
|
+
const fileId = stringValue(input.fileId)?.toUpperCase();
|
|
7
|
+
if (!fileId || !SLACK_FILE_ID.test(fileId))
|
|
8
|
+
throw new Error("Slack file retrieval requires a valid file id beginning with F.");
|
|
9
|
+
const teamId = stringValue(input.teamId) ?? slackTeamId(turn.metadata);
|
|
10
|
+
const token = slackWorkspaceCredentials(ctx.env, teamId).botToken?.trim();
|
|
11
|
+
if (!token)
|
|
12
|
+
return failure(fileId, "slack_file_read_not_configured", "No Slack file-read credential is configured for this workspace.");
|
|
13
|
+
const infoResponse = await ctx.fetch(`https://slack.com/api/files.info?file=${encodeURIComponent(fileId)}`, {
|
|
14
|
+
headers: { authorization: `Bearer ${token}` },
|
|
15
|
+
signal: AbortSignal.timeout(30_000)
|
|
16
|
+
});
|
|
17
|
+
const info = await infoResponse.json().catch(() => ({}));
|
|
18
|
+
if (!infoResponse.ok || info.ok !== true || !isRecord(info.file)) {
|
|
19
|
+
const detail = typeof info.error === "string" ? info.error : `HTTP ${infoResponse.status}`;
|
|
20
|
+
return failure(fileId, "slack_file_lookup_failed", `Slack could not resolve ${fileId}: ${detail}.`);
|
|
21
|
+
}
|
|
22
|
+
const file = info.file;
|
|
23
|
+
const downloadUrl = firstString(file.url_private_download, file.url_private);
|
|
24
|
+
if (!downloadUrl || !isAllowedSlackFileUrl(downloadUrl))
|
|
25
|
+
return failure(fileId, "slack_file_download_url_missing", "Slack returned no allowed private download URL.");
|
|
26
|
+
const announcedSize = finitePositiveNumber(file.size);
|
|
27
|
+
if (announcedSize !== undefined && announcedSize > MAX_FILE_BYTES)
|
|
28
|
+
throw new Error(`Slack file ${fileId} is above the 50 MB retrieval limit.`);
|
|
29
|
+
const download = await ctx.fetch(downloadUrl, {
|
|
30
|
+
headers: { authorization: `Bearer ${token}` },
|
|
31
|
+
redirect: "error",
|
|
32
|
+
signal: AbortSignal.timeout(90_000)
|
|
33
|
+
});
|
|
34
|
+
if (!download.ok)
|
|
35
|
+
return failure(fileId, "slack_file_download_failed", `Slack file download failed with HTTP ${download.status}.`);
|
|
36
|
+
const bytes = await boundedBytes(download, MAX_FILE_BYTES);
|
|
37
|
+
return {
|
|
38
|
+
ok: true,
|
|
39
|
+
fileId,
|
|
40
|
+
filename: safeFilename(stringValue(input.filename)) ?? safeFilename(firstString(file.name, file.title)) ?? `${fileId}.bin`,
|
|
41
|
+
bytesBase64: Buffer.from(bytes).toString("base64"),
|
|
42
|
+
sizeBytes: bytes.byteLength,
|
|
43
|
+
contentType: download.headers.get("content-type") ?? "application/octet-stream"
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function failure(fileId, error, message) { return { ok: false, fileId, error, message }; }
|
|
47
|
+
function stringValue(value) { return typeof value === "string" && value.trim() ? value.trim() : undefined; }
|
|
48
|
+
function slackTeamId(metadata) { const slack = isRecord(metadata?.slack) ? metadata.slack : undefined; return stringValue(slack?.teamId); }
|
|
49
|
+
function firstString(...values) { return values.find((value) => typeof value === "string" && value.trim() !== "")?.trim(); }
|
|
50
|
+
function finitePositiveNumber(value) { return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined; }
|
|
51
|
+
function isRecord(value) { return Boolean(value) && typeof value === "object" && !Array.isArray(value); }
|
|
52
|
+
function safeFilename(value) {
|
|
53
|
+
if (!value)
|
|
54
|
+
return undefined;
|
|
55
|
+
const cleaned = value.trim().replace(/[\\/\u0000-\u001f\u007f]+/gu, "-").replace(/[^a-z0-9._() -]+/giu, "-").replace(/\s+/gu, " ").replace(/^-+|-+$/gu, "").slice(0, 160);
|
|
56
|
+
return cleaned && cleaned !== "." && cleaned !== ".." ? cleaned : undefined;
|
|
57
|
+
}
|
|
58
|
+
function isAllowedSlackFileUrl(value) {
|
|
59
|
+
try {
|
|
60
|
+
const url = new URL(value);
|
|
61
|
+
const hostname = url.hostname.toLowerCase();
|
|
62
|
+
return url.protocol === "https:" && (hostname === "files.slack.com" || hostname.endsWith(".slack.com"));
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function boundedBytes(response, maxBytes) {
|
|
69
|
+
const declared = Number(response.headers.get("content-length"));
|
|
70
|
+
if (Number.isFinite(declared) && declared > maxBytes)
|
|
71
|
+
throw new Error("The Slack file exceeded the 50 MB retrieval limit.");
|
|
72
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
73
|
+
if (bytes.byteLength > maxBytes)
|
|
74
|
+
throw new Error("The Slack file exceeded the 50 MB retrieval limit.");
|
|
75
|
+
return bytes;
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=file-retrieval.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-retrieval.js","sourceRoot":"","sources":["../src/file-retrieval.ts"],"names":[],"mappings":"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"}
|