@cursor/july 0.1.27 → 0.1.29

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.
Files changed (180) hide show
  1. package/dist/bin/agent-serve.js +32 -5
  2. package/dist/channels/slack/auth.d.ts +13 -13
  3. package/dist/channels/slack/auth.d.ts.map +1 -1
  4. package/dist/channels/slack/auth.js +19 -10
  5. package/dist/channels/slack/cursor-account.d.ts.map +1 -1
  6. package/dist/channels/slack/cursor-account.js +3 -1
  7. package/dist/channels/slack/defaults.d.ts +7 -4
  8. package/dist/channels/slack/defaults.d.ts.map +1 -1
  9. package/dist/channels/slack/defaults.js +14 -4
  10. package/dist/channels/slack/dispatch.d.ts +6 -0
  11. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  12. package/dist/channels/slack/dispatch.js +59 -21
  13. package/dist/channels/slack/external-policy.d.ts +27 -0
  14. package/dist/channels/slack/external-policy.d.ts.map +1 -0
  15. package/dist/channels/slack/external-policy.js +53 -0
  16. package/dist/channels/slack/index.d.ts +1 -0
  17. package/dist/channels/slack/index.d.ts.map +1 -1
  18. package/dist/channels/slack/index.js +1 -0
  19. package/dist/channels/slack/interactive.d.ts +6 -0
  20. package/dist/channels/slack/interactive.d.ts.map +1 -1
  21. package/dist/channels/slack/interactive.js +42 -15
  22. package/dist/channels/slack/slack-channel.d.ts +19 -1
  23. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  24. package/dist/channels/slack/slack-channel.js +48 -6
  25. package/dist/channels/slack/types.d.ts +10 -0
  26. package/dist/channels/slack/types.d.ts.map +1 -1
  27. package/dist/docs/404.html +2 -2
  28. package/dist/docs/ab.html +3 -3
  29. package/dist/docs/assets/{app.BICLoJYF.js → app.QdunVQKg.js} +1 -1
  30. package/dist/docs/assets/chunks/@localSearchIndexroot.DGe61XHA.js +1 -0
  31. package/dist/docs/assets/chunks/{VPLocalSearchBox.wN4Ko9dT.js → VPLocalSearchBox.B6EpUXYf.js} +1 -1
  32. package/dist/docs/assets/chunks/{theme.dnnTE-Co.js → theme.BOTJVqh7.js} +2 -2
  33. package/dist/docs/assets/{guides_slack.md.D_xFKlnF.js → guides_slack.md.CjmJSvZS.js} +8 -4
  34. package/dist/docs/assets/guides_slack.md.CjmJSvZS.lean.js +1 -0
  35. package/dist/docs/assets/guides_webhooks.md.wSOYas3X.js +183 -0
  36. package/dist/docs/assets/{guides_webhooks.md.B1EswtUu.lean.js → guides_webhooks.md.wSOYas3X.lean.js} +1 -1
  37. package/dist/docs/assets/{index.md.Dfv5ic9t.js → index.md.CH_s5uZe.js} +1 -1
  38. package/dist/docs/assets/{reference_channels.md.DdmiKgqf.js → reference_channels.md.CDhTRfUz.js} +2 -2
  39. package/dist/docs/assets/{reference_channels.md.DdmiKgqf.lean.js → reference_channels.md.CDhTRfUz.lean.js} +1 -1
  40. package/dist/docs/building-with-agents.html +3 -3
  41. package/dist/docs/concepts.html +3 -3
  42. package/dist/docs/deployment.html +3 -3
  43. package/dist/docs/evals.html +3 -3
  44. package/dist/docs/example-agents/approval-buddy.html +3 -3
  45. package/dist/docs/example-agents/benny.html +3 -3
  46. package/dist/docs/example-agents/bugbot.html +3 -3
  47. package/dist/docs/example-agents/codebase-wiki.html +3 -3
  48. package/dist/docs/example-agents/codeowners-review.html +3 -3
  49. package/dist/docs/example-agents/concierge.html +3 -3
  50. package/dist/docs/example-agents/fsd.html +3 -3
  51. package/dist/docs/example-agents/index.html +3 -3
  52. package/dist/docs/example-agents/knowledge-base.html +3 -3
  53. package/dist/docs/example-agents/oncall.html +3 -3
  54. package/dist/docs/example-agents/security-reviewer.html +3 -3
  55. package/dist/docs/example-agents/slack-agent.html +3 -3
  56. package/dist/docs/example-agents/weather-agent.html +3 -3
  57. package/dist/docs/guides/agent-to-agent.html +3 -3
  58. package/dist/docs/guides/cloud-runtime.html +3 -3
  59. package/dist/docs/guides/github.html +3 -3
  60. package/dist/docs/guides/human-in-the-loop.html +3 -3
  61. package/dist/docs/guides/mcp-oauth.html +3 -3
  62. package/dist/docs/guides/slack.html +11 -7
  63. package/dist/docs/guides/webhooks.html +138 -5
  64. package/dist/docs/hashmap.json +1 -1
  65. package/dist/docs/hillclimbing.html +3 -3
  66. package/dist/docs/index.html +5 -5
  67. package/dist/docs/quickstart.html +3 -3
  68. package/dist/docs/reference/agent-config.html +3 -3
  69. package/dist/docs/reference/channels.html +5 -5
  70. package/dist/docs/reference/cli.html +3 -3
  71. package/dist/docs/reference/connections.html +3 -3
  72. package/dist/docs/reference/hooks.html +3 -3
  73. package/dist/docs/reference/http-api.html +3 -3
  74. package/dist/docs/reference/instructions.html +3 -3
  75. package/dist/docs/reference/playground.html +3 -3
  76. package/dist/docs/reference/project-layout.html +3 -3
  77. package/dist/docs/reference/prompt.html +3 -3
  78. package/dist/docs/reference/schedules.html +3 -3
  79. package/dist/docs/reference/sessions.html +3 -3
  80. package/dist/docs/reference/skills.html +3 -3
  81. package/dist/docs/reference/subagents.html +3 -3
  82. package/dist/docs/reference/tools.html +3 -3
  83. package/dist/docs/scaffolding-agents.html +3 -3
  84. package/dist/docs/storage.html +3 -3
  85. package/dist/docs/troubleshooting.html +3 -3
  86. package/dist/internal/cli-ax.d.ts +8 -0
  87. package/dist/internal/cli-ax.d.ts.map +1 -1
  88. package/dist/internal/cli-ax.js +116 -57
  89. package/dist/internal/cloud-turn-cost.d.ts +34 -0
  90. package/dist/internal/cloud-turn-cost.d.ts.map +1 -0
  91. package/dist/internal/cloud-turn-cost.js +106 -0
  92. package/dist/internal/cursor-slack-relay.d.ts +2 -0
  93. package/dist/internal/cursor-slack-relay.d.ts.map +1 -1
  94. package/dist/internal/cursor-slack-relay.js +1 -0
  95. package/dist/internal/deploy-client.d.ts +2 -0
  96. package/dist/internal/deploy-client.d.ts.map +1 -1
  97. package/dist/internal/deploy-client.js +1 -0
  98. package/dist/internal/discovery.d.ts.map +1 -1
  99. package/dist/internal/discovery.js +9 -2
  100. package/dist/internal/event-log.d.ts +6 -0
  101. package/dist/internal/event-log.d.ts.map +1 -1
  102. package/dist/internal/event-log.js +16 -0
  103. package/dist/internal/host-platforms.d.ts +13 -0
  104. package/dist/internal/host-platforms.d.ts.map +1 -1
  105. package/dist/internal/host-platforms.js +25 -16
  106. package/dist/internal/http-channel.d.ts.map +1 -1
  107. package/dist/internal/http-channel.js +3 -1
  108. package/dist/internal/local-control-plane.d.ts +25 -0
  109. package/dist/internal/local-control-plane.d.ts.map +1 -0
  110. package/dist/internal/local-control-plane.js +172 -0
  111. package/dist/internal/platform-schedule-sync.d.ts +17 -0
  112. package/dist/internal/platform-schedule-sync.d.ts.map +1 -0
  113. package/dist/internal/platform-schedule-sync.js +72 -0
  114. package/dist/internal/platform-timers.d.ts +28 -0
  115. package/dist/internal/platform-timers.d.ts.map +1 -0
  116. package/dist/internal/platform-timers.js +120 -0
  117. package/dist/internal/reminder-runner.d.ts +16 -12
  118. package/dist/internal/reminder-runner.d.ts.map +1 -1
  119. package/dist/internal/reminder-runner.js +112 -61
  120. package/dist/internal/reminder-store.d.ts +5 -4
  121. package/dist/internal/reminder-store.d.ts.map +1 -1
  122. package/dist/internal/reminder-store.js +5 -4
  123. package/dist/internal/sdk-runner.d.ts.map +1 -1
  124. package/dist/internal/sdk-runner.js +44 -1
  125. package/dist/internal/server.d.ts.map +1 -1
  126. package/dist/internal/server.js +115 -16
  127. package/dist/internal/session-engine.d.ts +22 -1
  128. package/dist/internal/session-engine.d.ts.map +1 -1
  129. package/dist/internal/session-engine.js +110 -4
  130. package/dist/internal/storage-coordinator.d.ts +4 -15
  131. package/dist/internal/storage-coordinator.d.ts.map +1 -1
  132. package/dist/internal/storage-coordinator.js +4 -30
  133. package/dist/playground/assets/index-CquRB-l0.js +85 -0
  134. package/dist/playground/assets/{index-Bul1WfrB.css → index-Dj0bWpkn.css} +1 -1
  135. package/dist/playground/index.html +2 -2
  136. package/dist/storage.d.ts +0 -3
  137. package/dist/storage.d.ts.map +1 -1
  138. package/dist/storage.js +0 -3
  139. package/dist/types.d.ts +11 -2
  140. package/dist/types.d.ts.map +1 -1
  141. package/docs/README.md +1 -0
  142. package/docs/guides/slack.md +32 -7
  143. package/docs/guides/webhooks.md +183 -0
  144. package/docs/reference/channels.md +2 -1
  145. package/package.json +1 -1
  146. package/skills/setup-slack/SKILL.md +3 -0
  147. package/src/bin/agent-serve.ts +33 -5
  148. package/src/channels/slack/auth.ts +29 -22
  149. package/src/channels/slack/cursor-account.ts +2 -0
  150. package/src/channels/slack/defaults.ts +24 -4
  151. package/src/channels/slack/dispatch.ts +42 -1
  152. package/src/channels/slack/external-policy.ts +62 -0
  153. package/src/channels/slack/index.ts +1 -0
  154. package/src/channels/slack/interactive.ts +42 -10
  155. package/src/channels/slack/slack-channel.ts +66 -3
  156. package/src/channels/slack/types.ts +12 -0
  157. package/src/internal/cli-ax.ts +110 -34
  158. package/src/internal/cloud-turn-cost.ts +158 -0
  159. package/src/internal/cursor-slack-relay.ts +3 -0
  160. package/src/internal/deploy-client.ts +3 -0
  161. package/src/internal/discovery.ts +9 -1
  162. package/src/internal/event-log.ts +17 -0
  163. package/src/internal/host-platforms.ts +40 -18
  164. package/src/internal/http-channel.ts +3 -1
  165. package/src/internal/local-control-plane.ts +188 -0
  166. package/src/internal/platform-schedule-sync.ts +81 -0
  167. package/src/internal/platform-timers.ts +173 -0
  168. package/src/internal/reminder-runner.ts +131 -67
  169. package/src/internal/reminder-store.ts +5 -4
  170. package/src/internal/sdk-runner.ts +49 -1
  171. package/src/internal/server.ts +130 -11
  172. package/src/internal/session-engine.ts +138 -3
  173. package/src/internal/storage-coordinator.ts +4 -41
  174. package/src/storage.ts +0 -5
  175. package/src/types.ts +11 -2
  176. package/dist/docs/assets/chunks/@localSearchIndexroot.CQJytDwU.js +0 -1
  177. package/dist/docs/assets/guides_slack.md.D_xFKlnF.lean.js +0 -1
  178. package/dist/docs/assets/guides_webhooks.md.B1EswtUu.js +0 -50
  179. package/dist/playground/assets/index-BDCeW4uc.js +0 -86
  180. /package/dist/docs/assets/{index.md.Dfv5ic9t.lean.js → index.md.CH_s5uZe.lean.js} +0 -0
