experimental-ash 0.10.4 → 0.11.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/CHANGELOG.md +63 -0
- package/README.md +1 -1
- package/dist/docs/public/channels/README.md +26 -22
- package/dist/docs/public/channels/slack.md +57 -65
- package/dist/docs/public/connections.md +26 -25
- package/dist/docs/public/project-layout.md +1 -1
- package/dist/docs/public/session-context.md +14 -12
- package/dist/docs/public/typescript-api.md +5 -4
- package/dist/src/internal/application/package.js +1 -1
- package/dist/src/public/channels/slack/defaults.d.ts +28 -0
- package/dist/src/public/channels/slack/defaults.js +223 -0
- package/dist/src/public/channels/slack/index.d.ts +1 -2
- package/dist/src/public/channels/slack/index.js +0 -1
- package/dist/src/public/channels/slack/slackChannel.d.ts +37 -21
- package/dist/src/public/channels/slack/slackChannel.js +29 -37
- package/package.json +1 -1
- package/dist/src/public/channels/slack/slack.d.ts +0 -20
- package/dist/src/public/channels/slack/slack.js +0 -200
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,68 @@
|
|
|
1
1
|
# experimental-ash
|
|
2
2
|
|
|
3
|
+
## 0.11.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- c823956: refactor(slack): collapse `slack` and `slackChannel` into one `slackChannel()`; merge `run` + `onMention` into a single mention hook
|
|
8
|
+
|
|
9
|
+
The previous `slack` (defaults wrapper) and `slackChannel` (raw config) factories are unified behind a single `slackChannel(config?)` exported from `experimental-ash/channels/slack`. Defaults always apply; any field you supply replaces the corresponding default. Events you don't override keep their defaults. The default `onMention` derives a workspace-scoped auth from the inbound Slack actor and posts a `"Thinking…"` typing indicator before the workflow runtime cold-starts.
|
|
10
|
+
|
|
11
|
+
The separate `run` (gate + auth) and `onMention` (pre-dispatch side effects) hooks are merged into a single `onMention(ctx, message)` callback. Return `{ auth }` to dispatch, `null` to drop, and perform any side effects inline. Errors thrown from `onMention` are caught, logged, and drop the mention — previously side-effect throws were logged but dispatch continued. Wrap best-effort side effects in `try/catch` if you want them to be non-fatal.
|
|
12
|
+
|
|
13
|
+
Migration:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
// before — slackChannel with separate run + onMention
|
|
17
|
+
import { slackChannel } from "experimental-ash/channels/slack";
|
|
18
|
+
|
|
19
|
+
export default slackChannel({
|
|
20
|
+
run(ctx, message) {
|
|
21
|
+
if (!ALLOWED.has(ctx.slack.channelId)) return null;
|
|
22
|
+
return {
|
|
23
|
+
auth: {
|
|
24
|
+
/* ... */
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
onMention(ctx) {
|
|
29
|
+
ctx.thread.startTyping("Thinking…");
|
|
30
|
+
},
|
|
31
|
+
events: {
|
|
32
|
+
/* ... */
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// before — slack() with defaults
|
|
37
|
+
import { slack } from "experimental-ash/channels/slack";
|
|
38
|
+
|
|
39
|
+
export default slack({
|
|
40
|
+
events: {
|
|
41
|
+
/* ... */
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// after — one factory, one onMention hook
|
|
46
|
+
import { slackChannel } from "experimental-ash/channels/slack";
|
|
47
|
+
|
|
48
|
+
export default slackChannel({
|
|
49
|
+
onMention(ctx, message) {
|
|
50
|
+
if (!ALLOWED.has(ctx.slack.channelId)) return null;
|
|
51
|
+
ctx.thread.startTyping("Thinking…");
|
|
52
|
+
return {
|
|
53
|
+
auth: {
|
|
54
|
+
/* ... */
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
events: {
|
|
59
|
+
/* ... */
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Drops the `SlackOptions` type and the `run` / `onMention` split on `SlackChannelConfig`. Adds `SlackMentionResult` (`{ auth } | null`) and `SlackMentionResultOrPromise`.
|
|
65
|
+
|
|
3
66
|
## 0.10.4
|
|
4
67
|
|
|
5
68
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ Every authored directory has a typed helper. Import each from the matching subpa
|
|
|
49
49
|
| `defineSkill(...)`, `getSkill(...)` | `experimental-ash/skills` | `skills/<name>.ts` (or `skills/<name>.md`) |
|
|
50
50
|
| `defineHook(...)` | `experimental-ash/hooks` | `hooks/<slug>.ts` |
|
|
51
51
|
| `defineChannel(...)`, `POST`, `GET` | `experimental-ash/channels` | `channels/<name>.ts` |
|
|
52
|
-
| `ashChannel(...)`, `
|
|
52
|
+
| `ashChannel(...)`, `slackChannel(...)`, `vercelOidc(...)` | `experimental-ash/channels/ash`, `/slack`, `/auth` | reused from `channels/<name>.ts` |
|
|
53
53
|
| `defineSandbox(...)` | `experimental-ash/sandbox` | `sandbox.ts` (or `sandbox/sandbox.ts`) |
|
|
54
54
|
| `defineSchedule(...)` | `experimental-ash/schedules` | `schedules/<name>.ts` (or `schedules/<name>.md`) |
|
|
55
55
|
| `defineEvalSuite(...)` | `experimental-ash/evals` | `evals/<name>.eval.ts` |
|
|
@@ -109,29 +109,33 @@ export default ashChannel({
|
|
|
109
109
|
|
|
110
110
|
## Slack Channels
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
Slack channels are authored with a single `slackChannel()` factory. Pass no config for the
|
|
113
|
+
zero-config defaults, or supply only the fields you want to override -- everything else keeps the
|
|
114
|
+
default.
|
|
113
115
|
|
|
114
|
-
###
|
|
116
|
+
### Zero-config
|
|
115
117
|
|
|
116
118
|
```ts
|
|
117
|
-
import {
|
|
119
|
+
import { slackChannel } from "experimental-ash/channels/slack";
|
|
118
120
|
|
|
119
|
-
export default
|
|
121
|
+
export default slackChannel();
|
|
120
122
|
```
|
|
121
123
|
|
|
122
|
-
Handles mentions, typing indicators, message delivery, and HITL interactions out of the box.
|
|
124
|
+
Handles mentions, typing indicators, message delivery, and HITL interactions out of the box. The
|
|
125
|
+
default `onMention` derives auth from the inbound Slack actor and posts a `"Thinking…"` typing
|
|
126
|
+
indicator before the workflow runtime starts.
|
|
123
127
|
|
|
124
|
-
###
|
|
128
|
+
### Custom config
|
|
125
129
|
|
|
126
|
-
When you need custom rendering or event handling,
|
|
127
|
-
|
|
128
|
-
|
|
130
|
+
When you need custom rendering or event handling, pass a config object. `onMention` decides whether
|
|
131
|
+
to dispatch and with what auth; `events` lets you replace individual event handlers; `onInteraction`
|
|
132
|
+
handles non-HITL button clicks.
|
|
129
133
|
|
|
130
134
|
```ts
|
|
131
135
|
import { slackChannel } from "experimental-ash/channels/slack";
|
|
132
136
|
|
|
133
137
|
export default slackChannel({
|
|
134
|
-
|
|
138
|
+
onMention(ctx, message) {
|
|
135
139
|
return {
|
|
136
140
|
auth: {
|
|
137
141
|
principalId: message.author.userId,
|
|
@@ -157,12 +161,16 @@ export default slackChannel({
|
|
|
157
161
|
});
|
|
158
162
|
```
|
|
159
163
|
|
|
164
|
+
Fields you supply fully replace the corresponding default -- if you pass your own
|
|
165
|
+
`"message.completed"` handler, the default's behavior for that event is gone. Other defaults stay
|
|
166
|
+
in place.
|
|
167
|
+
|
|
160
168
|
### Interactions
|
|
161
169
|
|
|
162
170
|
Interactions (button clicks, modals) are platform events, not agent events. They do not appear in the
|
|
163
171
|
event handlers.
|
|
164
172
|
|
|
165
|
-
`slackChannel` handles interactions
|
|
173
|
+
`slackChannel()` handles interactions in two paths:
|
|
166
174
|
|
|
167
175
|
- **HITL interactions** (approval buttons matching pending input requests) are delivered to the agent
|
|
168
176
|
automatically. The agent resumes.
|
|
@@ -172,7 +180,7 @@ event handlers.
|
|
|
172
180
|
import { slackChannel } from "experimental-ash/channels/slack";
|
|
173
181
|
|
|
174
182
|
export default slackChannel({
|
|
175
|
-
|
|
183
|
+
onMention(ctx, message) {
|
|
176
184
|
return {
|
|
177
185
|
auth: {
|
|
178
186
|
principalId: message.author.userId,
|
|
@@ -194,21 +202,17 @@ export default slackChannel({
|
|
|
194
202
|
});
|
|
195
203
|
```
|
|
196
204
|
|
|
197
|
-
### The
|
|
205
|
+
### The two shapes
|
|
198
206
|
|
|
199
207
|
```
|
|
200
|
-
defineChannel({ routes: [...], events: {...} })
|
|
201
|
-
slackChannel({
|
|
202
|
-
slack() -- default: everything handled
|
|
208
|
+
defineChannel({ routes: [...], events: {...} }) -- raw: you handle HTTP
|
|
209
|
+
slackChannel({ onMention?, events?, onInteraction? }) -- slack: everything wired, override as needed
|
|
203
210
|
```
|
|
204
211
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
- `slack()` -> `slackChannel(defaultConfig)`
|
|
208
|
-
- `slackChannel(config)` -> `defineChannel` + Chat SDK setup + typed event dispatch
|
|
209
|
-
- `defineChannel` -> the primitive
|
|
212
|
+
`slackChannel(config)` compiles down to `defineChannel` plus the Chat SDK setup and typed event
|
|
213
|
+
dispatch.
|
|
210
214
|
|
|
211
|
-
For a Slack app backed by Vercel
|
|
215
|
+
For a Slack app backed by Vercel Connect, see [Slack channel setup](./slack.md) to create the Connect client
|
|
212
216
|
and channel file.
|
|
213
217
|
|
|
214
218
|
## File Uploads
|
|
@@ -1,38 +1,38 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "Slack channel setup"
|
|
3
|
-
description: "Create a Slack-backed Ash channel with Vercel
|
|
3
|
+
description: "Create a Slack-backed Ash channel with Vercel Connect."
|
|
4
4
|
type: integration
|
|
5
5
|
related:
|
|
6
6
|
- /channels
|
|
7
7
|
- /connections
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
Ash Slack channels can use Vercel
|
|
11
|
-
verification. With
|
|
10
|
+
Ash Slack channels can use Vercel Connect for both outbound Slack bot tokens and inbound webhook
|
|
11
|
+
verification. With Connect, you do not need to manage `SLACK_BOT_TOKEN` or `SLACK_SIGNING_SECRET`
|
|
12
12
|
environment variables yourself.
|
|
13
13
|
|
|
14
14
|
## Prerequisites
|
|
15
15
|
|
|
16
16
|
- A Vercel project for the agent.
|
|
17
17
|
- A Slack workspace where you can install the app.
|
|
18
|
-
- Access to Vercel
|
|
18
|
+
- Access to Vercel Connect for your team.
|
|
19
19
|
|
|
20
|
-
For local development, link the project and pull Vercel OIDC env vars so
|
|
21
|
-
the Vercel API:
|
|
20
|
+
For local development, link the project and pull Vercel OIDC env vars so Connect can authenticate
|
|
21
|
+
to the Vercel API:
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
24
|
vercel link
|
|
25
25
|
vercel env pull
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
## Create The
|
|
28
|
+
## Create The Connect Client
|
|
29
29
|
|
|
30
|
-
Create a Slack
|
|
30
|
+
Create a Slack Connect client, then copy its UID. The UID is the value you pass to Ash, for example
|
|
31
31
|
`slack/my-agent`.
|
|
32
32
|
|
|
33
33
|
### Dashboard Path
|
|
34
34
|
|
|
35
|
-
Open [
|
|
35
|
+
Open [Connect in the Vercel dashboard](https://vercel.com/d?to=/%5Bteam%5D/~/connect&title=Go+to+Connect),
|
|
36
36
|
then:
|
|
37
37
|
|
|
38
38
|
1. Choose **Create Client**.
|
|
@@ -44,20 +44,20 @@ then:
|
|
|
44
44
|
|
|
45
45
|
### CLI Or Agent-Assisted Path
|
|
46
46
|
|
|
47
|
-
The `vercel
|
|
48
|
-
update the Vercel CLI and enable the
|
|
47
|
+
The `vercel connect` command is currently gated for allow-listed teams. If the command is missing,
|
|
48
|
+
update the Vercel CLI and enable the Connect flag in the terminal session where you will run the
|
|
49
49
|
commands:
|
|
50
50
|
|
|
51
51
|
```bash
|
|
52
52
|
pnpm i -g vercel@latest
|
|
53
|
-
export
|
|
54
|
-
vercel
|
|
53
|
+
export FF_CONNECT_ENABLED=1
|
|
54
|
+
vercel connect --help
|
|
55
55
|
```
|
|
56
56
|
|
|
57
57
|
The flag only applies to the current shell session. Export it again in new terminals, or add it to
|
|
58
|
-
your shell config while you are working with
|
|
58
|
+
your shell config while you are working with Connect.
|
|
59
59
|
|
|
60
|
-
Make sure the CLI is authenticated and scoped to the Vercel team that has
|
|
60
|
+
Make sure the CLI is authenticated and scoped to the Vercel team that has Connect access:
|
|
61
61
|
|
|
62
62
|
```bash
|
|
63
63
|
vercel login
|
|
@@ -65,34 +65,34 @@ vercel switch
|
|
|
65
65
|
vercel link
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
-
If you are using an AI coding agent, install the
|
|
68
|
+
If you are using an AI coding agent, install the Connect skill first:
|
|
69
69
|
|
|
70
70
|
```bash
|
|
71
|
-
npx skills add https://github.com/vercel/
|
|
71
|
+
npx skills add https://github.com/vercel/connect --skill vercel-connect
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
Then create the Slack client from the project or agent folder that will use it:
|
|
75
75
|
|
|
76
76
|
```bash
|
|
77
|
-
vercel
|
|
77
|
+
vercel connect create slack
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
Run
|
|
81
|
-
|
|
82
|
-
open a browser for Slack installation or OAuth consent; finish that flow before continuing.
|
|
80
|
+
Run Connect commands from the directory containing the agent's `package.json` or `vercel.json`.
|
|
81
|
+
Connect uses that project context to configure project access, webhooks, and triggers. The command
|
|
82
|
+
may open a browser for Slack installation or OAuth consent; finish that flow before continuing.
|
|
83
83
|
|
|
84
84
|
You can list existing clients with:
|
|
85
85
|
|
|
86
86
|
```bash
|
|
87
|
-
vercel
|
|
87
|
+
vercel connect list
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
-
## Install
|
|
90
|
+
## Install Connect
|
|
91
91
|
|
|
92
|
-
Add the
|
|
92
|
+
Add the Connect SDK to the Ash project:
|
|
93
93
|
|
|
94
94
|
```bash
|
|
95
|
-
pnpm add @vercel/
|
|
95
|
+
pnpm add @vercel/connect
|
|
96
96
|
```
|
|
97
97
|
|
|
98
98
|
## Add The Slack Channel File
|
|
@@ -100,35 +100,36 @@ pnpm add @vercel/connex
|
|
|
100
100
|
Create `agent/channels/slack.ts`:
|
|
101
101
|
|
|
102
102
|
```ts
|
|
103
|
-
import {
|
|
104
|
-
import {
|
|
103
|
+
import { connectSlackCredentials } from "@vercel/connect/ash";
|
|
104
|
+
import { slackChannel } from "experimental-ash/channels/slack";
|
|
105
105
|
|
|
106
|
-
export default
|
|
107
|
-
credentials:
|
|
106
|
+
export default slackChannel({
|
|
107
|
+
credentials: connectSlackCredentials("slack/my-agent"),
|
|
108
108
|
});
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
-
Replace `slack/my-agent` with the UID from the
|
|
111
|
+
Replace `slack/my-agent` with the UID from the Connect client.
|
|
112
112
|
|
|
113
113
|
The helper returns a complete Slack credentials object:
|
|
114
114
|
|
|
115
|
-
- `botToken` resolves an app-scoped Slack token through
|
|
116
|
-
- `webhookVerifier` verifies
|
|
115
|
+
- `botToken` resolves an app-scoped Slack token through Connect for outbound posts.
|
|
116
|
+
- `webhookVerifier` verifies Connect-forwarded Slack webhooks with Vercel OIDC.
|
|
117
117
|
|
|
118
|
-
That means token rotation, refresh, multi-workspace tenancy, and inbound request verification stay
|
|
119
|
-
|
|
118
|
+
That means token rotation, refresh, multi-workspace tenancy, and inbound request verification stay
|
|
119
|
+
in Connect instead of in your app environment.
|
|
120
120
|
|
|
121
121
|
## Customize Delivery
|
|
122
122
|
|
|
123
|
-
If you need custom Slack rendering or interaction handling,
|
|
123
|
+
If you need custom Slack rendering or interaction handling, pass a config object to `slackChannel()`.
|
|
124
|
+
Any field you supply replaces the corresponding default; fields you omit keep the defaults.
|
|
124
125
|
|
|
125
126
|
```ts
|
|
126
|
-
import {
|
|
127
|
+
import { connectSlackCredentials } from "@vercel/connect/ash";
|
|
127
128
|
import { slackChannel } from "experimental-ash/channels/slack";
|
|
128
129
|
|
|
129
130
|
export default slackChannel({
|
|
130
|
-
credentials:
|
|
131
|
-
|
|
131
|
+
credentials: connectSlackCredentials("slack/my-agent"),
|
|
132
|
+
onMention(ctx, message) {
|
|
132
133
|
return {
|
|
133
134
|
auth: {
|
|
134
135
|
principalId: message.author.userId,
|
|
@@ -152,15 +153,15 @@ export default slackChannel({
|
|
|
152
153
|
|
|
153
154
|
## Hooks
|
|
154
155
|
|
|
155
|
-
`slackChannel` exposes
|
|
156
|
+
`slackChannel()` exposes two places to plug in custom behavior on the inbound webhook side:
|
|
156
157
|
|
|
157
|
-
- **`
|
|
158
|
-
with what `auth` context
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
158
|
+
- **`onMention(ctx, message)`** -- decides whether to dispatch a turn for an inbound `app_mention`,
|
|
159
|
+
with what `auth` context, and runs any pre-dispatch side effects (typing indicators, logging,
|
|
160
|
+
feature-flag lookups). Return `{ auth }` to dispatch or `null` to silently drop the mention. May
|
|
161
|
+
be sync or async; the framework awaits the result before dispatching. Thrown errors are caught,
|
|
162
|
+
logged, and drop the mention -- wrap best-effort side effects in `try/catch` if you want them to
|
|
163
|
+
be non-fatal. The default `onMention` derives a workspace-scoped auth from the Slack actor and
|
|
164
|
+
posts a `"Thinking…"` typing indicator.
|
|
164
165
|
- **`onInteraction(action, ctx)`** -- handler for Slack `block_actions` callbacks (button clicks,
|
|
165
166
|
selects, etc.) that are not consumed by the framework's HITL pipeline. Runs on the inbound
|
|
166
167
|
webhook side via `waitUntil`, so the channel returns `200 OK` to Slack immediately.
|
|
@@ -180,11 +181,11 @@ connection pool:
|
|
|
180
181
|
|
|
181
182
|
```ts
|
|
182
183
|
import { createRedisState } from "@chat-adapter/state-redis";
|
|
183
|
-
import {
|
|
184
|
+
import { slackChannel } from "experimental-ash/channels/slack";
|
|
184
185
|
|
|
185
186
|
const stateAdapter = createRedisState({ url: process.env.REDIS_URL! });
|
|
186
187
|
|
|
187
|
-
export default
|
|
188
|
+
export default slackChannel({
|
|
188
189
|
stateAdapter,
|
|
189
190
|
});
|
|
190
191
|
```
|
|
@@ -194,8 +195,8 @@ so state stays coherent within a single agent run.
|
|
|
194
195
|
|
|
195
196
|
## Typing Indicators
|
|
196
197
|
|
|
197
|
-
Out of the box, `
|
|
198
|
-
thinking:
|
|
198
|
+
Out of the box, `slackChannel()` posts typing statuses so the user sees feedback before the agent
|
|
199
|
+
starts thinking:
|
|
199
200
|
|
|
200
201
|
- **`Thinking...`** -- posted by the default `onMention` hook the moment a mention arrives, on the
|
|
201
202
|
inbound webhook side, before the workflow runtime starts.
|
|
@@ -204,14 +205,17 @@ thinking:
|
|
|
204
205
|
- **Tool status** -- when an `actions.requested` event fires, the default handler updates the typing
|
|
205
206
|
indicator to show which tools are running.
|
|
206
207
|
|
|
207
|
-
To customize typing indicators,
|
|
208
|
-
|
|
208
|
+
To customize typing indicators, override `onMention` and/or specific event handlers. Replacing one
|
|
209
|
+
handler does not affect the others -- the rest keep their defaults.
|
|
209
210
|
|
|
210
211
|
```ts
|
|
211
212
|
import { slackChannel } from "experimental-ash/channels/slack";
|
|
212
213
|
|
|
213
214
|
export default slackChannel({
|
|
214
|
-
|
|
215
|
+
async onMention(ctx, message) {
|
|
216
|
+
await ctx.thread.startTyping(
|
|
217
|
+
message.text.includes("weather") ? "Checking the weather..." : "Thinking...",
|
|
218
|
+
);
|
|
215
219
|
return {
|
|
216
220
|
auth: {
|
|
217
221
|
principalId: message.author.userId,
|
|
@@ -221,23 +225,11 @@ export default slackChannel({
|
|
|
221
225
|
},
|
|
222
226
|
};
|
|
223
227
|
},
|
|
224
|
-
async onMention(ctx, message) {
|
|
225
|
-
await ctx.thread.startTyping(
|
|
226
|
-
message.text.includes("weather") ? "Checking the weather..." : "Thinking...",
|
|
227
|
-
);
|
|
228
|
-
},
|
|
229
228
|
events: {
|
|
230
229
|
async "actions.requested"(event, ctx) {
|
|
231
230
|
const labels = event.actions.map((a) => (a.kind === "tool-call" ? a.toolName : a.kind));
|
|
232
231
|
await ctx.thread.startTyping(`Running ${labels.join(", ")}...`);
|
|
233
232
|
},
|
|
234
|
-
async "message.completed"(event, ctx) {
|
|
235
|
-
if (event.finishReason === "tool-calls") return;
|
|
236
|
-
if (event.message) await ctx.thread.post(event.message);
|
|
237
|
-
},
|
|
238
|
-
async "session.failed"(_event, ctx) {
|
|
239
|
-
await ctx.thread.post("Something went wrong.");
|
|
240
|
-
},
|
|
241
233
|
},
|
|
242
234
|
});
|
|
243
235
|
```
|
|
@@ -15,11 +15,11 @@ The model sees each connection through `connection_search` and picks tools based
|
|
|
15
15
|
Every connection needs to decide how Ash obtains a bearer token for the MCP server. Pick one before
|
|
16
16
|
you write the file — the rest of the shape is the same:
|
|
17
17
|
|
|
18
|
-
| Strategy
|
|
19
|
-
|
|
|
20
|
-
| [Static token](#static-tokens)
|
|
21
|
-
| [
|
|
22
|
-
| [No auth](#no-auth)
|
|
18
|
+
| Strategy | Use it when |
|
|
19
|
+
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
20
|
+
| [Static token](#static-tokens) | You already have an API key or pre-provisioned JWT (env var, secrets manager, vault). |
|
|
21
|
+
| [Connect-managed OAuth](#oauth-via-connect) | The server uses OAuth and you want Vercel to own consent, token storage, and refresh. |
|
|
22
|
+
| [No auth](#no-auth) | The server doesn't require a token — localhost, public servers, or servers already authenticated through [`headers`](#headers). |
|
|
23
23
|
|
|
24
24
|
If you need something the table doesn't cover (BYO token vault, custom signing flow, on-demand JWT
|
|
25
25
|
exchange), use the static-token shape with whatever async logic you need inside `getToken`.
|
|
@@ -51,27 +51,27 @@ Ash defaults static `getToken`-only auth to `principalType: "app"`, which keys t
|
|
|
51
51
|
cache by `"app"` so all sessions share the same credential. Set `principalType: "user"` when each
|
|
52
52
|
end-user has their own token (see [Principal Type](#principal-type-user-vs-app)).
|
|
53
53
|
|
|
54
|
-
## OAuth via
|
|
54
|
+
## OAuth via Connect
|
|
55
55
|
|
|
56
|
-
For OAuth-backed connections, [Vercel
|
|
57
|
-
flow, encrypt token storage, and handle refresh. The `
|
|
58
|
-
collapses that into a single declaration and wires
|
|
56
|
+
For OAuth-backed connections, [Vercel Connect](https://vercel.com/docs/connect) can own the OAuth
|
|
57
|
+
flow, encrypt token storage, and handle refresh. The `connect` helper from `@vercel/connect/ash`
|
|
58
|
+
collapses that into a single declaration and wires Connect's typed errors into Ash's
|
|
59
59
|
`ConnectionAuthorizationRequiredError` / `ConnectionAuthorizationFailedError` so the runtime can
|
|
60
60
|
drive consent without any connection-specific glue.
|
|
61
61
|
|
|
62
|
-
**Prerequisites.**
|
|
63
|
-
the helper will work. Once you have access:
|
|
62
|
+
**Prerequisites.** Connect is currently in private beta — your team needs to be enabled for it
|
|
63
|
+
before the helper will work. Once you have access:
|
|
64
64
|
|
|
65
65
|
1. Install the SDK:
|
|
66
66
|
|
|
67
67
|
```bash
|
|
68
|
-
pnpm add @vercel/
|
|
68
|
+
pnpm add @vercel/connect
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
2. Create the
|
|
72
|
-
`vercel
|
|
73
|
-
the value you'll pass to `
|
|
74
|
-
3. Link the
|
|
71
|
+
2. Create the Connect client — either in the Vercel dashboard or with
|
|
72
|
+
`vercel connect create <type> --name <uid>` from the CLI. Pick a `uid` (e.g. `"linear"`); this is
|
|
73
|
+
the value you'll pass to `connect()`.
|
|
74
|
+
3. Link the Connect client to the Vercel project that runs your agent.
|
|
75
75
|
4. Run `vercel link` and `vercel env pull` so `VERCEL_OIDC_TOKEN` is available locally — the SDK
|
|
76
76
|
uses it to authenticate to the Vercel API. On Vercel deployments OIDC is auto-provisioned.
|
|
77
77
|
|
|
@@ -79,24 +79,25 @@ Then declare the connection:
|
|
|
79
79
|
|
|
80
80
|
```ts
|
|
81
81
|
// agent/connections/linear.ts
|
|
82
|
-
import {
|
|
82
|
+
import { connect } from "@vercel/connect/ash";
|
|
83
83
|
import { defineMcpClientConnection } from "experimental-ash/connections";
|
|
84
84
|
|
|
85
85
|
export default defineMcpClientConnection({
|
|
86
86
|
url: "https://mcp.linear.app/sse",
|
|
87
87
|
description: "Linear workspace — issues, projects, cycles, and comments.",
|
|
88
|
-
auth:
|
|
88
|
+
auth: connect("linear"),
|
|
89
89
|
});
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
`"linear"` is the UID you chose when registering the
|
|
93
|
-
with a Vercel-reserved prefix). Use the object form when you need to pass additional
|
|
92
|
+
`"linear"` is the UID you chose when registering the Connect client (any string that doesn't start
|
|
93
|
+
with a Vercel-reserved prefix). Use the object form when you need to pass additional Connect
|
|
94
|
+
options:
|
|
94
95
|
|
|
95
96
|
```ts
|
|
96
|
-
auth:
|
|
97
|
+
auth: connect({ clientId: "linear" }),
|
|
97
98
|
```
|
|
98
99
|
|
|
99
|
-
|
|
100
|
+
Connect-managed OAuth defaults to user-scoped auth — each end-user authorizes through their own
|
|
100
101
|
browser, and the runtime resolves the per-user token before each tool call.
|
|
101
102
|
|
|
102
103
|
## No Auth
|
|
@@ -113,7 +114,7 @@ export default defineMcpClientConnection({
|
|
|
113
114
|
|
|
114
115
|
## Principal Type: `user` vs `app`
|
|
115
116
|
|
|
116
|
-
|
|
117
|
+
Connect-managed OAuth defaults to user-scoped auth. Custom `getToken` auth may set `principalType`
|
|
117
118
|
when it needs a different cache and principal model:
|
|
118
119
|
|
|
119
120
|
- `principalType: "user"` returns an interactive definition. Each end-user authorizes the
|
|
@@ -151,7 +152,7 @@ import { once } from "experimental-ash/tools/approval";
|
|
|
151
152
|
export default defineMcpClientConnection({
|
|
152
153
|
url: "https://mcp.linear.app/sse",
|
|
153
154
|
description: "Linear workspace.",
|
|
154
|
-
auth:
|
|
155
|
+
auth: connect("linear"),
|
|
155
156
|
approval: once(),
|
|
156
157
|
});
|
|
157
158
|
```
|
|
@@ -168,7 +169,7 @@ Constrain which tools the model sees with `tools.allow` or `tools.block`:
|
|
|
168
169
|
export default defineMcpClientConnection({
|
|
169
170
|
url: "https://mcp.linear.app/sse",
|
|
170
171
|
description: "Linear — read-only.",
|
|
171
|
-
auth:
|
|
172
|
+
auth: connect("linear"),
|
|
172
173
|
tools: { allow: ["search_issues", "get_issue"] },
|
|
173
174
|
});
|
|
174
175
|
```
|
|
@@ -156,7 +156,7 @@ Use:
|
|
|
156
156
|
- `lib/` for shared helper modules imported by the slots above
|
|
157
157
|
|
|
158
158
|
See [Connections](./connections.md) for the connection authorization shape, including the
|
|
159
|
-
`@vercel/
|
|
159
|
+
`@vercel/connect/ash` helper for OAuth-backed connections.
|
|
160
160
|
|
|
161
161
|
## What To Read Next
|
|
162
162
|
|
|
@@ -110,9 +110,9 @@ See [Skills](./skills.md) for the full authoring model.
|
|
|
110
110
|
## Passing Custom Context From a Channel
|
|
111
111
|
|
|
112
112
|
Channels can inject custom typed durable context into the agent via the channel's `state` and
|
|
113
|
-
`context()` properties on `defineChannel`, or via
|
|
114
|
-
when a channel needs to pass platform-specific metadata (tenant
|
|
115
|
-
authored tools can read on the same turn and on later turns.
|
|
113
|
+
`context()` properties on `defineChannel`, or via the auth attributes returned from `onMention` on
|
|
114
|
+
`slackChannel()`. This is useful when a channel needs to pass platform-specific metadata (tenant
|
|
115
|
+
ID, feature flags, etc.) that authored tools can read on the same turn and on later turns.
|
|
116
116
|
|
|
117
117
|
### Defining a key
|
|
118
118
|
|
|
@@ -129,7 +129,7 @@ export const TenantKey = new ContextKey<string>("myapp.tenant");
|
|
|
129
129
|
|
|
130
130
|
### Setting context from a channel
|
|
131
131
|
|
|
132
|
-
Use `slackChannel` and seed context via the `
|
|
132
|
+
Use `slackChannel()` and seed context via the `onMention` return value's auth attributes:
|
|
133
133
|
|
|
134
134
|
`agent/channels/slack.ts`
|
|
135
135
|
|
|
@@ -138,7 +138,7 @@ import { slackChannel } from "experimental-ash/channels/slack";
|
|
|
138
138
|
import { TenantKey } from "./keys.js";
|
|
139
139
|
|
|
140
140
|
export default slackChannel({
|
|
141
|
-
|
|
141
|
+
onMention(ctx, message) {
|
|
142
142
|
const tenantId = lookupTenant(message);
|
|
143
143
|
return {
|
|
144
144
|
auth: {
|
|
@@ -149,17 +149,19 @@ export default slackChannel({
|
|
|
149
149
|
},
|
|
150
150
|
};
|
|
151
151
|
},
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
152
|
+
events: {
|
|
153
|
+
"message.completed"(event, ctx) {
|
|
154
|
+
if (event.finishReason === "tool-calls") return;
|
|
155
|
+
if (event.message) ctx.thread.post(event.message);
|
|
156
|
+
},
|
|
157
|
+
"session.failed"(event, ctx) {
|
|
158
|
+
ctx.thread.post("Something went wrong.");
|
|
159
|
+
},
|
|
158
160
|
},
|
|
159
161
|
});
|
|
160
162
|
```
|
|
161
163
|
|
|
162
|
-
Auth lives on the return value of `
|
|
164
|
+
Auth lives on the return value of `onMention`, not on a separate channel object.
|
|
163
165
|
|
|
164
166
|
### Reading context from a tool
|
|
165
167
|
|
|
@@ -66,12 +66,13 @@ Ash also exports lower-level runtime primitives such as `createToolLoopHarness(.
|
|
|
66
66
|
|
|
67
67
|
Channel and Slack types exported from `experimental-ash/channels/slack`:
|
|
68
68
|
|
|
69
|
-
- `
|
|
70
|
-
|
|
71
|
-
- `SlackChannelConfig` - config type for `slackChannel`
|
|
69
|
+
- `slackChannel` - Slack channel factory; zero-config by default, accepts a `SlackChannelConfig`
|
|
70
|
+
to override `onMention`, individual event handlers, `onInteraction`, etc.
|
|
71
|
+
- `SlackChannelConfig` - config type for `slackChannel(...)`
|
|
72
72
|
- `SlackContext` - context type for Slack event handlers (`thread`, `slack`)
|
|
73
73
|
- `SlackApiHandle` - Slack API handle with `channelId`, `threadTs`, `request()`
|
|
74
74
|
- `SlackInteractionAction` - action type for `onInteraction`
|
|
75
|
+
- `SlackMentionResult` - return type of `onMention` (`{ auth } | null`)
|
|
75
76
|
- `Thread`, `Message`, `Card`, `Button`, `Actions`, etc. - Chat SDK types for Slack rendering
|
|
76
77
|
|
|
77
78
|
Channel types exported from `experimental-ash/channels`:
|
|
@@ -114,7 +115,7 @@ import { defineAgent } from "experimental-ash";
|
|
|
114
115
|
import { defineChannel, POST, GET } from "experimental-ash/channels";
|
|
115
116
|
import { ashChannel } from "experimental-ash/channels/ash";
|
|
116
117
|
import { vercelOidc } from "experimental-ash/channels/auth";
|
|
117
|
-
import {
|
|
118
|
+
import { slackChannel } from "experimental-ash/channels/slack";
|
|
118
119
|
```
|
|
119
120
|
|
|
120
121
|
Inside a route handler, the helpers object exposes:
|