@cursor/july 0.1.27 → 0.1.28
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/dist/bin/agent-serve.js +12 -3
- package/dist/channels/slack/auth.d.ts +13 -13
- package/dist/channels/slack/auth.d.ts.map +1 -1
- package/dist/channels/slack/auth.js +19 -10
- package/dist/channels/slack/cursor-account.d.ts.map +1 -1
- package/dist/channels/slack/cursor-account.js +3 -1
- package/dist/channels/slack/defaults.d.ts +7 -4
- package/dist/channels/slack/defaults.d.ts.map +1 -1
- package/dist/channels/slack/defaults.js +14 -4
- package/dist/channels/slack/dispatch.d.ts +6 -0
- package/dist/channels/slack/dispatch.d.ts.map +1 -1
- package/dist/channels/slack/dispatch.js +59 -21
- package/dist/channels/slack/external-policy.d.ts +27 -0
- package/dist/channels/slack/external-policy.d.ts.map +1 -0
- package/dist/channels/slack/external-policy.js +53 -0
- package/dist/channels/slack/index.d.ts +1 -0
- package/dist/channels/slack/index.d.ts.map +1 -1
- package/dist/channels/slack/index.js +1 -0
- package/dist/channels/slack/interactive.d.ts +6 -0
- package/dist/channels/slack/interactive.d.ts.map +1 -1
- package/dist/channels/slack/interactive.js +42 -15
- package/dist/channels/slack/slack-channel.d.ts.map +1 -1
- package/dist/channels/slack/slack-channel.js +19 -6
- package/dist/channels/slack/types.d.ts +10 -0
- package/dist/channels/slack/types.d.ts.map +1 -1
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +3 -3
- package/dist/docs/assets/{app.BICLoJYF.js → app.CqjrYaT2.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.VWQoPFJ1.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.wN4Ko9dT.js → VPLocalSearchBox.ag-MwUel.js} +1 -1
- package/dist/docs/assets/chunks/{theme.dnnTE-Co.js → theme.BS-DJWat.js} +2 -2
- package/dist/docs/assets/{guides_slack.md.D_xFKlnF.js → guides_slack.md.CjmJSvZS.js} +8 -4
- package/dist/docs/assets/guides_slack.md.CjmJSvZS.lean.js +1 -0
- package/dist/docs/assets/guides_webhooks.md.wSOYas3X.js +183 -0
- package/dist/docs/assets/{guides_webhooks.md.B1EswtUu.lean.js → guides_webhooks.md.wSOYas3X.lean.js} +1 -1
- package/dist/docs/assets/{index.md.Dfv5ic9t.js → index.md.CH_s5uZe.js} +1 -1
- package/dist/docs/assets/{reference_channels.md.DdmiKgqf.js → reference_channels.md.CDhTRfUz.js} +2 -2
- package/dist/docs/assets/{reference_channels.md.DdmiKgqf.lean.js → reference_channels.md.CDhTRfUz.lean.js} +1 -1
- package/dist/docs/building-with-agents.html +3 -3
- package/dist/docs/concepts.html +3 -3
- package/dist/docs/deployment.html +3 -3
- package/dist/docs/evals.html +3 -3
- package/dist/docs/example-agents/approval-buddy.html +3 -3
- package/dist/docs/example-agents/benny.html +3 -3
- package/dist/docs/example-agents/bugbot.html +3 -3
- package/dist/docs/example-agents/codebase-wiki.html +3 -3
- package/dist/docs/example-agents/codeowners-review.html +3 -3
- package/dist/docs/example-agents/concierge.html +3 -3
- package/dist/docs/example-agents/fsd.html +3 -3
- package/dist/docs/example-agents/index.html +3 -3
- package/dist/docs/example-agents/knowledge-base.html +3 -3
- package/dist/docs/example-agents/oncall.html +3 -3
- package/dist/docs/example-agents/security-reviewer.html +3 -3
- package/dist/docs/example-agents/slack-agent.html +3 -3
- package/dist/docs/example-agents/weather-agent.html +3 -3
- package/dist/docs/guides/agent-to-agent.html +3 -3
- package/dist/docs/guides/cloud-runtime.html +3 -3
- package/dist/docs/guides/github.html +3 -3
- package/dist/docs/guides/human-in-the-loop.html +3 -3
- package/dist/docs/guides/mcp-oauth.html +3 -3
- package/dist/docs/guides/slack.html +11 -7
- package/dist/docs/guides/webhooks.html +138 -5
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +3 -3
- package/dist/docs/index.html +5 -5
- package/dist/docs/quickstart.html +3 -3
- package/dist/docs/reference/agent-config.html +3 -3
- package/dist/docs/reference/channels.html +5 -5
- package/dist/docs/reference/cli.html +3 -3
- package/dist/docs/reference/connections.html +3 -3
- package/dist/docs/reference/hooks.html +3 -3
- package/dist/docs/reference/http-api.html +3 -3
- package/dist/docs/reference/instructions.html +3 -3
- package/dist/docs/reference/playground.html +3 -3
- package/dist/docs/reference/project-layout.html +3 -3
- package/dist/docs/reference/prompt.html +3 -3
- package/dist/docs/reference/schedules.html +3 -3
- package/dist/docs/reference/sessions.html +3 -3
- package/dist/docs/reference/skills.html +3 -3
- package/dist/docs/reference/subagents.html +3 -3
- package/dist/docs/reference/tools.html +3 -3
- package/dist/docs/scaffolding-agents.html +3 -3
- package/dist/docs/storage.html +3 -3
- package/dist/docs/troubleshooting.html +3 -3
- package/dist/internal/cli-ax.d.ts +8 -0
- package/dist/internal/cli-ax.d.ts.map +1 -1
- package/dist/internal/cli-ax.js +116 -57
- package/dist/internal/cursor-slack-relay.d.ts +2 -0
- package/dist/internal/cursor-slack-relay.d.ts.map +1 -1
- package/dist/internal/cursor-slack-relay.js +1 -0
- package/dist/internal/deploy-client.d.ts +2 -0
- package/dist/internal/deploy-client.d.ts.map +1 -1
- package/dist/internal/deploy-client.js +1 -0
- package/dist/internal/discovery.js +8 -1
- package/dist/internal/server.js +10 -5
- package/dist/playground/assets/{index-BDCeW4uc.js → index-CkN74U_L.js} +48 -47
- package/dist/playground/index.html +1 -1
- package/docs/README.md +1 -0
- package/docs/guides/slack.md +32 -7
- package/docs/guides/webhooks.md +183 -0
- package/docs/reference/channels.md +2 -1
- package/package.json +1 -1
- package/skills/setup-slack/SKILL.md +3 -0
- package/src/bin/agent-serve.ts +12 -3
- package/src/channels/slack/auth.ts +29 -22
- package/src/channels/slack/cursor-account.ts +2 -0
- package/src/channels/slack/defaults.ts +24 -4
- package/src/channels/slack/dispatch.ts +42 -1
- package/src/channels/slack/external-policy.ts +62 -0
- package/src/channels/slack/index.ts +1 -0
- package/src/channels/slack/interactive.ts +42 -10
- package/src/channels/slack/slack-channel.ts +20 -3
- package/src/channels/slack/types.ts +12 -0
- package/src/internal/cli-ax.ts +110 -34
- package/src/internal/cursor-slack-relay.ts +3 -0
- package/src/internal/deploy-client.ts +3 -0
- package/src/internal/discovery.ts +8 -1
- package/src/internal/server.ts +9 -5
- package/dist/docs/assets/chunks/@localSearchIndexroot.CQJytDwU.js +0 -1
- package/dist/docs/assets/guides_slack.md.D_xFKlnF.lean.js +0 -1
- package/dist/docs/assets/guides_webhooks.md.B1EswtUu.js +0 -50
- /package/dist/docs/assets/{index.md.Dfv5ic9t.lean.js → index.md.CH_s5uZe.lean.js} +0 -0
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
/>
|
|
9
9
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
10
10
|
<title>agent-serve playground</title>
|
|
11
|
-
<script type="module" crossorigin src="./assets/index-
|
|
11
|
+
<script type="module" crossorigin src="./assets/index-CkN74U_L.js"></script>
|
|
12
12
|
<link rel="stylesheet" crossorigin href="./assets/index-Bul1WfrB.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
package/docs/README.md
CHANGED
|
@@ -56,6 +56,7 @@ Pick your entry point based on your goal.
|
|
|
56
56
|
| Learning from working agents | [Example agents](./example-agents/index.md) |
|
|
57
57
|
| Wiring an agent to Slack | [Slack guide](./guides/slack.md) |
|
|
58
58
|
| Wiring an agent to GitHub webhooks | [GitHub guide](./guides/github.md) |
|
|
59
|
+
| Driving an agent from Linear (or another tracker) | [Webhooks guide: Linear example](./guides/webhooks.md#example-linear-as-the-control-plane) |
|
|
59
60
|
| Making an existing agent measurably better | [Evals](./evals.md), then [Hillclimbing](./hillclimbing.md) |
|
|
60
61
|
| Comparing variants on live traffic | [Live A/B metrics](./ab.md) |
|
|
61
62
|
| Deploying with Cursor or on your own infrastructure | [Deployment](./deployment.md) |
|
package/docs/guides/slack.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "Slack"
|
|
3
|
-
description: "Put the agent in Slack over Socket Mode: manifests, tokens, doctor, engagement rules, and approval buttons."
|
|
3
|
+
description: "Put the agent in Slack over Socket Mode: manifests, tokens, doctor, engagement rules, access policy, and approval buttons."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Slack agents
|
|
@@ -31,12 +31,13 @@ export default slackChannel({ envPrefix: "WEATHER_AGENT" });
|
|
|
31
31
|
|
|
32
32
|
By default the channel connects over Socket Mode (the app connects
|
|
33
33
|
outward, so it works from behind any firewall) and responds only to
|
|
34
|
-
`app_mention` events and direct messages.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
`app_mention` events and direct messages. It also refuses Slack Connect
|
|
35
|
+
users, guests, and senders from other workspaces. Threads bind to
|
|
36
|
+
sessions with `continuationToken = channelId:threadTs`, so a thread is
|
|
37
|
+
one durable conversation. Replies stream (`chat.startStream` /
|
|
38
|
+
`appendStream` / `stopStream`) with a plain-message fallback, a rotating
|
|
39
|
+
status shows while the agent works, and the pack sets thread titles and
|
|
40
|
+
suggested prompts. When a Slack API is unavailable (`method_not_found`,
|
|
40
41
|
`missing_scope`), the pack degrades to `postMessage` instead of failing.
|
|
41
42
|
|
|
42
43
|
Missing tokens leave the channel idle (`channel idle … missing
|
|
@@ -71,6 +72,30 @@ claimed-thread reply). Channel watching and `toolApprovals` /
|
|
|
71
72
|
Block Kit clicks. Agent names must be unique on the host; an unmatched
|
|
72
73
|
`@Cursor <name>` stays on Cursor's normal Slack agent.
|
|
73
74
|
|
|
75
|
+
## Control who can message the agent
|
|
76
|
+
|
|
77
|
+
External senders are blocked by default. Slack Connect users, guests, and
|
|
78
|
+
people whose home workspace is not the install team never reach the
|
|
79
|
+
handler. That applies to Socket Mode and `cursorAccount: true`. Set
|
|
80
|
+
`blockExternals: false` only when the agent should serve people outside
|
|
81
|
+
your org:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
export default slackChannel({
|
|
85
|
+
policy: { blockExternals: false },
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Sessions are thread-scoped by default, so anyone in the thread can
|
|
90
|
+
continue. Restrict follow-ups to the person who started the session with
|
|
91
|
+
`respondTo: "author"`:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
export default slackChannel({
|
|
95
|
+
policy: { respondTo: "author" },
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
74
99
|
## Set it up
|
|
75
100
|
|
|
76
101
|
### Generate the app assets
|
package/docs/guides/webhooks.md
CHANGED
|
@@ -177,6 +177,189 @@ swaps the default for `bearerAuth` on every channel that doesn't author
|
|
|
177
177
|
its own chain, and `--allow-anonymous` swaps it for `allowAll()`
|
|
178
178
|
(trusted networks only). Authored `auth` arrays always win over both.
|
|
179
179
|
|
|
180
|
+
## Example: Linear as the control plane
|
|
181
|
+
|
|
182
|
+
Everything above composes into a working ticket-driven agent. This
|
|
183
|
+
example wires Linear to the agent: new issues and comments start or
|
|
184
|
+
resume sessions, and replies land back on the issue as comments. The
|
|
185
|
+
same shape works for any tracker with signed webhooks.
|
|
186
|
+
|
|
187
|
+
Create the webhook in Linear under Settings → API → "New webhook",
|
|
188
|
+
pointed at `https://<your-host>/<slug>/v1/channels/linear`, and copy
|
|
189
|
+
the signing secret. Linear requires a public HTTPS URL, so use a
|
|
190
|
+
tunnel during local development or test with signed fixtures (below).
|
|
191
|
+
Set three environment variables:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
LINEAR_WEBHOOK_SECRET=lin_wh_... # the webhook's signing secret
|
|
195
|
+
LINEAR_API_KEY=lin_api_... # posts replies as comments
|
|
196
|
+
LINEAR_AGENT_USER_ID=... # the API key's user: query { viewer { id } }
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`LINEAR_AGENT_USER_ID` matters: replies posted with the API key
|
|
200
|
+
trigger the Comment webhook again, so the channel must recognize and
|
|
201
|
+
skip its own comments. Without the guard, every reply starts another
|
|
202
|
+
turn.
|
|
203
|
+
|
|
204
|
+
Author `agent/channels/linear.ts`:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
import { Buffer } from "node:buffer";
|
|
208
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
209
|
+
import { allowAll, defineChannel, POST } from "@cursor/july/channels";
|
|
210
|
+
import { z } from "zod";
|
|
211
|
+
|
|
212
|
+
const secret = process.env.LINEAR_WEBHOOK_SECRET ?? "";
|
|
213
|
+
const apiKey = process.env.LINEAR_API_KEY ?? "";
|
|
214
|
+
const agentUserId = process.env.LINEAR_AGENT_USER_ID ?? "";
|
|
215
|
+
|
|
216
|
+
interface LinearWebhook {
|
|
217
|
+
action: string;
|
|
218
|
+
type: string;
|
|
219
|
+
url?: string;
|
|
220
|
+
webhookTimestamp: number;
|
|
221
|
+
data: {
|
|
222
|
+
id: string;
|
|
223
|
+
title?: string;
|
|
224
|
+
description?: string;
|
|
225
|
+
body?: string;
|
|
226
|
+
issueId?: string;
|
|
227
|
+
userId?: string;
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Linear signs the raw body: hex HMAC-SHA256 in `Linear-Signature`.
|
|
232
|
+
function verified(rawBody: string, header: string | null): boolean {
|
|
233
|
+
if (secret === "" || header === null) {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
const expected = createHmac("sha256", secret).update(rawBody).digest();
|
|
237
|
+
const received = Buffer.from(header, "hex");
|
|
238
|
+
return (
|
|
239
|
+
received.length === expected.length && timingSafeEqual(received, expected)
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function postComment(issueId: string, body: string): Promise<void> {
|
|
244
|
+
const response = await fetch("https://api.linear.app/graphql", {
|
|
245
|
+
method: "POST",
|
|
246
|
+
headers: { "content-type": "application/json", authorization: apiKey },
|
|
247
|
+
body: JSON.stringify({
|
|
248
|
+
query:
|
|
249
|
+
"mutation($input: CommentCreateInput!) { commentCreate(input: $input) { success } }",
|
|
250
|
+
variables: { input: { issueId, body } },
|
|
251
|
+
}),
|
|
252
|
+
});
|
|
253
|
+
if (!response.ok) {
|
|
254
|
+
throw new Error(`commentCreate failed: ${response.status}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function issueIdFromToken(token: string | null): string | null {
|
|
259
|
+
if (token === null || !token.startsWith("linear:")) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
return token.slice("linear:".length);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export default defineChannel({
|
|
266
|
+
// The HMAC check is the request auth, so admit everything at the route.
|
|
267
|
+
auth: [allowAll()],
|
|
268
|
+
routes: [
|
|
269
|
+
POST("/", {
|
|
270
|
+
description: "Linear webhook ingress",
|
|
271
|
+
// The payload shape varies by `Linear-Event`; parse after verifying.
|
|
272
|
+
bodySchema: z.unknown(),
|
|
273
|
+
handler: async (request, { send, waitUntil }) => {
|
|
274
|
+
const rawBody = await request.text();
|
|
275
|
+
if (!verified(rawBody, request.headers.get("linear-signature"))) {
|
|
276
|
+
return Response.json({ ok: false }, { status: 401 });
|
|
277
|
+
}
|
|
278
|
+
const event = JSON.parse(rawBody) as LinearWebhook;
|
|
279
|
+
// Reject stale deliveries to guard against replay.
|
|
280
|
+
if (Math.abs(Date.now() - event.webhookTimestamp) > 60_000) {
|
|
281
|
+
return Response.json({ ok: false }, { status: 401 });
|
|
282
|
+
}
|
|
283
|
+
// Skip the agent's own comments so replies don't re-trigger it.
|
|
284
|
+
if (event.type === "Comment" && event.data.userId === agentUserId) {
|
|
285
|
+
return Response.json({ ok: true });
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// New issue → new session. New comment → follow-up on the same
|
|
289
|
+
// session, keyed by issue id through the continuation token.
|
|
290
|
+
let issueId: string | undefined;
|
|
291
|
+
let message: string | undefined;
|
|
292
|
+
if (event.type === "Issue" && event.action === "create") {
|
|
293
|
+
issueId = event.data.id;
|
|
294
|
+
message = `New Linear issue: ${event.data.title}\n\n${
|
|
295
|
+
event.data.description ?? ""
|
|
296
|
+
}\n${event.url ?? ""}`;
|
|
297
|
+
} else if (event.type === "Comment" && event.action === "create") {
|
|
298
|
+
issueId = event.data.issueId;
|
|
299
|
+
message = event.data.body;
|
|
300
|
+
}
|
|
301
|
+
if (issueId === undefined || message === undefined) {
|
|
302
|
+
return Response.json({ ok: true });
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Linear retries on any non-200 and times out after five
|
|
306
|
+
// seconds: ACK now, run the turn in the background.
|
|
307
|
+
waitUntil(send(message, { continuationToken: `linear:${issueId}` }));
|
|
308
|
+
return Response.json({ ok: true });
|
|
309
|
+
},
|
|
310
|
+
}),
|
|
311
|
+
],
|
|
312
|
+
events: {
|
|
313
|
+
async "message.completed"(event, channel) {
|
|
314
|
+
if (event.data.finishReason === "tool_call" || event.data.text === "") {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const issueId = issueIdFromToken(channel.continuationToken);
|
|
318
|
+
if (issueId !== null) {
|
|
319
|
+
await postComment(issueId, event.data.text);
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
async "turn.failed"(event, channel) {
|
|
323
|
+
const issueId = issueIdFromToken(channel.continuationToken);
|
|
324
|
+
if (issueId !== null) {
|
|
325
|
+
await postComment(issueId, `Turn failed: ${event.data.message}`);
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
The channel handles ingress and reply delivery deterministically. To
|
|
333
|
+
let the model read and update Linear during the turn (search related
|
|
334
|
+
issues, change state, assign), add the Linear MCP connection alongside
|
|
335
|
+
it:
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
// agent/mcp-connections/linear.ts
|
|
339
|
+
import { defineConnection } from "@cursor/july/connections";
|
|
340
|
+
|
|
341
|
+
// Uses the signed-in Cursor account's Linear connector.
|
|
342
|
+
export default defineConnection({ cursorAccount: true, servers: ["Linear"] });
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
See [MCP connections](../reference/connections.md) for the direct
|
|
346
|
+
`https://mcp.linear.app/mcp` form when the host isn't signed in to
|
|
347
|
+
Cursor.
|
|
348
|
+
|
|
349
|
+
To test without a public URL, save a payload from the webhook's
|
|
350
|
+
delivery log (or the sample in
|
|
351
|
+
[Linear's webhook docs](https://linear.app/developers/webhooks)) under
|
|
352
|
+
`fixtures/`, refresh its `webhookTimestamp`, and sign it yourself:
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
SIG=$(node -e 'const {createHmac}=require("node:crypto");const fs=require("node:fs");
|
|
356
|
+
process.stdout.write(createHmac("sha256",process.env.LINEAR_WEBHOOK_SECRET)
|
|
357
|
+
.update(fs.readFileSync("fixtures/issue-create.json")).digest("hex"))')
|
|
358
|
+
curl -X POST http://127.0.0.1:3000/<slug>/v1/channels/linear/ \
|
|
359
|
+
-H 'content-type: application/json' -H "linear-signature: $SIG" \
|
|
360
|
+
--data-binary @fixtures/issue-create.json
|
|
361
|
+
```
|
|
362
|
+
|
|
180
363
|
## Test a channel
|
|
181
364
|
|
|
182
365
|
Start with curl and saved payloads under `fixtures/`. The playground's
|
|
@@ -174,7 +174,8 @@ declares `[localDevStrict()]` stays loopback-only even on an
|
|
|
174
174
|
## First class channels
|
|
175
175
|
|
|
176
176
|
**Slack** (`@cursor/july/channels/slack`): Socket Mode
|
|
177
|
-
transport, streaming replies, engagement rules, approval cards
|
|
177
|
+
transport, streaming replies, engagement rules, approval cards, and a
|
|
178
|
+
default block on Slack Connect / guest / other-workspace senders. Author
|
|
178
179
|
`agent/channels/slack.ts` with `slackChannel()`. Guide:
|
|
179
180
|
[Slack](../guides/slack.md).
|
|
180
181
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cursor/july",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.28",
|
|
4
4
|
"description": "(early alpha) Filesystem-first framework for defining Cursor agents as markdown and TypeScript and serving them over channels with the Cursor SDK.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"repository": {
|
|
@@ -97,6 +97,9 @@ export default slackChannel({
|
|
|
97
97
|
single token — string literals with spaces fail typecheck; runtime
|
|
98
98
|
values throw at construction/serve. Names must be unique on the host;
|
|
99
99
|
an unmatched `@Cursor <name>` stays with Cursor's normal Slack agent.
|
|
100
|
+
External senders (Slack Connect / guests / other workspaces) are
|
|
101
|
+
blocked by default; see `policy.blockExternals` in
|
|
102
|
+
[docs/guides/slack.md](../../docs/guides/slack.md).
|
|
100
103
|
|
|
101
104
|
### A2 — Sign in + serve (agent)
|
|
102
105
|
|
package/src/bin/agent-serve.ts
CHANGED
|
@@ -85,7 +85,7 @@ Usage:
|
|
|
85
85
|
${CLI} sessions [--url <baseUrl> | --prod] [--json]
|
|
86
86
|
[--slug <slug>] [--team <id>] [--dir <path>]
|
|
87
87
|
${CLI} session <sessionId> [--url <baseUrl> | --prod]
|
|
88
|
-
[--json | --text | --events]
|
|
88
|
+
[--json | --text | --events] [--out <file.ndjson>]
|
|
89
89
|
[--slug <slug>] [--team <id>] [--dir <path>]
|
|
90
90
|
${CLI} cost [sessionId] [--url <baseUrl> | --prod] [--json]
|
|
91
91
|
[--slug <slug>] [--team <id>] [--dir <path>]
|
|
@@ -110,11 +110,12 @@ Usage:
|
|
|
110
110
|
[--tag <tag>]... [--verbose] [--timeout-ms <n>]
|
|
111
111
|
[--strict] [--max-concurrency <n>]
|
|
112
112
|
[--junit <path>] [--artifacts <dir>|--no-artifacts]
|
|
113
|
+
[--out <file.json>]
|
|
113
114
|
[--skip-report] [--no-stream] [--no-wait]
|
|
114
115
|
[--prod | --url <baseUrl>] [--slug <slug>]
|
|
115
116
|
[--team <id>]
|
|
116
117
|
${CLI} eval status [runId] --prod|--url <baseUrl> [--dir <path>] [--json]
|
|
117
|
-
[--slug <slug>] [--team <id>]
|
|
118
|
+
[--out <file.json>] [--slug <slug>] [--team <id>]
|
|
118
119
|
${CLI} eval cancel <runId> --prod|--url <baseUrl> [--dir <path>] [--json]
|
|
119
120
|
[--slug <slug>] [--team <id>]
|
|
120
121
|
${CLI} info [--dir <path>] [--json]
|
|
@@ -392,7 +393,11 @@ Options:
|
|
|
392
393
|
--comment github replay: comment body for comment events.
|
|
393
394
|
--context github replay: commit-status context.
|
|
394
395
|
--dry-run github replay: print synthesized payloads, don't POST.
|
|
395
|
-
--out
|
|
396
|
+
--out eval / eval status <runId>: also write the full results
|
|
397
|
+
JSON to this path. session: write the raw NDJSON trace
|
|
398
|
+
there instead of printing (same format as run --events;
|
|
399
|
+
playground-openable). github replay: write synthesized
|
|
400
|
+
payloads as fixtures.
|
|
396
401
|
`;
|
|
397
402
|
|
|
398
403
|
async function main(argv: string[]): Promise<number> {
|
|
@@ -775,6 +780,7 @@ async function main(argv: string[]): Promise<number> {
|
|
|
775
780
|
apiKey: values["api-key"],
|
|
776
781
|
slug: values.slug,
|
|
777
782
|
bearerToken: values["bearer-token"],
|
|
783
|
+
out: values.out,
|
|
778
784
|
})
|
|
779
785
|
);
|
|
780
786
|
}
|
|
@@ -819,6 +825,7 @@ async function main(argv: string[]): Promise<number> {
|
|
|
819
825
|
artifacts: values["no-artifacts"] ? false : values.artifacts,
|
|
820
826
|
skipReport: values["skip-report"],
|
|
821
827
|
maxConcurrency: parseMaxConcurrency(values["max-concurrency"]),
|
|
828
|
+
out: values.out,
|
|
822
829
|
})
|
|
823
830
|
);
|
|
824
831
|
}
|
|
@@ -912,6 +919,7 @@ async function main(argv: string[]): Promise<number> {
|
|
|
912
919
|
json: values.json,
|
|
913
920
|
text: values.text,
|
|
914
921
|
rawEvents: values["raw-events"],
|
|
922
|
+
out: values.out,
|
|
915
923
|
})
|
|
916
924
|
);
|
|
917
925
|
}
|
|
@@ -1291,6 +1299,7 @@ function axFromOptions(
|
|
|
1291
1299
|
artifacts: partial.artifacts,
|
|
1292
1300
|
skipReport: partial.skipReport,
|
|
1293
1301
|
maxConcurrency: partial.maxConcurrency,
|
|
1302
|
+
out: partial.out,
|
|
1294
1303
|
};
|
|
1295
1304
|
}
|
|
1296
1305
|
|
|
@@ -4,29 +4,39 @@
|
|
|
4
4
|
|
|
5
5
|
import { samePrincipal } from "../../internal/auth.js";
|
|
6
6
|
import type { AuthContext } from "../../types.js";
|
|
7
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
SlackBinding,
|
|
9
|
+
SlackBlockAction,
|
|
10
|
+
SlackMessage,
|
|
11
|
+
SlackRespondTo,
|
|
12
|
+
} from "./types.js";
|
|
8
13
|
|
|
9
|
-
|
|
10
|
-
* Build a thread-scoped principal so any participant in the same Slack
|
|
11
|
-
* thread can continue the session. The triggering user is preserved in
|
|
12
|
-
* attributes for message context.
|
|
13
|
-
*/
|
|
14
|
-
export function buildSlackAuthContext(args: {
|
|
14
|
+
export type BuildSlackAuthArgs = {
|
|
15
15
|
channelId: string;
|
|
16
16
|
threadTs: string;
|
|
17
17
|
teamId?: string;
|
|
18
18
|
userId?: string;
|
|
19
19
|
userName?: string;
|
|
20
20
|
isBot?: boolean;
|
|
21
|
-
/**
|
|
22
|
-
* Engagement surface that started the session (e.g. `"channel_post"` for
|
|
23
|
-
* the opt-in channel watch). Recorded as an attribute; the principal stays
|
|
24
|
-
* thread-scoped so an @mention in the same thread continues the session.
|
|
25
|
-
*/
|
|
26
21
|
engagement?: string;
|
|
27
|
-
}
|
|
22
|
+
/** Defaults to `"thread"`. `"author"` appends `:{userId}` to the principal. */
|
|
23
|
+
respondTo?: SlackRespondTo;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Build a Slack session principal.
|
|
28
|
+
*
|
|
29
|
+
* - `"thread"` (default): any participant can continue.
|
|
30
|
+
* - `"author"`: only the starter (`…:{userId}`), same shape approvals already honor.
|
|
31
|
+
*/
|
|
32
|
+
export function buildSlackAuthContext(args: BuildSlackAuthArgs): AuthContext {
|
|
28
33
|
const team = args.teamId ?? "unknown";
|
|
29
|
-
const
|
|
34
|
+
const respondTo = args.respondTo ?? "thread";
|
|
35
|
+
const base = `slack:${team}:${args.channelId}:${args.threadTs}`;
|
|
36
|
+
const authorScoped =
|
|
37
|
+
respondTo === "author" && args.userId !== undefined && args.userId !== "";
|
|
38
|
+
const principalId = authorScoped ? `${base}:${args.userId}` : base;
|
|
39
|
+
|
|
30
40
|
const attributes: Record<string, string> = {
|
|
31
41
|
channel_id: args.channelId,
|
|
32
42
|
thread_ts: args.threadTs,
|
|
@@ -106,7 +116,7 @@ export function blockActionAuth(action: SlackBlockAction): AuthContext {
|
|
|
106
116
|
export function defaultSlackAuth(
|
|
107
117
|
message: SlackMessage,
|
|
108
118
|
ctx: SlackBinding,
|
|
109
|
-
options?: { engagement?: string }
|
|
119
|
+
options?: { engagement?: string; respondTo?: SlackRespondTo }
|
|
110
120
|
): AuthContext | null {
|
|
111
121
|
if (message.author === undefined) {
|
|
112
122
|
return null;
|
|
@@ -114,14 +124,11 @@ export function defaultSlackAuth(
|
|
|
114
124
|
return buildSlackAuthContext({
|
|
115
125
|
channelId: ctx.slack.channelId,
|
|
116
126
|
threadTs: ctx.slack.threadTs,
|
|
117
|
-
|
|
127
|
+
teamId: message.teamId,
|
|
118
128
|
userId: message.author.userId,
|
|
119
|
-
|
|
120
|
-
? {}
|
|
121
|
-
: { userName: message.author.userName }),
|
|
129
|
+
userName: message.author.userName,
|
|
122
130
|
isBot: message.author.isBot,
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
: { engagement: options.engagement }),
|
|
131
|
+
engagement: options?.engagement,
|
|
132
|
+
respondTo: options?.respondTo,
|
|
126
133
|
});
|
|
127
134
|
}
|
|
@@ -172,6 +172,8 @@ export async function dispatchCursorSlackEnvelope(args: {
|
|
|
172
172
|
log,
|
|
173
173
|
evalCommandsEnabled: runtime.options.evalCommands !== false,
|
|
174
174
|
cursorAgentName: envelope.agentName,
|
|
175
|
+
// Backend router already refused externals for this envelope.
|
|
176
|
+
blockExternals: false,
|
|
175
177
|
...(args.hasContinuationSession === undefined
|
|
176
178
|
? {}
|
|
177
179
|
: { hasContinuationSession: args.hasContinuationSession }),
|
|
@@ -20,9 +20,11 @@ import { createStreamDeliveryStrategy } from "./live-delivery.js";
|
|
|
20
20
|
import { defaultSlackLog, type SlackLog } from "./log.js";
|
|
21
21
|
import { buildTaskChunkForResult, buildTaskChunksForCalls } from "./stream.js";
|
|
22
22
|
import type {
|
|
23
|
+
SlackAccessPolicy,
|
|
23
24
|
SlackChannelState,
|
|
24
25
|
SlackCredentials,
|
|
25
26
|
SlackMessageHandler,
|
|
27
|
+
SlackRespondTo,
|
|
26
28
|
SlackSuggestedPrompt,
|
|
27
29
|
} from "./types.js";
|
|
28
30
|
|
|
@@ -30,6 +32,19 @@ const SLACK_TURN_FAILURE_FALLBACK =
|
|
|
30
32
|
"I hit an error while handling your request. Please try again or rephrase.";
|
|
31
33
|
const SLACK_TURN_FAILURE_MAX_CHARS = 400;
|
|
32
34
|
|
|
35
|
+
export function resolveSlackRespondTo(
|
|
36
|
+
policy: SlackAccessPolicy | undefined
|
|
37
|
+
): SlackRespondTo {
|
|
38
|
+
return policy?.respondTo === "author" ? "author" : "thread";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Defaults to true when omitted. */
|
|
42
|
+
export function resolveSlackBlockExternals(
|
|
43
|
+
policy: SlackAccessPolicy | undefined
|
|
44
|
+
): boolean {
|
|
45
|
+
return policy?.blockExternals !== false;
|
|
46
|
+
}
|
|
47
|
+
|
|
33
48
|
/** Scrubbed turn.failed body for Slack (secrets / paths / URLs stripped). */
|
|
34
49
|
export function formatSlackTurnFailureMessage(raw: string | undefined): string {
|
|
35
50
|
const scrubbed = scrubSlackErrorDetail(raw?.trim() ?? "");
|
|
@@ -68,29 +83,34 @@ function resolveLoadingMessages(
|
|
|
68
83
|
|
|
69
84
|
/** Default mention/DM inbound handler (optional custom loading rotation). */
|
|
70
85
|
export function createDefaultOnInbound(
|
|
71
|
-
loadingMessages?: readonly string[]
|
|
86
|
+
loadingMessages?: readonly string[],
|
|
87
|
+
policy?: SlackAccessPolicy
|
|
72
88
|
): SlackMessageHandler {
|
|
73
89
|
const rotating = resolveLoadingMessages(loadingMessages);
|
|
90
|
+
const respondTo = resolveSlackRespondTo(policy);
|
|
74
91
|
return async (ctx, message) => {
|
|
75
92
|
await ctx.thread.startTyping("Thinking…", rotating);
|
|
76
|
-
const auth = defaultSlackAuth(message, ctx);
|
|
93
|
+
const auth = defaultSlackAuth(message, ctx, { respondTo });
|
|
77
94
|
return auth === null ? null : { auth };
|
|
78
95
|
};
|
|
79
96
|
}
|
|
80
97
|
|
|
81
98
|
/**
|
|
82
99
|
* Default watched-channel-post handler (optional custom loading rotation).
|
|
83
|
-
* Same
|
|
100
|
+
* Same principal policy as mentions, tagged with
|
|
84
101
|
* `engagement: "channel_post"` for auth policies.
|
|
85
102
|
*/
|
|
86
103
|
export function createDefaultOnChannelPost(
|
|
87
|
-
loadingMessages?: readonly string[]
|
|
104
|
+
loadingMessages?: readonly string[],
|
|
105
|
+
policy?: SlackAccessPolicy
|
|
88
106
|
): SlackMessageHandler {
|
|
89
107
|
const rotating = resolveLoadingMessages(loadingMessages);
|
|
108
|
+
const respondTo = resolveSlackRespondTo(policy);
|
|
90
109
|
return async (ctx, message) => {
|
|
91
110
|
await ctx.thread.startTyping("Thinking…", rotating);
|
|
92
111
|
const auth = defaultSlackAuth(message, ctx, {
|
|
93
112
|
engagement: "channel_post",
|
|
113
|
+
respondTo,
|
|
94
114
|
});
|
|
95
115
|
return auth === null ? null : { auth };
|
|
96
116
|
};
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
parseSlackEvalDirective,
|
|
18
18
|
} from "./eval-directive.js";
|
|
19
19
|
import type { EventIdDedupe } from "./event-id-dedupe.js";
|
|
20
|
+
import { isExternalSlackSender } from "./external-policy.js";
|
|
20
21
|
import {
|
|
21
22
|
formatSlackContextBlock,
|
|
22
23
|
parseAppMentionEvent,
|
|
@@ -91,6 +92,11 @@ export interface SlackInboundDispatchDeps extends SlackContinuationProbes {
|
|
|
91
92
|
evalCommandsEnabled?: boolean;
|
|
92
93
|
/** cursorAccount agent name stripped before `eval` directive parsing. */
|
|
93
94
|
cursorAgentName?: string;
|
|
95
|
+
/**
|
|
96
|
+
* Drop external senders before the handler. Defaults to `true`; pass
|
|
97
|
+
* `false` to admit them (or when an upstream gate already refused them).
|
|
98
|
+
*/
|
|
99
|
+
blockExternals?: boolean;
|
|
94
100
|
}
|
|
95
101
|
|
|
96
102
|
function triggeringMergeMessage(
|
|
@@ -132,6 +138,7 @@ export async function dispatchInboundMessage(args: {
|
|
|
132
138
|
* (`OncallBuddy eval list`).
|
|
133
139
|
*/
|
|
134
140
|
cursorAgentName?: string;
|
|
141
|
+
blockExternals?: boolean;
|
|
135
142
|
}): Promise<void> {
|
|
136
143
|
const { message, kind } = args;
|
|
137
144
|
const log = args.log ?? defaultSlackLog;
|
|
@@ -142,7 +149,7 @@ export async function dispatchInboundMessage(args: {
|
|
|
142
149
|
host: args.host,
|
|
143
150
|
artifacts: args.artifacts,
|
|
144
151
|
log,
|
|
145
|
-
|
|
152
|
+
teamId: message.teamId,
|
|
146
153
|
});
|
|
147
154
|
|
|
148
155
|
log.info("inbound", {
|
|
@@ -155,6 +162,38 @@ export async function dispatchInboundMessage(args: {
|
|
|
155
162
|
team: message.teamId,
|
|
156
163
|
});
|
|
157
164
|
|
|
165
|
+
if (args.blockExternals !== false && message.author?.isBot !== true) {
|
|
166
|
+
const userId = message.author?.userId ?? "";
|
|
167
|
+
const teamId = message.teamId;
|
|
168
|
+
if (teamId === undefined || teamId === "") {
|
|
169
|
+
log.info("inbound dropped (missing team)", {
|
|
170
|
+
kind,
|
|
171
|
+
channel: message.channelId,
|
|
172
|
+
thread_ts: message.threadTs,
|
|
173
|
+
user: userId,
|
|
174
|
+
event_id: args.eventId,
|
|
175
|
+
});
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (
|
|
179
|
+
await isExternalSlackSender({
|
|
180
|
+
botToken: args.credentials?.botToken,
|
|
181
|
+
slackTeamId: teamId,
|
|
182
|
+
userId,
|
|
183
|
+
})
|
|
184
|
+
) {
|
|
185
|
+
log.info("inbound dropped (external sender)", {
|
|
186
|
+
kind,
|
|
187
|
+
channel: message.channelId,
|
|
188
|
+
thread_ts: message.threadTs,
|
|
189
|
+
user: userId,
|
|
190
|
+
team: teamId,
|
|
191
|
+
event_id: args.eventId,
|
|
192
|
+
});
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
158
197
|
const continuation = slackContinuationKey(
|
|
159
198
|
message.channelId,
|
|
160
199
|
message.threadTs
|
|
@@ -463,6 +502,7 @@ export function dispatchInboundFromEventBody(
|
|
|
463
502
|
log,
|
|
464
503
|
evalCommandsEnabled: deps.evalCommandsEnabled,
|
|
465
504
|
cursorAgentName: deps.cursorAgentName,
|
|
505
|
+
blockExternals: deps.blockExternals,
|
|
466
506
|
...(deps.hasContinuationSession === undefined
|
|
467
507
|
? {}
|
|
468
508
|
: { hasContinuationSession: deps.hasContinuationSession }),
|
|
@@ -504,6 +544,7 @@ export function dispatchInboundFromEventBody(
|
|
|
504
544
|
log,
|
|
505
545
|
evalCommandsEnabled: deps.evalCommandsEnabled,
|
|
506
546
|
cursorAgentName: deps.cursorAgentName,
|
|
547
|
+
blockExternals: deps.blockExternals,
|
|
507
548
|
...(deps.hasContinuationSession === undefined
|
|
508
549
|
? {}
|
|
509
550
|
: { hasContinuationSession: deps.hasContinuationSession }),
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace-membership checks for AgentKit Slack agents.
|
|
3
|
+
* Mirrors backend `isExternalSlackUser` (cloud-agent/slack/externalChannelPolicy).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { callSlackApi } from "./api.js";
|
|
7
|
+
import type { SlackBotToken } from "./types.js";
|
|
8
|
+
|
|
9
|
+
type SlackUserInfo = {
|
|
10
|
+
team_id?: string;
|
|
11
|
+
is_stranger?: boolean;
|
|
12
|
+
is_restricted?: boolean;
|
|
13
|
+
is_ultra_restricted?: boolean;
|
|
14
|
+
enterprise_user?: { teams?: string[] };
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** Pure classifier over a `users.info` user object. Fails open to external. */
|
|
18
|
+
export function classifySlackUserExternality(args: {
|
|
19
|
+
slackTeamId: string;
|
|
20
|
+
user: SlackUserInfo;
|
|
21
|
+
}): boolean {
|
|
22
|
+
const { slackTeamId, user } = args;
|
|
23
|
+
if (
|
|
24
|
+
user.is_stranger === true ||
|
|
25
|
+
user.is_restricted === true ||
|
|
26
|
+
user.is_ultra_restricted === true
|
|
27
|
+
) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
return (
|
|
31
|
+
user.team_id !== undefined &&
|
|
32
|
+
user.team_id !== slackTeamId &&
|
|
33
|
+
!(user.enterprise_user?.teams ?? []).includes(slackTeamId)
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Lookup + classify. Empty userId / API failure → external. */
|
|
38
|
+
export async function isExternalSlackSender(args: {
|
|
39
|
+
botToken: SlackBotToken | undefined;
|
|
40
|
+
slackTeamId: string;
|
|
41
|
+
userId: string;
|
|
42
|
+
}): Promise<boolean> {
|
|
43
|
+
if (args.userId === "") {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const result = await callSlackApi({
|
|
48
|
+
botToken: args.botToken,
|
|
49
|
+
operation: "users.info",
|
|
50
|
+
body: { user: args.userId },
|
|
51
|
+
});
|
|
52
|
+
if (result.ok !== true || result.user === undefined) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
return classifySlackUserExternality({
|
|
56
|
+
slackTeamId: args.slackTeamId,
|
|
57
|
+
user: result.user as SlackUserInfo,
|
|
58
|
+
});
|
|
59
|
+
} catch {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
}
|