@@ -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]
@@ -299,6 +300,9 @@ Options:
299
300
  connections (defineConnection({ agent })) on cloud-runtime
300
301
  turns; local turns always use loopback.
301
302
  --no-schedules Disable the cron runner outside dev mode.
303
+ --no-control-plane serve: do not start the bundled control plane (schedule
304
+ and reminder clocks). Cursor hosting passes this so the
305
+ kit uses the Cursor backend instead.
302
306
  --team deploy/deployments/deployment/stop/rotate-token and
303
307
  chat/resume/run/call/eval/logs/sessions/session/playground
304
308
  --prod: Cursor team id. Defaults to the account's team when
@@ -392,9 +396,25 @@ Options:
392
396
  --comment github replay: comment body for comment events.
393
397
  --context github replay: commit-status context.
394
398
  --dry-run github replay: print synthesized payloads, don't POST.
395
- --out github replay: write synthesized payloads as fixtures.
399
+ --out eval / eval status <runId>: also write the full results
400
+ JSON to this path. session: write the raw NDJSON trace
401
+ there instead of printing (same format as run --events;
402
+ playground-openable). github replay: write synthesized
403
+ payloads as fixtures.
396
404
  `;
397
405
 
406
+ function writeStdout(text: string): Promise<void> {
407
+ return new Promise((resolve, reject) => {
408
+ process.stdout.write(text, (error) => {
409
+ if (error) {
410
+ reject(error);
411
+ return;
412
+ }
413
+ resolve();
414
+ });
415
+ });
416
+ }
417
+
398
418
  async function main(argv: string[]): Promise<number> {
399
419
  const [command, ...restIn] = argv;
400
420
  if (
@@ -403,11 +423,13 @@ async function main(argv: string[]): Promise<number> {
403
423
  command === "--help" ||
404
424
  command === "-h"
405
425
  ) {
406
- process.stdout.write(HELP);
426
+ // Await the write: HELP is larger than a pipe buffer, and process.exit
427
+ // after main() would otherwise truncate --help under execFile / CI.
428
+ await writeStdout(HELP);
407
429
  return command === undefined ? 1 : 0;
408
430
  }
409
431
  if (command === "version" || command === "--version" || command === "-V") {
410
- process.stdout.write(`${CLI} ${packageVersion()}\n`);
432
+ await writeStdout(`${CLI} ${packageVersion()}\n`);
411
433
  return 0;
412
434
  }
413
435
 
@@ -436,6 +458,7 @@ async function main(argv: string[]): Promise<number> {
436
458
  "allow-anonymous-cursor-account-mcp": { type: "boolean", default: false },
437
459
  "cursor-github-proxy": { type: "boolean", default: false },
438
460
  "no-schedules": { type: "boolean", default: false },
461
+ "no-control-plane": { type: "boolean", default: false },
439
462
  "no-playground": { type: "boolean", default: false },
440
463
  "no-docs": { type: "boolean", default: false },
441
464
  url: { type: "string" },
@@ -775,6 +798,7 @@ async function main(argv: string[]): Promise<number> {
775
798
  apiKey: values["api-key"],
776
799
  slug: values.slug,
777
800
  bearerToken: values["bearer-token"],
801
+ out: values.out,
778
802
  })
779
803
  );
780
804
  }
@@ -819,6 +843,7 @@ async function main(argv: string[]): Promise<number> {
819
843
  artifacts: values["no-artifacts"] ? false : values.artifacts,
820
844
  skipReport: values["skip-report"],
821
845
  maxConcurrency: parseMaxConcurrency(values["max-concurrency"]),
846
+ out: values.out,
822
847
  })
823
848
  );
824
849
  }
@@ -912,6 +937,7 @@ async function main(argv: string[]): Promise<number> {
912
937
  json: values.json,
913
938
  text: values.text,
914
939
  rawEvents: values["raw-events"],
940
+ out: values.out,
915
941
  })
916
942
  );
917
943
  }
@@ -1140,6 +1166,7 @@ async function main(argv: string[]): Promise<number> {
1140
1166
  cursorGithubProxy:
1141
1167
  values["cursor-github-proxy"] === true ? true : undefined,
1142
1168
  schedules: values["no-schedules"] === true ? false : undefined,
1169
+ noControlPlane: values["no-control-plane"] === true ? true : undefined,
1143
1170
  playground: values["no-playground"] === true ? false : undefined,
1144
1171
  docs: values["no-docs"] === true ? false : undefined,
1145
1172
  publicUrl: values["public-url"],
@@ -1291,6 +1318,7 @@ function axFromOptions(
1291
1318
  artifacts: partial.artifacts,
1292
1319
  skipReport: partial.skipReport,
1293
1320
  maxConcurrency: partial.maxConcurrency,
1321
+ out: partial.out,
1294
1322
  };
1295
1323
  }
1296
1324
 
@@ -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 { SlackBinding, SlackBlockAction, SlackMessage } from "./types.js";
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
- }): AuthContext {
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 principalId = `slack:${team}:${args.channelId}:${args.threadTs}`;
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
- ...(message.teamId === undefined ? {} : { teamId: message.teamId }),
127
+ teamId: message.teamId,
118
128
  userId: message.author.userId,
119
- ...(message.author.userName === undefined
120
- ? {}
121
- : { userName: message.author.userName }),
129
+ userName: message.author.userName,
122
130
  isBot: message.author.isBot,
123
- ...(options?.engagement === undefined
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 thread-scoped principal as mentions, tagged with
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
- ...(message.teamId === undefined ? {} : { teamId: message.teamId }),
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
+ }
@@ -33,6 +33,7 @@ export * from "./dispatch.js";
33
33
  export * from "./doctor.js";
34
34
  export * from "./eval-directive.js";
35
35
  export * from "./event-id-dedupe.js";
36
+ export * from "./external-policy.js";
36
37
  export * from "./inbound.js";
37
38
  export * from "./init.js";
38
39
  export * from "./interactive.js";
@@ -20,6 +20,7 @@ import { updateMessage } from "./api.js";
20
20
  import { decisionFromActionId, parseApprovalActionValue } from "./approvals.js";
21
21
  import { buildSlackInteractiveApprovalAuth } from "./auth.js";
22
22
  import { dispatchInboundMessage } from "./dispatch.js";
23
+ import { isExternalSlackSender } from "./external-policy.js";
23
24
  import { defaultSlackLog, type SlackLog } from "./log.js";
24
25
  import type {
25
26
  SlackBlockAction,
@@ -41,6 +42,8 @@ export interface SlackInteractiveDispatchDeps {
41
42
  ) => Promise<PendingApproval>;
42
43
  waitUntil: (promise: Promise<unknown>) => void;
43
44
  log?: SlackLog;
45
+ /** Defaults to true. */
46
+ blockExternals?: boolean;
44
47
  }
45
48
 
46
49
  /**
@@ -116,11 +119,35 @@ export async function dispatchInteractiveFromPayload(
116
119
  }
117
120
 
118
121
  const teamId = attrs.team_id;
122
+ if (deps.blockExternals !== false && parsed.userId !== "") {
123
+ if (teamId === undefined || teamId === "") {
124
+ log.info("interactive approval dropped (missing team)", {
125
+ session: value.sessionId,
126
+ user: parsed.userId,
127
+ });
128
+ return true;
129
+ }
130
+ if (
131
+ await isExternalSlackSender({
132
+ botToken: deps.credentials?.botToken,
133
+ slackTeamId: teamId,
134
+ userId: parsed.userId,
135
+ })
136
+ ) {
137
+ log.info("interactive approval dropped (external sender)", {
138
+ session: value.sessionId,
139
+ user: parsed.userId,
140
+ team: teamId,
141
+ });
142
+ return true;
143
+ }
144
+ }
145
+
119
146
  const clickingAuth = buildSlackInteractiveApprovalAuth({
120
147
  sessionAuth,
121
148
  channelId: parsed.channelId || sessionChannel,
122
149
  threadTs: parsed.threadTs || sessionThread,
123
- ...(teamId === undefined || teamId === "" ? {} : { teamId }),
150
+ teamId: teamId === undefined || teamId === "" ? undefined : teamId,
124
151
  clickingUserId: parsed.userId,
125
152
  });
126
153
 
@@ -204,12 +231,9 @@ export function parseSlackBlockAction(
204
231
  : typeof user.name === "string"
205
232
  ? user.name
206
233
  : undefined;
207
- const teamId =
208
- typeof team.id === "string"
209
- ? team.id
210
- : typeof user.team_id === "string"
211
- ? user.team_id
212
- : undefined;
234
+ // Payload `team.id` only — never `user.team_id` (Connect home team).
235
+ // Membership gates must baseline the install workspace separately.
236
+ const teamId = typeof team.id === "string" ? team.id : undefined;
213
237
  const label =
214
238
  typeof action.text === "object" &&
215
239
  action.text !== null &&
@@ -278,6 +302,10 @@ export interface SlackBlockActionDispatchDeps extends SlackContinuationProbes {
278
302
  host: HostContext;
279
303
  artifacts: ArtifactsApi;
280
304
  log?: SlackLog;
305
+ /** Defaults to true. */
306
+ blockExternals?: boolean;
307
+ /** Install workspace from `auth.test`; baseline for the external gate. */
308
+ installTeamId?: string;
281
309
  }
282
310
 
283
311
  /**
@@ -285,7 +313,10 @@ export interface SlackBlockActionDispatchDeps extends SlackContinuationProbes {
285
313
  * `action_ts` (unique per click, never a real thread message), so ask-dedupe
286
314
  * claims and `<slack_thread>` exclusions stay per-click.
287
315
  */
288
- function blockActionAsMessage(action: SlackBlockAction): SlackMessage {
316
+ function blockActionAsMessage(
317
+ action: SlackBlockAction,
318
+ installTeamId: string | undefined
319
+ ): SlackMessage {
289
320
  const text = `[clicked: ${action.label ?? action.actionId}]`;
290
321
  return {
291
322
  text,
@@ -294,7 +325,7 @@ function blockActionAsMessage(action: SlackBlockAction): SlackMessage {
294
325
  ts: action.actionTs,
295
326
  threadTs: action.threadTs,
296
327
  channelId: action.channelId,
297
- teamId: action.teamId,
328
+ teamId: installTeamId,
298
329
  author: {
299
330
  userId: action.userId,
300
331
  userName: action.userName,
@@ -344,7 +375,7 @@ export async function dispatchBlockAction(
344
375
 
345
376
  await dispatchInboundMessage({
346
377
  kind: "block_action",
347
- message: blockActionAsMessage(action),
378
+ message: blockActionAsMessage(action, deps.installTeamId),
348
379
  handler: (ctx) => deps.handler(ctx, action),
349
380
  send: deps.send,
350
381
  credentials: deps.credentials,
@@ -353,6 +384,7 @@ export async function dispatchBlockAction(
353
384
  log,
354
385
  // Click text is synthetic — never treat it as a stop/eval directive.
355
386
  evalCommandsEnabled: false,
387
+ blockExternals: deps.blockExternals,
356
388
  hasContinuationSession: deps.hasContinuationSession,
357
389
  isContinuationBusy: deps.isContinuationBusy,
358
390
  getContinuationLastBotMessageTs: deps.getContinuationLastBotMessageTs,